packages feed

warp-3.4.15: Network/Wai/Handler/Warp/Response.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Network.Wai.Handler.Warp.Response (
    sendResponse,
    sanitizeHeaderValue, -- for testing
    containsRecoverableWhitespace, -- for benchmarking
    --  Provided here for backwards compatibility.
    warpVersion,
    hasBody,
    replaceHeader,
    addServer, -- testing
    addAltSvc,
) where

import qualified Control.Exception as E
import qualified Data.ByteString as S
import Data.ByteString.Internal (toForeignPtr, unsafeCreate)
import Data.ByteString.Builder (Builder, byteString)
import Data.ByteString.Builder.Extra (flush)
import Data.ByteString.Builder.HTTP.Chunked (
    chunkedTransferEncoding,
    chunkedTransferTerminator,
 )
import qualified Data.CaseInsensitive as CI
import Data.Foldable (for_)
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
import Data.Streaming.ByteString.Builder (
    newByteStringBuilderRecv,
    reuseBufferStrategy,
 )
import Data.Word8 (_cr, _lf, _nul, _space)
import Foreign (copyBytes, plusForeignPtr, pokeByteOff, withForeignPtr)
import qualified Network.HTTP.Types as H
import qualified Network.HTTP.Types.Header as Header
import Network.Wai
import Network.Wai.Internal
import qualified System.TimeManager as T

import Network.Wai.Handler.Warp.Buffer (toBuilderBuffer)
import qualified Network.Wai.Handler.Warp.Date as D
import Network.Wai.Handler.Warp.File
import Network.Wai.Handler.Warp.Header
import Network.Wai.Handler.Warp.IO (toBufIOWith)
import Network.Wai.Handler.Warp.Imports
import Network.Wai.Handler.Warp.ResponseHeader
import Network.Wai.Handler.Warp.Settings
import Network.Wai.Handler.Warp.Types

-- $setup
-- >>> :set -XOverloadedStrings

----------------------------------------------------------------

-- | Sending a HTTP response to 'Connection' according to 'Response'.
--
--   Applications/middlewares MUST provide a proper 'H.ResponseHeaders'.
--   so that inconsistency does not happen.
--   No header is deleted by this function.
--
--   Especially, Applications/middlewares MUST provide a proper
--   Content-Type. They MUST NOT provide
--   Content-Length, Content-Range, and Transfer-Encoding
--   because they are inserted, when necessary,
--   regardless they already exist.
--   This function does not insert Content-Encoding. It's middleware's
--   responsibility.
--
--   The Date and Server header is added if not exist
--   in HTTP response header.
--
--   There are three basic APIs to create 'Response':
--
--   ['responseBuilder' :: 'H.Status' -> 'H.ResponseHeaders' -> 'Builder' -> 'Response']
--     HTTP response body is created from 'Builder'.
--     Transfer-Encoding: chunked is used in HTTP/1.1.
--
--   ['responseStream' :: 'H.Status' -> 'H.ResponseHeaders' -> 'StreamingBody' -> 'Response']
--     HTTP response body is created from 'Builder'.
--     Transfer-Encoding: chunked is used in HTTP/1.1.
--
--   ['responseRaw' :: ('IO' 'ByteString' -> ('ByteString' -> 'IO' ()) -> 'IO' ()) -> 'Response' -> 'Response']
--     No header is added and no Transfer-Encoding: is applied.
--
--   ['responseFile' :: 'H.Status' -> 'H.ResponseHeaders' -> 'FilePath' -> 'Maybe' 'FilePart' -> 'Response']
--     HTTP response body is sent (by sendfile(), if possible) for GET method.
--     HTTP response body is not sent by HEAD method.
--     Content-Length and Content-Range are automatically
--     added into the HTTP response header if necessary.
--     If Content-Length and Content-Range exist in the HTTP response header,
--     they would cause inconsistency.
--     \"Accept-Ranges: bytes\" is also inserted.
--
--     Applications are categorized into simple and sophisticated.
--     Sophisticated applications should specify 'Just' to
--     'Maybe' 'FilePart'. They should treat the conditional request
--     by themselves. A proper 'Status' (200 or 206) must be provided.
--
--     Simple applications should specify 'Nothing' to
--     'Maybe' 'FilePart'. The size of the specified file is obtained
--     by disk access or from the file info cache.
--     If-Modified-Since, If-Unmodified-Since, If-Range and Range
--     are processed. Since a proper status is chosen, 'Status' is
--     ignored. Last-Modified is inserted.
sendResponse
    :: Settings
    -> Connection
    -> InternalInfo
    -> T.Handle
    -> Request
    -- ^ HTTP request.
    -> IndexedRequestHeader
    -- ^ Indexed header of HTTP request.
    -> IO ByteString
    -- ^ source from client, for raw response
    -> Response
    -- ^ HTTP response including status code and response header.
    -> IO Bool
    -- ^ Returing True if the connection is persistent.
