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

*) mod_http2: added support for bootstrapping WebSockets via HTTP/2, as

described in RFC 8441. A new directive 'H2WebSockets on|off' has been
     added. The feature is by default not enabled.
     As also discussed in the manual, this feature should work for setups
     using "ProxyPass backend-url upgrade=websocket" without further changes.
     Special server modules for WebSockets will have to be adapted,
     most likely, as the handling if IO events is different with HTTP/2.
     HTTP/2 WebSockets are supported on platforms with native pipes. This
     excludes Windows.



git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1910507 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Stefan Eissing
2023-06-20 12:01:09 +00:00
parent 93b072e61c
commit 3ed9d65b05
41 changed files with 2530 additions and 95 deletions

View File

@@ -197,7 +197,7 @@ jobs:
# -------------------------------------------------------------------------
- name: HTTP/2 test suite
config: --enable-mods-shared=reallyall --with-mpm=event --enable-mpms-shared=all
pkgs: curl python3-pytest nghttp2-client python3-cryptography python3-requests python3-multipart
pkgs: curl python3-pytest nghttp2-client python3-cryptography python3-requests python3-multipart python3-filelock python3-websockets
env: |
APR_VERSION=1.7.4
APU_VERSION=1.6.3
@@ -228,7 +228,7 @@ jobs:
### TODO: fix caching here.
- name: MOD_TLS test suite
config: --enable-mods-shared=reallyall --with-mpm=event --enable-mpms-shared=event
pkgs: curl python3-pytest nghttp2-client python3-cryptography python3-requests python3-multipart cargo cbindgen
pkgs: curl python3-pytest nghttp2-client python3-cryptography python3-requests python3-multipart python3-filelock python3-websockets cargo cbindgen
env: |
APR_VERSION=1.7.4
APU_VERSION=1.6.3

View File

@@ -497,6 +497,7 @@ SET(mod_http2_extra_sources
modules/http2/h2_request.c modules/http2/h2_session.c
modules/http2/h2_stream.c modules/http2/h2_switch.c
modules/http2/h2_util.c modules/http2/h2_workers.c
modules/http2/h2_ws.c
)
SET(mod_ldap_extra_defines LDAP_DECLARE_EXPORT)
SET(mod_ldap_extra_libs wldap32)

View File

@@ -0,0 +1,10 @@
*) mod_http2: added support for bootstrapping WebSockets via HTTP/2, as
described in RFC 8441. A new directive 'H2WebSockets on|off' has been
added. The feature is by default not enabled.
As also discussed in the manual, this feature should work for setups
using "ProxyPass backend-url upgrade=websocket" without further changes.
Special server modules for WebSockets will have to be adapted,
most likely, as the handling if IO events is different with HTTP/2.
HTTP/2 WebSockets are supported on platforms with native pipes. This
excludes Windows.
[Stefan Eissing]

View File

@@ -999,6 +999,7 @@ APACHE_FAST_OUTPUT(support/Makefile)
if test -d ./test; then
APACHE_FAST_OUTPUT(test/Makefile)
AC_CONFIG_FILES([test/pyhttpd/config.ini])
APACHE_FAST_OUTPUT(test/clients/Makefile)
fi
dnl ## Finalize the variables

View File

@@ -1082,4 +1082,42 @@ H2EarlyHint Link "</my.css>;rel=preload;as=style"
</usage>
</directivesynopsis>
<directivesynopsis>
<name>H2WebSockets</name>
<description>En-/Disable WebSockets via HTTP/2</description>
<syntax>H2WebSockets on|off</syntax>
<default>H2WebSockets off</default>
<contextlist>
<context>server config</context>
<context>virtual host</context>
</contextlist>
<compatibility>Available in version 2.5.1 and later.</compatibility>
<usage>
<p>
Use <directive>H2WebSockets</directive> to enable or disable
bootstrapping of WebSockets via the HTTP/2 protocol. This
protocol extension is defined in RFC 8441.
</p><p>
Such requests come as a CONNECT with an extra ':protocol'
header. Such requests are transformed inside the module to
their HTTP/1.1 equivalents before passing it to internal
processing.
</p><p>
This means that HTTP/2 WebSockets can be used for a
<directive module="mod_proxy">ProxyPass</directive> with
'upgrade=websocket' parameter without further changes.
</p><p>
For (3rd party) modules that handle WebSockets directly in the
server, the protocol bootstrapping itself will also work. However
the transfer of data does require extra support in case of HTTP/2.
The negotiated WebSocket will not be able to use the client connection
socket for polling IO related events.
</p><p>
Because enabling this feature might break backward compatibility
for such 3rd party modules, it is not enabled by default.
</p>
</usage>
</directivesynopsis>
</modulesynopsis>

View File

@@ -718,6 +718,7 @@
* 20211221.13 (2.5.1-dev) Add hook token_checker to check for authorization other
* than username / password. Add autht_provider structure.
* 20211221.14 (2.5.1-dev) Add request_rec->final_resp_passed bit
* 20211221.15 (2.5.1-dev) Add ap_get_pollfd_from_conn()
*/
#define MODULE_MAGIC_COOKIE 0x41503235UL /* "AP25" */
@@ -725,7 +726,7 @@
#ifndef MODULE_MAGIC_NUMBER_MAJOR
#define MODULE_MAGIC_NUMBER_MAJOR 20211221
#endif
#define MODULE_MAGIC_NUMBER_MINOR 14 /* 0...n */
#define MODULE_MAGIC_NUMBER_MINOR 15 /* 0...n */
/**
* Determine if the server's current MODULE_MAGIC_NUMBER is at least a

View File

@@ -31,6 +31,7 @@
#include "apr_optional.h"
#include "util_filter.h"
#include "ap_expr.h"
#include "apr_poll.h"
#include "apr_tables.h"
#include "http_config.h"
@@ -1109,6 +1110,30 @@ AP_DECLARE(int) ap_state_query(int query_code);
*/
AP_CORE_DECLARE(conn_rec *) ap_create_slave_connection(conn_rec *c);
/** Get a apr_pollfd_t populated with descriptor and descriptor type
* and the timeout to use for it.
* @return APR_ENOTIMPL if not supported for a connection.
*/
AP_DECLARE_HOOK(apr_status_t, get_pollfd_from_conn,
(conn_rec *c, struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout))
/**
* Pass in a `struct apr_pollfd_t*` and get `desc_type` and `desc`
* populated with a suitable value for polling connection input.
* For primary connection (c->master == NULL), this will be the connection
* socket. For secondary connections this may differ or not be available
* at all.
* Note that APR_NO_DESC may be set to indicate that the connection
* input is already closed.
*
* @param pfd the pollfd to set the descriptor in
* @param ptimeout != NULL to retrieve the timeout in effect
* @return ARP_SUCCESS when the information was assigned.
*/
AP_CORE_DECLARE(apr_status_t) ap_get_pollfd_from_conn(conn_rec *c,
struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout);
/** Macro to provide a default value if the pointer is not yet initialised
*/

View File

@@ -37,6 +37,7 @@ h2_stream.lo dnl
h2_switch.lo dnl
h2_util.lo dnl
h2_workers.lo dnl
h2_ws.lo dnl
"
dnl

View File

@@ -62,6 +62,8 @@ extern const char *H2_MAGIC_TOKEN;
#define H2_HEADER_AUTH_LEN 10
#define H2_HEADER_PATH ":path"
#define H2_HEADER_PATH_LEN 5
#define H2_HEADER_PROTO ":protocol"
#define H2_HEADER_PROTO_LEN 9
#define H2_CRLF "\r\n"
/* Size of the frame header itself in HTTP/2 */
@@ -153,6 +155,7 @@ struct h2_request {
const char *scheme;
const char *authority;
const char *path;
const char *protocol;
apr_table_t *headers;
apr_time_t request_time;

View File

@@ -268,6 +268,7 @@ static void beam_shutdown(h2_bucket_beam *beam, apr_shutdown_how_e how)
if (how == APR_SHUTDOWN_READWRITE) {
beam->cons_io_cb = NULL;
beam->recv_cb = NULL;
beam->eagain_cb = NULL;
}
/* shutdown sender (or both)? */
@@ -747,6 +748,9 @@ transfer:
leave:
H2_BEAM_LOG(beam, to, APLOG_TRACE2, rv, "end receive", bb);
if (rv == APR_EAGAIN && beam->eagain_cb) {
beam->eagain_cb(beam->eagain_ctx, beam);
}
apr_thread_mutex_unlock(beam->lock);
return rv;
}
@@ -769,6 +773,15 @@ void h2_beam_on_received(h2_bucket_beam *beam,
apr_thread_mutex_unlock(beam->lock);
}
void h2_beam_on_eagain(h2_bucket_beam *beam,
h2_beam_ev_callback *eagain_cb, void *ctx)
{
apr_thread_mutex_lock(beam->lock);
beam->eagain_cb = eagain_cb;
beam->eagain_ctx = ctx;
apr_thread_mutex_unlock(beam->lock);
}
void h2_beam_on_send(h2_bucket_beam *beam,
h2_beam_ev_callback *send_cb, void *ctx)
{
@@ -846,3 +859,25 @@ int h2_beam_report_consumption(h2_bucket_beam *beam)
apr_thread_mutex_unlock(beam->lock);
return rv;
}
int h2_beam_is_complete(h2_bucket_beam *beam)
{
int rv = 0;
apr_thread_mutex_lock(beam->lock);
if (beam->closed)
rv = 1;
else {
apr_bucket *b;
for (b = H2_BLIST_FIRST(&beam->buckets_to_send);
b != H2_BLIST_SENTINEL(&beam->buckets_to_send);
b = APR_BUCKET_NEXT(b)) {
if (APR_BUCKET_IS_EOS(b)) {
rv = 1;
break;
}
}
}
apr_thread_mutex_unlock(beam->lock);
return rv;
}

View File

