1 /* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
16 #include <nghttp2/nghttp2.h>
19 #include <mod_proxy.h>
20 #include "mod_http2.h"
23 #include "mod_proxy_http2.h"
26 #include "h2_version.h"
27 #include "h2_proxy_session.h"
29 static void register_hook(apr_pool_t *p);
31 AP_DECLARE_MODULE(proxy_http2) = {
32 STANDARD20_MODULE_STUFF,
33 NULL, /* create per-directory config structure */
34 NULL, /* merge per-directory config structures */
35 NULL, /* create per-server config structure */
36 NULL, /* merge per-server config structures */
37 NULL, /* command apr_table_t */
38 register_hook /* register hooks */
41 /* Optional functions from mod_http2 */
42 static int (*is_h2)(conn_rec *c);
43 static apr_status_t (*req_engine_push)(const char *name, request_rec *r,
44 http2_req_engine_init *einit);
45 static apr_status_t (*req_engine_pull)(h2_req_engine *engine,
46 apr_read_type_e block,
47 apr_uint32_t capacity,
49 static void (*req_engine_done)(h2_req_engine *engine, conn_rec *r_conn);
51 typedef struct h2_proxy_ctx {
56 const char *proxy_func;
57 char server_portstr[32];
58 proxy_conn_rec *p_conn;
60 proxy_server_conf *conf;
62 h2_req_engine *engine;
63 const char *engine_id;
64 const char *engine_type;
65 apr_pool_t *engine_pool;
66 apr_uint32_t req_buffer_size;
70 unsigned standalone : 1;
72 unsigned flushall : 1;
74 apr_status_t r_status; /* status of our first request work */
75 h2_proxy_session *session; /* current http2 session against backend */
78 static int h2_proxy_post_config(apr_pool_t *p, apr_pool_t *plog,
79 apr_pool_t *ptemp, server_rec *s)
82 const char *init_key = "mod_proxy_http2_init_counter";
84 apr_status_t status = APR_SUCCESS;
85 (void)plog;(void)ptemp;
87 apr_pool_userdata_get(&data, init_key, s->process->pool);
89 apr_pool_userdata_set((const void *)1, init_key,
90 apr_pool_cleanup_null, s->process->pool);
94 ngh2 = nghttp2_version(0);
95 ap_log_error( APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(03349)
96 "mod_proxy_http2 (v%s, nghttp2 %s), initializing...",
97 MOD_HTTP2_VERSION, ngh2? ngh2->version_str : "unknown");
99 is_h2 = APR_RETRIEVE_OPTIONAL_FN(http2_is_h2);
100 req_engine_push = APR_RETRIEVE_OPTIONAL_FN(http2_req_engine_push);
101 req_engine_pull = APR_RETRIEVE_OPTIONAL_FN(http2_req_engine_pull);
102 req_engine_done = APR_RETRIEVE_OPTIONAL_FN(http2_req_engine_done);
104 /* we need all of them */
105 if (!req_engine_push || !req_engine_pull || !req_engine_done) {
106 req_engine_push = NULL;
107 req_engine_pull = NULL;
108 req_engine_done = NULL;
115 * canonicalize the url into the request, if it is meant for us.
116 * slightly modified copy from mod_http
118 static int proxy_http2_canon(request_rec *r, char *url)
120 char *host, *path, sport[7];
124 const char *http_scheme;
125 apr_port_t port, def_port;
127 /* ap_port_of_scheme() */
128 if (h2_casecmpstrn(url, "h2c:", 4) == 0) {
131 http_scheme = "http";
133 else if (h2_casecmpstrn(url, "h2:", 3) == 0) {
136 http_scheme = "https";
141 port = def_port = ap_proxy_port_of_scheme(http_scheme);
143 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
144 "HTTP2: canonicalising URL %s", url);
146 /* do syntatic check.
147 * We break the URL into host, port, path, search
149 err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
151 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(03350)
152 "error parsing URL %s: %s", url, err);
153 return HTTP_BAD_REQUEST;
157 * now parse path/search args, according to rfc1738:
160 * In a reverse proxy, our URL has been processed, so canonicalise
161 * unless proxy-nocanon is set to say it's raw
162 * In a forward proxy, we have and MUST NOT MANGLE the original.
164 switch (r->proxyreq) {
165 default: /* wtf are we doing here? */
166 case PROXYREQ_REVERSE:
167 if (apr_table_get(r->notes, "proxy-nocanon")) {
168 path = url; /* this is the raw path */
171 path = ap_proxy_canonenc(r->pool, url, strlen(url),
172 enc_path, 0, r->proxyreq);
182 return HTTP_BAD_REQUEST;
185 if (port != def_port) {
186 apr_snprintf(sport, sizeof(sport), ":%d", port);
192 if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
193 host = apr_pstrcat(r->pool, "[", host, "]", NULL);
195 r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "://", host, sport,
196 "/", path, (search) ? "?" : "", (search) ? search : "", NULL);
200 static void out_consumed(void *baton, conn_rec *c, apr_off_t bytes)
202 h2_proxy_ctx *ctx = baton;
205 h2_proxy_session_update_window(ctx->session, c, bytes);
209 static apr_status_t proxy_engine_init(h2_req_engine *engine,
213 apr_uint32_t req_buffer_size,
215 http2_output_consumed **pconsumed,
218 h2_proxy_ctx *ctx = ap_get_module_config(r->connection->conn_config,
219 &proxy_http2_module);
221 conn_rec *c = ctx->owner;
224 /* we need another lifetime for this. If we do not host
225 * an engine, the context lives in r->pool. Since we expect
226 * to server more than r, we need to live longer */
227 nctx = apr_pcalloc(pool, sizeof(*nctx));
231 memcpy(nctx, ctx, sizeof(*nctx));
234 ctx->engine = engine;
236 ctx->engine_type = type;
237 ctx->engine_pool = pool;
238 ctx->req_buffer_size = req_buffer_size;
241 ap_set_module_config(c->conn_config, &proxy_http2_module, ctx);
243 *pconsumed = out_consumed;
247 ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(03368)
248 "h2_proxy_session, engine init, no ctx found");
252 static apr_status_t add_request(h2_proxy_session *session, request_rec *r)
254 h2_proxy_ctx *ctx = session->user_data;
258 url = apr_table_get(r->notes, H2_PROXY_REQ_URL_NOTE);
259 apr_table_setn(r->notes, "proxy-source-port", apr_psprintf(r->pool, "%hu",
260 ctx->p_conn->connection->local_addr->port));
261 status = h2_proxy_session_submit(session, url, r);
263 ap_log_cerror(APLOG_MARK, APLOG_ERR, status, r->connection, APLOGNO(03351)
264 "pass request body failed to %pI (%s) from %s (%s)",
265 ctx->p_conn->addr, ctx->p_conn->hostname ?
266 ctx->p_conn->hostname: "", session->c->client_ip,
267 session->c->remote_host ? session->c->remote_host: "");
272 static void request_done(h2_proxy_session *session, request_rec *r,
273 int complete, int touched)
275 h2_proxy_ctx *ctx = session->user_data;
276 const char *task_id = apr_table_get(r->connection->notes, H2_TASK_ID_NOTE);
278 if (!complete && !touched) {
279 /* untouched request, need rescheduling */
280 if (req_engine_push && is_h2 && is_h2(ctx->owner)) {
281 if (req_engine_push(ctx->engine_type, r, NULL) == APR_SUCCESS) {
283 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection,
285 "h2_proxy_session(%s): rescheduled request %s",
286 ctx->engine_id, task_id);
292 if (r == ctx->rbase && complete) {
293 ctx->r_status = APR_SUCCESS;
297 if (req_engine_done && ctx->engine) {
298 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection,
300 "h2_proxy_session(%s): finished request %s",
301 ctx->engine_id, task_id);
302 req_engine_done(ctx->engine, r->connection);
306 if (req_engine_done && ctx->engine) {
307 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection,
309 "h2_proxy_session(%s): failed request %s",
310 ctx->engine_id, task_id);
311 req_engine_done(ctx->engine, r->connection);
316 static apr_status_t next_request(h2_proxy_ctx *ctx, int before_leave)
321 else if (req_engine_pull && ctx->engine) {
323 status = req_engine_pull(ctx->engine, before_leave?
324 APR_BLOCK_READ: APR_NONBLOCK_READ,
325 ctx->capacity, &ctx->next);
326 ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, ctx->owner,
327 "h2_proxy_engine(%s): pulled request %s",
329 (ctx->next? ctx->next->the_request : "NULL"));
330 return APR_STATUS_IS_EAGAIN(status)? APR_SUCCESS : status;
335 static apr_status_t proxy_engine_run(h2_proxy_ctx *ctx) {
336 apr_status_t status = OK;
338 /* Step Four: Send the Request in a new HTTP/2 stream and
339 * loop until we got the response or encounter errors.
341 ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, ctx->owner,
342 "eng(%s): setup session", ctx->engine_id);
343 ctx->session = h2_proxy_session_setup(ctx->engine_id, ctx->p_conn, ctx->conf,
344 30, h2_log2(ctx->req_buffer_size),
347 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner,
348 APLOGNO(03372) "session unavailable");
349 return HTTP_SERVICE_UNAVAILABLE;
352 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, APLOGNO(03373)
353 "eng(%s): run session %s", ctx->engine_id, ctx->session->id);
354 ctx->session->user_data = ctx;
358 add_request(ctx->session, ctx->next);
362 status = h2_proxy_session_process(ctx->session);
364 if (status == APR_SUCCESS) {
366 /* ongoing processing, call again */
367 if (ctx->session->remote_max_concurrent > 0
368 && ctx->session->remote_max_concurrent != ctx->capacity) {
369 ctx->capacity = ctx->session->remote_max_concurrent;
371 s2 = next_request(ctx, 0);
372 if (s2 == APR_ECONNABORTED) {
373 /* master connection gone */
374 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, s2, ctx->owner,
375 APLOGNO(03374) "eng(%s): pull request",
380 if (!ctx->next && h2_ihash_empty(ctx->session->streams)) {
385 /* end of processing, maybe error */
386 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner,
387 APLOGNO(03375) "eng(%s): end of session run",
390 * Any open stream of that session needs to
391 * a) be reopened on the new session iff safe to do so
392 * b) reported as done (failed) otherwise
394 h2_proxy_session_cleanup(ctx->session, request_done);
399 ctx->session->user_data = NULL;
405 static h2_proxy_ctx *push_request_somewhere(h2_proxy_ctx *ctx)
407 conn_rec *c = ctx->owner;
408 const char *engine_type, *hostname;
410 hostname = (ctx->p_conn->ssl_hostname?
411 ctx->p_conn->ssl_hostname : ctx->p_conn->hostname);
412 engine_type = apr_psprintf(ctx->pool, "proxy_http2 %s%s", hostname,
413 ctx->server_portstr);
415 if (c->master && req_engine_push && ctx->next && is_h2 && is_h2(c)) {
416 /* If we are have req_engine capabilities, push the handling of this
417 * request (e.g. slave connection) to a proxy_http2 engine which
418 * uses the same backend. We may be called to create an engine
420 if (req_engine_push(engine_type, ctx->next, proxy_engine_init)
422 /* to renew the lifetime, we might have set a new ctx */
423 ctx = ap_get_module_config(c->conn_config, &proxy_http2_module);
424 if (ctx->engine == NULL) {
425 /* Another engine instance has taken over processing of this
427 ctx->r_status = SUSPENDED;
435 /* No engine was available or has been initialized, handle this
436 * request just by ourself. */
437 ctx->engine_id = apr_psprintf(ctx->pool, "eng-proxy-%ld", c->id);
438 ctx->engine_type = engine_type;
439 ctx->engine_pool = ctx->pool;
440 ctx->req_buffer_size = (32*1024);
442 ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
443 "h2_proxy_http2(%ld): setup standalone engine for type %s",
447 ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
448 "H2: hosting engine %s", ctx->engine_id);
453 static int proxy_http2_handler(request_rec *r,
454 proxy_worker *worker,
455 proxy_server_conf *conf,
457 const char *proxyname,
458 apr_port_t proxyport)
460 const char *proxy_func;
461 char *locurl = url, *u;
469 /* find the scheme */
470 if ((url[0] != 'h' && url[0] != 'H') || url[1] != '2') {
473 u = strchr(url, ':');
474 if (u == NULL || u[1] != '/' || u[2] != '/' || u[3] == '\0') {
484 if (url[2] != 'c' && url[2] != 'C') {
492 ctx = apr_pcalloc(r->pool, sizeof(*ctx));
493 ctx->owner = r->connection;
496 ctx->server = r->server;
497 ctx->proxy_func = proxy_func;
498 ctx->is_ssl = is_ssl;
499 ctx->worker = worker;
501 ctx->flushall = apr_table_get(r->subprocess_env, "proxy-flushall")? 1 : 0;
502 ctx->r_status = HTTP_SERVICE_UNAVAILABLE;
505 ap_set_module_config(ctx->owner->conn_config, &proxy_http2_module, ctx);
507 /* scheme says, this is for us. */
508 apr_table_setn(ctx->rbase->notes, H2_PROXY_REQ_URL_NOTE, url);
509 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, ctx->rbase,
510 "H2: serving URL %s", url);
513 /* Get a proxy_conn_rec from the worker, might be a new one, might
514 * be one still open from another request, or it might fail if the
515 * worker is stopped or in error. */
516 if ((status = ap_proxy_acquire_connection(ctx->proxy_func, &ctx->p_conn,
517 ctx->worker, ctx->server)) != OK) {
521 ctx->p_conn->is_ssl = ctx->is_ssl;
523 /* If there is still some data on an existing ssl connection, now
524 * would be a good timne to get rid of it. */
525 ap_proxy_ssl_connection_cleanup(ctx->p_conn, ctx->rbase);
528 /* Step One: Determine the URL to connect to (might be a proxy),
529 * initialize the backend accordingly and determine the server
530 * port string we can expect in responses. */
531 if ((status = ap_proxy_determine_connection(ctx->pool, ctx->rbase, conf, worker,
532 ctx->p_conn, &uri, &locurl,
533 proxyname, proxyport,
535 sizeof(ctx->server_portstr))) != OK) {
539 /* If we are not already hosting an engine, try to push the request
540 * to an already existing engine or host a new engine here. */
542 ctx = push_request_somewhere(ctx);
543 if (ctx->r_status == SUSPENDED) {
544 /* request was pushed to another engine */
549 /* Step Two: Make the Connection (or check that an already existing
550 * socket is still usable). On success, we have a socket connected to
551 * backend->hostname. */
552 if (ap_proxy_connect_backend(ctx->proxy_func, ctx->p_conn, ctx->worker,
554 ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, ctx->owner, APLOGNO(03352)
555 "H2: failed to make connection to backend: %s",
556 ctx->p_conn->hostname);
560 /* Step Three: Create conn_rec for the socket we have open now. */
561 if (!ctx->p_conn->connection) {
562 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, APLOGNO(03353)
563 "setup new connection: is_ssl=%d %s %s %s",
564 ctx->p_conn->is_ssl, ctx->p_conn->ssl_hostname,
565 locurl, ctx->p_conn->hostname);
566 status = ap_proxy_connection_create_ex(ctx->proxy_func,
567 ctx->p_conn, ctx->rbase);
573 * On SSL connections set a note on the connection what CN is
574 * requested, such that mod_ssl can check if it is requested to do
577 if (ctx->p_conn->ssl_hostname) {
578 apr_table_setn(ctx->p_conn->connection->notes,
579 "proxy-request-hostname", ctx->p_conn->ssl_hostname);
583 apr_table_setn(ctx->p_conn->connection->notes,
584 "proxy-request-alpn-protos", "h2");
589 status = proxy_engine_run(ctx);
590 if (status == APR_SUCCESS) {
591 /* session and connection still ok */
592 if (next_request(ctx, 1) == APR_SUCCESS) {
593 /* more requests, run again */
594 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, APLOGNO(03376)
595 "run_session, again");
603 if (!reconnected && ctx->engine && next_request(ctx, 1) == APR_SUCCESS) {
604 /* Still more to do, tear down old conn and start over */
606 ctx->p_conn->close = 1;
607 proxy_run_detach_backend(r, ctx->p_conn);
608 ap_proxy_release_connection(ctx->proxy_func, ctx->p_conn, ctx->server);
611 reconnected = 1; /* we do this only once, then fail */
616 if (status != APR_SUCCESS) {
617 /* close socket when errors happened or session shut down (EOF) */
618 ctx->p_conn->close = 1;
620 proxy_run_detach_backend(ctx->rbase, ctx->p_conn);
621 ap_proxy_release_connection(ctx->proxy_func, ctx->p_conn, ctx->server);
625 ap_set_module_config(ctx->owner->conn_config, &proxy_http2_module, NULL);
626 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner,
627 APLOGNO(03377) "leaving handler");
628 return ctx->r_status;
631 static void register_hook(apr_pool_t *p)
633 ap_hook_post_config(h2_proxy_post_config, NULL, NULL, APR_HOOK_MIDDLE);
635 proxy_hook_scheme_handler(proxy_http2_handler, NULL, NULL, APR_HOOK_FIRST);
636 proxy_hook_canon_handler(proxy_http2_canon, NULL, NULL, APR_HOOK_FIRST);