on master help sign in
implementing conditional request (#359)
commit69b8aabaa47f34e8e2d98729e0d498935f572ca6 authorKazu Yamamoto <kazu@iij.ad.jp> dateFri 4 Dec 2015, 12:09 (11 years ago) parent92b4d47
5 files changed+274−124
warp/Network/Wai/Handler/Warp/File.hs+157−33 warp/Network/Wai/Handler/Warp/Response.hs+73−45 warp/test/FileSpec.hs+42 warp/test/ResponseSpec.hs+1−46 warp/warp.cabal+1
warp/Network/Wai/Handler/Warp/File.hs+157−33
@@ -1,47 +1,171 @@
11{-# LANGUAGE OverloadedStrings #-}
2−{-# LANGUAGE ScopedTypeVariables #-}
3−{-# LANGUAGE TupleSections #-}
4−{-# LANGUAGE RankNTypes #-}
5−{-# LANGUAGE CPP #-}
2+{-# LANGUAGE BangPatterns #-}
63
7−module Network.Wai.Handler.Warp.File where
4+module Network.Wai.Handler.Warp.File (
5+ RspFileInfo(..)
6+ , conditionalRequest
7+ , addContentHeadersForFilePart
8+ ) where
89
9−#if __GLASGOW_HASKELL__ < 709
10−import Control.Applicative
11−#endif
12−import Control.Exception
10+import Control.Applicative ((<|>))
11+import Data.Array ((!))
12+import Data.ByteString (ByteString)
13+import qualified Data.ByteString as B hiding (pack)
14+import qualified Data.ByteString.Builder as B
15+import qualified Data.ByteString.Char8 as B (pack, readInteger)
16+import qualified Data.ByteString.Lazy as L
1317import Data.Maybe
18+import Network.HTTP.Date
1419import qualified Network.HTTP.Types as H
20+import qualified Network.HTTP.Types.Header as H
1521import Network.Wai
1622import qualified Network.Wai.Handler.Warp.FileInfoCache as I
17−import Network.Wai.Handler.Warp.Types
18−import Network.Wai.Internal
23+import Network.Wai.Handler.Warp.Header
24+import Numeric (showInt)
1925
2026----------------------------------------------------------------
2127
22−fileRange :: H.Status -> H.ResponseHeaders -> FilePath
23− -> Maybe FilePart -> Maybe HeaderValue
24− -> (FilePath -> IO I.FileInfo)
25− -> IO (Either IOException
26− (H.Status, H.ResponseHeaders, Integer, Integer))
27−fileRange s0 hs0 path Nothing mRange get = do
28− efinfo <- try (get path)
29− case efinfo of
30− Left e -> return $ Left e
31− Right finfo -> do
32− let ret = fileRangeSized s0 hs0 Nothing mRange $ I.fileInfoSize finfo
33− return $ Right ret
34−fileRange s0 hs0 _ mPart@(Just part) mRange _ =
35− return . Right $ fileRangeSized s0 hs0 mPart mRange size
28+data RspFileInfo = WithoutBody H.Status
29+ | WithBody H.Status H.ResponseHeaders Integer Integer
30+ deriving (Eq,Show)
31+
32+----------------------------------------------------------------
33+
34+conditionalRequest :: I.FileInfo
35+ -> H.ResponseHeaders -> IndexedHeader
36+ -> RspFileInfo
37+conditionalRequest finfo hs0 reqidx = case fromJust mx of
38+ nobody@(WithoutBody _) -> nobody
39+ WithBody s _ off len -> let hs = addContentHeaders hs0 off len size
40+ in WithBody s hs off len
3641 where
37− size = filePartFileSize part
42+ mtime = I.fileInfoTime finfo
43+ size = I.fileInfoSize finfo
44+ mx = ifmodified reqidx size mtime
45+ <|> ifunmodified reqidx size mtime
46+ <|> ifrange reqidx size mtime
47+ <|> unconditional reqidx size mtime
48+
49+----------------------------------------------------------------
50+
51+ifModifiedSince :: IndexedHeader -> Maybe HTTPDate
52+ifModifiedSince reqidx = reqidx ! idxIfModifiedSince >>= parseHTTPDate
53+
54+ifUnmodifiedSince :: IndexedHeader -> Maybe HTTPDate
55+ifUnmodifiedSince reqidx = reqidx ! idxIfUnmodifiedSince >>= parseHTTPDate
56+
57+ifRange :: IndexedHeader -> Maybe HTTPDate
58+ifRange reqidx = reqidx ! idxIfRange >>= parseHTTPDate
59+
60+----------------------------------------------------------------
61+
62+ifmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
63+ifmodified reqidx size mtime = do
64+ date <- ifModifiedSince reqidx
65+ if date /= mtime
66+ then unconditional reqidx size mtime
67+ else Just $ WithoutBody H.notModified304
68+
69+ifunmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
70+ifunmodified reqidx size mtime = do
71+ date <- ifUnmodifiedSince reqidx
72+ if date == mtime
73+ then unconditional reqidx size mtime
74+ else Just $ WithoutBody H.preconditionFailed412
75+
76+ifrange :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
77+ifrange reqidx size mtime = do
78+ date <- ifRange reqidx
79+ rng <- reqidx ! idxRange
80+ if date == mtime
81+ then parseRange rng size
82+ else Just $ WithBody H.ok200 [] 0 size
83+
84+unconditional :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
85+unconditional reqidx size _ = case reqidx ! idxRange of
86+ Nothing -> Just $ WithBody H.ok200 [] 0 size
87+ Just rng -> parseRange rng size
3888
39−fileRangeSized :: H.Status -> H.ResponseHeaders
40− -> Maybe FilePart -> Maybe HeaderValue -> Integer
41− -> (H.Status, H.ResponseHeaders, Integer, Integer)
42−fileRangeSized s0 hs0 mPart mRange fileSize = (s, hs, beg, len)
89+----------------------------------------------------------------
90+
91+parseRange :: ByteString -> Integer -> Maybe RspFileInfo
92+parseRange rng size = case parseByteRanges rng of
93+ Nothing -> Just $ WithoutBody H.requestedRangeNotSatisfiable416
94+ Just [] -> Just $ WithoutBody H.requestedRangeNotSatisfiable416
95+ Just (r:_) -> let (!beg, !end) = checkRange r size
96+ !len = end - beg + 1
97+ s = if beg == 0 && end == size - 1 then
98+ H.ok200
99+ else
100+ H.partialContent206
101+ in Just $ WithBody s [] beg len
102+
103+checkRange :: H.ByteRange -> Integer -> (Integer, Integer)
104+checkRange (H.ByteRangeFrom beg) size = (beg, size - 1)
105+checkRange (H.ByteRangeFromTo beg end) size = (beg, min (size - 1) end)
106+checkRange (H.ByteRangeSuffix count) size = (max 0 (size - count), size - 1)
107+
108+-- | Parse the value of a Range header into a 'H.ByteRanges'.
109+parseByteRanges :: B.ByteString -> Maybe H.ByteRanges
110+parseByteRanges bs1 = do
111+ bs2 <- stripPrefix "bytes=" bs1
112+ (r, bs3) <- range bs2
113+ ranges (r:) bs3
114+ where
115+ range bs2 = do
116+ (i, bs3) <- B.readInteger bs2
117+ if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-")
118+ then Just (H.ByteRangeSuffix (negate i), bs3)
119+ else do
120+ bs4 <- stripPrefix "-" bs3
121+ case B.readInteger bs4 of
122+ Just (j, bs5) | j >= i -> Just (H.ByteRangeFromTo i j, bs5)
123+ _ -> Just (H.ByteRangeFrom i, bs4)
124+ ranges front bs3
125+ | B.null bs3 = Just (front [])
126+ | otherwise = do
127+ bs4 <- stripPrefix "," bs3
128+ (r, bs5) <- range bs4
129+ ranges (front . (r:)) bs5
130+
131+ stripPrefix x y
132+ | x `B.isPrefixOf` y = Just (B.drop (B.length x) y)
133+ | otherwise = Nothing
134+
135+----------------------------------------------------------------
136+
137+-- | @contentRangeHeader beg end total@ constructs a Content-Range 'H.Header'
138+-- for the range specified.
139+contentRangeHeader :: Integer -> Integer -> Integer -> H.Header
140+contentRangeHeader beg end total = (H.hContentRange, range)
141+ where
142+ range = B.pack
143+ -- building with ShowS
144+ $ 'b' : 'y': 't' : 'e' : 's' : ' '
145+ : (if beg > end then ('*':) else
146+ showInt beg
147+ . ('-' :)
148+ . showInt end)
149+ ( '/'
150+ : showInt total "")
151+
152+addContentHeaders :: H.ResponseHeaders -> Integer -> Integer -> Integer -> H.ResponseHeaders
153+addContentHeaders hs off len size = hs''
43154 where
44− part = fromMaybe (chooseFilePart fileSize mRange) mPart
45− beg = filePartOffset part
155+ contentRange = contentRangeHeader off (off + len - 1) size
156+ lengthBS = L.toStrict $ B.toLazyByteString $ B.integerDec len
157+ hs' = (H.hContentLength, lengthBS):(H.hAcceptRanges, "bytes"):hs
158+ hs'' = if len == size then hs' else contentRange:hs'
159+
160+-- |
161+--
162+-- >>> addContentHeadersForFilePart [] (FilePart 2 10 16)
163+-- [("Content-Range","bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")]
164+-- >>> addContentHeadersForFilePart [] (FilePart 0 16 16)
165+-- [("Content-Length","16"),("Accept-Ranges","bytes")]
166+addContentHeadersForFilePart :: H.ResponseHeaders -> FilePart -> H.ResponseHeaders
167+addContentHeadersForFilePart hs part = addContentHeaders hs off len size
168+ where
169+ off = filePartOffset part
46170 len = filePartByteCount part
47− (s, hs) = adjustForFilePart s0 hs0 $ FilePart beg len fileSize
171+ size = filePartFileSize part
warp/Network/Wai/Handler/Warp/Response.hs+73−45
@@ -7,7 +7,6 @@
77module Network.Wai.Handler.Warp.Response (
88 sendResponse
99 , sanitizeHeaderValue -- for testing
10− , fileRange -- for testing
1110 , warpVersion
1211 , addDate
1312 , addServer
@@ -26,6 +25,7 @@ import Blaze.ByteString.Builder.HTTP (chunkedTransferEncoding, chunkedTransferTe
2625#if __GLASGOW_HASKELL__ < 709
2726import Control.Applicative
2827#endif
28+import qualified Control.Exception as E
2929import Control.Monad (unless, when)
3030import Data.Array ((!))
3131import Data.ByteString (ByteString)
@@ -159,13 +159,12 @@ sendResponse settings conn ii req reqidxhdr src response = do
159159 th = threadHandle ii
160160 dc = dateCacher ii
161161 addServerAndDate = addDate dc rspidxhdr . addServer defServer rspidxhdr
162− mRange = reqidxhdr ! idxRange
163162 (isPersist,isChunked0) = infoFromRequest req reqidxhdr
164163 isChunked = not isHead && isChunked0
165164 (isKeepAlive, needsChunked) = infoFromResponse rspidxhdr (isPersist,isChunked)
166165 isHead = requestMethod req == H.methodHead
167166 rsp = case response of
168− ResponseFile _ _ path mPart -> RspFile path mPart mRange isHead (T.tickle th)
167+ ResponseFile _ _ path mPart -> RspFile path mPart reqidxhdr isHead (T.tickle th)
169168 ResponseBuilder _ _ b
170169 | isHead -> RspNoBody
171170 | otherwise -> RspBuilder b needsChunked
@@ -207,7 +206,7 @@ sanitizeHeaderValue v = case S8.lines $ S.filter (/= _cr) v of
207206----------------------------------------------------------------
208207
209208data Rsp = RspNoBody
210− | RspFile FilePath (Maybe FilePart) (Maybe HeaderValue) Bool (IO ())
209+ | RspFile FilePath (Maybe FilePart) IndexedHeader Bool (IO ())
211210 | RspBuilder Builder Bool
212211 | RspStream StreamingBody Bool T.Handle
213212 | RspRaw (IO ByteString -> (ByteString -> IO ()) -> IO ()) (IO ByteString) (IO ())
@@ -232,47 +231,6 @@ sendRsp conn _ ver s hs RspNoBody = do
232231
233232----------------------------------------------------------------
234233
235−sendRsp conn ii ver s0 hs0 (RspFile path mPart mRange isHead hook) = do
236− ex <- fileRange s0 hs0 path mPart mRange get
237− case ex of
238− Left _ex ->
239−#ifdef WARP_DEBUG
240− print _ex >>
241−#endif
242− sendRsp conn ii ver s2 hs2 (RspBuilder body True)
243− Right (s, hs, beg, len)
244− | len >= 0 ->
245− if isHead then
246− sendRsp conn ii ver s hs RspNoBody
247− else do
248− lheader <- composeHeader ver s hs
249−#ifdef WINDOWS
250− let fid = FileId path Nothing
251− hook' = hook
252−#else
253− (mfd, hook') <- case mfdc of
254− -- settingsFdCacheDuration is 0
255− Nothing -> return (Nothing, hook)
256− Just fdc -> do
257− (fd, fresher) <- F.getFd fdc path
258− return (Just fd, hook >> fresher)
259− let fid = FileId path mfd
260−#endif
261− connSendFile conn fid beg len hook' [lheader]
262− return (Just s, Just len)
263− | otherwise -> do
264− sendRsp conn ii ver H.requestedRangeNotSatisfiable416
265− (filter (\(k, _) -> k /= "content-length") hs)
266− (RspBuilder mempty True)
267− where
268− s2 = H.notFound404
269− hs2 = replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
270− body = byteString "File not found"
271− mfdc = fdCacher ii
272− get = fileInfo ii
273−
274−----------------------------------------------------------------
275−
276234sendRsp conn _ ver s hs (RspBuilder body needsChunked) = do
277235 header <- composeHeaderBuilder ver s hs needsChunked
278236 let hdrBdy
@@ -320,6 +278,76 @@ sendRsp conn _ _ _ _ (RspRaw withApp src tickle) = do
320278 return bs
321279 send bs = connSendAll conn bs >> tickle
322280
281+----------------------------------------------------------------
282+
283+-- Sophisticated WAI applications.
284+-- We respect s0. s0 MUST be a proper value of either 200 or 206.
285+sendRsp conn ii ver s0 hs0 (RspFile path (Just part) _ isHead hook) =
286+ sendRspFile2XX conn ii ver s0 hs path beg len isHead hook
287+ where
288+ beg = filePartOffset part
289+ len = filePartByteCount part
290+ hs = addContentHeadersForFilePart hs0 part
291+
292+----------------------------------------------------------------
293+
294+-- Simple WAI applications.
295+-- Status is ignored
296+sendRsp conn ii ver _ hs0 (RspFile path Nothing idxhdr isHead hook) = do
297+ efinfo <- E.try $ fileInfo ii path
298+ case efinfo of
299+ Left (_ex :: E.IOException) ->
300+#ifdef WARP_DEBUG
301+ print _ex >>
302+#endif
303+ sendRspFile404 conn ii ver hs0
304+ Right finfo -> case conditionalRequest finfo hs0 idxhdr of
305+ WithoutBody s -> sendRsp conn ii ver s hs0 RspNoBody
306+ WithBody s hs beg len -> sendRspFile2XX conn ii ver s hs path beg len isHead hook
307+
308+----------------------------------------------------------------
309+
310+sendRspFile2XX :: Connection
311+ -> InternalInfo
312+ -> H.HttpVersion
313+ -> H.Status
314+ -> H.ResponseHeaders
315+ -> FilePath
316+ -> Integer
317+ -> Integer
318+ -> Bool
319+ -> IO ()
320+ -> IO (Maybe H.Status, Maybe Integer)
321+sendRspFile2XX conn ii ver s hs path beg len isHead hook
322+ | isHead = sendRsp conn ii ver s hs RspNoBody
323+ | otherwise = do
324+ lheader <- composeHeader ver s hs
325+#ifdef WINDOWS
326+ let fid = FileId path Nothing
327+ hook' = hook
328+#else
329+ (mfd, hook') <- case fdCacher ii of
330+ -- settingsFdCacheDuration is 0
331+ Nothing -> return (Nothing, hook)
332+ Just fdc -> do
333+ (fd, fresher) <- F.getFd fdc path
334+ return (Just fd, hook >> fresher)
335+ let fid = FileId path mfd
336+#endif
337+ connSendFile conn fid beg len hook' [lheader]
338+ return (Just s, Just len)
339+
340+sendRspFile404 :: Connection
341+ -> InternalInfo
342+ -> H.HttpVersion
343+ -> H.ResponseHeaders
344+ -> IO (Maybe H.Status, Maybe Integer)
345+sendRspFile404 conn ii ver hs0 = sendRsp conn ii ver s hs (RspBuilder body True)
346+ where
347+ s = H.notFound404
348+ hs = replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
349+ body = byteString "File not found"
350+
323351----------------------------------------------------------------
324352----------------------------------------------------------------
325353
warp/test/FileSpec.hsnew file+42
@@ -0,0 +1,42 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module FileSpec (main, spec) where
4+
5+import Network.HTTP.Types
6+import Network.Wai.Handler.Warp.File
7+import Network.Wai.Handler.Warp.FileInfoCache
8+import Network.Wai.Handler.Warp.Header
9+
10+import Test.Hspec
11+
12+main :: IO ()
13+main = hspec spec
14+
15+testFileRange :: String
16+ -> RequestHeaders -> FilePath
17+ -> RspFileInfo
18+ -> Spec
19+testFileRange desc reqhs file ans = it desc $ do
20+ finfo <- getInfo file
21+ conditionalRequest finfo [] (indexRequestHeader reqhs) `shouldBe` ans
22+
23+spec :: Spec
24+spec = do
25+ describe "conditionalRequest" $ do
26+ testFileRange
27+ "gets a file size from file system"
28+ [] "attic/hex"
29+ $ WithBody ok200 [("Content-Length","16"),("Accept-Ranges","bytes")] 0 16
30+ testFileRange
31+ "gets a file size from file system and handles Range and returns Partical Content"
32+ [("Range","bytes=2-14")] "attic/hex"
33+ $ WithBody status206 [("Content-Range","bytes 2-14/16"),("Content-Length","13"),("Accept-Ranges","bytes")] 2 13
34+ testFileRange
35+ "truncates end point of range to file size"
36+ [("Range","bytes=10-20")] "attic/hex"
37+ $ WithBody status206 [("Content-Range","bytes 10-15/16"),("Content-Length","6"),("Accept-Ranges","bytes")] 10 6
38+ testFileRange
39+ "gets a file size from file system and handles Range and returns OK if Range means the entire"
40+ [("Range:","bytes=0-15")] "attic/hex"
41+ $ WithBody status200 [("Content-Length","16"),("Accept-Ranges","bytes")] 0 16
42+
warp/test/ResponseSpec.hs+1−46
@@ -1,4 +1,5 @@
11{-# LANGUAGE OverloadedStrings #-}
2+
23module ResponseSpec (main, spec) where
34
45import Control.Concurrent (threadDelay)
@@ -71,18 +72,6 @@ testPartial size offset count out = it title $ withApp defaultSettings app $ \po
7172 _ -> Nothing
7273 range = "bytes " ++ show offset ++ "-" ++ show (offset + count - 1) ++ "/" ++ show size
7374
74−testFileRange :: String
75− -> Status -> ResponseHeaders -> FilePath
76− -> Maybe FilePart -> Maybe HeaderValue
77− -> Either String (Status, ResponseHeaders, Integer, Integer)
78− -> Spec
79−testFileRange desc s rsphdr file mPart mRange ans = it desc $ do
80− eres <- fileRange s rsphdr file mPart mRange getInfo
81− let res = case eres of
82− Left e -> Left $ show e
83− Right r -> Right r
84− res `shouldBe` ans
85−
8675spec :: Spec
8776spec = do
8877 describe "preventing response splitting attack" $ do
@@ -125,37 +114,3 @@ spec = do
125114 testPartial 16 2 2 "23"
126115 testPartial 16 0 2 "01"
127116 testPartial 16 3 8 "3456789a"
128−
129− describe "fileRange" $ do
130− testFileRange
131− "gets a file size from file system"
132− status200 [] "attic/hex" Nothing Nothing
133− $ Right (status200,[("Content-Length","16"), ("Accept-Ranges","bytes")],0,16)
134− testFileRange
135− "gets an error if a file does not exist"
136− status200 [] "attic/nonexist" Nothing Nothing
137− $ Left "attic/nonexist: getFileStatus: does not exist (No such file or directory)"
138− testFileRange
139− "changes status if FileParts is specified"
140− status200 [] "attic/hex" (Just (FilePart 2 10 16)) Nothing
141− $ Right (status206,[("Content-Range", "bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")],2,10)
142− testFileRange
143− "does not change status and does not add Content-Range if FileParts means the entire"
144− status200 [] "attic/hex" (Just (FilePart 0 16 16)) Nothing
145− $ Right (status200,[("Content-Length","16"), ("Accept-Ranges","bytes")],0,16)
146− testFileRange
147− "gets a file size from file system and handles Range and returns Partical Content"
148− status200 [] "attic/hex" Nothing (Just "bytes=2-14")
149− $ Right (status206,[("Content-Range","bytes 2-14/16"),("Content-Length","13"),("Accept-Ranges","bytes")],2,13)
150− testFileRange
151− "truncates end point of range to file size"
152− status200 [] "attic/hex" Nothing (Just "bytes=10-20")
153− $ Right (status206,[("Content-Range","bytes 10-15/16"),("Content-Length","6"),("Accept-Ranges","bytes")],10,6)
154− testFileRange
155− "gets a file size from file system and handles Range and returns OK if Range means the entire"
156− status200 [] "attic/hex" Nothing (Just "bytes=0-15")
157− $ Right (status200,[("Content-Length","16"),("Accept-Ranges","bytes")],0,16)
158− testFileRange
159− "igores Range if FilePart is specified"
160− status200 [] "attic/hex" (Just (FilePart 2 10 16)) (Just "bytes=8-9")
161− $ Right (status206,[("Content-Range", "bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")],2,10)
warp/warp.cabal+1
@@ -121,6 +121,7 @@ Test-Suite spec
121121 ConduitSpec
122122 ExceptionSpec
123123 FdCacheSpec
124+ FileSpec
124125 MultiMapSpec
125126 ReadIntSpec
126127 RequestSpec