@@ -67,6 +67,8 @@ struct h2_bucket_beam {
void *recv_ctx;
h2_beam_ev_callback *send_cb; /* event: buckets were added in h2_beam_send() */
void *send_ctx;
h2_beam_ev_callback *eagain_cb; /* event: a receive results in ARP_EAGAIN */
void *eagain_ctx;
apr_off_t recv_bytes; /* amount of bytes transferred in h2_beam_receive() */
apr_off_t recv_bytes_reported; /* amount of bytes reported as received via callback */
@@ -205,6 +207,16 @@ void h2_beam_on_consumed(h2_bucket_beam *beam,
void h2_beam_on_received(h2_bucket_beam *beam,
h2_beam_ev_callback *recv_cb, void *ctx);
/**
* Register a callback to be invoked on the receiver side whenever
* APR_EAGAIN is being returned in h2_beam_receive().
* @param beam the beam to set the callback on
* @param egain_cb the callback or NULL, called before APR_EAGAIN is returned
* @param ctx the context to use in callback invocation
*/
void h2_beam_on_eagain(h2_bucket_beam *beam,
h2_beam_ev_callback *eagain_cb, void *ctx);
/**
* Register a call back from the sender side to be invoked when send
* has added buckets to the beam.
@@ -246,4 +258,10 @@ apr_off_t h2_beam_get_buffered(h2_bucket_beam *beam);
*/
apr_off_t h2_beam_get_mem_used(h2_bucket_beam *beam);
/**
* @return != 0 iff beam has been closed or has an EOS bucket buffered
* waiting to be received.
*/
int h2_beam_is_complete(h2_bucket_beam *beam);
#endif /* h2_bucket_beam_h */

View File

@@ -267,7 +267,7 @@ static apr_status_t pass_output(h2_c1_io *io, int flush)
/* recursive call, may be triggered by an H2EOS bucket
* being destroyed and triggering sending more data? */
AP_DEBUG_ASSERT(0);
ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c, APLOGNO(10456)
ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(10456)
"h2_c1_io(%ld): recursive call of h2_c1_io_pass. "
"Denied to prevent output corruption. This "
"points to a bug in the HTTP/2 implementation.",

View File

@@ -48,6 +48,7 @@
#include "h2_headers.h"
#include "h2_session.h"
#include "h2_stream.h"
#include "h2_ws.h"
#include "h2_c2.h"
#include "h2_util.h"
@@ -173,6 +174,7 @@ void h2_c2_abort(conn_rec *c2, conn_rec *from)
typedef struct {
apr_bucket_brigade *bb; /* c2: data in holding area */
unsigned did_upgrade_eos:1; /* for Upgrade, we added an extra EOS */
} h2_c2_fctx_in_t;
static apr_status_t h2_c2_filter_in(ap_filter_t* f,
@@ -217,6 +219,16 @@ static apr_status_t h2_c2_filter_in(ap_filter_t* f,
}
}
/* If this is a HTTP Upgrade, it means the request we process
* has not Content, although the stream is not necessarily closed.
* On first read, we insert an EOS to signal processing that it
* has the complete body. */
if (conn_ctx->is_upgrade && !fctx->did_upgrade_eos) {
b = apr_bucket_eos_create(f->c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(fctx->bb, b);
fctx->did_upgrade_eos = 1;
}
while (APR_BRIGADE_EMPTY(fctx->bb)) {
/* Get more input data for our request. */
if (APLOGctrace2(f->c)) {
@@ -547,6 +559,31 @@ static int c2_hook_pre_connection(conn_rec *c2, void *csd)
return OK;
}
static apr_status_t c2_get_pollfd_from_conn(conn_rec *c,
struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout)
{
if (c->master) {
h2_conn_ctx_t *ctx = h2_conn_ctx_get(c);
if (ctx) {
if (ctx->beam_in && ctx->pipe_in[H2_PIPE_OUT]) {
pfd->desc_type = APR_POLL_FILE;
pfd->desc.f = ctx->pipe_in[H2_PIPE_OUT];
if (ptimeout)
*ptimeout = h2_beam_timeout_get(ctx->beam_in);
}
else {
/* no input */
pfd->desc_type = APR_NO_DESC;
if (ptimeout)
*ptimeout = -1;
}
return APR_SUCCESS;
}
}
return APR_ENOTIMPL;
}
void h2_c2_register_hooks(void)
{
/* When the connection processing actually starts, we might
@@ -558,8 +595,14 @@ void h2_c2_register_hooks(void)
/* We need to manipulate the standard HTTP/1.1 protocol filters and
* install our own. This needs to be done very early. */
ap_hook_pre_read_request(c2_pre_read_request, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_post_read_request(c2_post_read_request, NULL, NULL, APR_HOOK_REALLY_FIRST);
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)
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,
@@ -668,11 +711,21 @@ static apr_status_t c2_process(h2_conn_ctx_t *conn_ctx, conn_rec *c)
{
const h2_request *req = conn_ctx->request;
conn_state_t *cs = c->cs;
request_rec *r;
request_rec *r = NULL;
const char *tenc;
apr_time_t timeout;
apr_status_t rv = APR_SUCCESS;
if(req->protocol && !strcmp("websocket", req->protocol)) {
req = h2_ws_rewrite_request(req, c, conn_ctx->beam_in == NULL);
if (!req) {
rv = APR_EGENERAL;
goto cleanup;
}
}
r = h2_create_request_rec(req, c, conn_ctx->beam_in == NULL);
r = h2_create_request_rec(conn_ctx->request, c, conn_ctx->beam_in == NULL);
if (!r) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
"h2_c2(%s-%d): create request_rec failed, r=NULL",

View File

@@ -39,6 +39,7 @@
#include "h2_c2.h"
#include "h2_mplx.h"
#include "h2_request.h"
#include "h2_ws.h"
#include "h2_util.h"
@@ -108,15 +109,26 @@ apr_status_t h2_c2_filter_request_in(ap_filter_t *f,
/* This filter is a one-time wonder */
ap_remove_input_filter(f);
if (f->c->master && (conn_ctx = h2_conn_ctx_get(f->c)) && conn_ctx->stream_id) {
if (conn_ctx->request->http_status != H2_HTTP_STATUS_UNSET) {
if (f->c->master && (conn_ctx = h2_conn_ctx_get(f->c)) &&
conn_ctx->stream_id) {
const h2_request *req = conn_ctx->request;
if (req->http_status == H2_HTTP_STATUS_UNSET &&
req->protocol && !strcmp("websocket", req->protocol)) {
req = h2_ws_rewrite_request(req, f->c, conn_ctx->beam_in == NULL);
if (!req)
return APR_EGENERAL;
}
if (req->http_status != H2_HTTP_STATUS_UNSET) {
/* error was encountered preparing this request */
b = ap_bucket_error_create(conn_ctx->request->http_status, NULL, f->r->pool,
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(conn_ctx->request, f->r);
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);
@@ -184,7 +196,7 @@ static int uniq_field_values(void *d, const char *key, const char *val)
*/
for (i = 0, strpp = (char **) values->elts; i < values->nelts;
++i, ++strpp) {
if (*strpp && apr_strnatcasecmp(*strpp, start) == 0) {
if (*strpp && ap_cstr_casecmp(*strpp, start) == 0) {
break;
}
}
@@ -292,7 +304,7 @@ static h2_headers *create_response(request_rec *r)
while (field && (token = ap_get_list_item(r->pool, &field)) != NULL) {
for (i = 0; i < r->content_languages->nelts; ++i) {
if (!apr_strnatcasecmp(token, languages[i]))
if (!ap_cstr_casecmp(token, languages[i]))
break;
}
if (i == r->content_languages->nelts) {
@@ -636,11 +648,13 @@ apr_status_t h2_c2_filter_catch_h1_out(ap_filter_t* f, apr_bucket_brigade* bb)
int result = ap_map_http_request_error(conn_ctx->last_err,
HTTP_INTERNAL_SERVER_ERROR);
request_rec *r = h2_create_request_rec(conn_ctx->request, f->c, 1);
if (r) {
ap_die((result >= 400)? result : HTTP_INTERNAL_SERVER_ERROR, r);
b = ap_bucket_eor_create(f->c->bucket_alloc, r);
APR_BRIGADE_INSERT_TAIL(bb, b);
}
}
}
/* There are cases where we need to parse a serialized http/1.1 response.
* One example is a 100-continue answer via a mod_proxy setup. */
while (bb && !f->c->aborted && !conn_ctx->has_final_response) {

View File

@@ -77,6 +77,7 @@ typedef struct h2_config {
int output_buffered;
apr_interval_time_t stream_timeout;/* beam timeout */
int max_data_frame_len; /* max # bytes in a single h2 DATA frame */
int h2_websockets; /* if mod_h2 negotiating WebSockets */
} h2_config;
typedef struct h2_dir_config {
@@ -115,6 +116,7 @@ static h2_config defconf = {
1, /* stream output buffered */
-1, /* beam timeout */
0, /* max DATA frame len, 0 == no extra limit */
0, /* WebSockets negotiation, enabled */
};
static h2_dir_config defdconf = {
@@ -161,6 +163,7 @@ void *h2_config_create_svr(apr_pool_t *pool, server_rec *s)
conf->output_buffered = DEF_VAL;
conf->stream_timeout = DEF_VAL;
conf->max_data_frame_len = DEF_VAL;
conf->h2_websockets = DEF_VAL;
return conf;
}
@@ -210,6 +213,7 @@ static void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv)
n->padding_always = H2_CONFIG_GET(add, base, padding_always);
n->stream_timeout = H2_CONFIG_GET(add, base, stream_timeout);
n->max_data_frame_len = H2_CONFIG_GET(add, base, max_data_frame_len);
n->h2_websockets = H2_CONFIG_GET(add, base, h2_websockets);
return n;
}
@@ -301,6 +305,8 @@ static apr_int64_t h2_srv_config_geti64(const h2_config *conf, h2_config_var_t v
return H2_CONFIG_GET(conf, &defconf, stream_timeout);
case H2_CONF_MAX_DATA_FRAME_LEN:
return H2_CONFIG_GET(conf, &defconf, max_data_frame_len);
case H2_CONF_WEBSOCKETS:
return H2_CONFIG_GET(conf, &defconf, h2_websockets);
default:
return DEF_VAL;
}
@@ -363,6 +369,9 @@ static void h2_srv_config_seti(h2_config *conf, h2_config_var_t var, int val)
case H2_CONF_MAX_DATA_FRAME_LEN:
H2_CONFIG_SET(conf, max_data_frame_len, val);
break;
case H2_CONF_WEBSOCKETS:
H2_CONFIG_SET(conf, h2_websockets, val);
break;
default:
break;
}
@@ -681,6 +690,24 @@ static const char *h2_conf_set_push(cmd_parms *cmd, void *dirconf, const char *v
return "value must be On or Off";
}
static const char *h2_conf_set_websockets(cmd_parms *cmd,
void *dirconf, const char *value)
{
if (!strcasecmp(value, "On")) {
#if H2_USE_PIPES
CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 1);
return NULL;
#else
return "HTTP/2 WebSockets are not supported on this platform";
#endif
}
else if (!strcasecmp(value, "Off")) {
CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 0);
return NULL;
}
return "value must be On or Off";
}
static const char *h2_conf_add_push_priority(cmd_parms *cmd, void *_cfg,
const char *ctype, const char *sdependency,
const char *sweight)
@@ -1021,6 +1048,8 @@ const command_rec h2_cmds[] = {
RSRC_CONF, "maximum number of bytes in a single HTTP/2 DATA frame"),
AP_INIT_TAKE2("H2EarlyHint", h2_conf_add_early_hint, NULL,
OR_FILEINFO|OR_AUTHCFG, "add a a 'Link:' header for a 103 Early Hints response."),
AP_INIT_TAKE1("H2WebSockets", h2_conf_set_websockets, NULL,
RSRC_CONF, "off to disable WebSockets over HTTP/2"),
AP_END_CMD
};

View File

@@ -44,6 +44,7 @@ typedef enum {
H2_CONF_OUTPUT_BUFFER,
H2_CONF_STREAM_TIMEOUT,
H2_CONF_MAX_DATA_FRAME_LEN,
H2_CONF_WEBSOCKETS,
} h2_config_var_t;
struct apr_hash_t;

View File

@@ -53,7 +53,8 @@ struct h2_conn_ctx_t {
const struct h2_request *request; /* c2: the request to process */
struct h2_bucket_beam *beam_out; /* c2: data out, created from req_pool */
struct h2_bucket_beam *beam_in; /* c2: data in or NULL, borrowed from request stream */
unsigned int input_chunked; /* c2: if input needs HTTP/1.1 chunking applied */
unsigned input_chunked:1; /* c2: if input needs HTTP/1.1 chunking applied */
unsigned is_upgrade:1; /* c2: if requst is a HTTP Upgrade */
apr_file_t *pipe_in[2]; /* c2: input produced notification pipe */
apr_pollfd_t pfd; /* c1: poll socket input, c2: NUL */

View File

@@ -146,6 +146,7 @@ static void m_stream_cleanup(h2_mplx *m, h2_stream *stream)
if (c2_ctx->beam_in) {
h2_beam_on_send(c2_ctx->beam_in, NULL, NULL);
h2_beam_on_received(c2_ctx->beam_in, NULL, NULL);
h2_beam_on_eagain(c2_ctx->beam_in, NULL, NULL);
h2_beam_on_consumed(c2_ctx->beam_in, NULL, NULL);
}
}
@@ -666,7 +667,9 @@ static apr_status_t c1_process_stream(h2_mplx *m,
if (APLOGctrace1(m->c1)) {
const h2_request *r = stream->request;
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1,
H2_STRM_MSG(stream, "process %s %s%s%s%s"),
H2_STRM_MSG(stream, "process %s%s%s %s%s%s%s"),
r->protocol? r->protocol : "",
r->protocol? " " : "",
r->method, r->scheme? r->scheme : "",
r->scheme? "://" : "",
r->authority, r->path? r->path: "");
@@ -780,6 +783,19 @@ static void c2_beam_input_read_notify(void *ctx, h2_bucket_beam *beam)
}
}
static void c2_beam_input_read_eagain(void *ctx, h2_bucket_beam *beam)
{
conn_rec *c = ctx;
h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c);
/* installed in the input bucket beams when we use pipes.
* Drain the pipe just before the beam returns APR_EAGAIN.
* A clean state for allowing polling on the pipe to rest
* when the beam is empty */
if (conn_ctx && conn_ctx->pipe_in[H2_PIPE_OUT]) {
h2_util_drain_pipe(conn_ctx->pipe_in[H2_PIPE_OUT]);
}
}
static void c2_beam_output_write_notify(void *ctx, h2_bucket_beam *beam)
{
conn_rec *c = ctx;
@@ -824,6 +840,7 @@ static apr_status_t c2_setup_io(h2_mplx *m, conn_rec *c2, h2_stream *stream, h2_
c2->pool, c2->pool);
if (APR_SUCCESS != rv) goto cleanup;
#endif
h2_beam_on_eagain(stream->input, c2_beam_input_read_eagain, c2);
}
cleanup:
@@ -930,6 +947,15 @@ static void s_c2_done(h2_mplx *m, conn_rec *c2, h2_conn_ctx_t *conn_ctx)
"h2_c2(%s-%d): processing finished without final response",
conn_ctx->id, conn_ctx->stream_id);
c2->aborted = 1;
if (conn_ctx->beam_out)
h2_beam_abort(conn_ctx->beam_out, c2);
}
else if (!conn_ctx->beam_out || !h2_beam_is_complete(conn_ctx->beam_out)) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, conn_ctx->last_err, c2,
"h2_c2(%s-%d): processing finished with incomplete output",
conn_ctx->id, conn_ctx->stream_id);
c2->aborted = 1;
h2_beam_abort(conn_ctx->beam_out, c2);
}
else if (!c2->aborted) {
s_mplx_be_happy(m, c2, conn_ctx);

View File

@@ -381,7 +381,7 @@ static int iq_bubble_down(h2_proxy_iqueue *q, int i, int bottom,
* h2_proxy_ngheader
******************************************************************************/
#define H2_HD_MATCH_LIT_CS(l, name) \
((strlen(name) == sizeof(l) - 1) && !apr_strnatcasecmp(l, name))
((strlen(name) == sizeof(l) - 1) && !ap_cstr_casecmp(l, name))
static int h2_util_ignore_header(const char *name)
{
@@ -500,7 +500,7 @@ static int ignore_header(const literal *lits, size_t llen,
for (i = 0; i < llen; ++i) {
lit = &lits[i];
if (lit->len == nlen && !apr_strnatcasecmp(lit->name, name)) {
if (lit->len == nlen && !ap_cstr_casecmp(lit->name, name)) {
return 1;
}
}
@@ -542,7 +542,7 @@ void h2_proxy_util_camel_case_header(char *s, size_t len)
/** Match a header value against a string constance, case insensitive */
#define H2_HD_MATCH_LIT(l, name, nlen) \
((nlen == sizeof(l) - 1) && !apr_strnatcasecmp(l, name))
((nlen == sizeof(l) - 1) && !ap_cstr_casecmp(l, name))
static apr_status_t h2_headers_add_h1(apr_table_t *headers, apr_pool_t *pool,
const char *name, size_t nlen,

View File

@@ -426,7 +426,7 @@ static void inspect_link(link_ctx *ctx, const char *s, size_t slen)
static int head_iter(void *ctx, const char *key, const char *value)
{
if (!apr_strnatcasecmp("link", key)) {
if (!ap_cstr_casecmp("link", key)) {
inspect_link(ctx, value, strlen(value));
}
return 1;

View File

@@ -166,6 +166,10 @@ apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool,
&& !strncmp(H2_HEADER_AUTH, name, nlen)) {
req->authority = apr_pstrndup(pool, value, vlen);
}
else if (H2_HEADER_PROTO_LEN == nlen
&& !strncmp(H2_HEADER_PROTO, name, nlen)) {
req->protocol = apr_pstrndup(pool, value, vlen);
}
else {
char buffer[32];
memset(buffer, 0, 32);
@@ -214,6 +218,7 @@ h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src)
dst->scheme = apr_pstrdup(p, src->scheme);
dst->authority = apr_pstrdup(p, src->authority);
dst->path = apr_pstrdup(p, src->path);
dst->protocol = apr_pstrdup(p, src->protocol);
dst->headers = apr_table_clone(p, src->headers);
return dst;
}
@@ -299,13 +304,13 @@ apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r)
#endif
static void assign_headers(request_rec *r, const h2_request *req,
int no_body)
int no_body, int is_connect)
{
const char *cl;
r->headers_in = apr_table_clone(r->pool, req->headers);
if (req->authority) {
if (req->authority && !is_connect) {
/* for internal handling, we have to simulate that :authority
* came in as Host:, RFC 9113 ch. says that mismatches between
* :authority and Host: SHOULD be rejected as malformed. However,
@@ -324,6 +329,8 @@ static void assign_headers(request_rec *r, const h2_request *req,
"set 'Host: %s' from :authority", req->authority);
}
/* Unless we open a byte stream via CONNECT, apply content-length guards. */
if (!is_connect) {
cl = apr_table_get(req->headers, "Content-Length");
if (no_body) {
if (!cl && apr_table_get(req->headers, "Content-Type")) {
@@ -348,12 +355,14 @@ static void assign_headers(request_rec *r, const h2_request *req,
apr_table_mergen(r->headers_in, "Transfer-Encoding", "chunked");
}
#endif /* else AP_HAS_RESPONSE_BUCKETS */
}
}
request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c,
int no_body)
{
int access_status = HTTP_OK;
int is_connect = !ap_cstr_casecmp("CONNECT", req->method);
#if AP_MODULE_MAGIC_AT_LEAST(20120211, 106)
request_rec *r = ap_create_request(c);
@@ -362,24 +371,43 @@ request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c,
#endif
#if AP_MODULE_MAGIC_AT_LEAST(20120211, 107)
assign_headers(r, req, no_body);
assign_headers(r, req, no_body, is_connect);
ap_run_pre_read_request(r, c);
/* Time to populate r with the data we have. */
r->request_time = req->request_time;
AP_DEBUG_ASSERT(req->authority);
if (!apr_strnatcasecmp("CONNECT", req->method)) {
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->scheme && ap_cstr_casecmp(req->scheme, "http")
&& ap_cstr_casecmp(req->scheme, "https")) {
/* FIXME: we also need to create absolute uris when we are
* in a forward proxy configuration! But there is currently
* no way to detect that. */
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")) {
/* Client sent a ':scheme' pseudo header for something else
* than what we handle by default. 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",
req->method, req->scheme, req->authority,
req->path ? req->path : "");
@@ -420,7 +448,7 @@ request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c,
{
const char *s;
assign_headers(r, req, no_body);
assign_headers(r, req, no_body, is_connect);
ap_run_pre_read_request(r, c);
/* Time to populate r with the data we have. */

View File

@@ -621,9 +621,8 @@ static int on_invalid_header_cb(nghttp2_session *ngh2,
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, APLOGNO(03456)
H2_SSSN_STRM_MSG(session, frame->hd.stream_id,
"invalid header '%s: %s'"),
apr_pstrndup(session->pool, (const char *)name, namelen),
apr_pstrndup(session->pool, (const char *)value, valuelen));
"invalid header '%.*s: %.*s'"),
(int)namelen, name, (int)valuelen, value);
stream = get_stream(session, frame->hd.stream_id);
if (stream) {
h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR);
@@ -1003,7 +1002,7 @@ apr_status_t h2_session_create(h2_session **psession, conn_rec *c, request_rec *
static apr_status_t h2_session_start(h2_session *session, int *rv)
{
apr_status_t status = APR_SUCCESS;
nghttp2_settings_entry settings[3];
nghttp2_settings_entry settings[4];
size_t slen;
int win_size;
@@ -1070,6 +1069,11 @@ static apr_status_t h2_session_start(h2_session *session, int *rv)
settings[slen].value = win_size;
++slen;
}
if (h2_config_sgeti(session->s, H2_CONF_WEBSOCKETS)) {
settings[slen].settings_id = NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL;
settings[slen].value = 1;
++slen;
}
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c1,
H2_SSSN_LOG(APLOGNO(03201), session,

View File

@@ -767,6 +767,9 @@ apr_status_t h2_stream_add_header(h2_stream *stream,
status = h2_request_add_header(stream->rtmp, stream->pool,
name, nlen, value, vlen,
session->s->limit_req_fieldsize, &was_added);
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c1,
H2_STRM_MSG(stream, "add_header: '%.*s: %.*s"),
(int)nlen, name, (int)vlen, value);
if (was_added) ++stream->request_headers_added;
}
else if (H2_SS_OPEN == stream->state) {
@@ -897,7 +900,14 @@ 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->scheme || req->path) {
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"));
}
}
else if (req->scheme || req->path) {
ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1,
H2_STRM_LOG(APLOGNO(10384), stream, "Request to CONNECT "
"with :scheme or :path specified, sending 400 answer"));
@@ -1459,8 +1469,8 @@ static ssize_t stream_data_cb(nghttp2_session *ng2s,
* it is all fine. */
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1,
H2_SSSN_STRM_MSG(session, stream_id, "rst stream"));
h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR);
return NGHTTP2_ERR_CALLBACK_FAILURE;
h2_stream_rst(stream, H2_ERR_STREAM_CLOSED);
return NGHTTP2_ERR_DEFERRED;
}
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1,
H2_SSSN_STRM_MSG(session, stream_id,
@@ -1469,10 +1479,17 @@ static ssize_t stream_data_cb(nghttp2_session *ng2s,
eos = 1;
rv = APR_SUCCESS;
}
else if (APR_ECONNRESET == rv || APR_ECONNABORTED == rv) {
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c1,
H2_STRM_LOG(APLOGNO(), stream, "data_cb, reading data"));
h2_stream_rst(stream, H2_ERR_STREAM_CLOSED);
return NGHTTP2_ERR_DEFERRED;
}
else {
ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c1,
H2_STRM_LOG(APLOGNO(02938), stream, "data_cb, reading data"));
return NGHTTP2_ERR_CALLBACK_FAILURE;
h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR);
return NGHTTP2_ERR_DEFERRED;
}
}

View File

@@ -1281,8 +1281,8 @@ apr_size_t h2_util_bucket_print(char *buffer, apr_size_t bmax,
else if (bmax > off) {
off += apr_snprintf(buffer+off, bmax-off, "%s[%ld]",
b->type->name,
(long)(b->length == ((apr_size_t)-1)?
-1 : b->length));
(b->length == ((apr_size_t)-1)?
-1 : (long)b->length));
}
return off;
}
@@ -1650,7 +1650,7 @@ static int contains_name(const literal *lits, size_t llen, nghttp2_nv *nv)
for (i = 0; i < llen; ++i) {
lit = &lits[i];
if (lit->len == nv->namelen
&& !apr_strnatcasecmp(lit->name, (const char *)nv->name)) {
&& !ap_cstr_casecmp(lit->name, (const char *)nv->name)) {
return 1;
}
}
@@ -1706,7 +1706,7 @@ static apr_status_t req_add_header(apr_table_t *headers, apr_pool_t *pool,
return APR_SUCCESS;
}
else if (nv->namelen == sizeof("cookie")-1
&& !apr_strnatcasecmp("cookie", (const char *)nv->name)) {
&& !ap_cstr_casecmp("cookie", (const char *)nv->name)) {
existing = apr_table_get(headers, "cookie");
if (existing) {
/* Cookie header come separately in HTTP/2, but need
@@ -1725,7 +1725,7 @@ static apr_status_t req_add_header(apr_table_t *headers, apr_pool_t *pool,
}
}
else if (nv->namelen == sizeof("host")-1
&& !apr_strnatcasecmp("host", (const char *)nv->name)) {
&& !ap_cstr_casecmp("host", (const char *)nv->name)) {
if (apr_table_get(headers, "Host")) {
return APR_SUCCESS; /* ignore duplicate */
}
@@ -1883,6 +1883,13 @@ void h2_util_drain_pipe(apr_file_t *pipe)
{
char rb[512];
apr_size_t nr = sizeof(rb);
apr_interval_time_t timeout;
apr_status_t trv;
/* Make the pipe non-blocking if we can */
trv = apr_file_pipe_timeout_get(pipe, &timeout);
if (trv == APR_SUCCESS)
apr_file_pipe_timeout_set(pipe, 0);
while (apr_file_read(pipe, rb, &nr) == APR_SUCCESS) {
/* Although we write just one byte to the other end of the pipe
@@ -1893,6 +1900,8 @@ void h2_util_drain_pipe(apr_file_t *pipe)
if (nr != sizeof(rb))
break;
}
if (trv == APR_SUCCESS)
apr_file_pipe_timeout_set(pipe, timeout);
}
apr_status_t h2_util_wait_on_pipe(apr_file_t *pipe)

View File

@@ -337,7 +337,7 @@ apr_size_t h2_util_table_bytes(apr_table_t *t, apr_size_t pair_extra);
/** Match a header value against a string constance, case insensitive */
#define H2_HD_MATCH_LIT(l, name, nlen) \
((nlen == sizeof(l) - 1) && !apr_strnatcasecmp(l, name))
((nlen == sizeof(l) - 1) && !ap_cstr_casecmp(l, name))
/*******************************************************************************
* HTTP/2 header helpers

View File

@@ -27,7 +27,7 @@
* @macro
* Version number of the http2 module as c string
*/
#define MOD_HTTP2_VERSION "2.0.19-git"
#define MOD_HTTP2_VERSION "2.0.20-git"
/**
* @macro
@@ -35,7 +35,7 @@
* release. This is a 24 bit number with 8 bits for major number, 8 bits
* for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
*/
#define MOD_HTTP2_VERSION_NUM 0x020013
#define MOD_HTTP2_VERSION_NUM 0x020014
#endif /* mod_h2_h2_version_h */

326
modules/http2/h2_ws.c Normal file
View File

@@ -0,0 +1,326 @@
/* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include "apr.h"
#include "apr_strings.h"
#include "apr_lib.h"
#include "apr_encode.h"
#include "apr_sha1.h"
#include "apr_strmatch.h"
#include <ap_mmn.h>
#include <httpd.h>
#include <http_core.h>
#include <http_connection.h>
#include <http_protocol.h>
#include <http_request.h>
#include <http_log.h>
#include <http_ssl.h>
#include <http_vhost.h>
#include <util_filter.h>
#include <ap_mpm.h>
#include "h2_private.h"
#include "h2_config.h"
#include "h2_conn_ctx.h"
#include "h2_headers.h"
#include "h2_request.h"
#include "h2_ws.h"
static ap_filter_rec_t *c2_ws_out_filter_handle;
struct ws_filter_ctx {
const char *ws_accept_base64;
int has_final_response;
int override_body;
};
/**
* Generate the "Sec-WebSocket-Accept" header field for the given key
* (base64 encoded) as defined in RFC 6455 ch. 4.2.2 step 5.3
*/
static const char *gen_ws_accept(conn_rec *c, const char *key_base64)
{
apr_byte_t dgst[APR_SHA1_DIGESTSIZE];
const char ws_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
apr_sha1_ctx_t sha1_ctx;
apr_sha1_init(&sha1_ctx);
apr_sha1_update(&sha1_ctx, key_base64, (unsigned int)strlen(key_base64));
apr_sha1_update(&sha1_ctx, ws_guid, (unsigned int)strlen(ws_guid));
apr_sha1_final(dgst, &sha1_ctx);
return apr_pencode_base64_binary(c->pool, dgst, sizeof(dgst),
APR_ENCODE_NONE, NULL);
}
const h2_request *h2_ws_rewrite_request(const h2_request *req,
conn_rec *c2, int no_body)
{
h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2);
h2_request *wsreq;
unsigned char key_raw[16];
const char *key_base64, *accept_base64;
struct ws_filter_ctx *ws_ctx;
apr_status_t rv;
if (!conn_ctx || !req->protocol || strcmp("websocket", req->protocol))
return req;
if (ap_cstr_casecmp("CONNECT", req->method)) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2,
"h2_c2(%s-%d): websocket request with method %s",
conn_ctx->id, conn_ctx->stream_id, req->method);
return req;
}
if (!req->scheme) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2,
"h2_c2(%s-%d): websocket CONNECT without :scheme",
conn_ctx->id, conn_ctx->stream_id);
return req;
}
if (!req->path) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2,
"h2_c2(%s-%d): websocket CONNECT without :path",
conn_ctx->id, conn_ctx->stream_id);
return req;
}
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2,
"h2_c2(%s-%d): websocket CONNECT for %s",
conn_ctx->id, conn_ctx->stream_id, req->path);
/* Transform the HTTP/2 extended CONNECT to an internal GET using
* the HTTP/1.1 version of websocket connection setup. */
wsreq = h2_request_clone(c2->pool, req);
wsreq->method = "GET";
wsreq->protocol = NULL;
apr_table_set(wsreq->headers, "Upgrade", "websocket");
/* add Sec-WebSocket-Key header */
rv = apr_generate_random_bytes(key_raw, sizeof(key_raw));
if (rv != APR_SUCCESS) {
ap_log_error(APLOG_MARK, APLOG_CRIT, rv, NULL, APLOGNO(10461)
"error generating secret");
return NULL;
}
key_base64 = apr_pencode_base64_binary(c2->pool, key_raw, sizeof(key_raw),
APR_ENCODE_NONE, NULL);
apr_table_set(wsreq->headers, "Sec-WebSocket-Key", key_base64);
/* This is now the request to process internally */
/* When this request gets processed and delivers a 101 response,
* we expect it to carry a "Sec-WebSocket-Accept" header with
* exactly the following value, as per RFC 6455. */
accept_base64 = gen_ws_accept(c2, key_base64);
/* Add an output filter that intercepts generated responses:
* - if a valid WebSocket negotiation happens, transform the
* 101 response to a 200
* - if a 2xx response happens, that does not pass the Accept test,
* return a 502 indicating that the URI seems not support the websocket
* protocol (RFC 8441 does not define this, but it seems the best
* choice)
* - if a 3xx, 4xx or 5xx response happens, forward this unchanged.
*/
ws_ctx = apr_pcalloc(c2->pool, sizeof(*ws_ctx));
ws_ctx->ws_accept_base64 = accept_base64;
/* insert our filter just before the C2 core filter */
ap_remove_output_filter_byhandle(c2->output_filters, "H2_C2_NET_OUT");
ap_add_output_filter("H2_C2_WS_OUT", ws_ctx, NULL, c2);
ap_add_output_filter("H2_C2_NET_OUT", NULL, NULL, c2);
/* Mark the connection as being an Upgrade, with some special handling
* since the request needs an EOS, without the stream being closed */
conn_ctx->is_upgrade = 1;
return wsreq;
}
static apr_bucket *make_valid_resp(conn_rec *c2, int status,
apr_table_t *headers, apr_table_t *notes)
{
apr_table_t *nheaders, *nnotes;
ap_assert(headers);
nheaders = apr_table_clone(c2->pool, headers);
apr_table_unset(nheaders, "Connection");
apr_table_unset(nheaders, "Upgrade");
apr_table_unset(nheaders, "Sec-WebSocket-Accept");
nnotes = notes? apr_table_clone(c2->pool, notes) :
apr_table_make(c2->pool, 10);
#if AP_HAS_RESPONSE_BUCKETS
return ap_bucket_response_create(status, NULL, nheaders, nnotes,
c2->pool, c2->bucket_alloc);
#else
return h2_bucket_headers_create(c2->bucket_alloc,
h2_headers_create(status, nheaders,
nnotes, 0, c2->pool));
#endif
}
static apr_bucket *make_invalid_resp(conn_rec *c2, int status,
apr_table_t *notes)
{
apr_table_t *nheaders, *nnotes;
nheaders = apr_table_make(c2->pool, 10);
apr_table_setn(nheaders, "Content-Length", "0");
nnotes = notes? apr_table_clone(c2->pool, notes) :
apr_table_make(c2->pool, 10);
#if AP_HAS_RESPONSE_BUCKETS
return ap_bucket_response_create(status, NULL, nheaders, nnotes,
c2->pool, c2->bucket_alloc);
#else
return h2_bucket_headers_create(c2->bucket_alloc,
h2_headers_create(status, nheaders,
nnotes, 0, c2->pool));
#endif
}
static void ws_handle_resp(conn_rec *c2, h2_conn_ctx_t *conn_ctx,
struct ws_filter_ctx *ws_ctx, apr_bucket *b)
{
#if AP_HAS_RESPONSE_BUCKETS
ap_bucket_response *resp = b->data;
#else /* AP_HAS_RESPONSE_BUCKETS */
h2_headers *resp = h2_bucket_headers_get(b);
#endif /* !AP_HAS_RESPONSE_BUCKETS */
apr_bucket *b_override = NULL;
int is_final = 0;
int override_body = 0;
if (ws_ctx->has_final_response) {
/* already did, nop */
return;
}
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2,
"h2_c2(%s-%d): H2_C2_WS_OUT inspecting response %d",
conn_ctx->id, conn_ctx->stream_id, resp->status);
if (resp->status == HTTP_SWITCHING_PROTOCOLS) {
/* The resource agreed to switch protocol. But this is only valid
* if it send back the correct Sec-WebSocket-Accept header value */
const char *hd = apr_table_get(resp->headers, "Sec-WebSocket-Accept");
if (hd && !strcmp(ws_ctx->ws_accept_base64, hd)) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2,
"h2_c2(%s-%d): websocket CONNECT, valid 101 Upgrade"
", converting to 200 response",
conn_ctx->id, conn_ctx->stream_id);
b_override = make_valid_resp(c2, HTTP_OK, resp->headers, resp->notes);
is_final = 1;
}
else {
if (!hd) {
/* This points to someone being confused */
ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c2, APLOGNO(10462)
"h2_c2(%s-%d): websocket CONNECT, got 101 response "
"without Sec-WebSocket-Accept header",
conn_ctx->id, conn_ctx->stream_id);
}
else {
/* This points to a bug, either in our WebSockets negotiation
* or in the request processings implementation of WebSockets */
ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c2, APLOGNO(10463)
"h2_c2(%s-%d): websocket CONNECT, 101 response "
"without 'Sec-WebSocket-Accept: %s' but expected %s",
conn_ctx->id, conn_ctx->stream_id, hd,
ws_ctx->ws_accept_base64);
}
b_override = make_invalid_resp(c2, HTTP_BAD_GATEWAY, resp->notes);
override_body = is_final = 1;
}
}
else if (resp->status < 200) {
/* other intermediate response, pass through */
}
else if (resp->status < 300) {
/* Failure, we might be talking to a plain http resource */
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2,
"h2_c2(%s-%d): websocket CONNECT, invalid response %d",
conn_ctx->id, conn_ctx->stream_id, resp->status);
b_override = make_invalid_resp(c2, HTTP_BAD_GATEWAY, resp->notes);
override_body = is_final = 1;
}
else {
/* error response, pass through. */
ws_ctx->has_final_response = 1;
}
if (b_override) {
APR_BUCKET_INSERT_BEFORE(b, b_override);
apr_bucket_delete(b);
b = b_override;
}
if (override_body) {
APR_BUCKET_INSERT_AFTER(b, apr_bucket_eos_create(c2->bucket_alloc));
ws_ctx->override_body = 1;
}
if (is_final) {
ws_ctx->has_final_response = 1;
conn_ctx->has_final_response = 1;
}
}
static apr_status_t h2_c2_ws_filter_out(ap_filter_t* f, apr_bucket_brigade* bb)
{
struct ws_filter_ctx *ws_ctx = f->ctx;
h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c);
apr_bucket *b, *bnext;
ap_assert(conn_ctx);
if (ws_ctx->override_body) {
/* We have overridden the original response and also its body.
* If this filter is called again, we signal a hard abort to
* allow processing to terminate at the earliest. */
f->c->aborted = 1;
return APR_ECONNABORTED;
}
/* Inspect the brigade, looking for RESPONSE/HEADER buckets.
* Remember, this filter is only active for client websocket CONNECT
* requests that we translated to an internal GET with websocket
* headers.
* We inspect the repsone to see if the internal resource actually
* agrees to talk websocket or is "just" a normal HTTP resource that
* ignored the websocket request headers. */
for (b = APR_BRIGADE_FIRST(bb);
b != APR_BRIGADE_SENTINEL(bb);
b = bnext)
{
bnext = APR_BUCKET_NEXT(b);
if (APR_BUCKET_IS_METADATA(b)) {
#if AP_HAS_RESPONSE_BUCKETS
if (AP_BUCKET_IS_RESPONSE(b)) {
#else
if (H2_BUCKET_IS_HEADERS(b)) {
#endif /* !AP_HAS_RESPONSE_BUCKETS */
ws_handle_resp(f->c, conn_ctx, ws_ctx, b);
continue;
}
}
else if (ws_ctx->override_body) {
apr_bucket_delete(b);
}
}
return ap_pass_brigade(f->next, bb);
}
void h2_ws_register_hooks(void)
{
c2_ws_out_filter_handle =
ap_register_output_filter("H2_C2_WS_OUT", h2_c2_ws_filter_out,
NULL, AP_FTYPE_NETWORK);
}

