{-# LANGUAGE CPP,NoMonomorphismRestriction, FlexibleContexts #-}
module Happstack.Server.Internal.Compression
( compressedResponseFilter
, compressedResponseFilter'
, compressWithFilter
, gzipFilter
, deflateFilter
, identityFilter
, starFilter
, encodings
, standardEncodingHandlers
) where
import Happstack.Server.SimpleHTTP
import Text.ParserCombinators.Parsec
import Control.Monad
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail (MonadFail)
#endif
import Data.Maybe
import Data.List
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Codec.Compression.GZip as GZ
import qualified Codec.Compression.Zlib as Z
compressedResponseFilter :: (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m, MonadFail m) =>
m String
compressedResponseFilter = compressedResponseFilter' standardEncodingHandlers
compressedResponseFilter' ::
(FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m, MonadFail m)
=> [(String, String -> Bool -> m ())]
-> m String
compressedResponseFilter' encodingHandlers = do
getHeaderM "Accept-Encoding" >>=
(maybe (return "identity") installHandler)
where
badEncoding = "Encoding returned not in the list of known encodings"
installHandler accept = do
let eEncoding = bestEncoding (map fst encodingHandlers) $ BS.unpack accept
(coding, identityAllowed, action) <- case eEncoding of
Left _ -> do
setResponseCode 406
finishWith $ toResponse ""
Right encs@(a:_) -> return (a
, "identity" `elem` encs
, fromMaybe (\ _ _ -> fail badEncoding)
(lookup a encodingHandlers)
)
Right [] -> fail badEncoding
action coding identityAllowed
return coding
gzipFilter::(FilterMonad Response m) =>
String
-> Bool
-> m ()
gzipFilter = compressWithFilter GZ.compress
deflateFilter::(FilterMonad Response m) =>
String
-> Bool
-> m ()
deflateFilter = compressWithFilter Z.compress
identityFilter :: (FilterMonad Response m) =>
String
-> Bool
-> m ()
identityFilter = compressWithFilter id
starFilter :: (FilterMonad Response m, MonadFail m) =>
String
-> Bool
-> m ()
starFilter _ _ = fail "chose * as content encoding"
compressWithFilter :: (FilterMonad Response m) =>
(L.ByteString -> L.ByteString)
-> String
-> Bool
-> m ()
compressWithFilter compressor encoding identityAllowed =
composeFilter $ \r ->
case r of
Response{} -> setHeader "Content-Encoding" encoding $
setHeader "Vary" "Accept-Encoding" $
r {rsBody = compressor $ rsBody r}
_ | identityAllowed -> r
| otherwise -> (toResponse "") { rsCode = 406 }
bestEncoding :: [String] -> String -> Either String [String]
bestEncoding availableEncodings encs = do
encList<-either (Left . show) (Right) $ parse encodings "" encs
case acceptable encList of
[] -> Left "no encoding found"
a -> Right $ a
where
knownEncodings:: [(String,Maybe Double)] -> [(String, Maybe Double)]
knownEncodings m = intersectBy (\x y->fst x == fst y) m (map (\x -> (x,Nothing)) availableEncodings)
knownEncodings':: [(String,Maybe Double)] -> [(String, Maybe Double)]
knownEncodings' m = filter dropZero $ deleteBy (\(a,_) (b,_)->a==b) ("*",Nothing) $
case lookup "*" (knownEncodings m) of
Nothing -> addIdent $ knownEncodings m
Just (Just a) | a>0 -> addIdent $ knownEncodings m
| otherwise -> knownEncodings m
Just (Nothing) -> addIdent $ knownEncodings m
dropZero (_, Just a) | a==0 = False
| otherwise = True
dropZero (_, Nothing) = True
addIdent:: [(String,Maybe Double)] -> [(String, Maybe Double)]
addIdent m = if isNothing $ lookup "identity" m
then m ++ [("identity",Nothing)]
else m
acceptable:: [(String,Maybe Double)] -> [String]
acceptable l = map fst $ sortBy (flip cmp) $ knownEncodings' l
encOrder = reverse $ zip (reverse availableEncodings) [1..]
m0 = maybe (0.0::Double) id
cmp (s,mI) (t,mJ) | m0 mI == m0 mJ
= compare (m0 $ lookup s encOrder) (m0 $ lookup t encOrder)
| otherwise = compare (m0 mI) (m0 mJ)
standardEncodingHandlers :: (FilterMonad Response m, MonadFail m) =>
[(String, String -> Bool -> m ())]
standardEncodingHandlers = zip standardEncodings handlers
standardEncodings :: [String]
standardEncodings =
["gzip"
,"x-gzip"
,"deflate"
,"identity"
,"*"
]
handlers::(FilterMonad Response m, MonadFail m) => [String -> Bool -> m ()]
handlers =
[ gzipFilter
, gzipFilter
, deflateFilter
, identityFilter
, starFilter
]
encodings :: GenParser Char st [(String, Maybe Double)]
encodings = ws >> (encoding1 `sepBy` try sep) >>= (\x -> ws >> eof >> return x)
where
ws :: GenParser Char st ()
ws = many space >> return ()
sep :: GenParser Char st ()
sep = do
ws
_ <- char ','
ws
encoding1 :: GenParser Char st ([Char], Maybe Double)
encoding1 = do
encoding <- many1 (alphaNum <|> char '-') <|> string "*"
ws
quality<-optionMaybe qual
return (encoding, fmap read quality)
qual :: GenParser Char st String
qual = do
char ';' >> ws >> char 'q' >> ws >> char '=' >> ws
q<-float
return q
int :: GenParser Char st String
int = many1 digit
float :: GenParser Char st String
float = do
wholePart<-many1 digit
fractionalPart<-option "" fraction
return $ wholePart ++ fractionalPart
<|>
do
fractionalPart<-fraction
return fractionalPart
fraction :: GenParser Char st String
fraction = do
_ <- char '.'
fractionalPart<-option "" int
return $ '.':fractionalPart