diff --git a/changes-entries/h2_proxyrequests.txt b/changes-entries/h2_proxyrequests.txt new file mode 100644 index 00000000000..199d2c93b03 --- /dev/null +++ b/changes-entries/h2_proxyrequests.txt @@ -0,0 +1,6 @@ + *) mod_http2: new directive `H2ProxyRequests on|off` to enable handling + of HTTP/2 requests in a forward proxy configuration. + General forward proxying is enabled via `ProxyRequests`. If the + HTTP/2 protocol is also enabled for such a server/host, this new + directive is needed in addition. + [Stefan Eissing] diff --git a/docs/manual/mod/mod_http2.xml b/docs/manual/mod/mod_http2.xml index 0b9590f05d2..e769a759146 100644 --- a/docs/manual/mod/mod_http2.xml +++ b/docs/manual/mod/mod_http2.xml @@ -1120,4 +1120,28 @@ H2EarlyHint Link "</my.css>;rel=preload;as=style" + + H2ProxyRequests + En-/Disable forward proxy requests via HTTP/2 + H2ProxyRequests on|off + H2ProxyRequests off + + server config + virtual host + + Available in version 2.5.1 and later. + + +

+ Use H2ProxyRequests to enable or disable + handling of HTTP/2 requests in a forward proxy configuration. +

+ Similar to ProxyRequests, this + triggers the needed treatment of requests when HTTP/2 is enabled + in a forward proxy configuration. Both directive should be enabled. +

+

