Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions http-client.carp
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,28 @@ apart.
(set! acc (+ (* acc 16) d)))))
(if ok (Maybe.Just acc) (Maybe.Nothing))))

(hidden bws?)
(private bws?)
(defn bws? [c] (or (= c \space) (= c \tab)))

(hidden parse-size-line)
(private parse-size-line)
; chunk-size = 1*HEXDIG, then chunk-ext = *( BWS ";" ... ) per RFC 9112 §7.1
; trailing BWS without a chunk-ext is accepted too
(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 (= i len) (and (< i len) (= \; (String.char-at line i)))))
(parse-hex &(String.byte-slice line 0 digits))
(Maybe.Nothing))))

(hidden crlf-at?)
(private crlf-at?)
(defn crlf-at? [s i]
Expand Down Expand Up @@ -340,17 +362,14 @@ apart.
@"malformed chunked body: unterminated chunk size line"
@"malformed chunked body: missing terminating zero-size chunk")))
(set! running false))
(let [line &(String.byte-slice (buf s) p eol)
semi (String.index-of line \;)
hex &(String.trim
&(if (= semi -1) @line (String.byte-slice line 0 semi)))]
(match (parse-hex hex)
(let [line &(String.byte-slice (buf s) p eol)]
(match (parse-size-line line)
(Maybe.Nothing)
(do
(fail-parse! s
(fmt
"malformed chunked body: invalid chunk size '%s'"
hex))
line))
(set! running false))
(Maybe.Just size)
(if (= size 0)
Expand Down
42 changes: 42 additions & 0 deletions test/http-client.carp
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,38 @@
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-hex-prefix"))
"a 0x-prefixed chunk size is rejected")

(assert-equal test
"malformed chunked body: invalid chunk size ' 5'"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-leading-space"))
"a space before the chunk size is rejected")

(assert-equal test
"200 [hello]"
&(status-and-body
(Client.get "http://127.0.0.1:8791/chunked-trailing-space"))
"a space after the chunk size is allowed")

(assert-equal test
"malformed chunked body: invalid chunk size '\t5'"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-leading-tab"))
"a tab before the chunk size is rejected")

(assert-equal test
"malformed chunked body: invalid chunk size '5\r'"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-stray-cr"))
"a stray CR after the chunk size is rejected")

(assert-equal test
"malformed chunked body: invalid chunk size ' 0 '"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-padded-zero"))
"a zero-size chunk with a leading space does not end the body")

(assert-equal test
"200 [hello]"
&(status-and-body
(Client.get "http://127.0.0.1:8791/chunked-zero-trailing-space"))
"a zero-size chunk with a trailing space ends the body")

(assert-equal test
"malformed chunked body: truncated chunk data"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-truncated"))
Expand Down Expand Up @@ -767,6 +799,16 @@
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-ext"))
"a chunk extension after the size is ignored")

(assert-equal test
"200 [hello]"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-ext-space"))
"a space between the size and a chunk extension is allowed")

(assert-equal test
"200 [hello]"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-ext-tab"))
"a tab between the size and a chunk extension is allowed")

(assert-equal test
"200 [hello]"
&(status-and-body (Client.get "http://127.0.0.1:8791/chunked-trailer"))
Expand Down
16 changes: 16 additions & 0 deletions test/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ def _route(self):
return self._chunked_raw(b"5\r\nhello\r\n")
if path == "/chunked-missing-crlf":
return self._chunked_raw(b"5\r\nhelloXX0\r\n\r\n")
if path == "/chunked-leading-space":
return self._chunked_raw(b" 5\r\nhello\r\n0\r\n\r\n")
if path == "/chunked-trailing-space":
return self._chunked_raw(b"5 \r\nhello\r\n0\r\n\r\n")
if path == "/chunked-leading-tab":
return self._chunked_raw(b"\t5\r\nhello\r\n0\r\n\r\n")
if path == "/chunked-stray-cr":
return self._chunked_raw(b"5\r\r\nhello\r\n0\r\n\r\n")
if path == "/chunked-padded-zero":
return self._chunked_raw(b"5\r\nhello\r\n 0 \r\n\r\n")
if path == "/chunked-zero-trailing-space":
return self._chunked_raw(b"5\r\nhello\r\n0 \r\n\r\n")

# a chunk size above 16 MiB, whose data is never sent, so the size
# line alone decides the outcome
Expand All @@ -218,6 +230,10 @@ def _route(self):
# a well-formed body with a chunk extension and a trailer section
if path == "/chunked-ext":
return self._chunked_raw(b"5;name=value\r\nhello\r\n0\r\n\r\n")
if path == "/chunked-ext-space":
return self._chunked_raw(b"5 ;name\r\nhello\r\n0\r\n\r\n")
if path == "/chunked-ext-tab":
return self._chunked_raw(b"5\t;name\r\nhello\r\n0\r\n\r\n")
if path == "/chunked-trailer":
return self._chunked_raw(
b"5\r\nhello\r\n0\r\nX-Checksum: abc\r\nX-More: 1\r\n\r\n"
Expand Down
Loading