wai-middleware-static-0.8.2/0000755000000000000000000000000013262125467014077 5ustar0000000000000000wai-middleware-static-0.8.2/README.md0000644000000000000000000000036613262125467015363 0ustar0000000000000000# wai-middleware-static [![Build Status](https://travis-ci.org/scotty-web/wai-middleware-static.svg)](https://travis-ci.org/scotty-web/wai-middleware-static) WAI middleware that intercepts requests to static files and serves them if they exist. wai-middleware-static-0.8.2/changelog.md0000644000000000000000000000320713262125467016352 0ustar0000000000000000## 0.8.2 [2018.04.07] * Remove unused test suite. ## 0.8.1 * Add `Semigroup Policy` instance * Replace dependencies on `base16-bytestring` and `cryptohash` with the more modern `memory` and `cryptonite` packages, respectively [myfreeweb] ## 0.8.0 * The `mime-types` library is now used to lookup MIME types from extensions. As a result, some extensions now map to different MIME types. They are: Extension | `wai-middleware-static` | `mime-types` | --------- | ----------------------------- | ------------ | `class` | `application/octet-stream` | `application/java-vm` `dtd` | `text/xml` | `application/xml-dtd` `jar` | `application/x-java-archive` | `application/java-archive` `js` | `text/javascript` | `application/javascript` `ogg` | `application/ogg` | `audio/ogg` `ttf` | `application/x-font-truetype` | `application/x-font-ttf` * Exposed `getMimeType` function [Shimuuar] ## 0.7.0.1 * Fixed Windows build (by replacing `unix` dependency with equivalent `directory` function) ## 0.7.0.0 * Implement caching [agrafix] * Include mp4 and ogv mime_types [DrBoolean] * Dependency updates for ghc 7.10 [DougBurke] ## 0.6.0.1 * Update links to new wai-middleware-static github/issue tracker. * Bump upper bound for `text` ## 0.6.0 * Update to wai 3.0 ## 0.5.0.1 * Bump upper bound for `mtl` ## 0.5.0.0 * Add `isNotAbsolute` policy and change `static` and `staticPolicy` to use `noDots` and `isNotAbsolute` policies by default. (Thanks to Nick Hibberd!) * Add `unsafeStaticPolicy`, which behaves as the old insecure `staticPolicy` behaved. * Add changelog wai-middleware-static-0.8.2/wai-middleware-static.cabal0000644000000000000000000000441713262125467021251 0ustar0000000000000000Name: wai-middleware-static Version: 0.8.2 Synopsis: WAI middleware that serves requests to static files. Homepage: https://github.com/scotty-web/wai-middleware-static Bug-reports: https://github.com/scotty-web/wai-middleware-static/issues License: BSD3 License-file: LICENSE Author: Andrew Farmer Maintainer: Andrew Farmer Copyright: (c) 2012-2014 Andrew Farmer Category: Web Stability: experimental Build-type: Simple Cabal-version: >= 1.10 Description: WAI middleware that intercepts requests to static files and serves them if they exist. . [WAI] tested-with: GHC == 7.6.3 , GHC == 7.8.4 , GHC == 7.10.3 , GHC == 8.0.2 , GHC == 8.2.2 , GHC == 8.4.1 Extra-source-files: changelog.md, README.md Library Exposed-modules: Network.Wai.Middleware.Static default-language: Haskell2010 Build-depends: base >= 4.6.0.1 && < 5, bytestring >= 0.10.0.2 && < 0.11, containers >= 0.5.0.0 && < 0.6, cryptonite >= 0.10 && < 1.0, memory >= 0.10 && < 1.0, directory >= 1.2.0.1 && < 1.4, expiring-cache-map >= 0.0.5 && < 0.1, filepath >= 1.3.0.1 && < 1.5, http-types >= 0.8.2 && < 0.13, mime-types >= 0.1.0.3 && < 0.2, mtl >= 2.1.2 && < 2.3, old-locale >= 1.0 && < 1.1, semigroups >= 0.18 && < 1, text >= 0.11.3.1 && < 1.3, time >= 1.4 && < 1.9, wai >= 3.0.0 && < 3.3 GHC-options: -Wall -fno-warn-orphans source-repository head type: git location: git://github.com/scotty-web/wai-middleware-static.git wai-middleware-static-0.8.2/LICENSE0000644000000000000000000000304013262125467015101 0ustar0000000000000000Copyright (c) 2012-Present, Andrew Farmer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Andrew Farmer nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. wai-middleware-static-0.8.2/Setup.hs0000644000000000000000000000005613262125467015534 0ustar0000000000000000import Distribution.Simple main = defaultMain wai-middleware-static-0.8.2/Network/0000755000000000000000000000000013262125467015530 5ustar0000000000000000wai-middleware-static-0.8.2/Network/Wai/0000755000000000000000000000000013262125467016250 5ustar0000000000000000wai-middleware-static-0.8.2/Network/Wai/Middleware/0000755000000000000000000000000013262125467020325 5ustar0000000000000000wai-middleware-static-0.8.2/Network/Wai/Middleware/Static.hs0000644000000000000000000002407213262125467022115 0ustar0000000000000000{-# LANGUAGE CPP, OverloadedStrings #-} -- | Serve static files, subject to a policy that can filter or -- modify incoming URIs. The flow is: -- -- incoming request URI ==> policies ==> exists? ==> respond -- -- If any of the polices fail, or the file doesn't -- exist, then the middleware gives up and calls the inner application. -- If the file is found, the middleware chooses a content type based -- on the file extension and returns the file contents as the response. module Network.Wai.Middleware.Static ( -- * Middlewares static, staticPolicy, unsafeStaticPolicy , static', staticPolicy', unsafeStaticPolicy' , -- * Cache Control CachingStrategy(..), FileMeta(..), initCaching, CacheContainer , -- * Policies Policy, (<|>), (>->), policy, predicate , addBase, addSlash, contains, hasPrefix, hasSuffix, noDots, isNotAbsolute, only , -- * Utilities tryPolicy , -- * MIME types 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 import Data.Semigroup (Semigroup(..)) 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 -- | Take an incoming URI and optionally modify or filter it. -- The result will be treated as a filepath. newtype Policy = Policy { tryPolicy :: String -> Maybe String -- ^ Run a policy } -- | A cache strategy which should be used to -- serve content matching a policy. Meta information is cached for a maxium of -- 100 seconds before being recomputed. data CachingStrategy -- | Do not send any caching headers = NoCaching -- | Send common caching headers for public (non dynamic) static files | PublicStaticCaching -- | Compute caching headers using the user specified function. -- See for a detailed guide | CustomCaching (FileMeta -> RequestHeaders) -- | Note: -- '(<>)' == @>->@ (policy sequencing) instance Semigroup Policy where p1 <> p2 = policy (maybe Nothing (tryPolicy p2) . tryPolicy p1) -- | Note: -- 'mempty' == @policy Just@ (the always accepting policy) -- 'mappend' == @>->@ (policy sequencing) instance Monoid Policy where mempty = policy Just mappend = (<>) -- | Lift a function into a 'Policy' policy :: (String -> Maybe String) -> Policy policy = Policy -- | Lift a predicate into a 'Policy' predicate :: (String -> Bool) -> Policy predicate p = policy (\s -> if p s then Just s else Nothing) -- | Sequence two policies. They are run from left to right. (Note: this is `mappend`) infixr 5 >-> (>->) :: Policy -> Policy -> Policy (>->) = (<>) -- | Choose between two policies. If the first fails, run the second. infixr 4 <|> (<|>) :: Policy -> Policy -> Policy p1 <|> p2 = policy (\s -> maybe (tryPolicy p2 s) Just (tryPolicy p1 s)) -- | Add a base path to the URI -- -- > staticPolicy (addBase "/home/user/files") -- -- GET \"foo\/bar\" looks for \"\/home\/user\/files\/foo\/bar\" -- addBase :: String -> Policy addBase b = policy (Just . (b FP.)) -- | Add an initial slash to to the URI, if not already present. -- -- > staticPolicy addSlash -- -- GET \"foo\/bar\" looks for \"\/foo\/bar\" addSlash :: Policy addSlash = policy slashOpt where slashOpt s@('/':_) = Just s slashOpt s = Just ('/':s) -- | Accept only URIs with given suffix hasSuffix :: String -> Policy hasSuffix = predicate . isSuffixOf -- | Accept only URIs with given prefix hasPrefix :: String -> Policy hasPrefix = predicate . isPrefixOf -- | Accept only URIs containing given string contains :: String -> Policy contains = predicate . isInfixOf -- | Reject URIs containing \"..\" noDots :: Policy noDots = predicate (not . isInfixOf "..") -- | Reject URIs that are absolute paths isNotAbsolute :: Policy isNotAbsolute = predicate $ not . FP.isAbsolute -- | Use URI as the key to an association list, rejecting those not found. -- The policy result is the matching value. -- -- > staticPolicy (only [("foo/bar", "/home/user/files/bar")]) -- -- GET \"foo\/bar\" looks for \"\/home\/user\/files\/bar\" -- GET \"baz\/bar\" doesn't match anything -- only :: [(String,String)] -> Policy only al = policy (flip lookup al) -- | Serve static files out of the application root (current directory). -- If file is found, it is streamed to the client and no further middleware is run. Disables caching. -- -- Note: for security reasons, this uses the 'noDots' and 'isNotAbsolute' policy by default. static :: Middleware static = staticPolicy mempty -- | Serve static files out of the application root (current directory). -- If file is found, it is streamed to the client and no further middleware is run. Allows a 'CachingStrategy'. -- -- Note: for security reasons, this uses the 'noDots' and 'isNotAbsolute' policy by default. static' :: CacheContainer -> Middleware static' cc = staticPolicy' cc mempty -- | Serve static files subject to a 'Policy'. Disables caching. -- -- Note: for security reasons, this uses the 'noDots' and 'isNotAbsolute' policy by default. staticPolicy :: Policy -> Middleware staticPolicy = staticPolicy' CacheContainerEmpty -- | Serve static files subject to a 'Policy' using a specified 'CachingStrategy' -- -- Note: for security reasons, this uses the 'noDots' and 'isNotAbsolute' policy by default. staticPolicy' :: CacheContainer -> Policy -> Middleware staticPolicy' cc p = unsafeStaticPolicy' cc $ noDots >-> isNotAbsolute >-> p -- | Serve static files subject to a 'Policy'. Unlike 'static' and 'staticPolicy', this -- has no policies enabled by default, and is hence insecure. Disables caching. unsafeStaticPolicy :: Policy -> Middleware unsafeStaticPolicy = unsafeStaticPolicy' CacheContainerEmpty -- | Serve static files subject to a 'Policy'. Unlike 'static' and 'staticPolicy', this -- has no policies enabled by default, and is hence insecure. Also allows to set a 'CachingStrategy'. unsafeStaticPolicy' :: CacheContainer -> Policy -> Middleware unsafeStaticPolicy' cacheContainer p app req callback = maybe (app req callback) (\fp -> do exists <- liftIO $ doesFileExist fp if exists then case cacheContainer 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", getMimeType fp) ] headers = basicHeaders ++ extraHeaders callback $ responseFile status200 headers fp Nothing -- | Container caching file meta information. Create using 'initCaching' data CacheContainer = CacheContainerEmpty | CacheContainer (FilePath -> IO FileMeta) CachingStrategy -- | Meta information about a file to calculate cache headers data FileMeta = FileMeta { fm_lastModified :: !BS.ByteString , fm_etag :: !BS.ByteString , fm_fileName :: FilePath } deriving (Show, Eq) -- | Initialize caching. This should only be done once per application launch. 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 } -- | Guess MIME type from file extension getMimeType :: FilePath -> MimeType getMimeType = defaultMimeLookup . T.pack