diff --git a/src/h2.c b/src/h2.c index ecb2e014..3001cf45 100644 --- a/src/h2.c +++ b/src/h2.c @@ -1769,7 +1769,7 @@ h2_init_con (request_st * const restrict h2r, connection * const restrict con, c static const uint8_t h2settings[] = { /*(big-endian numbers)*/ /* SETTINGS */ - 0x00, 0x00, 0x0c /* frame length */ /* 6 * 2 for two settings */ + 0x00, 0x00, 0x12 /* frame length */ /* 6 * 3 for three settings */ ,H2_FTYPE_SETTINGS /* frame type */ ,0x00 /* frame flags */ ,0x00, 0x00, 0x00, 0x00 /* stream identifier */ @@ -1796,6 +1796,8 @@ h2_init_con (request_st * const restrict h2r, connection * const restrict con, c #endif ,0x00, H2_SETTINGS_MAX_HEADER_LIST_SIZE ,0x00, 0x00, 0xFF, 0xFF /* 65535 */ + ,0x00, H2_SETTINGS_ENABLE_CONNECT_PROTOCOL + ,0x00, 0x00, 0x00, 0x01 /* 1 */ #if 0 /* WINDOW_UPDATE */ diff --git a/src/h2.h b/src/h2.h index d53eb57b..8eae26b5 100644 --- a/src/h2.h +++ b/src/h2.h @@ -30,7 +30,8 @@ typedef enum { H2_SETTINGS_MAX_CONCURRENT_STREAMS = 0x03, H2_SETTINGS_INITIAL_WINDOW_SIZE = 0x04, H2_SETTINGS_MAX_FRAME_SIZE = 0x05, - H2_SETTINGS_MAX_HEADER_LIST_SIZE = 0x06 + H2_SETTINGS_MAX_HEADER_LIST_SIZE = 0x06, + H2_SETTINGS_ENABLE_CONNECT_PROTOCOL= 0x08 } request_h2settings_t; typedef enum { diff --git a/src/http-header-glue.c b/src/http-header-glue.c index e8e93d1b..339f9277 100644 --- a/src/http-header-glue.c +++ b/src/http-header-glue.c @@ -951,7 +951,8 @@ static int http_response_process_headers(request_st * const restrict r, http_res /*(flag only for mod_proxy and mod_cgi (for now))*/ if (opts->backend != BACKEND_PROXY && opts->backend != BACKEND_CGI) continue; - if (r->http_version >= HTTP_VERSION_2) continue; + if (r->http_version >= HTTP_VERSION_2 && !r->h2_connect_ext) + continue; break; case HTTP_HEADER_CONNECTION: if (opts->backend == BACKEND_PROXY) continue; diff --git a/src/http_header.h b/src/http_header.h index 2f8d9343..e58065a1 100644 --- a/src/http_header.h +++ b/src/http_header.h @@ -34,6 +34,7 @@ enum http_header_h2_e { /* pseudo-headers */ ,HTTP_HEADER_H2_PATH_INDEX_HTML = -6 ,HTTP_HEADER_H2_SCHEME_HTTP = -7 ,HTTP_HEADER_H2_SCHEME_HTTPS = -8 + ,HTTP_HEADER_H2_PROTOCOL = -9 }; enum http_header_e { HTTP_HEADER_OTHER = 0 diff --git a/src/mod_cgi.c b/src/mod_cgi.c index c4cb5b81..24fed9e6 100644 --- a/src/mod_cgi.c +++ b/src/mod_cgi.c @@ -522,6 +522,13 @@ static handler_t cgi_response_headers(request_st * const r, struct http_response if (light_btst(r->resp_htags, HTTP_HEADER_UPGRADE)) { if (hctx->conf.upgrade && r->http_status == 101) { /* 101 Switching Protocols; transition to transparent proxy */ + if (r->h2_connect_ext) { + r->http_status = 200; /* OK (response status for CONNECT) */ + http_header_response_unset(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + http_header_response_unset(r, HTTP_HEADER_OTHER, + CONST_STR_LEN("Sec-WebSocket-Accept")); + } http_response_upgrade_read_body_unknown(r); } else { @@ -535,9 +542,18 @@ static handler_t cgi_response_headers(request_st * const r, struct http_response #endif } } + else if (__builtin_expect( (r->h2_connect_ext != 0), 0) + && r->http_status < 300) { + /*(not handling other 1xx intermediate responses here; not expected)*/ + http_response_body_clear(r, 0); + r->handler_module = NULL; + r->http_status = 405; /* Method Not Allowed */ + return HANDLER_FINISHED; + } if (hctx->conf.upgrade - && !light_btst(r->resp_htags, HTTP_HEADER_UPGRADE)) { + && !(light_btst(r->resp_htags, HTTP_HEADER_UPGRADE) + || r->h2_connect_ext)) { chunkqueue *cq = &r->reqbody_queue; hctx->conf.upgrade = 0; if (cq->bytes_out == (off_t)r->reqbody_length) { @@ -825,8 +841,39 @@ static int cgi_create_env(request_st * const r, plugin_data * const p, handler_c /* create environment */ + off_t reqbody_length = r->reqbody_length; + if (r->h2_connect_ext) { + /*(SERVER_PROTOCOL=HTTP/1.1 instead of HTTP/2.0)*/ + r->http_version = HTTP_VERSION_1_1; + r->http_method = HTTP_METHOD_GET; + if (reqbody_length < 0) + r->reqbody_length = 0; + } + http_cgi_headers(r, &opts, cgi_env_add, env); + if (r->h2_connect_ext) { + r->http_version = HTTP_VERSION_2; + r->http_method = HTTP_METHOD_CONNECT; + r->reqbody_length = reqbody_length; + /* https://datatracker.ietf.org/doc/html/rfc6455#section-4.1 + * 7. The request MUST include a header field with the name + * |Sec-WebSocket-Key|. The value of this header field MUST be a + * nonce consisting of a randomly selected 16-byte value that has + * been base64-encoded (see Section 4 of [RFC4648]). The nonce + * MUST be selected randomly for each connection. + * Note: Sec-WebSocket-Key is not used in RFC8441; + * include Sec-WebSocket-Key for HTTP/1.1 compatibility; + * !!not random!! base64-encoded "0000000000000000" */ + if (!http_header_request_get(r, HTTP_HEADER_OTHER, + CONST_STR_LEN("Sec-WebSocket-Key"))) + cgi_env_add(env, CONST_STR_LEN("HTTP_SEC_WEBSOCKET_KEY"), + CONST_STR_LEN("MDAwMDAwMDAwMDAwMDAwMAo=")); + /*(Upgrade and Connection should not exist for HTTP/2 request)*/ + cgi_env_add(env, CONST_STR_LEN("HTTP_UPGRADE"), CONST_STR_LEN("websocket")); + cgi_env_add(env, CONST_STR_LEN("HTTP_CONNECTION"), CONST_STR_LEN("upgrade")); + } + /* for valgrind */ if (p->env.ld_preload) { cgi_env_add(env, CONST_STR_LEN("LD_PRELOAD"), BUF_PTR_LEN(p->env.ld_preload)); @@ -965,6 +1012,11 @@ URIHANDLER_FUNC(cgi_is_handled) { if (!S_ISREG(st->st_mode)) return HANDLER_GO_ON; if (p->conf.execute_x_only == 1 && (st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0) return HANDLER_GO_ON; + if (__builtin_expect( (r->h2_connect_ext != 0), 0) && !p->conf.upgrade) { + r->http_status = 405; /* Method Not Allowed */ + return HANDLER_FINISHED; + } + if (r->reqbody_length && p->tempfile_accum && !(r->conf.stream_request_body /*(if not streaming request body)*/ @@ -981,7 +1033,9 @@ URIHANDLER_FUNC(cgi_is_handled) { hctx->plugin_data = p; hctx->cgi_handler = &ds->value; memcpy(&hctx->conf, &p->conf, sizeof(plugin_config)); - if (!light_btst(r->rqst_htags, HTTP_HEADER_UPGRADE)) + if (__builtin_expect( (r->h2_connect_ext != 0), 0)) { + } + else if (!light_btst(r->rqst_htags, HTTP_HEADER_UPGRADE)) hctx->conf.upgrade = 0; else if (!hctx->conf.upgrade || r->http_version != HTTP_VERSION_1_1) { hctx->conf.upgrade = 0; @@ -1078,7 +1132,7 @@ SUBREQUEST_FUNC(mod_cgi_handle_subrequest) { * Send 411 Length Required if Content-Length missing. * (occurs here if client sends Transfer-Encoding: chunked * and module is flagged to stream request body to backend) */ - if (-1 == r->reqbody_length) { + if (-1 == r->reqbody_length && !r->h2_connect_ext) { return (r->conf.stream_request_body & FDEVENT_STREAM_REQUEST) ? http_response_reqbody_read_error(r, 411) : HANDLER_WAIT_FOR_EVENT; diff --git a/src/mod_proxy.c b/src/mod_proxy.c index a8041990..2e3d4df6 100644 --- a/src/mod_proxy.c +++ b/src/mod_proxy.c @@ -846,7 +846,10 @@ static handler_t proxy_create_env(gw_handler_ctx *gwhctx) { /* build header */ /* request line */ - const buffer * const m = http_method_buf(r->http_method); + const buffer * const m = + http_method_buf(!r->h2_connect_ext + ? r->http_method + : HTTP_METHOD_GET); /*(translate HTTP/2 CONNECT ext)*/ buffer_append_str3(b, BUF_PTR_LEN(m), CONST_STR_LEN(" "), @@ -891,6 +894,8 @@ static handler_t proxy_create_env(gw_handler_ctx *gwhctx) { r->reqbody_length); } } + else if (r->h2_connect_ext) { + } else if (-1 == r->reqbody_length && (r->conf.stream_request_body & (FDEVENT_STREAM_REQUEST | FDEVENT_STREAM_REQUEST_BUFMIN))) { @@ -931,7 +936,9 @@ static handler_t proxy_create_env(gw_handler_ctx *gwhctx) { te = &ds->value; /*("trailers")*/ break; case HTTP_HEADER_UPGRADE: - if (hctx->conf.header.force_http10 || r->http_version != HTTP_VERSION_1_1) continue; + if (hctx->conf.header.force_http10 + || (r->http_version != HTTP_VERSION_1_1 && !r->h2_connect_ext)) + continue; if (!hctx->conf.header.upgrade) continue; if (!buffer_is_blank(&ds->value)) upgrade = &ds->value; break; @@ -994,6 +1001,24 @@ static handler_t proxy_create_env(gw_handler_ctx *gwhctx) { buffer_append_string_len(b, CONST_STR_LEN(", upgrade")); buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n")); } + else if (r->h2_connect_ext) { + /* https://datatracker.ietf.org/doc/html/rfc6455#section-4.1 + * 7. The request MUST include a header field with the name + * |Sec-WebSocket-Key|. The value of this header field MUST be a + * nonce consisting of a randomly selected 16-byte value that has + * been base64-encoded (see Section 4 of [RFC4648]). The nonce + * MUST be selected randomly for each connection. + * Note: Sec-WebSocket-Key is not used in RFC8441; + * include Sec-WebSocket-Key for HTTP/1.1 compatibility; + * !!not random!! base64-encoded "0000000000000000" */ + if (!http_header_request_get(r, HTTP_HEADER_OTHER, + CONST_STR_LEN("Sec-WebSocket-Key"))) + buffer_append_string_len(b, CONST_STR_LEN( + "\r\nSec-WebSocket-Key: MDAwMDAwMDAwMDAwMDAwMAo=")); + buffer_append_string_len(b, CONST_STR_LEN( + "\r\nUpgrade: websocket" + "\r\nConnection: close, upgrade\r\n\r\n")); + } else /* mod_proxy always sends Connection: close to backend */ buffer_append_string_len(b, CONST_STR_LEN("\r\nConnection: close\r\n\r\n")); @@ -1037,6 +1062,13 @@ static handler_t proxy_response_headers(request_st * const r, struct http_respon if (light_btst(r->resp_htags, HTTP_HEADER_UPGRADE)) { if (remap_hdrs->upgrade && r->http_status == 101) { /* 101 Switching Protocols; transition to transparent proxy */ + if (r->h2_connect_ext) { + r->http_status = 200; /* OK (response status for CONNECT) */ + http_header_response_unset(r, HTTP_HEADER_UPGRADE, + CONST_STR_LEN("Upgrade")); + http_header_response_unset(r, HTTP_HEADER_OTHER, + CONST_STR_LEN("Sec-WebSocket-Accept")); + } gw_set_transparent(&hctx->gw); http_response_upgrade_read_body_unknown(r); } @@ -1051,6 +1083,14 @@ static handler_t proxy_response_headers(request_st * const r, struct http_respon #endif } } + else if (__builtin_expect( (r->h2_connect_ext != 0), 0) + && r->http_status < 300) { + /*(not handling other 1xx intermediate responses here; not expected)*/ + http_response_body_clear(r, 0); + r->handler_module = NULL; + r->http_status = 405; /* Method Not Allowed */ + return HANDLER_FINISHED; + } /* rewrite paths, if needed */ @@ -1099,7 +1139,8 @@ static handler_t mod_proxy_check_extension(request_st * const r, void *p_d) { hctx->conf = p->conf; /*(copies struct)*/ hctx->conf.header.http_host = r->http_host; - hctx->conf.header.upgrade &= (r->http_version == HTTP_VERSION_1_1); + hctx->conf.header.upgrade &= + (r->http_version == HTTP_VERSION_1_1 || r->h2_connect_ext); /* mod_proxy currently sends all backend requests as http. * https-remap is a flag since it might not be needed if backend * honors Forwarded or X-Forwarded-Proto headers, e.g. by using @@ -1112,7 +1153,13 @@ static handler_t mod_proxy_check_extension(request_st * const r, void *p_d) { if (r->http_method == HTTP_METHOD_CONNECT) { /*(note: not requiring HTTP/1.1 due to too many non-compliant * clients such as 'openssl s_client')*/ - if (hctx->conf.header.connect_method) { + if (r->h2_connect_ext + && (hctx->conf.header.connect_method = + hctx->conf.header.upgrade)) { /*(405 if not set)*/ + /*(not bothering to check (!hctx->conf.header.force_http10))*/ + /*hctx->gw.create_env = proxy_create_env;*/ /*(preserve)*/ + } + else if (hctx->conf.header.connect_method) { hctx->gw.create_env = proxy_create_env_connect; } else { diff --git a/src/mod_wstunnel.c b/src/mod_wstunnel.c index 6d17d4d4..df0715cf 100644 --- a/src/mod_wstunnel.c +++ b/src/mod_wstunnel.c @@ -349,14 +349,16 @@ static handler_t wstunnel_create_env(gw_handler_ctx *gwhctx) { handler_ctx *hctx = (handler_ctx *)gwhctx; request_st * const r = hctx->gw.r; handler_t rc; - if (0 == r->reqbody_length) { + if (0 == r->reqbody_length || r->http_version > HTTP_VERSION_1_1) { http_response_upgrade_read_body_unknown(r); chunkqueue_append_chunkqueue(&r->reqbody_queue, &r->read_queue); } rc = mod_wstunnel_handshake_create_response(hctx); if (rc != HANDLER_GO_ON) return rc; - r->http_status = 101; /* Switching Protocols */ + r->http_status = (r->http_version > HTTP_VERSION_1_1) + ? 200 /* OK (response status for CONNECT) */ + : 101; /* Switching Protocols */ r->resp_body_started = 1; hctx->ping_ts = log_monotonic_secs; @@ -553,11 +555,15 @@ static handler_t wstunnel_handler_setup (request_st * const r, plugin_data * con static handler_t mod_wstunnel_check_extension(request_st * const r, void *p_d) { plugin_data *p = p_d; - const buffer *vb; handler_t rc; if (NULL != r->handler_module) return HANDLER_GO_ON; + if (r->http_version > HTTP_VERSION_1_1) { + if (!r->h2_connect_ext) + return HANDLER_GO_ON; + } + else { if (r->http_method != HTTP_METHOD_GET) return HANDLER_GO_ON; if (r->http_version != HTTP_VERSION_1_1) @@ -567,6 +573,7 @@ static handler_t mod_wstunnel_check_extension(request_st * const r, void *p_d) { * Connection: upgrade, keep-alive, ... * Upgrade: WebSocket, ... */ + const buffer *vb; vb = http_header_request_get(r, HTTP_HEADER_UPGRADE, CONST_STR_LEN("Upgrade")); if (NULL == vb || !http_header_str_contains_token(BUF_PTR_LEN(vb), CONST_STR_LEN("websocket"))) @@ -575,6 +582,7 @@ static handler_t mod_wstunnel_check_extension(request_st * const r, void *p_d) { if (NULL == vb || !http_header_str_contains_token(BUF_PTR_LEN(vb), CONST_STR_LEN("upgrade"))) return HANDLER_GO_ON; + } mod_wstunnel_patch_config(r, p); if (NULL == p->conf.gw.exts) return HANDLER_GO_ON; @@ -774,6 +782,7 @@ static int create_response_ietf_00(handler_ctx *hctx) { static int create_response_rfc_6455(handler_ctx *hctx) { request_st * const r = hctx->gw.r; + if (r->http_version == HTTP_VERSION_1_1) { SHA_CTX sha; unsigned char sha_digest[SHA_DIGEST_LENGTH]; @@ -804,6 +813,7 @@ static int create_response_rfc_6455(handler_ctx *hctx) { http_header_response_set_ptr(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Accept")); buffer_append_base64_encode(value, sha_digest, SHA_DIGEST_LENGTH, BASE64_STANDARD); + } if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_BIN) http_header_response_set(r, HTTP_HEADER_OTHER, @@ -834,7 +844,7 @@ handler_t mod_wstunnel_handshake_create_response(handler_ctx *hctx) { #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ - if (hctx->hybivers == 0) { + if (hctx->hybivers == 0 && r->http_version == HTTP_VERSION_1_1) { #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ /* 8 bytes should have been sent with request * for draft-ietf-hybi-thewebsocketprotocol-00 */ diff --git a/src/reqpool.c b/src/reqpool.c index 87cc0edf..98864249 100644 --- a/src/reqpool.c +++ b/src/reqpool.c @@ -107,6 +107,7 @@ request_reset (request_st * const r) /*r->error_handler_saved_method = HTTP_METHOD_UNSET;*/ /*(error_handler_saved_method value is not valid * unless error_handler_saved_status is set)*/ + r->h2_connect_ext = 0; buffer_clear(&r->uri.scheme); diff --git a/src/request.c b/src/request.c index c825545a..09ad010e 100644 --- a/src/request.c +++ b/src/request.c @@ -528,13 +528,19 @@ http_request_validate_pseudohdrs (request_st * const restrict r, const int schem { /* :method is required to indicate method * CONNECT method must have :method and :authority + * unless RFC8441 CONNECT extension, which must follow 'other' (below) * All other methods must have at least :method :scheme :path */ if (HTTP_METHOD_UNSET == r->http_method) return http_request_header_line_invalid(r, 400, "missing pseudo-header method -> 400"); - if (__builtin_expect( (HTTP_METHOD_CONNECT != r->http_method), 1)) { + if (HTTP_METHOD_CONNECT != r->http_method) + r->h2_connect_ext = 0; + + if (__builtin_expect( (HTTP_METHOD_CONNECT != r->http_method), 1) + || __builtin_expect( (r->h2_connect_ext != 0), 0)) { + if (!scheme) return http_request_header_line_invalid(r, 400, "missing pseudo-header scheme -> 400"); @@ -638,6 +644,10 @@ http_request_parse_header (request_st * const restrict r, http_header_parse_ctx else if (0 == memcmp(k+1, "scheme", 6)) hpctx->id = HTTP_HEADER_H2_SCHEME_HTTP; break; + case 8: + if (0 == memcmp(k+1, "protocol", 8)) + hpctx->id = HTTP_HEADER_H2_PROTOCOL; + break; case 9: if (0 == memcmp(k+1, "authority", 9)) hpctx->id = HTTP_HEADER_H2_AUTHORITY; @@ -698,6 +708,14 @@ http_request_parse_header (request_st * const restrict r, http_header_parse_ctx return http_request_header_line_invalid(r, 400, "unknown pseudo-header scheme -> 400"); #endif + case HTTP_HEADER_H2_PROTOCOL: + /* support only ":protocol: websocket" for now */ + if (vlen != 9 || 0 != memcmp(v, "websocket", 9)) + return http_request_header_line_invalid(r, 405, + "unhandled :protocol value -> 405"); + /*(future: might be enum of recognized :protocol: ext values)*/ + r->h2_connect_ext = 1; + return 0; default: return http_request_header_line_invalid(r, 400, "invalid pseudo-header -> 400"); @@ -913,7 +931,7 @@ int http_request_parse_target(request_st * const r, int scheme_port) { buffer_copy_string_len(&r->uri.scheme, "https", scheme_port == 443 ? 5 : 4); buffer * const target = &r->target; - if (r->http_method == HTTP_METHOD_CONNECT + if ((r->http_method == HTTP_METHOD_CONNECT && !r->h2_connect_ext) || (r->http_method == HTTP_METHOD_OPTIONS && target->ptr[0] == '*' && target->ptr[1] == '\0')) { diff --git a/src/request.h b/src/request.h index 607aa991..e909a88f 100644 --- a/src/request.h +++ b/src/request.h @@ -193,6 +193,7 @@ struct request_st { struct stat_cache_entry *tmp_sce; /*(value valid only in sequential code)*/ int cond_captures; + int h2_connect_ext; }; diff --git a/src/response.c b/src/response.c index 5389c953..3f10fc68 100644 --- a/src/response.c +++ b/src/response.c @@ -426,7 +426,8 @@ http_response_prepare (request_st * const r) && r->uri.path.ptr[0] == '*' && r->uri.path.ptr[1] == '\0') return http_response_prepare_options_star(r); - if (__builtin_expect( (r->http_method == HTTP_METHOD_CONNECT), 0)) + if (__builtin_expect( (r->http_method == HTTP_METHOD_CONNECT), 0) + && (r->handler_module || !r->h2_connect_ext)) return http_response_prepare_connect(r); @@ -541,6 +542,10 @@ http_response_prepare (request_st * const r) http_response_body_clear(r, 0); http_response_prepare_options_star(r); /*(treat like "*")*/ } + else if (r->http_method == HTTP_METHOD_CONNECT) + /* 405 Method Not Allowed */ + return http_status_set_error_close(r, 405); + /*return http_response_prepare_connect(r);*/ else if (!http_method_get_head_post(r->http_method)) r->http_status = 501; else