Skip to content

Commit 6de4dc1

Browse files
authored
Merge pull request #91 from scrive/fix-dechunker
Fix consumeChunks: actually strip chunk size-lines and trailing CRLFs
2 parents 30e7802 + f6e50d4 commit 6de4dc1

2 files changed

Lines changed: 34 additions & 8 deletions

File tree

‎src/Happstack/Server/Internal/Handler.hs‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Happstack.Server.Internal.Handler
44
( request
55
, parseResponse
66
, putRequest
7+
, consumeChunks
78
) where
89

910
import qualified Paths_happstack_server as Paths
@@ -154,24 +155,32 @@ parseResponse inputStr =
154155
mbCL
155156
return $ Response {rsCode=code,rsHeaders=headers,rsBody=body,rsFlags=RsFlags ContentLength,rsValidator=Nothing}
156157

157-
-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
158-
-- note this does NOT handle extenions
159-
consumeChunks::L.ByteString->(L.ByteString,L.ByteString)
160-
consumeChunks str = let (parts,tr,rest) = consumeChunksImpl str in (L.concat . (++ [tr]) .map snd $ parts,rest)
158+
-- | Decode an HTTP/1.1 Transfer-Encoding: chunked body.
159+
-- See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html (chunk extensions are not handled).
160+
-- Returns @(decoded body, bytes following the chunked stream)@. The trailer (optional headers
161+
-- after the terminating 0-chunk, RFC 2616 §3.6.1) is dropped from the body — trailers are
162+
-- headers, not body content.
163+
consumeChunks :: L.ByteString -> (L.ByteString, L.ByteString)
164+
consumeChunks str = let (parts,_tr,rest) = consumeChunksImpl str in (L.concat . map snd $ parts, rest)
161165

166+
-- | Worker for 'consumeChunks'. Returns @(chunks, trailer, remainder)@, where each chunk is
167+
-- @(declared-length, body-bytes)@ and the terminating 0-chunk has empty body.
162168
consumeChunksImpl :: L.ByteString -> ([(Int64, L.ByteString)], L.ByteString, L.ByteString)
163169
consumeChunksImpl str
164170
| L.null str = ([],L.empty,str)
165-
| chunkLen == 0 = let (last,rest') = L.splitAt lenLine1 str
171+
| chunkLen == 0 = let (_sizeLine,rest') = L.splitAt lenLine1 str
166172
(tr',rest'') = getTrailer rest'
167-
in ([(0,last)],tr',rest'')
168-
| otherwise = ((chunkLen,part):crest,tr,rest2)
173+
in ([(0,L.empty)],tr',rest'')
174+
| otherwise = ((chunkLen,body):crest,tr,rest2)
169175
where
170176
line1 = head $ lazylines str
171177
lenLine1 = (L.length line1) + 1 -- endchar
172178
chunkLen = (fst $ head $ readHex $ L.unpack line1)
173179
len = chunkLen + lenLine1 + 2
174-
(part,rest) = L.splitAt len str
180+
-- Each frame on the wire is "<hex-size>\r\n<body-bytes>\r\n". Skip the size-line and
181+
-- take exactly chunkLen bytes so the trailing CRLF stays out of the decoded body.
182+
(frame,rest) = L.splitAt len str
183+
body = L.take chunkLen . L.drop lenLine1 $ frame
175184
(crest,tr,rest2) = consumeChunksImpl rest
176185
getTrailer s = L.splitAt index s
177186
where index | crlfLC `L.isPrefixOf` s = 2

‎tests/Happstack/Server/Tests.hs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Happstack.Server.FileServe.BuildingBlocks (sendFileResponse)
1919
import Happstack.Server.Cookie
2020
import Happstack.Server.Internal.Compression
2121
import Happstack.Server.Internal.Cookie
22+
import Happstack.Server.Internal.Handler (consumeChunks)
2223
import Happstack.Server.Internal.Multipart
2324
import Happstack.Server.Internal.MessageWrap
2425
import Happstack.Server.Internal.RFC822Headers (ContentDisposition(..), parseContentDisposition)
@@ -37,6 +38,7 @@ allTests =
3738
, cookieHeaderOrderTest
3839
, pContentDispositionFilename
3940
, applicativeTest
41+
, consumeChunksTest
4042
]
4143

4244
cookieParserTest :: Test
@@ -251,6 +253,21 @@ pContentDispositionFilename =
251253
c <- parseContentDisposition doesNotWorkWithOldParserButWithNew
252254
assertEqual "parseContentDisposition" c (ContentDisposition "form-data" [("filename","file.pdf"),("name","file")])
253255

256+
consumeChunksTest :: Test
257+
consumeChunksTest =
258+
"consumeChunks decodes Transfer-Encoding: chunked" ~:
259+
[ -- Single chunk
260+
consumeChunks (pack "5\r\nhello\r\n0\r\n\r\n") @?= (pack "hello", pack "")
261+
-- Multiple chunks concatenated
262+
, consumeChunks (pack "5\r\nhello\r\n5\r\nworld\r\n0\r\n\r\n")
263+
@?= (pack "helloworld", pack "")
264+
-- Empty body (only terminator)
265+
, consumeChunks (pack "0\r\n\r\n") @?= (pack "", pack "")
266+
-- Trailing bytes after final chunk are returned as remainder
267+
, consumeChunks (pack "5\r\nhello\r\n0\r\n\r\nNEXT")
268+
@?= (pack "hello", pack "NEXT")
269+
]
270+
254271
applicativeTest :: Test
255272
applicativeTest =
256273
"applicativeTest" ~:

0 commit comments

Comments
 (0)