diff --git a/src/configfile.c b/src/configfile.c index 7aa0c325..3e67ae9a 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -788,6 +788,11 @@ static int config_insert_srvconf(server *srv) { config_plugin_value_tobool( array_get_element_klen(cpv->v.a, CONST_STR_LEN("server.h2proto")), 0); + if (srv->srvconf.h2proto) + srv->srvconf.h2proto += + config_plugin_value_tobool( + array_get_element_klen(cpv->v.a, + CONST_STR_LEN("server.h2c")), 0); break; default:/* should not happen */ break; diff --git a/src/connections.c b/src/connections.c index 0bb889a2..8540ebb3 100644 --- a/src/connections.c +++ b/src/connections.c @@ -7,6 +7,7 @@ #include "log.h" #include "connections.h" #include "fdevent.h" +#include "h2.h" #include "http_header.h" #include "reqpool.h" @@ -129,8 +130,8 @@ static void connection_close(connection *con) { buffer_reset(&r->uri.path); buffer_reset(&r->uri.query); buffer_reset(&r->target_orig); - buffer_reset(&r->target); /*(see comments in connection_reset())*/ - buffer_reset(&r->pathinfo); /*(see comments in connection_reset())*/ + buffer_reset(&r->target); /*(see comments in request_reset())*/ + buffer_reset(&r->pathinfo); /*(see comments in request_reset())*/ chunkqueue_reset(con->read_queue); con->request_count = 0; @@ -228,7 +229,16 @@ static void connection_fdwaitqueue_append(connection *con) { connection_list_append(&con->srv->fdwaitqueue, con); } + static void connection_handle_response_end_state(request_st * const r, connection * const con) { + if (r->http_version > HTTP_VERSION_1_1) { + h2_retire_con(r, con); + r->keep_alive = 0; + /* set a status so that mod_accesslog, mod_rrdtool hooks are called + * in plugins_call_handle_request_done() (XXX: or set to 0 to omit) */ + r->http_status = 100; /* XXX: what if con->state == CON_STATE_ERROR? */ + } + /* call request_done hook if http_status set (e.g. to log request) */ /* (even if error, connection dropped, as long as http_status is set) */ if (r->http_status) plugins_call_handle_request_done(r); @@ -390,9 +400,8 @@ connection_write_100_continue (request_st * const r, connection * const con) } -static void connection_handle_write(connection *con) { +static void connection_handle_write(request_st * const r, connection * const con) { int rc = connection_write_chunkqueue(con, con->write_queue, MAX_WRITE_LIMIT); - request_st * const r = &con->request; switch (rc) { case 0: if (r->resp_body_finished) { @@ -408,7 +417,10 @@ static void connection_handle_write(connection *con) { connection_set_state(r, CON_STATE_ERROR); break; case 1: - con->is_writable = 0; + /* do not spin trying to send HTTP/2 server Connection Preface + * while waiting for TLS negotiation to complete */ + if (con->write_queue->bytes_out) + con->is_writable = 0; /* not finished yet -> WRITE */ break; @@ -418,9 +430,10 @@ static void connection_handle_write(connection *con) { static void connection_handle_write_state(request_st * const r, connection * const con) { do { /* only try to write if we have something in the queue */ - if (!chunkqueue_is_empty(con->write_queue)) { - if (con->is_writable) { - connection_handle_write(con); + if (!chunkqueue_is_empty(r->write_queue)) { + if (r->http_version <= HTTP_VERSION_1_1 && con->is_writable) { + /*(r->write_queue == con->write_queue)*//*(not HTTP/2 stream)*/ + connection_handle_write(r, con); if (r->state != CON_STATE_WRITE) break; } } else if (r->resp_body_finished) { @@ -437,6 +450,9 @@ static void connection_handle_write_state(request_st * const r, connection * con case HANDLER_GO_ON: break; case HANDLER_WAIT_FOR_FD: + /* (In addition to waiting for dispatch from fdwaitqueue, + * HTTP/2 connections may retry more frequently after any + * activity occurs on connection or on other streams) */ connection_fdwaitqueue_append(con); break; case HANDLER_COMEBACK: @@ -451,7 +467,8 @@ static void connection_handle_write_state(request_st * const r, connection * con } } } while (r->state == CON_STATE_WRITE - && (!chunkqueue_is_empty(con->write_queue) + && r->http_version <= HTTP_VERSION_1_1 + && (!chunkqueue_is_empty(r->write_queue) ? con->is_writable : r->resp_body_finished)); } @@ -535,12 +552,20 @@ static void connection_discard_blank_line(request_st * const r, const char * con } static chunk * connection_read_header_more(connection *con, chunkqueue *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) { con->read_idle_ts = log_epoch_secs; if (0 != con->network_read(con, cq, MAX_READ_LIMIT)) { request_st * const r = &con->request; connection_set_state(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) { @@ -558,12 +583,41 @@ static chunk * connection_read_header_more(connection *con, chunkqueue *cq, chun } +static void +connection_transition_h2 (request_st * const h2r, connection * const con) +{ + buffer_copy_string_len(&h2r->target, CONST_STR_LEN("*")); + buffer_copy_string_len(&h2r->target_orig, CONST_STR_LEN("*")); + buffer_copy_string_len(&h2r->uri.path, CONST_STR_LEN("*")); + h2r->http_method = HTTP_METHOD_PRI; + h2r->reqbody_length = -1; /*(unnecessary for h2r?)*/ + h2r->conf.stream_request_body |= FDEVENT_STREAM_REQUEST_POLLIN; + + /* (h2r->state == CON_STATE_READ) for transition by ALPN + * or starting cleartext HTTP/2 with Prior Knowledge + * (e.g. via HTTP Alternative Services) + * (h2r->state == CON_STATE_RESPONSE_END) for Upgrade: h2c */ + + if (h2r->state != CON_STATE_ERROR) + connection_set_state(h2r, CON_STATE_WRITE); + + #if 0 /* ... if it turns out we need a separate fdevent handler for HTTP/2 */ + con->fdn->handler = connection_handle_fdevent_h2; + #endif + + if (NULL == con->h2) /*(not yet transitioned to HTTP/2; not Upgrade: h2c)*/ + h2_init_con(h2r, con, NULL); +} + + /** * handle request header read * * we get called by the state-engine and by the fdevent-handler */ +__attribute_noinline__ static int connection_handle_read_state(connection * const con) { + /*(should not be reached by HTTP/2 streams)*/ chunkqueue * const cq = con->read_queue; chunk *c = cq->first; uint32_t clen = 0; @@ -660,6 +714,18 @@ static int connection_handle_read_state(connection * const con) { buffer_reset(&r->uri.query); buffer_reset(&r->target_orig); } + /* 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) @@ -669,15 +735,25 @@ static int connection_handle_read_state(connection * const con) { http_request_headers_process(r, hdrs, hoff, con->proto_default_port); chunkqueue_mark_written(cq, r->rqst_header_len); connection_set_state(r, CON_STATE_REQUEST_END); + + if (!con->is_ssl_sock && r->conf.h2proto && 0 == r->http_status + && h2_check_con_upgrade_h2c(r)) { + /*(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 0; + } + return 1; } +static void connection_state_machine_h2 (request_st *r, connection *con); + static handler_t connection_handle_fdevent(void *context, int revents) { connection *con = context; - joblist_append(con); - if (con->is_ssl_sock) { /* ssl may read and write for both reads and writes */ if (revents & (FDEVENT_IN | FDEVENT_OUT)) { @@ -696,14 +772,25 @@ static handler_t connection_handle_fdevent(void *context, int revents) { request_st * const r = &con->request; - if (r->state == CON_STATE_READ) { - connection_handle_read_state(con); + if (r->http_version == HTTP_VERSION_2) { + connection_state_machine_h2(r, con); + if (-1 == con->fd) /*(con closed; CON_STATE_CONNECT)*/ + return HANDLER_FINISHED; } + else { + joblist_append(con); - if (r->state == CON_STATE_WRITE && - !chunkqueue_is_empty(con->write_queue) && - con->is_writable) { - connection_handle_write(con); + if (r->state == CON_STATE_READ) { + if (!connection_handle_read_state(con) + && r->http_version == HTTP_VERSION_2) + connection_transition_h2(r, con); + } + + if (r->state == CON_STATE_WRITE && + !chunkqueue_is_empty(con->write_queue) && + con->is_writable) { + connection_handle_write(r, con); + } } if (r->state == CON_STATE_CLOSE) { @@ -958,6 +1045,7 @@ connection_state_machine_loop (request_st * const r, connection * const con) switch ((ostate = r->state)) { case CON_STATE_REQUEST_START: /* transient */ + /*(should not be reached by HTTP/2 streams)*/ r->start_ts = con->read_idle_ts = log_epoch_secs; if (r->conf.high_precision_timestamps) log_clock_gettime_realtime(&r->start_hp); @@ -968,7 +1056,15 @@ connection_state_machine_loop (request_st * const r, connection * const con) connection_set_state(r, CON_STATE_READ); /* fall through */ case CON_STATE_READ: - if (!connection_handle_read_state(con)) break; + /*(should not be reached by HTTP/2 streams)*/ + if (!connection_handle_read_state(con)) { + if (r->http_version == HTTP_VERSION_2) { + connection_transition_h2(r, con); + connection_state_machine_h2(r, con); + return; + } + break; + } /*if (r->state != CON_STATE_REQUEST_END) break;*/ /* fall through */ case CON_STATE_REQUEST_END: /* transient */ @@ -1000,6 +1096,8 @@ connection_state_machine_loop (request_st * const r, connection * const con) /* fall through */ /*case CON_STATE_RESPONSE_START:*//*occurred;transient*/ http_response_write_header(r); + if (r->http_version > HTTP_VERSION_1_1) + h2_send_cqheaders(r, con); connection_set_state(r, CON_STATE_WRITE); /* fall through */ case CON_STATE_WRITE: @@ -1008,9 +1106,12 @@ connection_state_machine_loop (request_st * const r, connection * const con) /* fall through */ case CON_STATE_RESPONSE_END: /* transient */ case CON_STATE_ERROR: /* transient */ + if (r->http_version > HTTP_VERSION_1_1 && r != &con->request) + return; connection_handle_response_end_state(r, con); break; case CON_STATE_CLOSE: + /*(should not be reached by HTTP/2 streams)*/ connection_handle_close_state(con); break; case CON_STATE_CONNECT: @@ -1048,6 +1149,8 @@ connection_set_fdevent_interest (request_st * const r, connection * const con) case CON_STATE_CLOSE: n = FDEVENT_IN; break; + case CON_STATE_CONNECT: + return; default: break; } @@ -1075,10 +1178,159 @@ connection_set_fdevent_interest (request_st * const r, connection * const con) } -void connection_state_machine(connection *con) { - request_st * const r = &con->request; - const int log_state_handling = r->conf.log_state_handling; +static void +connection_state_machine_h2 (request_st * const h2r, connection * const con) +{ + h2con * const h2c = con->h2; + + if (h2c->sent_goaway <= 0 + && (chunkqueue_is_empty(con->read_queue) || h2_parse_frames(con)) + && con->is_readable) { + chunkqueue * const cq = con->read_queue; + const off_t mark = cq->bytes_in; + if (0 == con->network_read(con, cq, MAX_READ_LIMIT)) { + if (mark < cq->bytes_in) + h2_parse_frames(con); + } + else { + /* network error; do not send GOAWAY, but pretend that we did */ + h2c->sent_goaway = H2_E_CONNECT_ERROR; /*any error (not NO_ERROR)*/ + connection_set_state(h2r, CON_STATE_ERROR); + } + } + + /* process requests on HTTP/2 streams */ + int resched = 0; + if (h2c->sent_goaway <= 0 && h2c->rused) { + /* coarse check for write throttling + * (connection.kbytes-per-second, server.kbytes-per-second) + * obtain an approximate limit, not refreshed per request_st, + * even though we are not calculating response HEADERS frames + * or frame overhead here */ + off_t max_bytes = con->is_writable + ? connection_write_throttle(con, MAX_WRITE_LIMIT) + : 0; + const off_t fsize = (off_t)h2c->s_max_frame_size; + + /* XXX: to avoid buffer bloat due to staging too much data in + * con->write_queue, consider setting limit on how much is staged + * for sending on con->write_queue: adjusting max_bytes down */ + + /* XXX: TODO: process requests in stream priority order */ + for (uint32_t i = 0; i < h2c->rused; ++i) { + request_st * const r = h2c->r[i]; + /* future: might track read/write interest per request + * to avoid iterating through all active requests */ + + /* XXX: h2.c manages r->h2state, but does not modify r->state + * (might revisit later and allow h2.c to modify both) */ + if (r->state < CON_STATE_REQUEST_END + && (r->h2state == H2_STATE_OPEN + || r->h2state == H2_STATE_HALF_CLOSED_REMOTE)) + connection_set_state(r, CON_STATE_REQUEST_END); + else if (r->h2state == H2_STATE_CLOSED + && r->state != CON_STATE_ERROR) + connection_set_state(r, CON_STATE_ERROR); + + #if 0 /*(done in connection_state_machine(), but w/o stream id)*/ + const int log_state_handling = r->conf.log_state_handling; + if (log_state_handling) + log_error(r->conf.errh, __FILE__, __LINE__, + "state at enter %d %d %s", con->fd, r->h2id, + connection_get_state(r->state)); + #endif + + connection_state_machine_loop(r, con); + + if (r->resp_header_len && !chunkqueue_is_empty(r->write_queue) + && (r->resp_body_finished || r->conf.stream_response_body)) { + + chunkqueue * const cq = r->write_queue; + off_t avail = cq->bytes_in - cq->bytes_out; + if (avail > max_bytes) avail = max_bytes; + if (avail > fsize) avail = fsize; + if (avail > r->h2_swin) avail = r->h2_swin; + if (avail > h2r->h2_swin) avail = h2r->h2_swin; + + if (avail > 0) { + max_bytes -= avail; + h2_send_cqdata(r, con, cq, (uint32_t)avail); + } + + if (r->resp_body_finished && chunkqueue_is_empty(cq)) { + connection_set_state(r, CON_STATE_RESPONSE_END); + if (r->conf.log_state_handling) + connection_state_machine_loop(r, con); + } + else if (avail) /*(do not spin if swin empty window)*/ + resched |= (!chunkqueue_is_empty(cq)); + } + + #if 0 /*(done in connection_state_machine(), but w/o stream id)*/ + /* XXX: TODO: r is invalid if retired; not properly handled here */ + if (log_state_handling) + log_error(r->conf.errh, __FILE__, __LINE__, + "state at exit %d %d %s", con->fd, r->h2id, + connection_get_state(r->state)); + #endif + + if (r->state==CON_STATE_RESPONSE_END || r->state==CON_STATE_ERROR) { + /*(trigger reschedule of con if frames pending)*/ + if (h2c->rused == sizeof(h2c->r)/sizeof(*h2c->r) + && !chunkqueue_is_empty(con->read_queue)) + resched |= 1; + h2_send_end_stream(r, con); + h2_retire_stream(r, con);/*r invalidated;removed from h2c->r[]*/ + --i;/* adjust loop i; h2c->rused was modified to retire r */ + } + } + } + + if (h2c->sent_goaway > 0 && h2c->rused) { + /* retire streams if an error has occurred + * note: this is not done to other streams in the loop above + * (besides the current stream in the loop) due to the specific + * implementation above, where doing so would mess up the iterator */ + for (uint32_t i = 0; i < h2c->rused; ++i) { + request_st * const r = h2c->r[i]; + /*assert(r->h2state == H2_STATE_CLOSED);*/ + h2_retire_stream(r, con);/*r invalidated;removed from h2c->r[]*/ + --i;/* adjust loop i; h2c->rused was modified to retire r */ + } + /* XXX: ? should we discard con->write_queue + * and change h2r->state to CON_STATE_RESPONSE_END ? */ + } + + if (h2r->state == CON_STATE_WRITE) { + /* write HTTP/2 frames to socket */ + if (!chunkqueue_is_empty(con->write_queue) && con->is_writable) + connection_handle_write(h2r, con); + + if (chunkqueue_is_empty(con->write_queue) + && 0 == h2c->rused && h2c->sent_goaway) + connection_set_state(h2r, CON_STATE_RESPONSE_END); + } + + if (h2r->state == CON_STATE_WRITE) { + if (resched && !con->traffic_limit_reached) + joblist_append(con); + + if (h2_want_read(con)) + h2r->conf.stream_request_body |= FDEVENT_STREAM_REQUEST_POLLIN; + else + h2r->conf.stream_request_body &= ~FDEVENT_STREAM_REQUEST_POLLIN; + } + else /* e.g. CON_STATE_RESPONSE_END or CON_STATE_ERROR */ + connection_state_machine_loop(h2r, con); + + connection_set_fdevent_interest(h2r, con); +} + +static void +connection_state_machine_h1 (request_st * const r, connection * const con) +{ + const int log_state_handling = r->conf.log_state_handling; if (log_state_handling) { log_error(r->conf.errh, __FILE__, __LINE__, "state at enter %d %s", con->fd, connection_get_state(r->state)); @@ -1094,6 +1346,18 @@ void connection_state_machine(connection *con) { connection_set_fdevent_interest(r, con); } + +void +connection_state_machine (connection * const con) +{ + request_st * const r = &con->request; + if (r->http_version == HTTP_VERSION_2) + connection_state_machine_h2(r, con); + else /* if (r->http_version <= HTTP_VERSION_1_1) */ + connection_state_machine_h1(r, con); +} + + static void connection_check_timeout (connection * const con, const time_t cur_ts) { const int waitevents = fdevent_fdnode_interest(con->fdn); int changed = 0; @@ -1104,7 +1368,73 @@ static void connection_check_timeout (connection * const con, const time_t cur_t if (cur_ts - con->close_timeout_ts > HTTP_LINGER_TIMEOUT) { changed = 1; } - } else if (waitevents & FDEVENT_IN) { + } + else if (con->h2 && r->state == CON_STATE_WRITE) { + h2con * const h2c = con->h2; + if (h2c->rused) { + for (uint32_t i = 0; i < h2c->rused; ++i) { + request_st * const rr = h2c->r[i]; + if (rr->state == CON_STATE_ERROR) { /*(should not happen)*/ + changed = 1; + continue; + } + if (rr->reqbody_length != rr->reqbody_queue->bytes_in) { + /* XXX: should timeout apply if not trying to read on h2con? + * (still applying timeout to catch stuck connections) */ + /* XXX: con->read_idle_ts is not per-request, so timeout + * will not occur if other read activity occurs on h2con + * (future: might keep separate timestamp per-request) */ + if (cur_ts - con->read_idle_ts > rr->conf.max_read_idle) { + /* time - out */ + if (rr->conf.log_request_handling) { + log_error(rr->conf.errh, __FILE__, __LINE__, + "request aborted - read timeout: %d", con->fd); + } + connection_set_state(r, CON_STATE_ERROR); + changed = 1; + } + } + + if (rr->state != CON_STATE_READ_POST + && con->write_request_ts != 0) { + /* XXX: con->write_request_ts is not per-request, so timeout + * will not occur if other write activity occurs on h2con + * (future: might keep separate timestamp per-request) */ + if (cur_ts - con->write_request_ts + > r->conf.max_write_idle) { + /*(see comment further down about 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", + BUFFER_INTLEN_PTR(con->dst_addr_buf), + BUFFER_INTLEN_PTR(&r->target), + (long long)r->write_queue->bytes_out, + (int)r->conf.max_write_idle); + } + connection_set_state(r, CON_STATE_ERROR); + changed = 1; + } + } + } + } + else { + if (cur_ts - con->read_idle_ts > con->keep_alive_idle) { + /* time - out */ + if (r->conf.log_request_handling) { + log_error(r->conf.errh, __FILE__, __LINE__, + "connection closed - keep-alive timeout: %d", + con->fd); + } + connection_set_state(r, CON_STATE_RESPONSE_END); + changed = 1; + } + } + } + else if (waitevents & FDEVENT_IN) { if (con->request_count == 1 || r->state != CON_STATE_READ) { /* e.g. CON_STATE_READ_POST || CON_STATE_WRITE */ if (cur_ts - con->read_idle_ts > r->conf.max_read_idle) { @@ -1137,8 +1467,8 @@ static void connection_check_timeout (connection * const con, const time_t cur_t * future: have separate backend timeout, and then change this * to check for write interest before checking for timeout */ /*if (waitevents & FDEVENT_OUT)*/ - if ((r->state == CON_STATE_WRITE) && - (con->write_request_ts != 0)) { + if (r->http_version <= HTTP_VERSION_1_1 + && 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__, @@ -1163,6 +1493,10 @@ static void connection_check_timeout (connection * const con, const time_t cur_t } } + /* lighttpd HTTP/2 limitation: rate limit config r->conf.bytes_per_second + * (currently) taken only from top-level config (socket), with host if SNI + * used, but not any other config conditions, e.g. not per-file-type */ + if (0 == (t_diff = cur_ts - con->connection_start)) t_diff = 1; if (con->traffic_limit_reached && @@ -1204,6 +1538,13 @@ void connection_graceful_shutdown_maint (server *srv) { if (log_epoch_secs - con->close_timeout_ts > HTTP_LINGER_TIMEOUT) changed = 1; } + else if (con->h2 && r->state == CON_STATE_WRITE) { + h2_send_goaway(con, H2_E_NO_ERROR); + if (0 == con->h2->rused && chunkqueue_is_empty(con->write_queue)) { + connection_set_state(r, CON_STATE_RESPONSE_END); + changed = 1; + } + } else if (r->state == CON_STATE_READ && con->request_count > 1 && chunkqueue_is_empty(con->read_queue)) { /* close connections in keep-alive waiting for next request */ @@ -1471,7 +1812,12 @@ connection_handle_read_post_state (request_st * const r) int is_closed = 0; - if (con->is_readable) { + if (r->http_version > HTTP_VERSION_1_1) { + /*(H2_STATE_HALF_CLOSED_REMOTE or H2_STATE_CLOSED)*/ + if (r->h2state >= H2_STATE_HALF_CLOSED_REMOTE) + is_closed = 1; + } + else if (con->is_readable) { con->read_idle_ts = log_epoch_secs; switch(con->network_read(con, cq, MAX_READ_LIMIT)) { @@ -1500,12 +1846,17 @@ connection_handle_read_post_state (request_st * const r) && buffer_eq_icase_slen(vb, CONST_STR_LEN("100-continue"))) { http_header_request_unset(r, HTTP_HEADER_EXPECT, CONST_STR_LEN("Expect")); - if (!connection_write_100_continue(r, con)) + if (r->http_version > HTTP_VERSION_1_1) + h2_send_100_continue(r, con); + else if (!connection_write_100_continue(r, con)) return HANDLER_ERROR; } } - if (r->reqbody_length < 0) { + if (r->http_version > HTTP_VERSION_1_1) { + /* h2_recv_data() places frame payload directly into r->reqbody_queue */ + } + else if (r->reqbody_length < 0) { /*(-1: Transfer-Encoding: chunked, -2: unspecified length)*/ handler_t rc = (-1 == r->reqbody_length) ? connection_handle_read_post_chunked(r, cq, dst_cq) diff --git a/src/h2.c b/src/h2.c index 84e5e8c0..bfb02941 100644 --- a/src/h2.c +++ b/src/h2.c @@ -22,7 +22,6 @@ static request_st * h2_init_stream (request_st * const h2r, connection * const con); -static void h2_retire_stream (request_st * const r, connection * const con); static void @@ -1375,6 +1374,8 @@ h2_init_con (request_st * const restrict h2r, connection * const restrict con, c h2con * const h2c = calloc(1, sizeof(h2con)); force_assert(h2c); con->h2 = h2c; + con->read_idle_ts = log_epoch_secs; + con->keep_alive_idle = h2r->conf.max_keep_alive_idle; h2r->h2_rwin = 65535; /* h2 connection recv window */ h2r->h2_swin = 65535; /* h2 connection send window */ @@ -1778,13 +1779,307 @@ h2_send_end_stream (request_st * const r, connection * const con) } +/* + * (XXX: might move below to separate file) + */ +#include "base64.h" +#include "chunk.h" +#include "plugin.h" +#include "plugin_config.h" +#include "reqpool.h" + + static request_st * h2_init_stream (request_st * const h2r, connection * const con) { + h2con * const h2c = con->h2; + ++con->request_count; + force_assert(h2c->rused < sizeof(h2c->r)/sizeof(*h2c->r)); + /* initialize stream as subrequest (request_st *) */ + request_st * const r = calloc(1, sizeof(request_st)); + force_assert(r); + /* XXX: TODO: assign default priority, etc. + * Perhaps store stream id and priority in separate table */ + h2c->r[h2c->rused++] = r; + server * const srv = con->srv; + request_init(r, con, srv); + r->h2_rwin = h2c->s_initial_window_size; + r->h2_swin = h2c->s_initial_window_size; + r->http_version = HTTP_VERSION_2; + + /* copy config state from h2r */ + const uint32_t used = srv->config_context->used; + r->conditional_is_valid = h2r->conditional_is_valid; + memcpy(r->cond_cache, h2r->cond_cache, used * sizeof(cond_cache_t)); + #ifdef HAVE_PCRE_H + if (used > 1) /*(save 128b per con if no conditions)*/ + memcpy(r->cond_match, h2r->cond_match, used * sizeof(cond_match_t)); + #endif + r->server_name = h2r->server_name; + memcpy(&r->conf, &h2r->conf, sizeof(request_config)); + + /* stream id must be assigned by caller */ + return r; } static void h2_release_stream (request_st * const r, connection * const con) { + if (r->http_status) { + /* (see comment in connection_handle_response_end_state()) */ + plugins_call_handle_request_done(r); + + #if 0 + /* (fuzzy accounting for mod_accesslog, mod_rrdtool to avoid + * double counting, but HTTP/2 framing and HPACK-encoded headers in + * con->read_queue and con->write_queue are not equivalent to the + * HPACK-decoded headers and request and response bodies in stream + * r->read_queue and r->write_queue) */ + /* DISABLED since mismatches invalidate the relationship between + * con->bytes_in and con->bytes_out */ + con->read_queue->bytes_in -= r->read_queue->bytes_in; + con->write_queue->bytes_out -= r->write_queue->bytes_out; + #else + UNUSED(con); + #endif + } + + request_reset(r); + /* future: might keep a pool of reusable (request_st *) */ + request_free(r); + free(r); +} + + +void +h2_retire_stream (request_st *r, connection * const con) +{ + if (r == NULL) return; /*(should not happen)*/ + h2con * const h2c = con->h2; + request_st ** const ar = h2c->r; + for (uint32_t i = 0, j = 0, rused = h2c->rused; i < rused; ++i) { + if (ar[i] != r) + ar[j++] = ar[i]; + else { + h2_release_stream(r, con); + r = NULL; + } + } + if (r == NULL) /* found */ + h2c->r[--h2c->rused] = NULL; + /*else ... should not happen*/ +} + + +void +h2_retire_con (request_st * const h2r, connection * const con) +{ + h2con * const h2c = con->h2; + if (NULL == h2c) return; + + if (h2r->state != CON_STATE_ERROR) { /*(CON_STATE_RESPONSE_END)*/ + h2_send_goaway(con, H2_E_NO_ERROR); + for (uint32_t i = 0, rused = h2c->rused; i < rused; ++i) { + /*(unexpected if CON_STATE_RESPONSE_END)*/ + request_st * const r = h2c->r[i]; + h2_send_rst_stream(r, con, H2_E_INTERNAL_ERROR); + h2_release_stream(r, con); + } + if (!chunkqueue_is_empty(con->write_queue)) { + /* similar to connection_handle_write() but without error checks, + * without MAX_WRITE_LIMIT, and without connection throttling */ + /*h2r->conf.bytes_per_second = 0;*/ /* disable rate limit */ + /*h2r->conf.global_bytes_per_second = 0;*/ /* disable rate limit */ + /*con->traffic_limit_reached = 0;*/ + chunkqueue * const cq = con->write_queue; + const off_t len = chunkqueue_length(cq); + off_t written = cq->bytes_out; + con->network_write(con, cq, len); + /*(optional accounting)*/ + written = cq->bytes_out - written; + con->bytes_written += written; + con->bytes_written_cur_second += written; + if (h2r->conf.global_bytes_per_second_cnt_ptr) + *(h2r->conf.global_bytes_per_second_cnt_ptr) += written; + } + } + else { /* CON_STATE_ERROR */ + for (uint32_t i = 0, rused = h2c->rused; i < rused; ++i) { + request_st * const r = h2c->r[i]; + h2_release_stream(r, con); + } + /* XXX: perhaps attempt to send GOAWAY? Not when CON_STATE_ERROR */ + } + + con->h2 = NULL; + + /* future: might keep a pool of reusable (h2con *) */ + free(h2c); +} + + +static void +h2_con_upgrade_h2c (request_st * const h2r, const buffer * const http2_settings) +{ + /* status: (h2r->state == CON_STATE_REQUEST_END) for Upgrade: h2c */ + + /* HTTP/1.1 101 Switching Protocols + * Connection: Upgrade + * Upgrade: h2c + */ + #if 1 + static const char switch_proto[] = "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: Upgrade\r\n" + "Upgrade: h2c\r\n\r\n"; + chunkqueue_append_mem(h2r->write_queue, + CONST_STR_LEN(switch_proto)); + h2r->resp_header_len = sizeof(switch_proto)-1; + #else + h2r->http_status = 101; + http_header_response_set(h2r, HTTP_HEADER_UPGRADE, CONST_STR_LEN("Upgrade"), + CONST_STR_LEN("h2c")); + http_response_write_header(h2r); + http_response_reset(h2r); + h2r->http_status = 0; + #endif + + connection * const con = h2r->con; + h2_init_con(h2r, con, http2_settings); + if (con->h2->sent_goaway) return; + + con->h2->h2_cid = 1; /* stream id 1 is assigned to h2c upgrade */ + + /* copy request state from &con->request to subrequest r + * XXX: would be nice if there were a cleaner way to do this + * (This is fragile and must be kept in-sync with request_st in request.h)*/ + + request_st * const r = h2_init_stream(h2r, con); + /*(undo double-count; already incremented in CON_STATE_REQUEST_START)*/ + --con->request_count; + r->state = h2r->state; /* CON_STATE_REQUEST_END */ + r->http_status = 0; + r->http_method = h2r->http_method; + r->h2state = H2_STATE_HALF_CLOSED_REMOTE; + r->h2id = 1; + r->rqst_htags = h2r->rqst_htags; + h2r->rqst_htags = 0; + r->rqst_header_len = h2r->rqst_header_len; + h2r->rqst_header_len = 0; + r->rqst_headers = h2r->rqst_headers; /* copy struct */ + memset(&h2r->rqst_headers, 0, sizeof(array)); + r->uri = h2r->uri; /* copy struct */ + #if 0 + r->physical = h2r->physical; /* copy struct */ + r->env = h2r->env; /* copy struct */ + #endif + memset(&h2r->rqst_headers, 0, sizeof(array)); + memset(&h2r->uri, 0, sizeof(request_uri)); + #if 0 + memset(&h2r->physical, 0, sizeof(physical)); + memset(&h2r->env, 0, sizeof(array)); + #endif + #if 0 /* expect empty request body */ + r->reqbody_length = h2r->reqbody_length; /* currently always 0 */ + r->te_chunked = h2r->te_chunked; /* must be 0 */ + swap(r->reqbody_queue, h2r->reqbody_queue); /*currently always empty queue*/ + #endif + r->http_host = h2r->http_host; + h2r->http_host = NULL; + #if 0 + r->server_name = h2r->server_name; + h2r->server_name = NULL; + #endif + r->target = h2r->target; /* copy struct */ + r->target_orig = h2r->target_orig; /* copy struct */ + #if 0 + r->pathinfo = h2r->pathinfo; /* copy struct */ + r->server_name_buf = h2r->server_name_buf; /* copy struct */ + #endif + memset(&h2r->target, 0, sizeof(buffer)); + memset(&h2r->target_orig, 0, sizeof(buffer)); + #if 0 + memset(&h2r->pathinfo, 0, sizeof(buffer)); + memset(&h2r->server_name_buf, 0, sizeof(buffer)); + #endif + #if 0 + /* skip copying response structures, other state not yet modified in h2r */ + /* r write_queue and read_queue are intentionally separate from h2r */ + /* r->gw_dechunk must be NULL for HTTP/2 */ + /* bytes_written_ckpt and bytes_read_ckpt are for HTTP/1.1 */ + /* error handlers have not yet been set */ + #endif + #if 0 + r->loops_per_request = h2r->loops_per_request; + r->async_callback = h2r->async_callback; + #endif + r->keep_alive = h2r->keep_alive; + r->tmp_buf = h2r->tmp_buf; /* shared; same as srv->tmp_buf */ + r->start_hp = h2r->start_hp; /* copy struct */ + r->start_ts = h2r->start_ts; + + /* Note: HTTP/1.1 101 Switching Protocols is not immediately written to + * the network here. As this is called from cleartext Upgrade: h2c, + * we choose to delay sending the status until the beginning of the response + * to the HTTP/1.1 request which included Upgrade: h2c */ +} + + +int +h2_check_con_upgrade_h2c (request_st * const r) +{ + /* RFC7540 3.2 Starting HTTP/2 for "http" URIs */ + + buffer *http_connection, *http2_settings; + buffer *upgrade = http_header_request_get(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + if (NULL == upgrade) return 0; + 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) { + http_header_request_unset(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + http_header_remove_token(http_connection, CONST_STR_LEN("Upgrade")); + return 0; + } + + if (!http_header_str_contains_token(CONST_BUF_LEN(upgrade), + CONST_STR_LEN("h2c"))) + return 0; + + http2_settings = http_header_request_get(r, HTTP_HEADER_HTTP2_SETTINGS, + CONST_STR_LEN("HTTP2-Settings")); + if (NULL != http2_settings) { + if (0 == r->reqbody_length) { + buffer * const b = r->tmp_buf; + buffer_clear(b); + if (r->conf.h2proto > 1/*(must be enabled with server.h2c feature)*/ + && + http_header_str_contains_token(CONST_BUF_LEN(http_connection), + CONST_STR_LEN("HTTP2-Settings")) + && buffer_append_base64_decode(b, CONST_BUF_LEN(http2_settings), + BASE64_URL)) { + h2_con_upgrade_h2c(r, b); + r->http_version = HTTP_VERSION_2; + } /* else ignore if invalid base64 */ + } + else { + /* ignore Upgrade: h2c if request body present since we do not + * (currently) handle request body before transition to h2c */ + /* RFC7540 3.2 Requests that contain a payload body MUST be sent + * in their entirety before the client can send HTTP/2 frames. */ + } + 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 ignore Upgrade: h2c; HTTP2-Settings required for Upgrade: h2c */ + http_header_request_unset(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + http_header_remove_token(http_connection, CONST_STR_LEN("Upgrade")); + return (r->http_version == HTTP_VERSION_2); } diff --git a/src/h2.h b/src/h2.h index 0ed7eeca..5a278f6d 100644 --- a/src/h2.h +++ b/src/h2.h @@ -106,4 +106,12 @@ void h2_send_cqdata (request_st *r, connection *con, struct chunkqueue *cq, uint void h2_send_end_stream (request_st *r, connection *con); +void h2_retire_stream (request_st *r, connection *con); + +void h2_retire_con (request_st *h2r, connection *con); + +__attribute_cold__ +__attribute_noinline__ +int h2_check_con_upgrade_h2c (request_st *r); + #endif diff --git a/src/http-header-glue.c b/src/http-header-glue.c index 12f3cdd5..2c84cd4f 100644 --- a/src/http-header-glue.c +++ b/src/http-header-glue.c @@ -878,7 +878,8 @@ void http_response_backend_done (request_st * const r) { } /* else fall through */ case CON_STATE_WRITE: if (!r->resp_body_finished) { - http_chunk_close(r); + if (r->http_version == HTTP_VERSION_1_1) + http_chunk_close(r); r->resp_body_finished = 1; } default: diff --git a/src/http_kv.c b/src/http_kv.c index 45f1ded1..d10cb27e 100644 --- a/src/http_kv.c +++ b/src/http_kv.c @@ -65,6 +65,7 @@ static const keyvalue http_methods[] = { { HTTP_METHOD_UPDATEREDIRECTREF, CONST_LEN_STR("UPDATEREDIRECTREF") }, { HTTP_METHOD_VERSION_CONTROL, CONST_LEN_STR("VERSION-CONTROL") }, + { HTTP_METHOD_PRI, CONST_LEN_STR("PRI") }, { HTTP_METHOD_UNSET, 0, NULL } }; diff --git a/src/http_kv.h b/src/http_kv.h index ec82d6ba..a3845677 100644 --- a/src/http_kv.h +++ b/src/http_kv.h @@ -15,6 +15,7 @@ */ typedef enum { + HTTP_METHOD_PRI = -2, /* [RFC7540], Section 3.5 */ HTTP_METHOD_UNSET = -1, HTTP_METHOD_GET, /* [RFC2616], Section 9.3 */ HTTP_METHOD_HEAD, /* [RFC2616], Section 9.4 */ diff --git a/src/reqpool.c b/src/reqpool.c index 88156fd8..3b71b815 100644 --- a/src/reqpool.c +++ b/src/reqpool.c @@ -25,6 +25,8 @@ request_init (request_st * const r, connection * const con, server * const srv) r->read_queue = chunkqueue_init(); r->reqbody_queue = chunkqueue_init(); + r->http_method = HTTP_METHOD_UNSET; + r->http_version = HTTP_VERSION_UNSET; r->resp_header_len = 0; r->loops_per_request = 0; r->con = con; @@ -57,6 +59,8 @@ request_reset (request_st * const r) r->resp_header_len = 0; r->loops_per_request = 0; + r->h2state = 0; /* H2_STATE_IDLE */ + r->h2id = 0; r->http_method = HTTP_METHOD_UNSET; r->http_version = HTTP_VERSION_UNSET; diff --git a/src/request.c b/src/request.c index 18ab388e..44cd7d4f 100644 --- a/src/request.c +++ b/src/request.c @@ -472,8 +472,11 @@ static int http_request_parse_single_header(request_st * const restrict r, const } break; case HTTP_HEADER_TRANSFER_ENCODING: - if (HTTP_VERSION_1_0 == r->http_version) { - return http_request_header_line_invalid(r, 400, "HTTP/1.0 with Transfer-Encoding (bad HTTP/1.0 proxy?) -> 400"); + if (HTTP_VERSION_1_1 != r->http_version) { + return http_request_header_line_invalid(r, 400, + HTTP_VERSION_1_0 == r->http_version + ? "HTTP/1.0 with Transfer-Encoding (bad HTTP/1.0 proxy?) -> 400" + : "HTTP/2 with Transfer-Encoding is invalid -> 400"); } if (!buffer_eq_icase_ss(v, vlen, CONST_STR_LEN("chunked"))) { @@ -613,7 +616,7 @@ static int http_request_parse_pseudohdrs(request_st * const restrict r, const ch if (HTTP_METHOD_UNSET != r->http_method) return http_request_header_line_invalid(r, 400, "repeated pseudo-header -> 400"); r->http_method = get_http_method_key(v, vlen); - if (HTTP_METHOD_UNSET == r->http_method) + if (HTTP_METHOD_UNSET >= r->http_method) return http_request_header_line_invalid(r, 501, "unknown http-method -> 501"); continue; } @@ -769,7 +772,7 @@ static int http_request_parse_reqline(request_st * const restrict r, const char #endif r->http_method = get_http_method_key(ptr, i); - if (HTTP_METHOD_UNSET == r->http_method) + if (HTTP_METHOD_UNSET >= r->http_method) return http_request_header_line_invalid(r, 501, "unknown http-method -> 501"); const char *uri = ptr + i + 1; diff --git a/src/response.c b/src/response.c index 0d0cb6d2..c9471a0f 100644 --- a/src/response.c +++ b/src/response.c @@ -888,6 +888,9 @@ http_response_write_prepare(request_st * const r) } } } + else if (r->http_version == HTTP_VERSION_2) { + /* handled by HTTP/2 framing */ + } else { /** * response is not yet finished, but we have all headers @@ -921,7 +924,7 @@ http_response_write_prepare(request_st * const r) CONST_STR_LEN("Transfer-Encoding"), CONST_STR_LEN("chunked")); } - else { + else { /* if (r->http_version == HTTP_VERSION_1_0) */ r->keep_alive = 0; } }