1
0
mirror of https://github.com/apache/httpd.git synced 2025-08-08 15:02:10 +03:00

*) 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
This commit is contained in:
Stefan Eissing
2023-06-21 12:14:08 +00:00
parent ece6bf4363
commit ac9f458776
8 changed files with 125 additions and 59 deletions

View File

@@ -33,6 +33,18 @@ struct h2_stream;
#define H2_USE_PIPES (APR_FILES_AS_SOCKETS && APR_VERSION_AT_LEAST(1,6,0)) #define H2_USE_PIPES (APR_FILES_AS_SOCKETS && APR_VERSION_AT_LEAST(1,6,0))
#endif #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 * The magic PRIamble of RFC 7540 that is always sent when starting
* a h2 communication. * a h2 communication.

View File

@@ -559,6 +559,7 @@ static int c2_hook_pre_connection(conn_rec *c2, void *csd)
return OK; return OK;
} }
#if H2_USE_POLLFD_FROM_CONN
static apr_status_t c2_get_pollfd_from_conn(conn_rec *c, static apr_status_t c2_get_pollfd_from_conn(conn_rec *c,
struct apr_pollfd_t *pfd, struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout) apr_interval_time_t *ptimeout)
@@ -583,6 +584,7 @@ static apr_status_t c2_get_pollfd_from_conn(conn_rec *c,
} }
return APR_ENOTIMPL; return APR_ENOTIMPL;
} }
#endif
void h2_c2_register_hooks(void) 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, ap_hook_post_read_request(c2_post_read_request, NULL, NULL,
APR_HOOK_REALLY_FIRST); APR_HOOK_REALLY_FIRST);
ap_hook_fixups(c2_hook_fixups, NULL, NULL, APR_HOOK_LAST); 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, ap_hook_get_pollfd_from_conn(c2_get_pollfd_from_conn, NULL, NULL,
APR_HOOK_MIDDLE); APR_HOOK_MIDDLE);
#endif #endif
c2_net_in_filter_handle = c2_net_in_filter_handle =
ap_register_input_filter("H2_C2_NET_IN", h2_c2_filter_in, ap_register_input_filter("H2_C2_NET_IN", h2_c2_filter_in,
NULL, AP_FTYPE_NETWORK); 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; cs->state = CONN_STATE_WRITE_COMPLETION;
cleanup: cleanup:
return APR_SUCCESS; return rv;
} }
conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent, conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent,

View File

