warp-3.4.2: test/RequestSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module RequestSpec (main, spec) where
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import Data.IORef
import Data.Monoid (Sum (..))
import qualified Network.HTTP.Types.Header as HH
import Network.Wai.Handler.Warp.File (parseByteRanges)
import Network.Wai.Handler.Warp.Request
import Network.Wai.Handler.Warp.Settings (
defaultSettings,
settingsMaxTotalHeaderLength,
)
import Network.Wai.Handler.Warp.Types
import Test.Hspec
import Test.Hspec.QuickCheck
main :: IO ()
main = hspec spec
defaultMaxTotalHeaderLength :: Int
defaultMaxTotalHeaderLength = settingsMaxTotalHeaderLength defaultSettings
spec :: Spec
spec = do
describe "headerLines" $ do
it "takes until blank" $
blankSafe `shouldReturn` ("", ["foo", "bar", "baz"])
it "ignored leading whitespace in bodies" $
whiteSafe `shouldReturn` (" hi there", ["foo", "bar", "baz"])
it "throws OverLargeHeader when too many" $
tooMany `shouldThrow` overLargeHeader
it "throws OverLargeHeader when too large" $
tooLarge `shouldThrow` overLargeHeader
it "known bad chunking behavior #239" $ do
let chunks =
[ "GET / HTTP/1.1\r\nConnection: Close\r"
, "\n\r\n"
]
(actual, src) <- headerLinesList' chunks
leftover <- readLeftoverSource src
leftover `shouldBe` S.empty
actual `shouldBe` ["GET / HTTP/1.1", "Connection: Close"]
prop "random chunking" $ \breaks extraS -> do
let bsFull = "GET / HTTP/1.1\r\nConnection: Close\r\n\r\n" `S8.append` extra
extra = S8.pack extraS
chunks = loop breaks bsFull
loop [] bs = [bs, undefined]
loop (x : xs) bs =
bs1 : loop xs bs2
where
(bs1, bs2) = S8.splitAt ((x `mod` 10) + 1) bs
(actual, src) <- headerLinesList' chunks
leftover <- consumeLen (length extraS) src
actual `shouldBe` ["GET / HTTP/1.1", "Connection: Close"]
leftover `shouldBe` extra
describe "parseByteRanges" $ do
let test x y = it x $ parseByteRanges (S8.pack x) `shouldBe` y
test "bytes=0-499" $ Just [HH.ByteRangeFromTo 0 499]
test "bytes=500-999" $ Just [HH.ByteRangeFromTo 500 999]
test "bytes=-500" $ Just [HH.ByteRangeSuffix 500]
test "bytes=9500-" $ Just [HH.ByteRangeFrom 9500]
test "foobytes=9500-" Nothing
test "bytes=0-0,-1" $ Just [HH.ByteRangeFromTo 0 0, HH.ByteRangeSuffix 1]
describe "headerLines" $ do
let parseHeaderLine chunks = do
src <- mkSourceFunc chunks >>= mkSource
x <- headerLines defaultMaxTotalHeaderLength FirstRequest src
x `shouldBe` ["Status: 200", "Content-Type: text/plain"]
it "can handle a normal case" $
parseHeaderLine ["Status: 200\r\nContent-Type: text/plain\r\n\r\n"]
it "can handle a nasty case (1)" $ do
parseHeaderLine ["Status: 200", "\r\nContent-Type: text/plain", "\r\n\r\n"]
parseHeaderLine ["Status: 200\r\n", "Content-Type: text/plain", "\r\n\r\n"]
it "can handle a nasty case (2)" $ do
parseHeaderLine
["Status: 200", "\r", "\nContent-Type: text/plain", "\r", "\n\r\n"]
it "can handle a nasty case (3)" $ do
parseHeaderLine
["Status: 200", "\r", "\n", "Content-Type: text/plain", "\r", "\n", "\r", "\n"]
it "can handle a stupid case (3)" $
parseHeaderLine $
S8.pack . (: []) <$> "Status: 200\r\nContent-Type: text/plain\r\n\r\n"
it "can (not) handle an illegal case (1)" $ do
let chunks = ["\nStatus:", "\n 200", "\nContent-Type: text/plain", "\r\n\r\n"]
src <- mkSourceFunc chunks >>= mkSource
x <- headerLines defaultMaxTotalHeaderLength FirstRequest src
x `shouldBe` []
y <- headerLines defaultMaxTotalHeaderLength FirstRequest src
y `shouldBe` ["Status:", " 200", "Content-Type: text/plain"]
let testLengthHeaders = ["Sta", "tus: 200\r", "\n", "Content-Type: ", "text/plain\r\n\r\n"]
headerLength = getSum $ foldMap (Sum . S.length) testLengthHeaders
testLength = headerLength - 2 -- Because the second CRLF at the end isn't counted
-- Length is 39, this shouldn't fail
it "doesn't throw on correct length" $ do
src <- mkSourceFunc testLengthHeaders >>= mkSource
x <- headerLines testLength FirstRequest src
x `shouldBe` ["Status: 200", "Content-Type: text/plain"]
-- Length is still 39, this should fail
it "throws error on correct length too long" $ do
src <- mkSourceFunc testLengthHeaders >>= mkSource
headerLines (testLength - 1) FirstRequest src `shouldThrow` (== OverLargeHeader)
where
blankSafe = headerLinesList ["f", "oo\n", "bar\nbaz\n\r\n"]
whiteSafe = headerLinesList ["foo\r\nbar\r\nbaz\r\n\r\n hi there"]
tooMany = headerLinesList $ repeat "f\n"
tooLarge = headerLinesList $ repeat "f"
headerLinesList :: [S8.ByteString] -> IO (S8.ByteString, [S8.ByteString])
headerLinesList orig = do
(res, src) <- headerLinesList' orig
leftover <- readLeftoverSource src
return (leftover, res)
headerLinesList' :: [S8.ByteString] -> IO ([S8.ByteString], Source)
headerLinesList' orig = do
ref <- newIORef orig
let src = do
x <- readIORef ref
case x of
[] -> return S.empty
y : z -> do
writeIORef ref z
return y
src' <- mkSource src
res <- headerLines defaultMaxTotalHeaderLength FirstRequest src'
return (res, src')
consumeLen :: Int -> Source -> IO S8.ByteString
consumeLen len0 src =
loop id len0
where
loop front len
| len <= 0 = return $ S.concat $ front []
| otherwise = do
bs <- readSource src
if S.null bs
then loop front 0
else do
let (x, _) = S.splitAt len bs
loop (front . (x :)) (len - S.length x)
overLargeHeader :: Selector InvalidRequest
overLargeHeader e = e == OverLargeHeader
mkSourceFunc :: [S8.ByteString] -> IO (IO S8.ByteString)
mkSourceFunc bss = do
ref <- newIORef bss
return $ reader ref
where
reader ref = do
xss <- readIORef ref
case xss of
[] -> return S.empty
(x : xs) -> do
writeIORef ref xs
return x