sendResponse settings conn ii th req reqidxhdr src response = do
    -- Decide connection persistence
    isShuttingDown <-
        case settingsServerState settings of
            Just serverState -> currentShuttingDownState serverState
            -- Should never be reached!
            -- (cf. 'makeServerState' in 'runSettingsConnectionMakerSecure')
            Nothing -> pure False
    let shouldPersist = not isShuttingDown && ret
        addConnection hs =
            if shouldPersist || responseWantsToClose
                then hs
                else (Header.hConnection, "close") : hs

    -- Adjust headers
    hs <- addConnection . addAltSvc settings <$> addServerAndDate hs0

    -- Start response logic
    if hasBody s
        then do
            -- The response to HEAD does not have body.
            -- But to handle the conditional requests defined RFC 7232 and
            -- to generate appropriate content-length, content-range,
            -- and status, the response to HEAD is processed here.
            --
            -- See definition of rsp below for proper body stripping.
            (ms, mlen) <- sendRsp conn ii th ver s hs rspidxhdr maxRspBufSize method rsp
            case ms of
                Nothing -> return ()
                Just realStatus -> logger req realStatus mlen
        else do
            _ <- sendRsp conn ii th ver s hs rspidxhdr maxRspBufSize method RspNoBody
            logger req s Nothing
    T.tickle th
    return shouldPersist
  where
    -- From Settings --
    defServer = settingsServerName settings
    logger = settingsLogger settings
    maxRspBufSize = settingsMaxBuilderResponseBufferSize settings

    -- From Request --
    method = requestMethod req
    isHead = method == H.methodHead
    ver = httpVersion req
    isHttp11 = ver == H.http11
    reqSaysPersist = checkReqConnectionHeader isHttp11 reqidxhdr

    -- From Response --
    s = responseStatus response
    hs0 = sanitizeHeaders $ responseHeaders response
    rspidxhdr = indexResponseHeader hs0
    hasLength = hasContentLength rspidxhdr
    responseWantsToClose =
        case hasConnection rspidxhdr of
            Nothing -> False
            Just v -> CI.foldCase v == "close"
    isPersist = reqSaysPersist && not responseWantsToClose

    -- Other --
    getdate = getDate ii
    addServerAndDate = addDate getdate rspidxhdr . addServer defServer rspidxhdr
    needsChunked = isHttp11 && not hasLength
    rsp = case response of
        ResponseFile _ _ path mPart -> RspFile path mPart reqidxhdr (T.tickle th)
        ResponseBuilder _ _ b
            | isHead -> RspNoBody
            | otherwise -> RspBuilder b needsChunked
        ResponseStream _ _ fb
            | isHead -> RspNoBody
            | otherwise -> RspStream fb needsChunked
        ResponseRaw raw _ -> RspRaw raw src
    -- Should be False if (http10 && not hasLength), regardless of what
    -- the 'Connection' header says. (as long as the response should have a body)
    isKeepAlive =
        isPersist && (isHttp11 || hasLength || isHead || not (hasBody s))
    -- Make sure we don't hang on to 'response' (avoid space leak)
    !ret = case response of
        -- Will get 'Content-Length' header later on using the
        -- 'addContentHeaders(ForFilePart)' functions, so if the
        -- 'Connection' header says we persist, we persist.
        ResponseFile{} -> isPersist
        ResponseBuilder{} -> isKeepAlive
        ResponseStream{} -> isKeepAlive
        -- Is already an ongoing open connection, so if it is done,
        -- the connection should be closed.
        ResponseRaw{} -> False

----------------------------------------------------------------

