on master help sign in
Merge pull request #477 from yesodweb/conditional-requests

Conditional requests

commited7072df125e9ddbebd8ee35a9a3effdfea15eab authorMichael Snoyman <michael@snoyman.com> dateMon 7 Dec 2015, 08:01 (11 years ago) parent776a8dc7977dea
7 files changed+360−175
warp/Network/Wai/Handler/Warp/File.hs+174 warp/Network/Wai/Handler/Warp/Header.hs+31−25 warp/Network/Wai/Handler/Warp/Request.hs+5−5 warp/Network/Wai/Handler/Warp/Response.hs+102−98 warp/test/FileSpec.hs+45 warp/test/ResponseSpec.hs+1−47 warp/warp.cabal+2
warp/Network/Wai/Handler/Warp/File.hsnew file+174
@@ -0,0 +1,174 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE BangPatterns #-}
3+
4+module Network.Wai.Handler.Warp.File (
5+ RspFileInfo(..)
6+ , conditionalRequest
7+ , addContentHeadersForFilePart
8+ ) where
9+
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
17+import Network.HTTP.Date
18+import qualified Network.HTTP.Types as H
19+import qualified Network.HTTP.Types.Header as H
20+import Network.Wai
21+import qualified Network.Wai.Handler.Warp.FileInfoCache as I
22+import Network.Wai.Handler.Warp.Header
23+import Numeric (showInt)
24+
25+----------------------------------------------------------------
26+
27+data RspFileInfo = WithoutBody H.Status
28+ | WithBody H.Status H.ResponseHeaders Integer Integer
29+ deriving (Eq,Show)
30+
31+----------------------------------------------------------------
32+
33+conditionalRequest :: I.FileInfo
34+ -> H.ResponseHeaders -> IndexedHeader
35+ -> RspFileInfo
36+conditionalRequest finfo hs0 reqidx = case condition of
37+ nobody@(WithoutBody _) -> nobody
38+ WithBody s _ off len -> let hs = (H.hLastModified,date) :
39+ addContentHeaders hs0 off len size
40+ in WithBody s hs off len
41+ where
42+ mtime = I.fileInfoTime finfo
43+ size = I.fileInfoSize finfo
44+ date = I.fileInfoDate finfo
45+ mcondition = ifmodified reqidx size mtime
46+ <|> ifunmodified reqidx size mtime
47+ <|> ifrange reqidx size mtime
48+ condition = case mcondition of
49+ Nothing -> unconditional reqidx size
50+ Just x -> x
51+
52+----------------------------------------------------------------
53+
54+ifModifiedSince :: IndexedHeader -> Maybe HTTPDate
55+ifModifiedSince reqidx = reqidx ! fromEnum ReqIfModifiedSince >>= parseHTTPDate
56+
57+ifUnmodifiedSince :: IndexedHeader -> Maybe HTTPDate
58+ifUnmodifiedSince reqidx = reqidx ! fromEnum ReqIfUnmodifiedSince >>= parseHTTPDate
59+
60+ifRange :: IndexedHeader -> Maybe HTTPDate
61+ifRange reqidx = reqidx ! fromEnum ReqIfRange >>= parseHTTPDate
62+
63+----------------------------------------------------------------
64+
65+ifmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
66+ifmodified reqidx size mtime = do
67+ date <- ifModifiedSince reqidx
68+ return $ if date /= mtime
69+ then unconditional reqidx size
70+ else WithoutBody H.notModified304
71+
72+ifunmodified :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
73+ifunmodified reqidx size mtime = do
74+ date <- ifUnmodifiedSince reqidx
75+ return $ if date == mtime
76+ then unconditional reqidx size
77+ else WithoutBody H.preconditionFailed412
78+
79+ifrange :: IndexedHeader -> Integer -> HTTPDate -> Maybe RspFileInfo
80+ifrange reqidx size mtime = do
81+ date <- ifRange reqidx
82+ rng <- reqidx ! fromEnum ReqRange
83+ return $ if date == mtime
84+ then parseRange rng size
85+ else WithBody H.ok200 [] 0 size
86+
87+unconditional :: IndexedHeader -> Integer -> RspFileInfo
88+unconditional reqidx size = case reqidx ! fromEnum ReqRange of
89+ Nothing -> WithBody H.ok200 [] 0 size
90+ Just rng -> parseRange rng size
91+
92+----------------------------------------------------------------
93+
94+parseRange :: ByteString -> Integer -> RspFileInfo
95+parseRange rng size = case parseByteRanges rng of
96+ Nothing -> WithoutBody H.requestedRangeNotSatisfiable416
97+ Just [] -> WithoutBody H.requestedRangeNotSatisfiable416
98+ Just (r:_) -> let (!beg, !end) = checkRange r size
99+ !len = end - beg + 1
100+ s = if beg == 0 && end == size - 1 then
101+ H.ok200
102+ else
103+ H.partialContent206
104+ in WithBody s [] beg len
105+
106+checkRange :: H.ByteRange -> Integer -> (Integer, Integer)
107+checkRange (H.ByteRangeFrom beg) size = (beg, size - 1)
108+checkRange (H.ByteRangeFromTo beg end) size = (beg, min (size - 1) end)
109+checkRange (H.ByteRangeSuffix count) size = (max 0 (size - count), size - 1)
110+
111+-- | Parse the value of a Range header into a 'H.ByteRanges'.
112+parseByteRanges :: B.ByteString -> Maybe H.ByteRanges
113+parseByteRanges bs1 = do
114+ bs2 <- stripPrefix "bytes=" bs1
115+ (r, bs3) <- range bs2
116+ ranges (r:) bs3
117+ where
118+ range bs2 = do
119+ (i, bs3) <- B.readInteger bs2
120+ if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-")
121+ then Just (H.ByteRangeSuffix (negate i), bs3)
122+ else do
123+ bs4 <- stripPrefix "-" bs3
124+ case B.readInteger bs4 of
125+ Just (j, bs5) | j >= i -> Just (H.ByteRangeFromTo i j, bs5)
126+ _ -> Just (H.ByteRangeFrom i, bs4)
127+ ranges front bs3
128+ | B.null bs3 = Just (front [])
129+ | otherwise = do
130+ bs4 <- stripPrefix "," bs3
131+ (r, bs5) <- range bs4
132+ ranges (front . (r:)) bs5
133+
134+ stripPrefix x y
135+ | x `B.isPrefixOf` y = Just (B.drop (B.length x) y)
136+ | otherwise = Nothing
137+
138+----------------------------------------------------------------
139+
140+-- | @contentRangeHeader beg end total@ constructs a Content-Range 'H.Header'
141+-- for the range specified.
142+contentRangeHeader :: Integer -> Integer -> Integer -> H.Header
143+contentRangeHeader beg end total = (H.hContentRange, range)
144+ where
145+ range = B.pack
146+ -- building with ShowS
147+ $ 'b' : 'y': 't' : 'e' : 's' : ' '
148+ : (if beg > end then ('*':) else
149+ showInt beg
150+ . ('-' :)
151+ . showInt end)
152+ ( '/'
153+ : showInt total "")
154+
155+addContentHeaders :: H.ResponseHeaders -> Integer -> Integer -> Integer -> H.ResponseHeaders
156+addContentHeaders hs off len size = hs''
157+ where
158+ contentRange = contentRangeHeader off (off + len - 1) size
159+ lengthBS = L.toStrict $ B.toLazyByteString $ B.integerDec len
160+ hs' = (H.hContentLength, lengthBS):(H.hAcceptRanges, "bytes"):hs
161+ hs'' = if len == size then hs' else contentRange:hs'
162+
163+-- |
164+--
165+-- >>> addContentHeadersForFilePart [] (FilePart 2 10 16)
166+-- [("Content-Range","bytes 2-11/16"),("Content-Length","10"),("Accept-Ranges","bytes")]
167+-- >>> addContentHeadersForFilePart [] (FilePart 0 16 16)
168+-- [("Content-Length","16"),("Accept-Ranges","bytes")]
169+addContentHeadersForFilePart :: H.ResponseHeaders -> FilePart -> H.ResponseHeaders
170+addContentHeadersForFilePart hs part = addContentHeaders hs off len size
171+ where
172+ off = filePartOffset part
173+ len = filePartByteCount part
174+ size = filePartFileSize part
warp/Network/Wai/Handler/Warp/Header.hs+31−25
@@ -17,29 +17,35 @@ type IndexedHeader = Array Int (Maybe HeaderValue)
1717indexRequestHeader :: RequestHeaders -> IndexedHeader
1818indexRequestHeader hdr = traverseHeader hdr requestMaxIndex requestKeyIndex
1919
20−idxContentLength,idxTransferEncoding,idxExpect :: Int
21−idxConnection,idxRange,idxHost :: Int
22−idxContentLength = 0
23−idxTransferEncoding = 1
24−idxExpect = 2
25−idxConnection = 3
26−idxRange = 4
27−idxHost = 5
20+data RequestHeaderIndex = ReqContentLength
21+ | ReqTransferEncoding
22+ | ReqExpect
23+ | ReqConnection
24+ | ReqRange
25+ | ReqHost
26+ | ReqIfModifiedSince
27+ | ReqIfUnmodifiedSince
28+ | ReqIfRange
29+ deriving (Enum,Bounded)
2830
2931-- | The size for 'IndexedHeader' for HTTP Request.
3032-- From 0 to this corresponds to \"Content-Length\", \"Transfer-Encoding\",
31−-- \"Expect\", \"Connection\", \"Range\", and \"Host\".
33+-- \"Expect\", \"Connection\", \"Range\", \"Host\",
34+-- \"If-Modified-Since\", \"If-Unmodified-Since\" and \"If-Range\".
3235requestMaxIndex :: Int
33−requestMaxIndex = 5
36+requestMaxIndex = fromEnum (maxBound :: RequestHeaderIndex)
3437
3538requestKeyIndex :: HeaderName -> Int
36−requestKeyIndex "content-length" = idxContentLength
37−requestKeyIndex "transfer-encoding" = idxTransferEncoding
38−requestKeyIndex "expect" = idxExpect
39−requestKeyIndex "connection" = idxConnection
40−requestKeyIndex "range" = idxRange
41−requestKeyIndex "host" = idxHost
42−requestKeyIndex _ = -1
39+requestKeyIndex "content-length" = fromEnum ReqContentLength
40+requestKeyIndex "transfer-encoding" = fromEnum ReqTransferEncoding
41+requestKeyIndex "expect" = fromEnum ReqExpect
42+requestKeyIndex "connection" = fromEnum ReqConnection
43+requestKeyIndex "range" = fromEnum ReqRange
44+requestKeyIndex "host" = fromEnum ReqHost
45+requestKeyIndex "if-modified-since" = fromEnum ReqIfModifiedSince
46+requestKeyIndex "if-unmodified-since" = fromEnum ReqIfUnmodifiedSince
47+requestKeyIndex "if-range" = fromEnum ReqIfRange
48+requestKeyIndex _ = -1
4349
4450defaultIndexRequestHeader :: IndexedHeader
4551defaultIndexRequestHeader = array (0,requestMaxIndex) [(i,Nothing)|i<-[0..requestMaxIndex]]
@@ -49,19 +55,19 @@ defaultIndexRequestHeader = array (0,requestMaxIndex) [(i,Nothing)|i<-[0..reques
4955indexResponseHeader :: ResponseHeaders -> IndexedHeader
5056indexResponseHeader hdr = traverseHeader hdr responseMaxIndex responseKeyIndex
5157
52−idxServer, idxDate :: Int
53−--idxContentLength = 0
54−idxServer = 1
55−idxDate = 2
58+data ResponseHeaderIndex = ResContentLength
59+ | ResServer
60+ | ResDate
61+ deriving (Enum,Bounded)
5662
5763-- | The size for 'IndexedHeader' for HTTP Response.
5864responseMaxIndex :: Int
59−responseMaxIndex = 2
65+responseMaxIndex = fromEnum (maxBound :: ResponseHeaderIndex)
6066
6167responseKeyIndex :: HeaderName -> Int
62−responseKeyIndex "content-length" = idxContentLength
63−responseKeyIndex "server" = idxServer
64−responseKeyIndex "date" = idxDate
68+responseKeyIndex "content-length" = fromEnum ResContentLength
69+responseKeyIndex "server" = fromEnum ResServer
70+responseKeyIndex "date" = fromEnum ResDate
6571responseKeyIndex _ = -1
6672
6773----------------------------------------------------------------
warp/Network/Wai/Handler/Warp/Request.hs+5−5
@@ -63,9 +63,9 @@ recvRequest settings conn ii addr src = do
6363 hdrlines <- headerLines src
6464 (method, unparsedPath, path, query, httpversion, hdr) <- parseHeaderLines hdrlines
6565 let idxhdr = indexRequestHeader hdr
66− expect = idxhdr ! idxExpect
67− cl = idxhdr ! idxContentLength
68− te = idxhdr ! idxTransferEncoding
66+ expect = idxhdr ! fromEnum ReqExpect
67+ cl = idxhdr ! fromEnum ReqContentLength
68+ te = idxhdr ! fromEnum ReqTransferEncoding
6969 handle100Continue = handleExpect conn httpversion expect
7070 (rbody, remainingRef, bodyLength) <- bodyAndSource src cl te
7171 -- body producing function which will produce '100-continue', if needed
@@ -85,8 +85,8 @@ recvRequest settings conn ii addr src = do
8585 , requestBody = rbody'
8686 , vault = vaultValue
8787 , requestBodyLength = bodyLength
88− , requestHeaderHost = idxhdr ! idxHost
89− , requestHeaderRange = idxhdr ! idxRange
88+ , requestHeaderHost = idxhdr ! fromEnum ReqHost
89+ , requestHeaderRange = idxhdr ! fromEnum ReqRange
9090 }
9191 return (req, remainingRef, idxhdr, rbodyFlush)
9292 where
warp/Network/Wai/Handler/Warp/Response.hs+102−98
@@ -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,7 +25,7 @@ import Blaze.ByteString.Builder.HTTP (chunkedTransferEncoding, chunkedTransferTe
2625#if __GLASGOW_HASKELL__ < 709
2726import Control.Applicative
2827#endif
29−import Control.Exception
28+import qualified Control.Exception as E
3029import Control.Monad (unless, when)
3130import Data.Array ((!))
3231import Data.ByteString (ByteString)
@@ -57,7 +56,7 @@ import Network.Wai
5756import Network.Wai.Handler.Warp.Buffer (toBuilderBuffer)
5857import qualified Network.Wai.Handler.Warp.Date as D
5958import qualified Network.Wai.Handler.Warp.FdCache as F
60−import qualified Network.Wai.Handler.Warp.FileInfoCache as I
59+import Network.Wai.Handler.Warp.File
6160import Network.Wai.Handler.Warp.Header
6261import Network.Wai.Handler.Warp.IO (toBufIOWith)
6362import Network.Wai.Handler.Warp.ResponseHeader
@@ -77,42 +76,14 @@ import qualified Paths_warp
7776
7877----------------------------------------------------------------
7978
80−fileRange :: H.Status -> H.ResponseHeaders -> FilePath
81− -> Maybe FilePart -> Maybe HeaderValue
82− -> (FilePath -> IO I.FileInfo)
83− -> IO (Either IOException
84− (H.Status, H.ResponseHeaders, Integer, Integer))
85−fileRange s0 hs0 path Nothing mRange get = do
86− efinfo <- try (get path)
87− case efinfo of
88− Left e -> return $ Left e
89− Right finfo -> do
90− let ret = fileRangeSized s0 hs0 Nothing mRange $ I.fileInfoSize finfo
91− return $ Right ret
92−fileRange s0 hs0 _ mPart@(Just part) mRange _ =
93− return . Right $ fileRangeSized s0 hs0 mPart mRange size
94− where
95− size = filePartFileSize part
96−
97−fileRangeSized :: H.Status -> H.ResponseHeaders
98− -> Maybe FilePart -> Maybe HeaderValue -> Integer
99− -> (H.Status, H.ResponseHeaders, Integer, Integer)
100−fileRangeSized s0 hs0 mPart mRange fileSize = (s, hs, beg, len)
101− where
102− part = fromMaybe (chooseFilePart fileSize mRange) mPart
103− beg = filePartOffset part
104− len = filePartByteCount part
105− (s, hs) = adjustForFilePart s0 hs0 $ FilePart beg len fileSize
106−
107−----------------------------------------------------------------
108−
10979-- | Sending a HTTP response to 'Connection' according to 'Response'.
11080--
111−-- Applications/middlewares MUST specify a proper 'H.ResponseHeaders'.
81+-- Applications/middlewares MUST provide a proper 'H.ResponseHeaders'.
11282-- so that inconsistency does not happen.
11383-- No header is deleted by this function.
11484--
115−-- Especially, Applications/middlewares MUST take care of
85+-- Especially, Applications/middlewares MUST provide a proper
86+-- Content-Type. They MUST NOT provide
11687-- Content-Length, Content-Range, and Transfer-Encoding
11788-- because they are inserted, when necessary,
11889-- regardless they already exist.
@@ -124,22 +95,6 @@ fileRangeSized s0 hs0 mPart mRange fileSize = (s, hs, beg, len)
12495--
12596-- There are three basic APIs to create 'Response':
12697--
127−-- ['responseFile' :: 'H.Status' -> 'H.ResponseHeaders' -> 'FilePath' -> 'Maybe' 'FilePart' -> 'Response']
128−-- HTTP response body is sent by sendfile() for GET method.
129−-- HTTP response body is not sent by HEAD method.
130−-- Applications are categorized into simple and sophisticated.
131−-- Simple applications should specify 'Nothing' to
132−-- 'Maybe' 'FilePart'. The size of the specified file is obtained
133−-- by disk access. Then Range is handled.
134−-- Sophisticated applications should specify 'Just' to
135−-- 'Maybe' 'FilePart'. They should treat Range (and If-Range) by
136−-- themselves. In both cases,
137−-- Content-Length and Content-Range (if necessary) are automatically
138−-- added into the HTTP response header.
139−-- If Content-Length and Content-Range exist in the HTTP response header,
140−-- they would cause inconsistency.
141−-- Status is also changed to 206 (Partial Content) if necessary.
142−--
14398-- ['responseBuilder' :: 'H.Status' -> 'H.ResponseHeaders' -> 'Builder' -> 'Response']
14499-- HTTP response body is created from 'Builder'.
145100-- Transfer-Encoding: chunked is used in HTTP/1.1.
@@ -150,6 +105,27 @@ fileRangeSized s0 hs0 mPart mRange fileSize = (s, hs, beg, len)
150105--
151106-- ['responseRaw' :: ('IO' 'ByteString' -> ('ByteString' -> 'IO' ()) -> 'IO' ()) -> 'Response' -> 'Response']
152107-- No header is added and no Transfer-Encoding: is applied.
108+--
109+-- ['responseFile' :: 'H.Status' -> 'H.ResponseHeaders' -> 'FilePath' -> 'Maybe' 'FilePart' -> 'Response']
110+-- HTTP response body is sent (by sendfile(), if possible) for GET method.
111+-- HTTP response body is not sent by HEAD method.
112+-- Content-Length and Content-Range are automatically
113+-- added into the HTTP response header if necessary.
114+-- If Content-Length and Content-Range exist in the HTTP response header,
115+-- they would cause inconsistency.
116+-- \"Accept-Ranges: bytes\" is also inserted.
117+--
118+-- Applications are categorized into simple and sophisticated.
119+-- Sophisticated applications should specify 'Just' to
120+-- 'Maybe' 'FilePart'. They should treat the conditional request
121+-- by themselves. A proper 'Status' (200 or 206) must be provided.
122+--
123+-- Simple applications should specify 'Nothing' to
124+-- 'Maybe' 'FilePart'. The size of the specified file is obtained
125+-- by disk access or from the file infor cache.
126+-- If-Modified-Since, If-Unmodified-Since, If-Range and Range
127+-- are processed. Since a proper status is chosen, 'Status' is
128+-- ignored. Last-Modified is inserted.
153129
154130sendResponse :: Settings
155131 -> Connection
@@ -189,13 +165,12 @@ sendResponse settings conn ii req reqidxhdr src response = do
189165 th = threadHandle ii
190166 dc = dateCacher ii
191167 addServerAndDate = addDate dc rspidxhdr . addServer defServer rspidxhdr
192− mRange = reqidxhdr ! idxRange
193168 (isPersist,isChunked0) = infoFromRequest req reqidxhdr
194169 isChunked = not isHead && isChunked0
195170 (isKeepAlive, needsChunked) = infoFromResponse rspidxhdr (isPersist,isChunked)
196171 isHead = requestMethod req == H.methodHead
197172 rsp = case response of
198− ResponseFile _ _ path mPart -> RspFile path mPart mRange isHead (T.tickle th)
173+ ResponseFile _ _ path mPart -> RspFile path mPart reqidxhdr isHead (T.tickle th)
199174 ResponseBuilder _ _ b
200175 | isHead -> RspNoBody
201176 | otherwise -> RspBuilder b needsChunked
@@ -237,7 +212,7 @@ sanitizeHeaderValue v = case S8.lines $ S.filter (/= _cr) v of
237212----------------------------------------------------------------
238213
239214data Rsp = RspNoBody
240− | RspFile FilePath (Maybe FilePart) (Maybe HeaderValue) Bool (IO ())
215+ | RspFile FilePath (Maybe FilePart) IndexedHeader Bool (IO ())
241216 | RspBuilder Builder Bool
242217 | RspStream StreamingBody Bool T.Handle
243218 | RspRaw (IO ByteString -> (ByteString -> IO ()) -> IO ()) (IO ByteString) (IO ())
@@ -262,47 +237,6 @@ sendRsp conn _ ver s hs RspNoBody = do
262237
263238----------------------------------------------------------------
264239
265−sendRsp conn ii ver s0 hs0 (RspFile path mPart mRange isHead hook) = do
266− ex <- fileRange s0 hs0 path mPart mRange get
267− case ex of
268− Left _ex ->
269−#ifdef WARP_DEBUG
270− print _ex >>
271−#endif
272− sendRsp conn ii ver s2 hs2 (RspBuilder body True)
273− Right (s, hs, beg, len)
274− | len >= 0 ->
275− if isHead then
276− sendRsp conn ii ver s hs RspNoBody
277− else do
278− lheader <- composeHeader ver s hs
279−#ifdef WINDOWS
280− let fid = FileId path Nothing
281− hook' = hook
282−#else
283− (mfd, hook') <- case mfdc of
284− -- settingsFdCacheDuration is 0
285− Nothing -> return (Nothing, hook)
286− Just fdc -> do
287− (fd, fresher) <- F.getFd fdc path
288− return (Just fd, hook >> fresher)
289− let fid = FileId path mfd
290−#endif
291− connSendFile conn fid beg len hook' [lheader]
292− return (Just s, Just len)
293− | otherwise -> do
294− sendRsp conn ii ver H.requestedRangeNotSatisfiable416
295− (filter (\(k, _) -> k /= "content-length") hs)
296− (RspBuilder mempty True)
297− where
298− s2 = H.notFound404
299− hs2 = replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
300− body = byteString "File not found"
301− mfdc = fdCacher ii
302− get = fileInfo ii
303−
304−----------------------------------------------------------------
305−
306240sendRsp conn _ ver s hs (RspBuilder body needsChunked) = do
307241 header <- composeHeaderBuilder ver s hs needsChunked
308242 let hdrBdy
@@ -350,6 +284,76 @@ sendRsp conn _ _ _ _ (RspRaw withApp src tickle) = do
350284 return bs
351285 send bs = connSendAll conn bs >> tickle
352286
287+----------------------------------------------------------------
288+
289+-- Sophisticated WAI applications.
290+-- We respect s0. s0 MUST be a proper value.
291+sendRsp conn ii ver s0 hs0 (RspFile path (Just part) _ isHead hook) =
292+ sendRspFile2XX conn ii ver s0 hs path beg len isHead hook
293+ where
294+ beg = filePartOffset part
295+ len = filePartByteCount part
296+ hs = addContentHeadersForFilePart hs0 part
297+
298+----------------------------------------------------------------
299+
300+-- Simple WAI applications.
301+-- Status is ignored
302+sendRsp conn ii ver _ hs0 (RspFile path Nothing idxhdr isHead hook) = do
303+ efinfo <- E.try $ fileInfo ii path
304+ case efinfo of
305+ Left (_ex :: E.IOException) ->
306+#ifdef WARP_DEBUG
307+ print _ex >>
308+#endif
309+ sendRspFile404 conn ii ver hs0
310+ Right finfo -> case conditionalRequest finfo hs0 idxhdr of
311+ WithoutBody s -> sendRsp conn ii ver s hs0 RspNoBody
312+ WithBody s hs beg len -> sendRspFile2XX conn ii ver s hs path beg len isHead hook
313+
314+----------------------------------------------------------------
315+
316+sendRspFile2XX :: Connection
317+ -> InternalInfo
318+ -> H.HttpVersion
319+ -> H.Status
320+ -> H.ResponseHeaders
321+ -> FilePath
322+ -> Integer
323+ -> Integer
324+ -> Bool
325+ -> IO ()
326+ -> IO (Maybe H.Status, Maybe Integer)
327+sendRspFile2XX conn ii ver s hs path beg len isHead hook
328+ | isHead = sendRsp conn ii ver s hs RspNoBody
329+ | otherwise = do
330+ lheader <- composeHeader ver s hs
331+#ifdef WINDOWS
332+ let fid = FileId path Nothing
333+ hook' = hook
334+#else
335+ (mfd, hook') <- case fdCacher ii of
336+ -- settingsFdCacheDuration is 0
337+ Nothing -> return (Nothing, hook)
338+ Just fdc -> do
339+ (fd, fresher) <- F.getFd fdc path
340+ return (Just fd, hook >> fresher)
341+ let fid = FileId path mfd
342+#endif
343+ connSendFile conn fid beg len hook' [lheader]
344+ return (Just s, Just len)
345+
346+sendRspFile404 :: Connection
347+ -> InternalInfo
348+ -> H.HttpVersion
349+ -> H.ResponseHeaders
350+ -> IO (Maybe H.Status, Maybe Integer)
351+sendRspFile404 conn ii ver hs0 = sendRsp conn ii ver s hs (RspBuilder body True)
352+ where
353+ s = H.notFound404
354+ hs = replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
355+ body = byteString "File not found"
356+
353357----------------------------------------------------------------
354358----------------------------------------------------------------
355359
@@ -376,7 +380,7 @@ checkPersist req reqidxhdr
376380 | otherwise = checkPersist10 conn
377381 where
378382 ver = httpVersion req
379− conn = reqidxhdr ! idxConnection
383+ conn = reqidxhdr ! fromEnum ReqConnection
380384 checkPersist11 (Just x)
381385 | CI.foldCase x == "close" = False
382386 checkPersist11 _ = True
@@ -401,7 +405,7 @@ infoFromResponse rspidxhdr (isPersist,isChunked) = (isKeepAlive, needsChunked)
401405 where
402406 needsChunked = isChunked && not hasLength
403407 isKeepAlive = isPersist && (isChunked || hasLength)
404− hasLength = isJust $ rspidxhdr ! idxContentLength
408+ hasLength = isJust $ rspidxhdr ! fromEnum ResContentLength
405409
406410----------------------------------------------------------------
407411
@@ -422,7 +426,7 @@ addTransferEncoding hdrs = ("transfer-encoding", "chunked") : hdrs
422426#endif
423427
424428addDate :: D.DateCache -> IndexedHeader -> H.ResponseHeaders -> IO H.ResponseHeaders
425−addDate dc rspidxhdr hdrs = case rspidxhdr ! idxDate of
429+addDate dc rspidxhdr hdrs = case rspidxhdr ! fromEnum ResDate of
426430 Nothing -> do
427431 gmtdate <- D.getDate dc
428432 return $ (H.hDate, gmtdate) : hdrs
@@ -435,7 +439,7 @@ warpVersion :: String
435439warpVersion = showVersion Paths_warp.version
436440
437441addServer :: HeaderValue -> IndexedHeader -> H.ResponseHeaders -> H.ResponseHeaders
438−addServer serverName rspidxhdr hdrs = case rspidxhdr ! idxServer of
442+addServer serverName rspidxhdr hdrs = case rspidxhdr ! fromEnum ResServer of
439443 Nothing -> (H.hServer, serverName) : hdrs
440444 _ -> hdrs
441445
warp/test/FileSpec.hsnew file+45
@@ -0,0 +1,45 @@
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+ let WithBody s hs off len = ans
22+ hs' = ("Last-Modified",fileInfoDate finfo) : hs
23+ ans' = WithBody s hs' off len
24+ conditionalRequest finfo [] (indexRequestHeader reqhs) `shouldBe` ans'
25+
26+spec :: Spec
27+spec = do
28+ describe "conditionalRequest" $ do
29+ testFileRange
30+ "gets a file size from file system"
31+ [] "attic/hex"
32+ $ WithBody ok200 [("Content-Length","16"),("Accept-Ranges","bytes")] 0 16
33+ testFileRange
34+ "gets a file size from file system and handles Range and returns Partical Content"
35+ [("Range","bytes=2-14")] "attic/hex"
36+ $ WithBody status206 [("Content-Range","bytes 2-14/16"),("Content-Length","13"),("Accept-Ranges","bytes")] 2 13
37+ testFileRange
38+ "truncates end point of range to file size"
39+ [("Range","bytes=10-20")] "attic/hex"
40+ $ WithBody status206 [("Content-Range","bytes 10-15/16"),("Content-Length","6"),("Accept-Ranges","bytes")] 10 6
41+ testFileRange
42+ "gets a file size from file system and handles Range and returns OK if Range means the entire"
43+ [("Range:","bytes=0-15")] "attic/hex"
44+ $ WithBody status200 [("Content-Length","16"),("Accept-Ranges","bytes")] 0 16
45+
warp/test/ResponseSpec.hs+1−47
@@ -1,4 +1,5 @@
11{-# LANGUAGE OverloadedStrings #-}
2+
23module ResponseSpec (main, spec) where
34
45import Control.Concurrent (threadDelay)
@@ -9,7 +10,6 @@ import Network (connectTo, PortID (PortNumber))
910import Network.HTTP.Types
1011import Network.Wai
1112import Network.Wai.Handler.Warp
12−import Network.Wai.Handler.Warp.FileInfoCache
1313import Network.Wai.Handler.Warp.Response
1414import RunSpec (withApp)
1515import System.IO (hClose, hFlush)
@@ -71,18 +71,6 @@ testPartial size offset count out = it title $ withApp defaultSettings app $ \po
7171 _ -> Nothing
7272 range = "bytes " ++ show offset ++ "-" ++ show (offset + count - 1) ++ "/" ++ show size
7373
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−
8674spec :: Spec
8775spec = do
8876 describe "preventing response splitting attack" $ do
@@ -125,37 +113,3 @@ spec = do
125113 testPartial 16 2 2 "23"
126114 testPartial 16 0 2 "01"
127115 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+2
@@ -66,6 +66,7 @@ Library
6666 Network.Wai.Handler.Warp.Counter
6767 Network.Wai.Handler.Warp.Date
6868 Network.Wai.Handler.Warp.FdCache
69+ Network.Wai.Handler.Warp.File
6970 Network.Wai.Handler.Warp.FileInfoCache
7071 Network.Wai.Handler.Warp.HTTP2
7172 Network.Wai.Handler.Warp.HTTP2.EncodeFrame
@@ -120,6 +121,7 @@ Test-Suite spec
120121 ConduitSpec
121122 ExceptionSpec
122123 FdCacheSpec
124+ FileSpec
123125 MultiMapSpec
124126 ReadIntSpec
125127 RequestSpec