{-# LANGUAGE CPP, OverloadedStrings #-}
module Network.Wai.Middleware.Static
(
static, staticPolicy, unsafeStaticPolicy
, static', staticPolicy', unsafeStaticPolicy'
, staticWithOptions, staticPolicyWithOptions, unsafeStaticPolicyWithOptions
,
Options, cacheContainer, mimeTypes, defaultOptions
,
CachingStrategy(..), FileMeta(..), initCaching, CacheContainer
,
Policy, (<|>), (>->), policy, predicate
, addBase, addSlash, contains, hasPrefix, hasSuffix, noDots, isNotAbsolute, only
,
tryPolicy
,
getMimeType
) where
import Caching.ExpiringCacheMap.HashECM (newECMIO, lookupECM, CacheSettings(..), consistentDuration)
import Control.Monad.Trans (liftIO)
import Data.List
#if !(MIN_VERSION_base(4,8,0))
import Data.Monoid (Monoid(..))
#endif
#if !(MIN_VERSION_base(4,11,0))
import Data.Semigroup (Semigroup(..))
#endif
import Data.Time
import Data.Time.Clock.POSIX
import Network.HTTP.Types (status200, status304)
import Network.HTTP.Types.Header (RequestHeaders)
import Network.Mime (MimeType, defaultMimeLookup)
import Network.Wai
import System.Directory (doesFileExist, getModificationTime)
#if !(MIN_VERSION_time(1,5,0))
import System.Locale
#endif
import Crypto.Hash.Algorithms
import Crypto.Hash
import Data.ByteArray.Encoding
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Text as T
import qualified System.FilePath as FP
newtype Policy = Policy { tryPolicy :: String -> Maybe String
}
data Options = Options { cacheContainer :: CacheContainer
, mimeTypes :: FilePath -> MimeType
}
defaultOptions :: Options
defaultOptions = Options { cacheContainer = CacheContainerEmpty, mimeTypes = getMimeType }
data CachingStrategy
= NoCaching
| PublicStaticCaching
| CustomCaching (FileMeta -> RequestHeaders)
instance Semigroup Policy where
p1 <> p2 = policy (maybe Nothing (tryPolicy p2) . tryPolicy p1)
instance Monoid Policy where
mempty = policy Just
mappend = (<>)
policy :: (String -> Maybe String) -> Policy
policy = Policy
predicate :: (String -> Bool) -> Policy
predicate p = policy (\s -> if p s then Just s else Nothing)
infixr 5 >->
(>->) :: Policy -> Policy -> Policy
(>->) = (<>)
infixr 4 <|>
(<|>) :: Policy -> Policy -> Policy
p1 <|> p2 = policy (\s -> maybe (tryPolicy p2 s) Just (tryPolicy p1 s))
addBase :: String -> Policy
addBase b = policy (Just . (b FP.</>))
addSlash :: Policy
addSlash = policy slashOpt
where slashOpt s@('/':_) = Just s
slashOpt s = Just ('/':s)
hasSuffix :: String -> Policy
hasSuffix = predicate . isSuffixOf
hasPrefix :: String -> Policy
hasPrefix = predicate . isPrefixOf
contains :: String -> Policy
contains = predicate . isInfixOf
noDots :: Policy
noDots = predicate (not . isInfixOf "..")
isNotAbsolute :: Policy
isNotAbsolute = predicate $ not . FP.isAbsolute
only :: [(String, String)] -> Policy
only al = policy (flip lookup al)
static :: Middleware
static = staticPolicy mempty
{-# DEPRECATED static'
[ "Use 'staticWithOptions' instead. "
, "This function will be removed in the next major release."
] #-}
static' :: CacheContainer -> Middleware
static' cc = staticPolicy' cc mempty
staticWithOptions :: Options -> Middleware
staticWithOptions options = staticPolicyWithOptions options mempty
staticPolicy :: Policy -> Middleware
staticPolicy = staticPolicy' (cacheContainer defaultOptions)
{-# DEPRECATED staticPolicy'
[ "Use 'staticPolicyWithOptions' instead. "
, "This function will be removed in the next major release."
] #-}
staticPolicy' :: CacheContainer -> Policy -> Middleware
staticPolicy' cc p = unsafeStaticPolicy' cc $ noDots >-> isNotAbsolute >-> p
staticPolicyWithOptions :: Options -> Policy -> Middleware
staticPolicyWithOptions options p = unsafeStaticPolicyWithOptions options $ noDots >-> isNotAbsolute >-> p
unsafeStaticPolicy :: Policy -> Middleware
unsafeStaticPolicy = unsafeStaticPolicy' (cacheContainer defaultOptions)
{-# DEPRECATED unsafeStaticPolicy'
[ "Use 'unsafeStaticPolicyWithOptions' instead. "
, "This function will be removed in the next major release."
] #-}
unsafeStaticPolicy' :: CacheContainer -> Policy -> Middleware
unsafeStaticPolicy' cc = unsafeStaticPolicyWithOptions (defaultOptions { cacheContainer = cc })
unsafeStaticPolicyWithOptions :: Options -> Policy -> Middleware
unsafeStaticPolicyWithOptions options p app req callback =
maybe (app req callback)
(\fp ->
do exists <- liftIO $ doesFileExist fp
if exists
then case cacheContainer options of
CacheContainerEmpty ->
sendFile fp []
CacheContainer _ NoCaching ->
sendFile fp []
CacheContainer getFileMeta strategy ->
do fileMeta <- getFileMeta fp
if checkNotModified fileMeta (readHeader "If-Modified-Since") (readHeader "If-None-Match")
then sendNotModified fileMeta strategy
else sendFile fp (computeHeaders fileMeta strategy)
else app req callback)
(tryPolicy p $ T.unpack $ T.intercalate "/" $ pathInfo req)
where
readHeader header =
lookup header $ requestHeaders req
checkNotModified fm modSince etag =
or [ Just (fm_lastModified fm) == modSince
, Just (fm_etag fm) == etag
]
computeHeaders fm cs =
case cs of
NoCaching -> []
PublicStaticCaching ->
[ ("Cache-Control", "no-transform,public,max-age=300,s-maxage=900")
, ("Last-Modified", fm_lastModified fm)
, ("ETag", fm_etag fm)
, ("Vary", "Accept-Encoding")
]
CustomCaching f -> f fm
sendNotModified fm cs =
do let cacheHeaders = computeHeaders fm cs
callback $ responseLBS status304 cacheHeaders BSL.empty
sendFile fp extraHeaders =
do let basicHeaders =
[ ("Content-Type", mimeTypes options fp)
]
headers =
basicHeaders ++ extraHeaders
callback $ responseFile status200 headers fp Nothing
data CacheContainer
= CacheContainerEmpty
| CacheContainer (FilePath -> IO FileMeta) CachingStrategy
data FileMeta
= FileMeta
{ fm_lastModified :: !BS.ByteString
, fm_etag :: !BS.ByteString
, fm_fileName :: FilePath
} deriving (Show, Eq)
initCaching :: CachingStrategy -> IO CacheContainer
initCaching cs =
do let cacheAccess =
consistentDuration 100 $ \state fp ->
do fileMeta <- computeFileMeta fp
return $! (state, fileMeta)
cacheTick =
do time <- getPOSIXTime
return (round (time * 100))
cacheFreq = 1
cacheLRU =
CacheWithLRUList 100 100 200
filecache <- newECMIO cacheAccess cacheTick cacheFreq cacheLRU
return (CacheContainer (lookupECM filecache) cs)
computeFileMeta :: FilePath -> IO FileMeta
computeFileMeta fp =
do mtime <- getModificationTime fp
ct <- BSL.readFile fp
return $ FileMeta
{ fm_lastModified =
BSC.pack $ formatTime defaultTimeLocale "%a, %d-%b-%Y %X %Z" mtime
, fm_etag = convertToBase Base16 (hashlazy ct :: Digest SHA1)
, fm_fileName = fp
}
getMimeType :: FilePath -> MimeType
getMimeType = defaultMimeLookup . T.pack