packages feed

twain-2.2.0.1: src/Web/Twain/Internal.hs

module Web.Twain.Internal where

import Control.Exception (Exception, handle, throwIO)
import Control.Monad (join)
import Control.Monad.Catch (throwM, try)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as JSON
import qualified Data.ByteString as B
import Data.ByteString.Builder (toLazyByteString)
import qualified Data.ByteString.Lazy as BL
import Data.Int
import Data.List as L
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.Text.Encoding
import qualified Data.Vault.Lazy as V
import Data.Word (Word64)
import Network.HTTP.Types (Method, hCookie, mkStatus, status204, status400, status413, status500)
import Network.HTTP2.Frame (ErrorCodeId (..))
import Network.Wai (Application, Middleware, Request (..), lazyRequestBody, queryString, requestHeaders, requestMethod, responseLBS)
import Network.Wai.Parse (File, ParseRequestBodyOptions, lbsBackEnd, noLimitParseRequestBodyOptions, parseRequestBodyEx)
import Network.Wai.Request (RequestSizeException (..), requestSizeCheck)
import System.IO.Unsafe (unsafePerformIO)
import Web.Cookie (SetCookie, parseCookiesText, renderSetCookie)
import Web.Twain.Types
import Network.HTTP2.Client (HTTP2Error (..), ErrorCode(..))
import qualified Data.ByteString.Char8 as BC

parsedReqKey :: V.Key ParsedRequest
parsedReqKey = unsafePerformIO V.newKey
{-# NOINLINE parsedReqKey #-}

responderOptsKey :: V.Key ResponderOptions
responderOptsKey = unsafePerformIO V.newKey
{-# NOINLINE responderOptsKey #-}

defaultResponderOpts :: ResponderOptions
defaultResponderOpts =
  ResponderOptions
    { optsMaxBodySize = 64000,
      optsParseBody = noLimitParseRequestBodyOptions
    }

getRequest :: ResponderM Request
getRequest = ResponderM $ \r -> return (Right (r, r))

setRequest :: Request -> ResponderM ()
setRequest r = ResponderM $ \_ -> return (Right ((), r))

concatParams :: ParsedRequest -> [Param]
concatParams
  ParsedRequest
    { preqBody = Just (FormBody (fps, _)),
      preqCookieParams = cps,
      preqPathParams = pps,
      preqQueryParams = qps
    } = qps <> pps <> cps <> fps
concatParams preq =
  preqQueryParams preq <> preqPathParams preq <> preqCookieParams preq

parseRequest :: Request -> ParsedRequest
parseRequest req =
  case V.lookup parsedReqKey (vault req) of
    Just preq -> preq
    Nothing ->
      ParsedRequest
        { preqPathParams = [],
          preqQueryParams = decodeQueryParam <$> queryString req,
          preqCookieParams = parseCookieParams req,
          preqBody = Nothing
        }

match :: Maybe Method -> PathPattern -> Request -> Maybe [Param]
match method (MatchPath f) req
  | maybe True (requestMethod req ==) method = f req
  | otherwise = Nothing

-- | Parse form request body.
parseBodyForm :: ResponderM ParsedRequest
parseBodyForm = do
  req <- getRequest
  let preq = fromMaybe (parseRequest req) $ V.lookup parsedReqKey (vault req)
  case preqBody preq of
    Just (FormBody _) -> return preq
    _ -> do
      let optsM = optsParseBody <$> V.lookup responderOptsKey (vault req)
          opts = fromMaybe noLimitParseRequestBodyOptions optsM
      (ps, fs) <- liftIO $ wrapErr $ parseRequestBodyEx opts lbsBackEnd req
      let parsedBody = FormBody (decodeBsParam <$> ps, fs)
          preq' = preq {preqBody = Just parsedBody}
      setRequest $ req {vault = V.insert parsedReqKey preq' (vault req)}
      return preq'

-- | Parse JSON request body.
parseBodyJson :: ResponderM JSON.Value
parseBodyJson = do
  req <- getRequest
  let preq = fromMaybe (parseRequest req) $ V.lookup parsedReqKey (vault req)
  case preqBody preq of
    Just (JSONBody json) -> return json
    _ -> do
      jsonE <- liftIO $ wrapErr $ JSON.eitherDecode <$> lazyRequestBody req
      case jsonE of
        Left msg -> throwM $ HttpError status400 msg
        Right json -> do
          let preq' = preq {preqBody = Just (JSONBody json)}
          setRequest $ req {vault = V.insert parsedReqKey preq' (vault req)}
          return json

wrapErr :: IO a -> IO a
wrapErr = handle wrapMaxReqErr . handle wrapParseErr

wrapMaxReqErr :: RequestSizeException -> IO a
wrapMaxReqErr (RequestSizeException max) =
  throwIO $ HttpError status413 $
    "Request body size larger than " <> show max <> " bytes."


wrapParseErr :: HTTP2Exception -> IO a
wrapParseErr (HTTP2Exception (ErrorCode code)) = do
  let statusCode = fromIntegral code
      statusMsg = BC.pack $ "HTTP/2 error: " ++ show code
      status = mkStatus statusCode statusMsg
      errorMsg = T.unpack $ decodeUtf8 statusMsg
  throwIO $ HttpError status errorMsg


parseCookieParams :: Request -> [Param]
parseCookieParams req =
  let headers = snd <$> L.filter ((==) hCookie . fst) (requestHeaders req)
   in join $ parseCookiesText <$> headers

setCookieByteString :: SetCookie -> B.ByteString
setCookieByteString setCookie =
  BL.toStrict (toLazyByteString (renderSetCookie setCookie))

decodeQueryParam :: (B.ByteString, Maybe B.ByteString) -> Param
decodeQueryParam (a, b) = (decodeUtf8 a, maybe "" decodeUtf8 b)

decodeBsParam :: (B.ByteString, B.ByteString) -> Param
decodeBsParam (a, b) = (decodeUtf8 a, decodeUtf8 b)