/* * 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 #include #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")); #ifdef __COVERITY__ if (NULL == upgrade) return 0; /*(checked by caller)*/ #endif 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; }