@@ -120,20 +120,28 @@ apr_status_t h2_c2_filter_request_in(ap_filter_t *f,
return APR_EGENERAL; 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) { if (req->http_status != H2_HTTP_STATUS_UNSET) {
/* error was encountered preparing this request */ /* 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, b = ap_bucket_error_create(req->http_status, NULL, f->r->pool,
f->c->bucket_alloc); f->c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b); APR_BRIGADE_INSERT_TAIL(bb, b);
return APR_SUCCESS; return APR_SUCCESS;
} }
b = h2_request_create_bucket(req, f->r);
APR_BRIGADE_INSERT_TAIL(bb, b);
if (!conn_ctx->beam_in) { if (!conn_ctx->beam_in) {
b = apr_bucket_eos_create(f->c->bucket_alloc); b = apr_bucket_eos_create(f->c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b); APR_BRIGADE_INSERT_TAIL(bb, b);
} }
return APR_SUCCESS; return APR_SUCCESS;
} }

View File

@@ -694,11 +694,13 @@ static const char *h2_conf_set_websockets(cmd_parms *cmd,
void *dirconf, const char *value) void *dirconf, const char *value)
{ {
if (!strcasecmp(value, "On")) { if (!strcasecmp(value, "On")) {
#if H2_USE_PIPES #if H2_USE_WEBSOCKETS
CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 1); CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 1);
return NULL; return NULL;
#else #elif !H2_USE_PIPES
return "HTTP/2 WebSockets are not supported on this platform"; return "HTTP/2 WebSockets are not supported on this platform";
#else
return "HTTP/2 WebSockets are not supported in this server version";
#endif #endif
} }
else if (!strcasecmp(value, "Off")) { else if (!strcasecmp(value, "Off")) {

View File

@@ -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); apr_table_t *headers = apr_table_clone(r->pool, req->headers);
const char *uri = req->path; const char *uri = req->path;
AP_DEBUG_ASSERT(req->method);
AP_DEBUG_ASSERT(req->authority); AP_DEBUG_ASSERT(req->authority);
if (req->scheme && (ap_cstr_casecmp(req->scheme, if (!ap_cstr_casecmp("CONNECT", req->method)) {
ap_ssl_conn_is_ssl(c->master? c->master : c)? "https" : "http") uri = req->authority;
|| !ap_cstr_casecmp("CONNECT", req->method))) { }
/* Client sent a non-matching ':scheme' pseudo header or CONNECT. else if (req->scheme && (ap_cstr_casecmp(req->scheme, "http") &&
* In this case, we use an absolute URI. ap_cstr_casecmp(req->scheme, "https"))) {
*/ /* Client sent a non-http ':scheme', use an absolute URI */
uri = apr_psprintf(r->pool, "%s://%s%s", uri = apr_psprintf(r->pool, "%s://%s%s",
req->scheme, req->authority, req->path ? req->path : ""); 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); AP_DEBUG_ASSERT(req->authority);
if (is_connect) { if (is_connect) {
/* CONNECT MUST NOT have scheme or path */ /* CONNECT MUST NOT have scheme or path */
if (req->scheme) { r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0",
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458) req->method, req->authority);
"':scheme: %s' header present in CONNECT request", if (req->scheme) {
req->scheme); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458)
access_status = HTTP_BAD_REQUEST; "':scheme: %s' header present in CONNECT request",
goto die; req->scheme);
} access_status = HTTP_BAD_REQUEST;
if (req->path) { goto die;
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459) }
"':path: %s' header present in CONNECT request", else if (req->path) {
req->path); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459)
access_status = HTTP_BAD_REQUEST; "':path: %s' header present in CONNECT request",
goto die; req->path);
} access_status = HTTP_BAD_REQUEST;
r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", goto die;
req->method, req->authority); }
} }
else if (req->protocol) { else if (req->scheme && ap_cstr_casecmp(req->scheme, "http")
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10460) && ap_cstr_casecmp(req->scheme, "https")) {
"':protocol: %s' header present in %s request",
req->protocol, req->method);
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")) {
/* Client sent a ':scheme' pseudo header for something else /* Client sent a ':scheme' pseudo header for something else
* than what we have on this connection. Make an absolute URI. */ * 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", r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0",

View File