+
+
+ diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c index 7f9f18078c0..e87b1c82a4a 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 proxy_requests; /* act as forward proxy */ int h2_websockets; /* if mod_h2 negotiating WebSockets */ } h2_config; @@ -116,6 +117,7 @@ static h2_config defconf = { 1, /* stream output buffered */ -1, /* beam timeout */ 0, /* max DATA frame len, 0 == no extra limit */ + 0, /* forward proxy */ 0, /* WebSockets negotiation, enabled */ }; @@ -163,6 +165,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->proxy_requests = DEF_VAL; conf->h2_websockets = DEF_VAL; return conf; } @@ -213,6 +216,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->proxy_requests = H2_CONFIG_GET(add, base, proxy_requests); n->h2_websockets = H2_CONFIG_GET(add, base, h2_websockets); return n; } @@ -305,6 +309,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_PROXY_REQUESTS: + return H2_CONFIG_GET(conf, &defconf, proxy_requests); case H2_CONF_WEBSOCKETS: return H2_CONFIG_GET(conf, &defconf, h2_websockets); default: @@ -369,6 +375,8 @@ 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_PROXY_REQUESTS: + H2_CONFIG_SET(conf, proxy_requests, val); case H2_CONF_WEBSOCKETS: H2_CONFIG_SET(conf, h2_websockets, val); break; @@ -981,6 +989,20 @@ static const char *h2_conf_set_stream_timeout(cmd_parms *cmd, return NULL; } +static const char *h2_conf_set_proxy_requests(cmd_parms *cmd, + void *dirconf, const char *value) +{ + if (!strcasecmp(value, "On")) { + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_PROXY_REQUESTS, 1); + return NULL; + } + else if (!strcasecmp(value, "Off")) { + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_PROXY_REQUESTS, 0); + return NULL; + } + return "value must be On or Off"; +} + void h2_get_workers_config(server_rec *s, int *pminw, int *pmaxw, apr_time_t *pidle_limit) { @@ -1050,6 +1072,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("H2ProxyRequests", h2_conf_set_proxy_requests, NULL, + OR_FILEINFO, "Enables forward proxy requests via HTTP/2"), 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 1c8f86509ff..15242db522b 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_PROXY_REQUESTS, H2_CONF_WEBSOCKETS, } h2_config_var_t; diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c index 4e363ab0aa3..135a465cfd2 100644 --- a/modules/http2/h2_request.c +++ b/modules/http2/h2_request.c @@ -38,6 +38,7 @@ #include "h2_private.h" #include "h2_config.h" +#include "h2_conn_ctx.h" #include "h2_push.h" #include "h2_request.h" #include "h2_util.h" @@ -292,8 +293,14 @@ apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r) if (!ap_cstr_casecmp("CONNECT", req->method)) { uri = req->authority; } - else if (req->scheme && (ap_cstr_casecmp(req->scheme, "http") && - ap_cstr_casecmp(req->scheme, "https"))) { + else if (h2_config_cgeti(c, H2_CONF_PROXY_REQUESTS)) { + /* Forward proxying: always absolute uris */ + uri = apr_psprintf(r->pool, "%s://%s%s", + req->scheme, req->authority, + req->path ? req->path : ""); + } + else if (req->scheme && ap_cstr_casecmp(req->scheme, "http") + && ap_cstr_casecmp(req->scheme, "https")) { /* Client sent a non-http ':scheme', use an absolute URI */ uri = apr_psprintf(r->pool, "%s://%s%s", req->scheme, req->authority, req->path ? req->path : ""); @@ -397,6 +404,30 @@ request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c, goto die; } } + else if (req->protocol) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10460) + "':protocol: %s' header present in %s request", + req->protocol, req->method); + access_status = HTTP_BAD_REQUEST; + goto die; + } + else if (h2_config_cgeti(c, H2_CONF_PROXY_REQUESTS)) { + if (!req->scheme) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO() + "H2ProxyRequests on, but request misses :scheme"); + access_status = HTTP_BAD_REQUEST; + goto die; + } + if (!req->authority) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO() + "H2ProxyRequests on, but request misses :authority"); + access_status = HTTP_BAD_REQUEST; + goto die; + } + r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0", + req->method, req->scheme, req->authority, + req->path ? req->path : ""); + } else if (req->scheme && ap_cstr_casecmp(req->scheme, "http") && ap_cstr_casecmp(req->scheme, "https")) { /* Client sent a ':scheme' pseudo header for something else diff --git a/test/modules/http2/test_503_proxy_fwd.py b/test/modules/http2/test_503_proxy_fwd.py new file mode 100644 index 00000000000..478a52d08c9 --- /dev/null +++ b/test/modules/http2/test_503_proxy_fwd.py @@ -0,0 +1,79 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestProxyFwd: + + @classmethod + def config_fwd_proxy(cls, env, h2_enabled=False): + conf = H2Conf(env, extras={ + 'base': [ + f'Listen {env.proxy_port}', + 'Protocols h2c http/1.1', + 'LogLevel proxy_http2:trace2 proxy:trace2', + ], + }) + conf.add_vhost_cgi(proxy_self=False, h2proxy_self=False) + conf.start_vhost(domains=[f"test1.{env.http_tld}"], + port=env.proxy_port, with_ssl=True) + conf.add([ + 'Protocols h2c http/1.1', + 'ProxyRequests on', + f'H2ProxyRequests {"on" if h2_enabled else "off"}', + ]) + conf.end_vhost() + conf.install() + assert env.apache_restart() == 0 + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(cls, env): + cls.config_fwd_proxy(env) + + # test the HTTP/1.1 setup working + def test_h2_503_01_proxy_fwd_h1(self, env): + url = f'http://localhost:{env.http_port}/hello.py' + proxy_host = f'test1.{env.http_tld}' + options = [ + '--proxy', f'https://{proxy_host}:{env.proxy_port}', + '--resolve', f'{proxy_host}:{env.proxy_port}:127.0.0.1', + '--proxy-cacert', f'{env.get_ca_pem_file(proxy_host)}', + ] + r = env.curl_get(url, 5, options=options) + assert r.exit_code == 0, f'{r}' + assert r.response['status'] == 200 + assert r.json['port'] == f'{env.http_port}' + + def test_h2_503_02_fwd_proxy_h2_off(self, env): + if not env.curl_is_at_least('8.1.0'): + pytest.skip(f'need at least curl v8.1.0 for this') + url = f'http://localhost:{env.http_port}/hello.py' + proxy_host = f'test1.{env.http_tld}' + options = [ + '--proxy-http2', '-v', + '--proxy', f'https://{proxy_host}:{env.proxy_port}', + '--resolve', f'{proxy_host}:{env.proxy_port}:127.0.0.1', + '--proxy-cacert', f'{env.get_ca_pem_file(proxy_host)}', + ] + r = env.curl_get(url, 5, options=options) + assert r.exit_code == 0, f'{r}' + assert r.response['status'] == 404 + + # test the HTTP/2 setup working + def test_h2_503_03_proxy_fwd_h2_on(self, env): + if not env.curl_is_at_least('8.1.0'): + pytest.skip(f'need at least curl v8.1.0 for this') + self.config_fwd_proxy(env, h2_enabled=True) + url = f'http://localhost:{env.http_port}/hello.py' + proxy_host = f'test1.{env.http_tld}' + options = [ + '--proxy-http2', '-v', + '--proxy', f'https://{proxy_host}:{env.proxy_port}', + '--resolve', f'{proxy_host}:{env.proxy_port}:127.0.0.1', + '--proxy-cacert', f'{env.get_ca_pem_file(proxy_host)}', + ] + r = env.curl_get(url, 5, options=options) + assert r.exit_code == 0, f'{r}' + assert r.response['status'] == 200 + assert r.json['port'] == f'{env.http_port}'