Skip to content

Commit

Permalink
*) mod_http2: added support for bootstrapping WebSockets via HTTP/2, as
Browse files Browse the repository at this point in the history
     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
  • Loading branch information
icing committed Jun 20, 2023
1 parent 93b072e commit 3ed9d65
Show file tree
Hide file tree
Showing 41 changed files with 2,530 additions and 95 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions changes-entries/h2_websockets.txt
Original file line number Diff line number Diff line change
@@ -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]
1 change: 1 addition & 0 deletions configure.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions docs/manual/mod/mod_http2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
3 changes: 2 additions & 1 deletion include/ap_mmn.h
Original file line number Diff line number Diff line change
Expand Up @@ -718,14 +718,15 @@
* 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" */

#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
Expand Down
25 changes: 25 additions & 0 deletions include/http_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions modules/http2/config2.m4
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions modules/http2/h2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions modules/http2/h2_bucket_beam.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)? */
Expand Down Expand Up @@ -747,6 +748,9 @@ apr_status_t h2_beam_receive(h2_bucket_beam *beam,

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;
}
Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
}
18 changes: 18 additions & 0 deletions modules/http2/h2_bucket_beam.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 */
2 changes: 1 addition & 1 deletion modules/http2/h2_c1_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
61 changes: 57 additions & 4 deletions modules/http2/h2_c2.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -216,7 +218,17 @@ static apr_status_t h2_c2_filter_in(ap_filter_t* f,
APR_BRIGADE_INSERT_TAIL(fctx->bb, b);
}
}


/* 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)) {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 3ed9d65

Please sign in to comment.