Skip to content

Commit

Permalink
*) mod_http2: new directive H2ProxyRequests on|off to enable handling
Browse files Browse the repository at this point in the history
     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.



git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1910656 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
icing committed Jun 28, 2023
1 parent faefe15 commit 77ae6da
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 2 deletions.
6 changes: 6 additions & 0 deletions changes-entries/h2_proxyrequests.txt
Original file line number Diff line number Diff line change
@@ -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]
24 changes: 24 additions & 0 deletions docs/manual/mod/mod_http2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1120,4 +1120,28 @@ H2EarlyHint Link "</my.css>;rel=preload;as=style"
</usage>
</directivesynopsis>

<directivesynopsis>
<name>H2ProxyRequests</name>
<description>En-/Disable forward proxy requests via HTTP/2</description>
<syntax>H2ProxyRequests on|off</syntax>
<default>H2ProxyRequests 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>H2ProxyRequests</directive> to enable or disable
handling of HTTP/2 requests in a forward proxy configuration.
</p><p>
Similar to <directive module="proxy">ProxyRequests</directive>, this
triggers the needed treatment of requests when HTTP/2 is enabled
in a forward proxy configuration. Both directive should be enabled.
</p><p>
</p>
</usage>
</directivesynopsis>

</modulesynopsis>
24 changes: 24 additions & 0 deletions modules/http2/h2_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 */
};

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions modules/http2/h2_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
35 changes: 33 additions & 2 deletions modules/http2/h2_request.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 : "");
Expand Down Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions test/modules/http2/test_503_proxy_fwd.py
Original file line number Diff line number Diff line change
@@ -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}'

0 comments on commit 77ae6da

Please sign in to comment.