on master help sign in
Initial version
commit4e560e1d3e07cb2bcf45f3fbee76628294889887 authorMichael Snoyman <michael@snoyman.com> dateMon 17 Jan 2011, 16:41 (16 years ago) parent092a3fb
4 files changed+298
LICENSE+25 Network/Wai/Application/Static.hs+217 wai-app-static.cabal+35 warp-static.hs+21
LICENSEnew file+25
@@ -0,0 +1,25 @@
1+The following license covers this documentation, and the source code, except
2+where otherwise indicated.
3+
4+Copyright 2010, Michael Snoyman. All rights reserved.
5+
6+Redistribution and use in source and binary forms, with or without
7+modification, are permitted provided that the following conditions are met:
8+
9+* Redistributions of source code must retain the above copyright notice, this
10+ list of conditions and the following disclaimer.
11+
12+* Redistributions in binary form must reproduce the above copyright notice,
13+ this list of conditions and the following disclaimer in the documentation
14+ and/or other materials provided with the distribution.
15+
16+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
17+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
20+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
22+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
24+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Network/Wai/Application/Static.hsnew file+217
@@ -0,0 +1,217 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+-- | Static file serving for WAI.
3+module Network.Wai.Application.Static
4+ ( -- * Generic, non-WAI code
5+ -- ** Mime types
6+ MimeType
7+ , defaultMimeType
8+ -- ** Mime type by file extension
9+ , MimeMap
10+ , defaultMimeTypes
11+ , mimeTypeByExt
12+ , defaultMimeTypeByExt
13+ -- ** Finding files
14+ , CheckPieces
15+ , checkPieces
16+ -- * WAI application
17+ , StaticSettings (..)
18+ , staticApp
19+ ) where
20+
21+import qualified Network.Wai as W
22+import Data.Map (Map)
23+import qualified Data.Map as Map
24+import Data.ByteString (ByteString)
25+import System.FilePath (takeExtensions)
26+import Data.Maybe (fromMaybe)
27+import System.Directory (doesFileExist, doesDirectoryExist)
28+import qualified Data.ByteString.Char8 as S8
29+import qualified Data.ByteString.Lazy as L
30+import Data.ByteString.Lazy.Char8 ()
31+import Web.Routes.Base (decodePathInfo, encodePathInfo)
32+import System.PosixCompat.Files (fileSize, getFileStatus)
33+import Control.Monad.IO.Class (liftIO)
34+
35+type MimeType = ByteString
36+type MimeMap = Map FilePath MimeType
37+
38+defaultMimeType :: MimeType
39+defaultMimeType = "application/octet-stream"
40+
41+-- taken from snap-core Snap.Util.FileServer
42+defaultMimeTypes :: MimeMap
43+defaultMimeTypes = Map.fromList [
44+ ( ".asc" , "text/plain" ),
45+ ( ".asf" , "video/x-ms-asf" ),
46+ ( ".asx" , "video/x-ms-asf" ),
47+ ( ".avi" , "video/x-msvideo" ),
48+ ( ".bz2" , "application/x-bzip" ),
49+ ( ".c" , "text/plain" ),
50+ ( ".class" , "application/octet-stream" ),
51+ ( ".conf" , "text/plain" ),
52+ ( ".cpp" , "text/plain" ),
53+ ( ".css" , "text/css" ),
54+ ( ".cxx" , "text/plain" ),
55+ ( ".dtd" , "text/xml" ),
56+ ( ".dvi" , "application/x-dvi" ),
57+ ( ".gif" , "image/gif" ),
58+ ( ".gz" , "application/x-gzip" ),
59+ ( ".hs" , "text/plain" ),
60+ ( ".htm" , "text/html" ),
61+ ( ".html" , "text/html" ),
62+ ( ".jar" , "application/x-java-archive" ),
63+ ( ".jpeg" , "image/jpeg" ),
64+ ( ".jpg" , "image/jpeg" ),
65+ ( ".js" , "text/javascript" ),
66+ ( ".log" , "text/plain" ),
67+ ( ".m3u" , "audio/x-mpegurl" ),
68+ ( ".mov" , "video/quicktime" ),
69+ ( ".mp3" , "audio/mpeg" ),
70+ ( ".mpeg" , "video/mpeg" ),
71+ ( ".mpg" , "video/mpeg" ),
72+ ( ".ogg" , "application/ogg" ),
73+ ( ".pac" , "application/x-ns-proxy-autoconfig" ),
74+ ( ".pdf" , "application/pdf" ),
75+ ( ".png" , "image/png" ),
76+ ( ".ps" , "application/postscript" ),
77+ ( ".qt" , "video/quicktime" ),
78+ ( ".sig" , "application/pgp-signature" ),
79+ ( ".spl" , "application/futuresplash" ),
80+ ( ".swf" , "application/x-shockwave-flash" ),
81+ ( ".tar" , "application/x-tar" ),
82+ ( ".tar.bz2" , "application/x-bzip-compressed-tar" ),
83+ ( ".tar.gz" , "application/x-tgz" ),
84+ ( ".tbz" , "application/x-bzip-compressed-tar" ),
85+ ( ".text" , "text/plain" ),
86+ ( ".tgz" , "application/x-tgz" ),
87+ ( ".torrent" , "application/x-bittorrent" ),
88+ ( ".ttf" , "application/x-font-truetype" ),
89+ ( ".txt" , "text/plain" ),
90+ ( ".wav" , "audio/x-wav" ),
91+ ( ".wax" , "audio/x-ms-wax" ),
92+ ( ".wma" , "audio/x-ms-wma" ),
93+ ( ".wmv" , "video/x-ms-wmv" ),
94+ ( ".xbm" , "image/x-xbitmap" ),
95+ ( ".xml" , "text/xml" ),
96+ ( ".xpm" , "image/x-xpixmap" ),
97+ ( ".xwd" , "image/x-xwindowdump" ),
98+ ( ".zip" , "application/zip" )]
99+
100+mimeTypeByExt :: MimeMap
101+ -> MimeType -- ^ default mime type
102+ -> FilePath
103+ -> MimeType
104+mimeTypeByExt mm def fp = fromMaybe def $ Map.lookup (takeExtensions fp) mm
105+
106+defaultMimeTypeByExt :: FilePath -> MimeType
107+defaultMimeTypeByExt = mimeTypeByExt defaultMimeTypes defaultMimeType
108+
109+data CheckPieces
110+ = Redirect [String]
111+ | Forbidden
112+ | NotFound
113+ | FileResponse FilePath
114+ | DirectoryResponse FilePath
115+ deriving Show
116+
117+anyButLast :: (a -> Bool) -> [a] -> Bool
118+anyButLast _ [] = False
119+anyButLast _ [_] = False
120+anyButLast p (x:xs)
121+ | p x == True = True
122+ | otherwise = anyButLast p xs
123+
124+filterButLast :: (a -> Bool) -> [a] -> [a]
125+filterButLast _ [] = []
126+filterButLast _ [x] = [x]
127+filterButLast f (x:xs)
128+ | f x = x : filterButLast f xs
129+ | otherwise = filterButLast f xs
130+
131+unsafe :: FilePath -> Bool
132+unsafe ('.':_) = True
133+unsafe s = any (== '/') s
134+
135+stripTrailingSlash :: FilePath -> FilePath
136+stripTrailingSlash "/" = ""
137+stripTrailingSlash "" = ""
138+stripTrailingSlash (x:xs) = x : stripTrailingSlash xs
139+
140+checkPieces :: FilePath -- ^ static file prefix
141+ -> [String] -- ^ parsed request
142+ -> IO CheckPieces
143+checkPieces prefix pieces
144+ | any unsafe pieces = return Forbidden
145+ | anyButLast null pieces =
146+ return $ Redirect $ filterButLast (not . null) pieces
147+ | otherwise = do
148+ let fp = concat $ prefix : map ((:) '/') pieces
149+ let (isFile, isFolder) =
150+ case () of
151+ ()
152+ | null pieces -> (True, True)
153+ | null (last pieces) -> (False, True)
154+ | otherwise -> (True, False)
155+ fe <- doesFileExist $ stripTrailingSlash fp
156+ case (fe, isFile) of
157+ (True, True) -> return $ FileResponse fp
158+ (True, False) -> return $ Redirect $ init pieces
159+ (False, _) -> do
160+ de <- doesDirectoryExist fp
161+ case (de, isFolder) of
162+ (True, True) -> return $ DirectoryResponse fp
163+ (True, False) -> return $ Redirect $ pieces ++ [""]
164+ (False, _) -> return NotFound
165+
166+type Listing = FilePath -> IO L.ByteString
167+
168+data StaticSettings = StaticSettings
169+ { ssFolder :: FilePath
170+ , ssIndices :: [FilePath]
171+ , ssListing :: Maybe Listing
172+ , ssGetMimeType :: FilePath -> IO MimeType
173+ }
174+
175+staticApp :: StaticSettings -> W.Application
176+staticApp (StaticSettings folder indices mlisting getmime) req = liftIO $ do
177+ let pieces = decodePathInfo $ S8.unpack $ W.pathInfo req
178+ cp <- checkPieces folder pieces
179+ case cp of
180+ Redirect pieces' -> do
181+ let loc = S8.pack $ '/' : encodePathInfo pieces' []
182+ return $ W.responseLBS W.status301
183+ [ ("Content-Type", "text/plain")
184+ , ("Location", loc)
185+ ] "Redirect"
186+ Forbidden -> return $ W.responseLBS W.status403
187+ [ ("Content-Type", "text/plain")
188+ ] "Forbidden"
189+ NotFound -> return $ W.responseLBS W.status404
190+ [ ("Content-Type", "text/plain")
191+ ] "File not found"
192+ FileResponse fp -> sendfile fp
193+ DirectoryResponse fp -> sendfolder fp indices
194+ where
195+ sendfolder fp (i:is) = do
196+ let fp' = fp ++ '/' : i
197+ fe <- doesFileExist fp'
198+ if fe
199+ then sendfile fp'
200+ else sendfolder fp is
201+ sendfolder fp [] =
202+ case mlisting of
203+ Just listing -> do
204+ lbs <- listing fp
205+ return $ W.responseLBS W.status200
206+ [ ("Content-Type", "text/html")
207+ ] lbs
208+ Nothing -> return $ W.responseLBS W.status403
209+ [ ("Content-Type", "text/plain")
210+ ] "Directory listings disabled"
211+ sendfile fp = do
212+ mimetype <- getmime fp
213+ filesize <- fileSize `fmap` getFileStatus fp
214+ return $ W.ResponseFile W.status200
215+ [ ("Content-Type", mimetype)
216+ , ("Content-Length", S8.pack $ show filesize)
217+ ] fp
wai-app-static.cabalnew file+35
@@ -0,0 +1,35 @@
1+name: wai-app-static
2+version: 0.0.0
3+license: BSD3
4+license-file: LICENSE
5+author: Michael Snoyman <michael@snoyman.com>
6+maintainer: Michael Snoyman <michael@snoyman.com>
7+synopsis: WAI application for static serving
8+description: Also provides some helper functions and datatypes for use outside of WAI.
9+category: Web, Yesod
10+stability: Stable
11+cabal-version: >= 1.6
12+build-type: Simple
13+homepage: http://docs.yesodweb.com/
14+
15+library
16+ build-depends: base >= 4 && < 5
17+ , wai >= 0.3 && < 0.4
18+ , bytestring >= 0.9.1.4 && < 0.10
19+ , web-routes >= 0.23 && < 0.24
20+ , transformers >= 0.2.2 && < 0.3
21+ , unix-compat >= 0.2 && < 0.3
22+ , directory >= 1.0 && < 1.2
23+ , filepath >= 1.1 && < 1.2
24+ , containers >= 0.2 && < 0.5
25+ exposed-modules: Network.Wai.Application.Static
26+ ghc-options: -Wall
27+
28+executable warp-static
29+ build-depends: warp >= 0.3 && < 0.4
30+ main-is: warp-static.hs
31+ ghc-options: -Wall
32+
33+source-repository head
34+ type: git
35+ location: git://github.com/snoyberg/wai-app-static.git
warp-static.hsnew file+21
@@ -0,0 +1,21 @@
1+import Network.Wai.Application.Static
2+ ( StaticSettings (..), staticApp, defaultMimeTypeByExt
3+ )
4+import Network.Wai.Handler.Warp (run)
5+import System.Environment (getArgs)
6+
7+main :: IO ()
8+main = do
9+ args <- getArgs
10+ (folder, port) <- return $
11+ case args of
12+ [] -> (".", 3000)
13+ [f] -> (f, 3000)
14+ [f, p] -> (f, read p)
15+ _ -> error "Usage: warp-static [root [port]]"
16+ run port $ staticApp StaticSettings
17+ { ssFolder = folder
18+ , ssIndices = ["index.html", "index.htm"]
19+ , ssListing = Nothing
20+ , ssGetMimeType = return . defaultMimeTypeByExt
21+ }