diff --git a/modules/http2/h2.h b/modules/http2/h2.h index 4babbf81d6b..2bb59ecb9c0 100644 --- a/modules/http2/h2.h +++ b/modules/http2/h2.h @@ -33,6 +33,18 @@ struct h2_stream; #define H2_USE_PIPES (APR_FILES_AS_SOCKETS && APR_VERSION_AT_LEAST(1,6,0)) #endif +#if AP_MODULE_MAGIC_AT_LEAST(20211221, 15) +#define H2_USE_POLLFD_FROM_CONN 1 +#else +#define H2_USE_POLLFD_FROM_CONN 0 +#endif + +#if H2_USE_POLLFD_FROM_CONN && H2_USE_PIPES +#define H2_USE_WEBSOCKETS 1 +#else +#define H2_USE_WEBSOCKETS 0 +#endif + /** * The magic PRIamble of RFC 7540 that is always sent when starting * a h2 communication. diff --git a/modules/http2/h2_c2.c b/modules/http2/h2_c2.c index 537163bf79d..783a297fe0b 100644 --- a/modules/http2/h2_c2.c +++ b/modules/http2/h2_c2.c @@ -559,6 +559,7 @@ static int c2_hook_pre_connection(conn_rec *c2, void *csd) return OK; } +#if H2_USE_POLLFD_FROM_CONN static apr_status_t c2_get_pollfd_from_conn(conn_rec *c, struct apr_pollfd_t *pfd, apr_interval_time_t *ptimeout) @@ -583,6 +584,7 @@ static apr_status_t c2_get_pollfd_from_conn(conn_rec *c, } return APR_ENOTIMPL; } +#endif void h2_c2_register_hooks(void) { @@ -598,12 +600,11 @@ void h2_c2_register_hooks(void) ap_hook_post_read_request(c2_post_read_request, NULL, NULL, APR_HOOK_REALLY_FIRST); ap_hook_fixups(c2_hook_fixups, NULL, NULL, APR_HOOK_LAST); -#if AP_MODULE_MAGIC_AT_LEAST(20211221, 15) +#if H2_USE_POLLFD_FROM_CONN ap_hook_get_pollfd_from_conn(c2_get_pollfd_from_conn, NULL, NULL, APR_HOOK_MIDDLE); #endif - c2_net_in_filter_handle = ap_register_input_filter("H2_C2_NET_IN", h2_c2_filter_in, NULL, AP_FTYPE_NETWORK); @@ -788,7 +789,7 @@ static apr_status_t c2_process(h2_conn_ctx_t *conn_ctx, conn_rec *c) cs->state = CONN_STATE_WRITE_COMPLETION; cleanup: - return APR_SUCCESS; + return rv; } conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent, diff --git a/modules/http2/h2_c2_filter.c b/modules/http2/h2_c2_filter.c index 846344c6b47..97c38b3f6dc 100644 --- a/modules/http2/h2_c2_filter.c +++ b/modules/http2/h2_c2_filter.c @@ -120,20 +120,28 @@ apr_status_t h2_c2_filter_request_in(ap_filter_t *f, return APR_EGENERAL; } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c, + "h2_c2_filter_request_in(%s): adding request bucket", + conn_ctx->id); + b = h2_request_create_bucket(req, f->r); + APR_BRIGADE_INSERT_TAIL(bb, b); + if (req->http_status != H2_HTTP_STATUS_UNSET) { /* error was encountered preparing this request */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c, + "h2_c2_filter_request_in(%s): adding error bucket %d", + conn_ctx->id, req->http_status); b = ap_bucket_error_create(req->http_status, NULL, f->r->pool, f->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, b); return APR_SUCCESS; } - b = h2_request_create_bucket(req, f->r); - APR_BRIGADE_INSERT_TAIL(bb, b); if (!conn_ctx->beam_in) { b = apr_bucket_eos_create(f->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, b); } + return APR_SUCCESS; } diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c index a8b19739026..7f9f18078c0 100644 --- a/modules/http2/h2_config.c +++ b/modules/http2/h2_config.c @@ -694,11 +694,13 @@ static const char *h2_conf_set_websockets(cmd_parms *cmd, void *dirconf, const char *value) { if (!strcasecmp(value, "On")) { -#if H2_USE_PIPES +#if H2_USE_WEBSOCKETS CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 1); return NULL; -#else +#elif !H2_USE_PIPES return "HTTP/2 WebSockets are not supported on this platform"; +#else + return "HTTP/2 WebSockets are not supported in this server version"; #endif } else if (!strcasecmp(value, "Off")) { diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c index b55d5720a02..4e363ab0aa3 100644 --- a/modules/http2/h2_request.c +++ b/modules/http2/h2_request.c @@ -287,13 +287,14 @@ apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r) apr_table_t *headers = apr_table_clone(r->pool, req->headers); const char *uri = req->path; + AP_DEBUG_ASSERT(req->method); AP_DEBUG_ASSERT(req->authority); - if (req->scheme && (ap_cstr_casecmp(req->scheme, - ap_ssl_conn_is_ssl(c->master? c->master : c)? "https" : "http") - || !ap_cstr_casecmp("CONNECT", req->method))) { - /* Client sent a non-matching ':scheme' pseudo header or CONNECT. - * In this case, we use an absolute URI. - */ + if (!ap_cstr_casecmp("CONNECT", req->method)) { + uri = req->authority; + } + else if (req->scheme && (ap_cstr_casecmp(req->scheme, "http") && + ap_cstr_casecmp(req->scheme, "https"))) { + /* Client sent a non-http ':scheme', use an absolute URI */ uri = apr_psprintf(r->pool, "%s://%s%s", req->scheme, req->authority, req->path ? req->path : ""); } @@ -379,33 +380,25 @@ request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c, AP_DEBUG_ASSERT(req->authority); if (is_connect) { /* CONNECT MUST NOT have scheme or path */ - if (req->scheme) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458) - "':scheme: %s' header present in CONNECT request", - req->scheme); - access_status = HTTP_BAD_REQUEST; - goto die; - } - if (req->path) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459) - "':path: %s' header present in CONNECT request", - req->path); - access_status = HTTP_BAD_REQUEST; - goto die; - } - r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", - req->method, req->authority); - } - else if (req->protocol) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10460) - "':protocol: %s' header present in %s request", - req->protocol, req->method); - access_status = HTTP_BAD_REQUEST; - goto die; + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->authority); + if (req->scheme) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458) + "':scheme: %s' header present in CONNECT request", + req->scheme); + access_status = HTTP_BAD_REQUEST; + goto die; + } + else if (req->path) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459) + "':path: %s' header present in CONNECT request", + req->path); + access_status = HTTP_BAD_REQUEST; + goto die; + } } - else if (req->scheme && - ap_cstr_casecmp(req->scheme, ap_ssl_conn_is_ssl(c->master? c->master : c)? - "https" : "http")) { + else if (req->scheme && ap_cstr_casecmp(req->scheme, "http") + && ap_cstr_casecmp(req->scheme, "https")) { /* Client sent a ':scheme' pseudo header for something else * than what we have on this connection. Make an absolute URI. */ r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0", diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c index 24d0268f38b..19527600e08 100644 --- a/modules/http2/h2_stream.c +++ b/modules/http2/h2_stream.c @@ -900,11 +900,23 @@ apr_status_t h2_stream_end_headers(h2_stream *stream, int eos, size_t raw_bytes) * of CONNECT requests (see [RFC7230], Section 5.3)). */ if (!ap_cstr_casecmp(req->method, "CONNECT")) { - if (req->protocol && !strcmp("websocket", req->protocol)) { - if (!req->scheme || !req->path) { - ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, - H2_STRM_LOG(APLOGNO(10457), stream, "Request to websocket CONNECT " - "without :scheme or :path, sending 400 answer")); + if (req->protocol) { + if (!strcmp("websocket", req->protocol)) { + if (!req->scheme || !req->path) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10457), stream, "Request to websocket CONNECT " + "without :scheme or :path, sending 400 answer")); + set_error_response(stream, HTTP_BAD_REQUEST); + goto cleanup; + } + } + else { + /* do not know that protocol */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->session->c1, APLOGNO(10460) + "':protocol: %s' header present in %s request", + req->protocol, req->method); + set_error_response(stream, HTTP_NOT_IMPLEMENTED); + goto cleanup; } } else if (req->scheme || req->path) { diff --git a/modules/http2/h2_ws.c b/modules/http2/h2_ws.c index e3bdadb32d9..d2a51af6b7b 100644 --- a/modules/http2/h2_ws.c +++ b/modules/http2/h2_ws.c @@ -43,6 +43,8 @@ #include "h2_request.h" #include "h2_ws.h" +#if H2_USE_WEBSOCKETS + static ap_filter_rec_t *c2_ws_out_filter_handle; struct ws_filter_ctx { @@ -318,9 +320,41 @@ static apr_status_t h2_c2_ws_filter_out(ap_filter_t* f, apr_bucket_brigade* bb) return ap_pass_brigade(f->next, bb); } +static int ws_post_read(request_rec *r) +{ + + if (r->connection->master) { + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(r->connection); + if (conn_ctx && conn_ctx->is_upgrade && + !h2_config_sgeti(r->server, H2_CONF_WEBSOCKETS)) { + return HTTP_NOT_IMPLEMENTED; + } + } + return DECLINED; +} + void h2_ws_register_hooks(void) { + ap_hook_post_read_request(ws_post_read, NULL, NULL, APR_HOOK_MIDDLE); c2_ws_out_filter_handle = ap_register_output_filter("H2_C2_WS_OUT", h2_c2_ws_filter_out, NULL, AP_FTYPE_NETWORK); } + +#else /* H2_USE_WEBSOCKETS */ + +const h2_request *h2_ws_rewrite_request(const h2_request *req, + conn_rec *c2, int no_body) +{ + (void)c2; + (void)no_body; + /* no rewriting */ + return req; +} + +void h2_ws_register_hooks(void) +{ + /* NOP */ +} + +#endif /* H2_USE_WEBSOCKETS (else part) */ diff --git a/test/modules/http2/test_800_websockets.py b/test/modules/http2/test_800_websockets.py index 58ac4eb4e39..76f9fe9475a 100644 --- a/test/modules/http2/test_800_websockets.py +++ b/test/modules/http2/test_800_websockets.py @@ -5,11 +5,8 @@ import subprocess import time from datetime import timedelta, datetime -from typing import Tuple, Union, List -import packaging.version import pytest -import websockets from pyhttpd.result import ExecResult from pyhttpd.ws_util import WsFrameReader, WsFrame @@ -18,18 +15,15 @@ log = logging.getLogger(__name__) -ws_version = packaging.version.parse(websockets.version.version) -ws_version_min = packaging.version.Version('10.4') - -def ws_run(env: H2TestEnv, path, do_input=None, - inbytes=None, send_close=True, - timeout=5, scenario='ws-stdin', - wait_close: float = 0.0) -> Tuple[ExecResult, List[str], Union[List[WsFrame], bytes]]: +def ws_run(env: H2TestEnv, path, authority=None, do_input=None, inbytes=None, + send_close=True, timeout=5, scenario='ws-stdin', + wait_close: float = 0.0): """ Run the h2ws test client in various scenarios with given input and timings. :param env: the test environment :param path: the path on the Apache server to CONNECt to + :param authority: the host:port to use as :param do_input: a Callable for sending input to h2ws :param inbytes: fixed bytes to send to h2ws, unless do_input is given :param send_close: send a CLOSE WebSockets frame at the end @@ -41,9 +35,11 @@ def ws_run(env: H2TestEnv, path, do_input=None, h2ws = os.path.join(env.clients_dir, 'h2ws') if not os.path.exists(h2ws): pytest.fail(f'test client not build: {h2ws}') + if authority is None: + authority = f'cgi.{env.http_tld}:{env.http_port}' args = [ h2ws, '-vv', '-c', f'localhost:{env.http_port}', - f'ws://cgi.{env.http_tld}:{env.http_port}{path}', + f'ws://{authority}{path}', scenario ] # we write all output to files, because we manipulate input timings @@ -80,8 +76,8 @@ def ws_run(env: H2TestEnv, path, do_input=None, @pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") -@pytest.mark.skipif(condition=ws_version < ws_version_min, - reason=f'websockets is {ws_version}, need at least {ws_version_min}') +@pytest.mark.skipif(condition=not H2TestEnv().httpd_is_at_least("2.5.0"), + reason=f'need at least httpd 2.5.0 for this') class TestWebSockets: @pytest.fixture(autouse=True, scope='class') @@ -97,6 +93,7 @@ def _class_scope(self, env): ] }) conf.add_vhost_cgi(proxy_self=True, h2proxy_self=True).install() + conf.add_vhost_test1(proxy_self=True, h2proxy_self=True).install() assert env.apache_restart() == 0 def ws_check_alive(self, env, timeout=5): @@ -150,7 +147,7 @@ def test_h2_800_01_ws_empty(self, env: H2TestEnv, ws_server): def test_h2_800_02_fail_proto(self, env: H2TestEnv, ws_server): r, infos, frames = ws_run(env, path='/ws/echo/', scenario='fail-proto') assert r.exit_code == 0, f'{r}' - assert infos == ['[1] :status: 400', '[1] EOF'], f'{r}' + assert infos == ['[1] :status: 501', '[1] EOF'], f'{r}' # CONNECT to a URL path that does not exist on the server def test_h2_800_03_not_found(self, env: H2TestEnv, ws_server): @@ -193,11 +190,18 @@ def test_h2_800_08_miss_scheme(self, env: H2TestEnv, ws_server): assert infos == ['[1] RST'], f'{r}' # CONNECT missing the :authority header - def test_h2_800_09_miss_authority(self, env: H2TestEnv, ws_server): + def test_h2_800_09a_miss_authority(self, env: H2TestEnv, ws_server): r, infos, frames = ws_run(env, path='/ws/echo/', scenario='miss-authority') assert r.exit_code == 0, f'{r}' assert infos == ['[1] RST'], f'{r}' + # CONNECT to authority with disabled websockets + def test_h2_800_09b_unsupported(self, env: H2TestEnv, ws_server): + r, infos, frames = ws_run(env, path='/ws/echo/', + authority=f'test1.{env.http_tld}:{env.http_port}') + assert r.exit_code == 0, f'{r}' + assert infos == ['[1] :status: 501', '[1] EOF'], f'{r}' + # CONNECT and exchange a PING def test_h2_800_10_ws_ping(self, env: H2TestEnv, ws_server): ping = WsFrame.client_ping(b'12345')