on master help sign in
Converted some middleware and handlers from Hack
commitb549d326980faf6c6d89827208df7525b69926a9 authorMichael Snoyman <michael@snoyman.com> dateSat 30 Jan 2010, 23:53 (17 years ago) parentc9802a0
10 files changed+732
.gitignore+2 LICENSE+25 Network/Wai/Handler/CGI.hs+109 Network/Wai/Handler/SimpleServer.hs+186 Network/Wai/Middleware/CleanPath.hs+65 Network/Wai/Middleware/ClientSession.hs+136 Network/Wai/Middleware/Gzip.hs+63 Network/Wai/Middleware/Jsonp.hs+71 Network/Wai/Middleware/MethodOverride.hs+38 wai-extra.cabal+37
.gitignorenew file+2
@@ -0,0 +1,2 @@
1+*.swp
2+dist
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/Handler/CGI.hsnew file+109
@@ -0,0 +1,109 @@
1+module Network.Wai.Handler.CGI
2+ ( run
3+ , run'
4+ ) where
5+
6+import Network.Wai
7+import Network.Wai.Enumerator (fromEitherFile)
8+import System.Environment (getEnvironment)
9+import Data.Maybe (fromMaybe)
10+import qualified Data.ByteString.Char8 as B
11+import Control.Arrow ((***))
12+import Data.Char (toLower)
13+import qualified System.IO
14+import Control.Concurrent
15+
16+safeRead :: Read a => a -> String -> a
17+safeRead d s =
18+ case reads s of
19+ ((x, _):_) -> x
20+ [] -> d
21+
22+lookup' :: String -> [(String, String)] -> String
23+lookup' key pairs = fromMaybe "" $ lookup key pairs
24+
25+run :: Application -> IO ()
26+run app = do
27+ vars <- getEnvironment
28+ run' vars System.IO.stdin System.IO.stdout app
29+
30+run' :: [(String, String)] -- ^ all variables
31+ -> System.IO.Handle -- ^ body of input
32+ -> System.IO.Handle -- ^ destination for output
33+ -> Application
34+ -> IO ()
35+run' vars inputH outputH app = do
36+ let rmethod = safeRead GET $ lookup' "REQUEST_METHOD" vars
37+ pinfo = lookup' "PATH_INFO" vars
38+ qstring = lookup' "QUERY_STRING" vars
39+ servername = lookup' "SERVER_NAME" vars
40+ serverport = safeRead 80 $ lookup' "SERVER_PORT" vars
41+ contentLength = safeRead 0 $ lookup' "CONTENT_LENGTH" vars
42+ remoteHost' = safeRead "" $ lookup' "REMOTE_HOST" vars
43+ urlScheme' =
44+ case map toLower $ lookup' "SERVER_PROTOCOL" vars of
45+ "https" -> HTTPS
46+ _ -> HTTP
47+ mContentLength <- newMVar contentLength
48+ let env = Request
49+ { requestMethod = rmethod
50+ , pathInfo = B.pack pinfo
51+ , queryString = B.pack qstring
52+ , serverName = B.pack servername
53+ , serverPort = serverport
54+ , httpHeaders = map (cleanupVarName *** B.pack) vars
55+ , urlScheme = urlScheme'
56+ , requestBody = requestBodyHandle inputH mContentLength
57+ , errorHandler = System.IO.hPutStr System.IO.stderr
58+ , remoteHost = B.pack remoteHost'
59+ , httpVersion = HttpVersion B.empty
60+ }
61+ res <- app env
62+ let h = headers res
63+ let h' = case lookup ContentType h of
64+ Nothing -> (ContentType, B.pack "text/html; charset=utf-8")
65+ : h
66+ Just _ -> h
67+ let hPut = B.hPut outputH
68+ hPut $ B.pack $ "Status: " ++ (show $ statusCode $ status res) ++ " "
69+ hPut $ statusMessage $ status res
70+ hPut $ B.singleton '\n'
71+ mapM_ (printHeader hPut) h'
72+ hPut $ B.singleton '\n'
73+ _ <- fromEitherFile (body res) (myPut outputH) ()
74+ return ()
75+
76+myPut :: System.IO.Handle -> () -> B.ByteString -> IO (Either () ())
77+myPut outputH _ bs = B.hPut outputH bs >> return (Right ())
78+
79+requestBodyHandle :: System.IO.Handle -> MVar Int -> Enumerator a
80+requestBodyHandle h mlen iter accum = modifyMVar mlen (helper accum) where
81+ helper a 0 = return (0, Right a)
82+ helper a len = do
83+ let maxChunkSize = 1024
84+ bs <- B.hGet h $ min len maxChunkSize
85+ let newLen = len - B.length bs
86+ ea' <- iter a bs
87+ case ea' of
88+ Left a' -> return (newLen, Left a')
89+ Right a' -> helper a' newLen
90+
91+printHeader :: (B.ByteString -> IO ())
92+ -> (ResponseHeader, B.ByteString)
93+ -> IO ()
94+printHeader f (x, y) = do
95+ f $ responseHeaderToBS x
96+ f $ B.pack ": "
97+ f y
98+ f $ B.singleton '\n'
99+
100+cleanupVarName :: String -> RequestHeader
101+cleanupVarName ('H':'T':'T':'P':'_':a:as) =
102+ requestHeaderFromBS $ B.pack $ a : helper' as where
103+ helper' ('_':x:rest) = '-' : x : helper' rest
104+ helper' (x:rest) = toLower x : helper' rest
105+ helper' [] = []
106+cleanupVarName "CONTENT_TYPE" = ReqContentType
107+cleanupVarName "CONTENT_LENGTH" = ReqContentLength
108+cleanupVarName "SCRIPT_NAME" = RequestHeader $ B.pack "CGI-Script-Name"
109+cleanupVarName x = requestHeaderFromBS $ B.pack x -- FIXME remove?
Network/Wai/Handler/SimpleServer.hsnew file+186
@@ -0,0 +1,186 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+---------------------------------------------------------
4+-- |
5+-- Module : Network.Wai.Handler.SimpleServer
6+-- Copyright : Michael Snoyman
7+-- License : BSD3
8+--
9+-- Maintainer : Michael Snoyman <michael@snoyman.com>
10+-- Stability : Stable
11+-- Portability : portable
12+--
13+-- A simplistic HTTP server handler for Wai.
14+--
15+---------------------------------------------------------
16+module Network.Wai.Handler.SimpleServer
17+ ( run
18+ ) where
19+
20+import Network.Wai
21+import qualified System.IO
22+
23+import qualified Data.ByteString as BS
24+import qualified Data.ByteString.Char8 as B8
25+import Network
26+ ( listenOn, accept, sClose, PortID(PortNumber), Socket
27+ , withSocketsDo)
28+import Control.Exception (bracket, finally, Exception)
29+import System.IO (Handle, hClose)
30+import Control.Concurrent
31+import Control.Monad (unless)
32+import Data.Maybe (isJust, fromJust, fromMaybe)
33+
34+import Control.Failure
35+import Data.Typeable (Typeable)
36+
37+import Web.Encodings.StringLike (StringLike)
38+import qualified Web.Encodings.StringLike as SL
39+
40+import qualified Safe
41+import Network.Socket.SendFile
42+import Control.Arrow (first)
43+
44+run :: Port -> Application -> IO ()
45+run port = withSocketsDo .
46+ bracket
47+ (listenOn $ PortNumber $ fromIntegral port)
48+ sClose .
49+ serveConnections port
50+type Port = Int
51+
52+serveConnections :: Port -> Application -> Socket -> IO ()
53+serveConnections port app socket = do
54+ (conn, remoteHost', _) <- accept socket
55+ _ <- forkIO $ serveConnection port app conn remoteHost'
56+ serveConnections port app socket
57+
58+serveConnection :: Port -> Application -> Handle -> String -> IO ()
59+serveConnection port app conn remoteHost' =
60+ finally
61+ serveConnection'
62+ (hClose conn)
63+ where
64+ serveConnection' = do
65+ env <- hParseRequest port conn remoteHost'
66+ res <- app env
67+ sendResponse conn res
68+
69+hParseRequest :: Port -> Handle -> String -> IO Request
70+hParseRequest port conn remoteHost' = do
71+ headers' <- takeUntilBlank conn id
72+ parseRequest port headers' conn remoteHost'
73+
74+takeUntilBlank :: Handle
75+ -> ([BS.ByteString] -> [BS.ByteString])
76+ -> IO [BS.ByteString]
77+takeUntilBlank h front = do
78+ l <- stripCR `fmap` BS.hGetLine h
79+ if BS.null l
80+ then return $ front []
81+ else takeUntilBlank h $ front . (:) l
82+
83+stripCR :: BS.ByteString -> BS.ByteString
84+stripCR bs
85+ | BS.null bs = bs
86+ | B8.last bs == '\r' = BS.init bs
87+ | otherwise = bs
88+
89+data InvalidRequest =
90+ NotEnoughLines [String]
91+ | HostNotIncluded
92+ | BadFirstLine String
93+ | NonHttp11
94+ deriving (Show, Typeable)
95+instance Exception InvalidRequest
96+
97+-- | Parse a set of header lines and body into a 'Request'.
98+parseRequest :: Port
99+ -> [BS.ByteString]
100+ -> Handle
101+ -> String
102+ -> IO Request
103+parseRequest port lines' handle remoteHost' = do
104+ case lines' of
105+ (_:_:_) -> return ()
106+ _ -> failure $ NotEnoughLines $ map SL.unpack lines'
107+ (method', rpath', gets) <- parseFirst $ head lines'
108+ let method = methodFromBS method'
109+ let rpath = '/' : case SL.unpack rpath' of
110+ ('/':x) -> x
111+ _ -> SL.unpack rpath'
112+ let heads = map (first requestHeaderFromBS . parseHeaderNoAttr)
113+ $ tail lines'
114+ let host' = lookup Host heads
115+ unless (isJust host') $ failure HostNotIncluded
116+ let host = fromJust host'
117+ let len = fromMaybe 0 $ do
118+ bs <- lookup ReqContentLength heads
119+ let str = SL.unpack bs
120+ Safe.readMay str
121+ mlen <- newMVar len
122+ let (serverName', _) = SL.breakChar ':' host
123+ return $ Request
124+ { requestMethod = method
125+ , httpVersion = Http11
126+ , pathInfo = SL.pack rpath
127+ , queryString = gets
128+ , serverName = serverName'
129+ , serverPort = port
130+ , httpHeaders = heads
131+ , urlScheme = HTTP
132+ , requestBody = requestBodyHandle handle mlen
133+ , errorHandler = System.IO.hPutStr System.IO.stderr
134+ , remoteHost = B8.pack remoteHost'
135+ }
136+
137+requestBodyHandle :: Handle -> MVar Int -> Enumerator a
138+requestBodyHandle h mlen iter accum = modifyMVar mlen (helper accum) where
139+ helper a 0 = return (0, Right a)
140+ helper a len = do
141+ let maxChunkSize = 1024
142+ bs <- BS.hGet h $ min len maxChunkSize
143+ let newLen = len - BS.length bs
144+ ea' <- iter a bs
145+ case ea' of
146+ Left a' -> return (newLen, Left a')
147+ Right a' -> helper a' newLen
148+
149+parseFirst :: (StringLike s, MonadFailure InvalidRequest m) =>
150+ s
151+ -> m (s, s, s)
152+parseFirst s = do
153+ let pieces = SL.split ' ' s
154+ (method, query, http') <-
155+ case pieces of
156+ [x, y, z] -> return (x, y, z)
157+ _ -> failure $ BadFirstLine $ SL.unpack s
158+ unless (http' == SL.pack "HTTP/1.1") $ failure NonHttp11
159+ let (rpath, qstring) = SL.breakChar '?' query
160+ return (method, rpath, qstring)
161+
162+sendResponse :: Handle -> Response -> IO ()
163+sendResponse h res = do
164+ BS.hPut h $ SL.pack "HTTP/1.1 "
165+ BS.hPut h $ SL.pack $ show $ statusCode $ status res
166+ BS.hPut h $ statusMessage $ status res
167+ BS.hPut h $ SL.pack "\r\n"
168+ mapM_ putHeader $ headers res
169+ BS.hPut h $ SL.pack "\r\n"
170+ case body res of
171+ Left fp -> unsafeSendFile h fp
172+ Right enum -> enum myPut h >> return ()
173+ where
174+ myPut _ bs = do
175+ BS.hPut h bs
176+ return (Right h)
177+ putHeader (x, y) = do
178+ BS.hPut h $ responseHeaderToBS x
179+ BS.hPut h $ SL.pack ": "
180+ BS.hPut h y
181+ BS.hPut h $ SL.pack "\r\n"
182+
183+parseHeaderNoAttr :: StringLike a => a -> (a, a)
184+parseHeaderNoAttr s =
185+ let (k, rest) = SL.span (/= ':') s
186+ in (k, SL.dropPrefix' (SL.pack ": ") rest)
Network/Wai/Middleware/CleanPath.hsnew file+65
@@ -0,0 +1,65 @@
1+module Network.Wai.Middleware.CleanPath (cleanPath, splitPath) where
2+
3+import Network.Wai
4+import Web.Encodings
5+import qualified Data.ByteString.Char8 as B
6+
7+-- | Performs redirects as per 'splitPath'.
8+cleanPath :: ([B.ByteString] -> Request -> IO Response)
9+ -> Request
10+ -> IO Response
11+cleanPath app env =
12+ case splitPath $ pathInfo env of
13+ Left p -> do
14+ -- include the query string if present
15+ let suffix = case B.uncons $ queryString env of
16+ Nothing -> B.empty
17+ Just ('?', _) -> queryString env
18+ _ -> B.cons '?' $ queryString env
19+ return $ Response Status303 [(Location, B.append p suffix)]
20+ $ Right emptyEnum
21+ Right pieces -> app pieces env
22+
23+emptyEnum :: Enumerator a
24+emptyEnum _ = return . Right
25+
26+-- | Given a certain requested path, return either a corrected path
27+-- to redirect to or the tokenized path.
28+--
29+-- This code corrects for the following issues:
30+--
31+-- * It is missing a trailing slash, and there is no period after the
32+-- last slash.
33+--
34+-- * There are any doubled slashes.
35+splitPath :: B.ByteString -> Either B.ByteString [B.ByteString]
36+splitPath s =
37+ let corrected = B.pack $ ats $ rds $ B.unpack s
38+ in if corrected == s
39+ then Right $ map decodeUrl
40+ $ filter (not . B.null)
41+ $ B.split '/' s
42+ else Left corrected
43+
44+-- | Remove double slashes
45+rds :: String -> String
46+rds [] = []
47+rds [x] = [x]
48+rds (a:b:c)
49+ | a == '/' && b == '/' = rds (b:c)
50+ | otherwise = a : rds (b:c)
51+
52+-- | Add a trailing slash if it is missing. Empty string is left alone.
53+ats :: String -> String
54+ats [] = []
55+ats s =
56+ if last s == '/' || dbs (reverse s)
57+ then s
58+ else s ++ "/"
59+
60+-- | Is there a period before a slash here?
61+dbs :: String -> Bool
62+dbs ('/':_) = False
63+dbs ('.':_) = True
64+dbs (_:x) = dbs x
65+dbs [] = False
Network/Wai/Middleware/ClientSession.hsnew file+136
@@ -0,0 +1,136 @@
1+module Network.Wai.Middleware.ClientSession
2+ ( clientsession
3+ -- * Generating keys
4+ , Word256
5+ , defaultKeyFile
6+ , getKey
7+ , getDefaultKey
8+ ) where
9+
10+import Prelude hiding (exp)
11+import Network.Wai
12+import Web.Encodings
13+import Data.List (partition)
14+import Data.Function.Predicate (is, isn't, equals)
15+import Data.Maybe (fromMaybe, mapMaybe)
16+import Web.ClientSession
17+import Data.Time.Clock (getCurrentTime, UTCTime, addUTCTime)
18+import Data.Time.LocalTime () -- Show instance of UTCTime
19+import Data.Time.Format (formatTime) -- Read instance of UTCTime
20+import System.Locale (defaultTimeLocale)
21+import Control.Monad (guard)
22+import qualified Data.ByteString.Char8 as B
23+import Control.Arrow (first)
24+
25+-- | Automatic encrypting and decrypting of client session data.
26+--
27+-- Using the clientsession package, this middleware handles automatic
28+-- encryption, decryption, checking, expiration and renewal of whichever
29+-- cookies you ask it to. For example, if you tell it to deal with the
30+-- cookie \"IDENTIFIER\", it will do the following:
31+--
32+-- * When you specify an \"IDENTIFIER\" value in your 'Response', it will
33+-- encrypt the value, along with the session expiration date and the
34+-- REMOTE_HOST of the user. It will then be set as a cookie on the client.
35+--
36+-- * When there is an incoming \"IDENTIFIER\" cookie from the user, it will
37+-- decrypt it and check both the expiration date and the REMOTE_HOST. If
38+-- everything matches up, it will set the \"IDENTIFIER\" value in
39+-- 'hackHeaders'.
40+--
41+-- * If the client sent an \"IDENTIFIER\" and the application does not set
42+-- a new value, this will reset the cookie to a new expiration date. This
43+-- way, you do not have sessions timing out every 20 minutes.
44+--
45+-- As far as security: clientsesion itself handles hashing and encrypting
46+-- the data to make sure that the user can neither see not tamper with it.
47+clientsession :: [B.ByteString] -- ^ list of cookies to intercept
48+ -> Word256 -- ^ encryption key
49+ -> Int -- ^ minutes to live
50+ -> ([(B.ByteString, B.ByteString)] -> Application)
51+ -> Request
52+ -> IO Response
53+clientsession cnames key minutesToLive app env = do
54+ let hs = httpHeaders env
55+ initCookiesRaw :: B.ByteString
56+ initCookiesRaw = fromMaybe B.empty $ lookup Cookie hs
57+ nonCookies :: [(RequestHeader, B.ByteString)]
58+ nonCookies = filter (fst `isn't` (== Cookie)) hs
59+ initCookies :: [(B.ByteString, B.ByteString)]
60+ initCookies = decodeCookies initCookiesRaw
61+ cookies, interceptCookies :: [(B.ByteString, B.ByteString)]
62+ (interceptCookies, cookies) = partition (fst `is` (`elem` cnames))
63+ initCookies
64+ cookiesRaw, remoteHost' :: B.ByteString
65+ cookiesRaw = B.concat $ combineCookies cookies
66+ remoteHost' = remoteHost env
67+ now <- getCurrentTime
68+ let convertedCookies :: [(B.ByteString, B.ByteString)]
69+ convertedCookies =
70+ mapMaybe (decodeCookie key now remoteHost') interceptCookies
71+ let env' = env { httpHeaders =
72+ (Cookie, cookiesRaw)
73+ -- FIXME not sure how I feel about the next line
74+ : filter (fst `equals` Cookie) (httpHeaders env)
75+ ++ nonCookies
76+ }
77+ res <- app convertedCookies env'
78+ let interceptHeaders, headers' :: [(ResponseHeader, B.ByteString)]
79+ (interceptHeaders, headers') =
80+ partition ((responseHeaderToBS . fst) `is` (`elem` cnames))
81+ $ headers res
82+ interceptHeaders' :: [(B.ByteString, B.ByteString)]
83+ interceptHeaders' = map (first responseHeaderToBS) interceptHeaders
84+ let timeToLive :: Int
85+ timeToLive = minutesToLive * 60
86+ let exp = fromIntegral timeToLive `addUTCTime` now
87+ let formattedExp = B.pack $ formatTime defaultTimeLocale "%a, %d-%b-%Y %X %Z" exp
88+ let oldCookies :: [(B.ByteString, B.ByteString)]
89+ oldCookies = filter
90+ (\(k, _) -> k `notElem` map fst interceptHeaders')
91+ convertedCookies
92+ let newCookies = map (setCookie key exp formattedExp remoteHost') $
93+ oldCookies ++ interceptHeaders'
94+ let res' = res { headers = newCookies ++ headers' }
95+ return res'
96+
97+combineCookies :: [(B.ByteString, B.ByteString)] -> [B.ByteString]
98+combineCookies [] = []
99+combineCookies ((k, v):rest) = k : B.singleton '=' : v : B.pack "; "
100+ : combineCookies rest
101+setCookie :: Word256
102+ -> UTCTime -- ^ expiration time
103+ -> B.ByteString -- ^ formatted expiration time
104+ -> B.ByteString -- ^ remote host
105+ -> (B.ByteString, B.ByteString)
106+ -> (ResponseHeader, B.ByteString)
107+setCookie key exp fexp rhost (cname, cval) =
108+ (SetCookie, B.concat
109+ [ cname
110+ , B.singleton '='
111+ , B.pack $ encrypt key $ B.pack $ show $ ACookie exp rhost cval
112+ , B.pack "; path=/; expires="
113+ , fexp
114+ ])
115+
116+data ACookie = ACookie UTCTime B.ByteString B.ByteString
117+ deriving (Show, Read)
118+
119+decodeCookie :: Word256 -- ^ key
120+ -> UTCTime -- ^ current time
121+ -> B.ByteString -- ^ remote host field
122+ -> (B.ByteString, B.ByteString) -- ^ cookie pair
123+ -> Maybe (B.ByteString, B.ByteString)
124+decodeCookie key now rhost (cname, encrypted) = do
125+ decrypted <- decrypt key $ B.unpack encrypted
126+ (ACookie exp rhost' val) <- mread $ B.unpack decrypted
127+ guard $ exp > now
128+ guard $ rhost' == rhost
129+ guard $ not $ B.null val
130+ return (cname, val)
131+
132+mread :: (Monad m, Read a) => String -> m a
133+mread s =
134+ case reads s of
135+ [] -> fail $ "Reading of " ++ s ++ " failed"
136+ ((x, _):_) -> return x
Network/Wai/Middleware/Gzip.hsnew file+63
@@ -0,0 +1,63 @@
1+{-# LANGUAGE Rank2Types #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+---------------------------------------------------------
4+-- |
5+-- Module : Network.Wai.Middleware.Gzip
6+-- Copyright : Michael Snoyman
7+-- License : BSD3
8+--
9+-- Maintainer : Michael Snoyman <michael@snoyman.com>
10+-- Stability : Unstable
11+-- Portability : portable
12+--
13+-- Automatic gzip compression of responses.
14+--
15+---------------------------------------------------------
16+module Network.Wai.Middleware.Gzip (gzip) where
17+
18+import Network.Wai
19+import Network.Wai.Enumerator (mapE)
20+import Codec.Compression.GZip (compress)
21+import Data.Maybe (fromMaybe)
22+import Data.List.Split (splitOneOf)
23+import qualified Data.ByteString.Char8 as B
24+import qualified Data.ByteString.Lazy as L
25+
26+-- | Use gzip to compress the body of the response.
27+--
28+-- Analyzes the \"Accept-Encoding\" header from the client to determine
29+-- if gzip is supported.
30+--
31+-- Possible future enhancements:
32+--
33+-- * Only compress if the response is above a certain size.
34+--
35+-- * Add Content-Length.
36+--
37+-- * I read somewhere that \"the beast\" (MSIE) can\'t support compression
38+-- for Javascript files..
39+gzip :: Middleware
40+gzip app env = do
41+ res <- app env
42+ case body res of
43+ Left _ -> return res
44+ Right _ -> do
45+ let enc = fromMaybe []
46+ $ (splitOneOf "," . B.unpack)
47+ `fmap` lookup AcceptEncoding
48+ (httpHeaders env)
49+ if "gzip" `elem` enc
50+ then return res
51+ { body = compressE $ body res
52+ , headers = (ContentEncoding, B.pack "gzip")
53+ : headers res
54+ }
55+ else return res
56+
57+compressE :: Either FilePath (Enumerator a) -> Either FilePath (Enumerator a)
58+compressE = either (Left . id) (Right . mapE compress')
59+
60+-- | Note: this might lose a lot of the compression power by dealing with
61+-- smaller chunks, I'm not certain.
62+compress' :: B.ByteString -> B.ByteString
63+compress' bs = B.concat $ L.toChunks $ compress $ L.fromChunks [bs]
Network/Wai/Middleware/Jsonp.hsnew file+71
@@ -0,0 +1,71 @@
1+---------------------------------------------------------
2+-- |
3+-- Module : Network.Wai.Middleware.Jsonp
4+-- Copyright : Michael Snoyman
5+-- License : BSD3
6+--
7+-- Maintainer : Michael Snoyman <michael@snoyman.com>
8+-- Stability : Unstable
9+-- Portability : portable
10+--
11+-- Automatic wrapping of JSON responses to convert into JSONP.
12+--
13+---------------------------------------------------------
14+module Network.Wai.Middleware.Jsonp (jsonp) where
15+
16+import Network.Wai
17+import Network.Wai.Enumerator (fromEitherFile)
18+import Web.Encodings (decodeUrlPairs)
19+import qualified Data.ByteString.Char8 as B8
20+import Data.Maybe (fromMaybe)
21+
22+-- | Wrap json responses in a jsonp callback.
23+--
24+-- Basically, if the user requested a \"text\/javascript\" and supplied a
25+-- \"callback\" GET parameter, ask the application for an
26+-- \"application/json\" response, then convern that into a JSONP response,
27+-- having a content type of \"text\/javascript\" and calling the specified
28+-- callback function.
29+jsonp :: Middleware
30+jsonp app env = do
31+ let accept = fromMaybe B8.empty $ lookup Accept $ httpHeaders env
32+ let gets = decodeUrlPairs $ queryString env
33+ let callback :: Maybe B8.ByteString
34+ callback =
35+ if B8.pack "text/javascript" `B8.isInfixOf` accept
36+ then lookup (B8.pack "callback") gets
37+ else Nothing
38+ let env' =
39+ case callback of
40+ Nothing -> env
41+ Just _ -> env
42+ { httpHeaders = changeVal Accept
43+ "application/json"
44+ $ httpHeaders env
45+ }
46+ res <- app env'
47+ case (fmap B8.unpack $ lookup ContentType $ headers res, callback) of
48+ (Just "application/json", Just c) -> return $ res
49+ { headers = changeVal ContentType "text/javascript" $ headers res
50+ , body = Right $ addCallback c $ fromEitherFile $ body res
51+ }
52+ _ -> return res
53+
54+addCallback :: B8.ByteString -> Enumerator a -> Enumerator a
55+addCallback cb e iter a = do
56+ ea' <- iter a $ B8.snoc cb '('
57+ case ea' of
58+ Left a' -> return $ Left a'
59+ Right a' -> do
60+ ea'' <- e iter a'
61+ case ea'' of
62+ Left a'' -> return $ Left a''
63+ Right a'' -> iter a'' $ B8.singleton ')'
64+
65+changeVal :: Eq a
66+ => a
67+ -> String
68+ -> [(a, B8.ByteString)]
69+ -> [(a, B8.ByteString)]
70+changeVal key val old = (key, B8.pack val)
71+ : filter (\(k, _) -> k /= key) old
Network/Wai/Middleware/MethodOverride.hsnew file+38
@@ -0,0 +1,38 @@
1+---------------------------------------------------------
2+-- |
3+-- Module : Network.Wai.Middleware.MethodOverride
4+-- Copyright : Michael Snoyman
5+-- License : BSD3
6+--
7+-- Maintainer : Michael Snoyman <michael@snoyman.com>
8+-- Stability : Unstable
9+-- Portability : portable
10+--
11+-- Override the HTTP method based on either:
12+-- The X-HTTP-Method-Override header.
13+-- The _method_override GET parameter.
14+--
15+---------------------------------------------------------
16+module Network.Wai.Middleware.MethodOverride (methodOverride) where
17+
18+import Network.Wai
19+import Web.Encodings (decodeUrlPairs)
20+import Data.Monoid (mappend)
21+import Data.Char
22+import qualified Data.ByteString.Char8 as B8
23+
24+moHeader :: RequestHeader
25+moHeader = requestHeaderFromBS $ B8.pack "X-HTTP-Method-Override"
26+
27+moParam :: B8.ByteString
28+moParam = B8.pack "_method_override"
29+
30+methodOverride :: Middleware
31+methodOverride app env = do
32+ let mo1 = lookup moHeader $ httpHeaders env
33+ gets = decodeUrlPairs $ queryString env
34+ mo2 = lookup moParam gets
35+ app $
36+ case mo1 `mappend` mo2 of
37+ Nothing -> env
38+ Just nm -> env { requestMethod = methodFromBS $ B8.map toUpper nm }
wai-extra.cabalnew file+37
@@ -0,0 +1,37 @@
1+Name: wai-extra
2+Version: 0.0.0
3+Synopsis: Provides some basic WAI handlers and middleware.
4+Description: The goal here is to provide common features without many dependencies.
5+License: BSD3
6+License-file: LICENSE
7+Author: Michael Snoyman
8+Maintainer: michael@snoyman.com
9+Homepage: http://github.com/snoyberg/wai-extra
10+Category: Web
11+Build-Type: Simple
12+Cabal-Version: >=1.2
13+Stability: Stable
14+
15+Library
16+ Build-Depends: base >= 3 && < 5,
17+ bytestring >= 0.9 && < 0.10,
18+ wai >= 0.0.0 && < 0.1,
19+ split >= 0.1.2 && < 0.2,
20+ web-encodings >= 0.2.2 && < 0.3,
21+ old-locale >= 1.0 && < 1.1,
22+ time >= 1.1.4 && < 1.2,
23+ clientsession >= 0.2.0 && < 0.3,
24+ predicates >= 0.1 && < 0.2,
25+ zlib >= 0.5.2.0 && < 0.6,
26+ sendfile >= 0.6.1 && < 0.7,
27+ safe >= 0.2 && < 0.3,
28+ failure >= 0.0.0.2 && < 0.1,
29+ network >= 2.2.1.5 && < 2.3
30+ Exposed-modules: Network.Wai.Handler.CGI
31+ Network.Wai.Handler.SimpleServer
32+ Network.Wai.Middleware.CleanPath
33+ Network.Wai.Middleware.ClientSession
34+ Network.Wai.Middleware.Gzip
35+ Network.Wai.Middleware.Jsonp
36+ Network.Wai.Middleware.MethodOverride
37+ ghc-options: -Wall -Werror