From: Jim Jagielski Date: Thu, 12 May 2016 21:31:44 +0000 (+0000) Subject: Merge r1729208, r1735668, r1735668, r1735931, r1735935, r1735942 from trunk: X-Git-Tag: 2.4.21~176 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=e6c05cd9672218c0e4f26f7ece61339c54f2c365;p=apache Merge r1729208, r1735668, r1735668, r1735931, r1735935, r1735942 from trunk: let proxy handler forward ALPN protocol strings for ssl proxy connections Remove leftover comment Remove leftover comment APLOGNO update for mod_proxy_http2 fix APLOGNO at wrong place, me stupid h2_proxy_session: fill in missing APLOGNO()s. Submitted by: icing, jailletc36, jailletc36, icing, icing, ylavic Reviewed/backported by: jim git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1743577 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/docs/manual/mod/mod_proxy_http2.xml b/docs/manual/mod/mod_proxy_http2.xml new file mode 100644 index 0000000000..bce46b9228 --- /dev/null +++ b/docs/manual/mod/mod_proxy_http2.xml @@ -0,0 +1,83 @@ + + + + + + + + + +mod_proxy_http2 +HTTP/2 support module for +mod_proxy +Extension +mod_proxy_http2.c +proxy_http2_module + + +

This module requires the service of mod_proxy. It provides the features used for + proxying HTTP/2 requests. mod_proxy_http2 + supports HTTP/2 only. It does not + provide any downgrades to HTTP/1.1.

+ +

Thus, in order to get the ability of handling HTTP/2 proxy requests, + mod_proxy and mod_proxy_http2 + have to be present in the server.

+ +

mod_proxy_http2 works with incoming requests + over HTTP/1.1 and HTTP/2 requests. If mod_proxy_http2 + handles the frontend connection, requests against the same HTTP/2 + backend are sent over a single connection, whenever possible.

+ +

This module relies on libnghttp2 + to provide the core http/2 engine.

+ + Warning +

This module is experimental. Its behaviors, directives, and + defaults are subject to more change from release to + release relative to other standard modules. Users are encouraged to + consult the "CHANGES" file for potential updates.

+
+ + Warning +

Do not enable proxying until you have secured your server. Open proxy + servers are dangerous both to your network and to the Internet at + large.

+
+
+mod_http2 +mod_proxy +mod_proxy_connect + +
Request notes +

mod_proxy_http creates the following request notes for + logging using the %{VARNAME}n format in + LogFormat or + ErrorLogFormat: +

