packages feed

warp-3.4.17: Network/Wai/Handler/Warp/RequestHeader.hs

{-# LANGUAGE BangPatterns #-}

module Network.Wai.Handler.Warp.RequestHeader (
    parseHeaderLines,
) where

import Control.Exception (throwIO)
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as C8 (unpack)
import Data.ByteString.Internal (memchr)
import qualified Data.CaseInsensitive as CI
import Data.Word8
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Ptr (Ptr, minusPtr, nullPtr, plusPtr)
import Foreign.Storable (peek)
import qualified Network.HTTP.Types as H

import Network.Wai.Handler.Warp.Imports
import Network.Wai.Handler.Warp.Types

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

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

parseHeaderLines
    :: [ByteString]
    -> IO
        ( H.Method
        , ByteString --  Path
        , ByteString --  Path, parsed
        , ByteString --  Query
        , H.HttpVersion
        , H.RequestHeaders
        )
parseHeaderLines [] = throwIO $ NotEnoughLines []
parseHeaderLines (firstLine : otherLines) = do
    (method, path', query, httpversion) <- parseRequestLine firstLine
    let path = H.extractPath path'
    hdr <- mapM parseHeader otherLines
    return (method, path', path, query, httpversion, hdr)

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

-- |
--
-- >>> parseRequestLine "GET / HTTP/1.1"
-- ("GET","/","",HTTP/1.1)
-- >>> parseRequestLine "POST /cgi/search.cgi?key=foo HTTP/1.0"
-- ("POST","/cgi/search.cgi","?key=foo",HTTP/1.0)
-- >>> parseRequestLine "GET "
-- *** Exception: Warp: Invalid first line of request: "GET "
-- >>> parseRequestLine "GET /NotHTTP UNKNOWN/1.1"
-- *** Exception: Warp: Request line specified a non-HTTP request
-- >>> parseRequestLine "PRI * HTTP/2.0"
-- ("PRI","*","",HTTP/2.0)
parseRequestLine
    :: ByteString
    -> IO
        ( H.Method
        , ByteString -- Path
        , ByteString -- Query
        , H.HttpVersion
        )
parseRequestLine requestLine@(PS fptr off len) = withForeignPtr fptr $ \ptr -> do
    when (len < 14) $ throwIO baderr
    let methodptr = ptr `plusPtr` off
        limptr = methodptr `plusPtr` len
        lim0 = fromIntegral len

    pathptr0 <- memchr methodptr _space lim0
    when (pathptr0 == nullPtr || (limptr `minusPtr` pathptr0) < 11) $
        throwIO baderr
    let pathptr = pathptr0 `plusPtr` 1
        lim1 = fromIntegral (limptr `minusPtr` pathptr0)

    httpptr0 <- memchr pathptr _space lim1
    when (httpptr0 == nullPtr || (limptr `minusPtr` httpptr0) < 9) $
        throwIO baderr
    let httpptr = httpptr0 `plusPtr` 1
        lim2 = fromIntegral (httpptr0 `minusPtr` pathptr)

    checkHTTP httpptr
    !hv <- httpVersion httpptr
    queryptr <- memchr pathptr _question lim2

    let !method = bs ptr methodptr pathptr0
        !path
            | queryptr == nullPtr = bs ptr pathptr httpptr0
            | otherwise = bs ptr pathptr queryptr
        !query
            | queryptr == nullPtr = S.empty
            | otherwise = bs ptr queryptr httpptr0

    return (method, path, query, hv)
  where
    baderr = BadFirstLine $ C8.unpack requestLine
    check :: Ptr Word8 -> Int -> Word8 -> IO ()
    check p n w = do
        w0 <- peek $ p `plusPtr` n
        when (w0 /= w) $ throwIO NonHttp
    checkHTTP httpptr = do
        check httpptr 0 _H
        check httpptr 1 _T
        check httpptr 2 _T
        check httpptr 3 _P
        check httpptr 4 _slash
        check httpptr 6 _period
    httpVersion httpptr = do
        major <- peek (httpptr `plusPtr` 5) :: IO Word8
        minor <- peek (httpptr `plusPtr` 7) :: IO Word8
        let version
                | major == _1 = if minor == _1 then H.http11 else H.http10
                | major == _2 && minor == _0 = H.http20
                | otherwise = H.http10
        return version
    bs ptr p0 p1 = PS fptr o l
      where
        o = p0 `minusPtr` ptr
        l = p1 `minusPtr` p0

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

-- |
--
-- >>> parseHeader "Content-Length:47"
-- ("Content-Length","47")
-- >>> parseHeader "Accept-Ranges: bytes"
-- ("Accept-Ranges","bytes")
-- >>> parseHeader "Host:  example.com:8080"
-- ("Host","example.com:8080")
-- >>> parseHeader "NoSemiColon"
-- ("NoSemiColon","")
-- >>> parseHeader "Content-Length : 47"
-- *** Exception: Warp: Invalid header field name: "Content-Length "
-- >>> parseHeader "X-Foo: a\rContent-Length: 47"
-- *** Exception: Warp: CR or NUL in a header field line
parseHeader :: ByteString -> IO H.Header
parseHeader s = do
    -- The line was ended by an LF, and the CR before it is already
    -- gone, so any CR left here is a bare one. RFC 9110 section 5.5
    -- asks for it to be refused or replaced: a recipient which takes a
    -- bare CR for a line terminator reads a field, and a framing field
    -- among them, where warp reads only data.
    when (hasControl s) $ throwIO BadHeaderLine
    -- RFC 9112 asks for 400 here, because a recipient which instead
    -- repairs the name reads a header where warp reads none: a
    -- Content-Length hidden that way is a request body warp does not
    -- know it is being sent.
    when (S.null k || not (S.all isTchar k)) $ throwIO $ BadFieldName k
    return (CI.mk k, rest')
  where
    (k, rest) = S.break (== _colon) s
    rest' = S.dropWhile isOWS $ S.drop 1 rest
    isOWS c = c == _space || c == _tab
    -- Two passes of memchr beat one pass of a lambda here by a wide
    -- margin, and 'S.elem' is the way to ask for memchr: measured over
    -- a browser's request headers, this takes a fifth of the time that
    -- @S.any (_cr ==)@ does and a fifteenth of the time that one
    -- @S.any@ with a two-way lambda does.
    hasControl bs = S.elem _cr bs || S.elem _nul bs

-- | The characters RFC 9110 allows in a header field name.
isTchar :: Word8 -> Bool
isTchar w =
    isAsciiUpper w
        || isAsciiLower w
        || isDigit w
        || w
            `elem` [ _exclam
                   , _numbersign
                   , _dollar
                   , _percent
                   , _ampersand
                   , _quotesingle
                   , _asterisk
                   , _plus
                   , _hyphen
                   , _period
                   , _circum
                   , _underscore
                   , _grave
                   , _bar
                   , _tilde
                   ]