[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
personal/stbuehler/mod-csrf
Glenn Strauss 2016-12-16 11:06:29 -05:00
parent f792d84cf9
commit 4d7f5737f1
10 changed files with 419 additions and 58 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
*

View File

@ -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;

View File

@ -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