summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGlenn Strauss <gstrauss@gluelogic.com>2016-12-16 11:06:29 -0500
committerGlenn Strauss <gstrauss@gluelogic.com>2016-12-16 16:58:04 -0500
commit4d7f5737f150c4bab8d3832bab9da62eddfea7d2 (patch)
tree3f5e56edf1e30f1a8da842e28ca283f196e8560b
parentf792d84cf9d16e8089aef7206b4a60d31c11ad55 (diff)
downloadlighttpd-git-4d7f5737f150c4bab8d3832bab9da62eddfea7d2.tar.gz
[core] support Transfer-Encoding: chunked req body (fixes #2156)
support Transfer-Encoding: chunked request body in conjunction with server.stream-request-body = 0 dynamic handlers will still return 411 Length Required if server.stream-request-body = 1 or 2 (!= 0) since CGI-like env requires CONTENT_LENGTH be set (and mod_proxy currently sends HTTP/1.0 requests to backends, and Content-Length recommended for robust interaction with backend) x-ref: "request: support Chunked Transfer Coding for HTTP PUT" https://redmine.lighttpd.net/issues/2156
-rw-r--r--src/base.h3
-rw-r--r--src/connections-glue.c235
-rw-r--r--src/connections.c6
-rw-r--r--src/connections.h1
-rw-r--r--src/mod_cgi.c21
-rw-r--r--src/mod_fastcgi.c8
-rw-r--r--src/mod_proxy.c9
-rw-r--r--src/mod_scgi.c20
-rw-r--r--src/request.c57
-rwxr-xr-xtests/request.t117
10 files changed, 419 insertions, 58 deletions
diff --git a/src/base.h b/src/base.h
index 4f88e4b7..12585ef3 100644
--- a/src/base.h
+++ b/src/base.h
@@ -170,7 +170,8 @@ typedef struct {
array *headers;
/* CONTENT */
- size_t content_length; /* returned by strtoul() */
+ off_t content_length; /* returned by strtoll() */
+ off_t te_chunked;
/* internal representation */
int accept_encoding;
diff --git a/src/connections-glue.c b/src/connections-glue.c
index e1118b37..d30c229a 100644
--- a/src/connections-glue.c
+++ b/src/connections-glue.c
@@ -315,6 +315,221 @@ int connection_handle_read(server *srv, connection *con) {
return 0;
}
+static int connection_handle_read_post_cq_compact(chunkqueue *cq) {
+ /* combine first mem chunk with next non-empty mem chunk
+ * (loop if next chunk is empty) */
+ chunk *c;
+ while (NULL != (c = cq->first) && NULL != c->next) {
+ buffer *mem = c->next->mem;
+ off_t offset = c->next->offset;
+ size_t blen = buffer_string_length(mem) - (size_t)offset;
+ force_assert(c->type == MEM_CHUNK);
+ force_assert(c->next->type == MEM_CHUNK);
+ buffer_append_string_len(c->mem, mem->ptr+offset, blen);
+ c->next->offset = c->offset;
+ c->next->mem = c->mem;
+ c->mem = mem;
+ c->offset = offset + (off_t)blen;
+ chunkqueue_remove_finished_chunks(cq);
+ if (0 != blen) return 1;
+ }
+ return 0;
+}
+
+static int connection_handle_read_post_chunked_crlf(chunkqueue *cq) {
+ /* caller might check chunkqueue_length(cq) >= 2 before calling here
+ * to limit return value to either 1 for good or -1 for error */
+ chunk *c;
+ buffer *b;
+ char *p;
+ size_t len;
+
+ /* caller must have called chunkqueue_remove_finished_chunks(cq), so if
+ * chunkqueue is not empty, it contains chunk with at least one char */
+ if (chunkqueue_is_empty(cq)) return 0;
+
+ c = cq->first;
+ b = c->mem;
+ p = b->ptr+c->offset;
+ if (p[0] != '\r') return -1; /* error */
+ if (p[1] == '\n') return 1;
+ len = buffer_string_length(b) - (size_t)c->offset;
+ if (1 != len) return -1; /* error */
+
+ while (NULL != (c = c->next)) {
+ b = c->mem;
+ len = buffer_string_length(b) - (size_t)c->offset;
+ if (0 == len) continue;
+ p = b->ptr+c->offset;
+ return (p[0] == '\n') ? 1 : -1; /* error if not '\n' */
+ }
+ return 0;
+}
+
+handler_t connection_handle_read_post_error(server *srv, connection *con, int http_status) {
+ UNUSED(srv);
+
+ con->keep_alive = 0;
+
+ /*(do not change status if response headers already set and possibly sent)*/
+ if (0 != con->bytes_header) return HANDLER_ERROR;
+
+ con->http_status = http_status;
+ con->mode = DIRECT;
+ chunkqueue_reset(con->write_queue);
+ return HANDLER_FINISHED;
+}
+
+static handler_t connection_handle_read_post_chunked(server *srv, connection *con, chunkqueue *cq, chunkqueue *dst_cq) {
+
+ /* con->conf.max_request_size is in kBytes */
+ const off_t max_request_size = (off_t)con->conf.max_request_size << 10;
+ off_t te_chunked = con->request.te_chunked;
+ do {
+ off_t len = cq->bytes_in - cq->bytes_out;
+
+ while (0 == te_chunked) {
+ char *p;
+ chunk *c = cq->first;
+ force_assert(c->type == MEM_CHUNK);
+ p = strchr(c->mem->ptr+c->offset, '\n');
+ if (NULL != p) { /* found HTTP chunked header line */
+ off_t hsz = p + 1 - (c->mem->ptr+c->offset);
+ unsigned char *s = (unsigned char *)c->mem->ptr+c->offset;
+ for (unsigned char u;(u=(unsigned char)hex2int(*s))!=0xFF;++s) {
+ if (te_chunked > (~((off_t)-1) >> 4)) {
+ log_error_write(srv, __FILE__, __LINE__, "s",
+ "chunked data size too large -> 400");
+ /* 400 Bad Request */
+ return connection_handle_read_post_error(srv, con, 400);
+ }
+ te_chunked <<= 4;
+ te_chunked |= u;
+ }
+ while (*s == ' ' || *s == '\t') ++s;
+ if (*s != '\r' && *s != ';') {
+ log_error_write(srv, __FILE__, __LINE__, "s",
+ "chunked header invalid chars -> 400");
+ /* 400 Bad Request */
+ return connection_handle_read_post_error(srv, con, 400);
+ }
+
+ if (hsz >= 1024) {
+ /* prevent theoretical integer overflow
+ * casting to (size_t) and adding 2 (for "\r\n") */
+ log_error_write(srv, __FILE__, __LINE__, "s",
+ "chunked header line too long -> 400");
+ /* 400 Bad Request */
+ return connection_handle_read_post_error(srv, con, 400);
+ }
+
+ if (0 == te_chunked) {
+ /* do not consume final chunked header until
+ * (optional) trailers received along with
+ * request-ending blank line "\r\n" */
+ if (p[0] == '\r' && p[1] == '\n') {
+ /*(common case with no trailers; final \r\n received)*/
+ hsz += 2;
+ }
+ else {
+ /* trailers or final CRLF crosses into next cq chunk */
+ hsz -= 2;
+ do {
+ c = cq->first;
+ p = strstr(c->mem->ptr+c->offset+hsz, "\r\n\r\n");
+ } while (NULL == p
+ && connection_handle_read_post_cq_compact(cq));
+ if (NULL == p) {
+ /*(effectively doubles max request field size
+ * potentially received by backend, if in the future
+ * these trailers are added to request headers)*/
+ if ((off_t)buffer_string_length(c->mem) - c->offset
+ < srv->srvconf.max_request_field_size) {
+ break;
+ }
+ else {
+ /* ignore excessively long trailers;
+ * disable keep-alive on connection */
+ con->keep_alive = 0;
+ }
+ }
+ hsz = p + 4 - (c->mem->ptr+c->offset);
+ /* trailers currently ignored, but could be processed
+ * here if 0 == con->conf.stream_request_body, taking
+ * care to reject any fields forbidden in trailers,
+ * making trailers available to CGI and other backends*/
+ }
+ chunkqueue_mark_written(cq, (size_t)hsz);
+ con->request.content_length = dst_cq->bytes_in;
+ break; /* done reading HTTP chunked request body */
+ }
+
+ /* consume HTTP chunked header */
+ chunkqueue_mark_written(cq, (size_t)hsz);
+ len = cq->bytes_in - cq->bytes_out;
+
+ if (0 !=max_request_size
+ && (max_request_size < te_chunked
+ || max_request_size - te_chunked < dst_cq->bytes_in)) {
+ log_error_write(srv, __FILE__, __LINE__, "sos",
+ "request-size too long:",
+ dst_cq->bytes_in + te_chunked, "-> 413");
+ /* 413 Payload Too Large */
+ return connection_handle_read_post_error(srv, con, 413);
+ }
+
+ te_chunked += 2; /*(for trailing "\r\n" after chunked data)*/
+
+ break; /* read HTTP chunked header */
+ }
+
+ /*(likely better ways to handle chunked header crossing chunkqueue
+ * chunks, but this situation is not expected to occur frequently)*/
+ if ((off_t)buffer_string_length(c->mem) - c->offset >= 1024) {
+ log_error_write(srv, __FILE__, __LINE__, "s",
+ "chunked header line too long -> 400");
+ /* 400 Bad Request */
+ return connection_handle_read_post_error(srv, con, 400);
+ }
+ else if (!connection_handle_read_post_cq_compact(cq)) {
+ break;
+ }
+ }
+ if (0 == te_chunked) break;
+
+ if (te_chunked > 2) {
+ if (len > te_chunked-2) len = te_chunked-2;
+ if (dst_cq->bytes_in + te_chunked <= 64*1024) {
+ /* avoid buffering request bodies <= 64k on disk */
+ chunkqueue_steal(dst_cq, cq, len);
+ }
+ else if (0 != chunkqueue_steal_with_tempfiles(srv,dst_cq,cq,len)) {
+ /* 500 Internal Server Error */
+ return connection_handle_read_post_error(srv, con, 500);
+ }
+ te_chunked -= len;
+ len = cq->bytes_in - cq->bytes_out;
+ }
+
+ if (len < te_chunked) break;
+
+ if (2 == te_chunked) {
+ if (-1 == connection_handle_read_post_chunked_crlf(cq)) {
+ log_error_write(srv, __FILE__, __LINE__, "s",
+ "chunked data missing end CRLF -> 400");
+ /* 400 Bad Request */
+ return connection_handle_read_post_error(srv, con, 400);
+ }
+ chunkqueue_mark_written(cq, 2);/*consume \r\n at end of chunk data*/
+ te_chunked -= 2;
+ }
+
+ } while (!chunkqueue_is_empty(cq));
+
+ con->request.te_chunked = te_chunked;
+ return HANDLER_GO_ON;
+}
+
handler_t connection_handle_read_post_state(server *srv, connection *con) {
chunkqueue *cq = con->read_queue;
chunkqueue *dst_cq = con->request_content_queue;
@@ -337,18 +552,17 @@ handler_t connection_handle_read_post_state(server *srv, connection *con) {
chunkqueue_remove_finished_chunks(cq);
- if (con->request.content_length <= 64*1024) {
+ if (-1 == con->request.content_length) { /*(Transfer-Encoding: chunked)*/
+ handler_t rc = connection_handle_read_post_chunked(srv, con, cq, dst_cq);
+ if (HANDLER_GO_ON != rc) return rc;
+ }
+ else if (con->request.content_length <= 64*1024) {
/* don't buffer request bodies <= 64k on disk */
chunkqueue_steal(dst_cq, cq, (off_t)con->request.content_length - dst_cq->bytes_in);
}
else if (0 != chunkqueue_steal_with_tempfiles(srv, dst_cq, cq, (off_t)con->request.content_length - dst_cq->bytes_in)) {
/* writing to temp file failed */
- con->http_status = 500; /* Internal Server Error */
- con->keep_alive = 0;
- con->mode = DIRECT;
- chunkqueue_reset(con->write_queue);
-
- return HANDLER_FINISHED;
+ return connection_handle_read_post_error(srv, con, 500); /* Internal Server Error */
}
chunkqueue_remove_finished_chunks(cq);
@@ -362,12 +576,7 @@ handler_t connection_handle_read_post_state(server *srv, connection *con) {
return HANDLER_GO_ON;
} else if (is_closed) {
#if 0
- con->http_status = 400; /* Bad Request */
- con->keep_alive = 0;
- con->mode = DIRECT;
- chunkqueue_reset(con->write_queue);
-
- return HANDLER_FINISHED;
+ return connection_handle_read_post_error(srv, con, 400); /* Bad Request */
#endif
return HANDLER_ERROR;
} else {
diff --git a/src/connections.c b/src/connections.c
index 13417d4f..4b4c94a2 100644
--- a/src/connections.c
+++ b/src/connections.c
@@ -298,8 +298,7 @@ static void connection_handle_response_end_state(server *srv, connection *con) {
if (con->state != CON_STATE_ERROR) srv->con_written++;
- if ((con->request.content_length
- && (off_t)con->request.content_length > con->request_content_queue->bytes_in)
+ if (con->request.content_length != con->request_content_queue->bytes_in
|| con->state == CON_STATE_ERROR) {
/* request body is present and has not been read completely */
con->keep_alive = 0;
@@ -766,6 +765,7 @@ int connection_reset(server *srv, connection *con) {
CLEAN(http_content_type);
#undef CLEAN
con->request.content_length = 0;
+ con->request.te_chunked = 0;
array_reset(con->request.headers);
array_reset(con->environment);
@@ -1203,7 +1203,7 @@ int connection_state_machine(server *srv, connection *con) {
plugins_call_connection_reset(srv, con);
if (con->request.content_length) {
- if ((off_t)con->request.content_length != chunkqueue_length(con->request_content_queue)) {
+ if (con->request.content_length != con->request_content_queue->bytes_in) {
con->keep_alive = 0;
}
con->request.content_length = 0;
diff --git a/src/connections.h b/src/connections.h
index bb19f2d9..fa98120c 100644
--- a/src/connections.h
+++ b/src/connections.h
@@ -18,6 +18,7 @@ const char * connection_get_short_state(connection_state_t state);
int connection_state_machine(server *srv, connection *con);
int connection_handle_read(server *srv, connection *con);
handler_t connection_handle_read_post_state(server *srv, connection *con);
+handler_t connection_handle_read_post_error(server *srv, connection *con, int http_status);
void connection_response_reset(server *srv, connection *con);
#endif
diff --git a/src/mod_cgi.c b/src/mod_cgi.c
index b48fe3cb..e32db360 100644
--- a/src/mod_cgi.c
+++ b/src/mod_cgi.c
@@ -94,6 +94,7 @@ typedef struct {
buffer *response;
buffer *response_header;
+ buffer *cgi_handler; /* dumb pointer */
plugin_config conf;
} handler_ctx;
@@ -542,7 +543,7 @@ static int cgi_demux_response(server *srv, handler_ctx *hctx) {
buffer_copy_buffer(con->request.uri, ds->value);
if (con->request.content_length) {
- if ((off_t)con->request.content_length != chunkqueue_length(con->request_content_queue)) {
+ if (con->request.content_length != con->request_content_queue->bytes_in) {
con->keep_alive = 0;
}
con->request.content_length = 0;
@@ -1055,7 +1056,7 @@ static int cgi_write_request(server *srv, handler_ctx *hctx, int fd) {
}
} else {
off_t cqlen = cq->bytes_in - cq->bytes_out;
- if (cq->bytes_in < (off_t)con->request.content_length && cqlen < 65536 - 16384) {
+ if (cq->bytes_in != con->request.content_length && cqlen < 65536 - 16384) {
/*(con->conf.stream_request_body & FDEVENT_STREAM_REQUEST)*/
if (!(con->conf.stream_request_body & FDEVENT_STREAM_REQUEST_POLLIN)) {
con->conf.stream_request_body |= FDEVENT_STREAM_REQUEST_POLLIN;
@@ -1330,6 +1331,7 @@ URIHANDLER_FUNC(cgi_is_handled) {
stat_cache_entry *sce = NULL;
struct stat stbuf;
struct stat *st;
+ buffer *cgi_handler;
if (con->mode != DIRECT) return HANDLER_GO_ON;
@@ -1349,10 +1351,11 @@ URIHANDLER_FUNC(cgi_is_handled) {
if (!S_ISREG(st->st_mode)) return HANDLER_GO_ON;
if (p->conf.execute_x_only == 1 && (st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0) return HANDLER_GO_ON;
- if (NULL != cgi_get_handler(p->conf.cgi, fn)) {
+ if (NULL != (cgi_handler = cgi_get_handler(p->conf.cgi, fn))) {
handler_ctx *hctx = cgi_handler_ctx_init();
hctx->remote_conn = con;
hctx->plugin_data = p;
+ hctx->cgi_handler = cgi_handler;
memcpy(&hctx->conf, &p->conf, sizeof(plugin_config));
con->plugin_ctx[p->id] = hctx;
con->mode = p->id;
@@ -1457,13 +1460,19 @@ SUBREQUEST_FUNC(mod_cgi_handle_subrequest) {
}
}
if (r != HANDLER_GO_ON) return r;
+
+ /* CGI environment requires that Content-Length be set.
+ * Send 411 Length Required if Content-Length missing.
+ * (occurs here if client sends Transfer-Encoding: chunked
+ * and module is flagged to stream request body to backend) */
+ if (-1 == con->request.content_length) {
+ return connection_handle_read_post_error(srv, con, 411);
+ }
}
}
if (-1 == hctx->fd) {
- buffer *handler = cgi_get_handler(hctx->conf.cgi, con->physical.path);
- if (!handler) return HANDLER_GO_ON; /*(should not happen; checked in cgi_is_handled())*/
- if (cgi_create_env(srv, con, p, hctx, handler)) {
+ if (cgi_create_env(srv, con, p, hctx, hctx->cgi_handler)) {
con->http_status = 500;
con->mode = DIRECT;
diff --git a/src/mod_fastcgi.c b/src/mod_fastcgi.c
index 1e38befd..77e0aae7 100644
--- a/src/mod_fastcgi.c
+++ b/src/mod_fastcgi.c
@@ -3034,6 +3034,14 @@ SUBREQUEST_FUNC(mod_fastcgi_handle_subrequest) {
}
}
if (r != HANDLER_GO_ON) return r;
+
+ /* CGI environment requires that Content-Length be set.
+ * Send 411 Length Required if Content-Length missing.
+ * (occurs here if client sends Transfer-Encoding: chunked
+ * and module is flagged to stream request body to backend) */
+ if (-1 == con->request.content_length) {
+ return connection_handle_read_post_error(srv, con, 411);
+ }
}
}
diff --git a/src/mod_proxy.c b/src/mod_proxy.c
index a916fae6..2881efce 100644
--- a/src/mod_proxy.c
+++ b/src/mod_proxy.c
@@ -1169,6 +1169,15 @@ SUBREQUEST_FUNC(mod_proxy_handle_subrequest) {
}
}
if (r != HANDLER_GO_ON) return r;
+
+ /* mod_proxy sends HTTP/1.0 request and ideally should send
+ * Content-Length with request if request body is present, so
+ * send 411 Length Required if Content-Length missing.
+ * (occurs here if client sends Transfer-Encoding: chunked
+ * and module is flagged to stream request body to backend) */
+ if (-1 == con->request.content_length) {
+ return connection_handle_read_post_error(srv, con, 411);
+ }
}
}
diff --git a/src/mod_scgi.c b/src/mod_scgi.c
index 348dd1c6..449b5fce 100644
--- a/src/mod_scgi.c
+++ b/src/mod_scgi.c
@@ -2468,6 +2468,14 @@ SUBREQUEST_FUNC(mod_scgi_handle_subrequest) {
}
}
if (r != HANDLER_GO_ON) return r;
+
+ /* SCGI requires that Content-Length be set.
+ * Send 411 Length Required if Content-Length missing.
+ * (occurs here if client sends Transfer-Encoding: chunked
+ * and module is flagged to stream request body to backend) */
+ if (-1 == con->request.content_length) {
+ return connection_handle_read_post_error(srv, con, 411);
+ }
}
}
@@ -2736,18 +2744,6 @@ static handler_t scgi_check_extension(server *srv, connection *con, void *p_d, i
/* a note about no handler is not sent yet */
extension->note_is_sent = 0;
- /* SCGI requires that Content-Length be set.
- * Send 411 Length Required if Content-Length missing.
- * (Alternatively, collect full request body before proceeding
- * in mod_scgi_handle_subrequest()) */
- if (0 == con->request.content_length
- && array_get_element(con->request.headers, "Transfer-Encoding")) {
- con->keep_alive = 0;
- con->http_status = 411; /* Length Required */
- con->mode = DIRECT;
- return HANDLER_FINISHED;
- }
-
/*
* if check-local is disabled, use the uri.path handler
*
diff --git a/src/request.c b/src/request.c
index d21e8d52..4065cd9f 100644
--- a/src/request.c
+++ b/src/request.c
@@ -954,7 +954,7 @@ int http_request_parse(server *srv, connection *con) {
} else if (cmp > 0 && 0 == (cmp = buffer_caseless_compare(CONST_BUF_LEN(ds->key), CONST_STR_LEN("Content-Length")))) {
char *err;
- unsigned long int r;
+ off_t r;
size_t j, jlen;
if (con_length_set) {
@@ -987,9 +987,9 @@ int http_request_parse(server *srv, connection *con) {
}
}
- r = strtoul(ds->value->ptr, &err, 10);
+ r = strtoll(ds->value->ptr, &err, 10);
- if (*err == '\0') {
+ if (*err == '\0' && r >= 0) {
con_length_set = 1;
con->request.content_length = r;
} else {
@@ -1236,6 +1236,38 @@ int http_request_parse(server *srv, connection *con) {
return 0;
}
+ {
+ data_string *ds = (data_string *)array_get_element(con->request.headers, "Transfer-Encoding");
+ if (NULL != ds) {
+ if (con->request.http_version == HTTP_VERSION_1_0) {
+ log_error_write(srv, __FILE__, __LINE__, "s",
+ "HTTP/1.0 with Transfer-Encoding (bad HTTP/1.0 proxy?) -> 400");
+ con->keep_alive = 0;
+ con->http_status = 400; /* Bad Request */
+ return 0;
+ }
+
+ if (0 != strcasecmp(ds->value->ptr, "chunked")) {
+ /* Transfer-Encoding might contain additional encodings,
+ * which are not currently supported by lighttpd */
+ con->keep_alive = 0;
+ con->http_status = 501; /* Not Implemented */
+ return 0;
+ }
+
+ /* reset value for Transfer-Encoding, a hop-by-hop header,
+ * which must not be blindly forwarded to backends */
+ buffer_reset(ds->value); /* headers with empty values are ignored */
+
+ con_length_set = 1;
+ con->request.content_length = -1;
+
+ /*(note: ignore whether or not Content-Length was provided)*/
+ ds = (data_string *)array_get_element(con->request.headers, "Content-Length");
+ if (NULL != ds) buffer_reset(ds->value); /* headers with empty values are ignored */
+ }
+ }
+
switch(con->request.http_method) {
case HTTP_METHOD_GET:
case HTTP_METHOD_HEAD:
@@ -1264,31 +1296,12 @@ int http_request_parse(server *srv, connection *con) {
}
break;
default:
- /* require Content-Length if request contains request body */
- if (array_get_element(con->request.headers, "Transfer-Encoding")) {
- /* presence of Transfer-Encoding in request headers requires "chunked"
- * be final encoding in HTTP/1.1. Return 411 Length Required as
- * lighttpd does not support request input transfer-encodings */
- con->keep_alive = 0;
- con->http_status = 411; /* 411 Length Required */
- return 0;
- }
break;
}
/* check if we have read post data */
if (con_length_set) {
- /* don't handle more the SSIZE_MAX bytes in content-length */
- if (con->request.content_length > SSIZE_MAX) {
- con->http_status = 413;
- con->keep_alive = 0;
-
- log_error_write(srv, __FILE__, __LINE__, "sos",
- "request-size too long:", (off_t) con->request.content_length, "-> 413");
- return 0;
- }
-
/* we have content */
if (con->request.content_length != 0) {
return 1;
diff --git a/tests/request.t b/tests/request.t
index a1ff4db0..a073a7c1 100755
--- a/tests/request.t
+++ b/tests/request.t
@@ -8,7 +8,7 @@ BEGIN {
use strict;
use IO::Socket;
-use Test::More tests => 52;
+use Test::More tests => 59;
use LightyTest;
my $tf = LightyTest->new();
@@ -119,6 +119,121 @@ EOF
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 417 } ];
ok($tf->handle_http($t) == 0, 'Continue, Expect');
+# note Transfer-Encoding: chunked tests will fail with 411 Length Required if
+# server.stream-request-body != 0 in lighttpd.conf
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, lc hex');
+
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+A
+0123456789
+0
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, uc hex');
+
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+Test-Trailer: testing
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, with trailer');
+
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+a; comment
+0123456789
+0
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, chunked header comment');
+
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+az
+0123456789
+0
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 400 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked; bad chunked header');
+
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+a
+0123456789xxxxxxxx
+0
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 400 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked; mismatch chunked header size and chunked data size');
+
+$t->{REQUEST} = ( <<EOF
+POST /get-post-len.pl HTTP/1.1
+Host: www.example.org
+Connection: close
+Content-Type: application/x-www-form-urlencoded
+Transfer-Encoding: chunked
+
+a ; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+0123456789
+0
+
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 400 } ];
+ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked; chunked header too long');
+
## ranges
$t->{REQUEST} = ( <<EOF