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:
@@ -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.
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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")) {
|
||||
|
@@ -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);
|
||||
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->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;
|
||||
}
|
||||
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",
|
||||
|
@@ -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) {
|
||||
|
@@ -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) */
|
||||
|
@@ -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')
|
||||
|
Reference in New Issue
Block a user