@@ -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)). * of CONNECT requests (see [RFC7230], Section 5.3)).
*/ */
if (!ap_cstr_casecmp(req->method, "CONNECT")) { if (!ap_cstr_casecmp(req->method, "CONNECT")) {
if (req->protocol && !strcmp("websocket", req->protocol)) { if (req->protocol) {
if (!req->scheme || !req->path) { if (!strcmp("websocket", req->protocol)) {
ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, if (!req->scheme || !req->path) {
H2_STRM_LOG(APLOGNO(10457), stream, "Request to websocket CONNECT " ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1,
"without :scheme or :path, sending 400 answer")); 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) { else if (req->scheme || req->path) {

View File

@@ -43,6 +43,8 @@
#include "h2_request.h" #include "h2_request.h"
#include "h2_ws.h" #include "h2_ws.h"
#if H2_USE_WEBSOCKETS
static ap_filter_rec_t *c2_ws_out_filter_handle; static ap_filter_rec_t *c2_ws_out_filter_handle;
struct ws_filter_ctx { 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); 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) void h2_ws_register_hooks(void)
{ {
ap_hook_post_read_request(ws_post_read, NULL, NULL, APR_HOOK_MIDDLE);
c2_ws_out_filter_handle = c2_ws_out_filter_handle =
ap_register_output_filter("H2_C2_WS_OUT", h2_c2_ws_filter_out, ap_register_output_filter("H2_C2_WS_OUT", h2_c2_ws_filter_out,
NULL, AP_FTYPE_NETWORK); 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) */

View File

@@ -5,11 +5,8 @@ import shutil
import subprocess import subprocess
import time import time
from datetime import timedelta, datetime from datetime import timedelta, datetime
from typing import Tuple, Union, List
import packaging.version
import pytest import pytest
import websockets
from pyhttpd.result import ExecResult from pyhttpd.result import ExecResult
from pyhttpd.ws_util import WsFrameReader, WsFrame from pyhttpd.ws_util import WsFrameReader, WsFrame
@@ -18,18 +15,15 @@ from .env import H2Conf, H2TestEnv
log = logging.getLogger(__name__) 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, authority=None, do_input=None, inbytes=None,
def ws_run(env: H2TestEnv, path, do_input=None, send_close=True, timeout=5, scenario='ws-stdin',
inbytes=None, send_close=True, wait_close: float = 0.0):
timeout=5, scenario='ws-stdin',
wait_close: float = 0.0) -> Tuple[ExecResult, List[str], Union[List[WsFrame], bytes]]:
""" Run the h2ws test client in various scenarios with given input and """ Run the h2ws test client in various scenarios with given input and
timings. timings.
:param env: the test environment :param env: the test environment
:param path: the path on the Apache server to CONNECt to :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 do_input: a Callable for sending input to h2ws
:param inbytes: fixed bytes to send to h2ws, unless do_input is given :param inbytes: fixed bytes to send to h2ws, unless do_input is given
:param send_close: send a CLOSE WebSockets frame at the end :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') h2ws = os.path.join(env.clients_dir, 'h2ws')
if not os.path.exists(h2ws): if not os.path.exists(h2ws):
pytest.fail(f'test client not build: {h2ws}') pytest.fail(f'test client not build: {h2ws}')
if authority is None:
authority = f'cgi.{env.http_tld}:{env.http_port}'
args = [ args = [
h2ws, '-vv', '-c', f'localhost:{env.http_port}', h2ws, '-vv', '-c', f'localhost:{env.http_port}',
f'ws://cgi.{env.http_tld}:{env.http_port}{path}', f'ws://{authority}{path}',
scenario scenario
] ]
# we write all output to files, because we manipulate input timings # 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=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
@pytest.mark.skipif(condition=ws_version < ws_version_min, @pytest.mark.skipif(condition=not H2TestEnv().httpd_is_at_least("2.5.0"),
reason=f'websockets is {ws_version}, need at least {ws_version_min}') reason=f'need at least httpd 2.5.0 for this')
class TestWebSockets: class TestWebSockets:
@pytest.fixture(autouse=True, scope='class') @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_cgi(proxy_self=True, h2proxy_self=True).install()
conf.add_vhost_test1(proxy_self=True, h2proxy_self=True).install()
assert env.apache_restart() == 0 assert env.apache_restart() == 0
def ws_check_alive(self, env, timeout=5): 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): def test_h2_800_02_fail_proto(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/echo/', scenario='fail-proto') r, infos, frames = ws_run(env, path='/ws/echo/', scenario='fail-proto')
assert r.exit_code == 0, f'{r}' 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 # CONNECT to a URL path that does not exist on the server
def test_h2_800_03_not_found(self, env: H2TestEnv, ws_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}' assert infos == ['[1] RST'], f'{r}'
# CONNECT missing the :authority header # 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') r, infos, frames = ws_run(env, path='/ws/echo/', scenario='miss-authority')
assert r.exit_code == 0, f'{r}' assert r.exit_code == 0, f'{r}'
assert infos == ['[1] RST'], 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 # CONNECT and exchange a PING
def test_h2_800_10_ws_ping(self, env: H2TestEnv, ws_server): def test_h2_800_10_ws_ping(self, env: H2TestEnv, ws_server):
ping = WsFrame.client_ping(b'12345') ping = WsFrame.client_ping(b'12345')