35
modules/http2/h2_ws.h Normal file
View File

@@ -0,0 +1,35 @@
/* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef __mod_h2__h2_ws__
#define __mod_h2__h2_ws__
#include "h2.h"
/**
* Rewrite a websocket request.
*
* @param req the h2 request to rewrite
* @param conn the connection to process the request on
* @param no_body != 0 iff the request is known to have no body
* @return the websocket request for internal submit
*/
const h2_request *h2_ws_rewrite_request(const h2_request *req,
conn_rec *c2, int no_body);
void h2_ws_register_hooks(void);
#endif /* defined(__mod_h2__h2_ws__) */

View File

@@ -42,6 +42,7 @@
#include "h2_switch.h"
#include "h2_version.h"
#include "h2_bucket_beam.h"
#include "h2_ws.h"
static void h2_hooks(apr_pool_t *pool);
@@ -199,6 +200,7 @@ static void h2_hooks(apr_pool_t *pool)
h2_c1_register_hooks();
h2_switch_register_hooks();
h2_c2_register_hooks();
h2_ws_register_hooks();
/* Setup subprocess env for certain variables
*/

View File

@@ -173,6 +173,10 @@ SOURCE=./h2_workers.c
# End Source File
# Begin Source File
SOURCE=./h2_ws.c
# End Source File
# Begin Source File
SOURCE=./mod_http2.c
# End Source File
# Begin Source File

