diff options
Diffstat (limited to 'src/h1.c')
-rw-r--r-- | src/h1.c | 927 |
1 files changed, 927 insertions, 0 deletions
diff --git a/src/h1.c b/src/h1.c new file mode 100644 index 00000000..e10ca7db --- /dev/null +++ b/src/h1.c @@ -0,0 +1,927 @@ +/* + * h1 - HTTP/1.x protocol layer + * + * Copyright(c) 2023 Glenn Strauss gstrauss()gluelogic.com All rights reserved + * License: BSD 3-clause (same as lighttpd) + */ +#include "first.h" +#include "h1.h" + +#include <stdlib.h> +#include <string.h> + +#include "base.h" +#include "buffer.h" +#include "chunk.h" +#include "fdevent.h" /* FDEVENT_STREAM_REQUEST_BUFMIN */ +#include "http_date.h" +#include "http_header.h" +#include "log.h" +#include "reqpool.h" /* request_reset_ex() */ +#include "request.h" +#include "response.h" /* http_response_reqbody_read_error() */ + + +static int +h1_send_1xx_info (request_st * const r, connection * const con) +{ + /* (Note: prior 1xx intermediate responses may be present in cq) */ + /* (Note: also choosing not to update con->write_request_ts + * which differs from connection_write_chunkqueue()) */ + chunkqueue * const cq = con->write_queue; + off_t written = cq->bytes_out; + + int rc = con->network_write(con, cq, MAX_WRITE_LIMIT); + + written = cq->bytes_out - written; + con->bytes_written_cur_second += written; + if (r->conf.global_bytes_per_second_cnt_ptr) + *(r->conf.global_bytes_per_second_cnt_ptr) += written; + + if (rc < 0) { + request_set_state_error(r, CON_STATE_ERROR); + return 0; /* error */ + } + + if (!chunkqueue_is_empty(cq)) { /* partial write (unlikely) */ + con->is_writable = 0; + if (cq == &r->write_queue) { + /* save partial write of 1xx in separate chunkqueue + * Note: sending of remainder of 1xx might be delayed + * until next set of response headers are sent */ + con->write_queue = chunkqueue_init(NULL); + /* (copy bytes for accounting purposes in event of failure) */ + con->write_queue->bytes_in = cq->bytes_out; /*(yes, bytes_out)*/ + con->write_queue->bytes_out = cq->bytes_out; + chunkqueue_append_chunkqueue(con->write_queue, cq); + } + } + + #if 0 + /* XXX: accounting inconsistency + * 1xx is not currently included in r->resp_header_len, + * so mod_accesslog reporting of %b or %B (FORMAT_BYTES_OUT_NO_HEADER) + * reports all bytes out minus len of final response headers, + * but including 1xx intermediate responses. If 1xx intermediate + * responses were included in r->resp_header_len, then there are a + * few places in the code which must be adjusted to use r->resp_header_done + * instead of (0 == r->resp_header_len) as flag that final response was set + * (Doing the following would "discard" the 1xx len from bytes_out) + */ + r->write_queue.bytes_in = r->write_queue.bytes_out = 0; + #endif + + return 1; /* success */ +} + + +__attribute_cold__ +int +h1_send_1xx (request_st * const r, connection * const con) +{ + /* Make best effort to send HTTP/1.1 1xx intermediate */ + /* (Note: if other modules set response headers *before* the + * handle_response_start hook, and the backends subsequently sends 1xx, + * then the response headers are sent here with 1xx and might be cleared + * by caller (http_response_parse_headers() and http_response_check_1xx()), + * instead of being sent with the final response. + * (e.g. mod_magnet setting response headers, then backend sending 103)) */ + + chunkqueue * const cq = con->write_queue; /*(bypass r->write_queue)*/ + + buffer * const b = chunkqueue_append_buffer_open(cq); + buffer_copy_string_len(b, CONST_STR_LEN("HTTP/1.1 ")); + http_status_append(b, r->http_status); + for (uint32_t i = 0; i < r->resp_headers.used; ++i) { + const data_string * const ds = (data_string *)r->resp_headers.data[i]; + const uint32_t klen = buffer_clen(&ds->key); + const uint32_t vlen = buffer_clen(&ds->value); + if (0 == klen || 0 == vlen) continue; + buffer_append_str2(b, CONST_STR_LEN("\r\n"), ds->key.ptr, klen); + buffer_append_str2(b, CONST_STR_LEN(": "), ds->value.ptr, vlen); + } + buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n")); + chunkqueue_append_buffer_commit(cq); + + if (con->traffic_limit_reached) + return 1; /* success; send later if throttled */ + + return h1_send_1xx_info(r, con); +} + + +static int +h1_send_100_continue (request_st * const r, connection * const con) +{ + /* Make best effort to send "HTTP/1.1 100 Continue" */ + static const char http_100_continue[] = "HTTP/1.1 100 Continue\r\n\r\n"; + + if (con->traffic_limit_reached) + return 1; /* success; skip sending if throttled */ + + chunkqueue * const cq = con->write_queue; /*(bypass r->write_queue)*/ + chunkqueue_append_mem(cq, http_100_continue, sizeof(http_100_continue)-1); + return h1_send_1xx_info(r, con); +} + + +__attribute_cold__ +static void +h1_send_headers_partial_1xx (request_st * const r, buffer * const b) +{ + /* take data in con->write_queue and move into b + * (to be sent prior to final response headers in r->write_queue) */ + connection * const con = r->con; + /*assert(&r->write_queue != con->write_queue);*/ + chunkqueue * const cq = con->write_queue; + con->write_queue = &r->write_queue; + + /*assert(0 == buffer_clen(b));*//*expect empty buffer from caller*/ + uint32_t len = (uint32_t)chunkqueue_length(cq); + /*(expecting MEM_CHUNK(s), so not expecting error reading files)*/ + if (chunkqueue_read_data(cq, buffer_string_prepare_append(b, len), + len, r->conf.errh) < 0) + len = 0; + buffer_truncate(b, len);/*expect initial empty buffer from caller*/ + chunkqueue_free(cq); +} + + +void +h1_send_headers (request_st * const r) +{ + /* disable keep-alive if requested */ + + r->con->keep_alive_idle = r->conf.max_keep_alive_idle; + if (__builtin_expect( (0 == r->conf.max_keep_alive_idle), 0) + || r->con->request_count > r->conf.max_keep_alive_requests) { + r->keep_alive = 0; + } + else if (0 != r->reqbody_length + && r->reqbody_length != r->reqbody_queue.bytes_in + && (NULL == r->handler_module + || 0 == (r->conf.stream_request_body + & (FDEVENT_STREAM_REQUEST + | FDEVENT_STREAM_REQUEST_BUFMIN)))) { + r->keep_alive = 0; + } + + if (light_btst(r->resp_htags, HTTP_HEADER_UPGRADE) + && r->http_version == HTTP_VERSION_1_1) { + http_header_response_set(r, HTTP_HEADER_CONNECTION, + CONST_STR_LEN("Connection"), + CONST_STR_LEN("upgrade")); + } + else if (r->keep_alive <= 0) { + if (r->keep_alive < 0) + http_response_delay(r->con); + http_header_response_set(r, HTTP_HEADER_CONNECTION, + CONST_STR_LEN("Connection"), + CONST_STR_LEN("close")); + } + else if (r->http_version == HTTP_VERSION_1_0) {/*(&& r->keep_alive > 0)*/ + http_header_response_set(r, HTTP_HEADER_CONNECTION, + CONST_STR_LEN("Connection"), + CONST_STR_LEN("keep-alive")); + } + + if (304 == r->http_status + && light_btst(r->resp_htags, HTTP_HEADER_CONTENT_ENCODING)) { + http_header_response_unset(r, HTTP_HEADER_CONTENT_ENCODING, + CONST_STR_LEN("Content-Encoding")); + } + + chunkqueue * const cq = &r->write_queue; + buffer * const b = chunkqueue_prepend_buffer_open(cq); + + if (cq != r->con->write_queue) + h1_send_headers_partial_1xx(r, b); + + buffer_append_string_len(b, + (r->http_version == HTTP_VERSION_1_1) + ? "HTTP/1.1 " + : "HTTP/1.0 ", + sizeof("HTTP/1.1 ")-1); + http_status_append(b, r->http_status); + + /* add all headers */ + for (size_t i = 0, used = r->resp_headers.used; i < used; ++i) { + const data_string * const ds = (data_string *)r->resp_headers.data[i]; + const uint32_t klen = buffer_clen(&ds->key); + const uint32_t vlen = buffer_clen(&ds->value); + if (__builtin_expect( (0 == klen), 0)) continue; + if (__builtin_expect( (0 == vlen), 0)) continue; + if ((ds->key.ptr[0] & 0xdf) == 'X' && http_response_omit_header(r, ds)) + continue; + char * restrict s = buffer_extend(b, klen+vlen+4); + s[0] = '\r'; + s[1] = '\n'; + memcpy(s+2, ds->key.ptr, klen); + s += 2+klen; + s[0] = ':'; + s[1] = ' '; + memcpy(s+2, ds->value.ptr, vlen); + } + + if (!light_btst(r->resp_htags, HTTP_HEADER_DATE)) { + /* HTTP/1.1 and later requires a Date: header */ + /* "\r\nDate: " 8-chars + 30-chars "%a, %d %b %Y %T GMT" + '\0' */ + static unix_time64_t tlast = 0; + static char tstr[40] = "\r\nDate: "; + + /* cache the generated timestamp */ + const unix_time64_t cur_ts = log_epoch_secs; + if (__builtin_expect ( (tlast != cur_ts), 0)) + http_date_time_to_str(tstr+8, sizeof(tstr)-8, (tlast = cur_ts)); + + buffer_append_string_len(b, tstr, 37); + } + + if (!light_btst(r->resp_htags, HTTP_HEADER_SERVER) && r->conf.server_tag) + buffer_append_str2(b, CONST_STR_LEN("\r\nServer: "), + BUF_PTR_LEN(r->conf.server_tag)); + + buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n")); + r->resp_header_len = buffer_clen(b); + + if (r->conf.log_response_header) + log_error_multiline(r->conf.errh, __FILE__, __LINE__, + BUF_PTR_LEN(b), "fd:%d resp: ", r->con->fd); + + chunkqueue_prepend_buffer_commit(cq); + + /*(optimization to use fewer syscalls to send a small response)*/ + off_t cqlen; + if (r->resp_body_finished + && light_btst(r->resp_htags, HTTP_HEADER_CONTENT_LENGTH) + && (cqlen = chunkqueue_length(cq) - r->resp_header_len) > 0 + && cqlen < 16384) + chunkqueue_small_resp_optim(cq); +} + + +__attribute_cold__ +static chunk * +h1_discard_blank_line (chunkqueue * const cq, uint32_t header_len) +{ + /*(separate func only to be able to mark with compiler hint as cold)*/ + chunkqueue_mark_written(cq, header_len); + return cq->first; /* refresh c after chunkqueue_mark_written() */ +} + + +static chunk * +h1_recv_headers_more (connection * const con, chunkqueue * const cq, chunk *c, const size_t olen) +{ + /*(should not be reached by HTTP/2 streams)*/ + /*if (r->http_version == HTTP_VERSION_2) return NULL;*/ + /*(However, new connections over TLS may become HTTP/2 connections via ALPN + * and return from this routine with r->http_version == HTTP_VERSION_2) */ + + if ((NULL == c || NULL == c->next) && con->is_readable > 0) { + con->read_idle_ts = log_monotonic_secs; + if (0 != con->network_read(con, cq, MAX_READ_LIMIT)) { + request_st * const r = &con->request; + request_set_state_error(r, CON_STATE_ERROR); + } + /* check if switched to HTTP/2 (ALPN "h2" during TLS negotiation) */ + request_st * const r = &con->request; + if (r->http_version == HTTP_VERSION_2) return NULL; + } + + if (cq->first != cq->last && 0 != olen) { + const size_t clen = chunkqueue_length(cq); + size_t block = (olen + (16384-1)) & ~(16384-1); + block += (block - olen > 1024 ? 0 : 16384); + chunkqueue_compact_mem(cq, block > clen ? clen : block); + } + + /* detect if data is added to chunk */ + c = cq->first; + return (c && (size_t)c->offset + olen < buffer_clen(c->mem)) + ? c + : NULL; +} + + +#include "plugin_config.h" /* COMP_SERVER_SOCKET COMP_HTTP_REMOTE_IP */ + +__attribute_cold__ +static int +h1_check_upgrade (request_st * const r, connection * const con) +{ + buffer *upgrade = http_header_request_get(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + /*if (NULL == upgrade) return 0;*//*(checked by caller)*/ + + buffer * const http_connection = + http_header_request_get(r, HTTP_HEADER_CONNECTION, + CONST_STR_LEN("Connection")); + if (NULL == http_connection) { + http_header_request_unset(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + return 0; + } + + if (r->http_version == HTTP_VERSION_1_1) { + /* Upgrade: websocket (not handled here) + * (potentially handled by modules elsewhere) */ + + if (!http_header_str_contains_token(BUF_PTR_LEN(upgrade), + CONST_STR_LEN("h2c"))) + return 0; /*(preserve Connection and Upgrade as-is)*/ + + /* Upgrade: h2c + * RFC7540 3.2 Starting HTTP/2 for "http" URIs */ + + if (http_header_str_contains_token(BUF_PTR_LEN(http_connection), + CONST_STR_LEN("HTTP2-Settings"))) { + if (http_dispatch[HTTP_VERSION_2].upgrade_h2c) + http_dispatch[HTTP_VERSION_2].upgrade_h2c(r, con); + } /*else ignore Upgrade: h2c; HTTP2-Settings required for Upgrade: h2c*/ + /*(remove "HTTP2-Settings", even if not listed in "Connection")*/ + http_header_request_unset(r, HTTP_HEADER_HTTP2_SETTINGS, + CONST_STR_LEN("HTTP2-Settings")); + http_header_remove_token(http_connection, + CONST_STR_LEN("HTTP2-Settings")); + } /*(else invalid with HTTP/1.0; remove Connection and Upgrade)*/ + + http_header_request_unset(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + http_header_remove_token(http_connection, CONST_STR_LEN("Upgrade")); + + if (r->http_version != HTTP_VERSION_2) + return 0; + + /*(Upgrade: h2c over cleartext does not have SNI; no COMP_HTTP_HOST)*/ + r->conditional_is_valid = (1 << COMP_SERVER_SOCKET) + | (1 << COMP_HTTP_REMOTE_IP); + /*connection_handle_write(r, con);*//* defer write to network */ + return 1; +} + + +int +h1_recv_headers (request_st * const r, connection * const con) +{ + chunkqueue * const cq = con->read_queue; + chunk *c = cq->first; + uint32_t clen = 0; + uint32_t header_len = 0; + uint8_t keepalive_request_start = 0; + uint8_t pipelined_request_start = 0; + uint8_t discard_blank = 0; + unsigned short hoff[8192]; /* max num header lines + 3; 16k on stack */ + + if (con->request_count > 1) { + discard_blank = 1; + if (cq->bytes_in == r->x.h1.bytes_read_ckpt) { + keepalive_request_start = 1; + if (NULL != c) { /* !chunkqueue_is_empty(cq)) */ + pipelined_request_start = 1; + /* partial header of next request has already been read, + * so optimistically check for more data received on + * socket while processing the previous request */ + con->is_readable = 1; + /*(if partially read next request and unable to read any bytes, + * then will unnecessarily scan again before subsequent read)*/ + } + } + } + + do { + if (NULL == c) continue; + clen = buffer_clen(c->mem) - c->offset; + if (0 == clen) continue; + if (__builtin_expect( (c->offset > USHRT_MAX), 0)) /*(highly unlikely)*/ + chunkqueue_compact_mem_offset(cq); + + hoff[0] = 1; /* number of lines */ + hoff[1] = (unsigned short)c->offset; /* base offset for all lines */ + /*hoff[2] = ...;*/ /* offset from base for 2nd line */ + + header_len = http_header_parse_hoff(c->mem->ptr + c->offset,clen,hoff); + + /* casting to (unsigned short) might truncate, and the hoff[] + * addition might overflow, but max_request_field_size is USHRT_MAX, + * so failure will be detected below */ + const uint32_t max_request_field_size = r->conf.max_request_field_size; + if ((header_len ? header_len : clen) > max_request_field_size + || hoff[0] >= sizeof(hoff)/sizeof(hoff[0])-1) { + log_error(r->conf.errh, __FILE__, __LINE__, "%s", + "oversized request-header -> sending Status 431"); + r->http_status = 431; /* Request Header Fields Too Large */ + r->keep_alive = 0; + return 1; + } + + if (__builtin_expect( (0 != header_len), 1)) { + if (__builtin_expect( (hoff[0] > 1), 1)) + break; /* common case; request headers complete */ + + if (discard_blank) { /* skip one blank line e.g. following POST */ + if (header_len == clen) continue; + const int ch = c->mem->ptr[c->offset+header_len]; + if (ch != '\r' && ch != '\n') { + /* discard prior blank line if next line is not blank */ + discard_blank = 0; + clen = 0;/*(for h1_recv_headers_more() to return c)*/ + c = h1_discard_blank_line(cq, header_len);/*cold*/ + continue; + } /*(else fall through to error out in next block)*/ + } + } + + if (((unsigned char *)c->mem->ptr)[c->offset] < 32) { + /* expecting ASCII method beginning with alpha char + * or HTTP/2 pseudo-header beginning with ':' */ + /*(TLS handshake begins with SYN 0x16 (decimal 22))*/ + log_error(r->conf.errh, __FILE__, __LINE__, "%s (%s)", + c->mem->ptr[c->offset] == 0x16 + ? "unexpected TLS ClientHello on clear port" + : "invalid request-line -> sending Status 400", + con->dst_addr_buf.ptr); + r->http_status = 400; /* Bad Request */ + r->keep_alive = 0; + return 1; + } + } while ((c = h1_recv_headers_more(con, cq, c, clen))); + + if (keepalive_request_start) { + if (cq->bytes_in > r->x.h1.bytes_read_ckpt) { + /* update r->start_hp.tv_sec timestamp when first byte of + * next request is received on a keep-alive connection */ + r->start_hp.tv_sec = log_epoch_secs; + if (r->conf.high_precision_timestamps) + log_clock_gettime_realtime(&r->start_hp); + } + if (pipelined_request_start && c) + con->read_idle_ts = log_monotonic_secs; + } + + if (NULL == c) return 0; /* incomplete request headers */ + + #ifdef __COVERITY__ + if (buffer_clen(c->mem) < hoff[1]) { + return 1; + } + #endif + + char * const hdrs = c->mem->ptr + hoff[1]; + + if (con->request_count > 1) { + /* adjust r->x.h1.bytes_read_ckpt for http_request_stats_bytes_in() + * (headers_len is still in cq; marked written, bytes_out incr below) */ + r->x.h1.bytes_read_ckpt = cq->bytes_out; + /* clear buffers which may have been kept for reporting on keep-alive, + * (e.g. mod_status) */ + request_reset_ex(r); + } + /* RFC7540 3.5 HTTP/2 Connection Preface + * "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + * (Connection Preface MUST be exact match) + * If ALT-SVC used to advertise HTTP/2, then client might start + * http connection (not TLS) sending HTTP/2 connection preface. + * (note: intentionally checking only on initial request) */ + else if (!con->is_ssl_sock && r->conf.h2proto + && hoff[0] == 2 && hoff[2] == 16 + && hdrs[0]=='P' && hdrs[1]=='R' && hdrs[2]=='I' && hdrs[3]==' ') { + r->http_version = HTTP_VERSION_2; + return 0; + } + + r->rqst_header_len = header_len; + if (r->conf.log_request_header) + log_error_multiline(r->conf.errh, __FILE__, __LINE__, + hdrs, header_len, "fd:%d rqst: ", con->fd); + http_request_headers_process(r, hdrs, hoff, con->proto_default_port); + chunkqueue_mark_written(cq, r->rqst_header_len); + + if (light_btst(r->rqst_htags, HTTP_HEADER_UPGRADE) + && 0 == r->http_status + && h1_check_upgrade(r, con)) + return 0; + + return 1; +} + + +__attribute_cold__ +static int +h1_check_expect_100 (request_st * const r, connection * const con) +{ + if (con->is_writable <= 0) + return 1; + + const buffer * const vb = + http_header_request_get(r, HTTP_HEADER_EXPECT, + CONST_STR_LEN("Expect")); + if (NULL == vb) + return 1; + + /* (always unset Expect header so that check is not repeated for request */ + int rc = buffer_eq_icase_slen(vb, CONST_STR_LEN("100-continue")); + http_header_request_unset(r, HTTP_HEADER_EXPECT, + CONST_STR_LEN("Expect")); + if (!rc + || 0 != r->reqbody_queue.bytes_in + || !chunkqueue_is_empty(&r->read_queue) + || !chunkqueue_is_empty(&r->write_queue) + || r->http_version == HTTP_VERSION_1_0) + return 1; + + /* send 100 Continue only if no request body data received yet + * and response has not yet started (checked above) */ + return h1_send_100_continue(r, con); +} + + +static int +h1_cq_compact (chunkqueue * const cq) +{ + /* combine first mem chunk with next non-empty mem chunk + * (loop if next chunk is empty) */ + chunk *c = cq->first; + if (NULL == c) return 0; + const uint32_t mlen = buffer_clen(c->mem) - (size_t)c->offset; + while ((c = c->next)) { + const uint32_t blen = buffer_clen(c->mem) - (size_t)c->offset; + if (0 == blen) continue; + chunkqueue_compact_mem(cq, mlen + blen); + return 1; + } + return 0; +} + + +__attribute_pure__ +static int +h1_chunked_crlf (chunkqueue * const 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_clen(b) - (size_t)c->offset; + if (1 != len) return -1; /* error */ + + while (NULL != (c = c->next)) { + b = c->mem; + len = buffer_clen(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; +} + + +static handler_t +h1_chunked (request_st * const r, chunkqueue * const cq, chunkqueue * const dst_cq) +{ + /* r->conf.max_request_size is in kBytes */ + const off_t max_request_size = (off_t)r->conf.max_request_size << 10; + off_t te_chunked = r->x.h1.te_chunked; + do { + off_t len = chunkqueue_length(cq); + + while (0 == te_chunked) { + char *p; + chunk *c = cq->first; + if (NULL == c) break; + 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)(1uLL<<(8*sizeof(off_t)-5))-1-2) { + log_error(r->conf.errh, __FILE__, __LINE__, + "chunked data size too large -> 400"); + /* 400 Bad Request */ + return http_response_reqbody_read_error(r, 400); + } + te_chunked <<= 4; + te_chunked |= u; + } + if (s == (unsigned char *)c->mem->ptr+c->offset) { /*(no hex)*/ + log_error(r->conf.errh, __FILE__, __LINE__, + "chunked header invalid chars -> 400"); + /* 400 Bad Request */ + return http_response_reqbody_read_error(r, 400); + } + while (*s == ' ' || *s == '\t') ++s; + if (*s != '\r' && *s != ';') { + log_error(r->conf.errh, __FILE__, __LINE__, + "chunked header invalid chars -> 400"); + /* 400 Bad Request */ + return http_response_reqbody_read_error(r, 400); + } + + if (hsz >= 1024) { + /* prevent theoretical integer overflow + * casting to (size_t) and adding 2 (for "\r\n") */ + log_error(r->conf.errh, __FILE__, __LINE__, + "chunked header line too long -> 400"); + /* 400 Bad Request */ + return http_response_reqbody_read_error(r, 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 && h1_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_clen(c->mem) - c->offset + < (off_t)r->conf.max_request_field_size) { + break; + } + else { + /* ignore excessively long trailers; + * disable keep-alive on connection */ + r->keep_alive = 0; + p = c->mem->ptr + buffer_clen(c->mem) + - 4; + } + } + hsz = p + 4 - (c->mem->ptr+c->offset); + /* trailers currently ignored, but could be processed + * here if 0 == (r->conf.stream_request_body & + * & (FDEVENT_STREAM_REQUEST + * |FDEVENT_STREAM_REQUEST_BUFMIN)) + * taking care to reject fields forbidden in trailers, + * making trailers available to CGI and other backends*/ + } + chunkqueue_mark_written(cq, (size_t)hsz); + r->reqbody_length = dst_cq->bytes_in; + break; /* done reading HTTP chunked request body */ + } + + /* consume HTTP chunked header */ + chunkqueue_mark_written(cq, (size_t)hsz); + len = chunkqueue_length(cq); + + if (0 !=max_request_size + && (max_request_size < te_chunked + || max_request_size - te_chunked < dst_cq->bytes_in)) { + log_error(r->conf.errh, __FILE__, __LINE__, + "request-size too long: %lld -> 413", + (long long)(dst_cq->bytes_in + te_chunked)); + /* 413 Payload Too Large */ + return http_response_reqbody_read_error(r, 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_clen(c->mem) - c->offset >= 1024) { + log_error(r->conf.errh, __FILE__, __LINE__, + "chunked header line too long -> 400"); + /* 400 Bad Request */ + return http_response_reqbody_read_error(r, 400); + } + else if (!h1_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(dst_cq, cq, len, + r->conf.errh)) { + /* 500 Internal Server Error */ + return http_response_reqbody_read_error(r, 500); + } + te_chunked -= len; + len = chunkqueue_length(cq); + } + + if (len < te_chunked) break; + + if (2 == te_chunked) { + if (-1 == h1_chunked_crlf(cq)) { + log_error(r->conf.errh, __FILE__, __LINE__, + "chunked data missing end CRLF -> 400"); + /* 400 Bad Request */ + return http_response_reqbody_read_error(r, 400); + } + chunkqueue_mark_written(cq, 2);/*consume \r\n at end of chunk data*/ + te_chunked -= 2; + } + + } while (!chunkqueue_is_empty(cq)); + + r->x.h1.te_chunked = te_chunked; + return HANDLER_GO_ON; +} + + +static handler_t +h1_read_body_unknown (request_st * const r, chunkqueue * const cq, chunkqueue * const dst_cq) +{ + /* r->conf.max_request_size is in kBytes */ + const off_t max_request_size = (off_t)r->conf.max_request_size << 10; + chunkqueue_append_chunkqueue(dst_cq, cq); + if (0 != max_request_size && dst_cq->bytes_in > max_request_size) { + log_error(r->conf.errh, __FILE__, __LINE__, + "request-size too long: %lld -> 413", (long long)dst_cq->bytes_in); + /* 413 Payload Too Large */ + return http_response_reqbody_read_error(r, 413); + } + return HANDLER_GO_ON; +} + + +handler_t +h1_reqbody_read (request_st * const r) +{ + connection * const con = r->con; + chunkqueue * const cq = &r->read_queue; + chunkqueue * const dst_cq = &r->reqbody_queue; + + int is_closed = 0; + + if (con->is_readable > 0) { + con->read_idle_ts = log_monotonic_secs; + const off_t max_per_read = + !(r->conf.stream_request_body /*(if not streaming request body)*/ + & (FDEVENT_STREAM_REQUEST|FDEVENT_STREAM_REQUEST_BUFMIN)) + ? MAX_READ_LIMIT + : (r->conf.stream_request_body & FDEVENT_STREAM_REQUEST_BUFMIN) + ? 16384 /* FDEVENT_STREAM_REQUEST_BUFMIN */ + : 65536; /* FDEVENT_STREAM_REQUEST */ + switch(con->network_read(con, cq, max_per_read)) { + case -1: + request_set_state_error(r, CON_STATE_ERROR); + return HANDLER_ERROR; + case -2: + is_closed = 1; + break; + default: + break; + } + + chunkqueue_remove_finished_chunks(cq); + } + + /* Check for Expect: 100-continue in request headers */ + if (light_btst(r->rqst_htags, HTTP_HEADER_EXPECT) + && !h1_check_expect_100(r, con)) + return HANDLER_ERROR; + + if (r->reqbody_length < 0) { + /*(-1: Transfer-Encoding: chunked, -2: unspecified length)*/ + handler_t rc = (-1 == r->reqbody_length) + ? h1_chunked(r, cq, dst_cq) + : h1_read_body_unknown(r, cq, dst_cq); + if (HANDLER_GO_ON != rc) return rc; + chunkqueue_remove_finished_chunks(cq); + } + else { + off_t len = (off_t)r->reqbody_length - dst_cq->bytes_in; + if (r->reqbody_length <= 64*1024) { + /* don't buffer request bodies <= 64k on disk */ + chunkqueue_steal(dst_cq, cq, len); + } + else if (chunkqueue_length(dst_cq) + len <= 64*1024 + && (!dst_cq->first || dst_cq->first->type == MEM_CHUNK)) { + /* avoid tempfiles when streaming request body to fast backend */ + chunkqueue_steal(dst_cq, cq, len); + } + else if (0 != + chunkqueue_steal_with_tempfiles(dst_cq,cq,len,r->conf.errh)) { + /* writing to temp file failed */ /* Internal Server Error */ + return http_response_reqbody_read_error(r, 500); + } + chunkqueue_remove_finished_chunks(cq); + } + + if (dst_cq->bytes_in == (off_t)r->reqbody_length) { + /* Content is ready */ + r->conf.stream_request_body &= ~FDEVENT_STREAM_REQUEST_POLLIN; + if (r->state == CON_STATE_READ_POST) { + request_set_state(r, CON_STATE_HANDLE_REQUEST); + } + return HANDLER_GO_ON; + } + else if (is_closed) { + #if 0 + return http_response_reqbody_read_error(r, 400); /* Bad Request */ + #endif + return HANDLER_ERROR; + } + else { + r->conf.stream_request_body |= FDEVENT_STREAM_REQUEST_POLLIN; + return (r->conf.stream_request_body & FDEVENT_STREAM_REQUEST) + ? HANDLER_GO_ON + : HANDLER_WAIT_FOR_EVENT; + } +} + + +/* keep in sync with connections.c */ +#define HTTP_LINGER_TIMEOUT 5 + +int +h1_check_timeout (connection * const con, const unix_time64_t cur_ts) +{ + request_st * const r = &con->request; + const int waitevents = fdevent_fdnode_interest(con->fdn); + int changed = 0; + + if (r->state == CON_STATE_CLOSE) { + if (cur_ts - con->close_timeout_ts > HTTP_LINGER_TIMEOUT) + changed = 1; + } + else if (waitevents & FDEVENT_IN) { + /* keep-alive or else expect CON_STATE_READ_POST || CON_STATE_WRITE */ + int keep_alive = con->request_count != 1 && r->state == CON_STATE_READ; + int idle_timeout = keep_alive + ? con->keep_alive_idle + : (int)r->conf.max_read_idle; + if (cur_ts - con->read_idle_ts > idle_timeout) { + if (r->conf.log_request_handling) + log_error(r->conf.errh, __FILE__, __LINE__, + "connection closed - %s timeout: %d", + keep_alive ? "keep-alive" : "read", con->fd); + request_set_state_error(r, CON_STATE_ERROR); + changed = 1; + } + } + + /* max_write_idle timeout currently functions as backend timeout, + * too, after response has been started. + * Although backend timeouts now exist, there is no default for timeouts + * to backends, so were this client timeout now to be changed to check + * for write interest to the client, then timeout would not occur if the + * backend hung and there was no backend read timeout set. Therefore, + * max_write_idle timeout remains timeout for both reading from backend + * and writing to client, though this check here is only for HTTP/1.1. + * In the future, if there were a quick way to detect that a backend + * read timeout was in effect, then this timeout could check for write + * interest to client. (not a priority) */ + /*if (waitevents & FDEVENT_OUT)*/ + if (r->http_version <= HTTP_VERSION_1_1 /*(func reused by h2, h3)*/ + && r->state == CON_STATE_WRITE && con->write_request_ts != 0) { + #if 0 + if (cur_ts - con->write_request_ts > 60) { + log_error(r->conf.errh, __FILE__, __LINE__, + "connection closed - pre-write-request-timeout: %d %d", + con->fd, cur_ts - con->write_request_ts); + } + #endif + + if (cur_ts - con->write_request_ts > r->conf.max_write_idle) { + /* time - out */ + if (r->conf.log_timeouts) { + log_error(r->conf.errh, __FILE__, __LINE__, + "NOTE: a request from %s for %.*s timed out after writing " + "%lld bytes. We waited %d seconds. If this is a problem, " + "increase server.max-write-idle", + r->dst_addr_buf->ptr, + BUFFER_INTLEN_PTR(&r->target), + (long long)con->write_queue->bytes_out, + (int)r->conf.max_write_idle); + } + request_set_state_error(r, CON_STATE_ERROR); + changed = 1; + } + } + + return changed; +} |