[multiple] WebSockets over HTTP/2 (fixes #3151)

Add support for WebSockets over HTTP/2 to lighttpd core and to
  mod_cgi       w/ config: cgi.upgrade = "enable"
  mod_proxy     w/ config: proxy.header += ("upgrade" => "enable")
  mod_wstunnel

HTTP/2 CONNECT extension defined in RFC8441 is translated to HTTP/1.1
'Upgrade: websocket' requests to mod_cgi or mod_proxy, and is handled
directly in mod_wstunnel.

x-ref:
  WebSockets over HTTP/2
  https://redmine.lighttpd.net/issues/3151
  Bootstrapping WebSockets with HTTP/2
  https://datatracker.ietf.org/doc/html/rfc8441
master
Glenn Strauss 8 months ago
parent 8fe9f1c053
commit 5d1aa5d06f
  1. 4
      src/h2.c
  2. 3
      src/h2.h
  3. 3
      src/http-header-glue.c
  4. 1
      src/http_header.h
  5. 60
      src/mod_cgi.c
  6. 55
      src/mod_proxy.c
  7. 18
      src/mod_wstunnel.c
  8. 1
      src/reqpool.c
  9. 22
      src/request.c
  10. 1
      src/request.h
  11. 7
      src/response.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 */

@ -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 {

@ -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;

@ -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

@ -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;

@ -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 {

@ -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 */

@ -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);

@ -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')) {

@ -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;
};

@ -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

Loading…
Cancel
Save