+
+
proxy-source-port
+
The local port used for the connection to the backend server.
+
proxy-status
+
The HTTP/2 status received from the backend server.
+
+
+ +
diff --git a/modules/http2/NWGNUproxyht2 b/modules/http2/NWGNUproxyht2 new file mode 100644 index 0000000000..7153d084fc --- /dev/null +++ b/modules/http2/NWGNUproxyht2 @@ -0,0 +1,287 @@ +# +# This Makefile requires the environment var NGH2SRC +# pointing to the base directory of nghttp2 source tree. +# + +# +# Declare the sub-directories to be built here +# + +SUBDIRS = \ + $(EOLIST) + +# +# Get the 'head' of the build environment. This includes default targets and +# paths to tools +# + +include $(AP_WORK)/build/NWGNUhead.inc + +# +# build this level's files +# +# Make sure all needed macro's are defined +# + +# +# These directories will be at the beginning of the include list, followed by +# INCDIRS +# +XINCDIRS += \ + $(APR)/include \ + $(APRUTIL)/include \ + $(SRC)/include \ + $(NGH2SRC)/lib/includes \ + $(STDMOD)/proxy \ + $(SERVER)/mpm/NetWare \ + $(NWOS) \ + $(EOLIST) + +# +# These flags will come after CFLAGS +# +XCFLAGS += \ + $(EOLIST) + +# +# These defines will come after DEFINES +# +XDEFINES += \ + $(EOLIST) + +# +# These flags will be added to the link.opt file +# +XLFLAGS += \ + -L$(OBJDIR) \ + $(EOLIST) + +# +# These values will be appended to the correct variables based on the value of +# RELEASE +# +ifeq "$(RELEASE)" "debug" +XINCDIRS += \ + $(EOLIST) + +XCFLAGS += \ + $(EOLIST) + +XDEFINES += \ + $(EOLIST) + +XLFLAGS += \ + $(EOLIST) +endif + +ifeq "$(RELEASE)" "noopt" +XINCDIRS += \ + $(EOLIST) + +XCFLAGS += \ + $(EOLIST) + +XDEFINES += \ + $(EOLIST) + +XLFLAGS += \ + $(EOLIST) +endif + +ifeq "$(RELEASE)" "release" +XINCDIRS += \ + $(EOLIST) + +XCFLAGS += \ + $(EOLIST) + +XDEFINES += \ + $(EOLIST) + +XLFLAGS += \ + $(EOLIST) +endif + +# +# These are used by the link target if an NLM is being generated +# This is used by the link 'name' directive to name the nlm. If left blank +# TARGET_nlm (see below) will be used. +# +NLM_NAME = proxyht2 + +# +# This is used by the link '-desc ' directive. +# If left blank, NLM_NAME will be used. +# +NLM_DESCRIPTION = Apache $(VERSION_STR) HTTP2 Proxy module +# +# This is used by the '-threadname' directive. If left blank, +# NLM_NAME Thread will be used. +# +NLM_THREAD_NAME = $(NLM_NAME) + +# +# If this is specified, it will override VERSION value in +# $(AP_WORK)/build/NWGNUenvironment.inc +# +NLM_VERSION = + +# +# If this is specified, it will override the default of 64K +# +NLM_STACK_SIZE = 65536 + +# +# If this is specified it will be used by the link '-entry' directive +# +NLM_ENTRY_SYM = + +# +# If this is specified it will be used by the link '-exit' directive +# +NLM_EXIT_SYM = + +# +# If this is specified it will be used by the link '-check' directive +# +NLM_CHECK_SYM = + +# +# If this is specified it will be used by the link '-flags' directive +# +NLM_FLAGS = + +# +# If this is specified it will be linked in with the XDCData option in the def +# file instead of the default of $(NWOS)/apache.xdc. XDCData can be disabled +# by setting APACHE_UNIPROC in the environment +# +XDCDATA = + +# +# Declare all target files (you must add your files here) +# + +# +# If there is an NLM target, put it here +# +TARGET_nlm = \ + $(OBJDIR)/$(NLM_NAME).nlm \ + $(EOLIST) + +# +# If there is an LIB target, put it here +# +TARGET_lib = \ + $(EOLIST) + +# +# These are the OBJ files needed to create the NLM target above. +# Paths must all use the '/' character +# +FILES_nlm_objs = \ + $(OBJDIR)/mod_proxy_http2.o \ + $(OBJDIR)/h2_proxy_session.o \ + $(EOLIST) + +# +# These are the LIB files needed to create the NLM target above. +# These will be added as a library command in the link.opt file. +# +FILES_nlm_libs = \ + $(PRELUDE) \ + $(EOLIST) + +# +# These are the modules that the above NLM target depends on to load. +# These will be added as a module command in the link.opt file. +# +FILES_nlm_modules = \ + Libc \ + Apache2 \ + mod_proxy \ + mod_http2 \ + $(EOLIST) + +# +# If the nlm has a msg file, put it's path here +# +FILE_nlm_msg = + +# +# If the nlm has a hlp file put it's path here +# +FILE_nlm_hlp = + +# +# If this is specified, it will override $(NWOS)\copyright.txt. +# +FILE_nlm_copyright = + +# +# Any additional imports go here +# +FILES_nlm_Ximports = \ + @libc.imp \ + @aprlib.imp \ + @httpd.imp \ + @$(OBJDIR)/mod_http2.imp \ + ap_proxy_acquire_connection \ + ap_proxy_canon_netloc \ + ap_proxy_canonenc \ + ap_proxy_connect_backend \ + ap_proxy_connection_create \ + ap_proxy_cookie_reverse_map \ + ap_proxy_determine_connection \ + ap_proxy_location_reverse_map \ + ap_proxy_port_of_scheme \ + ap_proxy_release_connection \ + ap_proxy_ssl_connection_cleanup \ + ap_sock_disable_nagle \ + proxy_hook_canon_handler \ + proxy_hook_scheme_handler \ + proxy_module \ + proxy_run_detach_backend \ + $(EOLIST) + +# +# Any symbols exported to here +# +FILES_nlm_exports = \ + proxy_http2_module \ + $(EOLIST) + +# +# These are the OBJ files needed to create the LIB target above. +# Paths must all use the '/' character +# +FILES_lib_objs := +# +# implement targets and dependancies (leave this section alone) +# + +libs :: $(OBJDIR) $(TARGET_lib) + +nlms :: libs $(TARGET_nlm) + +# +# Updated this target to create necessary directories and copy files to the +# correct place. (See $(AP_WORK)/build/NWGNUhead.inc for examples) +# +install :: nlms FORCE + $(call COPY,$(OBJDIR)/*.nlm, $(INSTALLBASE)/modules/) + +clean :: + +# +# Any specialized rules here +# + +# +# Include the 'tail' makefile that has targets that depend on variables defined +# in this makefile +# + +include $(APBUILD)/NWGNUtail.inc + + diff --git a/modules/http2/h2_proxy_session.c b/modules/http2/h2_proxy_session.c new file mode 100644 index 0000000000..feb1c231a9 --- /dev/null +++ b/modules/http2/h2_proxy_session.c @@ -0,0 +1,1356 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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 +#include +#include + +#include +#include +#include + +#include "mod_http2.h" +#include "h2.h" +#include "h2_int_queue.h" +#include "h2_request.h" +#include "h2_util.h" +#include "h2_proxy_session.h" + +APLOG_USE_MODULE(proxy_http2); + +typedef struct h2_proxy_stream { + int id; + apr_pool_t *pool; + h2_proxy_session *session; + + const char *url; + request_rec *r; + h2_request *req; + + h2_stream_state_t state; + unsigned int suspended : 1; + unsigned int data_received : 1; + + apr_bucket_brigade *input; + apr_bucket_brigade *output; + + apr_table_t *saves; +} h2_proxy_stream; + + +static void dispatch_event(h2_proxy_session *session, h2_proxys_event_t ev, + int arg, const char *msg); + + +static apr_status_t proxy_session_pre_close(void *theconn) +{ + proxy_conn_rec *p_conn = (proxy_conn_rec *)theconn; + h2_proxy_session *session = p_conn->data; + + if (session && session->ngh2) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "proxy_session(%s): pool cleanup, state=%d, streams=%d", + session->id, session->state, + (int)h2_ihash_count(session->streams)); + session->aborted = 1; + dispatch_event(session, H2_PROXYS_EV_PRE_CLOSE, 0, NULL); + nghttp2_session_del(session->ngh2); + session->ngh2 = NULL; + p_conn->data = NULL; + } + return APR_SUCCESS; +} + +static int proxy_pass_brigade(apr_bucket_alloc_t *bucket_alloc, + proxy_conn_rec *p_conn, + conn_rec *origin, apr_bucket_brigade *bb, + int flush) +{ + apr_status_t status; + apr_off_t transferred; + + if (flush) { + apr_bucket *e = apr_bucket_flush_create(bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, e); + } + apr_brigade_length(bb, 0, &transferred); + if (transferred != -1) + p_conn->worker->s->transferred += transferred; + status = ap_pass_brigade(origin->output_filters, bb); + /* Cleanup the brigade now to avoid buckets lifetime + * issues in case of error returned below. */ + apr_brigade_cleanup(bb); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, origin, APLOGNO(03357) + "pass output failed to %pI (%s)", + p_conn->addr, p_conn->hostname); + } + return status; +} + +static ssize_t raw_send(nghttp2_session *ngh2, const uint8_t *data, + size_t length, int flags, void *user_data) +{ + h2_proxy_session *session = user_data; + apr_bucket *b; + apr_status_t status; + int flush = 1; + + if (data) { + b = apr_bucket_transient_create((const char*)data, length, + session->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(session->output, b); + } + + status = proxy_pass_brigade(session->c->bucket_alloc, + session->p_conn, session->c, + session->output, flush); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, + "h2_proxy_sesssion(%s): raw_send %d bytes, flush=%d", + session->id, (int)length, flush); + if (status != APR_SUCCESS) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return length; +} + +static int on_frame_recv(nghttp2_session *ngh2, const nghttp2_frame *frame, + void *user_data) +{ + h2_proxy_session *session = user_data; + int n; + + if (APLOGcdebug(session->c)) { + char buffer[256]; + + h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03341) + "h2_proxy_session(%s): recv FRAME[%s]", + session->id, buffer); + } + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + break; + case NGHTTP2_PUSH_PROMISE: + break; + case NGHTTP2_SETTINGS: + if (frame->settings.niv > 0) { + n = nghttp2_session_get_remote_settings(ngh2, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); + if (n > 0) { + session->remote_max_concurrent = n; + } + } + break; + case NGHTTP2_GOAWAY: + /* we expect the remote server to tell us the highest stream id + * that it has started processing. */ + session->last_stream_id = frame->goaway.last_stream_id; + dispatch_event(session, H2_PROXYS_EV_REMOTE_GOAWAY, 0, NULL); + if (APLOGcinfo(session->c)) { + char buffer[256]; + + h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03342) + "h2_proxy_session(%s): recv FRAME[%s]", + session->id, buffer); + } + break; + default: + break; + } + return 0; +} + +static int before_frame_send(nghttp2_session *ngh2, + const nghttp2_frame *frame, void *user_data) +{ + h2_proxy_session *session = user_data; + if (APLOGcdebug(session->c)) { + char buffer[256]; + + h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03343) + "h2_proxy_session(%s): sent FRAME[%s]", + session->id, buffer); + } + return 0; +} + +static int add_header(void *table, const char *n, const char *v) +{ + apr_table_addn(table, n, v); + return 1; +} + +static void process_proxy_header(request_rec *r, const char *n, const char *v) +{ + static const struct { + const char *name; + ap_proxy_header_reverse_map_fn func; + } transform_hdrs[] = { + { "Location", ap_proxy_location_reverse_map }, + { "Content-Location", ap_proxy_location_reverse_map }, + { "URI", ap_proxy_location_reverse_map }, + { "Destination", ap_proxy_location_reverse_map }, + { "Set-Cookie", ap_proxy_cookie_reverse_map }, + { NULL, NULL } + }; + proxy_dir_conf *dconf; + int i; + + for (i = 0; transform_hdrs[i].name; ++i) { + if (!strcasecmp(transform_hdrs[i].name, n)) { + dconf = ap_get_module_config(r->per_dir_config, &proxy_module); + apr_table_add(r->headers_out, n, + (*transform_hdrs[i].func)(r, dconf, v)); + return; + } + } + apr_table_add(r->headers_out, n, v); +} + +static apr_status_t h2_proxy_stream_add_header_out(h2_proxy_stream *stream, + const char *n, apr_size_t nlen, + const char *v, apr_size_t vlen) +{ + if (n[0] == ':') { + if (!stream->data_received && !strncmp(":status", n, nlen)) { + char *s = apr_pstrndup(stream->r->pool, v, vlen); + + apr_table_setn(stream->r->notes, "proxy-status", s); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, + "h2_proxy_stream(%s-%d): got status %s", + stream->session->id, stream->id, s); + stream->r->status = (int)apr_atoi64(s); + if (stream->r->status <= 0) { + stream->r->status = 500; + return APR_EGENERAL; + } + } + return APR_SUCCESS; + } + + if (!h2_proxy_res_ignore_header(n, nlen)) { + char *hname, *hvalue; + + hname = apr_pstrndup(stream->pool, n, nlen); + h2_util_camel_case_header(hname, nlen); + hvalue = apr_pstrndup(stream->pool, v, vlen); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, + "h2_proxy_stream(%s-%d): got header %s: %s", + stream->session->id, stream->id, hname, hvalue); + process_proxy_header(stream->r, hname, hvalue); + } + return APR_SUCCESS; +} + +static int log_header(void *ctx, const char *key, const char *value) +{ + h2_proxy_stream *stream = ctx; + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, stream->r, + "h2_proxy_stream(%s-%d), header_out %s: %s", + stream->session->id, stream->id, key, value); + return 1; +} + +static void h2_proxy_stream_end_headers_out(h2_proxy_stream *stream) +{ + h2_proxy_session *session = stream->session; + request_rec *r = stream->r; + apr_pool_t *p = r->pool; + + /* Now, add in the cookies from the response to the ones already saved */ + apr_table_do(add_header, stream->saves, r->headers_out, "Set-Cookie", NULL); + + /* and now load 'em all in */ + if (!apr_is_empty_table(stream->saves)) { + apr_table_unset(r->headers_out, "Set-Cookie"); + r->headers_out = apr_table_overlay(p, r->headers_out, stream->saves); + } + + /* handle Via header in response */ + if (session->conf->viaopt != via_off + && session->conf->viaopt != via_block) { + const char *server_name = ap_get_server_name(stream->r); + apr_port_t port = ap_get_server_port(stream->r); + char portstr[32]; + + /* If USE_CANONICAL_NAME_OFF was configured for the proxy virtual host, + * then the server name returned by ap_get_server_name() is the + * origin server name (which does make too much sense with Via: headers) + * so we use the proxy vhost's name instead. + */ + if (server_name == stream->r->hostname) { + server_name = stream->r->server->server_hostname; + } + if (ap_is_default_port(port, stream->r)) { + portstr[0] = '\0'; + } + else { + apr_snprintf(portstr, sizeof(portstr), ":%d", port); + } + + /* create a "Via:" response header entry and merge it */ + apr_table_addn(r->headers_out, "Via", + (session->conf->viaopt == via_full) + ? apr_psprintf(p, "%d.%d %s%s (%s)", + HTTP_VERSION_MAJOR(r->proto_num), + HTTP_VERSION_MINOR(r->proto_num), + server_name, portstr, + AP_SERVER_BASEVERSION) + : apr_psprintf(p, "%d.%d %s%s", + HTTP_VERSION_MAJOR(r->proto_num), + HTTP_VERSION_MINOR(r->proto_num), + server_name, portstr) + ); + } + + if (APLOGrtrace2(stream->r)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, stream->r, + "h2_proxy_stream(%s-%d), header_out after merging", + stream->session->id, stream->id); + apr_table_do(log_header, stream, stream->r->headers_out, NULL); + } +} + +static int on_data_chunk_recv(nghttp2_session *ngh2, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) +{ + h2_proxy_session *session = user_data; + h2_proxy_stream *stream; + apr_bucket *b; + apr_status_t status; + + stream = nghttp2_session_get_stream_user_data(ngh2, stream_id); + if (!stream) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, stream->r, APLOGNO(03358) + "h2_proxy_session(%s): recv data chunk for " + "unknown stream %d, ignored", + session->id, stream_id); + return 0; + } + + if (!stream->data_received) { + /* last chance to manipulate response headers. + * after this, only trailers */ + h2_proxy_stream_end_headers_out(stream); + stream->data_received = 1; + } + + b = apr_bucket_transient_create((const char*)data, len, + stream->r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + if (flags & NGHTTP2_DATA_FLAG_EOF) { + b = apr_bucket_flush_create(stream->r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + } + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, stream->r, APLOGNO(03359) + "h2_proxy_session(%s): pass response data for " + "stream %d, %d bytes", session->id, stream_id, (int)len); + status = ap_pass_brigade(stream->r->output_filters, stream->output); + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, APLOGNO(03344) + "h2_proxy_session(%s): passing output on stream %d", + session->id, stream->id); + nghttp2_submit_rst_stream(ngh2, NGHTTP2_FLAG_NONE, + stream_id, NGHTTP2_STREAM_CLOSED); + return NGHTTP2_ERR_STREAM_CLOSING; + } + return 0; +} + +static int on_stream_close(nghttp2_session *ngh2, int32_t stream_id, + uint32_t error_code, void *user_data) +{ + h2_proxy_session *session = user_data; + if (!session->aborted) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03360) + "h2_proxy_session(%s): stream=%d, closed, err=%d", + session->id, stream_id, error_code); + dispatch_event(session, H2_PROXYS_EV_STREAM_DONE, stream_id, NULL); + } + return 0; +} + +static int on_header(nghttp2_session *ngh2, const nghttp2_frame *frame, + const uint8_t *namearg, size_t nlen, + const uint8_t *valuearg, size_t vlen, uint8_t flags, + void *user_data) +{ + h2_proxy_session *session = user_data; + h2_proxy_stream *stream; + const char *n = (const char*)namearg; + const char *v = (const char*)valuearg; + + (void)session; + if (frame->hd.type == NGHTTP2_HEADERS && nlen) { + stream = nghttp2_session_get_stream_user_data(ngh2, frame->hd.stream_id); + if (stream) { + if (h2_proxy_stream_add_header_out(stream, n, nlen, v, vlen)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + } + else if (frame->hd.type == NGHTTP2_PUSH_PROMISE) { + } + + return 0; +} + +static ssize_t stream_data_read(nghttp2_session *ngh2, int32_t stream_id, + uint8_t *buf, size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) +{ + h2_proxy_stream *stream; + apr_status_t status = APR_SUCCESS; + + *data_flags = 0; + stream = nghttp2_session_get_stream_user_data(ngh2, stream_id); + if (!stream) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, stream->r, APLOGNO(03361) + "h2_proxy_stream(%s): data_read, stream %d not found", + stream->session->id, stream_id); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + if (APR_BRIGADE_EMPTY(stream->input)) { + status = ap_get_brigade(stream->r->input_filters, stream->input, + AP_MODE_READBYTES, APR_NONBLOCK_READ, + H2MAX(APR_BUCKET_BUFF_SIZE, length)); + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, status, stream->r, + "h2_proxy_stream(%s-%d): request body read", + stream->session->id, stream->id); + } + + if (status == APR_SUCCESS) { + ssize_t readlen = 0; + while (status == APR_SUCCESS + && (readlen < length) + && !APR_BRIGADE_EMPTY(stream->input)) { + apr_bucket* b = APR_BRIGADE_FIRST(stream->input); + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + else { + /* we do nothing more regarding any meta here */ + } + } + else { + const char *bdata = NULL; + apr_size_t blen = 0; + status = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ); + + if (status == APR_SUCCESS && blen > 0) { + ssize_t copylen = H2MIN(length - readlen, blen); + memcpy(buf, bdata, copylen); + buf += copylen; + readlen += copylen; + if (copylen < blen) { + /* We have data left in the bucket. Split it. */ + status = apr_bucket_split(b, copylen); + } + } + } + apr_bucket_delete(b); + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, status, stream->r, + "h2_proxy_stream(%d): request body read %ld bytes, flags=%d", + stream->id, (long)readlen, (int)*data_flags); + return readlen; + } + else if (APR_STATUS_IS_EAGAIN(status)) { + /* suspended stream, needs to be re-awakened */ + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, status, stream->r, + "h2_proxy_stream(%s-%d): suspending", + stream->session->id, stream_id); + stream->suspended = 1; + h2_iq_add(stream->session->suspended, stream->id, NULL, NULL); + return NGHTTP2_ERR_DEFERRED; + } + else { + nghttp2_submit_rst_stream(ngh2, NGHTTP2_FLAG_NONE, + stream_id, NGHTTP2_STREAM_CLOSED); + return NGHTTP2_ERR_STREAM_CLOSING; + } +} + +h2_proxy_session *h2_proxy_session_setup(const char *id, proxy_conn_rec *p_conn, + proxy_server_conf *conf, + unsigned char window_bits_connection, + unsigned char window_bits_stream, + h2_proxy_request_done *done) +{ + if (!p_conn->data) { + apr_pool_t *pool = p_conn->scpool; + h2_proxy_session *session; + nghttp2_session_callbacks *cbs; + nghttp2_option *option; + + session = apr_pcalloc(pool, sizeof(*session)); + apr_pool_pre_cleanup_register(pool, p_conn, proxy_session_pre_close); + p_conn->data = session; + + session->id = apr_pstrdup(p_conn->scpool, id); + session->c = p_conn->connection; + session->p_conn = p_conn; + session->conf = conf; + session->pool = p_conn->scpool; + session->state = H2_PROXYS_ST_INIT; + session->window_bits_stream = window_bits_stream; + session->window_bits_connection = window_bits_connection; + session->streams = h2_ihash_create(pool, offsetof(h2_proxy_stream, id)); + session->suspended = h2_iq_create(pool, 5); + session->done = done; + + session->input = apr_brigade_create(session->pool, session->c->bucket_alloc); + session->output = apr_brigade_create(session->pool, session->c->bucket_alloc); + + nghttp2_session_callbacks_new(&cbs); + nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(cbs, on_data_chunk_recv); + nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close); + nghttp2_session_callbacks_set_on_header_callback(cbs, on_header); + nghttp2_session_callbacks_set_before_frame_send_callback(cbs, before_frame_send); + nghttp2_session_callbacks_set_send_callback(cbs, raw_send); + + nghttp2_option_new(&option); + nghttp2_option_set_peer_max_concurrent_streams(option, 100); + nghttp2_option_set_no_auto_window_update(option, 1); + + nghttp2_session_client_new2(&session->ngh2, cbs, session, option); + + nghttp2_option_del(option); + nghttp2_session_callbacks_del(cbs); + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03362) + "setup session for %s", p_conn->hostname); + } + return p_conn->data; +} + +static apr_status_t session_start(h2_proxy_session *session) +{ + nghttp2_settings_entry settings[2]; + int rv, add_conn_window; + apr_socket_t *s; + + s = ap_get_conn_socket(session->c); + if (s) { + ap_sock_disable_nagle(s); + } + + settings[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; + settings[0].value = 0; + settings[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + settings[1].value = (1 << session->window_bits_stream) - 1; + + rv = nghttp2_submit_settings(session->ngh2, NGHTTP2_FLAG_NONE, settings, + H2_ALEN(settings)); + + /* If the connection window is larger than our default, trigger a WINDOW_UPDATE */ + add_conn_window = ((1 << session->window_bits_connection) - 1 - + NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE); + if (!rv && add_conn_window != 0) { + rv = nghttp2_submit_window_update(session->ngh2, NGHTTP2_FLAG_NONE, 0, add_conn_window); + } + return rv? APR_EGENERAL : APR_SUCCESS; +} + +static apr_status_t open_stream(h2_proxy_session *session, const char *url, + request_rec *r, h2_proxy_stream **pstream) +{ + h2_proxy_stream *stream; + apr_uri_t puri; + const char *authority, *scheme, *path; + + stream = apr_pcalloc(r->pool, sizeof(*stream)); + + stream->pool = r->pool; + stream->url = url; + stream->r = r; + stream->session = session; + stream->state = H2_STREAM_ST_IDLE; + + stream->input = apr_brigade_create(stream->pool, session->c->bucket_alloc); + stream->output = apr_brigade_create(stream->pool, session->c->bucket_alloc); + + stream->req = h2_request_create(1, stream->pool, 0); + + apr_uri_parse(stream->pool, url, &puri); + scheme = (strcmp(puri.scheme, "h2")? "http" : "https"); + authority = puri.hostname; + if (!ap_strchr_c(authority, ':') && puri.port + && apr_uri_port_of_scheme(scheme) != puri.port) { + /* port info missing and port is not default for scheme: append */ + authority = apr_psprintf(stream->pool, "%s:%d", authority, puri.port); + } + path = apr_uri_unparse(stream->pool, &puri, APR_URI_UNP_OMITSITEPART); + h2_request_make(stream->req, stream->pool, r->method, scheme, + authority, path, r->headers_in); + + /* Tuck away all already existing cookies */ + stream->saves = apr_table_make(r->pool, 2); + apr_table_do(add_header, stream->saves, r->headers_out,"Set-Cookie", NULL); + + *pstream = stream; + + return APR_SUCCESS; +} + +static apr_status_t submit_stream(h2_proxy_session *session, h2_proxy_stream *stream) +{ + h2_ngheader *hd; + nghttp2_data_provider *pp = NULL; + nghttp2_data_provider provider; + int rv; + apr_status_t status; + + hd = h2_util_ngheader_make_req(stream->pool, stream->req); + + status = ap_get_brigade(stream->r->input_filters, stream->input, + AP_MODE_READBYTES, APR_NONBLOCK_READ, + APR_BUCKET_BUFF_SIZE); + if ((status == APR_SUCCESS && !APR_BUCKET_IS_EOS(APR_BRIGADE_FIRST(stream->input))) + || APR_STATUS_IS_EAGAIN(status)) { + /* there might be data coming */ + provider.source.fd = 0; + provider.source.ptr = NULL; + provider.read_callback = stream_data_read; + pp = &provider; + } + + rv = nghttp2_submit_request(session->ngh2, NULL, + hd->nv, hd->nvlen, pp, stream); + + if (APLOGcdebug(session->c)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03363) + "h2_proxy_session(%s): submit %s%s -> %d", + session->id, stream->req->authority, stream->req->path, + rv); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + "h2_proxy_session(%s-%d): submit %s%s", + session->id, rv, stream->req->authority, stream->req->path); + } + + if (rv > 0) { + stream->id = rv; + stream->state = H2_STREAM_ST_OPEN; + h2_ihash_add(session->streams, stream); + dispatch_event(session, H2_PROXYS_EV_STREAM_SUBMITTED, rv, NULL); + + return APR_SUCCESS; + } + return APR_EGENERAL; +} + +static apr_status_t feed_brigade(h2_proxy_session *session, apr_bucket_brigade *bb) +{ + apr_status_t status = APR_SUCCESS; + apr_size_t readlen = 0; + ssize_t n; + + while (status == APR_SUCCESS && !APR_BRIGADE_EMPTY(bb)) { + apr_bucket* b = APR_BRIGADE_FIRST(bb); + + if (APR_BUCKET_IS_METADATA(b)) { + /* nop */ + } + else { + const char *bdata = NULL; + apr_size_t blen = 0; + + status = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ); + if (status == APR_SUCCESS && blen > 0) { + n = nghttp2_session_mem_recv(session->ngh2, (const uint8_t *)bdata, blen); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_proxy_session(%s): feeding %ld bytes -> %ld", + session->id, (long)blen, (long)n); + if (n < 0) { + if (nghttp2_is_fatal((int)n)) { + status = APR_EGENERAL; + } + } + else { + readlen += n; + if (n < blen) { + apr_bucket_split(b, n); + } + } + } + } + apr_bucket_delete(b); + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, + "h2_proxy_session(%s): fed %ld bytes of input to session", + session->id, (long)readlen); + if (readlen == 0 && status == APR_SUCCESS) { + return APR_EAGAIN; + } + return status; +} + +static apr_status_t h2_proxy_session_read(h2_proxy_session *session, int block, + apr_interval_time_t timeout) +{ + apr_status_t status = APR_SUCCESS; + + if (APR_BRIGADE_EMPTY(session->input)) { + apr_socket_t *socket = NULL; + apr_time_t save_timeout = -1; + + if (block) { + socket = ap_get_conn_socket(session->c); + if (socket) { + apr_socket_timeout_get(socket, &save_timeout); + apr_socket_timeout_set(socket, timeout); + } + else { + /* cannot block on timeout */ + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, session->c, APLOGNO(03379) + "h2_proxy_session(%s): unable to get conn socket", + session->id); + return APR_ENOTIMPL; + } + } + + status = ap_get_brigade(session->c->input_filters, session->input, + AP_MODE_READBYTES, + block? APR_BLOCK_READ : APR_NONBLOCK_READ, + 64 * 1024); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c, + "h2_proxy_session(%s): read from conn", session->id); + if (socket && save_timeout != -1) { + apr_socket_timeout_set(socket, save_timeout); + } + } + + if (status == APR_SUCCESS) { + status = feed_brigade(session, session->input); + } + else if (APR_STATUS_IS_TIMEUP(status)) { + /* nop */ + } + else if (!APR_STATUS_IS_EAGAIN(status)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, APLOGNO(03380) + "h2_proxy_session(%s): read error", session->id); + dispatch_event(session, H2_PROXYS_EV_CONN_ERROR, status, NULL); + } + + return status; +} + +apr_status_t h2_proxy_session_submit(h2_proxy_session *session, + const char *url, request_rec *r) +{ + h2_proxy_stream *stream; + apr_status_t status; + + status = open_stream(session, url, r, &stream); + if (status == OK) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03381) + "process stream(%d): %s %s%s, original: %s", + stream->id, stream->req->method, + stream->req->authority, stream->req->path, + r->the_request); + status = submit_stream(session, stream); + } + return status; +} + +static apr_status_t check_suspended(h2_proxy_session *session) +{ + h2_proxy_stream *stream; + int i, stream_id; + apr_status_t status; + + for (i = 0; i < session->suspended->nelts; ++i) { + stream_id = session->suspended->elts[i]; + stream = nghttp2_session_get_stream_user_data(session->ngh2, stream_id); + if (stream) { + status = ap_get_brigade(stream->r->input_filters, stream->input, + AP_MODE_READBYTES, APR_NONBLOCK_READ, + APR_BUCKET_BUFF_SIZE); + if (status == APR_SUCCESS && !APR_BRIGADE_EMPTY(stream->input)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c, + "h2_proxy_stream(%s-%d): resuming", + session->id, stream_id); + stream->suspended = 0; + h2_iq_remove(session->suspended, stream_id); + nghttp2_session_resume_data(session->ngh2, stream_id); + dispatch_event(session, H2_PROXYS_EV_STREAM_RESUMED, 0, NULL); + check_suspended(session); + return APR_SUCCESS; + } + else if (status != APR_SUCCESS && !APR_STATUS_IS_EAGAIN(status)) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, session->c, + APLOGNO(03382) "h2_proxy_stream(%s-%d): check input", + session->id, stream_id); + h2_iq_remove(session->suspended, stream_id); + dispatch_event(session, H2_PROXYS_EV_STREAM_RESUMED, 0, NULL); + check_suspended(session); + return APR_SUCCESS; + } + } + else { + /* gone? */ + h2_iq_remove(session->suspended, stream_id); + check_suspended(session); + return APR_SUCCESS; + } + } + return APR_EAGAIN; +} + +static apr_status_t session_shutdown(h2_proxy_session *session, int reason, + const char *msg) +{ + apr_status_t status = APR_SUCCESS; + const char *err = msg; + + AP_DEBUG_ASSERT(session); + if (!err && reason) { + err = nghttp2_strerror(reason); + } + nghttp2_submit_goaway(session->ngh2, NGHTTP2_FLAG_NONE, 0, + reason, (uint8_t*)err, err? strlen(err):0); + status = nghttp2_session_send(session->ngh2); + dispatch_event(session, H2_PROXYS_EV_LOCAL_GOAWAY, reason, err); + return status; +} + + +static const char *StateNames[] = { + "INIT", /* H2_PROXYS_ST_INIT */ + "DONE", /* H2_PROXYS_ST_DONE */ + "IDLE", /* H2_PROXYS_ST_IDLE */ + "BUSY", /* H2_PROXYS_ST_BUSY */ + "WAIT", /* H2_PROXYS_ST_WAIT */ + "LSHUTDOWN", /* H2_PROXYS_ST_LOCAL_SHUTDOWN */ + "RSHUTDOWN", /* H2_PROXYS_ST_REMOTE_SHUTDOWN */ +}; + +static const char *state_name(h2_proxys_state state) +{ + if (state >= (sizeof(StateNames)/sizeof(StateNames[0]))) { + return "unknown"; + } + return StateNames[state]; +} + +static int is_accepting_streams(h2_proxy_session *session) +{ + switch (session->state) { + case H2_PROXYS_ST_IDLE: + case H2_PROXYS_ST_BUSY: + case H2_PROXYS_ST_WAIT: + return 1; + default: + return 0; + } +} + +static void transit(h2_proxy_session *session, const char *action, + h2_proxys_state nstate) +{ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03345) + "h2_proxy_session(%s): transit [%s] -- %s --> [%s]", session->id, + state_name(session->state), action, state_name(nstate)); + session->state = nstate; +} + +static void ev_init(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_INIT: + if (h2_ihash_is_empty(session->streams)) { + transit(session, "init", H2_PROXYS_ST_IDLE); + } + else { + transit(session, "init", H2_PROXYS_ST_BUSY); + } + break; + + default: + /* nop */ + break; + } +} + +static void ev_local_goaway(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + /* already did that? */ + break; + case H2_PROXYS_ST_IDLE: + case H2_PROXYS_ST_REMOTE_SHUTDOWN: + /* all done */ + transit(session, "local goaway", H2_PROXYS_ST_DONE); + break; + default: + transit(session, "local goaway", H2_PROXYS_ST_LOCAL_SHUTDOWN); + break; + } +} + +static void ev_remote_goaway(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_REMOTE_SHUTDOWN: + /* already received that? */ + break; + case H2_PROXYS_ST_IDLE: + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + /* all done */ + transit(session, "remote goaway", H2_PROXYS_ST_DONE); + break; + default: + transit(session, "remote goaway", H2_PROXYS_ST_REMOTE_SHUTDOWN); + break; + } +} + +static void ev_conn_error(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_INIT: + case H2_PROXYS_ST_DONE: + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + /* just leave */ + transit(session, "conn error", H2_PROXYS_ST_DONE); + break; + + default: + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, arg, session->c, + "h2_proxy_session(%s): conn error -> shutdown", session->id); + session_shutdown(session, arg, msg); + break; + } +} + +static void ev_proto_error(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_DONE: + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + /* just leave */ + transit(session, "proto error", H2_PROXYS_ST_DONE); + break; + + default: + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_proxy_session(%s): proto error -> shutdown", session->id); + session_shutdown(session, arg, msg); + break; + } +} + +static void ev_conn_timeout(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + transit(session, "conn timeout", H2_PROXYS_ST_DONE); + break; + default: + session_shutdown(session, arg, msg); + transit(session, "conn timeout", H2_PROXYS_ST_DONE); + break; + } +} + +static void ev_no_io(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_BUSY: + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + case H2_PROXYS_ST_REMOTE_SHUTDOWN: + /* nothing for input and output to do. If we remain + * in this state, we go into a tight loop and suck up + * CPU cycles. Ideally, we'd like to do a blocking read, but that + * is not possible if we have scheduled tasks and wait + * for them to produce something. */ + if (h2_ihash_is_empty(session->streams)) { + if (!is_accepting_streams(session)) { + /* We are no longer accepting new streams and have + * finished processing existing ones. Time to leave. */ + session_shutdown(session, arg, msg); + transit(session, "no io", H2_PROXYS_ST_DONE); + } + else { + /* When we have no streams, no task events are possible, + * switch to blocking reads */ + transit(session, "no io", H2_PROXYS_ST_IDLE); + } + } + else { + /* Unable to do blocking reads, as we wait on events from + * task processing in other threads. Do a busy wait with + * backoff timer. */ + transit(session, "no io", H2_PROXYS_ST_WAIT); + } + break; + default: + /* nop */ + break; + } +} + +static void ev_stream_submitted(h2_proxy_session *session, int stream_id, + const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_IDLE: + case H2_PROXYS_ST_WAIT: + transit(session, "stream submitted", H2_PROXYS_ST_BUSY); + break; + default: + /* nop */ + break; + } +} + +static void ev_stream_done(h2_proxy_session *session, int stream_id, + const char *msg) +{ + h2_proxy_stream *stream; + + stream = nghttp2_session_get_stream_user_data(session->ngh2, stream_id); + if (stream) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03364) + "h2_proxy_sesssion(%s): stream(%d) closed", + session->id, stream_id); + + if (!stream->data_received) { + apr_bucket *b; + /* if the response had no body, this is the time to flush + * an empty brigade which will also "write" the resonse + * headers */ + h2_proxy_stream_end_headers_out(stream); + stream->data_received = 1; + b = apr_bucket_flush_create(stream->r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + b = apr_bucket_eos_create(stream->r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + ap_pass_brigade(stream->r->output_filters, stream->output); + } + + stream->state = H2_STREAM_ST_CLOSED; + h2_ihash_remove(session->streams, stream_id); + h2_iq_remove(session->suspended, stream_id); + if (session->done) { + session->done(session, stream->r, 1, 1); + } + } + + switch (session->state) { + default: + /* nop */ + break; + } +} + +static void ev_stream_resumed(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_WAIT: + transit(session, "stream resumed", H2_PROXYS_ST_BUSY); + break; + default: + /* nop */ + break; + } +} + +static void ev_data_read(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_IDLE: + case H2_PROXYS_ST_WAIT: + transit(session, "data read", H2_PROXYS_ST_BUSY); + break; + /* fall through */ + default: + /* nop */ + break; + } +} + +static void ev_ngh2_done(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_DONE: + /* nop */ + break; + default: + transit(session, "nghttp2 done", H2_PROXYS_ST_DONE); + break; + } +} + +static void ev_pre_close(h2_proxy_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_PROXYS_ST_DONE: + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + /* nop */ + break; + default: + session_shutdown(session, arg, msg); + break; + } +} + +static void dispatch_event(h2_proxy_session *session, h2_proxys_event_t ev, + int arg, const char *msg) +{ + switch (ev) { + case H2_PROXYS_EV_INIT: + ev_init(session, arg, msg); + break; + case H2_PROXYS_EV_LOCAL_GOAWAY: + ev_local_goaway(session, arg, msg); + break; + case H2_PROXYS_EV_REMOTE_GOAWAY: + ev_remote_goaway(session, arg, msg); + break; + case H2_PROXYS_EV_CONN_ERROR: + ev_conn_error(session, arg, msg); + break; + case H2_PROXYS_EV_PROTO_ERROR: + ev_proto_error(session, arg, msg); + break; + case H2_PROXYS_EV_CONN_TIMEOUT: + ev_conn_timeout(session, arg, msg); + break; + case H2_PROXYS_EV_NO_IO: + ev_no_io(session, arg, msg); + break; + case H2_PROXYS_EV_STREAM_SUBMITTED: + ev_stream_submitted(session, arg, msg); + break; + case H2_PROXYS_EV_STREAM_DONE: + ev_stream_done(session, arg, msg); + break; + case H2_PROXYS_EV_STREAM_RESUMED: + ev_stream_resumed(session, arg, msg); + break; + case H2_PROXYS_EV_DATA_READ: + ev_data_read(session, arg, msg); + break; + case H2_PROXYS_EV_NGH2_DONE: + ev_ngh2_done(session, arg, msg); + break; + case H2_PROXYS_EV_PRE_CLOSE: + ev_pre_close(session, arg, msg); + break; + default: + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + "h2_proxy_session(%s): unknown event %d", + session->id, ev); + break; + } +} + +apr_status_t h2_proxy_session_process(h2_proxy_session *session) +{ + apr_status_t status; + int have_written = 0, have_read = 0; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_proxy_session(%s): process", session->id); + +run_loop: + switch (session->state) { + case H2_PROXYS_ST_INIT: + status = session_start(session); + if (status == APR_SUCCESS) { + dispatch_event(session, H2_PROXYS_EV_INIT, 0, NULL); + goto run_loop; + } + else { + dispatch_event(session, H2_PROXYS_EV_CONN_ERROR, status, NULL); + } + break; + + case H2_PROXYS_ST_BUSY: + case H2_PROXYS_ST_LOCAL_SHUTDOWN: + case H2_PROXYS_ST_REMOTE_SHUTDOWN: + while (nghttp2_session_want_write(session->ngh2)) { + int rv = nghttp2_session_send(session->ngh2); + if (rv < 0 && nghttp2_is_fatal(rv)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_proxy_session(%s): write, rv=%d", session->id, rv); + dispatch_event(session, H2_PROXYS_EV_CONN_ERROR, rv, NULL); + break; + } + have_written = 1; + } + + if (nghttp2_session_want_read(session->ngh2)) { + status = h2_proxy_session_read(session, 0, 0); + if (status == APR_SUCCESS) { + have_read = 1; + } + } + + if (!have_written && !have_read + && !nghttp2_session_want_write(session->ngh2)) { + dispatch_event(session, H2_PROXYS_EV_NO_IO, 0, NULL); + goto run_loop; + } + break; + + case H2_PROXYS_ST_WAIT: + if (check_suspended(session) == APR_EAGAIN) { + /* no stream has become resumed. Do a blocking read with + * ever increasing timeouts... */ + if (session->wait_timeout < 25) { + session->wait_timeout = 25; + } + else { + session->wait_timeout = H2MIN(apr_time_from_msec(100), + 2*session->wait_timeout); + } + + status = h2_proxy_session_read(session, 1, session->wait_timeout); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, + APLOGNO(03365) + "h2_proxy_session(%s): WAIT read, timeout=%fms", + session->id, (float)session->wait_timeout/1000.0); + if (status == APR_SUCCESS) { + have_read = 1; + dispatch_event(session, H2_PROXYS_EV_DATA_READ, 0, NULL); + } + else if (APR_STATUS_IS_TIMEUP(status) + || APR_STATUS_IS_EAGAIN(status)) { + /* go back to checking all inputs again */ + transit(session, "wait cycle", H2_PROXYS_ST_BUSY); + } + } + break; + + case H2_PROXYS_ST_IDLE: + break; + + case H2_PROXYS_ST_DONE: /* done, session terminated */ + return APR_EOF; + + default: + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, session->c, + APLOGNO(03346)"h2_proxy_session(%s): unknown state %d", + session->id, session->state); + dispatch_event(session, H2_PROXYS_EV_PROTO_ERROR, 0, NULL); + break; + } + + + if (have_read || have_written) { + session->wait_timeout = 0; + } + + if (!nghttp2_session_want_read(session->ngh2) + && !nghttp2_session_want_write(session->ngh2)) { + dispatch_event(session, H2_PROXYS_EV_NGH2_DONE, 0, NULL); + } + + return APR_SUCCESS; /* needs to be called again */ +} + +typedef struct { + h2_proxy_session *session; + h2_proxy_request_done *done; +} cleanup_iter_ctx; + +static int done_iter(void *udata, void *val) +{ + cleanup_iter_ctx *ctx = udata; + h2_proxy_stream *stream = val; + int touched = (!ctx->session->last_stream_id || + stream->id <= ctx->session->last_stream_id); + ctx->done(ctx->session, stream->r, 0, touched); + return 1; +} + +void h2_proxy_session_cleanup(h2_proxy_session *session, + h2_proxy_request_done *done) +{ + if (session->streams && !h2_ihash_is_empty(session->streams)) { + cleanup_iter_ctx ctx; + ctx.session = session; + ctx.done = done; + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03366) + "h2_proxy_session(%s): terminated, %d streams unfinished", + session->id, (int)h2_ihash_count(session->streams)); + h2_ihash_iter(session->streams, done_iter, &ctx); + h2_ihash_clear(session->streams); + } +} + +typedef struct { + h2_proxy_session *session; + conn_rec *c; + apr_off_t bytes; + int updated; +} win_update_ctx; + +static int win_update_iter(void *udata, void *val) +{ + win_update_ctx *ctx = udata; + h2_proxy_stream *stream = val; + + if (stream->r && stream->r->connection == ctx->c) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, ctx->session->c, + "h2_proxy_session(%s-%d): win_update %ld bytes", + ctx->session->id, (int)stream->id, (long)ctx->bytes); + nghttp2_session_consume(ctx->session->ngh2, stream->id, ctx->bytes); + ctx->updated = 1; + return 0; + } + return 1; +} + + +void h2_proxy_session_update_window(h2_proxy_session *session, + conn_rec *c, apr_off_t bytes) +{ + if (session->streams && !h2_ihash_is_empty(session->streams)) { + win_update_ctx ctx; + ctx.session = session; + ctx.c = c; + ctx.bytes = bytes; + ctx.updated = 0; + h2_ihash_iter(session->streams, win_update_iter, &ctx); + + if (!ctx.updated) { + /* could not find the stream any more, possibly closed, update + * the connection window at least */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + "h2_proxy_session(%s): win_update conn %ld bytes", + session->id, (long)bytes); + nghttp2_session_consume_connection(session->ngh2, (size_t)bytes); + } + } +} + diff --git a/modules/http2/h2_proxy_session.h b/modules/http2/h2_proxy_session.h new file mode 100644 index 0000000000..7078981c7a --- /dev/null +++ b/modules/http2/h2_proxy_session.h @@ -0,0 +1,111 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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 h2_proxy_session_h +#define h2_proxy_session_h + +#define H2_ALEN(a) (sizeof(a)/sizeof((a)[0])) + +#include + +struct h2_int_queue; +struct h2_ihash_t; + +typedef enum { + H2_PROXYS_ST_INIT, /* send initial SETTINGS, etc. */ + H2_PROXYS_ST_DONE, /* finished, connection close */ + H2_PROXYS_ST_IDLE, /* no streams to process */ + H2_PROXYS_ST_BUSY, /* read/write without stop */ + H2_PROXYS_ST_WAIT, /* waiting for tasks reporting back */ + H2_PROXYS_ST_LOCAL_SHUTDOWN, /* we announced GOAWAY */ + H2_PROXYS_ST_REMOTE_SHUTDOWN, /* client announced GOAWAY */ +} h2_proxys_state; + +typedef enum { + H2_PROXYS_EV_INIT, /* session was initialized */ + H2_PROXYS_EV_LOCAL_GOAWAY, /* we send a GOAWAY */ + H2_PROXYS_EV_REMOTE_GOAWAY, /* remote send us a GOAWAY */ + H2_PROXYS_EV_CONN_ERROR, /* connection error */ + H2_PROXYS_EV_PROTO_ERROR, /* protocol error */ + H2_PROXYS_EV_CONN_TIMEOUT, /* connection timeout */ + H2_PROXYS_EV_NO_IO, /* nothing has been read or written */ + H2_PROXYS_EV_STREAM_SUBMITTED, /* stream has been submitted */ + H2_PROXYS_EV_STREAM_DONE, /* stream has been finished */ + H2_PROXYS_EV_STREAM_RESUMED, /* stream signalled availability of headers/data */ + H2_PROXYS_EV_DATA_READ, /* connection data has been read */ + H2_PROXYS_EV_NGH2_DONE, /* nghttp2 wants neither read nor write anything */ + H2_PROXYS_EV_PRE_CLOSE, /* connection will close after this */ +} h2_proxys_event_t; + + +typedef struct h2_proxy_session h2_proxy_session; +typedef void h2_proxy_request_done(h2_proxy_session *s, request_rec *r, + int complete, int touched); + +struct h2_proxy_session { + const char *id; + conn_rec *c; + proxy_conn_rec *p_conn; + proxy_server_conf *conf; + apr_pool_t *pool; + nghttp2_session *ngh2; /* the nghttp2 session itself */ + + unsigned int aborted : 1; + + h2_proxy_request_done *done; + void *user_data; + + unsigned char window_bits_stream; + unsigned char window_bits_connection; + + h2_proxys_state state; + apr_interval_time_t wait_timeout; + + struct h2_ihash_t *streams; + struct h2_int_queue *suspended; + apr_size_t remote_max_concurrent; + int last_stream_id; /* last stream id processed by backend, or 0 */ + + apr_bucket_brigade *input; + apr_bucket_brigade *output; +}; + +h2_proxy_session *h2_proxy_session_setup(const char *id, proxy_conn_rec *p_conn, + proxy_server_conf *conf, + unsigned char window_bits_connection, + unsigned char window_bits_stream, + h2_proxy_request_done *done); + +apr_status_t h2_proxy_session_submit(h2_proxy_session *s, const char *url, + request_rec *r); + +/** + * Perform a step in processing the proxy session. Will return aftert + * one read/write cycle and indicate session status by status code. + * @param s the session to process + * @return APR_EAGAIN when processing needs to be invoked again + * APR_SUCCESS when all streams have been processed, session still live + * APR_EOF when the session has been terminated + */ +apr_status_t h2_proxy_session_process(h2_proxy_session *s); + +void h2_proxy_session_cleanup(h2_proxy_session *s, h2_proxy_request_done *done); + +void h2_proxy_session_update_window(h2_proxy_session *s, + conn_rec *c, apr_off_t bytes); + +#define H2_PROXY_REQ_URL_NOTE "h2-proxy-req-url" + +#endif /* h2_proxy_session_h */ diff --git a/modules/http2/mod_proxy_http2.c b/modules/http2/mod_proxy_http2.c new file mode 100644 index 0000000000..6722cb7333 --- /dev/null +++ b/modules/http2/mod_proxy_http2.c @@ -0,0 +1,639 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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 + +#include +#include +#include "mod_http2.h" + + +#include "mod_proxy_http2.h" +#include "h2_int_queue.h" +#include "h2_request.h" +#include "h2_util.h" +#include "h2_version.h" +#include "h2_proxy_session.h" + +static void register_hook(apr_pool_t *p); + +AP_DECLARE_MODULE(proxy_http2) = { + STANDARD20_MODULE_STUFF, + NULL, /* create per-directory config structure */ + NULL, /* merge per-directory config structures */ + NULL, /* create per-server config structure */ + NULL, /* merge per-server config structures */ + NULL, /* command apr_table_t */ + register_hook /* register hooks */ +}; + +/* Optional functions from mod_http2 */ +static int (*is_h2)(conn_rec *c); +static apr_status_t (*req_engine_push)(const char *name, request_rec *r, + http2_req_engine_init *einit); +static apr_status_t (*req_engine_pull)(h2_req_engine *engine, + apr_read_type_e block, + apr_uint32_t capacity, + request_rec **pr); +static void (*req_engine_done)(h2_req_engine *engine, conn_rec *r_conn); + +typedef struct h2_proxy_ctx { + conn_rec *owner; + apr_pool_t *pool; + request_rec *rbase; + server_rec *server; + const char *proxy_func; + char server_portstr[32]; + proxy_conn_rec *p_conn; + proxy_worker *worker; + proxy_server_conf *conf; + + h2_req_engine *engine; + const char *engine_id; + const char *engine_type; + apr_pool_t *engine_pool; + apr_uint32_t req_buffer_size; + request_rec *next; + apr_size_t capacity; + + unsigned standalone : 1; + unsigned is_ssl : 1; + unsigned flushall : 1; + + apr_status_t r_status; /* status of our first request work */ + h2_proxy_session *session; /* current http2 session against backend */ +} h2_proxy_ctx; + +static int h2_proxy_post_config(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *s) +{ + void *data = NULL; + const char *init_key = "mod_proxy_http2_init_counter"; + nghttp2_info *ngh2; + apr_status_t status = APR_SUCCESS; + (void)plog;(void)ptemp; + + apr_pool_userdata_get(&data, init_key, s->process->pool); + if ( data == NULL ) { + apr_pool_userdata_set((const void *)1, init_key, + apr_pool_cleanup_null, s->process->pool); + return APR_SUCCESS; + } + + ngh2 = nghttp2_version(0); + ap_log_error( APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(03349) + "mod_proxy_http2 (v%s, nghttp2 %s), initializing...", + MOD_HTTP2_VERSION, ngh2? ngh2->version_str : "unknown"); + + is_h2 = APR_RETRIEVE_OPTIONAL_FN(http2_is_h2); + req_engine_push = APR_RETRIEVE_OPTIONAL_FN(http2_req_engine_push); + req_engine_pull = APR_RETRIEVE_OPTIONAL_FN(http2_req_engine_pull); + req_engine_done = APR_RETRIEVE_OPTIONAL_FN(http2_req_engine_done); + + /* we need all of them */ + if (!req_engine_push || !req_engine_pull || !req_engine_done) { + req_engine_push = NULL; + req_engine_pull = NULL; + req_engine_done = NULL; + } + + return status; +} + +/** + * canonicalize the url into the request, if it is meant for us. + * slightly modified copy from mod_http + */ +static int proxy_http2_canon(request_rec *r, char *url) +{ + char *host, *path, sport[7]; + char *search = NULL; + const char *err; + const char *scheme; + const char *http_scheme; + apr_port_t port, def_port; + + /* ap_port_of_scheme() */ + if (strncasecmp(url, "h2c:", 4) == 0) { + url += 4; + scheme = "h2c"; + http_scheme = "http"; + } + else if (strncasecmp(url, "h2:", 3) == 0) { + url += 3; + scheme = "h2"; + http_scheme = "https"; + } + else { + return DECLINED; + } + port = def_port = ap_proxy_port_of_scheme(http_scheme); + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "HTTP2: canonicalising URL %s", url); + + /* do syntatic check. + * We break the URL into host, port, path, search + */ + err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port); + if (err) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(03350) + "error parsing URL %s: %s", url, err); + return HTTP_BAD_REQUEST; + } + + /* + * now parse path/search args, according to rfc1738: + * process the path. + * + * In a reverse proxy, our URL has been processed, so canonicalise + * unless proxy-nocanon is set to say it's raw + * In a forward proxy, we have and MUST NOT MANGLE the original. + */ + switch (r->proxyreq) { + default: /* wtf are we doing here? */ + case PROXYREQ_REVERSE: + if (apr_table_get(r->notes, "proxy-nocanon")) { + path = url; /* this is the raw path */ + } + else { + path = ap_proxy_canonenc(r->pool, url, strlen(url), + enc_path, 0, r->proxyreq); + search = r->args; + } + break; + case PROXYREQ_PROXY: + path = url; + break; + } + + if (path == NULL) { + return HTTP_BAD_REQUEST; + } + + if (port != def_port) { + apr_snprintf(sport, sizeof(sport), ":%d", port); + } + else { + sport[0] = '\0'; + } + + if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */ + host = apr_pstrcat(r->pool, "[", host, "]", NULL); + } + r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "://", host, sport, + "/", path, (search) ? "?" : "", (search) ? search : "", NULL); + return OK; +} + +static void out_consumed(void *baton, conn_rec *c, apr_off_t bytes) +{ + h2_proxy_ctx *ctx = baton; + + if (ctx->session) { + h2_proxy_session_update_window(ctx->session, c, bytes); + } +} + +static apr_status_t proxy_engine_init(h2_req_engine *engine, + const char *id, + const char *type, + apr_pool_t *pool, + apr_uint32_t req_buffer_size, + request_rec *r, + http2_output_consumed **pconsumed, + void **pctx) +{ + h2_proxy_ctx *ctx = ap_get_module_config(r->connection->conn_config, + &proxy_http2_module); + if (ctx) { + conn_rec *c = ctx->owner; + h2_proxy_ctx *nctx; + + /* we need another lifetime for this. If we do not host + * an engine, the context lives in r->pool. Since we expect + * to server more than r, we need to live longer */ + nctx = apr_pcalloc(pool, sizeof(*nctx)); + if (nctx == NULL) { + return APR_ENOMEM; + } + memcpy(nctx, ctx, sizeof(*nctx)); + ctx = nctx; + ctx->pool = pool; + ctx->engine = engine; + ctx->engine_id = id; + ctx->engine_type = type; + ctx->engine_pool = pool; + ctx->req_buffer_size = req_buffer_size; + ctx->capacity = 100; + + ap_set_module_config(c->conn_config, &proxy_http2_module, ctx); + + *pconsumed = out_consumed; + *pctx = ctx; + return APR_SUCCESS; + } + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(03368) + "h2_proxy_session, engine init, no ctx found"); + return APR_ENOTIMPL; +} + +static apr_status_t add_request(h2_proxy_session *session, request_rec *r) +{ + h2_proxy_ctx *ctx = session->user_data; + const char *url; + apr_status_t status; + + url = apr_table_get(r->notes, H2_PROXY_REQ_URL_NOTE); + apr_table_setn(r->notes, "proxy-source-port", apr_psprintf(r->pool, "%hu", + ctx->p_conn->connection->local_addr->port)); + status = h2_proxy_session_submit(session, url, r); + if (status != OK) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, r->connection, APLOGNO(03351) + "pass request body failed to %pI (%s) from %s (%s)", + ctx->p_conn->addr, ctx->p_conn->hostname ? + ctx->p_conn->hostname: "", session->c->client_ip, + session->c->remote_host ? session->c->remote_host: ""); + } + return status; +} + +static void request_done(h2_proxy_session *session, request_rec *r, + int complete, int touched) +{ + h2_proxy_ctx *ctx = session->user_data; + const char *task_id = apr_table_get(r->connection->notes, H2_TASK_ID_NOTE); + + if (!complete && !touched) { + /* untouched request, need rescheduling */ + if (req_engine_push && is_h2 && is_h2(ctx->owner)) { + if (req_engine_push(ctx->engine_type, r, NULL) == APR_SUCCESS) { + /* push to engine */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection, + APLOGNO(03369) + "h2_proxy_session(%s): rescheduled request %s", + ctx->engine_id, task_id); + return; + } + } + } + + if (r == ctx->rbase && complete) { + ctx->r_status = APR_SUCCESS; + } + + if (complete) { + if (req_engine_done && ctx->engine) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection, + APLOGNO(03370) + "h2_proxy_session(%s): finished request %s", + ctx->engine_id, task_id); + req_engine_done(ctx->engine, r->connection); + } + } + else { + if (req_engine_done && ctx->engine) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, r->connection, + APLOGNO(03371) + "h2_proxy_session(%s): failed request %s", + ctx->engine_id, task_id); + req_engine_done(ctx->engine, r->connection); + } + } +} + +static apr_status_t next_request(h2_proxy_ctx *ctx, int before_leave) +{ + if (ctx->next) { + return APR_SUCCESS; + } + else if (req_engine_pull && ctx->engine) { + apr_status_t status; + status = req_engine_pull(ctx->engine, before_leave? + APR_BLOCK_READ: APR_NONBLOCK_READ, + ctx->capacity, &ctx->next); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, ctx->owner, + "h2_proxy_engine(%s): pulled request %s", + ctx->engine_id, + (ctx->next? ctx->next->the_request : "NULL")); + return APR_STATUS_IS_EAGAIN(status)? APR_SUCCESS : status; + } + return APR_EOF; +} + +static apr_status_t proxy_engine_run(h2_proxy_ctx *ctx) { + apr_status_t status = OK; + + /* Step Four: Send the Request in a new HTTP/2 stream and + * loop until we got the response or encounter errors. + */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, ctx->owner, + "eng(%s): setup session", ctx->engine_id); + ctx->session = h2_proxy_session_setup(ctx->engine_id, ctx->p_conn, ctx->conf, + 30, h2_log2(ctx->req_buffer_size), + request_done); + if (!ctx->session) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, + APLOGNO(03372) "session unavailable"); + return HTTP_SERVICE_UNAVAILABLE; + } + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, APLOGNO(03373) + "eng(%s): run session %s", ctx->engine_id, ctx->session->id); + ctx->session->user_data = ctx; + + while (1) { + if (ctx->next) { + add_request(ctx->session, ctx->next); + ctx->next = NULL; + } + + status = h2_proxy_session_process(ctx->session); + + if (status == APR_SUCCESS) { + apr_status_t s2; + /* ongoing processing, call again */ + if (ctx->session->remote_max_concurrent > 0 + && ctx->session->remote_max_concurrent != ctx->capacity) { + ctx->capacity = ctx->session->remote_max_concurrent; + } + s2 = next_request(ctx, 0); + if (s2 == APR_ECONNABORTED) { + /* master connection gone */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, s2, ctx->owner, + APLOGNO(03374) "eng(%s): pull request", + ctx->engine_id); + status = s2; + break; + } + if (!ctx->next && h2_ihash_is_empty(ctx->session->streams)) { + break; + } + } + else { + /* end of processing, maybe error */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, + APLOGNO(03375) "eng(%s): end of session run", + ctx->engine_id); + /* + * Any open stream of that session needs to + * a) be reopened on the new session iff safe to do so + * b) reported as done (failed) otherwise + */ + h2_proxy_session_cleanup(ctx->session, request_done); + break; + } + } + + ctx->session->user_data = NULL; + ctx->session = NULL; + + return status; +} + +static h2_proxy_ctx *push_request_somewhere(h2_proxy_ctx *ctx) +{ + conn_rec *c = ctx->owner; + const char *engine_type, *hostname; + + hostname = (ctx->p_conn->ssl_hostname? + ctx->p_conn->ssl_hostname : ctx->p_conn->hostname); + engine_type = apr_psprintf(ctx->pool, "proxy_http2 %s%s", hostname, + ctx->server_portstr); + + if (c->master && req_engine_push && ctx->next && is_h2 && is_h2(c)) { + /* If we are have req_engine capabilities, push the handling of this + * request (e.g. slave connection) to a proxy_http2 engine which + * uses the same backend. We may be called to create an engine + * ourself. */ + if (req_engine_push(engine_type, ctx->next, proxy_engine_init) + == APR_SUCCESS) { + /* to renew the lifetime, we might have set a new ctx */ + ctx = ap_get_module_config(c->conn_config, &proxy_http2_module); + if (ctx->engine == NULL) { + /* Another engine instance has taken over processing of this + * request. */ + ctx->r_status = SUSPENDED; + ctx->next = NULL; + return ctx; + } + } + } + + if (!ctx->engine) { + /* No engine was available or has been initialized, handle this + * request just by ourself. */ + ctx->engine_id = apr_psprintf(ctx->pool, "eng-proxy-%ld", c->id); + ctx->engine_type = engine_type; + ctx->engine_pool = ctx->pool; + ctx->req_buffer_size = (32*1024); + ctx->standalone = 1; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_proxy_http2(%ld): setup standalone engine for type %s", + c->id, engine_type); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "H2: hosting engine %s", ctx->engine_id); + } + return ctx; +} + +static int proxy_http2_handler(request_rec *r, + proxy_worker *worker, + proxy_server_conf *conf, + char *url, + const char *proxyname, + apr_port_t proxyport) +{ + const char *proxy_func; + char *locurl = url, *u; + apr_size_t slen; + int is_ssl = 0; + apr_status_t status; + h2_proxy_ctx *ctx; + apr_uri_t uri; + int reconnected = 0; + + /* find the scheme */ + if ((url[0] != 'h' && url[0] != 'H') || url[1] != '2') { + return DECLINED; + } + u = strchr(url, ':'); + if (u == NULL || u[1] != '/' || u[2] != '/' || u[3] == '\0') { + return DECLINED; + } + slen = (u - url); + switch(slen) { + case 2: + proxy_func = "H2"; + is_ssl = 1; + break; + case 3: + if (url[2] != 'c' && url[2] != 'C') { + return DECLINED; + } + proxy_func = "H2C"; + break; + default: + return DECLINED; + } + ctx = apr_pcalloc(r->pool, sizeof(*ctx)); + ctx->owner = r->connection; + ctx->pool = r->pool; + ctx->rbase = r; + ctx->server = r->server; + ctx->proxy_func = proxy_func; + ctx->is_ssl = is_ssl; + ctx->worker = worker; + ctx->conf = conf; + ctx->flushall = apr_table_get(r->subprocess_env, "proxy-flushall")? 1 : 0; + ctx->r_status = HTTP_SERVICE_UNAVAILABLE; + ctx->next = r; + r = NULL; + ap_set_module_config(ctx->owner->conn_config, &proxy_http2_module, ctx); + + /* scheme says, this is for us. */ + apr_table_setn(ctx->rbase->notes, H2_PROXY_REQ_URL_NOTE, url); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, ctx->rbase, + "H2: serving URL %s", url); + +run_connect: + /* Get a proxy_conn_rec from the worker, might be a new one, might + * be one still open from another request, or it might fail if the + * worker is stopped or in error. */ + if ((status = ap_proxy_acquire_connection(ctx->proxy_func, &ctx->p_conn, + ctx->worker, ctx->server)) != OK) { + goto cleanup; + } + + ctx->p_conn->is_ssl = ctx->is_ssl; + if (ctx->is_ssl) { + /* If there is still some data on an existing ssl connection, now + * would be a good timne to get rid of it. */ + ap_proxy_ssl_connection_cleanup(ctx->p_conn, ctx->rbase); + } + + /* Step One: Determine the URL to connect to (might be a proxy), + * initialize the backend accordingly and determine the server + * port string we can expect in responses. */ + if ((status = ap_proxy_determine_connection(ctx->pool, ctx->rbase, conf, worker, + ctx->p_conn, &uri, &locurl, + proxyname, proxyport, + ctx->server_portstr, + sizeof(ctx->server_portstr))) != OK) { + goto cleanup; + } + + /* If we are not already hosting an engine, try to push the request + * to an already existing engine or host a new engine here. */ + if (!ctx->engine) { + ctx = push_request_somewhere(ctx); + if (ctx->r_status == SUSPENDED) { + /* request was pushed to another engine */ + goto cleanup; + } + } + + /* Step Two: Make the Connection (or check that an already existing + * socket is still usable). On success, we have a socket connected to + * backend->hostname. */ + if (ap_proxy_connect_backend(ctx->proxy_func, ctx->p_conn, ctx->worker, + ctx->server)) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, ctx->owner, APLOGNO(03352) + "H2: failed to make connection to backend: %s", + ctx->p_conn->hostname); + goto cleanup; + } + + /* Step Three: Create conn_rec for the socket we have open now. */ + if (!ctx->p_conn->connection) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, APLOGNO(03353) + "setup new connection: is_ssl=%d %s %s %s", + ctx->p_conn->is_ssl, ctx->p_conn->ssl_hostname, + locurl, ctx->p_conn->hostname); + if ((status = ap_proxy_connection_create(ctx->proxy_func, ctx->p_conn, + ctx->owner, + ctx->server)) != OK) { + goto cleanup; + } + + /* + * On SSL connections set a note on the connection what CN is + * requested, such that mod_ssl can check if it is requested to do + * so. + */ + if (ctx->p_conn->ssl_hostname) { + apr_table_setn(ctx->p_conn->connection->notes, + "proxy-request-hostname", ctx->p_conn->ssl_hostname); + } + + if (ctx->is_ssl) { + apr_table_setn(ctx->p_conn->connection->notes, + "proxy-request-alpn-protos", "h2"); + } + } + +run_session: + status = proxy_engine_run(ctx); + if (status == APR_SUCCESS) { + /* session and connection still ok */ + if (next_request(ctx, 1) == APR_SUCCESS) { + /* more requests, run again */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, APLOGNO(03376) + "run_session, again"); + goto run_session; + } + /* done */ + ctx->engine = NULL; + } + +cleanup: + if (!reconnected && ctx->engine && next_request(ctx, 1) == APR_SUCCESS) { + /* Still more to do, tear down old conn and start over */ + if (ctx->p_conn) { + ctx->p_conn->close = 1; + /*proxy_run_detach_backend(r, ctx->p_conn);*/ + ap_proxy_release_connection(ctx->proxy_func, ctx->p_conn, ctx->server); + ctx->p_conn = NULL; + } + reconnected = 1; /* we do this only once, then fail */ + goto run_connect; + } + + if (ctx->p_conn) { + if (status != APR_SUCCESS) { + /* close socket when errors happened or session shut down (EOF) */ + ctx->p_conn->close = 1; + } +/* proxy_run_detach_backend(ctx->rbase, ctx->p_conn);*/ + ap_proxy_release_connection(ctx->proxy_func, ctx->p_conn, ctx->server); + ctx->p_conn = NULL; + } + + ap_set_module_config(ctx->owner->conn_config, &proxy_http2_module, NULL); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, + APLOGNO(03377) "leaving handler"); + return ctx->r_status; +} + +static void register_hook(apr_pool_t *p) +{ + ap_hook_post_config(h2_proxy_post_config, NULL, NULL, APR_HOOK_MIDDLE); + + proxy_hook_scheme_handler(proxy_http2_handler, NULL, NULL, APR_HOOK_FIRST); + proxy_hook_canon_handler(proxy_http2_canon, NULL, NULL, APR_HOOK_FIRST); +} + diff --git a/modules/http2/mod_proxy_http2.h b/modules/http2/mod_proxy_http2.h new file mode 100644 index 0000000000..7da84f0fce --- /dev/null +++ b/modules/http2/mod_proxy_http2.h @@ -0,0 +1,20 @@ +/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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_PROXY_HTTP2_H__ +#define __MOD_PROXY_HTTP2_H__ + + +#endif