summaryrefslogtreecommitdiffstats
path: root/modules/http2
diff options
context:
space:
mode:
authorStefan Eissing <icing@apache.org>2023-06-20 14:01:09 +0200
committerStefan Eissing <icing@apache.org>2023-06-20 14:01:09 +0200
commit3ed9d65b05184f8859d9d37654c54a0d00ef0a96 (patch)
tree46e9fd75f8a294b0463b238d4b2793499d8ed65f /modules/http2
parentstealing numbers (diff)
downloadapache2-3ed9d65b05184f8859d9d37654c54a0d00ef0a96.tar.xz
apache2-3ed9d65b05184f8859d9d37654c54a0d00ef0a96.zip
*) 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
Diffstat (limited to 'modules/http2')
-rw-r--r--modules/http2/config2.m41
-rw-r--r--modules/http2/h2.h3
-rw-r--r--modules/http2/h2_bucket_beam.c35
-rw-r--r--modules/http2/h2_bucket_beam.h18
-rw-r--r--modules/http2/h2_c1_io.c2
-rw-r--r--modules/http2/h2_c2.c61
-rw-r--r--modules/http2/h2_c2_filter.c32
-rw-r--r--modules/http2/h2_config.c29
-rw-r--r--modules/http2/h2_config.h1
-rw-r--r--modules/http2/h2_conn_ctx.h3
-rw-r--r--modules/http2/h2_mplx.c28
-rw-r--r--modules/http2/h2_proxy_util.c6
-rw-r--r--modules/http2/h2_push.c2
-rw-r--r--modules/http2/h2_request.c92
-rw-r--r--modules/http2/h2_session.c14
-rw-r--r--modules/http2/h2_stream.c25
-rw-r--r--modules/http2/h2_util.c19
-rw-r--r--modules/http2/h2_util.h2
-rw-r--r--modules/http2/h2_version.h4
-rw-r--r--modules/http2/h2_ws.c326
-rw-r--r--modules/http2/h2_ws.h35
-rw-r--r--modules/http2/mod_http2.c2
-rw-r--r--modules/http2/mod_http2.dsp4
23 files changed, 675 insertions, 69 deletions
diff --git a/modules/http2/config2.m4 b/modules/http2/config2.m4
index f89f5baa6c..c4579c4dbb 100644
--- a/modules/http2/config2.m4
+++ b/modules/http2/config2.m4
@@ -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
diff --git a/modules/http2/h2.h b/modules/http2/h2.h
index aa58b61a5a..4babbf81d6 100644
--- a/modules/http2/h2.h
+++ b/modules/http2/h2.h
@@ -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;
diff --git a/modules/http2/h2_bucket_beam.c b/modules/http2/h2_bucket_beam.c
index 72baea3e33..69782541aa 100644
--- a/modules/http2/h2_bucket_beam.c
+++ b/modules/http2/h2_bucket_beam.c
@@ -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;
+}
diff --git a/modules/http2/h2_bucket_beam.h b/modules/http2/h2_bucket_beam.h
index 94e788a03e..c58ce9823c 100644
--- a/modules/http2/h2_bucket_beam.h
+++ b/modules/http2/h2_bucket_beam.h
@@ -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 */
@@ -206,6 +208,16 @@ 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.
* Unregister by passing a NULL on_send_cb.
@@ -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 */
diff --git a/modules/http2/h2_c1_io.c b/modules/http2/h2_c1_io.c
index c09e2deb40..1d5cf4f33d 100644
--- a/modules/http2/h2_c1_io.c
+++ b/modules/http2/h2_c1_io.c
@@ -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.",
diff --git a/modules/http2/h2_c2.c b/modules/http2/h2_c2.c
index 0c2edba55b..537163bf79 100644
--- a/modules/http2/h2_c2.c
+++ b/modules/http2/h2_c2.c
@@ -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,
@@ -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)) {
@@ -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",
diff --git a/modules/http2/h2_c2_filter.c b/modules/http2/h2_c2_filter.c
index 5b1838b773..846344c6b4 100644
--- a/modules/http2/h2_c2_filter.c
+++ b/modules/http2/h2_c2_filter.c
@@ -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,9 +648,11 @@ 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);
- 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);
+ 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.
diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c
index 670833ec12..a8b1973902 100644
--- a/modules/http2/h2_config.c
+++ b/modules/http2/h2_config.c
@@ -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
};
diff --git a/modules/http2/h2_config.h b/modules/http2/h2_config.h
index 5a78371284..1c8f86509f 100644
--- a/modules/http2/h2_config.h
+++ b/modules/http2/h2_config.h
@@ -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;
diff --git a/modules/http2/h2_conn_ctx.h b/modules/http2/h2_conn_ctx.h
index 90dc9f627d..3b44856f95 100644
--- a/modules/http2/h2_conn_ctx.h
+++ b/modules/http2/h2_conn_ctx.h
@@ -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 */
diff --git a/modules/http2/h2_mplx.c b/modules/http2/h2_mplx.c
index ecb2b7c4d4..b3138dc4c1 100644
--- a/modules/http2/h2_mplx.c
+++ b/modules/http2/h2_mplx.c
@@ -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);
diff --git a/modules/http2/h2_proxy_util.c b/modules/http2/h2_proxy_util.c
index dc69ec0636..5e1ebe663d 100644
--- a/modules/http2/h2_proxy_util.c
+++ b/modules/http2/h2_proxy_util.c
@@ -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,
diff --git a/modules/http2/h2_push.c b/modules/http2/h2_push.c
index dd0928b719..e6a10c5ab1 100644
--- a/modules/http2/h2_push.c
+++ b/modules/http2/h2_push.c
@@ -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;
diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c
index 3a6c42b092..b55d5720a0 100644
--- a/modules/http2/h2_request.c
+++ b/modules/http2/h2_request.c
@@ -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,36 +329,40 @@ static void assign_headers(request_rec *r, const h2_request *req,
"set 'Host: %s' from :authority", req->authority);
}
- cl = apr_table_get(req->headers, "Content-Length");
- if (no_body) {
- if (!cl && apr_table_get(req->headers, "Content-Type")) {
- /* If we have a content-type, but already seen eos, no more
- * data will come. Signal a zero content length explicitly.
- */
- apr_table_setn(req->headers, "Content-Length", "0");
+ /* 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")) {
+ /* If we have a content-type, but already seen eos, no more
+ * data will come. Signal a zero content length explicitly.
+ */
+ apr_table_setn(req->headers, "Content-Length", "0");
+ }
}
- }
#if !AP_HAS_RESPONSE_BUCKETS
- else if (!cl) {
- /* there may be a body and we have internal HTTP/1.1 processing.
- * If the Content-Length is unspecified, we MUST simulate
- * chunked Transfer-Encoding.
- *
- * HTTP/2 does not need a Content-Length for framing. Ideally
- * all clients set the EOS flag on the header frame if they
- * do not intent to send a body. However, forwarding proxies
- * might just no know at the time and send an empty DATA
- * frame with EOS much later.
- */
- apr_table_mergen(r->headers_in, "Transfer-Encoding", "chunked");
- }
+ else if (!cl) {
+ /* there may be a body and we have internal HTTP/1.1 processing.
+ * If the Content-Length is unspecified, we MUST simulate
+ * chunked Transfer-Encoding.
+ *
+ * HTTP/2 does not need a Content-Length for framing. Ideally
+ * all clients set the EOS flag on the header frame if they
+ * do not intent to send a body. However, forwarding proxies
+ * might just no know at the time and send an empty DATA
+ * frame with EOS much later.
+ */
+ 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. */
diff --git a/modules/http2/h2_session.c b/modules/http2/h2_session.c
index 603e47ce85..c104cac184 100644
--- a/modules/http2/h2_session.c
+++ b/modules/http2/h2_session.c
@@ -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,7 +1069,12 @@ 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,
"start, INITIAL_WINDOW_SIZE=%ld, MAX_CONCURRENT_STREAMS=%d"),
diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c
index 635caf7cb3..24d0268f38 100644
--- a/modules/http2/h2_stream.c
+++ b/modules/http2/h2_stream.c
@@ -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;
}
}
diff --git a/modules/http2/h2_util.c b/modules/http2/h2_util.c
index 728cee95aa..8e53cebdf9 100644
--- a/modules/http2/h2_util.c
+++ b/modules/http2/h2_util.c
@@ -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)
diff --git a/modules/http2/h2_util.h b/modules/http2/h2_util.h
index d2e6548ba8..dcec73eaf2 100644
--- a/modules/http2/h2_util.h
+++ b/modules/http2/h2_util.h
@@ -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
diff --git a/modules/http2/h2_version.h b/modules/http2/h2_version.h
index 7de3144ec7..59dc06c8b6 100644
--- a/modules/http2/h2_version.h
+++ b/modules/http2/h2_version.h
@@ -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 */
diff --git a/modules/http2/h2_ws.c b/modules/http2/h2_ws.c
new file mode 100644
index 0000000000..e3bdadb32d
--- /dev/null
+++ b/modules/http2/h2_ws.c
@@ -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);
+}
diff --git a/modules/http2/h2_ws.h b/modules/http2/h2_ws.h
new file mode 100644
index 0000000000..4e8967c792
--- /dev/null
+++ b/modules/http2/h2_ws.h
@@ -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__) */
diff --git a/modules/http2/mod_http2.c b/modules/http2/mod_http2.c
index 8a1ee3faa5..1bd34b207d 100644
--- a/modules/http2/mod_http2.c
+++ b/modules/http2/mod_http2.c
@@ -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
*/
diff --git a/modules/http2/mod_http2.dsp b/modules/http2/mod_http2.dsp
index d9ff22203a..977553436d 100644
--- a/modules/http2/mod_http2.dsp
+++ b/modules/http2/mod_http2.dsp
@@ -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