Skip to content

Commit f6e50d4

Browse files
committed
Fix consumeChunks: actually strip chunk size-lines and trailing CRLFs
Previously consumeChunks returned the raw chunked stream unchanged, so any caller using Transfer-Encoding: chunked input got chunk-size hex prefixes embedded in the body. Multipart parsing in particular saw hex digits inside the file content and rejected the upload. This fixes consumeChunks/consumeChunksImpl to extract just the chunk body bytes (dropping the size-line and trailing CRLF), and to drop the HTTP trailer from the body output (trailers are headers per RFC 2616, not body content). consumeChunks is also exported from Internal.Handler so it can be tested.
1 parent 8a460dd commit f6e50d4

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)