{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK hide, not-home #-}
module Network.Http.ResponseParser (
readResponseHeader,
readResponseBody,
UnexpectedCompression(..),
readDecimal
) where
import Prelude hiding (take, takeWhile)
import Control.Exception (Exception, throwIO)
import Control.Monad (void)
import Control.Monad.IO.Class (liftIO)
import Data.Attoparsec.ByteString.Char8
import Data.Bits (Bits (..))
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S
import Data.CaseInsensitive (mk)
import Data.Char (ord)
import Data.Int (Int64)
import Data.Typeable (Typeable)
import System.IO.Streams (Generator, InputStream)
import qualified System.IO.Streams as Streams
import qualified System.IO.Streams.Attoparsec as Streams
import Control.Applicative as App
import Network.Http.Internal
import Network.Http.Utilities
#if defined(MIN_VERSION_brotli_streams)
import qualified System.IO.Streams.Brotli as Brotli
brotliDecompress :: InputStream ByteString -> IO (InputStream ByteString)
brotliDecompress = Brotli.decompress
#else
brotliDecompress :: InputStream ByteString -> IO (InputStream ByteString)
brotliDecompress _ = throwIO (UnexpectedCompression "br")
#endif
__BITE_SIZE__ :: Int
__BITE_SIZE__ = 32 * 1024
readResponseHeader :: InputStream ByteString -> IO Response
readResponseHeader i = do
(sc,sm) <- Streams.parseFromStream parseStatusLine i
hs <- readHeaderFields i
let h = buildHeaders hs
let te = case lookupHeader h "Transfer-Encoding" of
Just x' -> if mk x' == "chunked"
then Chunked
else None
Nothing -> None
let ce = case lookupHeader h "Content-Encoding" of
Just x' -> case mk x' of
"gzip" -> Gzip
"br" -> Br
"deflate" -> Deflate
"identity" -> Identity
_ -> UnknownCE x'
Nothing -> Identity
let nm = case lookupHeader h "Content-Length" of
Just x' -> Just (readDecimal x' :: Int64)
Nothing -> case sc of
204 -> Just 0
304 -> Just 0
100 -> Just 0
_ -> Nothing
return Response {
pStatusCode = sc,
pStatusMsg = sm,
pTransferEncoding = te,
pContentEncoding = ce,
pContentLength = nm,
pHeaders = h
}
parseStatusLine :: Parser (StatusCode,ByteString)
parseStatusLine = do
sc <- string "HTTP/1." *> satisfy version *> char ' ' *> decimal <* char ' '
sm <- takeTill (== '\r') <* crlf
return (sc,sm)
where
version c = c == '1' || c == '0'
crlf :: Parser ByteString
crlf = string "\r\n"
readResponseBody :: Response -> InputStream ByteString -> IO (InputStream ByteString)
readResponseBody p i1 = do
i2 <- case t of
None -> case l of
Just n -> readFixedLengthBody i1 n
Nothing -> readUnlimitedBody i1
Chunked -> readChunkedBody i1
i3 <- case c of
Identity -> App.pure i2
Gzip -> Streams.gunzip i2
Br -> brotliDecompress i2
Deflate -> throwIO (UnexpectedCompression "deflate")
UnknownCE x -> throwIO (UnexpectedCompression (S.unpack x))
return i3
where
t = pTransferEncoding p
c = pContentEncoding p
l = pContentLength p
readDecimal :: (Enum α, Num α, Bits α) => ByteString -> α
readDecimal str' =
S.foldl' f 0 x'
where
f !cnt !i = cnt * 10 + digitToInt i
x' = head $ S.words str'
{-# INLINE digitToInt #-}
digitToInt :: (Enum α, Num α, Bits α) => Char -> α
digitToInt c | c >= '0' && c <= '9' = toEnum $! ord c - ord '0'
| otherwise = error $ "'" ++ [c] ++ "' is not an ascii digit"
{-# INLINE readDecimal #-}
data UnexpectedCompression = UnexpectedCompression String
deriving (Typeable, Show)
instance Exception UnexpectedCompression
readChunkedBody :: InputStream ByteString -> IO (InputStream ByteString)
readChunkedBody i1 = do
i2 <- Streams.fromGenerator (consumeChunks i1)
return i2
consumeChunks :: InputStream ByteString -> Generator ByteString ()
consumeChunks i1 = do
!n <- parseSize
if n > 0
then do
go n
skipCRLF
consumeChunks i1
else do
skipEnd
where
go 0 = return ()
go !n = do
(!x',!r) <- liftIO $ readN n i1
Streams.yield x'
go r
parseSize = do
n <- liftIO $ Streams.parseFromStream transferChunkSize i1
return n
skipEnd = do
liftIO $ do
_ <- readHeaderFields i1
return ()
skipCRLF = do
liftIO $ do
_ <- Streams.parseFromStream crlf i1
return ()
readN :: Int -> InputStream ByteString -> IO (ByteString, Int)
readN n i1 = do
!x' <- Streams.readExactly p i1
return (x', r)
where
!d = n - size
!p = if d > 0
then size
else n
!r = if d > 0
then d
else 0
size = __BITE_SIZE__
transferChunkSize :: Parser (Int)
transferChunkSize = do
!n <- hexadecimal
void (takeTill (== '\r'))
void crlf
return n
readFixedLengthBody :: InputStream ByteString -> Int64 -> IO (InputStream ByteString)
readFixedLengthBody i1 n = do
i2 <- Streams.takeBytes n i1
return i2
readUnlimitedBody :: InputStream ByteString -> IO (InputStream ByteString)
readUnlimitedBody i1 = do
return i1