Skip to content

Commit

Permalink
*) mod_http2: adding checks for websocket support on platform and
Browse files Browse the repository at this point in the history
     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
  • Loading branch information
icing committed Jun 21, 2023
1 parent ece6bf4 commit ac9f458
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 59 deletions.
12 changes: 12 additions & 0 deletions modules/http2/h2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions modules/http2/h2_c2.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
{
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions modules/http2/h2_c2_filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
6 changes: 4 additions & 2 deletions modules/http2/h2_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
57 changes: 25 additions & 32 deletions modules/http2/h2_request.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 : "");
}
Expand Down Expand Up @@ -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",
Expand Down
22 changes: 17 additions & 5 deletions modules/http2/h2_stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 34 additions & 0 deletions modules/http2/h2_ws.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) */
34 changes: 19 additions & 15 deletions test/modules/http2/test_800_websockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit ac9f458

Please sign in to comment.