From 4d7f5737f150c4bab8d3832bab9da62eddfea7d2 Mon Sep 17 00:00:00 2001 From: Glenn Strauss Date: Fri, 16 Dec 2016 11:06:29 -0500 Subject: [PATCH] [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 --- src/base.h | 3 +- src/connections-glue.c | 235 ++++++++++++++++++++++++++++++++++++++++++++++--- src/connections.c | 6 +- src/connections.h | 1 + src/mod_cgi.c | 21 +++-- src/mod_fastcgi.c | 8 ++ src/mod_proxy.c | 9 ++ src/mod_scgi.c | 20 ++--- src/request.c | 57 +++++++----- tests/request.t | 117 +++++++++++++++++++++++- 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} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, lc hex'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, uc hex'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; +ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked, with trailer'); + +$t->{REQUEST} = ( <{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} = ( <{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} = ( <{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} = ( <{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} = ( <