-- | As per RFC 9110 we replace any newlines (\r\n) or \NUL with spaces
-- Values without CR/LF/NUL (the overwhelmingly common case) leave the
-- header list untouched; only a dirty value triggers a rebuild.
sanitizeHeaders :: H.ResponseHeaders -> H.ResponseHeaders
sanitizeHeaders hdrs
      -- slow path
    | any (containsRecoverableWhitespace . snd) hdrs = map (sanitize <$>) hdrs
      -- fast path
    | otherwise = hdrs
  where
    sanitize bs
        | containsRecoverableWhitespace bs = sanitizeHeaderValue bs
        | otherwise = bs

-- Yes, this is quicker than @S.any (\w -> w == _lf || w == _cr || w == _nul)@
-- because of `bytestring`'s rewrite rules.
containsRecoverableWhitespace :: ByteString -> Bool
containsRecoverableWhitespace bs =
    S.any (_lf ==) bs || S.any (_cr ==) bs || S.any (_nul ==) bs

{-# INLINE isRecoverableWhitespace #-}
-- | CR, LF and NUL can safely be replaced with a SP according to RFC 9110
-- <https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5>
isRecoverableWhitespace :: Word8 -> Bool
isRecoverableWhitespace w = w == _cr || w == _lf || w == _nul

sanitizeHeaderValue :: ByteString -> ByteString
sanitizeHeaderValue v =
    case S.findIndices isRecoverableWhitespace v of
        -- Nothing to replace
        [] -> v
        -- Found CR, LF or NUL.
        ixs ->
            unsafeCreate len $ \dst -> do
                withForeignPtr fptr $ \src -> do
                    -- copy the bytestring
                    copyBytes dst src len
                    -- and then replace the offending bytes
                    for_ ixs $ \ix -> pokeByteOff dst ix _space
  where
    (fptr', offset, len) = toForeignPtr v
    -- We need to use the offset for backwards compatibility with
    -- "bytestring < 0.11"
    fptr = fptr' `plusForeignPtr` offset

----------------------------------------------------------------

data Rsp
    = RspNoBody
    | RspFile FilePath (Maybe FilePart) IndexedRequestHeader (IO ())
    | RspBuilder Builder Bool
    | RspStream StreamingBody Bool
    | RspRaw (IO ByteString -> (ByteString -> IO ()) -> IO ()) (IO ByteString)

----------------------------------------------------------------

sendRsp
    :: Connection
    -> InternalInfo
    -> T.Handle
    -> H.HttpVersion
    -> H.Status
    -> H.ResponseHeaders
    -> ResponseHeaderPresence
    -> Int -- maxBuilderResponseBufferSize
    -> H.Method
    -> Rsp
    -> IO (Maybe H.Status, Maybe Integer)
----------------------------------------------------------------

sendRsp conn _ _ ver s hs _ _ _ RspNoBody = do
    -- Not adding Content-Length.
    -- User agents treats it as Content-Length: 0.
    composeHeader ver s hs >>= connSendAll conn
    return (Just s, Just 0)

----------------------------------------------------------------

sendRsp conn _ th ver s hs rspidxhdr maxRspBufSize _ (RspBuilder body needsChunked) = do
    (header, hdrLen) <- composeHeaderBuilder ver s hs rspidxhdr needsChunked
    let hdrBdy
            | needsChunked =
                header
                    <> chunkedTransferEncoding body
                    <> chunkedTransferTerminator
            | otherwise = header <> body
        writeBufferRef = connWriteBuffer conn
    len <-
        toBufIOWith
            maxRspBufSize
            writeBufferRef
            (\bs -> connSendAll conn bs >> T.tickle th)
            hdrBdy
    --              small adjustment to only count the body
    return (Just s, Just $ len - fromIntegral hdrLen)

----------------------------------------------------------------

sendRsp conn _ th ver s hs rspidxhdr _ _ (RspStream streamingBody needsChunked) = do
    (header, hdrLen) <- composeHeaderBuilder ver s hs rspidxhdr needsChunked
    (recv, finish) <-
        newByteStringBuilderRecv $
            reuseBufferStrategy $
                toBuilderBuffer $
                    connWriteBuffer conn
    -- We'll be counting how many bytes we send with this 'IORef'
    sizeCounter <- newIORef (0 :: Integer)
    let sendFragmentAndCount bs = do
            sendFragment conn th bs
            -- add amount of bytes to count
            S.length bs `addToCounter` sizeCounter
    let send builder = do
            popper <- recv builder
            let loop = do
                    bs <- popper
                    unless (S.null bs) $ do
                        sendFragmentAndCount bs
                        loop
            loop
        sendChunk
            | needsChunked = send . chunkedTransferEncoding
            | otherwise = send
    send header
    streamingBody sendChunk (sendChunk flush)
    when needsChunked $ send chunkedTransferTerminator
    -- final flush
    finish >>= mapM_ sendFragmentAndCount
    finalSize <- readIORef sizeCounter
    --              small adjustment to only count the body
    return (Just s, Just $ finalSize - fromIntegral hdrLen)
  where
    addToCounter :: Int -> IORef Integer -> IO ()
    addToCounter bytes ref =
        atomicModifyIORef' ref $ \old ->
            (old + fromIntegral bytes, ())

----------------------------------------------------------------

sendRsp conn _ th _ _ _ _ _ _ (RspRaw withApp src) = do
    withApp recv send
    return (Nothing, Nothing)
  where
    recv = do
        bs <- src
        unless (S.null bs) $ T.tickle th
        return bs
    send bs = connSendAll conn bs >> T.tickle th

----------------------------------------------------------------

-- Sophisticated WAI applications.
-- We respect s0. s0 MUST be a proper value.
sendRsp conn ii th ver s0 hs0 rspidxhdr maxRspBufSize method (RspFile path (Just part) _ hook) =
    sendRspFile2XX
        conn
        ii
        th
        ver
        s0
        hs
        rspidxhdr
        maxRspBufSize
        method
        path
        beg
        len
        hook
  where
    beg = filePartOffset part
    len = filePartByteCount part
    hs = addContentHeadersForFilePart hs0 part

----------------------------------------------------------------

-- Simple WAI applications.
-- Status is ignored
sendRsp conn ii th ver _ hs0 rspidxhdr maxRspBufSize method (RspFile path Nothing reqidxhdr hook) = do
    efinfo <- E.try $ getFileInfo ii path
    case efinfo of
        Left (_ex :: E.IOException) ->
#ifdef WARP_DEBUG
            print _ex >>
#endif
            sendRspFile404 conn ii th ver hs0 rspidxhdr maxRspBufSize method
        Right finfo -> case conditionalRequest finfo hs0 method rspidxhdr reqidxhdr of
            WithoutBody s ->
                sendRsp conn ii th ver s hs0 rspidxhdr maxRspBufSize method RspNoBody
            WithBody s hs beg len ->
                sendRspFile2XX
                    conn
                    ii
                    th
                    ver
                    s
                    hs
                    rspidxhdr
                    maxRspBufSize
                    method
                    path
                    beg
                    len
                    hook

----------------------------------------------------------------

sendRspFile2XX
    :: Connection
    -> InternalInfo
    -> T.Handle
    -> H.HttpVersion
    -> H.Status
    -> H.ResponseHeaders
    -> ResponseHeaderPresence
    -> Int
    -> H.Method
    -> FilePath
    -> Integer
    -> Integer
    -> IO ()
    -> IO (Maybe H.Status, Maybe Integer)
sendRspFile2XX conn ii th ver s hs rspidxhdr maxRspBufSize method path beg len hook
    | method == H.methodHead =
        -- FIXME: We could check the size of the file and add a
        -- 'Content-Length' header to give the requester more information?
        sendRsp conn ii th ver s hs rspidxhdr maxRspBufSize method RspNoBody
    | otherwise = do
        lheader <- composeHeader ver s hs
        (mfd, fresher) <- getFd ii path
        let fid = FileId path mfd
            hook' = hook >> fresher
        connSendFile conn fid beg len hook' [lheader]
        return (Just s, Just len)

sendRspFile404
    :: Connection
    -> InternalInfo
    -> T.Handle
    -> H.HttpVersion
    -> H.ResponseHeaders
    -> ResponseHeaderPresence
    -> Int
    -> H.Method
    -> IO (Maybe H.Status, Maybe Integer)
sendRspFile404 conn ii th ver hs0 rspidxhdr maxRspBufSize method =
    sendRsp
        conn
        ii
        th
        ver
        s
        hs
        rspidxhdr
        maxRspBufSize
        method
        (RspBuilder body True)
  where
    s = H.notFound404
    hs = replaceHeader Header.hContentType "text/plain; charset=utf-8" hs0
    body = byteString "File not found"

----------------------------------------------------------------
----------------------------------------------------------------

-- | Use 'connSendAll' to send this data while respecting timeout rules.
sendFragment :: Connection -> T.Handle -> ByteString -> IO ()
sendFragment Connection{connSendAll = send} th bs = do
    T.resume th
    send bs
    T.pause th

-- We pause timeouts before passing control back to user code. This ensures
-- that a timeout will only ever be executed when Warp is in control. We
-- also make sure to resume the timeout after the completion of user code
-- so that we can kill idle connections.

----------------------------------------------------------------

-- | We infer from the request whether the connection should be persisted.
checkReqConnectionHeader :: Bool -> IndexedRequestHeader -> Bool
checkReqConnectionHeader isHttp11 reqidxhdr =
    case reqidxConnection reqidxhdr of
        -- If no "Connection" header, then default: HTTP/1.1 == persist
        Nothing -> isHttp11
        Just val ->
            let connValue = CI.foldCase val
             in if isHttp11
                    then connValue /= "close"
                    else connValue == "keep-alive"

----------------------------------------------------------------

-- | Only checks for status codes, NOT for methods.
--
-- This is by design and some handling relies on HEAD being a separate check.
hasBody :: H.Status -> Bool
hasBody s =
    sc /= 204
        && sc /= 304
        && sc >= 200
  where
    sc = H.statusCode s

----------------------------------------------------------------

-- | We ASSUME there's no middleware that will chunk the transfer, so
-- we'll add it to the headers if there's no other encoding, or add it
-- to the end in case it is.
-- (e.g. if a 'Middleware' were to add "Transfer-Encoding: gzip")
addTransferEncoding :: ResponseHeaderPresence -> H.ResponseHeaders -> H.ResponseHeaders
addTransferEncoding rspidxhdr =
    case hasTransferEncoding rspidxhdr of
        Just value -> replaceHeader Header.hTransferEncoding (value <> ", chunked")
        Nothing -> ((Header.hTransferEncoding, "chunked") :)

addDate
    :: IO D.GMTDate -> ResponseHeaderPresence -> H.ResponseHeaders -> IO H.ResponseHeaders
addDate getdate rspidxhdr hdrs
    | hasDate rspidxhdr = return hdrs
    | otherwise = do
        gmtdate <- getdate
        return $ (Header.hDate, gmtdate) : hdrs

----------------------------------------------------------------

{-# INLINE addServer #-}
addServer
    :: HeaderValue -> ResponseHeaderPresence -> H.ResponseHeaders -> H.ResponseHeaders
addServer serverName rspidxhdr hdrs =
    case serverName of
        -- empty string means there shouldn't be a "Server" header
        ""
            | serverPresent -> filter ((/= Header.hServer) . fst) hdrs
            | otherwise -> hdrs
        -- Anything else should set the "Server" header if it isn't already set
        _
            | not serverPresent -> (Header.hServer, serverName) : hdrs
            | otherwise -> hdrs
  where
    serverPresent = hasServer rspidxhdr

addAltSvc :: Settings -> H.ResponseHeaders -> H.ResponseHeaders
addAltSvc settings hs = case settingsAltSvc settings of
    Nothing -> hs
    Just v -> ("Alt-Svc", v) : hs

----------------------------------------------------------------

-- | Replaces a header, instead of just adding it which might lead to
-- duplicate entries of the same header name.
--
-- >>> replaceHeader "Content-Type" "new" [("content-type","old")]
-- [("Content-Type","new")]
replaceHeader
    :: H.HeaderName -> HeaderValue -> H.ResponseHeaders -> H.ResponseHeaders
replaceHeader k v hdrs = (k, v) : filter ((/= k) . fst) hdrs

----------------------------------------------------------------

composeHeaderBuilder
    :: H.HttpVersion -> H.Status -> H.ResponseHeaders -> ResponseHeaderPresence -> Bool -> IO (Builder, Int)
composeHeaderBuilder ver s hs rspidxhdr shouldChunk = do
    bs <- composeHeader ver s finalHdrs
    pure (byteString bs, S.length bs)
  where
    finalHdrs
        | shouldChunk = addTransferEncoding rspidxhdr hs
        | otherwise = hs