From ac9f45877649038f7e1e983b1c56a0e3de7787c1 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Wed, 21 Jun 2023 12:14:08 +0000 Subject: *) mod_http2: adding checks for websocket support on platform and server versions. Give error message accordingly when trying to enable websockets in unsupported configurations. Add test and code to check the, finally selected, server of a request_rec for websocket support or 501 the request. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1910535 13f79535-47bb-0310-9956-ffa450edef68 --- modules/http2/h2.h | 12 +++++++ modules/http2/h2_c2.c | 7 ++-- modules/http2/h2_c2_filter.c | 12 +++++-- modules/http2/h2_config.c | 6 ++-- modules/http2/h2_request.c | 57 ++++++++++++++----------------- modules/http2/h2_stream.c | 22 +++++++++--- modules/http2/h2_ws.c | 34 ++++++++++++++++++ test/modules/http2/test_800_websockets.py | 34 ++++++++++-------- 8 files changed, 125 insertions(+), 59 deletions(-) diff --git a/modules/http2/h2.h b/modules/http2/h2.h index 4babbf81d6..2bb59ecb9c 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 537163bf79..783a297fe0 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 846344c6b4..97c38b3f6d 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 a8b1973902..7f9f18078c 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 b55d5720a0..4e363ab0aa 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 24d0268f38..19527600e0 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 e3bdadb32d..d2a51af6b7 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 58ac4eb4e3..76f9fe9475 100644 --- a/test/modules/http2/test_800_websockets.py +++ b/test/modules/http2/test_800_websockets.py @@ -5,11 +5,8 @@ import shutil 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 @@ from .env import H2Conf, H2TestEnv 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 @@ class TestWebSockets: ] }) 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 @@ class TestWebSockets: 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 @@ class TestWebSockets: 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') -- cgit v1.2.3