View File

@@ -21,6 +21,7 @@
#include "apr_version.h"
#include "apr_strings.h"
#include "apr_hash.h"
#include "http_core.h"
#include "proxy_util.h"
#include "ajp.h"
#include "scgi.h"
@@ -4871,7 +4872,7 @@ PROXY_DECLARE(apr_status_t) ap_proxy_tunnel_create(proxy_tunnel_rec **ptunnel,
{
apr_status_t rv;
conn_rec *c_i = r->connection;
apr_interval_time_t timeout = -1;
apr_interval_time_t client_timeout = -1, origin_timeout = -1;
proxy_tunnel_rec *tunnel;
*ptunnel = NULL;
@@ -4898,9 +4899,16 @@ PROXY_DECLARE(apr_status_t) ap_proxy_tunnel_create(proxy_tunnel_rec **ptunnel,
tunnel->client->bb = apr_brigade_create(c_i->pool, c_i->bucket_alloc);
tunnel->client->pfd = &APR_ARRAY_PUSH(tunnel->pfds, apr_pollfd_t);
tunnel->client->pfd->p = r->pool;
tunnel->client->pfd->desc_type = APR_POLL_SOCKET;
tunnel->client->pfd->desc.s = ap_get_conn_socket(c_i);
tunnel->client->pfd->desc_type = APR_NO_DESC;
rv = ap_get_pollfd_from_conn(tunnel->client->c,
tunnel->client->pfd, &client_timeout);
if (rv != APR_SUCCESS) {
return rv;
}
tunnel->client->pfd->client_data = tunnel->client;
if (tunnel->client->pfd->desc_type == APR_POLL_SOCKET) {
apr_socket_opt_set(tunnel->client->pfd->desc.s, APR_SO_NONBLOCK, 1);
}
tunnel->origin->c = c_o;
tunnel->origin->name = "origin";
@@ -4910,17 +4918,12 @@ PROXY_DECLARE(apr_status_t) ap_proxy_tunnel_create(proxy_tunnel_rec **ptunnel,
tunnel->origin->pfd->desc_type = APR_POLL_SOCKET;
tunnel->origin->pfd->desc.s = ap_get_conn_socket(c_o);
tunnel->origin->pfd->client_data = tunnel->origin;
apr_socket_timeout_get(tunnel->origin->pfd->desc.s, &origin_timeout);
apr_socket_opt_set(tunnel->origin->pfd->desc.s, APR_SO_NONBLOCK, 1);
/* Defaults to the biggest timeout of both connections */
apr_socket_timeout_get(tunnel->client->pfd->desc.s, &timeout);
apr_socket_timeout_get(tunnel->origin->pfd->desc.s, &tunnel->timeout);
if (timeout >= 0 && (tunnel->timeout < 0 || tunnel->timeout < timeout)) {
tunnel->timeout = timeout;
}
/* We should be nonblocking from now on the sockets */
apr_socket_opt_set(tunnel->client->pfd->desc.s, APR_SO_NONBLOCK, 1);
apr_socket_opt_set(tunnel->origin->pfd->desc.s, APR_SO_NONBLOCK, 1);
tunnel->timeout = (origin_timeout >= 0 && origin_timeout > client_timeout)?
origin_timeout : client_timeout;
/* Bidirectional non-HTTP stream will confuse mod_reqtimeoout */
ap_remove_input_filter_byhandle(c_i->input_filters, "reqtimeout");
@@ -4938,15 +4941,44 @@ PROXY_DECLARE(apr_status_t) ap_proxy_tunnel_create(proxy_tunnel_rec **ptunnel,
tunnel->nohalfclose = 1;
}
/* Start with POLLOUT and let ap_proxy_tunnel_run() schedule both
* directions when there are no output data pending (anymore).
*/
if (tunnel->client->pfd->desc_type == APR_POLL_SOCKET) {
/* Both ends are sockets, the poll strategy is:
* - poll both sides POLLOUT
* - when one side is writable, remove the POLLOUT
* and add POLLIN to the other side.
* - tunnel arriving data, remove POLLIN from the source
* again and add POLLOUT to the receiving side
* - on EOF on read, remove the POLLIN from that side
* Repeat until both sides are down */
tunnel->client->pfd->reqevents = APR_POLLOUT | APR_POLLERR;
tunnel->origin->pfd->reqevents = APR_POLLOUT | APR_POLLERR;
if ((rv = apr_pollset_add(tunnel->pollset, tunnel->client->pfd))
|| (rv = apr_pollset_add(tunnel->pollset, tunnel->origin->pfd))) {
if ((rv = apr_pollset_add(tunnel->pollset, tunnel->origin->pfd)) ||
(rv = apr_pollset_add(tunnel->pollset, tunnel->client->pfd))) {
return rv;
}
}
else if (tunnel->client->pfd->desc_type == APR_POLL_FILE) {
/* Input is a PIPE fd, the poll strategy is:
* - always POLLIN on origin
* - use socket strategy described above for client only
* otherwise the same
*/
tunnel->client->pfd->reqevents = 0;
tunnel->origin->pfd->reqevents = APR_POLLIN | APR_POLLHUP |
APR_POLLOUT | APR_POLLERR;
if ((rv = apr_pollset_add(tunnel->pollset, tunnel->origin->pfd))) {
return rv;
}
}
else {
/* input is already closed, unsual, but we know nothing about
* the tunneled protocol. */
tunnel->client->down_in = 1;
tunnel->origin->pfd->reqevents = APR_POLLIN | APR_POLLHUP;
if ((rv = apr_pollset_add(tunnel->pollset, tunnel->origin->pfd))) {
return rv;
}
}
*ptunnel = tunnel;
return APR_SUCCESS;
@@ -5054,8 +5086,24 @@ static int proxy_tunnel_transfer(proxy_tunnel_rec *tunnel,
}
del_pollset(tunnel->pollset, in->pfd, APR_POLLIN);
if (out->pfd->desc_type == APR_POLL_SOCKET) {
/* if the output is a SOCKET, we can stop polling the input
* until the output signals POLLOUT again. */
add_pollset(tunnel->pollset, out->pfd, APR_POLLOUT);
}
else {
/* We can't use POLLOUT in this direction for the only
* APR_POLL_FILE case we have so far (mod_h2's "signal" pipe),
* we assume that the client's ouput filters chain will block/flush
* if necessary (i.e. no pending data), hence that the origin
* is EOF when reaching here. This direction is over. */
ap_assert(in->down_in && APR_STATUS_IS_EOF(rv));
ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, tunnel->r,
"proxy: %s: %s write shutdown",
tunnel->scheme, out->name);
out->down_out = 1;
}
}
return OK;
}

View File

@@ -92,6 +92,7 @@
APR_HOOK_STRUCT(
APR_HOOK_LINK(get_mgmt_items)
APR_HOOK_LINK(insert_network_bucket)
APR_HOOK_LINK(get_pollfd_from_conn)
)
AP_IMPLEMENT_HOOK_RUN_ALL(int, get_mgmt_items,
@@ -103,6 +104,11 @@ AP_IMPLEMENT_HOOK_RUN_FIRST(apr_status_t, insert_network_bucket,
apr_socket_t *socket),
(c, bb, socket), AP_DECLINED)
AP_IMPLEMENT_HOOK_RUN_FIRST(apr_status_t, get_pollfd_from_conn,
(conn_rec *c, struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout),
(c, pfd, ptimeout), APR_ENOTIMPL)
/* Server core module... This module provides support for really basic
* server operations, including options and commands which control the
* operation of other modules. Consider this the bureaucracy module.
@@ -5971,6 +5977,28 @@ static int core_upgrade_storage(request_rec *r)
return DECLINED;
}
static apr_status_t core_get_pollfd_from_conn(conn_rec *c,
struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout)
{
if (c && !c->master) {
pfd->desc_type = APR_POLL_SOCKET;
pfd->desc.s = ap_get_conn_socket(c);
if (ptimeout) {
apr_socket_timeout_get(pfd->desc.s, ptimeout);
}
return APR_SUCCESS;
}
return APR_ENOTIMPL;
}
AP_CORE_DECLARE(apr_status_t) ap_get_pollfd_from_conn(conn_rec *c,
struct apr_pollfd_t *pfd,
apr_interval_time_t *ptimeout)
{
return ap_run_get_pollfd_from_conn(c, pfd, ptimeout);
}
static void register_hooks(apr_pool_t *p)
{
errorlog_hash = apr_hash_make(p);
@@ -6016,6 +6044,8 @@ static void register_hooks(apr_pool_t *p)
ap_hook_open_htaccess(ap_open_htaccess, NULL, NULL, APR_HOOK_REALLY_LAST);
ap_hook_optional_fn_retrieve(core_optional_fn_retrieve, NULL, NULL,
APR_HOOK_MIDDLE);
ap_hook_get_pollfd_from_conn(core_get_pollfd_from_conn, NULL, NULL,
APR_HOOK_REALLY_LAST);
ap_hook_input_pending(ap_filter_input_pending, NULL, NULL,
APR_HOOK_MIDDLE);

1
test/clients/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
h2ws

20
test/clients/Makefile.in Normal file
View File

@@ -0,0 +1,20 @@
DISTCLEAN_TARGETS = h2ws
CLEAN_TARGETS = h2ws
bin_PROGRAMS = h2ws
TARGETS = $(bin_PROGRAMS)
PROGRAM_LDADD = $(UTIL_LDFLAGS) $(PROGRAM_DEPENDENCIES) $(EXTRA_LIBS) $(AP_LIBS)
PROGRAM_DEPENDENCIES =
include $(top_builddir)/build/rules.mk
h2ws.lo: h2ws.c
$(LIBTOOL) --mode=compile $(CC) $(ab_CFLAGS) $(ALL_CFLAGS) $(ALL_CPPFLAGS) \
$(ALL_INCLUDES) $(PICFLAGS) $(LTCFLAGS) -c $< && touch $@
h2ws_OBJECTS = h2ws.lo
h2ws_LDADD = -lnghttp2
h2ws: $(h2ws_OBJECTS)
$(LIBTOOL) --mode=link $(CC) $(ALL_CFLAGS) $(PILDFLAGS) \
$(LT_LDFLAGS) $(ALL_LDFLAGS) -o $@ $(h2ws_LTFLAGS) $(h2ws_OBJECTS) $(h2ws_LDADD)

1096
test/clients/h2ws.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
import inspect
import logging
import os
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
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]]:
""" 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 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
:param timeout: timeout for waiting on h2ws to finish
:param scenario: name of scenario h2ws should run in
:param wait_close: time to wait before closing input
:return: ExecResult with exit_code/stdout/stderr of run
"""
h2ws = os.path.join(env.clients_dir, 'h2ws')
if not os.path.exists(h2ws):
pytest.fail(f'test client not build: {h2ws}')
args = [
h2ws, '-vv', '-c', f'localhost:{env.http_port}',
f'ws://cgi.{env.http_tld}:{env.http_port}{path}',
scenario
]
# we write all output to files, because we manipulate input timings
# and would run in deadlock situations with h2ws blocking operations
# because its output is not consumed
with open(f'{env.gen_dir}/h2ws.stdout', 'w') as fdout:
with open(f'{env.gen_dir}/h2ws.stderr', 'w') as fderr:
proc = subprocess.Popen(args=args, stdin=subprocess.PIPE,
stdout=fdout, stderr=fderr)
if do_input is not None:
do_input(proc)
elif inbytes is not None:
proc.stdin.write(inbytes)
proc.stdin.flush()
if wait_close > 0:
time.sleep(wait_close)
try:
inbytes = WsFrame.client_close(code=1000).to_network() if send_close else None
proc.communicate(input=inbytes, timeout=timeout)
except subprocess.TimeoutExpired:
log.error(f'ws_run: timeout expired')
proc.kill()
proc.communicate(timeout=timeout)
lines = open(f'{env.gen_dir}/h2ws.stdout').read().splitlines()
infos = [line for line in lines if line.startswith('[1] ')]
hex_content = ' '.join([line for line in lines if not line.startswith('[1] ')])
if len(infos) > 0 and infos[0] == '[1] :status: 200':
frames = WsFrameReader.parse(bytearray.fromhex(hex_content))
else:
frames = bytearray.fromhex(hex_content)
return ExecResult(args=args, exit_code=proc.returncode,
stdout=b'', stderr=b''), infos, frames
@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}')
class TestWebSockets:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
# Apache config that CONNECT proxies a WebSocket server for paths starting
# with '/ws/'
# The WebSocket server is started in pytest fixture 'ws_server' below.
conf = H2Conf(env, extras={
f'cgi.{env.http_tld}': [
f' H2WebSockets on',
f' ProxyPass /ws/ http://127.0.0.1:{env.ws_port}/ \\',
f' upgrade=websocket timeout=10',
]
})
conf.add_vhost_cgi(proxy_self=True, h2proxy_self=True).install()
assert env.apache_restart() == 0
def ws_check_alive(self, env, timeout=5):
url = f'http://localhost:{env.ws_port}/'
end = datetime.now() + timedelta(seconds=timeout)
while datetime.now() < end:
r = env.curl_get(url, 5)
if r.exit_code == 0:
return True
time.sleep(.1)
return False
def _mkpath(self, path):
if not os.path.exists(path):
return os.makedirs(path)
def _rmrf(self, path):
if os.path.exists(path):
return shutil.rmtree(path)
@pytest.fixture(autouse=True, scope='class')
def ws_server(self, env):
# Run our python websockets server that has some special behaviour
# for the different path to CONNECT to.
run_dir = os.path.join(env.gen_dir, 'ws-server')
err_file = os.path.join(run_dir, 'stderr')
self._rmrf(run_dir)
self._mkpath(run_dir)
with open(err_file, 'w') as cerr:
cmd = os.path.join(os.path.dirname(inspect.getfile(TestWebSockets)),
'ws_server.py')
args = ['python3', cmd, '--port', str(env.ws_port)]
p = subprocess.Popen(args=args, cwd=run_dir, stderr=cerr,
stdout=cerr)
if not self.ws_check_alive(env):
p.kill()
p.wait()
pytest.fail(f'ws_server did not start. stderr={open(err_file).readlines()}')
yield
p.terminate()
# a correct CONNECT, send CLOSE, expect CLOSE, basic success
def test_h2_800_01_ws_empty(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/echo/')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) == 1, f'{frames}'
assert frames[0].opcode == WsFrame.CLOSE, f'{frames}'
# CONNECT with invalid :protocol header, must fail
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}'
# CONNECT to a URL path that does not exist on the server
def test_h2_800_03_not_found(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/does-not-exist')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 404', '[1] EOF'], f'{r}'
# CONNECT to a URL path that is a normal HTTP file resource
# we do not want to receive the body of that
def test_h2_800_04_non_ws_resource(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/alive.json')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 502', '[1] EOF'], f'{r}'
assert frames == b''
# CONNECT to a URL path that sends a delayed HTTP response body
# we do not want to receive the body of that
def test_h2_800_05_non_ws_delay_resource(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/h2test/error?body_delay=100ms')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 502', '[1] EOF'], f'{r}'
assert frames == b''
# CONNECT missing the sec-webSocket-version header
def test_h2_800_06_miss_version(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/echo/', scenario='miss-version')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 400', '[1] EOF'], f'{r}'
# CONNECT missing the :path header
def test_h2_800_07_miss_path(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/echo/', scenario='miss-path')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] RST'], f'{r}'
# CONNECT missing the :scheme header
def test_h2_800_08_miss_scheme(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/echo/', scenario='miss-scheme')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] RST'], f'{r}'
# CONNECT missing the :authority header
def test_h2_800_09_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 and exchange a PING
def test_h2_800_10_ws_ping(self, env: H2TestEnv, ws_server):
ping = WsFrame.client_ping(b'12345')
r, infos, frames = ws_run(env, path='/ws/echo/', inbytes=ping.to_network())
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) == 2, f'{frames}'
assert frames[0].opcode == WsFrame.PONG, f'{frames}'
assert frames[0].data == ping.data, f'{frames}'
assert frames[1].opcode == WsFrame.CLOSE, f'{frames}'
# CONNECT and send several PINGs with a delay of 200ms
def test_h2_800_11_ws_timed_pings(self, env: H2TestEnv, ws_server):
frame_count = 5
ping = WsFrame.client_ping(b'12345')
def do_send(proc):
for _ in range(frame_count):
try:
proc.stdin.write(ping.to_network())
proc.stdin.flush()
proc.wait(timeout=0.2)
except subprocess.TimeoutExpired:
pass
r, infos, frames = ws_run(env, path='/ws/echo/', do_input=do_send)
assert r.exit_code == 0
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) == frame_count + 1, f'{frames}'
assert frames[-1].opcode == WsFrame.CLOSE, f'{frames}'
for i in range(frame_count):
assert frames[i].opcode == WsFrame.PONG, f'{frames}'
assert frames[i].data == ping.data, f'{frames}'
# CONNECT to path that closes immediately
def test_h2_800_12_ws_unknown(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/unknown')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) == 1, f'{frames}'
# expect a CLOSE with code=4999, reason='path unknown'
assert frames[0].opcode == WsFrame.CLOSE, f'{frames}'
assert frames[0].data[2:].decode() == 'path unknown', f'{frames}'
# CONNECT to a path that sends us 1 TEXT frame
def test_h2_800_13_ws_text(self, env: H2TestEnv, ws_server):
r, infos, frames = ws_run(env, path='/ws/text/')
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) == 2, f'{frames}'
assert frames[0].opcode == WsFrame.TEXT, f'{frames}'
assert frames[0].data.decode() == 'hello!', f'{frames}'
assert frames[1].opcode == WsFrame.CLOSE, f'{frames}'
# CONNECT to a path that sends us a named file in BINARY frames
@pytest.mark.parametrize("fname,flen", [
("data-1k", 1000),
("data-10k", 10000),
("data-100k", 100*1000),
("data-1m", 1000*1000),
])
def test_h2_800_14_ws_file(self, env: H2TestEnv, ws_server, fname, flen):
r, infos, frames = ws_run(env, path=f'/ws/file/{fname}', wait_close=0.5)
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) > 0
total_len = sum([f.data_len for f in frames if f.opcode == WsFrame.BINARY])
assert total_len == flen, f'{frames}'
# CONNECT to path with 1MB file and trigger varying BINARY frame lengths
@pytest.mark.parametrize("frame_len", [
1000 * 1024,
100 * 1024,
10 * 1024,
1 * 1024,
512,
])
def test_h2_800_15_ws_frame_len(self, env: H2TestEnv, ws_server, frame_len):
fname = "data-1m"
flen = 1000*1000
r, infos, frames = ws_run(env, path=f'/ws/file/{fname}/{frame_len}', wait_close=0.5)
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) > 0
total_len = sum([f.data_len for f in frames if f.opcode == WsFrame.BINARY])
assert total_len == flen, f'{frames}'
# CONNECT to path with 1MB file and trigger delays between BINARY frame writes
@pytest.mark.parametrize("frame_delay", [
1,
10,
50,
100,
])
def test_h2_800_16_ws_frame_delay(self, env: H2TestEnv, ws_server, frame_delay):
fname = "data-1m"
flen = 1000*1000
# adjust frame_len to allow for 1 second overall duration
frame_len = int(flen / (1000 / frame_delay))
r, infos, frames = ws_run(env, path=f'/ws/file/{fname}/{frame_len}/{frame_delay}',
wait_close=1.5)
assert r.exit_code == 0, f'{r}'
assert infos == ['[1] :status: 200', '[1] EOF'], f'{r}'
assert len(frames) > 0
total_len = sum([f.data_len for f in frames if f.opcode == WsFrame.BINARY])
assert total_len == flen, f'{frames}\n{r}'

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
import argparse
import asyncio
import logging
import os
import sys
import time
import websockets.server as ws_server
from websockets.exceptions import ConnectionClosedError
log = logging.getLogger(__name__)
logging.basicConfig(
format="[%(asctime)s] %(message)s",
level=logging.DEBUG,
)
async def echo(websocket):
try:
async for message in websocket:
try:
log.info(f'got request {message}')
except Exception as e:
log.error(f'error {e} getting path from {message}')
await websocket.send(message)
except ConnectionClosedError:
pass
async def on_async_conn(conn):
rpath = str(conn.path)
pcomps = rpath[1:].split('/')
if len(pcomps) == 0:
pcomps = ['echo'] # default handler
log.info(f'connection for {pcomps}')
if pcomps[0] == 'echo':
log.info(f'/echo endpoint')
for message in await conn.recv():
await conn.send(message)
elif pcomps[0] == 'text':
await conn.send('hello!')
elif pcomps[0] == 'file':
if len(pcomps) < 2:
conn.close(code=4999, reason='unknown file')
return
fpath = os.path.join('../', pcomps[1])
if not os.path.exists(fpath):
conn.close(code=4999, reason='file not found')
return
bufsize = 0
if len(pcomps) > 2:
bufsize = int(pcomps[2])
if bufsize <= 0:
bufsize = 16*1024
delay_ms = 0
if len(pcomps) > 3:
delay_ms = int(pcomps[3])
with open(fpath, 'r+b') as fd:
while True:
buf = fd.read(bufsize)
if buf is None or len(buf) == 0:
break
await conn.send(buf)
if delay_ms > 0:
time.sleep(delay_ms/1000)
else:
log.info(f'unknown endpoint: {rpath}')
await conn.close(code=4999, reason='path unknown')
await conn.close(code=1000, reason='')
async def run_server(port):
log.info(f'starting server on port {port}')
async with ws_server.serve(ws_handler=on_async_conn,
host="localhost", port=port):
await asyncio.Future()
async def main():
parser = argparse.ArgumentParser(prog='scorecard',
description="Run a websocket echo server.")
parser.add_argument("--port", type=int,
default=0, help="port to listen on")
args = parser.parse_args()
if args.port == 0:
sys.stderr.write('need --port\n')
sys.exit(1)
logging.basicConfig(
format="%(asctime)s %(message)s",
level=logging.DEBUG,
)
await run_server(args.port)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -26,6 +26,7 @@ http_port = 5002
https_port = 5001
proxy_port = 5003
http_port2 = 5004
ws_port = 5100
http_tld = tests.httpd.apache.org
test_dir = @abs_srcdir@
test_src_dir = @abs_srcdir@

View File

@@ -250,8 +250,10 @@ class HttpdTestEnv:
self._http_port2 = int(self.config.get('test', 'http_port2'))
self._https_port = int(self.config.get('test', 'https_port'))
self._proxy_port = int(self.config.get('test', 'proxy_port'))
self._ws_port = int(self.config.get('test', 'ws_port'))
self._http_tld = self.config.get('test', 'http_tld')
self._test_dir = self.config.get('test', 'test_dir')
self._clients_dir = os.path.join(os.path.dirname(self._test_dir), 'clients')
self._gen_dir = self.config.get('test', 'gen_dir')
self._server_dir = os.path.join(self._gen_dir, 'apache')
self._server_conf_dir = os.path.join(self._server_dir, "conf")
@@ -366,6 +368,10 @@ class HttpdTestEnv:
def proxy_port(self) -> int:
return self._proxy_port
@property
def ws_port(self) -> int:
return self._ws_port
@property
def http_tld(self) -> str:
return self._http_tld
@@ -390,6 +396,10 @@ class HttpdTestEnv:
def test_dir(self) -> str:
return self._test_dir
@property
def clients_dir(self) -> str:
return self._clients_dir
@property
def server_dir(self) -> str:
return self._server_dir
@@ -519,12 +529,14 @@ class HttpdTestEnv:
if not os.path.exists(path):
return os.makedirs(path)
def run(self, args, stdout_list=False, intext=None, debug_log=True):
def run(self, args, stdout_list=False, intext=None, inbytes=None, debug_log=True):
if debug_log:
log.debug(f"run: {args}")
start = datetime.now()
if intext is not None:
inbytes = intext.encode()
p = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
input=intext.encode() if intext else None)
input=inbytes)
stdout_as_list = None
if stdout_list:
try:

137
test/pyhttpd/ws_util.py Normal file
View File

@@ -0,0 +1,137 @@
import logging
import struct
log = logging.getLogger(__name__)
class WsFrame:
CONT = 0
TEXT = 1
BINARY = 2
RSVD3 = 3
RSVD4 = 4
RSVD5 = 5
RSVD6 = 6
RSVD7 = 7
CLOSE = 8
PING = 9
PONG = 10
RSVD11 = 11
RSVD12 = 12
RSVD13 = 13
RSVD14 = 14
RSVD15 = 15
OP_NAMES = [
"CONT",
"TEXT",
"BINARY",
"RSVD3",
"RSVD4",
"RSVD5",
"RSVD6",
"RSVD7",
"CLOSE",
"PING",
"PONG",
"RSVD11",
"RSVD12",
"RSVD13",
"RSVD14",
"RSVD15",
]
def __init__(self, opcode: int, fin: bool, mask: bytes, data: bytes):
self.opcode = opcode
self.fin = fin
self.mask = mask
self.data = data
self.length = len(data)
def __repr__(self):
return f'WsFrame[{self.OP_NAMES[self.opcode]} fin={self.fin}, mask={self.mask}, len={len(self.data)}]'
@property
def data_len(self) -> int:
return len(self.data) if self.data else 0
def to_network(self) -> bytes:
nd = bytearray()
h1 = self.opcode
if self.fin:
h1 |= 0x80
nd.extend(struct.pack("!B", h1))
mask_bit = 0x80 if self.mask is not None else 0x0
h2 = self.data_len
if h2 > 65535:
nd.extend(struct.pack("!BQ", 127|mask_bit, h2))
elif h2 > 126:
nd.extend(struct.pack("!BH", 126|mask_bit, h2))
else:
nd.extend(struct.pack("!B", h2|mask_bit))
if self.mask is not None:
nd.extend(self.mask)
if self.data is not None:
nd.extend(self.data)
return nd
@classmethod
def client_ping(cls, data: bytes, mask: bytes = None) -> 'WsFrame':
if mask is None:
mask = bytes.fromhex('00 00 00 00')
return WsFrame(opcode=WsFrame.PING, fin=True, mask=mask, data=data)
@classmethod
def client_close(cls, code: int, reason: str = None,
mask: bytes = None) -> 'WsFrame':
data = bytearray(struct.pack("!H", code))
if reason is not None:
data.extend(reason.encode())
if mask is None:
mask = bytes.fromhex('00 00 00 00')
return WsFrame(opcode=WsFrame.CLOSE, fin=True, mask=mask, data=data)
class WsFrameReader:
def __init__(self, data: bytes):
self.data = data
def _read(self, n: int):
if len(self.data) < n:
raise EOFError(f'have {len(self.data)} bytes left, but {n} requested')
elif n == 0:
return b''
chunk = self.data[:n]
del self.data[:n]
return chunk
def next_frame(self):
data = self._read(2)
h1, h2 = struct.unpack("!BB", data)
log.debug(f'parsed h1={h1} h2={h2} from {data}')
fin = True if h1 & 0x80 else False
opcode = h1 & 0xf
has_mask = True if h2 & 0x80 else False
mask = None
dlen = h2 & 0x7f
if dlen == 126:
(dlen,) = struct.unpack("!H", self._read(2))
elif dlen == 127:
(dlen,) = struct.unpack("!Q", self._read(8))
if has_mask:
mask = self._read(4)
return WsFrame(opcode=opcode, fin=fin, mask=mask, data=self._read(dlen))
def eof(self):
return len(self.data) == 0
@classmethod
def parse(cls, data: bytes):
frames = []
reader = WsFrameReader(data=data)
while not reader.eof():
frames.append(reader.next_frame())
return frames

View File

@@ -221,6 +221,8 @@ if ! test -v SKIP_TESTING; then
fi
if test -v TEST_H2 -a $RV -eq 0; then
# Build the test clients
(cd test/clients && make)
# Run HTTP/2 tests.
MPM=event py.test-3 test/modules/http2
RV=$?