diff --git a/docs/TransferEncoding.html b/docs/TransferEncoding.html index c18716a..6dfc20a 100644 --- a/docs/TransferEncoding.html +++ b/docs/TransferEncoding.html @@ -216,12 +216,14 @@

(dechunk body)

-

decodes a chunked transfer-encoded body per RFC 7230 §4.1.

+

decodes a chunked transfer-encoded body per RFC 9112 §7.1.

Each chunk is a hexadecimal size line — any chunk extension after a ; is ignored — followed by that many bytes of data and a CRLF, repeating until a zero-size chunk. Optional trailer headers after the final chunk are ignored. Returns the decoded body, or an (Error String) on malformed framing such as a -bad hex size or a truncated chunk.

+bad hex size or a truncated chunk. The size line is read strictly: it must begin +with a hexadecimal digit, and nothing but a chunk extension may follow the +size.

diff --git a/http.carp b/http.carp index 1b2aeaf..495a035 100644 --- a/http.carp +++ b/http.carp @@ -661,19 +661,42 @@ Use `Request.chunked?`/`Response.chunked?` to detect a chunked body, then (set! acc (+ (* acc 16) d))))) (if ok (Maybe.Just acc) (Maybe.Nothing)))) + (private bws?) + (hidden bws?) + (defn bws? [c] (or (= c \space) (= c \tab))) + + ; chunk-size = 1*HEXDIG, then chunk-ext = *( BWS ";" ... ) per RFC 9112 §7.1 + (private parse-size-line) + (hidden parse-size-line) + (defn parse-size-line [line] + (let-do [len (String.length line) + digits 0 + i 0] + (while (and (< digits len) + (>= (hex-nibble (String.char-at line digits)) 0)) + (set! digits (Int.inc digits))) + (set! i digits) + (while (and (< i len) (bws? (String.char-at line i))) (set! i (Int.inc i))) + (if (and (> digits 0) + (or (= digits len) (and (< i len) (= \; (String.char-at line i))))) + (parse-hex &(String.byte-slice line 0 digits)) + (Maybe.Nothing)))) + (private crlf-at?) (hidden crlf-at?) (defn crlf-at? [s i] (and (= (String.char-at s i) \return) (= (String.char-at s (Int.inc i)) \newline))) - (doc dechunk "decodes a chunked transfer-encoded `body` per RFC 7230 §4.1. + (doc dechunk "decodes a chunked transfer-encoded `body` per RFC 9112 §7.1. Each chunk is a hexadecimal size line — any chunk extension after a `;` is ignored — followed by that many bytes of data and a CRLF, repeating until a zero-size chunk. Optional trailer headers after the final chunk are ignored. Returns the decoded body, or an `(Error String)` on malformed framing such as a -bad hex size or a truncated chunk.") +bad hex size or a truncated chunk. The size line is read strictly: it must begin +with a hexadecimal digit, and nothing but a chunk extension may follow the +size.") (defn dechunk [body] (let-do [len (String.length body) pos 0 @@ -684,18 +707,13 @@ bad hex size or a truncated chunk.") (let [eol (String.find-crlf body pos len)] (if (= eol -1) (set! err @"malformed chunked body: unterminated chunk size line") - (let [size-line &(String.byte-slice body pos eol) - semi (String.index-of size-line \;) - hex &(String.trim - &(if (= semi -1) - @size-line - (String.byte-slice size-line 0 semi)))] - (match (parse-hex hex) + (let [size-line &(String.byte-slice body pos eol)] + (match (parse-size-line size-line) (Maybe.Nothing) (set! err (fmt "malformed chunked body: invalid chunk size '%s'" - hex)) + size-line)) (Maybe.Just size) (if (= size 0) (set! terminated true) diff --git a/test/http.carp b/test/http.carp index a274628..e5a5085 100644 --- a/test/http.carp +++ b/test/http.carp @@ -2195,6 +2195,40 @@ (dechunk-err? "4\r\nWiki\r\n5\r\npedia\r\n") "dechunk errors on a missing terminating zero-size chunk") + (assert-true test + (dechunk-err? " 5\r\nhello\r\n0\r\n\r\n") + "dechunk rejects a chunk size line with leading whitespace") + + (assert-true test + (dechunk-err? "\t5\r\nhello\r\n0\r\n\r\n") + "dechunk rejects a chunk size line starting with a tab") + + (assert-true test + (dechunk-err? "5 \r\nhello\r\n0\r\n\r\n") + "dechunk rejects a chunk size with trailing whitespace and no extension") + + (assert-true test + (dechunk-err? "5\r\r\nhello\r\n0\r\n\r\n") + "dechunk rejects a stray CR between the chunk size and the CRLF") + + (assert-true test + (dechunk-err? " 0 \r\n\r\n") + "dechunk rejects a whitespace-padded terminating chunk") + + (assert-true test + (dechunk-err? ";name=value\r\nhello\r\n0\r\n\r\n") + "dechunk rejects a chunk extension with no size before it") + + (assert-equal test + &(dechunk-ok "5 ;name=value\r\nhello\r\n0\r\n\r\n") + "hello" + "dechunk allows the bad whitespace RFC 9112 §7.1.1 permits before an extension") + + (assert-equal test + &(dechunk-ok "1C\r\nabcdefghijklmnopqrstuvwxyz01\r\n0\r\n\r\n") + "abcdefghijklmnopqrstuvwxyz01" + "dechunk decodes an uppercase hex chunk size") + (assert-equal test &(match (Response.parse "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n")