{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}

module RequestSpec (main, spec) where

import Network.Wai.Handler.Warp.File (parseByteRanges)
import Network.Wai.Handler.Warp.Request
import Network.Wai.Handler.Warp.Types
import Network.Wai.Handler.Warp.Settings (settingsMaxTotalHeaderLength, defaultSettings)
import Test.Hspec
import Test.Hspec.QuickCheck
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import qualified Network.HTTP.Types.Header as HH
import Data.IORef

main :: IO ()
main = hspec spec

defaultMaxTotalHeaderLength :: Int
defaultMaxTotalHeaderLength = settingsMaxTotalHeaderLength defaultSettings

spec :: Spec
spec = do
  describe "headerLines" $ do
    it "takes until blank" $
        blankSafe >>= (`shouldBe` ("", ["foo", "bar", "baz"]))
    it "ignored leading whitespace in bodies" $
        whiteSafe >>= (`shouldBe` (" 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
      it "can handle a normal case" $ do
          src <- mkSourceFunc ["Status: 200\r\nContent-Type: text/plain\r\n\r\n"] >>= mkSource
          x <- headerLines defaultMaxTotalHeaderLength True src
          x `shouldBe` ["Status: 200", "Content-Type: text/plain"]

      it "can handle a nasty case (1)" $ do
          src <- mkSourceFunc ["Status: 200", "\r\nContent-Type: text/plain", "\r\n\r\n"] >>= mkSource
          x <- headerLines defaultMaxTotalHeaderLength True src
          x `shouldBe` ["Status: 200", "Content-Type: text/plain"]

      it "can handle a nasty case (1)" $ do
          src <- mkSourceFunc ["Status: 200", "\r", "\nContent-Type: text/plain", "\r", "\n\r\n"] >>= mkSource
          x <- headerLines defaultMaxTotalHeaderLength True src
          x `shouldBe` ["Status: 200", "Content-Type: text/plain"]

      it "can handle a nasty case (1)" $ do
          src <- mkSourceFunc ["Status: 200", "\r", "\n", "Content-Type: text/plain", "\r", "\n", "\r", "\n"] >>= mkSource
          x <- headerLines defaultMaxTotalHeaderLength True src
          x `shouldBe` ["Status: 200", "Content-Type: text/plain"]

      it "can handle an illegal case (1)" $ do
          src <- mkSourceFunc ["\nStatus:", "\n 200", "\nContent-Type: text/plain", "\r\n\r\n"] >>= mkSource
          x <- headerLines defaultMaxTotalHeaderLength True src
          x `shouldBe` []
          y <- headerLines defaultMaxTotalHeaderLength True src
          y `shouldBe` ["Status: 200", "Content-Type: text/plain"]

      -- Length is 39, this shouldn't fail
      let testLengthHeaders = ["Sta", "tus: 200\r", "\n", "Content-Type: ", "text/plain\r\n\r\n"]
      it "doesn't throw on correct length" $ do
          src <- mkSourceFunc testLengthHeaders >>= mkSource
          x <- headerLines 39 True 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 38 True 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 True 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
