diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-25 04:41:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-25 04:41:27 +0000 |
commit | c54018b07a9085c0a3aedbc2bd01a85a3b3e20cf (patch) | |
tree | f6e1d6fcf9f6db3794c418b2f89ecf9e08ff41c8 /modules/http2 | |
parent | Adding debian version 2.4.38-3+deb10u10. (diff) | |
download | apache2-c54018b07a9085c0a3aedbc2bd01a85a3b3e20cf.tar.xz apache2-c54018b07a9085c0a3aedbc2bd01a85a3b3e20cf.zip |
Merging upstream version 2.4.59.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/http2')
62 files changed, 9971 insertions, 10173 deletions
diff --git a/modules/http2/.gitignore b/modules/http2/.gitignore deleted file mode 100644 index ca49620..0000000 --- a/modules/http2/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -*.xcuserstate -sandbox/httpd/packages/httpd-2.4.x.tar.gz -sandbox/test/conf/sites/mod-h2.greenbytes.de.conf -*.o -*.slo -*.lo -*.la -*.pcap -.libs -.configured -.deps -compile -aclocal.m4 -autom4te.cache -autoscan.log -config.guess -config.log -config.status -config.sub -config.h -config.h.in -config.h.in~ -configure -configure.scan -depcomp -install-sh -libtool -ltmain.sh -missing -stamp-h1 -Makefile.in -Makefile -mod_h2-*.tar.gz -mod_h2/h2_version.h -m4 diff --git a/modules/http2/config2.m4 b/modules/http2/config2.m4 index e8cefe3..c4579c4 100644 --- a/modules/http2/config2.m4 +++ b/modules/http2/config2.m4 @@ -19,27 +19,25 @@ APACHE_MODPATH_INIT(http2) dnl # list of module object files http2_objs="dnl mod_http2.lo dnl -h2_alt_svc.lo dnl h2_bucket_beam.lo dnl h2_bucket_eos.lo dnl +h2_c1.lo dnl +h2_c1_io.lo dnl +h2_c2.lo dnl +h2_c2_filter.lo dnl h2_config.lo dnl -h2_conn.lo dnl -h2_conn_io.lo dnl -h2_ctx.lo dnl -h2_filter.lo dnl -h2_from_h1.lo dnl -h2_h2.lo dnl +h2_conn_ctx.lo dnl h2_headers.lo dnl h2_mplx.lo dnl -h2_ngn_shed.lo dnl +h2_protocol.lo dnl h2_push.lo dnl h2_request.lo dnl h2_session.lo dnl h2_stream.lo dnl h2_switch.lo dnl -h2_task.lo dnl h2_util.lo dnl h2_workers.lo dnl +h2_ws.lo dnl " dnl @@ -164,6 +162,12 @@ dnl # nghttp2 >= 1.14.0: invalid header callback dnl # nghttp2 >= 1.15.0: get/set stream window sizes AC_CHECK_FUNCS([nghttp2_session_get_stream_local_window_size], [APR_ADDTO(MOD_CPPFLAGS, ["-DH2_NG2_LOCAL_WIN_SIZE"])], []) +dnl # nghttp2 >= 1.15.0: don't keep info on closed streams + AC_CHECK_FUNCS([nghttp2_option_set_no_closed_streams], + [APR_ADDTO(MOD_CPPFLAGS, ["-DH2_NG2_NO_CLOSED_STREAMS"])], []) +dnl # nghttp2 >= 1.50.0: rfc9113 leading/trailing whitespec strictness + AC_CHECK_FUNCS([nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation], + [APR_ADDTO(MOD_CPPFLAGS, ["-DH2_NG2_RFC9113_STRICTNESS"])], []) else AC_MSG_WARN([nghttp2 version is too old]) fi diff --git a/modules/http2/h2.h b/modules/http2/h2.h index 38b4019..f496a6d 100644 --- a/modules/http2/h2.h +++ b/modules/http2/h2.h @@ -17,6 +17,38 @@ #ifndef __mod_h2__h2__ #define __mod_h2__h2__ +#include <apr_version.h> +#include <ap_mmn.h> + +#include <nghttp2/nghttp2ver.h> + +struct h2_session; +struct h2_stream; + +/* + * When apr pollsets can poll file descriptors (e.g. pipes), + * we use it for polling stream input/output. + */ +#ifdef H2_NO_PIPES +#define H2_USE_PIPES 0 +#else +#define H2_USE_PIPES (APR_FILES_AS_SOCKETS && APR_VERSION_AT_LEAST(1,6,0)) +#endif + +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 129) +#define H2_USE_POLLFD_FROM_CONN 1 +#else +#define H2_USE_POLLFD_FROM_CONN 0 +#endif + +/* WebSockets support requires apr 1.7.0 for apr_encode.h, plus the + * WebSockets features of nghttp2 1.34.0 and later. */ +#if H2_USE_PIPES && defined(NGHTTP2_VERSION_NUM) && NGHTTP2_VERSION_NUM >= 0x012200 && APR_VERSION_AT_LEAST(1,7,0) +#define H2_USE_WEBSOCKETS 1 +#else +#define H2_USE_WEBSOCKETS 0 +#endif + /** * The magic PRIamble of RFC 7540 that is always sent when starting * a h2 communication. @@ -46,14 +78,16 @@ extern const char *H2_MAGIC_TOKEN; #define H2_HEADER_AUTH_LEN 10 #define H2_HEADER_PATH ":path" #define H2_HEADER_PATH_LEN 5 +#define H2_HEADER_PROTO ":protocol" +#define H2_HEADER_PROTO_LEN 9 #define H2_CRLF "\r\n" -/* Max data size to write so it fits inside a TLS record */ -#define H2_DATA_CHUNK_SIZE ((16*1024) - 100 - 9) - /* Size of the frame header itself in HTTP/2 */ #define H2_FRAME_HDR_LEN 9 +/* Max data size to write so it fits inside a TLS record */ +#define H2_DATA_CHUNK_SIZE ((16*1024) - 100 - H2_FRAME_HDR_LEN) + /* Maximum number of padding bytes in a frame, rfc7540 */ #define H2_MAX_PADLEN 256 /* Initial default window size, RFC 7540 ch. 6.5.2 */ @@ -89,7 +123,7 @@ typedef enum { H2_SESSION_ST_DONE, /* finished, connection close */ H2_SESSION_ST_IDLE, /* nothing to write, expecting data inc */ H2_SESSION_ST_BUSY, /* read/write without stop */ - H2_SESSION_ST_WAIT, /* waiting for tasks reporting back */ + H2_SESSION_ST_WAIT, /* waiting for c1 incoming + c2s output */ H2_SESSION_ST_CLEANUP, /* pool is being cleaned up */ } h2_session_state; @@ -99,6 +133,7 @@ typedef struct h2_session_props { int emitted_count; /* the number of local streams sent */ int emitted_max; /* the highest local stream id sent */ int error; /* the last session error encountered */ + const char *error_msg; /* the short message given on the error */ unsigned int accepting : 1; /* if the session is accepting new streams */ unsigned int shutdown : 1; /* if the final GOAWAY has been sent */ } h2_session_props; @@ -120,7 +155,9 @@ typedef enum { H2_SEV_CLOSED_R, H2_SEV_CANCELLED, H2_SEV_EOS_SENT, + H2_SEV_IN_ERROR, H2_SEV_IN_DATA_PENDING, + H2_SEV_OUT_C1_BLOCK, } h2_stream_event_t; @@ -129,38 +166,46 @@ typedef enum { * become a request_rec to be handled by soemone. */ typedef struct h2_request h2_request; - struct h2_request { const char *method; /* pseudo header values, see ch. 8.1.2.3 */ const char *scheme; const char *authority; const char *path; + const char *protocol; apr_table_t *headers; apr_time_t request_time; - unsigned int chunked : 1; /* iff requst body needs to be forwarded as chunked */ - unsigned int serialize : 1; /* iff this request is written in HTTP/1.1 serialization */ apr_off_t raw_bytes; /* RAW network bytes that generated this request - if known. */ + int http_status; /* Store a possible HTTP status code that gets + * defined before creating the dummy HTTP/1.1 + * request e.g. due to an error already + * detected. + */ }; -typedef struct h2_headers h2_headers; - -struct h2_headers { - int status; - apr_table_t *headers; - apr_table_t *notes; - apr_off_t raw_bytes; /* RAW network bytes that generated this request - if known. */ -}; +/* + * A possible HTTP status code is not defined yet. See the http_status field + * in struct h2_request above for further explanation. + */ +#define H2_HTTP_STATUS_UNSET (0) typedef apr_status_t h2_io_data_cb(void *ctx, const char *data, apr_off_t len); -typedef int h2_stream_pri_cmp(int stream_id1, int stream_id2, void *ctx); - -/* Note key to attach connection task id to conn_rec/request_rec instances */ +typedef int h2_stream_pri_cmp_fn(int stream_id1, int stream_id2, void *session); +typedef struct h2_stream *h2_stream_get_fn(struct h2_session *session, int stream_id); -#define H2_TASK_ID_NOTE "http2-task-id" -#define H2_FILTER_DEBUG_NOTE "http2-debug" +/* Note key to attach stream id to conn_rec/request_rec instances */ #define H2_HDR_CONFORMANCE "http2-hdr-conformance" #define H2_HDR_CONFORMANCE_UNSAFE "unsafe" +#define H2_PUSH_MODE_NOTE "http2-push-mode" + + +#if AP_MODULE_MAGIC_AT_LEAST(20211221, 6) +#define AP_HAS_RESPONSE_BUCKETS 1 + +#else /* AP_MODULE_MAGIC_AT_LEAST(20211221, 6) */ +#define AP_HAS_RESPONSE_BUCKETS 0 + +#endif /* else AP_MODULE_MAGIC_AT_LEAST(20211221, 6) */ #endif /* defined(__mod_h2__h2__) */ diff --git a/modules/http2/h2_alt_svc.c b/modules/http2/h2_alt_svc.c deleted file mode 100644 index 295a16d..0000000 --- a/modules/http2/h2_alt_svc.c +++ /dev/null @@ -1,131 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <apr_strings.h> -#include <httpd.h> -#include <http_core.h> -#include <http_connection.h> -#include <http_protocol.h> -#include <http_log.h> - -#include "h2_private.h" -#include "h2_alt_svc.h" -#include "h2_ctx.h" -#include "h2_config.h" -#include "h2_h2.h" -#include "h2_util.h" - -static int h2_alt_svc_handler(request_rec *r); - -void h2_alt_svc_register_hooks(void) -{ - ap_hook_post_read_request(h2_alt_svc_handler, NULL, NULL, APR_HOOK_MIDDLE); -} - -/** - * Parse an Alt-Svc specifier as described in "HTTP Alternative Services" - * (https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-04) - * with the following changes: - * - do not percent encode token values - * - do not use quotation marks - */ -h2_alt_svc *h2_alt_svc_parse(const char *s, apr_pool_t *pool) -{ - const char *sep = ap_strchr_c(s, '='); - if (sep) { - const char *alpn = apr_pstrmemdup(pool, s, sep - s); - const char *host = NULL; - int port = 0; - s = sep + 1; - sep = ap_strchr_c(s, ':'); /* mandatory : */ - if (sep) { - if (sep != s) { /* optional host */ - host = apr_pstrmemdup(pool, s, sep - s); - } - s = sep + 1; - if (*s) { /* must be a port number */ - port = (int)apr_atoi64(s); - if (port > 0 && port < (0x1 << 16)) { - h2_alt_svc *as = apr_pcalloc(pool, sizeof(*as)); - as->alpn = alpn; - as->host = host; - as->port = port; - return as; - } - } - } - } - return NULL; -} - -#define h2_alt_svc_IDX(list, i) ((h2_alt_svc**)(list)->elts)[i] - -static int h2_alt_svc_handler(request_rec *r) -{ - const h2_config *cfg; - int i; - - if (r->connection->keepalives > 0) { - /* Only announce Alt-Svc on the first response */ - return DECLINED; - } - - if (h2_ctx_rget(r)) { - return DECLINED; - } - - cfg = h2_config_sget(r->server); - if (r->hostname && cfg && cfg->alt_svcs && cfg->alt_svcs->nelts > 0) { - const char *alt_svc_used = apr_table_get(r->headers_in, "Alt-Svc-Used"); - if (!alt_svc_used) { - /* We have alt-svcs defined and client is not already using - * one, announce the services that were configured and match. - * The security of this connection determines if we allow - * other host names or ports only. - */ - const char *alt_svc = ""; - const char *svc_ma = ""; - int secure = h2_h2_is_tls(r->connection); - int ma = h2_config_geti(cfg, H2_CONF_ALT_SVC_MAX_AGE); - if (ma >= 0) { - svc_ma = apr_psprintf(r->pool, "; ma=%d", ma); - } - ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03043) - "h2_alt_svc: announce %s for %s:%d", - (secure? "secure" : "insecure"), - r->hostname, (int)r->server->port); - for (i = 0; i < cfg->alt_svcs->nelts; ++i) { - h2_alt_svc *as = h2_alt_svc_IDX(cfg->alt_svcs, i); - const char *ahost = as->host; - if (ahost && !apr_strnatcasecmp(ahost, r->hostname)) { - ahost = NULL; - } - if (secure || !ahost) { - alt_svc = apr_psprintf(r->pool, "%s%s%s=\"%s:%d\"%s", - alt_svc, - (*alt_svc? ", " : ""), as->alpn, - ahost? ahost : "", as->port, - svc_ma); - } - } - if (*alt_svc) { - apr_table_setn(r->headers_out, "Alt-Svc", alt_svc); - } - } - } - - return DECLINED; -} diff --git a/modules/http2/h2_bucket_beam.c b/modules/http2/h2_bucket_beam.c index f79cbe3..6978254 100644 --- a/modules/http2/h2_bucket_beam.c +++ b/modules/http2/h2_bucket_beam.c @@ -24,261 +24,123 @@ #include <httpd.h> #include <http_protocol.h> +#include <http_request.h> #include <http_log.h> #include "h2_private.h" +#include "h2_conn_ctx.h" +#include "h2_headers.h" #include "h2_util.h" #include "h2_bucket_beam.h" -static void h2_beam_emitted(h2_bucket_beam *beam, h2_beam_proxy *proxy); -#define H2_BPROXY_NEXT(e) APR_RING_NEXT((e), link) -#define H2_BPROXY_PREV(e) APR_RING_PREV((e), link) -#define H2_BPROXY_REMOVE(e) APR_RING_REMOVE((e), link) - -#define H2_BPROXY_LIST_INIT(b) APR_RING_INIT(&(b)->list, h2_beam_proxy, link); -#define H2_BPROXY_LIST_SENTINEL(b) APR_RING_SENTINEL(&(b)->list, h2_beam_proxy, link) -#define H2_BPROXY_LIST_EMPTY(b) APR_RING_EMPTY(&(b)->list, h2_beam_proxy, link) -#define H2_BPROXY_LIST_FIRST(b) APR_RING_FIRST(&(b)->list) -#define H2_BPROXY_LIST_LAST(b) APR_RING_LAST(&(b)->list) -#define H2_PROXY_BLIST_INSERT_HEAD(b, e) do { \ - h2_beam_proxy *ap__b = (e); \ - APR_RING_INSERT_HEAD(&(b)->list, ap__b, h2_beam_proxy, link); \ +#define H2_BLIST_INIT(b) APR_RING_INIT(&(b)->list, apr_bucket, link); +#define H2_BLIST_SENTINEL(b) APR_RING_SENTINEL(&(b)->list, apr_bucket, link) +#define H2_BLIST_EMPTY(b) APR_RING_EMPTY(&(b)->list, apr_bucket, link) +#define H2_BLIST_FIRST(b) APR_RING_FIRST(&(b)->list) +#define H2_BLIST_LAST(b) APR_RING_LAST(&(b)->list) +#define H2_BLIST_INSERT_HEAD(b, e) do { \ + apr_bucket *ap__b = (e); \ + APR_RING_INSERT_HEAD(&(b)->list, ap__b, apr_bucket, link); \ } while (0) -#define H2_BPROXY_LIST_INSERT_TAIL(b, e) do { \ - h2_beam_proxy *ap__b = (e); \ - APR_RING_INSERT_TAIL(&(b)->list, ap__b, h2_beam_proxy, link); \ +#define H2_BLIST_INSERT_TAIL(b, e) do { \ + apr_bucket *ap__b = (e); \ + APR_RING_INSERT_TAIL(&(b)->list, ap__b, apr_bucket, link); \ } while (0) -#define H2_BPROXY_LIST_CONCAT(a, b) do { \ - APR_RING_CONCAT(&(a)->list, &(b)->list, h2_beam_proxy, link); \ +#define H2_BLIST_CONCAT(a, b) do { \ + APR_RING_CONCAT(&(a)->list, &(b)->list, apr_bucket, link); \ } while (0) -#define H2_BPROXY_LIST_PREPEND(a, b) do { \ - APR_RING_PREPEND(&(a)->list, &(b)->list, h2_beam_proxy, link); \ +#define H2_BLIST_PREPEND(a, b) do { \ + APR_RING_PREPEND(&(a)->list, &(b)->list, apr_bucket, link); \ } while (0) -/******************************************************************************* - * beam bucket with reference to beam and bucket it represents - ******************************************************************************/ - -const apr_bucket_type_t h2_bucket_type_beam; - -#define H2_BUCKET_IS_BEAM(e) (e->type == &h2_bucket_type_beam) - -struct h2_beam_proxy { - apr_bucket_refcount refcount; - APR_RING_ENTRY(h2_beam_proxy) link; - h2_bucket_beam *beam; - apr_bucket *bsender; - apr_size_t n; -}; - -static const char Dummy = '\0'; - -static apr_status_t beam_bucket_read(apr_bucket *b, const char **str, - apr_size_t *len, apr_read_type_e block) -{ - h2_beam_proxy *d = b->data; - if (d->bsender) { - const char *data; - apr_status_t status = apr_bucket_read(d->bsender, &data, len, block); - if (status == APR_SUCCESS) { - *str = data + b->start; - *len = b->length; - } - return status; - } - *str = &Dummy; - *len = 0; - return APR_ECONNRESET; -} - -static void beam_bucket_destroy(void *data) -{ - h2_beam_proxy *d = data; - - if (apr_bucket_shared_destroy(d)) { - /* When the beam gets destroyed before this bucket, it will - * NULLify its reference here. This is not protected by a mutex, - * so it will not help with race conditions. - * But it lets us shut down memory pool with circulare beam - * references. */ - if (d->beam) { - h2_beam_emitted(d->beam, d); - } - apr_bucket_free(d); - } -} - -static apr_bucket * h2_beam_bucket_make(apr_bucket *b, - h2_bucket_beam *beam, - apr_bucket *bsender, apr_size_t n) -{ - h2_beam_proxy *d; - - d = apr_bucket_alloc(sizeof(*d), b->list); - H2_BPROXY_LIST_INSERT_TAIL(&beam->proxies, d); - d->beam = beam; - d->bsender = bsender; - d->n = n; - - b = apr_bucket_shared_make(b, d, 0, bsender? bsender->length : 0); - b->type = &h2_bucket_type_beam; - - return b; -} - -static apr_bucket *h2_beam_bucket_create(h2_bucket_beam *beam, - apr_bucket *bsender, - apr_bucket_alloc_t *list, - apr_size_t n) -{ - apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); - - APR_BUCKET_INIT(b); - b->free = apr_bucket_free; - b->list = list; - return h2_beam_bucket_make(b, beam, bsender, n); -} - -const apr_bucket_type_t h2_bucket_type_beam = { - "BEAM", 5, APR_BUCKET_DATA, - beam_bucket_destroy, - beam_bucket_read, - apr_bucket_setaside_noop, - apr_bucket_shared_split, - apr_bucket_shared_copy -}; - -/******************************************************************************* - * h2_blist, a brigade without allocations - ******************************************************************************/ - -static apr_array_header_t *beamers; +static int buffer_is_empty(h2_bucket_beam *beam); +static apr_off_t get_buffered_data_len(h2_bucket_beam *beam); -static apr_status_t cleanup_beamers(void *dummy) +static int h2_blist_count(h2_blist *blist) { - (void)dummy; - beamers = NULL; - return APR_SUCCESS; -} - -void h2_register_bucket_beamer(h2_bucket_beamer *beamer) -{ - if (!beamers) { - apr_pool_cleanup_register(apr_hook_global_pool, NULL, - cleanup_beamers, apr_pool_cleanup_null); - beamers = apr_array_make(apr_hook_global_pool, 10, - sizeof(h2_bucket_beamer*)); - } - APR_ARRAY_PUSH(beamers, h2_bucket_beamer*) = beamer; -} - -static apr_bucket *h2_beam_bucket(h2_bucket_beam *beam, - apr_bucket_brigade *dest, - const apr_bucket *src) -{ - apr_bucket *b = NULL; - int i; - if (beamers) { - for (i = 0; i < beamers->nelts && b == NULL; ++i) { - h2_bucket_beamer *beamer; - - beamer = APR_ARRAY_IDX(beamers, i, h2_bucket_beamer*); - b = beamer(beam, dest, src); - } - } - return b; -} - - -/******************************************************************************* - * bucket beam that can transport buckets across threads - ******************************************************************************/ - -static void mutex_leave(void *ctx, apr_thread_mutex_t *lock) -{ - apr_thread_mutex_unlock(lock); -} + apr_bucket *b; + int count = 0; -static apr_status_t mutex_enter(void *ctx, h2_beam_lock *pbl) -{ - h2_bucket_beam *beam = ctx; - pbl->mutex = beam->lock; - pbl->leave = mutex_leave; - return apr_thread_mutex_lock(pbl->mutex); -} + for (b = H2_BLIST_FIRST(blist); b != H2_BLIST_SENTINEL(blist); + b = APR_BUCKET_NEXT(b)) { + ++count; + } + return count; +} + +#define H2_BEAM_LOG(beam, c, level, rv, msg, bb) \ + do { \ + if (APLOG_C_IS_LEVEL((c),(level))) { \ + char buffer[4 * 1024]; \ + apr_size_t len, bmax = sizeof(buffer)/sizeof(buffer[0]); \ + len = bb? h2_util_bb_print(buffer, bmax, "", "", bb) : 0; \ + ap_log_cerror(APLOG_MARK, (level), rv, (c), \ + "BEAM[%s,%s%sdata=%ld,buckets(send/consumed)=%d/%d]: %s %s", \ + (beam)->name, \ + (beam)->aborted? "aborted," : "", \ + buffer_is_empty(beam)? "empty," : "", \ + (long)get_buffered_data_len(beam), \ + h2_blist_count(&(beam)->buckets_to_send), \ + h2_blist_count(&(beam)->buckets_consumed), \ + (msg), len? buffer : ""); \ + } \ + } while (0) -static apr_status_t enter_yellow(h2_bucket_beam *beam, h2_beam_lock *pbl) -{ - return mutex_enter(beam, pbl); -} -static void leave_yellow(h2_bucket_beam *beam, h2_beam_lock *pbl) +static int bucket_is_mmap(apr_bucket *b) { - if (pbl->leave) { - pbl->leave(pbl->leave_ctx, pbl->mutex); - } +#if APR_HAS_MMAP + return APR_BUCKET_IS_MMAP(b); +#else + /* if it is not defined as enabled, it should always be no */ + return 0; +#endif } static apr_off_t bucket_mem_used(apr_bucket *b) { - if (APR_BUCKET_IS_FILE(b)) { + if (APR_BUCKET_IS_FILE(b) || bucket_is_mmap(b)) { return 0; } else { /* should all have determinate length */ - return b->length; + return (apr_off_t)b->length; } } -static int report_consumption(h2_bucket_beam *beam, h2_beam_lock *pbl) +static int report_consumption(h2_bucket_beam *beam, int locked) { int rv = 0; - apr_off_t len = beam->received_bytes - beam->cons_bytes_reported; + apr_off_t len = beam->recv_bytes - beam->recv_bytes_reported; h2_beam_io_callback *cb = beam->cons_io_cb; if (len > 0) { if (cb) { void *ctx = beam->cons_ctx; - if (pbl) leave_yellow(beam, pbl); + if (locked) apr_thread_mutex_unlock(beam->lock); cb(ctx, beam, len); - if (pbl) enter_yellow(beam, pbl); + if (locked) apr_thread_mutex_lock(beam->lock); rv = 1; } - beam->cons_bytes_reported += len; + beam->recv_bytes_reported += len; } return rv; } -static void report_prod_io(h2_bucket_beam *beam, int force, h2_beam_lock *pbl) -{ - apr_off_t len = beam->sent_bytes - beam->prod_bytes_reported; - if (force || len > 0) { - h2_beam_io_callback *cb = beam->prod_io_cb; - if (cb) { - void *ctx = beam->prod_ctx; - - leave_yellow(beam, pbl); - cb(ctx, beam, len); - enter_yellow(beam, pbl); - } - beam->prod_bytes_reported += len; - } -} - static apr_size_t calc_buffered(h2_bucket_beam *beam) { apr_size_t len = 0; apr_bucket *b; - for (b = H2_BLIST_FIRST(&beam->send_list); - b != H2_BLIST_SENTINEL(&beam->send_list); + for (b = H2_BLIST_FIRST(&beam->buckets_to_send); + b != H2_BLIST_SENTINEL(&beam->buckets_to_send); b = APR_BUCKET_NEXT(b)) { if (b->length == ((apr_size_t)-1)) { /* do not count */ } - else if (APR_BUCKET_IS_FILE(b)) { + else if (APR_BUCKET_IS_FILE(b) || bucket_is_mmap(b)) { /* if unread, has no real mem footprint. */ } else { @@ -288,13 +150,30 @@ static apr_size_t calc_buffered(h2_bucket_beam *beam) return len; } -static void r_purge_sent(h2_bucket_beam *beam) +static void purge_consumed_buckets(h2_bucket_beam *beam) +{ + apr_bucket *b; + /* delete all sender buckets in purge brigade, needs to be called + * from sender thread only */ + while (!H2_BLIST_EMPTY(&beam->buckets_consumed)) { + b = H2_BLIST_FIRST(&beam->buckets_consumed); + if(AP_BUCKET_IS_EOR(b)) { + APR_BUCKET_REMOVE(b); + H2_BLIST_INSERT_TAIL(&beam->buckets_eor, b); + } + else { + apr_bucket_delete(b); + } + } +} + +static void purge_eor_buckets(h2_bucket_beam *beam) { apr_bucket *b; /* delete all sender buckets in purge brigade, needs to be called * from sender thread only */ - while (!H2_BLIST_EMPTY(&beam->purge_list)) { - b = H2_BLIST_FIRST(&beam->purge_list); + while (!H2_BLIST_EMPTY(&beam->buckets_eor)) { + b = H2_BLIST_FIRST(&beam->buckets_eor); apr_bucket_delete(b); } } @@ -302,7 +181,7 @@ static void r_purge_sent(h2_bucket_beam *beam) static apr_size_t calc_space_left(h2_bucket_beam *beam) { if (beam->max_buf_size > 0) { - apr_off_t len = calc_buffered(beam); + apr_size_t len = calc_buffered(beam); return (beam->max_buf_size > len? (beam->max_buf_size - len) : 0); } return APR_SIZE_MAX; @@ -310,31 +189,10 @@ static apr_size_t calc_space_left(h2_bucket_beam *beam) static int buffer_is_empty(h2_bucket_beam *beam) { - return ((!beam->recv_buffer || APR_BRIGADE_EMPTY(beam->recv_buffer)) - && H2_BLIST_EMPTY(&beam->send_list)); + return H2_BLIST_EMPTY(&beam->buckets_to_send); } -static apr_status_t wait_empty(h2_bucket_beam *beam, apr_read_type_e block, - apr_thread_mutex_t *lock) -{ - apr_status_t rv = APR_SUCCESS; - - while (!buffer_is_empty(beam) && APR_SUCCESS == rv) { - if (APR_BLOCK_READ != block || !lock) { - rv = APR_EAGAIN; - } - else if (beam->timeout > 0) { - rv = apr_thread_cond_timedwait(beam->change, lock, beam->timeout); - } - else { - rv = apr_thread_cond_wait(beam->change, lock); - } - } - return rv; -} - -static apr_status_t wait_not_empty(h2_bucket_beam *beam, apr_read_type_e block, - apr_thread_mutex_t *lock) +static apr_status_t wait_not_empty(h2_bucket_beam *beam, conn_rec *c, apr_read_type_e block) { apr_status_t rv = APR_SUCCESS; @@ -345,21 +203,24 @@ static apr_status_t wait_not_empty(h2_bucket_beam *beam, apr_read_type_e block, else if (beam->closed) { rv = APR_EOF; } - else if (APR_BLOCK_READ != block || !lock) { + else if (APR_BLOCK_READ != block) { rv = APR_EAGAIN; } else if (beam->timeout > 0) { - rv = apr_thread_cond_timedwait(beam->change, lock, beam->timeout); + H2_BEAM_LOG(beam, c, APLOG_TRACE2, rv, "wait_not_empty, timeout", NULL); + rv = apr_thread_cond_timedwait(beam->change, beam->lock, beam->timeout); } else { - rv = apr_thread_cond_wait(beam->change, lock); + H2_BEAM_LOG(beam, c, APLOG_TRACE2, rv, "wait_not_empty, forever", NULL); + rv = apr_thread_cond_wait(beam->change, beam->lock); } } return rv; } -static apr_status_t wait_not_full(h2_bucket_beam *beam, apr_read_type_e block, - apr_size_t *pspace_left, h2_beam_lock *bl) +static apr_status_t wait_not_full(h2_bucket_beam *beam, conn_rec *c, + apr_read_type_e block, + apr_size_t *pspace_left) { apr_status_t rv = APR_SUCCESS; apr_size_t left; @@ -368,15 +229,17 @@ static apr_status_t wait_not_full(h2_bucket_beam *beam, apr_read_type_e block, if (beam->aborted) { rv = APR_ECONNABORTED; } - else if (block != APR_BLOCK_READ || !bl->mutex) { + else if (block != APR_BLOCK_READ) { rv = APR_EAGAIN; } else { if (beam->timeout > 0) { - rv = apr_thread_cond_timedwait(beam->change, bl->mutex, beam->timeout); + H2_BEAM_LOG(beam, c, APLOG_TRACE2, rv, "wait_not_full, timeout", NULL); + rv = apr_thread_cond_timedwait(beam->change, beam->lock, beam->timeout); } else { - rv = apr_thread_cond_wait(beam->change, bl->mutex); + H2_BEAM_LOG(beam, c, APLOG_TRACE2, rv, "wait_not_full, forever", NULL); + rv = apr_thread_cond_wait(beam->change, beam->lock); } } } @@ -384,73 +247,6 @@ static apr_status_t wait_not_full(h2_bucket_beam *beam, apr_read_type_e block, return rv; } -static void h2_beam_emitted(h2_bucket_beam *beam, h2_beam_proxy *proxy) -{ - h2_beam_lock bl; - apr_bucket *b, *next; - - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - /* even when beam buckets are split, only the one where - * refcount drops to 0 will call us */ - H2_BPROXY_REMOVE(proxy); - /* invoked from receiver thread, the last beam bucket for the send - * bucket is about to be destroyed. - * remove it from the hold, where it should be now */ - if (proxy->bsender) { - for (b = H2_BLIST_FIRST(&beam->hold_list); - b != H2_BLIST_SENTINEL(&beam->hold_list); - b = APR_BUCKET_NEXT(b)) { - if (b == proxy->bsender) { - break; - } - } - if (b != H2_BLIST_SENTINEL(&beam->hold_list)) { - /* bucket is in hold as it should be, mark this one - * and all before it for purging. We might have placed meta - * buckets without a receiver proxy into the hold before it - * and schedule them for purging now */ - for (b = H2_BLIST_FIRST(&beam->hold_list); - b != H2_BLIST_SENTINEL(&beam->hold_list); - b = next) { - next = APR_BUCKET_NEXT(b); - if (b == proxy->bsender) { - APR_BUCKET_REMOVE(b); - H2_BLIST_INSERT_TAIL(&beam->purge_list, b); - break; - } - else if (APR_BUCKET_IS_METADATA(b)) { - APR_BUCKET_REMOVE(b); - H2_BLIST_INSERT_TAIL(&beam->purge_list, b); - } - else { - /* another data bucket before this one in hold. this - * is normal since DATA buckets need not be destroyed - * in order */ - } - } - - proxy->bsender = NULL; - } - else { - /* it should be there unless we screwed up */ - ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, beam->send_pool, - APLOGNO(03384) "h2_beam(%d-%s): emitted bucket not " - "in hold, n=%d", beam->id, beam->tag, - (int)proxy->n); - ap_assert(!proxy->bsender); - } - } - /* notify anyone waiting on space to become available */ - if (!bl.mutex) { - r_purge_sent(beam); - } - else { - apr_thread_cond_broadcast(beam->change); - } - leave_yellow(beam, &bl); - } -} - static void h2_blist_cleanup(h2_blist *bl) { apr_bucket *e; @@ -461,335 +257,203 @@ static void h2_blist_cleanup(h2_blist *bl) } } -static apr_status_t beam_close(h2_bucket_beam *beam) +static void beam_shutdown(h2_bucket_beam *beam, apr_shutdown_how_e how) { - if (!beam->closed) { - beam->closed = 1; - apr_thread_cond_broadcast(beam->change); + if (!beam->pool) { + /* pool being cleared already */ + return; } - return APR_SUCCESS; -} - -int h2_beam_is_closed(h2_bucket_beam *beam) -{ - return beam->closed; -} -static int pool_register(h2_bucket_beam *beam, apr_pool_t *pool, - apr_status_t (*cleanup)(void *)) -{ - if (pool && pool != beam->pool) { - apr_pool_pre_cleanup_register(pool, beam, cleanup); - return 1; + /* shutdown both receiver and sender? */ + if (how == APR_SHUTDOWN_READWRITE) { + beam->cons_io_cb = NULL; + beam->recv_cb = NULL; + beam->eagain_cb = NULL; } - return 0; -} -static int pool_kill(h2_bucket_beam *beam, apr_pool_t *pool, - apr_status_t (*cleanup)(void *)) { - if (pool && pool != beam->pool) { - apr_pool_cleanup_kill(pool, beam, cleanup); - return 1; + /* shutdown sender (or both)? */ + if (how != APR_SHUTDOWN_READ) { + purge_consumed_buckets(beam); + h2_blist_cleanup(&beam->buckets_to_send); } - return 0; } -static apr_status_t beam_recv_cleanup(void *data) +static apr_status_t beam_cleanup(void *data) { h2_bucket_beam *beam = data; - /* receiver pool has gone away, clear references */ - beam->recv_buffer = NULL; - beam->recv_pool = NULL; + beam_shutdown(beam, APR_SHUTDOWN_READWRITE); + purge_eor_buckets(beam); + beam->pool = NULL; /* the pool is clearing now */ return APR_SUCCESS; } -static apr_status_t beam_send_cleanup(void *data) +apr_status_t h2_beam_destroy(h2_bucket_beam *beam, conn_rec *c) { - h2_bucket_beam *beam = data; - /* sender is going away, clear up all references to its memory */ - r_purge_sent(beam); - h2_blist_cleanup(&beam->send_list); - report_consumption(beam, NULL); - while (!H2_BPROXY_LIST_EMPTY(&beam->proxies)) { - h2_beam_proxy *proxy = H2_BPROXY_LIST_FIRST(&beam->proxies); - H2_BPROXY_REMOVE(proxy); - proxy->beam = NULL; - proxy->bsender = NULL; + if (beam->pool) { + H2_BEAM_LOG(beam, c, APLOG_TRACE2, 0, "destroy", NULL); + apr_pool_cleanup_run(beam->pool, beam, beam_cleanup); } - h2_blist_cleanup(&beam->purge_list); - h2_blist_cleanup(&beam->hold_list); - beam->send_pool = NULL; + H2_BEAM_LOG(beam, c, APLOG_TRACE2, 0, "destroyed", NULL); return APR_SUCCESS; } -static void beam_set_send_pool(h2_bucket_beam *beam, apr_pool_t *pool) -{ - if (beam->send_pool != pool) { - if (beam->send_pool && beam->send_pool != beam->pool) { - pool_kill(beam, beam->send_pool, beam_send_cleanup); - beam_send_cleanup(beam); - } - beam->send_pool = pool; - pool_register(beam, beam->send_pool, beam_send_cleanup); - } -} - -static void recv_buffer_cleanup(h2_bucket_beam *beam, h2_beam_lock *bl) -{ - if (beam->recv_buffer && !APR_BRIGADE_EMPTY(beam->recv_buffer)) { - apr_bucket_brigade *bb = beam->recv_buffer; - apr_off_t bblen = 0; - - beam->recv_buffer = NULL; - apr_brigade_length(bb, 0, &bblen); - beam->received_bytes += bblen; - - /* need to do this unlocked since bucket destroy might - * call this beam again. */ - if (bl) leave_yellow(beam, bl); - apr_brigade_destroy(bb); - if (bl) enter_yellow(beam, bl); - - apr_thread_cond_broadcast(beam->change); - if (beam->cons_ev_cb) { - beam->cons_ev_cb(beam->cons_ctx, beam); - } - } -} - -static apr_status_t beam_cleanup(h2_bucket_beam *beam, int from_pool) -{ - apr_status_t status = APR_SUCCESS; - int safe_send = (beam->owner == H2_BEAM_OWNER_SEND); - int safe_recv = (beam->owner == H2_BEAM_OWNER_RECV); - - /* - * Owner of the beam is going away, depending on which side it owns, - * cleanup strategies will differ. - * - * In general, receiver holds references to memory from sender. - * Clean up receiver first, if safe, then cleanup sender, if safe. - */ - - /* When called from pool destroy, io callbacks are disabled */ - if (from_pool) { - beam->cons_io_cb = NULL; - } - - /* When modify send is not safe, this means we still have multi-thread - * protection and the owner is receiving the buckets. If the sending - * side has not gone away, this means we could have dangling buckets - * in our lists that never get destroyed. This should not happen. */ - ap_assert(safe_send || !beam->send_pool); - if (!H2_BLIST_EMPTY(&beam->send_list)) { - ap_assert(beam->send_pool); - } - - if (safe_recv) { - if (beam->recv_pool) { - pool_kill(beam, beam->recv_pool, beam_recv_cleanup); - beam->recv_pool = NULL; - } - recv_buffer_cleanup(beam, NULL); - } - else { - beam->recv_buffer = NULL; - beam->recv_pool = NULL; - } - - if (safe_send && beam->send_pool) { - pool_kill(beam, beam->send_pool, beam_send_cleanup); - status = beam_send_cleanup(beam); - } - - if (safe_recv) { - ap_assert(H2_BPROXY_LIST_EMPTY(&beam->proxies)); - ap_assert(H2_BLIST_EMPTY(&beam->send_list)); - ap_assert(H2_BLIST_EMPTY(&beam->hold_list)); - ap_assert(H2_BLIST_EMPTY(&beam->purge_list)); - } - return status; -} - -static apr_status_t beam_pool_cleanup(void *data) -{ - return beam_cleanup(data, 1); -} - -apr_status_t h2_beam_destroy(h2_bucket_beam *beam) -{ - apr_pool_cleanup_kill(beam->pool, beam, beam_pool_cleanup); - return beam_cleanup(beam, 0); -} - -apr_status_t h2_beam_create(h2_bucket_beam **pbeam, apr_pool_t *pool, - int id, const char *tag, - h2_beam_owner_t owner, +apr_status_t h2_beam_create(h2_bucket_beam **pbeam, conn_rec *from, + apr_pool_t *pool, int id, const char *tag, apr_size_t max_buf_size, apr_interval_time_t timeout) { h2_bucket_beam *beam; - apr_status_t rv = APR_SUCCESS; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(from); + apr_status_t rv; beam = apr_pcalloc(pool, sizeof(*beam)); - if (!beam) { - return APR_ENOMEM; - } - - beam->id = id; - beam->tag = tag; beam->pool = pool; - beam->owner = owner; - H2_BLIST_INIT(&beam->send_list); - H2_BLIST_INIT(&beam->hold_list); - H2_BLIST_INIT(&beam->purge_list); - H2_BPROXY_LIST_INIT(&beam->proxies); + beam->from = from; + beam->id = id; + beam->name = apr_psprintf(pool, "%s-%d-%s", + conn_ctx->id, id, tag); + + H2_BLIST_INIT(&beam->buckets_to_send); + H2_BLIST_INIT(&beam->buckets_consumed); + H2_BLIST_INIT(&beam->buckets_eor); beam->tx_mem_limits = 1; beam->max_buf_size = max_buf_size; beam->timeout = timeout; rv = apr_thread_mutex_create(&beam->lock, APR_THREAD_MUTEX_DEFAULT, pool); - if (APR_SUCCESS == rv) { - rv = apr_thread_cond_create(&beam->change, pool); - if (APR_SUCCESS == rv) { - apr_pool_pre_cleanup_register(pool, beam, beam_pool_cleanup); - *pbeam = beam; - } - } + if (APR_SUCCESS != rv) goto cleanup; + rv = apr_thread_cond_create(&beam->change, pool); + if (APR_SUCCESS != rv) goto cleanup; + apr_pool_pre_cleanup_register(pool, beam, beam_cleanup); + +cleanup: + H2_BEAM_LOG(beam, from, APLOG_TRACE2, rv, "created", NULL); + *pbeam = (APR_SUCCESS == rv)? beam : NULL; return rv; } void h2_beam_buffer_size_set(h2_bucket_beam *beam, apr_size_t buffer_size) { - h2_beam_lock bl; - - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - beam->max_buf_size = buffer_size; - leave_yellow(beam, &bl); - } + apr_thread_mutex_lock(beam->lock); + beam->max_buf_size = buffer_size; + apr_thread_mutex_unlock(beam->lock); } -apr_size_t h2_beam_buffer_size_get(h2_bucket_beam *beam) +void h2_beam_set_copy_files(h2_bucket_beam * beam, int enabled) { - h2_beam_lock bl; - apr_size_t buffer_size = 0; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - buffer_size = beam->max_buf_size; - leave_yellow(beam, &bl); - } - return buffer_size; + apr_thread_mutex_lock(beam->lock); + beam->copy_files = enabled; + apr_thread_mutex_unlock(beam->lock); } -void h2_beam_timeout_set(h2_bucket_beam *beam, apr_interval_time_t timeout) +apr_size_t h2_beam_buffer_size_get(h2_bucket_beam *beam) { - h2_beam_lock bl; + apr_size_t buffer_size = 0; - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - beam->timeout = timeout; - leave_yellow(beam, &bl); - } + apr_thread_mutex_lock(beam->lock); + buffer_size = beam->max_buf_size; + apr_thread_mutex_unlock(beam->lock); + return buffer_size; } apr_interval_time_t h2_beam_timeout_get(h2_bucket_beam *beam) { - h2_beam_lock bl; - apr_interval_time_t timeout = 0; - - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - timeout = beam->timeout; - leave_yellow(beam, &bl); - } + apr_interval_time_t timeout; + + apr_thread_mutex_lock(beam->lock); + timeout = beam->timeout; + apr_thread_mutex_unlock(beam->lock); return timeout; } -void h2_beam_abort(h2_bucket_beam *beam) +void h2_beam_timeout_set(h2_bucket_beam *beam, apr_interval_time_t timeout) { - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - beam->aborted = 1; - r_purge_sent(beam); - h2_blist_cleanup(&beam->send_list); - report_consumption(beam, &bl); - apr_thread_cond_broadcast(beam->change); - leave_yellow(beam, &bl); - } + apr_thread_mutex_lock(beam->lock); + beam->timeout = timeout; + apr_thread_mutex_unlock(beam->lock); } -apr_status_t h2_beam_close(h2_bucket_beam *beam) +void h2_beam_abort(h2_bucket_beam *beam, conn_rec *c) { - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - r_purge_sent(beam); - beam_close(beam); - report_consumption(beam, &bl); - leave_yellow(beam, &bl); - } - return beam->aborted? APR_ECONNABORTED : APR_SUCCESS; -} + apr_thread_mutex_lock(beam->lock); + beam->aborted = 1; + if (c == beam->from) { + /* sender aborts */ + if (beam->send_cb) { + beam->send_cb(beam->send_ctx, beam); + } + if (beam->was_empty_cb && buffer_is_empty(beam)) { + beam->was_empty_cb(beam->was_empty_ctx, beam); + } + /* no more consumption reporting to sender */ + report_consumption(beam, 1); + beam->cons_ctx = NULL; -apr_status_t h2_beam_leave(h2_bucket_beam *beam) -{ - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - recv_buffer_cleanup(beam, &bl); - beam->aborted = 1; - beam_close(beam); - leave_yellow(beam, &bl); + beam_shutdown(beam, APR_SHUTDOWN_WRITE); } - return APR_SUCCESS; -} - -apr_status_t h2_beam_wait_empty(h2_bucket_beam *beam, apr_read_type_e block) -{ - apr_status_t status; - h2_beam_lock bl; - - if ((status = enter_yellow(beam, &bl)) == APR_SUCCESS) { - status = wait_empty(beam, block, bl.mutex); - leave_yellow(beam, &bl); + else { + /* receiver aborts */ + beam_shutdown(beam, APR_SHUTDOWN_READ); } - return status; + apr_thread_cond_broadcast(beam->change); + apr_thread_mutex_unlock(beam->lock); } -static void move_to_hold(h2_bucket_beam *beam, - apr_bucket_brigade *sender_bb) +void h2_beam_close(h2_bucket_beam *beam, conn_rec *c) { - apr_bucket *b; - while (sender_bb && !APR_BRIGADE_EMPTY(sender_bb)) { - b = APR_BRIGADE_FIRST(sender_bb); - APR_BUCKET_REMOVE(b); - H2_BLIST_INSERT_TAIL(&beam->send_list, b); + apr_thread_mutex_lock(beam->lock); + if (!beam->closed) { + /* should only be called from sender */ + ap_assert(c == beam->from); + beam->closed = 1; + if (beam->send_cb) { + beam->send_cb(beam->send_ctx, beam); + } + if (beam->was_empty_cb && buffer_is_empty(beam)) { + beam->was_empty_cb(beam->was_empty_ctx, beam); + } + apr_thread_cond_broadcast(beam->change); } + apr_thread_mutex_unlock(beam->lock); } -static apr_status_t append_bucket(h2_bucket_beam *beam, - apr_bucket *b, +static apr_status_t append_bucket(h2_bucket_beam *beam, + apr_bucket_brigade *bb, apr_read_type_e block, apr_size_t *pspace_left, - h2_beam_lock *pbl) + apr_off_t *pwritten) { + apr_bucket *b; const char *data; apr_size_t len; - apr_status_t status; - int can_beam = 0, check_len; + apr_status_t rv = APR_SUCCESS; + int can_beam = 0; + (void)block; if (beam->aborted) { - return APR_ECONNABORTED; + rv = APR_ECONNABORTED; + goto cleanup; } - + + ap_assert(beam->pool); + + b = APR_BRIGADE_FIRST(bb); if (APR_BUCKET_IS_METADATA(b)) { - if (APR_BUCKET_IS_EOS(b)) { - beam->closed = 1; - } APR_BUCKET_REMOVE(b); - H2_BLIST_INSERT_TAIL(&beam->send_list, b); - return APR_SUCCESS; + apr_bucket_setaside(b, beam->pool); + H2_BLIST_INSERT_TAIL(&beam->buckets_to_send, b); + goto cleanup; + } + /* non meta bucket */ + + /* in case of indeterminate length, we need to read the bucket, + * so that it transforms itself into something stable. */ + if (b->length == ((apr_size_t)-1)) { + rv = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (rv != APR_SUCCESS) goto cleanup; } - else if (APR_BUCKET_IS_FILE(b)) { + + if (APR_BUCKET_IS_FILE(b)) { /* For file buckets the problem is their internal readpool that * is used on the first read to allocate buffer/mmap. * Since setting aside a file bucket will de-register the @@ -806,478 +470,414 @@ static apr_status_t append_bucket(h2_bucket_beam *beam, * of open file handles and rather use a less efficient beam * transport. */ apr_bucket_file *bf = b->data; - apr_file_t *fd = bf->fd; - can_beam = (bf->refcount.refcount == 1); - if (can_beam && beam->can_beam_fn) { - can_beam = beam->can_beam_fn(beam->can_beam_ctx, beam, fd); - } - check_len = !can_beam; + can_beam = !beam->copy_files && (bf->refcount.refcount == 1); } - else { - if (b->length == ((apr_size_t)-1)) { - const char *data; - status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); - if (status != APR_SUCCESS) { - return status; - } - } - check_len = 1; + else if (bucket_is_mmap(b)) { + can_beam = !beam->copy_files; } - - if (check_len) { - if (b->length > *pspace_left) { - apr_bucket_split(b, *pspace_left); - } - *pspace_left -= b->length; + + if (b->length == 0) { + apr_bucket_delete(b); + rv = APR_SUCCESS; + goto cleanup; } - /* The fundamental problem is that reading a sender bucket from - * a receiver thread is a total NO GO, because the bucket might use - * its pool/bucket_alloc from a foreign thread and that will - * corrupt. */ - status = APR_ENOTIMPL; - if (APR_BUCKET_IS_TRANSIENT(b)) { - /* this takes care of transient buckets and converts them - * into heap ones. Other bucket types might or might not be - * affected by this. */ - status = apr_bucket_setaside(b, beam->send_pool); + if (!*pspace_left) { + rv = APR_EAGAIN; + goto cleanup; } - else if (APR_BUCKET_IS_HEAP(b)) { - /* For heap buckets read from a receiver thread is fine. The + + /* bucket is accepted and added to beam->buckets_to_send */ + if (APR_BUCKET_IS_HEAP(b)) { + /* For heap buckets, a read from a receiver thread is fine. The * data will be there and live until the bucket itself is * destroyed. */ - status = APR_SUCCESS; + rv = apr_bucket_setaside(b, beam->pool); + if (rv != APR_SUCCESS) goto cleanup; } - else if (APR_BUCKET_IS_POOL(b)) { - /* pool buckets are bastards that register at pool cleanup - * to morph themselves into heap buckets. That may happen anytime, - * even after the bucket data pointer has been read. So at - * any time inside the receiver thread, the pool bucket memory - * may disappear. yikes. */ - status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); - if (status == APR_SUCCESS) { - apr_bucket_heap_make(b, data, len, NULL); - } + else if (can_beam && (APR_BUCKET_IS_FILE(b) || bucket_is_mmap(b))) { + rv = apr_bucket_setaside(b, beam->pool); + if (rv != APR_SUCCESS) goto cleanup; } - else if (APR_BUCKET_IS_FILE(b) && can_beam) { - status = apr_bucket_setaside(b, beam->send_pool); + else { + /* we know of no special shortcut to transfer the bucket to + * another pool without copying. So we make it a heap bucket. */ + apr_bucket *b2; + + rv = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (rv != APR_SUCCESS) goto cleanup; + /* this allocates and copies data */ + b2 = apr_bucket_heap_create(data, len, NULL, bb->bucket_alloc); + apr_bucket_delete(b); + b = b2; + APR_BRIGADE_INSERT_HEAD(bb, b); } - if (status == APR_ENOTIMPL) { - /* we have no knowledge about the internals of this bucket, - * but hope that after read, its data stays immutable for the - * lifetime of the bucket. (see pool bucket handling above for - * a counter example). - * We do the read while in the sender thread, so that the bucket may - * use pools/allocators safely. */ - status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); - if (status == APR_SUCCESS) { - status = apr_bucket_setaside(b, beam->send_pool); - } + APR_BUCKET_REMOVE(b); + H2_BLIST_INSERT_TAIL(&beam->buckets_to_send, b); + *pwritten += (apr_off_t)b->length; + if (b->length > *pspace_left) { + *pspace_left = 0; } - - if (status != APR_SUCCESS && status != APR_ENOTIMPL) { - return status; + else { + *pspace_left -= b->length; } - - APR_BUCKET_REMOVE(b); - H2_BLIST_INSERT_TAIL(&beam->send_list, b); - beam->sent_bytes += b->length; - - return APR_SUCCESS; -} -void h2_beam_send_from(h2_bucket_beam *beam, apr_pool_t *p) -{ - h2_beam_lock bl; - /* Called from the sender thread to add buckets to the beam */ - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - r_purge_sent(beam); - beam_set_send_pool(beam, p); - leave_yellow(beam, &bl); - } +cleanup: + return rv; } -apr_status_t h2_beam_send(h2_bucket_beam *beam, +apr_status_t h2_beam_send(h2_bucket_beam *beam, conn_rec *from, apr_bucket_brigade *sender_bb, - apr_read_type_e block) + apr_read_type_e block, + apr_off_t *pwritten) { - apr_bucket *b; apr_status_t rv = APR_SUCCESS; apr_size_t space_left = 0; - h2_beam_lock bl; + int was_empty; + + ap_assert(beam->pool); /* Called from the sender thread to add buckets to the beam */ - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - ap_assert(beam->send_pool); - r_purge_sent(beam); - + apr_thread_mutex_lock(beam->lock); + ap_assert(beam->from == from); + ap_assert(sender_bb); + H2_BEAM_LOG(beam, from, APLOG_TRACE2, rv, "start send", sender_bb); + purge_consumed_buckets(beam); + *pwritten = 0; + was_empty = buffer_is_empty(beam); + + space_left = calc_space_left(beam); + while (!APR_BRIGADE_EMPTY(sender_bb) && APR_SUCCESS == rv) { + rv = append_bucket(beam, sender_bb, block, &space_left, pwritten); if (beam->aborted) { - move_to_hold(beam, sender_bb); - rv = APR_ECONNABORTED; - } - else if (sender_bb) { - int force_report = !APR_BRIGADE_EMPTY(sender_bb); - - space_left = calc_space_left(beam); - while (!APR_BRIGADE_EMPTY(sender_bb) && APR_SUCCESS == rv) { - if (space_left <= 0) { - report_prod_io(beam, force_report, &bl); - r_purge_sent(beam); - rv = wait_not_full(beam, block, &space_left, &bl); - if (APR_SUCCESS != rv) { - break; - } - } - b = APR_BRIGADE_FIRST(sender_bb); - rv = append_bucket(beam, b, block, &space_left, &bl); + goto cleanup; + } + else if (APR_EAGAIN == rv) { + /* bucket was not added, as beam buffer has no space left. + * Trigger event callbacks, so receiver can know there is something + * to receive before we do a conditional wait. */ + purge_consumed_buckets(beam); + if (beam->send_cb) { + beam->send_cb(beam->send_ctx, beam); } - - report_prod_io(beam, force_report, &bl); - apr_thread_cond_broadcast(beam->change); + if (was_empty && beam->was_empty_cb) { + beam->was_empty_cb(beam->was_empty_ctx, beam); + } + rv = wait_not_full(beam, from, block, &space_left); + if (APR_SUCCESS != rv) { + break; + } + was_empty = buffer_is_empty(beam); } - report_consumption(beam, &bl); - leave_yellow(beam, &bl); } + +cleanup: + if (beam->send_cb && !buffer_is_empty(beam)) { + beam->send_cb(beam->send_ctx, beam); + } + if (was_empty && beam->was_empty_cb && !buffer_is_empty(beam)) { + beam->was_empty_cb(beam->was_empty_ctx, beam); + } + apr_thread_cond_broadcast(beam->change); + + report_consumption(beam, 1); + if (beam->aborted) { + rv = APR_ECONNABORTED; + } + H2_BEAM_LOG(beam, from, APLOG_TRACE2, rv, "end send", sender_bb); + if(rv != APR_SUCCESS && !APR_STATUS_IS_EAGAIN(rv) && sender_bb != NULL) { + apr_brigade_cleanup(sender_bb); + } + apr_thread_mutex_unlock(beam->lock); return rv; } -apr_status_t h2_beam_receive(h2_bucket_beam *beam, +apr_status_t h2_beam_receive(h2_bucket_beam *beam, + conn_rec *to, apr_bucket_brigade *bb, apr_read_type_e block, apr_off_t readbytes) { - h2_beam_lock bl; apr_bucket *bsender, *brecv, *ng; int transferred = 0; - apr_status_t status = APR_SUCCESS; + apr_status_t rv = APR_SUCCESS; apr_off_t remain; - int transferred_buckets = 0; - - /* Called from the receiver thread to take buckets from the beam */ - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - if (readbytes <= 0) { - readbytes = APR_SIZE_MAX; - } - remain = readbytes; - + int consumed_buckets = 0; + + apr_thread_mutex_lock(beam->lock); + H2_BEAM_LOG(beam, to, APLOG_TRACE2, 0, "start receive", bb); + if (readbytes <= 0) { + readbytes = (apr_off_t)APR_SIZE_MAX; + } + remain = readbytes; + transfer: - if (beam->aborted) { - recv_buffer_cleanup(beam, &bl); - status = APR_ECONNABORTED; - goto leave; - } + if (beam->aborted) { + beam_shutdown(beam, APR_SHUTDOWN_READ); + rv = APR_ECONNABORTED; + goto leave; + } - /* transfer enough buckets from our receiver brigade, if we have one */ - while (remain >= 0 - && beam->recv_buffer - && !APR_BRIGADE_EMPTY(beam->recv_buffer)) { - - brecv = APR_BRIGADE_FIRST(beam->recv_buffer); - if (brecv->length > 0 && remain <= 0) { - break; - } - APR_BUCKET_REMOVE(brecv); - APR_BRIGADE_INSERT_TAIL(bb, brecv); - remain -= brecv->length; - ++transferred; + ap_assert(beam->pool); + + /* transfer from our sender brigade, transforming sender buckets to + * receiver ones until we have enough */ + while (remain >= 0 && !H2_BLIST_EMPTY(&beam->buckets_to_send)) { + + brecv = NULL; + bsender = H2_BLIST_FIRST(&beam->buckets_to_send); + if (bsender->length > 0 && remain <= 0) { + break; } - /* transfer from our sender brigade, transforming sender buckets to - * receiver ones until we have enough */ - while (remain >= 0 && !H2_BLIST_EMPTY(&beam->send_list)) { - - brecv = NULL; - bsender = H2_BLIST_FIRST(&beam->send_list); - if (bsender->length > 0 && remain <= 0) { - break; + if (APR_BUCKET_IS_METADATA(bsender)) { + /* we need a real copy into the receivers bucket_alloc */ + if (APR_BUCKET_IS_EOS(bsender)) { + /* this closes the beam */ + beam->closed = 1; + brecv = apr_bucket_eos_create(bb->bucket_alloc); } - - if (APR_BUCKET_IS_METADATA(bsender)) { - if (APR_BUCKET_IS_EOS(bsender)) { - brecv = apr_bucket_eos_create(bb->bucket_alloc); - beam->close_sent = 1; - } - else if (APR_BUCKET_IS_FLUSH(bsender)) { - brecv = apr_bucket_flush_create(bb->bucket_alloc); - } - else if (AP_BUCKET_IS_ERROR(bsender)) { - ap_bucket_error *eb = (ap_bucket_error *)bsender; - brecv = ap_bucket_error_create(eb->status, eb->data, - bb->p, bb->bucket_alloc); - } + else if (APR_BUCKET_IS_FLUSH(bsender)) { + brecv = apr_bucket_flush_create(bb->bucket_alloc); } - else if (bsender->length == 0) { - APR_BUCKET_REMOVE(bsender); - H2_BLIST_INSERT_TAIL(&beam->hold_list, bsender); - continue; +#if AP_HAS_RESPONSE_BUCKETS + else if (AP_BUCKET_IS_RESPONSE(bsender)) { + brecv = ap_bucket_response_clone(bsender, bb->p, bb->bucket_alloc); } - else if (APR_BUCKET_IS_FILE(bsender)) { - /* This is set aside into the target brigade pool so that - * any read operation messes with that pool and not - * the sender one. */ - apr_bucket_file *f = (apr_bucket_file *)bsender->data; - apr_file_t *fd = f->fd; - int setaside = (f->readpool != bb->p); - - if (setaside) { - status = apr_file_setaside(&fd, fd, bb->p); - if (status != APR_SUCCESS) { - goto leave; - } - ++beam->files_beamed; - } - ng = apr_brigade_insert_file(bb, fd, bsender->start, bsender->length, - bb->p); -#if APR_HAS_MMAP - /* disable mmap handling as this leads to segfaults when - * the underlying file is changed while memory pointer has - * been handed out. See also PR 59348 */ - apr_bucket_file_enable_mmap(ng, 0); -#endif - APR_BUCKET_REMOVE(bsender); - H2_BLIST_INSERT_TAIL(&beam->hold_list, bsender); - - remain -= bsender->length; - ++transferred; - ++transferred_buckets; - continue; + else if (AP_BUCKET_IS_REQUEST(bsender)) { + brecv = ap_bucket_request_clone(bsender, bb->p, bb->bucket_alloc); } - else { - /* create a "receiver" standin bucket. we took care about the - * underlying sender bucket and its data when we placed it into - * the sender brigade. - * the beam bucket will notify us on destruction that bsender is - * no longer needed. */ - brecv = h2_beam_bucket_create(beam, bsender, bb->bucket_alloc, - beam->buckets_sent++); + else if (AP_BUCKET_IS_HEADERS(bsender)) { + brecv = ap_bucket_headers_clone(bsender, bb->p, bb->bucket_alloc); } - - /* Place the sender bucket into our hold, to be destroyed when no - * receiver bucket references it any more. */ - APR_BUCKET_REMOVE(bsender); - H2_BLIST_INSERT_TAIL(&beam->hold_list, bsender); - - beam->received_bytes += bsender->length; - ++transferred_buckets; - - if (brecv) { - APR_BRIGADE_INSERT_TAIL(bb, brecv); - remain -= brecv->length; - ++transferred; +#else + else if (H2_BUCKET_IS_HEADERS(bsender)) { + brecv = h2_bucket_headers_clone(bsender, bb->p, bb->bucket_alloc); } - else { - /* let outside hook determine how bucket is beamed */ - leave_yellow(beam, &bl); - brecv = h2_beam_bucket(beam, bb, bsender); - enter_yellow(beam, &bl); - - while (brecv && brecv != APR_BRIGADE_SENTINEL(bb)) { - ++transferred; - remain -= brecv->length; - brecv = APR_BUCKET_NEXT(brecv); - } +#endif /* AP_HAS_RESPONSE_BUCKETS */ + else if (AP_BUCKET_IS_ERROR(bsender)) { + ap_bucket_error *eb = bsender->data; + brecv = ap_bucket_error_create(eb->status, eb->data, + bb->p, bb->bucket_alloc); } } - - if (remain < 0) { - /* too much, put some back into out recv_buffer */ - remain = readbytes; - for (brecv = APR_BRIGADE_FIRST(bb); - brecv != APR_BRIGADE_SENTINEL(bb); - brecv = APR_BUCKET_NEXT(brecv)) { - remain -= (beam->tx_mem_limits? bucket_mem_used(brecv) - : brecv->length); - if (remain < 0) { - apr_bucket_split(brecv, brecv->length+remain); - beam->recv_buffer = apr_brigade_split_ex(bb, - APR_BUCKET_NEXT(brecv), - beam->recv_buffer); - break; - } - } + else if (bsender->length == 0) { + /* nop */ } - - if (beam->closed && buffer_is_empty(beam)) { - /* beam is closed and we have nothing more to receive */ - if (!beam->close_sent) { - apr_bucket *b = apr_bucket_eos_create(bb->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - beam->close_sent = 1; - ++transferred; - status = APR_SUCCESS; - } +#if APR_HAS_MMAP + else if (APR_BUCKET_IS_MMAP(bsender)) { + apr_bucket_mmap *bmmap = bsender->data; + apr_mmap_t *mmap; + rv = apr_mmap_dup(&mmap, bmmap->mmap, bb->p); + if (rv != APR_SUCCESS) goto leave; + brecv = apr_bucket_mmap_create(mmap, bsender->start, bsender->length, bb->bucket_alloc); } - - if (transferred_buckets > 0) { - if (beam->cons_ev_cb) { - beam->cons_ev_cb(beam->cons_ctx, beam); +#endif + else if (APR_BUCKET_IS_FILE(bsender)) { + /* This is setaside into the target brigade pool so that + * any read operation messes with that pool and not + * the sender one. */ + apr_bucket_file *f = (apr_bucket_file *)bsender->data; + apr_file_t *fd = f->fd; + int setaside = (f->readpool != bb->p); + + if (setaside) { + rv = apr_file_setaside(&fd, fd, bb->p); + if (rv != APR_SUCCESS) goto leave; } - } - - if (transferred) { - apr_thread_cond_broadcast(beam->change); - status = APR_SUCCESS; + ng = apr_brigade_insert_file(bb, fd, bsender->start, (apr_off_t)bsender->length, + bb->p); +#if APR_HAS_MMAP + /* disable mmap handling as this leads to segfaults when + * the underlying file is changed while memory pointer has + * been handed out. See also PR 59348 */ + apr_bucket_file_enable_mmap(ng, 0); +#endif + remain -= bsender->length; + ++transferred; } else { - status = wait_not_empty(beam, block, bl.mutex); - if (status != APR_SUCCESS) { - goto leave; - } - goto transfer; + const char *data; + apr_size_t dlen; + /* we did that when the bucket was added, so this should + * give us the same data as before without changing the bucket + * or anything (pool) connected to it. */ + rv = apr_bucket_read(bsender, &data, &dlen, APR_BLOCK_READ); + if (rv != APR_SUCCESS) goto leave; + rv = apr_brigade_write(bb, NULL, NULL, data, dlen); + if (rv != APR_SUCCESS) goto leave; + + remain -= dlen; + ++transferred; + } + + if (brecv) { + /* we have a proxy that we can give the receiver */ + APR_BRIGADE_INSERT_TAIL(bb, brecv); + remain -= brecv->length; + ++transferred; + } + APR_BUCKET_REMOVE(bsender); + H2_BLIST_INSERT_TAIL(&beam->buckets_consumed, bsender); + beam->recv_bytes += bsender->length; + ++consumed_buckets; + } + + if (beam->recv_cb && consumed_buckets > 0) { + beam->recv_cb(beam->recv_ctx, beam); + } + + if (transferred) { + apr_thread_cond_broadcast(beam->change); + rv = APR_SUCCESS; + } + else if (beam->aborted) { + rv = APR_ECONNABORTED; + } + else if (beam->closed) { + rv = APR_EOF; + } + else { + rv = wait_not_empty(beam, to, block); + if (rv != APR_SUCCESS) { + goto leave; } -leave: - leave_yellow(beam, &bl); + goto transfer; + } + +leave: + H2_BEAM_LOG(beam, to, APLOG_TRACE2, rv, "end receive", bb); + if (rv == APR_EAGAIN && beam->eagain_cb) { + beam->eagain_cb(beam->eagain_ctx, beam); } - return status; + apr_thread_mutex_unlock(beam->lock); + return rv; } void h2_beam_on_consumed(h2_bucket_beam *beam, - h2_beam_ev_callback *ev_cb, h2_beam_io_callback *io_cb, void *ctx) { - h2_beam_lock bl; - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - beam->cons_ev_cb = ev_cb; - beam->cons_io_cb = io_cb; - beam->cons_ctx = ctx; - leave_yellow(beam, &bl); - } + apr_thread_mutex_lock(beam->lock); + beam->cons_io_cb = io_cb; + beam->cons_ctx = ctx; + apr_thread_mutex_unlock(beam->lock); } -void h2_beam_on_produced(h2_bucket_beam *beam, - h2_beam_io_callback *io_cb, void *ctx) +void h2_beam_on_received(h2_bucket_beam *beam, + h2_beam_ev_callback *recv_cb, void *ctx) { - h2_beam_lock bl; - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - beam->prod_io_cb = io_cb; - beam->prod_ctx = ctx; - leave_yellow(beam, &bl); - } + apr_thread_mutex_lock(beam->lock); + beam->recv_cb = recv_cb; + beam->recv_ctx = ctx; + apr_thread_mutex_unlock(beam->lock); } -void h2_beam_on_file_beam(h2_bucket_beam *beam, - h2_beam_can_beam_callback *cb, void *ctx) +void h2_beam_on_eagain(h2_bucket_beam *beam, + h2_beam_ev_callback *eagain_cb, void *ctx) { - h2_beam_lock bl; - - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - beam->can_beam_fn = cb; - beam->can_beam_ctx = ctx; - leave_yellow(beam, &bl); - } + apr_thread_mutex_lock(beam->lock); + beam->eagain_cb = eagain_cb; + beam->eagain_ctx = ctx; + apr_thread_mutex_unlock(beam->lock); } +void h2_beam_on_send(h2_bucket_beam *beam, + h2_beam_ev_callback *send_cb, void *ctx) +{ + apr_thread_mutex_lock(beam->lock); + beam->send_cb = send_cb; + beam->send_ctx = ctx; + apr_thread_mutex_unlock(beam->lock); +} -apr_off_t h2_beam_get_buffered(h2_bucket_beam *beam) +void h2_beam_on_was_empty(h2_bucket_beam *beam, + h2_beam_ev_callback *was_empty_cb, void *ctx) { - apr_bucket *b; - apr_off_t l = 0; - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - for (b = H2_BLIST_FIRST(&beam->send_list); - b != H2_BLIST_SENTINEL(&beam->send_list); - b = APR_BUCKET_NEXT(b)) { - /* should all have determinate length */ - l += b->length; - } - leave_yellow(beam, &bl); - } - return l; + apr_thread_mutex_lock(beam->lock); + beam->was_empty_cb = was_empty_cb; + beam->was_empty_ctx = ctx; + apr_thread_mutex_unlock(beam->lock); } -apr_off_t h2_beam_get_mem_used(h2_bucket_beam *beam) + +static apr_off_t get_buffered_data_len(h2_bucket_beam *beam) { apr_bucket *b; apr_off_t l = 0; - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - for (b = H2_BLIST_FIRST(&beam->send_list); - b != H2_BLIST_SENTINEL(&beam->send_list); - b = APR_BUCKET_NEXT(b)) { - l += bucket_mem_used(b); - } - leave_yellow(beam, &bl); + + for (b = H2_BLIST_FIRST(&beam->buckets_to_send); + b != H2_BLIST_SENTINEL(&beam->buckets_to_send); + b = APR_BUCKET_NEXT(b)) { + /* should all have determinate length */ + l += b->length; } return l; } -int h2_beam_empty(h2_bucket_beam *beam) +apr_off_t h2_beam_get_buffered(h2_bucket_beam *beam) { - int empty = 1; - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - empty = (H2_BLIST_EMPTY(&beam->send_list) - && (!beam->recv_buffer || APR_BRIGADE_EMPTY(beam->recv_buffer))); - leave_yellow(beam, &bl); - } - return empty; -} + apr_off_t l = 0; -int h2_beam_holds_proxies(h2_bucket_beam *beam) -{ - int has_proxies = 1; - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - has_proxies = !H2_BPROXY_LIST_EMPTY(&beam->proxies); - leave_yellow(beam, &bl); - } - return has_proxies; + apr_thread_mutex_lock(beam->lock); + l = get_buffered_data_len(beam); + apr_thread_mutex_unlock(beam->lock); + return l; } -int h2_beam_was_received(h2_bucket_beam *beam) +apr_off_t h2_beam_get_mem_used(h2_bucket_beam *beam) { - int happend = 0; - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - happend = (beam->received_bytes > 0); - leave_yellow(beam, &bl); - } - return happend; -} + apr_bucket *b; + apr_off_t l = 0; -apr_size_t h2_beam_get_files_beamed(h2_bucket_beam *beam) -{ - apr_size_t n = 0; - h2_beam_lock bl; - - if (beam && enter_yellow(beam, &bl) == APR_SUCCESS) { - n = beam->files_beamed; - leave_yellow(beam, &bl); + apr_thread_mutex_lock(beam->lock); + for (b = H2_BLIST_FIRST(&beam->buckets_to_send); + b != H2_BLIST_SENTINEL(&beam->buckets_to_send); + b = APR_BUCKET_NEXT(b)) { + l += bucket_mem_used(b); } - return n; + apr_thread_mutex_unlock(beam->lock); + return l; } -int h2_beam_no_files(void *ctx, h2_bucket_beam *beam, apr_file_t *file) +int h2_beam_empty(h2_bucket_beam *beam) { - return 0; + int empty = 1; + + apr_thread_mutex_lock(beam->lock); + empty = buffer_is_empty(beam); + apr_thread_mutex_unlock(beam->lock); + return empty; } int h2_beam_report_consumption(h2_bucket_beam *beam) { - h2_beam_lock bl; int rv = 0; - if (enter_yellow(beam, &bl) == APR_SUCCESS) { - rv = report_consumption(beam, &bl); - leave_yellow(beam, &bl); - } + + apr_thread_mutex_lock(beam->lock); + rv = report_consumption(beam, 1); + apr_thread_mutex_unlock(beam->lock); return rv; } -void h2_beam_log(h2_bucket_beam *beam, conn_rec *c, int level, const char *msg) +int h2_beam_is_complete(h2_bucket_beam *beam) { - if (beam && APLOG_C_IS_LEVEL(c,level)) { - ap_log_cerror(APLOG_MARK, level, 0, c, - "beam(%ld-%d,%s,closed=%d,aborted=%d,empty=%d,buf=%ld): %s", - (c->master? c->master->id : c->id), beam->id, beam->tag, - beam->closed, beam->aborted, h2_beam_empty(beam), - (long)h2_beam_get_buffered(beam), msg); + int rv = 0; + + apr_thread_mutex_lock(beam->lock); + if (beam->closed) + rv = 1; + else { + apr_bucket *b; + for (b = H2_BLIST_FIRST(&beam->buckets_to_send); + b != H2_BLIST_SENTINEL(&beam->buckets_to_send); + b = APR_BUCKET_NEXT(b)) { + if (APR_BUCKET_IS_EOS(b)) { + rv = 1; + break; + } + } } + apr_thread_mutex_unlock(beam->lock); + return rv; } - - diff --git a/modules/http2/h2_bucket_beam.h b/modules/http2/h2_bucket_beam.h index f260762..c58ce98 100644 --- a/modules/http2/h2_bucket_beam.h +++ b/modules/http2/h2_bucket_beam.h @@ -17,191 +17,63 @@ #ifndef h2_bucket_beam_h #define h2_bucket_beam_h +#include "h2_conn_ctx.h" + struct apr_thread_mutex_t; struct apr_thread_cond_t; -/******************************************************************************* - * apr_bucket list without bells and whistles - ******************************************************************************/ - -/** - * h2_blist can hold a list of buckets just like apr_bucket_brigade, but - * does not to any allocations or related features. - */ -typedef struct { - APR_RING_HEAD(h2_bucket_list, apr_bucket) list; -} h2_blist; - -#define H2_BLIST_INIT(b) APR_RING_INIT(&(b)->list, apr_bucket, link); -#define H2_BLIST_SENTINEL(b) APR_RING_SENTINEL(&(b)->list, apr_bucket, link) -#define H2_BLIST_EMPTY(b) APR_RING_EMPTY(&(b)->list, apr_bucket, link) -#define H2_BLIST_FIRST(b) APR_RING_FIRST(&(b)->list) -#define H2_BLIST_LAST(b) APR_RING_LAST(&(b)->list) -#define H2_BLIST_INSERT_HEAD(b, e) do { \ - apr_bucket *ap__b = (e); \ - APR_RING_INSERT_HEAD(&(b)->list, ap__b, apr_bucket, link); \ - } while (0) -#define H2_BLIST_INSERT_TAIL(b, e) do { \ - apr_bucket *ap__b = (e); \ - APR_RING_INSERT_TAIL(&(b)->list, ap__b, apr_bucket, link); \ - } while (0) -#define H2_BLIST_CONCAT(a, b) do { \ - APR_RING_CONCAT(&(a)->list, &(b)->list, apr_bucket, link); \ - } while (0) -#define H2_BLIST_PREPEND(a, b) do { \ - APR_RING_PREPEND(&(a)->list, &(b)->list, apr_bucket, link); \ - } while (0) - -/******************************************************************************* - * h2_bucket_beam - ******************************************************************************/ - /** * A h2_bucket_beam solves the task of transferring buckets, esp. their data, - * across threads with zero buffer copies. - * - * When a thread, let's call it the sender thread, wants to send buckets to - * another, the green thread, it creates a h2_bucket_beam and adds buckets - * via the h2_beam_send(). It gives the beam to the green thread which then - * can receive buckets into its own brigade via h2_beam_receive(). - * - * Sending and receiving can happen concurrently. - * - * The beam can limit the amount of data it accepts via the buffer_size. This - * can also be adjusted during its lifetime. Sends and receives can be done blocking. - * A timeout can be set for such blocks. - * - * Care needs to be taken when terminating the beam. The beam registers at - * the pool it was created with and will cleanup after itself. However, if - * received buckets do still exist, already freed memory might be accessed. - * The beam does a assertion on this condition. - * - * The proper way of shutting down a beam is to first make sure there are no - * more green buckets out there, then cleanup the beam to purge eventually - * still existing sender buckets and then, possibly, terminate the beam itself - * (or the pool it was created with). - * - * The following restrictions apply to bucket transport: - * - only EOS and FLUSH meta buckets are copied through. All other meta buckets - * are kept in the beams hold. - * - all kind of data buckets are transported through: - * - transient buckets are converted to heap ones on send - * - heap and pool buckets require no extra handling - * - buckets with indeterminate length are read on send - * - file buckets will transfer the file itself into a new bucket, if allowed - * - all other buckets are read on send to make sure data is present - * - * This assures that when the sender thread sends its sender buckets, the data - * is made accessible while still on the sender side. The sender bucket then enters - * the beams hold storage. - * When the green thread calls receive, sender buckets in the hold are wrapped - * into special beam buckets. Beam buckets on read present the data directly - * from the internal sender one, but otherwise live on the green side. When a - * beam bucket gets destroyed, it notifies its beam that the corresponding - * sender bucket from the hold may be destroyed. - * Since the destruction of green buckets happens in the green thread, any - * corresponding sender bucket can not immediately be destroyed, as that would - * result in race conditions. - * Instead, the beam transfers such sender buckets from the hold to the purge - * storage. Next time there is a call from the sender side, the buckets in - * purge will be deleted. - * - * There are callbacks that can be registesender with a beam: - * - a "consumed" callback that gets called on the sender side with the - * amount of data that has been received by the green side. The amount - * is a delta from the last callback invocation. The sender side can trigger - * these callbacks by calling h2_beam_send() with a NULL brigade. - * - a "can_beam_file" callback that can prohibit the transfer of file handles - * through the beam. This will cause file buckets to be read on send and - * its data buffer will then be transports just like a heap bucket would. - * When no callback is registered, no restrictions apply and all files are - * passed through. - * File handles transfersender to the green side will stay there until the - * receiving brigade's pool is destroyed/cleared. If the pool lives very - * long or if many different files are beamed, the process might run out - * of available file handles. - * - * The name "beam" of course is inspired by good old transporter - * technology where humans are kept inside the transporter's memory - * buffers until the transmission is complete. Star gates use a similar trick. + * across threads with as little copying as possible. */ -typedef void h2_beam_mutex_leave(void *ctx, struct apr_thread_mutex_t *lock); - -typedef struct { - apr_thread_mutex_t *mutex; - h2_beam_mutex_leave *leave; - void *leave_ctx; -} h2_beam_lock; - typedef struct h2_bucket_beam h2_bucket_beam; -typedef apr_status_t h2_beam_mutex_enter(void *ctx, h2_beam_lock *pbl); - typedef void h2_beam_io_callback(void *ctx, h2_bucket_beam *beam, apr_off_t bytes); typedef void h2_beam_ev_callback(void *ctx, h2_bucket_beam *beam); -typedef struct h2_beam_proxy h2_beam_proxy; -typedef struct { - APR_RING_HEAD(h2_beam_proxy_list, h2_beam_proxy) list; -} h2_bproxy_list; - -typedef int h2_beam_can_beam_callback(void *ctx, h2_bucket_beam *beam, - apr_file_t *file); - -typedef enum { - H2_BEAM_OWNER_SEND, - H2_BEAM_OWNER_RECV -} h2_beam_owner_t; - /** - * Will deny all transfer of apr_file_t across the beam and force - * a data copy instead. + * h2_blist can hold a list of buckets just like apr_bucket_brigade, but + * does not to any allocations or related features. */ -int h2_beam_no_files(void *ctx, h2_bucket_beam *beam, apr_file_t *file); +typedef struct { + APR_RING_HEAD(h2_bucket_list, apr_bucket) list; +} h2_blist; struct h2_bucket_beam { int id; - const char *tag; + const char *name; + conn_rec *from; apr_pool_t *pool; - h2_beam_owner_t owner; - h2_blist send_list; - h2_blist hold_list; - h2_blist purge_list; - apr_bucket_brigade *recv_buffer; - h2_bproxy_list proxies; - apr_pool_t *send_pool; - apr_pool_t *recv_pool; - + h2_blist buckets_to_send; + h2_blist buckets_consumed; + h2_blist buckets_eor; + apr_size_t max_buf_size; apr_interval_time_t timeout; - apr_off_t sent_bytes; /* amount of bytes send */ - apr_off_t received_bytes; /* amount of bytes received */ - - apr_size_t buckets_sent; /* # of beam buckets sent */ - apr_size_t files_beamed; /* how many file handles have been set aside */ - - unsigned int aborted : 1; - unsigned int closed : 1; - unsigned int close_sent : 1; - unsigned int tx_mem_limits : 1; /* only memory size counts on transfers */ + int aborted; + int closed; + int tx_mem_limits; /* only memory size counts on transfers */ + int copy_files; struct apr_thread_mutex_t *lock; struct apr_thread_cond_t *change; - apr_off_t cons_bytes_reported; /* amount of bytes reported as consumed */ - h2_beam_ev_callback *cons_ev_cb; - h2_beam_io_callback *cons_io_cb; + h2_beam_ev_callback *was_empty_cb; /* event: beam changed to non-empty in h2_beam_send() */ + void *was_empty_ctx; + h2_beam_ev_callback *recv_cb; /* event: buckets were transfered in h2_beam_receive() */ + void *recv_ctx; + h2_beam_ev_callback *send_cb; /* event: buckets were added in h2_beam_send() */ + void *send_ctx; + h2_beam_ev_callback *eagain_cb; /* event: a receive results in ARP_EAGAIN */ + void *eagain_ctx; + + apr_off_t recv_bytes; /* amount of bytes transferred in h2_beam_receive() */ + apr_off_t recv_bytes_reported; /* amount of bytes reported as received via callback */ + h2_beam_io_callback *cons_io_cb; /* report: recv_bytes deltas for sender */ void *cons_ctx; - - apr_off_t prod_bytes_reported; /* amount of bytes reported as produced */ - h2_beam_io_callback *prod_io_cb; - void *prod_ctx; - - h2_beam_can_beam_callback *can_beam_fn; - void *can_beam_ctx; }; /** @@ -212,56 +84,66 @@ struct h2_bucket_beam { * that is only used inside that same mutex. * * @param pbeam will hold the created beam on return + * @param c_from connection from which buchets are sent * @param pool pool owning the beam, beam will cleanup when pool released * @param id identifier of the beam * @param tag tag identifying beam for logging - * @param owner if the beam is owned by the sender or receiver, e.g. if - * the pool owner is using this beam for sending or receiving * @param buffer_size maximum memory footprint of buckets buffered in beam, or * 0 for no limitation * @param timeout timeout for blocking operations */ apr_status_t h2_beam_create(h2_bucket_beam **pbeam, + conn_rec *from, apr_pool_t *pool, int id, const char *tag, - h2_beam_owner_t owner, apr_size_t buffer_size, apr_interval_time_t timeout); /** * Destroys the beam immediately without cleanup. */ -apr_status_t h2_beam_destroy(h2_bucket_beam *beam); +apr_status_t h2_beam_destroy(h2_bucket_beam *beam, conn_rec *c); /** - * Send buckets from the given brigade through the beam. Will hold buckets - * internally as long as they have not been processed by the receiving side. - * All accepted buckets are removed from the given brigade. Will return with - * APR_EAGAIN on non-blocking sends when not all buckets could be accepted. - * - * Call from the sender side only. + * Switch copying of file buckets on/off. */ -apr_status_t h2_beam_send(h2_bucket_beam *beam, - apr_bucket_brigade *bb, - apr_read_type_e block); +void h2_beam_set_copy_files(h2_bucket_beam * beam, int enabled); /** - * Register the pool from which future buckets are send. This defines - * the lifetime of the buckets, e.g. the pool should not be cleared/destroyed - * until the data is no longer needed (or has been received). + * Send buckets from the given brigade through the beam. + * This can block of the amount of bucket data is above the buffer limit. + * @param beam the beam to add buckets to + * @param from the connection the sender operates on, must be the same as + * used to create the beam + * @param bb the brigade to take buckets from + * @param block if the sending should block when the buffer is full + * @param pwritten on return, contains the number of data bytes sent + * @return APR_SUCCESS when buckets were added to the beam. This can be + * a partial transfer and other buckets may still remain in bb + * APR_EAGAIN on non-blocking send when the buffer is full + * APR_TIMEUP on blocking semd that time out + * APR_ECONNABORTED when beam has been aborted */ -void h2_beam_send_from(h2_bucket_beam *beam, apr_pool_t *p); +apr_status_t h2_beam_send(h2_bucket_beam *beam, conn_rec *from, + apr_bucket_brigade *bb, + apr_read_type_e block, + apr_off_t *pwritten); /** - * Receive buckets from the beam into the given brigade. Will return APR_EOF - * when reading past an EOS bucket. Reads can be blocking until data is - * available or the beam has been closed. Non-blocking calls return APR_EAGAIN - * if no data is available. - * - * Call from the receiver side only. + * Receive buckets from the beam into the given brigade. The caller is + * operating on connection `to`. + * @param beam the beam to receive buckets from + * @param to the connection the receiver is working with + * @param bb the bucket brigade to append to + * @param block if the read should block when buckets are unavailable + * @param readbytes the amount of data the receiver wants + * @return APR_SUCCESS when buckets were appended + * APR_EAGAIN on non-blocking read when no buckets are available + * APR_TIMEUP on blocking reads that time out + * APR_ECONNABORTED when beam has been aborted */ -apr_status_t h2_beam_receive(h2_bucket_beam *beam, - apr_bucket_brigade *green_buckets, +apr_status_t h2_beam_receive(h2_bucket_beam *beam, conn_rec *to, + apr_bucket_brigade *bb, apr_read_type_e block, apr_off_t readbytes); @@ -271,53 +153,27 @@ apr_status_t h2_beam_receive(h2_bucket_beam *beam, int h2_beam_empty(h2_bucket_beam *beam); /** - * Determine if beam has handed out proxy buckets that are not destroyed. - */ -int h2_beam_holds_proxies(h2_bucket_beam *beam); - -/** - * Abort the beam. Will cleanup any buffered buckets and answer all send - * and receives with APR_ECONNABORTED. - * - * Call from the sender side only. - */ -void h2_beam_abort(h2_bucket_beam *beam); - -/** - * Close the beam. Sending an EOS bucket serves the same purpose. - * - * Call from the sender side only. - */ -apr_status_t h2_beam_close(h2_bucket_beam *beam); - -/** - * Receives leaves the beam, e.g. will no longer read. This will - * interrupt any sender blocked writing and fail future send. - * - * Call from the receiver side only. + * Abort the beam, either from receiving or sending side. + * + * @param beam the beam to abort + * @param c the connection the caller is working with */ -apr_status_t h2_beam_leave(h2_bucket_beam *beam); - -int h2_beam_is_closed(h2_bucket_beam *beam); +void h2_beam_abort(h2_bucket_beam *beam, conn_rec *c); /** - * Return APR_SUCCESS when all buckets in transit have been handled. - * When called with APR_BLOCK_READ and a mutex set, will wait until the green - * side has consumed all data. Otherwise APR_EAGAIN is returned. - * With clear_buffers set, any queued data is discarded. - * If a timeout is set on the beam, waiting might also time out and - * return APR_ETIMEUP. + * Close the beam. Make certain an EOS is sent. * - * Call from the sender side only. + * @param beam the beam to abort + * @param c the connection the caller is working with */ -apr_status_t h2_beam_wait_empty(h2_bucket_beam *beam, apr_read_type_e block); +void h2_beam_close(h2_bucket_beam *beam, conn_rec *c); -/** - * Set/get the timeout for blocking read/write operations. Only works - * if a mutex has been set for the beam. +/** + * Set/get the timeout for blocking sebd/receive operations. */ void h2_beam_timeout_set(h2_bucket_beam *beam, apr_interval_time_t timeout); + apr_interval_time_t h2_beam_timeout_get(h2_bucket_beam *beam); /** @@ -332,7 +188,6 @@ apr_size_t h2_beam_buffer_size_get(h2_bucket_beam *beam); * amount of bytes that have been consumed by the receiver, since the * last callback invocation or reset. * @param beam the beam to set the callback on - * @param ev_cb the callback or NULL, called when bytes are consumed * @param io_cb the callback or NULL, called on sender with bytes consumed * @param ctx the context to use in callback invocation * @@ -340,43 +195,58 @@ apr_size_t h2_beam_buffer_size_get(h2_bucket_beam *beam); * from any side. */ void h2_beam_on_consumed(h2_bucket_beam *beam, - h2_beam_ev_callback *ev_cb, h2_beam_io_callback *io_cb, void *ctx); /** - * Call any registered consumed handler, if any changes have happened - * since the last invocation. - * @return !=0 iff a handler has been called - * - * Needs to be invoked from the sending side. + * Register a callback to be invoked on the receiver side whenever + * buckets have been transfered in a h2_beam_receive() call. + * @param beam the beam to set the callback on + * @param recv_cb the callback or NULL, called when buckets are received + * @param ctx the context to use in callback invocation */ -int h2_beam_report_consumption(h2_bucket_beam *beam); +void h2_beam_on_received(h2_bucket_beam *beam, + h2_beam_ev_callback *recv_cb, void *ctx); /** - * Register a callback to be invoked on the receiver side with the - * amount of bytes that have been produces by the sender, since the - * last callback invocation or reset. + * Register a callback to be invoked on the receiver side whenever + * APR_EAGAIN is being returned in h2_beam_receive(). * @param beam the beam to set the callback on - * @param io_cb the callback or NULL, called on receiver with bytes produced + * @param egain_cb the callback or NULL, called before APR_EAGAIN is returned * @param ctx the context to use in callback invocation - * - * Call from the receiver side, callbacks invoked on either side. */ -void h2_beam_on_produced(h2_bucket_beam *beam, - h2_beam_io_callback *io_cb, void *ctx); +void h2_beam_on_eagain(h2_bucket_beam *beam, + h2_beam_ev_callback *eagain_cb, void *ctx); /** - * Register a callback that may prevent a file from being beam as - * file handle, forcing the file content to be copied. Then no callback - * is set (NULL), file handles are transferred directly. + * Register a call back from the sender side to be invoked when send + * has added buckets to the beam. + * Unregister by passing a NULL on_send_cb. * @param beam the beam to set the callback on - * @param io_cb the callback or NULL, called on receiver with bytes produced + * @param on_send_cb the callback to invoke after buckets were added * @param ctx the context to use in callback invocation - * - * Call from the receiver side, callbacks invoked on either side. */ -void h2_beam_on_file_beam(h2_bucket_beam *beam, - h2_beam_can_beam_callback *cb, void *ctx); +void h2_beam_on_send(h2_bucket_beam *beam, + h2_beam_ev_callback *on_send_cb, void *ctx); + +/** + * Register a call back from the sender side to be invoked when send + * has added to a previously empty beam. + * Unregister by passing a NULL was_empty_cb. + * @param beam the beam to set the callback on + * @param was_empty_cb the callback to invoke on blocked send + * @param ctx the context to use in callback invocation + */ +void h2_beam_on_was_empty(h2_bucket_beam *beam, + h2_beam_ev_callback *was_empty_cb, void *ctx); + +/** + * Call any registered consumed handler, if any changes have happened + * since the last invocation. + * @return !=0 iff a handler has been called + * + * Needs to be invoked from the sending side. + */ +int h2_beam_report_consumption(h2_bucket_beam *beam); /** * Get the amount of bytes currently buffered in the beam (unread). @@ -389,18 +259,9 @@ apr_off_t h2_beam_get_buffered(h2_bucket_beam *beam); apr_off_t h2_beam_get_mem_used(h2_bucket_beam *beam); /** - * Return != 0 iff (some) data from the beam has been received. + * @return != 0 iff beam has been closed or has an EOS bucket buffered + * waiting to be received. */ -int h2_beam_was_received(h2_bucket_beam *beam); - -apr_size_t h2_beam_get_files_beamed(h2_bucket_beam *beam); - -typedef apr_bucket *h2_bucket_beamer(h2_bucket_beam *beam, - apr_bucket_brigade *dest, - const apr_bucket *src); - -void h2_register_bucket_beamer(h2_bucket_beamer *beamer); - -void h2_beam_log(h2_bucket_beam *beam, conn_rec *c, int level, const char *msg); +int h2_beam_is_complete(h2_bucket_beam *beam); #endif /* h2_bucket_beam_h */ diff --git a/modules/http2/h2_bucket_eos.c b/modules/http2/h2_bucket_eos.c index c89d499..fa46a30 100644 --- a/modules/http2/h2_bucket_eos.c +++ b/modules/http2/h2_bucket_eos.c @@ -13,22 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ #include <assert.h> #include <stddef.h> @@ -37,6 +21,7 @@ #include <http_core.h> #include <http_connection.h> #include <http_log.h> +#include <http_protocol.h> #include "h2_private.h" #include "h2.h" diff --git a/modules/http2/h2_c1.c b/modules/http2/h2_c1.c new file mode 100644 index 0000000..afb26fc --- /dev/null +++ b/modules/http2/h2_c1.c @@ -0,0 +1,323 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <apr_strings.h> + +#include <ap_mpm.h> +#include <ap_mmn.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_config.h> +#include <http_log.h> +#include <http_connection.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_ssl.h> + +#include <mpm_common.h> + +#include "h2_private.h" +#include "h2.h" +#include "h2_bucket_beam.h" +#include "h2_config.h" +#include "h2_conn_ctx.h" +#include "h2_mplx.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_protocol.h" +#include "h2_workers.h" +#include "h2_c1.h" +#include "h2_version.h" +#include "h2_util.h" + +static struct h2_workers *workers; + +static int async_mpm; + +APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_in) *h2_c_logio_add_bytes_in; +APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_out) *h2_c_logio_add_bytes_out; + +apr_status_t h2_c1_child_init(apr_pool_t *pool, server_rec *s) +{ + apr_status_t status = APR_SUCCESS; + int minw, maxw; + apr_time_t idle_limit; + + status = ap_mpm_query(AP_MPMQ_IS_ASYNC, &async_mpm); + if (status != APR_SUCCESS) { + /* some MPMs do not implemnent this */ + async_mpm = 0; + status = APR_SUCCESS; + } + + h2_config_init(pool); + + h2_get_workers_config(s, &minw, &maxw, &idle_limit); + workers = h2_workers_create(s, pool, maxw, minw, idle_limit); + + h2_c_logio_add_bytes_in = APR_RETRIEVE_OPTIONAL_FN(ap_logio_add_bytes_in); + h2_c_logio_add_bytes_out = APR_RETRIEVE_OPTIONAL_FN(ap_logio_add_bytes_out); + + return h2_mplx_c1_child_init(pool, s); +} + +void h2_c1_child_stopping(apr_pool_t *pool, int graceful) +{ + if (workers) { + h2_workers_shutdown(workers, graceful); + } +} + + +apr_status_t h2_c1_setup(conn_rec *c, request_rec *r, server_rec *s) +{ + h2_session *session; + h2_conn_ctx_t *ctx; + apr_status_t rv; + + if (!workers) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02911) + "workers not initialized"); + rv = APR_EGENERAL; + goto cleanup; + } + + rv = h2_session_create(&session, c, r, s, workers); + if (APR_SUCCESS != rv) goto cleanup; + + ctx = h2_conn_ctx_get(c); + ap_assert(ctx); + h2_conn_ctx_assign_session(ctx, session); + /* remove the input filter of mod_reqtimeout, now that the connection + * is established and we have switched to h2. reqtimeout has supervised + * possibly configured handshake timeouts and needs to get out of the way + * now since the rest of its state handling assumes http/1.x to take place. */ + ap_remove_input_filter_byhandle(c->input_filters, "reqtimeout"); + +cleanup: + return rv; +} + +apr_status_t h2_c1_run(conn_rec *c) +{ + apr_status_t status; + int mpm_state = 0; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + + ap_assert(conn_ctx); + ap_assert(conn_ctx->session); + do { + if (c->cs) { + c->cs->sense = CONN_SENSE_DEFAULT; + c->cs->state = CONN_STATE_HANDLER; + } + + status = h2_session_process(conn_ctx->session, async_mpm); + + if (APR_STATUS_IS_EOF(status)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, + H2_SSSN_LOG(APLOGNO(03045), conn_ctx->session, + "process, closing conn")); + c->keepalive = AP_CONN_CLOSE; + } + else { + c->keepalive = AP_CONN_KEEPALIVE; + } + + if (ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state)) { + break; + } + } while (!async_mpm + && c->keepalive == AP_CONN_KEEPALIVE + && mpm_state != AP_MPMQ_STOPPING); + + if (c->cs) { + switch (conn_ctx->session->state) { + case H2_SESSION_ST_INIT: + case H2_SESSION_ST_IDLE: + case H2_SESSION_ST_BUSY: + case H2_SESSION_ST_WAIT: + c->cs->state = CONN_STATE_WRITE_COMPLETION; + if (c->cs && !conn_ctx->session->remote.emitted_count) { + /* let the MPM know that we are not done and want + * the Timeout behaviour instead of a KeepAliveTimeout + * See PR 63534. + */ + c->cs->sense = CONN_SENSE_WANT_READ; + } + break; + case H2_SESSION_ST_CLEANUP: + case H2_SESSION_ST_DONE: + default: + c->cs->state = CONN_STATE_LINGER; + break; + } + } + + return APR_SUCCESS; +} + +apr_status_t h2_c1_pre_close(struct h2_conn_ctx_t *ctx, conn_rec *c) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + + if (conn_ctx && conn_ctx->session) { + apr_status_t status = h2_session_pre_close(conn_ctx->session, async_mpm); + return (status == APR_SUCCESS)? DONE : status; + } + return DONE; +} + +int h2_c1_allows_direct(conn_rec *c) +{ + if (!c->master) { + int is_tls = ap_ssl_conn_is_ssl(c); + const char *needed_protocol = is_tls? "h2" : "h2c"; + int h2_direct = h2_config_cgeti(c, H2_CONF_DIRECT); + + if (h2_direct < 0) { + h2_direct = is_tls? 0 : 1; + } + return (h2_direct && ap_is_allowed_protocol(c, NULL, NULL, needed_protocol)); + } + return 0; +} + +int h2_c1_can_upgrade(request_rec *r) +{ + if (!r->connection->master) { + int h2_upgrade = h2_config_rgeti(r, H2_CONF_UPGRADE); + return h2_upgrade > 0 || (h2_upgrade < 0 && !ap_ssl_conn_is_ssl(r->connection)); + } + return 0; +} + +static int h2_c1_hook_process_connection(conn_rec* c) +{ + apr_status_t status; + h2_conn_ctx_t *ctx; + + if (c->master) goto declined; + ctx = h2_conn_ctx_get(c); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn"); + if (!ctx && c->keepalives == 0) { + const char *proto = ap_get_protocol(c); + + if (APLOGctrace1(c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn, " + "new connection using protocol '%s', direct=%d, " + "tls acceptable=%d", proto, h2_c1_allows_direct(c), + h2_protocol_is_acceptable_c1(c, NULL, 1)); + } + + if (!strcmp(AP_PROTOCOL_HTTP1, proto) + && h2_c1_allows_direct(c) + && h2_protocol_is_acceptable_c1(c, NULL, 1)) { + /* Fresh connection still is on http/1.1 and H2Direct is enabled. + * Otherwise connection is in a fully acceptable state. + * -> peek at the first 24 incoming bytes + */ + apr_bucket_brigade *temp; + char *peek = NULL; + apr_size_t peeklen; + + temp = apr_brigade_create(c->pool, c->bucket_alloc); + status = ap_get_brigade(c->input_filters, temp, + AP_MODE_SPECULATIVE, APR_BLOCK_READ, 24); + + if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, APLOGNO(03054) + "h2_h2, error reading 24 bytes speculative"); + apr_brigade_destroy(temp); + return DECLINED; + } + + apr_brigade_pflatten(temp, &peek, &peeklen, c->pool); + if ((peeklen >= 24) && !memcmp(H2_MAGIC_TOKEN, peek, 24)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_h2, direct mode detected"); + ctx = h2_conn_ctx_create_for_c1(c, c->base_server, + ap_ssl_conn_is_ssl(c)? "h2" : "h2c"); + } + else if (APLOGctrace2(c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "h2_h2, not detected in %d bytes(base64): %s", + (int)peeklen, h2_util_base64url_encode(peek, peeklen, c->pool)); + } + apr_brigade_destroy(temp); + } + } + + if (!ctx) goto declined; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "process_conn"); + if (!ctx->session) { + status = h2_c1_setup(c, NULL, ctx->server? ctx->server : c->base_server); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, c, "conn_setup"); + if (status != APR_SUCCESS) { + h2_conn_ctx_detach(c); + return !OK; + } + } + h2_c1_run(c); + return OK; + +declined: + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, declined"); + return DECLINED; +} + +static int h2_c1_hook_pre_close(conn_rec *c) +{ + h2_conn_ctx_t *ctx; + + /* secondary connection? */ + if (c->master) { + return DECLINED; + } + + ctx = h2_conn_ctx_get(c); + if (ctx) { + /* If the session has been closed correctly already, we will not + * find a h2_conn_ctx_there. The presence indicates that the session + * is still ongoing. */ + return h2_c1_pre_close(ctx, c); + } + return DECLINED; +} + +static const char* const mod_ssl[] = { "mod_ssl.c", NULL}; +static const char* const mod_reqtimeout[] = { "mod_ssl.c", "mod_reqtimeout.c", NULL}; + +void h2_c1_register_hooks(void) +{ + /* Our main processing needs to run quite late. Definitely after mod_ssl, + * as we need its connection filters, but also before reqtimeout as its + * method of timeouts is specific to HTTP/1.1 (as of now). + * The core HTTP/1 processing run as REALLY_LAST, so we will have + * a chance to take over before it. + */ + ap_hook_process_connection(h2_c1_hook_process_connection, + mod_reqtimeout, NULL, APR_HOOK_LAST); + + /* One last chance to properly say goodbye if we have not done so + * already. */ + ap_hook_pre_close_connection(h2_c1_hook_pre_close, NULL, mod_ssl, APR_HOOK_LAST); +} + diff --git a/modules/http2/h2_c1.h b/modules/http2/h2_c1.h new file mode 100644 index 0000000..41527f6 --- /dev/null +++ b/modules/http2/h2_c1.h @@ -0,0 +1,83 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_c1__ +#define __mod_h2__h2_c1__ + +#include <http_core.h> + +struct h2_conn_ctx_t; + +extern APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_in) *h2_c_logio_add_bytes_in; +extern APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_out) *h2_c_logio_add_bytes_out; + +/* Initialize this child process for h2 primary connection work, + * to be called once during child init before multi processing + * starts. + */ +apr_status_t h2_c1_child_init(apr_pool_t *pool, server_rec *s); + +/** + * Setup the primary connection and our context for HTTP/2 processing + * + * @param c the connection HTTP/2 is starting on + * @param r the upgrade request that still awaits an answer, optional + * @param s the server selected for this connection (can be != c->base_server) + */ +apr_status_t h2_c1_setup(conn_rec *c, request_rec *r, server_rec *s); + +/** + * Run the HTTP/2 primary connection in synchronous fashion. + * Return when the HTTP/2 session is done + * and the connection will close or a fatal error occurred. + * + * @param c the http2 connection to run + * @return APR_SUCCESS when session is done. + */ +apr_status_t h2_c1_run(conn_rec *c); + +/** + * The primary connection is about to close. If we have not send a GOAWAY + * yet, this is the last chance. + */ +apr_status_t h2_c1_pre_close(struct h2_conn_ctx_t *ctx, conn_rec *c); + +/** + * Check if the connection allows a direct detection of HTTPP/2, + * as configurable by the H2Direct directive. + * @param c the connection to check on + * @return != 0 if direct detection is enabled + */ +int h2_c1_allows_direct(conn_rec *c); + +/** + * Check if the "Upgrade" HTTP/1.1 mode of protocol switching is enabled + * for the given request. + * @param r the request to check + * @return != 0 iff Upgrade switching is enabled + */ +int h2_c1_can_upgrade(request_rec *r); + +/* Register hooks for h2 handling on primary connections. + */ +void h2_c1_register_hooks(void); + +/** + * Child is about to be stopped, release unused resources + */ +void h2_c1_child_stopping(apr_pool_t *pool, int graceful); + +#endif /* defined(__mod_h2__h2_c1__) */ diff --git a/modules/http2/h2_c1_io.c b/modules/http2/h2_c1_io.c new file mode 100644 index 0000000..5ed4ee8 --- /dev/null +++ b/modules/http2/h2_c1_io.c @@ -0,0 +1,559 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <apr_strings.h> +#include <ap_mpm.h> +#include <mpm_common.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_log.h> +#include <http_connection.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_ssl.h> + +#include "h2_private.h" +#include "h2_bucket_eos.h" +#include "h2_config.h" +#include "h2_c1.h" +#include "h2_c1_io.h" +#include "h2_protocol.h" +#include "h2_session.h" +#include "h2_util.h" + +#define TLS_DATA_MAX (16*1024) + +/* Calculated like this: assuming MTU 1500 bytes + * 1500 - 40 (IP) - 20 (TCP) - 40 (TCP options) + * - TLS overhead (60-100) + * ~= 1300 bytes */ +#define WRITE_SIZE_INITIAL 1300 + +/* The maximum we'd like to write in one chunk is + * the max size of a TLS record. When pushing + * many frames down the h2 connection, this might + * align differently because of headers and other + * frames or simply as not sufficient data is + * in a response body. + * However keeping frames at or below this limit + * should make optimizations at the layer that writes + * to TLS easier. + */ +#define WRITE_SIZE_MAX (TLS_DATA_MAX) + +#define BUF_REMAIN ((apr_size_t)(bmax-off)) + +static void h2_c1_io_bb_log(conn_rec *c, int stream_id, int level, + const char *tag, apr_bucket_brigade *bb) +{ + char buffer[16 * 1024]; + const char *line = "(null)"; + int bmax = sizeof(buffer)/sizeof(buffer[0]); + int off = 0; + apr_bucket *b; + + (void)stream_id; + if (bb) { + memset(buffer, 0, bmax--); + for (b = APR_BRIGADE_FIRST(bb); + bmax && (b != APR_BRIGADE_SENTINEL(bb)); + b = APR_BUCKET_NEXT(b)) { + + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + off += apr_snprintf(buffer+off, BUF_REMAIN, "eos "); + } + else if (APR_BUCKET_IS_FLUSH(b)) { + off += apr_snprintf(buffer+off, BUF_REMAIN, "flush "); + } + else if (AP_BUCKET_IS_EOR(b)) { + off += apr_snprintf(buffer+off, BUF_REMAIN, "eor "); + } + else if (H2_BUCKET_IS_H2EOS(b)) { + off += apr_snprintf(buffer+off, BUF_REMAIN, "h2eos "); + } + else { + off += apr_snprintf(buffer+off, BUF_REMAIN, "meta(unknown) "); + } + } + else { + const char *btype = "data"; + if (APR_BUCKET_IS_FILE(b)) { + btype = "file"; + } + else if (APR_BUCKET_IS_PIPE(b)) { + btype = "pipe"; + } + else if (APR_BUCKET_IS_SOCKET(b)) { + btype = "socket"; + } + else if (APR_BUCKET_IS_HEAP(b)) { + btype = "heap"; + } + else if (APR_BUCKET_IS_TRANSIENT(b)) { + btype = "transient"; + } + else if (APR_BUCKET_IS_IMMORTAL(b)) { + btype = "immortal"; + } +#if APR_HAS_MMAP + else if (APR_BUCKET_IS_MMAP(b)) { + btype = "mmap"; + } +#endif + else if (APR_BUCKET_IS_POOL(b)) { + btype = "pool"; + } + + off += apr_snprintf(buffer+off, BUF_REMAIN, "%s[%ld] ", + btype, + (long)(b->length == ((apr_size_t)-1)? -1UL : b->length)); + } + } + line = *buffer? buffer : "(empty)"; + } + /* Intentional no APLOGNO */ + ap_log_cerror(APLOG_MARK, level, 0, c, "h2_session(%ld)-%s: %s", + c->id, tag, line); + +} +#define C1_IO_BB_LOG(c, stream_id, level, tag, bb) \ + if (APLOG_C_IS_LEVEL(c, level)) { \ + h2_c1_io_bb_log((c), (stream_id), (level), (tag), (bb)); \ + } + + +apr_status_t h2_c1_io_init(h2_c1_io *io, h2_session *session) +{ + conn_rec *c = session->c1; + + io->session = session; + io->output = apr_brigade_create(c->pool, c->bucket_alloc); + io->is_tls = ap_ssl_conn_is_ssl(session->c1); + io->buffer_output = io->is_tls; + io->flush_threshold = 4 * (apr_size_t)h2_config_sgeti64(session->s, H2_CONF_STREAM_MAX_MEM); + + if (io->buffer_output) { + /* This is what we start with, + * see https://issues.apache.org/jira/browse/TS-2503 + */ + io->warmup_size = h2_config_sgeti64(session->s, H2_CONF_TLS_WARMUP_SIZE); + io->cooldown_usecs = (h2_config_sgeti(session->s, H2_CONF_TLS_COOLDOWN_SECS) + * APR_USEC_PER_SEC); + io->cooldown_usecs = 0; + io->write_size = (io->cooldown_usecs > 0? + WRITE_SIZE_INITIAL : WRITE_SIZE_MAX); + } + else { + io->warmup_size = 0; + io->cooldown_usecs = 0; + io->write_size = 0; + } + + if (APLOGctrace1(c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, c, + "h2_c1_io(%ld): init, buffering=%d, warmup_size=%ld, " + "cd_secs=%f", c->id, io->buffer_output, + (long)io->warmup_size, + ((double)io->cooldown_usecs/APR_USEC_PER_SEC)); + } + + return APR_SUCCESS; +} + +static void append_scratch(h2_c1_io *io) +{ + if (io->scratch && io->slen > 0) { + apr_bucket *b = apr_bucket_heap_create(io->scratch, io->slen, + apr_bucket_free, + io->session->c1->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(io->output, b); + io->buffered_len += io->slen; + io->scratch = NULL; + io->slen = io->ssize = 0; + } +} + +static apr_size_t assure_scratch_space(h2_c1_io *io) { + apr_size_t remain = io->ssize - io->slen; + if (io->scratch && remain == 0) { + append_scratch(io); + } + if (!io->scratch) { + /* we control the size and it is larger than what buckets usually + * allocate. */ + io->scratch = apr_bucket_alloc(io->write_size, io->session->c1->bucket_alloc); + io->ssize = io->write_size; + io->slen = 0; + remain = io->ssize; + } + return remain; +} + +static apr_status_t read_to_scratch(h2_c1_io *io, apr_bucket *b) +{ + apr_status_t status; + const char *data; + apr_size_t len; + + if (!b->length) { + return APR_SUCCESS; + } + + ap_assert(b->length <= (io->ssize - io->slen)); + if (APR_BUCKET_IS_FILE(b)) { + apr_bucket_file *f = (apr_bucket_file *)b->data; + apr_file_t *fd = f->fd; + apr_off_t offset = b->start; + + len = b->length; + /* file buckets will read 8000 byte chunks and split + * themselves. However, we do know *exactly* how many + * bytes we need where. So we read the file directly to + * where we need it. + */ + status = apr_file_seek(fd, APR_SET, &offset); + if (status != APR_SUCCESS) { + return status; + } + status = apr_file_read(fd, io->scratch + io->slen, &len); + if (status != APR_SUCCESS && status != APR_EOF) { + return status; + } + io->slen += len; + } + else if (APR_BUCKET_IS_MMAP(b)) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, io->session->c1, + "h2_c1_io(%ld): seeing mmap bucket of size %ld, scratch remain=%ld", + io->session->c1->id, (long)b->length, (long)(io->ssize - io->slen)); + status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (status == APR_SUCCESS) { + memcpy(io->scratch+io->slen, data, len); + io->slen += len; + } + } + else { + status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (status == APR_SUCCESS) { + memcpy(io->scratch+io->slen, data, len); + io->slen += len; + } + } + return status; +} + +static apr_status_t pass_output(h2_c1_io *io, int flush) +{ + conn_rec *c = io->session->c1; + apr_off_t bblen = 0; + apr_status_t rv; + + if (io->is_passing) { + /* recursive call, may be triggered by an H2EOS bucket + * being destroyed and triggering sending more data? */ + AP_DEBUG_ASSERT(0); + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(10456) + "h2_c1_io(%ld): recursive call of h2_c1_io_pass. " + "Denied to prevent output corruption. This " + "points to a bug in the HTTP/2 implementation.", + c->id); + return APR_EGENERAL; + } + io->is_passing = 1; + + append_scratch(io); + if (flush) { + if (!APR_BUCKET_IS_FLUSH(APR_BRIGADE_LAST(io->output))) { + apr_bucket *b = apr_bucket_flush_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(io->output, b); + } + } + if (APR_BRIGADE_EMPTY(io->output)) { + rv = APR_SUCCESS; + goto cleanup; + } + + io->unflushed = !APR_BUCKET_IS_FLUSH(APR_BRIGADE_LAST(io->output)); + apr_brigade_length(io->output, 0, &bblen); + C1_IO_BB_LOG(c, 0, APLOG_TRACE2, "out", io->output); + + rv = ap_pass_brigade(c->output_filters, io->output); + if (APR_SUCCESS != rv) goto cleanup; + io->bytes_written += (apr_size_t)bblen; + + if (io->write_size < WRITE_SIZE_MAX + && io->bytes_written >= io->warmup_size) { + /* connection is hot, use max size */ + io->write_size = WRITE_SIZE_MAX; + } + else if (io->cooldown_usecs > 0 + && io->write_size > WRITE_SIZE_INITIAL) { + apr_time_t now = apr_time_now(); + if ((now - io->last_write) >= io->cooldown_usecs) { + /* long time not written, reset write size */ + io->write_size = WRITE_SIZE_INITIAL; + io->bytes_written = 0; + } + else { + io->last_write = now; + } + } + +cleanup: + if (APR_SUCCESS != rv) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c, APLOGNO(03044) + "h2_c1_io(%ld): pass_out brigade %ld bytes", + c->id, (long)bblen); + } + apr_brigade_cleanup(io->output); + io->buffered_len = 0; + io->is_passing = 0; + return rv; +} + +int h2_c1_io_needs_flush(h2_c1_io *io) +{ + return io->buffered_len >= io->flush_threshold; +} + +int h2_c1_io_pending(h2_c1_io *io) +{ + return !APR_BRIGADE_EMPTY(io->output) || (io->scratch && io->slen > 0); +} + +apr_status_t h2_c1_io_pass(h2_c1_io *io) +{ + apr_status_t rv = APR_SUCCESS; + + if (h2_c1_io_pending(io)) { + rv = pass_output(io, 0); + } + return rv; +} + +apr_status_t h2_c1_io_assure_flushed(h2_c1_io *io) +{ + apr_status_t rv = APR_SUCCESS; + + if (h2_c1_io_pending(io) || io->unflushed) { + rv = pass_output(io, 1); + if (APR_SUCCESS != rv) goto cleanup; + } +cleanup: + return rv; +} + +apr_status_t h2_c1_io_add_data(h2_c1_io *io, const char *data, size_t length) +{ + apr_status_t status = APR_SUCCESS; + apr_size_t remain; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->session->c1, + "h2_c1_io(%ld): adding %ld data bytes", + io->session->c1->id, (long)length); + if (io->buffer_output) { + while (length > 0) { + remain = assure_scratch_space(io); + if (remain >= length) { + memcpy(io->scratch + io->slen, data, length); + io->slen += length; + length = 0; + } + else { + memcpy(io->scratch + io->slen, data, remain); + io->slen += remain; + data += remain; + length -= remain; + } + } + } + else { + status = apr_brigade_write(io->output, NULL, NULL, data, length); + io->buffered_len += length; + } + return status; +} + +apr_status_t h2_c1_io_append(h2_c1_io *io, apr_bucket_brigade *bb) +{ + apr_bucket *b; + apr_status_t rv = APR_SUCCESS; + + while (!APR_BRIGADE_EMPTY(bb)) { + b = APR_BRIGADE_FIRST(bb); + if (APR_BUCKET_IS_METADATA(b) || APR_BUCKET_IS_MMAP(b)) { + /* need to finish any open scratch bucket, as meta data + * needs to be forward "in order". */ + append_scratch(io); + APR_BUCKET_REMOVE(b); + APR_BRIGADE_INSERT_TAIL(io->output, b); + } + else if (io->buffer_output) { + apr_size_t remain = assure_scratch_space(io); + if (b->length > remain) { + apr_bucket_split(b, remain); + if (io->slen == 0) { + /* complete write_size bucket, append unchanged */ + APR_BUCKET_REMOVE(b); + APR_BRIGADE_INSERT_TAIL(io->output, b); + io->buffered_len += b->length; + continue; + } + } + else { + /* bucket fits in remain, copy to scratch */ + rv = read_to_scratch(io, b); + apr_bucket_delete(b); + if (APR_SUCCESS != rv) goto cleanup; + continue; + } + } + else { + /* no buffering, forward buckets setaside on flush */ + apr_bucket_setaside(b, io->session->c1->pool); + APR_BUCKET_REMOVE(b); + APR_BRIGADE_INSERT_TAIL(io->output, b); + io->buffered_len += b->length; + } + } +cleanup: + return rv; +} + +static apr_status_t c1_in_feed_bucket(h2_session *session, + apr_bucket *b, apr_ssize_t *inout_len) +{ + apr_status_t rv = APR_SUCCESS; + apr_size_t len; + const char *data; + ssize_t n; + + rv = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + while (APR_SUCCESS == rv && len > 0) { + n = nghttp2_session_mem_recv(session->ngh2, (const uint8_t *)data, len); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, session->c1, + H2_SSSN_MSG(session, "fed %ld bytes to nghttp2, %ld read"), + (long)len, (long)n); + if (n < 0) { + if (nghttp2_is_fatal((int)n)) { + h2_session_event(session, H2_SESSION_EV_PROTO_ERROR, + (int)n, nghttp2_strerror((int)n)); + rv = APR_EGENERAL; + } + } + else { + *inout_len += n; + if ((apr_ssize_t)len <= n) { + break; + } + len -= (apr_size_t)n; + data += n; + } + } + + return rv; +} + +static apr_status_t c1_in_feed_brigade(h2_session *session, + apr_bucket_brigade *bb, + apr_ssize_t *inout_len) +{ + apr_status_t rv = APR_SUCCESS; + apr_bucket* b; + + *inout_len = 0; + while (!APR_BRIGADE_EMPTY(bb)) { + b = APR_BRIGADE_FIRST(bb); + if (!APR_BUCKET_IS_METADATA(b)) { + rv = c1_in_feed_bucket(session, b, inout_len); + if (APR_SUCCESS != rv) goto cleanup; + } + apr_bucket_delete(b); + } +cleanup: + apr_brigade_cleanup(bb); + return rv; +} + +static apr_status_t read_and_feed(h2_session *session) +{ + apr_ssize_t bytes_fed, bytes_requested; + apr_status_t rv; + + bytes_requested = H2MAX(APR_BUCKET_BUFF_SIZE, session->max_stream_mem * 4); + rv = ap_get_brigade(session->c1->input_filters, + session->bbtmp, AP_MODE_READBYTES, + APR_NONBLOCK_READ, bytes_requested); + + if (APR_SUCCESS == rv) { + if (!APR_BRIGADE_EMPTY(session->bbtmp)) { + h2_util_bb_log(session->c1, session->id, APLOG_TRACE2, "c1 in", + session->bbtmp); + rv = c1_in_feed_brigade(session, session->bbtmp, &bytes_fed); + session->io.bytes_read += bytes_fed; + } + else { + rv = APR_EAGAIN; + } + } + return rv; +} + +apr_status_t h2_c1_read(h2_session *session) +{ + apr_status_t rv; + + /* H2_IN filter handles all incoming data against the session. + * We just pull at the filter chain to make it happen */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_SSSN_MSG(session, "session_read start")); + rv = read_and_feed(session); + + if (APR_SUCCESS == rv) { + h2_session_dispatch_event(session, H2_SESSION_EV_INPUT_PENDING, 0, NULL); + } + else if (APR_STATUS_IS_EAGAIN(rv)) { + /* Signal that we have exhausted the input momentarily. + * This might switch to polling the socket */ + h2_session_dispatch_event(session, H2_SESSION_EV_INPUT_EXHAUSTED, 0, NULL); + } + else if (APR_SUCCESS != rv) { + if (APR_STATUS_IS_ETIMEDOUT(rv) + || APR_STATUS_IS_ECONNABORTED(rv) + || APR_STATUS_IS_ECONNRESET(rv) + || APR_STATUS_IS_EOF(rv) + || APR_STATUS_IS_EBADF(rv)) { + /* common status for a client that has left */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, session->c1, + H2_SSSN_MSG(session, "input gone")); + } + else { + /* uncommon status, log on INFO so that we see this */ + ap_log_cerror( APLOG_MARK, APLOG_DEBUG, rv, session->c1, + H2_SSSN_LOG(APLOGNO(02950), session, + "error reading, terminating")); + } + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); + } + + apr_brigade_cleanup(session->bbtmp); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, session->c1, + H2_SSSN_MSG(session, "session_read done")); + return rv; +} diff --git a/modules/http2/h2_conn_io.h b/modules/http2/h2_c1_io.h index 2c3be1c..c4dac38 100644 --- a/modules/http2/h2_conn_io.h +++ b/modules/http2/h2_c1_io.h @@ -14,8 +14,8 @@ * limitations under the License. */ -#ifndef __mod_h2__h2_conn_io__ -#define __mod_h2__h2_conn_io__ +#ifndef __mod_h2__h2_c1_io__ +#define __mod_h2__h2_c1_io__ struct h2_config; struct h2_session; @@ -27,10 +27,11 @@ struct h2_session; * directly without copying. */ typedef struct { - conn_rec *c; + struct h2_session *session; apr_bucket_brigade *output; int is_tls; + int unflushed; apr_time_t cooldown_usecs; apr_int64_t warmup_size; @@ -40,38 +41,61 @@ typedef struct { apr_int64_t bytes_written; int buffer_output; - apr_size_t flush_threshold; + apr_off_t buffered_len; + apr_off_t flush_threshold; unsigned int is_flushed : 1; - + unsigned int is_passing : 1; + char *scratch; apr_size_t ssize; apr_size_t slen; -} h2_conn_io; +} h2_c1_io; -apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c, - const struct h2_config *cfg); +apr_status_t h2_c1_io_init(h2_c1_io *io, struct h2_session *session); /** * Append data to the buffered output. * @param buf the data to append * @param length the length of the data to append */ -apr_status_t h2_conn_io_write(h2_conn_io *io, +apr_status_t h2_c1_io_add_data(h2_c1_io *io, const char *buf, size_t length); -apr_status_t h2_conn_io_pass(h2_conn_io *io, apr_bucket_brigade *bb); +apr_status_t h2_c1_io_add(h2_c1_io *io, apr_bucket *b); + +apr_status_t h2_c1_io_append(h2_c1_io *io, apr_bucket_brigade *bb); /** * Pass any buffered data on to the connection output filters. * @param io the connection io - * @param flush if a flush bucket should be appended to any output */ -apr_status_t h2_conn_io_flush(h2_conn_io *io); +apr_status_t h2_c1_io_pass(h2_c1_io *io); + +/** + * if there is any data pendiong or was any data send + * since the last FLUSH, send out a FLUSH now. + */ +apr_status_t h2_c1_io_assure_flushed(h2_c1_io *io); /** * Check if the buffered amount of data needs flushing. */ -int h2_conn_io_needs_flush(h2_conn_io *io); +int h2_c1_io_needs_flush(h2_c1_io *io); + +/** + * Check if we have output pending. + */ +int h2_c1_io_pending(h2_c1_io *io); + +struct h2_session; + +/** + * Read c1 input and pass it on to nghttp2. + * @param session the session + * @param when_pending != 0 if only pending input (sitting in filters) + * needs to be read + */ +apr_status_t h2_c1_read(struct h2_session *session); -#endif /* defined(__mod_h2__h2_conn_io__) */ +#endif /* defined(__mod_h2__h2_c1_io__) */ diff --git a/modules/http2/h2_c2.c b/modules/http2/h2_c2.c new file mode 100644 index 0000000..a955200 --- /dev/null +++ b/modules/http2/h2_c2.c @@ -0,0 +1,942 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <stddef.h> + +#include <apr_atomic.h> +#include <apr_strings.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_config.h> +#include <http_connection.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_log.h> +#include <http_vhost.h> +#include <util_filter.h> +#include <ap_mmn.h> +#include <ap_mpm.h> +#include <mpm_common.h> +#include <mod_core.h> +#include <scoreboard.h> + +#include "h2_private.h" +#include "h2.h" +#include "h2_bucket_beam.h" +#include "h2_c1.h" +#include "h2_config.h" +#include "h2_conn_ctx.h" +#include "h2_c2_filter.h" +#include "h2_protocol.h" +#include "h2_mplx.h" +#include "h2_request.h" +#include "h2_headers.h" +#include "h2_session.h" +#include "h2_stream.h" +#include "h2_ws.h" +#include "h2_c2.h" +#include "h2_util.h" +#include "mod_http2.h" + + +static module *mpm_module; +static int mpm_supported = 1; +static apr_socket_t *dummy_socket; + +#if AP_HAS_RESPONSE_BUCKETS + +static ap_filter_rec_t *c2_net_in_filter_handle; +static ap_filter_rec_t *c2_net_out_filter_handle; +static ap_filter_rec_t *c2_request_in_filter_handle; +static ap_filter_rec_t *c2_notes_out_filter_handle; + +#endif /* AP_HAS_RESPONSE_BUCKETS */ + +static void check_modules(int force) +{ + static int checked = 0; + int i; + + if (force || !checked) { + for (i = 0; ap_loaded_modules[i]; ++i) { + module *m = ap_loaded_modules[i]; + + if (!strcmp("event.c", m->name)) { + mpm_module = m; + break; + } + else if (!strcmp("motorz.c", m->name)) { + mpm_module = m; + break; + } + else if (!strcmp("mpm_netware.c", m->name)) { + mpm_module = m; + break; + } + else if (!strcmp("prefork.c", m->name)) { + mpm_module = m; + /* While http2 can work really well on prefork, it collides + * today's use case for prefork: running single-thread app engines + * like php. If we restrict h2_workers to 1 per process, php will + * work fine, but browser will be limited to 1 active request at a + * time. */ + mpm_supported = 0; + break; + } + else if (!strcmp("simple_api.c", m->name)) { + mpm_module = m; + mpm_supported = 0; + break; + } + else if (!strcmp("mpm_winnt.c", m->name)) { + mpm_module = m; + break; + } + else if (!strcmp("worker.c", m->name)) { + mpm_module = m; + break; + } + } + checked = 1; + } +} + +const char *h2_conn_mpm_name(void) +{ + check_modules(0); + return mpm_module? mpm_module->name : "unknown"; +} + +int h2_mpm_supported(void) +{ + check_modules(0); + return mpm_supported; +} + +apr_status_t h2_c2_child_init(apr_pool_t *pool, server_rec *s) +{ + check_modules(1); + return apr_socket_create(&dummy_socket, APR_INET, SOCK_STREAM, + APR_PROTO_TCP, pool); +} + +static void h2_c2_log_io(conn_rec *c2, apr_off_t bytes_sent) +{ + if (bytes_sent && h2_c_logio_add_bytes_out) { + h2_c_logio_add_bytes_out(c2, bytes_sent); + } +} + +void h2_c2_destroy(conn_rec *c2) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, c2, + "h2_c2(%s): destroy", c2->log_id); + if(!c2->aborted && conn_ctx && conn_ctx->bytes_sent) { + h2_c2_log_io(c2, conn_ctx->bytes_sent); + } + apr_pool_destroy(c2->pool); +} + +void h2_c2_abort(conn_rec *c2, conn_rec *from) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2); + + AP_DEBUG_ASSERT(conn_ctx); + AP_DEBUG_ASSERT(conn_ctx->stream_id); + if(!c2->aborted && conn_ctx->bytes_sent) { + h2_c2_log_io(c2, conn_ctx->bytes_sent); + } + + if (conn_ctx->beam_in) { + h2_beam_abort(conn_ctx->beam_in, from); + } + if (conn_ctx->beam_out) { + h2_beam_abort(conn_ctx->beam_out, from); + } + c2->aborted = 1; +} + +typedef struct { + apr_bucket_brigade *bb; /* c2: data in holding area */ + unsigned did_upgrade_eos:1; /* for Upgrade, we added an extra EOS */ +} h2_c2_fctx_in_t; + +static apr_status_t h2_c2_filter_in(ap_filter_t* f, + apr_bucket_brigade* bb, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes) +{ + h2_conn_ctx_t *conn_ctx; + h2_c2_fctx_in_t *fctx = f->ctx; + apr_status_t status = APR_SUCCESS; + apr_bucket *b; + apr_off_t bblen; + apr_size_t rmax = (readbytes < APR_INT32_MAX)? + (apr_size_t)readbytes : APR_INT32_MAX; + + conn_ctx = h2_conn_ctx_get(f->c); + AP_DEBUG_ASSERT(conn_ctx); + + if (mode == AP_MODE_INIT) { + return ap_get_brigade(f->c->input_filters, bb, mode, block, readbytes); + } + + if (f->c->aborted) { + return APR_ECONNABORTED; + } + + if (APLOGctrace3(f->c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, f->c, + "h2_c2_in(%s-%d): read, mode=%d, block=%d, readbytes=%ld", + conn_ctx->id, conn_ctx->stream_id, mode, block, + (long)readbytes); + } + + if (!fctx) { + fctx = apr_pcalloc(f->c->pool, sizeof(*fctx)); + f->ctx = fctx; + fctx->bb = apr_brigade_create(f->c->pool, f->c->bucket_alloc); + if (!conn_ctx->beam_in) { + b = apr_bucket_eos_create(f->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(fctx->bb, b); + } + } + + /* If this is a HTTP Upgrade, it means the request we process + * has not Content, although the stream is not necessarily closed. + * On first read, we insert an EOS to signal processing that it + * has the complete body. */ + if (conn_ctx->is_upgrade && !fctx->did_upgrade_eos) { + b = apr_bucket_eos_create(f->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(fctx->bb, b); + fctx->did_upgrade_eos = 1; + } + + while (APR_BRIGADE_EMPTY(fctx->bb)) { + /* Get more input data for our request. */ + if (APLOGctrace2(f->c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, f->c, + "h2_c2_in(%s-%d): get more data from mplx, block=%d, " + "readbytes=%ld", + conn_ctx->id, conn_ctx->stream_id, block, (long)readbytes); + } + if (conn_ctx->beam_in) { + if (conn_ctx->pipe_in[H2_PIPE_OUT]) { +receive: + status = h2_beam_receive(conn_ctx->beam_in, f->c, fctx->bb, APR_NONBLOCK_READ, + conn_ctx->mplx->stream_max_mem); + if (APR_STATUS_IS_EAGAIN(status) && APR_BLOCK_READ == block) { + status = h2_util_wait_on_pipe(conn_ctx->pipe_in[H2_PIPE_OUT]); + if (APR_SUCCESS == status) { + goto receive; + } + } + } + else { + status = h2_beam_receive(conn_ctx->beam_in, f->c, fctx->bb, block, + conn_ctx->mplx->stream_max_mem); + } + } + else { + status = APR_EOF; + } + + if (APLOGctrace3(f->c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, f->c, + "h2_c2_in(%s-%d): read returned", + conn_ctx->id, conn_ctx->stream_id); + } + if (APR_STATUS_IS_EAGAIN(status) + && (mode == AP_MODE_GETLINE || block == APR_BLOCK_READ)) { + /* chunked input handling does not seem to like it if we + * return with APR_EAGAIN from a GETLINE read... + * upload 100k test on test-ser.example.org hangs */ + status = APR_SUCCESS; + } + else if (APR_STATUS_IS_EOF(status)) { + break; + } + else if (status != APR_SUCCESS) { + conn_ctx->last_err = status; + return status; + } + + if (APLOGctrace3(f->c)) { + h2_util_bb_log(f->c, conn_ctx->stream_id, APLOG_TRACE3, + "c2 input recv raw", fctx->bb); + } + if (h2_c_logio_add_bytes_in) { + apr_brigade_length(bb, 0, &bblen); + h2_c_logio_add_bytes_in(f->c, bblen); + } + } + + /* Nothing there, no more data to get. Return. */ + if (status == APR_EOF && APR_BRIGADE_EMPTY(fctx->bb)) { + return status; + } + + if (APLOGctrace3(f->c)) { + h2_util_bb_log(f->c, conn_ctx->stream_id, APLOG_TRACE3, + "c2 input.bb", fctx->bb); + } + + if (APR_BRIGADE_EMPTY(fctx->bb)) { + if (APLOGctrace3(f->c)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, f->c, + "h2_c2_in(%s-%d): no data", + conn_ctx->id, conn_ctx->stream_id); + } + return (block == APR_NONBLOCK_READ)? APR_EAGAIN : APR_EOF; + } + + if (mode == AP_MODE_EXHAUSTIVE) { + /* return all we have */ + APR_BRIGADE_CONCAT(bb, fctx->bb); + } + else if (mode == AP_MODE_READBYTES) { + status = h2_brigade_concat_length(bb, fctx->bb, rmax); + } + else if (mode == AP_MODE_SPECULATIVE) { + status = h2_brigade_copy_length(bb, fctx->bb, rmax); + } + else if (mode == AP_MODE_GETLINE) { + /* we are reading a single LF line, e.g. the HTTP headers. + * this has the nasty side effect to split the bucket, even + * though it ends with CRLF and creates a 0 length bucket */ + status = apr_brigade_split_line(bb, fctx->bb, block, + HUGE_STRING_LEN); + if (APLOGctrace3(f->c)) { + char buffer[1024]; + apr_size_t len = sizeof(buffer)-1; + apr_brigade_flatten(bb, buffer, &len); + buffer[len] = 0; + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, f->c, + "h2_c2_in(%s-%d): getline: %s", + conn_ctx->id, conn_ctx->stream_id, buffer); + } + } + else { + /* Hmm, well. There is mode AP_MODE_EATCRLF, but we chose not + * to support it. Seems to work. */ + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOTIMPL, f->c, + APLOGNO(03472) + "h2_c2_in(%s-%d), unsupported READ mode %d", + conn_ctx->id, conn_ctx->stream_id, mode); + status = APR_ENOTIMPL; + } + + if (APLOGctrace3(f->c)) { + apr_brigade_length(bb, 0, &bblen); + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, f->c, + "h2_c2_in(%s-%d): %ld data bytes", + conn_ctx->id, conn_ctx->stream_id, (long)bblen); + } + return status; +} + +static apr_status_t beam_out(conn_rec *c2, h2_conn_ctx_t *conn_ctx, apr_bucket_brigade* bb) +{ + apr_off_t written = 0; + apr_status_t rv; + + rv = h2_beam_send(conn_ctx->beam_out, c2, bb, APR_BLOCK_READ, &written); + if (APR_STATUS_IS_EAGAIN(rv)) { + rv = APR_SUCCESS; + } + return rv; +} + +static apr_status_t h2_c2_filter_out(ap_filter_t* f, apr_bucket_brigade* bb) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); + apr_status_t rv; + + ap_assert(conn_ctx); +#if AP_HAS_RESPONSE_BUCKETS + if (!conn_ctx->has_final_response) { + apr_bucket *e; + + for (e = APR_BRIGADE_FIRST(bb); + e != APR_BRIGADE_SENTINEL(bb); + e = APR_BUCKET_NEXT(e)) + { + if (AP_BUCKET_IS_RESPONSE(e)) { + ap_bucket_response *resp = e->data; + if (resp->status >= 200) { + conn_ctx->has_final_response = 1; + break; + } + } + if (APR_BUCKET_IS_EOS(e)) { + break; + } + } + } +#endif /* AP_HAS_RESPONSE_BUCKETS */ + rv = beam_out(f->c, conn_ctx, bb); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, f->c, + "h2_c2(%s-%d): output leave", + conn_ctx->id, conn_ctx->stream_id); + if (APR_SUCCESS != rv) { + h2_c2_abort(f->c, f->c); + } + return rv; +} + +static int addn_headers(void *udata, const char *name, const char *value) +{ + apr_table_t *dest = udata; + apr_table_addn(dest, name, value); + return 1; +} + +static void check_early_hints(request_rec *r, const char *tag) +{ + apr_array_header_t *push_list = h2_config_push_list(r); + apr_table_t *early_headers = h2_config_early_headers(r); + + if (!r->expecting_100 && + ((push_list && push_list->nelts > 0) || + (early_headers && !apr_is_empty_table(early_headers)))) { + int have_hints = 0, i; + + if (push_list && push_list->nelts > 0) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "%s, early announcing %d resources for push", + tag, push_list->nelts); + for (i = 0; i < push_list->nelts; ++i) { + h2_push_res *push = &APR_ARRAY_IDX(push_list, i, h2_push_res); + apr_table_add(r->headers_out, "Link", + apr_psprintf(r->pool, "<%s>; rel=preload%s", + push->uri_ref, push->critical? "; critical" : "")); + } + have_hints = 1; + } + if (early_headers && !apr_is_empty_table(early_headers)) { + apr_table_do(addn_headers, r->headers_out, early_headers, NULL); + have_hints = 1; + } + + if (have_hints) { + int old_status; + const char *old_line; + + if (h2_config_rgeti(r, H2_CONF_PUSH) == 0 && + h2_config_sgeti(r->server, H2_CONF_PUSH) != 0) { + apr_table_setn(r->connection->notes, H2_PUSH_MODE_NOTE, "0"); + } + old_status = r->status; + old_line = r->status_line; + r->status = 103; + r->status_line = "103 Early Hints"; + ap_send_interim_response(r, 1); + r->status = old_status; + r->status_line = old_line; + } + } +} + +static int c2_hook_fixups(request_rec *r) +{ + conn_rec *c2 = r->connection; + h2_conn_ctx_t *conn_ctx; + + if (!c2->master || !(conn_ctx = h2_conn_ctx_get(c2)) || !conn_ctx->stream_id) { + return DECLINED; + } + + check_early_hints(r, "late_fixup"); + + return DECLINED; +} + +static apr_status_t http2_get_pollfd_from_conn(conn_rec *c, + struct apr_pollfd_t *pfd, + apr_interval_time_t *ptimeout) +{ +#if H2_USE_PIPES + if (c->master) { + h2_conn_ctx_t *ctx = h2_conn_ctx_get(c); + if (ctx) { + if (ctx->beam_in && ctx->pipe_in[H2_PIPE_OUT]) { + pfd->desc_type = APR_POLL_FILE; + pfd->desc.f = ctx->pipe_in[H2_PIPE_OUT]; + if (ptimeout) + *ptimeout = h2_beam_timeout_get(ctx->beam_in); + } + else { + /* no input */ + pfd->desc_type = APR_NO_DESC; + if (ptimeout) + *ptimeout = -1; + } + return APR_SUCCESS; + } + } +#else + (void)c; + (void)pfd; + (void)ptimeout; +#endif /* H2_USE_PIPES */ + return APR_ENOTIMPL; +} + +#if AP_HAS_RESPONSE_BUCKETS + +static void c2_pre_read_request(request_rec *r, conn_rec *c2) +{ + h2_conn_ctx_t *conn_ctx; + + if (!c2->master || !(conn_ctx = h2_conn_ctx_get(c2)) || !conn_ctx->stream_id) { + return; + } + ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, + "h2_c2(%s-%d): adding request filters", + conn_ctx->id, conn_ctx->stream_id); + ap_add_input_filter_handle(c2_request_in_filter_handle, NULL, r, r->connection); + ap_add_output_filter_handle(c2_notes_out_filter_handle, NULL, r, r->connection); +} + +static int c2_post_read_request(request_rec *r) +{ + h2_conn_ctx_t *conn_ctx; + conn_rec *c2 = r->connection; + apr_time_t timeout; + + if (!c2->master || !(conn_ctx = h2_conn_ctx_get(c2)) || !conn_ctx->stream_id) { + return DECLINED; + } + /* Now that the request_rec is fully initialized, set relevant params */ + conn_ctx->server = r->server; + timeout = h2_config_geti64(r, r->server, H2_CONF_STREAM_TIMEOUT); + if (timeout <= 0) { + timeout = r->server->timeout; + } + h2_conn_ctx_set_timeout(conn_ctx, timeout); + /* We only handle this one request on the connection and tell everyone + * that there is no need to keep it "clean" if something fails. Also, + * this prevents mod_reqtimeout from doing funny business with monitoring + * keepalive timeouts. + */ + r->connection->keepalive = AP_CONN_CLOSE; + + if (conn_ctx->beam_in && !apr_table_get(r->headers_in, "Content-Length")) { + r->body_indeterminate = 1; + } + + if (h2_config_sgeti(conn_ctx->server, H2_CONF_COPY_FILES)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "h2_mplx(%s-%d): copy_files in output", + conn_ctx->id, conn_ctx->stream_id); + h2_beam_set_copy_files(conn_ctx->beam_out, 1); + } + + /* Add the raw bytes of the request (e.g. header frame lengths to + * the logio for this request. */ + if (conn_ctx->request->raw_bytes && h2_c_logio_add_bytes_in) { + h2_c_logio_add_bytes_in(c2, conn_ctx->request->raw_bytes); + } + return OK; +} + +static int c2_hook_pre_connection(conn_rec *c2, void *csd) +{ + h2_conn_ctx_t *conn_ctx; + + if (!c2->master || !(conn_ctx = h2_conn_ctx_get(c2)) || !conn_ctx->stream_id) { + return DECLINED; + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, + "h2_c2(%s-%d), adding filters", + conn_ctx->id, conn_ctx->stream_id); + ap_add_input_filter_handle(c2_net_in_filter_handle, NULL, NULL, c2); + ap_add_output_filter_handle(c2_net_out_filter_handle, NULL, NULL, c2); + if (c2->keepalives == 0) { + /* Simulate that we had already a request on this connection. Some + * hooks trigger special behaviour when keepalives is 0. + * (Not necessarily in pre_connection, but later. Set it here, so it + * is in place.) */ + c2->keepalives = 1; + /* We signal that this connection will be closed after the request. + * Which is true in that sense that we throw away all traffic data + * on this c2 connection after each requests. Although we might + * reuse internal structures like memory pools. + * The wanted effect of this is that httpd does not try to clean up + * any dangling data on this connection when a request is done. Which + * is unnecessary on a h2 stream. + */ + c2->keepalive = AP_CONN_CLOSE; + } + return OK; +} + +void h2_c2_register_hooks(void) +{ + /* When the connection processing actually starts, we might + * take over, if the connection is for a h2 stream. + */ + ap_hook_pre_connection(c2_hook_pre_connection, + NULL, NULL, APR_HOOK_MIDDLE); + + /* We need to manipulate the standard HTTP/1.1 protocol filters and + * install our own. This needs to be done very early. */ + ap_hook_pre_read_request(c2_pre_read_request, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_post_read_request(c2_post_read_request, NULL, NULL, + APR_HOOK_REALLY_FIRST); + ap_hook_fixups(c2_hook_fixups, NULL, NULL, APR_HOOK_LAST); +#if H2_USE_POLLFD_FROM_CONN + ap_hook_get_pollfd_from_conn(http2_get_pollfd_from_conn, NULL, NULL, + APR_HOOK_MIDDLE); +#endif + APR_REGISTER_OPTIONAL_FN(http2_get_pollfd_from_conn); + + c2_net_in_filter_handle = + ap_register_input_filter("H2_C2_NET_IN", h2_c2_filter_in, + NULL, AP_FTYPE_NETWORK); + c2_net_out_filter_handle = + ap_register_output_filter("H2_C2_NET_OUT", h2_c2_filter_out, + NULL, AP_FTYPE_NETWORK); + c2_request_in_filter_handle = + ap_register_input_filter("H2_C2_REQUEST_IN", h2_c2_filter_request_in, + NULL, AP_FTYPE_PROTOCOL); + c2_notes_out_filter_handle = + ap_register_output_filter("H2_C2_NOTES_OUT", h2_c2_filter_notes_out, + NULL, AP_FTYPE_PROTOCOL); +} + +#else /* AP_HAS_RESPONSE_BUCKETS */ + +static apr_status_t c2_run_pre_connection(conn_rec *c2, apr_socket_t *csd) +{ + if (c2->keepalives == 0) { + /* Simulate that we had already a request on this connection. Some + * hooks trigger special behaviour when keepalives is 0. + * (Not necessarily in pre_connection, but later. Set it here, so it + * is in place.) */ + c2->keepalives = 1; + /* We signal that this connection will be closed after the request. + * Which is true in that sense that we throw away all traffic data + * on this c2 connection after each requests. Although we might + * reuse internal structures like memory pools. + * The wanted effect of this is that httpd does not try to clean up + * any dangling data on this connection when a request is done. Which + * is unnecessary on a h2 stream. + */ + c2->keepalive = AP_CONN_CLOSE; + return ap_run_pre_connection(c2, csd); + } + ap_assert(c2->output_filters); + return APR_SUCCESS; +} + +apr_status_t h2_c2_process(conn_rec *c2, apr_thread_t *thread, int worker_id) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2); + + ap_assert(conn_ctx); + ap_assert(conn_ctx->mplx); + + /* See the discussion at <https://github.com/icing/mod_h2/issues/195> + * + * Each conn_rec->id is supposed to be unique at a point in time. Since + * some modules (and maybe external code) uses this id as an identifier + * for the request_rec they handle, it needs to be unique for secondary + * connections also. + * + * The MPM module assigns the connection ids and mod_unique_id is using + * that one to generate identifier for requests. While the implementation + * works for HTTP/1.x, the parallel execution of several requests per + * connection will generate duplicate identifiers on load. + * + * The original implementation for secondary connection identifiers used + * to shift the master connection id up and assign the stream id to the + * lower bits. This was cramped on 32 bit systems, but on 64bit there was + * enough space. + * + * As issue 195 showed, mod_unique_id only uses the lower 32 bit of the + * connection id, even on 64bit systems. Therefore collisions in request ids. + * + * The way master connection ids are generated, there is some space "at the + * top" of the lower 32 bits on allmost all systems. If you have a setup + * with 64k threads per child and 255 child processes, you live on the edge. + * + * The new implementation shifts 8 bits and XORs in the worker + * id. This will experience collisions with > 256 h2 workers and heavy + * load still. There seems to be no way to solve this in all possible + * configurations by mod_h2 alone. + */ + c2->id = (c2->master->id << 8)^worker_id; + + if (!conn_ctx->pre_conn_done) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, + "h2_c2(%s-%d), adding filters", + conn_ctx->id, conn_ctx->stream_id); + ap_add_input_filter("H2_C2_NET_IN", NULL, NULL, c2); + ap_add_output_filter("H2_C2_NET_CATCH_H1", NULL, NULL, c2); + ap_add_output_filter("H2_C2_NET_OUT", NULL, NULL, c2); + + c2_run_pre_connection(c2, ap_get_conn_socket(c2)); + conn_ctx->pre_conn_done = 1; + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): process connection", + conn_ctx->id, conn_ctx->stream_id); + + c2->current_thread = thread; + ap_run_process_connection(c2); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): processing done", + conn_ctx->id, conn_ctx->stream_id); + + return APR_SUCCESS; +} + +static apr_status_t c2_process(h2_conn_ctx_t *conn_ctx, conn_rec *c) +{ + const h2_request *req = conn_ctx->request; + conn_state_t *cs = c->cs; + request_rec *r = NULL; + const char *tenc; + apr_time_t timeout; + apr_status_t rv = APR_SUCCESS; + + if (req->protocol && !strcmp("websocket", req->protocol)) { + req = h2_ws_rewrite_request(req, c, conn_ctx->beam_in == NULL); + if (!req) { + rv = APR_EGENERAL; + goto cleanup; + } + } + + r = h2_create_request_rec(req, c, conn_ctx->beam_in == NULL); + + if (!r) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_c2(%s-%d): create request_rec failed, r=NULL", + conn_ctx->id, conn_ctx->stream_id); + goto cleanup; + } + if (r->status != HTTP_OK) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_c2(%s-%d): create request_rec failed, r->status=%d", + conn_ctx->id, conn_ctx->stream_id, r->status); + goto cleanup; + } + + tenc = apr_table_get(r->headers_in, "Transfer-Encoding"); + conn_ctx->input_chunked = tenc && ap_is_chunked(r->pool, tenc); + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_c2(%s-%d): created request_rec for %s", + conn_ctx->id, conn_ctx->stream_id, r->the_request); + conn_ctx->server = r->server; + timeout = h2_config_geti64(r, r->server, H2_CONF_STREAM_TIMEOUT); + if (timeout <= 0) { + timeout = r->server->timeout; + } + h2_conn_ctx_set_timeout(conn_ctx, timeout); + + if (h2_config_sgeti(conn_ctx->server, H2_CONF_COPY_FILES)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_mplx(%s-%d): copy_files in output", + conn_ctx->id, conn_ctx->stream_id); + h2_beam_set_copy_files(conn_ctx->beam_out, 1); + } + + ap_update_child_status(c->sbh, SERVER_BUSY_WRITE, r); + if (cs) { + cs->state = CONN_STATE_HANDLER; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_c2(%s-%d): start process_request", + conn_ctx->id, conn_ctx->stream_id); + + /* Add the raw bytes of the request (e.g. header frame lengths to + * the logio for this request. */ + if (req->raw_bytes && h2_c_logio_add_bytes_in) { + h2_c_logio_add_bytes_in(c, req->raw_bytes); + } + + ap_process_request(r); + /* After the call to ap_process_request, the + * request pool may have been deleted. */ + r = NULL; + if (conn_ctx->beam_out) { + h2_beam_close(conn_ctx->beam_out, c); + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_c2(%s-%d): process_request done", + conn_ctx->id, conn_ctx->stream_id); + if (cs) + cs->state = CONN_STATE_WRITE_COMPLETION; + +cleanup: + return rv; +} + +conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent, + apr_bucket_alloc_t *buckt_alloc) +{ + apr_pool_t *pool; + conn_rec *c2; + void *cfg; + + ap_assert(c1); + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, c1, + "h2_c2: create for c1(%ld)", c1->id); + + /* We create a pool with its own allocator to be used for + * processing a request. This is the only way to have the processing + * independent of its parent pool in the sense that it can work in + * another thread. + */ + apr_pool_create(&pool, parent); + apr_pool_tag(pool, "h2_c2_conn"); + + c2 = (conn_rec *) apr_palloc(pool, sizeof(conn_rec)); + memcpy(c2, c1, sizeof(conn_rec)); + + c2->master = c1; + c2->pool = pool; + c2->conn_config = ap_create_conn_config(pool); + c2->notes = apr_table_make(pool, 5); + c2->input_filters = NULL; + c2->output_filters = NULL; + c2->keepalives = 0; +#if AP_MODULE_MAGIC_AT_LEAST(20180903, 1) + c2->filter_conn_ctx = NULL; +#endif + c2->bucket_alloc = apr_bucket_alloc_create(pool); +#if !AP_MODULE_MAGIC_AT_LEAST(20180720, 1) + c2->data_in_input_filters = 0; + c2->data_in_output_filters = 0; +#endif + /* prevent mpm_event from making wrong assumptions about this connection, + * like e.g. using its socket for an async read check. */ + c2->clogging_input_filters = 1; + c2->log = NULL; + c2->aborted = 0; + /* We cannot install the master connection socket on the secondary, as + * modules mess with timeouts/blocking of the socket, with + * unwanted side effects to the master connection processing. + * Fortunately, since we never use the secondary socket, we can just install + * a single, process-wide dummy and everyone is happy. + */ + ap_set_module_config(c2->conn_config, &core_module, dummy_socket); + /* TODO: these should be unique to this thread */ + c2->sbh = NULL; /*c1->sbh;*/ + /* TODO: not all mpm modules have learned about secondary connections yet. + * copy their config from master to secondary. + */ + if (mpm_module) { + cfg = ap_get_module_config(c1->conn_config, mpm_module); + ap_set_module_config(c2->conn_config, mpm_module, cfg); + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, c2, + "h2_c2(%s): created", c2->log_id); + return c2; +} + +static int h2_c2_hook_post_read_request(request_rec *r) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(r->connection); + + if (conn_ctx && conn_ctx->stream_id && ap_is_initial_req(r)) { + + ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, + "h2_c2(%s-%d): adding request filters", + conn_ctx->id, conn_ctx->stream_id); + + /* setup the correct filters to process the request for h2 */ + ap_add_input_filter("H2_C2_REQUEST_IN", NULL, r, r->connection); + + /* replace the core http filter that formats response headers + * in HTTP/1 with our own that collects status and headers */ + ap_remove_output_filter_byhandle(r->output_filters, "HTTP_HEADER"); + + ap_add_output_filter("H2_C2_RESPONSE_OUT", NULL, r, r->connection); + ap_add_output_filter("H2_C2_TRAILERS_OUT", NULL, r, r->connection); + } + return DECLINED; +} + +static int h2_c2_hook_process(conn_rec* c) +{ + h2_conn_ctx_t *ctx; + + if (!c->master) { + return DECLINED; + } + + ctx = h2_conn_ctx_get(c); + if (ctx->stream_id) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "h2_h2, processing request directly"); + c2_process(ctx, c); + return DONE; + } + else { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "secondary_conn(%ld): no h2 stream assing?", c->id); + } + return DECLINED; +} + +void h2_c2_register_hooks(void) +{ + /* When the connection processing actually starts, we might + * take over, if the connection is for a h2 stream. + */ + ap_hook_process_connection(h2_c2_hook_process, + NULL, NULL, APR_HOOK_FIRST); + /* We need to manipulate the standard HTTP/1.1 protocol filters and + * install our own. This needs to be done very early. */ + ap_hook_post_read_request(h2_c2_hook_post_read_request, NULL, NULL, APR_HOOK_REALLY_FIRST); + ap_hook_fixups(c2_hook_fixups, NULL, NULL, APR_HOOK_LAST); +#if H2_USE_POLLFD_FROM_CONN + ap_hook_get_pollfd_from_conn(http2_get_pollfd_from_conn, NULL, NULL, + APR_HOOK_MIDDLE); +#endif + APR_REGISTER_OPTIONAL_FN(http2_get_pollfd_from_conn); + + ap_register_input_filter("H2_C2_NET_IN", h2_c2_filter_in, + NULL, AP_FTYPE_NETWORK); + ap_register_output_filter("H2_C2_NET_OUT", h2_c2_filter_out, + NULL, AP_FTYPE_NETWORK); + ap_register_output_filter("H2_C2_NET_CATCH_H1", h2_c2_filter_catch_h1_out, + NULL, AP_FTYPE_NETWORK); + + ap_register_input_filter("H2_C2_REQUEST_IN", h2_c2_filter_request_in, + NULL, AP_FTYPE_PROTOCOL); + ap_register_output_filter("H2_C2_RESPONSE_OUT", h2_c2_filter_response_out, + NULL, AP_FTYPE_PROTOCOL); + ap_register_output_filter("H2_C2_TRAILERS_OUT", h2_c2_filter_trailers_out, + NULL, AP_FTYPE_PROTOCOL); +} + +#endif /* else AP_HAS_RESPONSE_BUCKETS */ diff --git a/modules/http2/h2_c2.h b/modules/http2/h2_c2.h new file mode 100644 index 0000000..f278382 --- /dev/null +++ b/modules/http2/h2_c2.h @@ -0,0 +1,57 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_c2__ +#define __mod_h2__h2_c2__ + +#include <http_core.h> + +#include "h2.h" + +const char *h2_conn_mpm_name(void); +int h2_mpm_supported(void); + +/* Initialize this child process for h2 secondary connection work, + * to be called once during child init before multi processing + * starts. + */ +apr_status_t h2_c2_child_init(apr_pool_t *pool, server_rec *s); + +#if !AP_HAS_RESPONSE_BUCKETS + +conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent, + apr_bucket_alloc_t *buckt_alloc); + +/** + * Process a secondary connection for a HTTP/2 stream request. + */ +apr_status_t h2_c2_process(conn_rec *c, apr_thread_t *thread, int worker_id); + +#endif /* !AP_HAS_RESPONSE_BUCKETS */ + +void h2_c2_destroy(conn_rec *c2); + +/** + * Abort the I/O processing of a secondary connection. And + * in-/output beams will return errors and c2->aborted is set. + * @param c2 the secondary connection to abort + * @param from the connection this is invoked from + */ +void h2_c2_abort(conn_rec *c2, conn_rec *from); + +void h2_c2_register_hooks(void); + +#endif /* defined(__mod_h2__h2_c2__) */ diff --git a/modules/http2/h2_from_h1.c b/modules/http2/h2_c2_filter.c index d69c53c..554f88b 100644 --- a/modules/http2/h2_from_h1.c +++ b/modules/http2/h2_c2_filter.c @@ -30,12 +30,139 @@ #include <util_time.h> #include "h2_private.h" +#include "h2.h" +#include "h2_config.h" +#include "h2_conn_ctx.h" #include "h2_headers.h" -#include "h2_from_h1.h" -#include "h2_task.h" +#include "h2_c1.h" +#include "h2_c2_filter.h" +#include "h2_c2.h" +#include "h2_mplx.h" +#include "h2_request.h" +#include "h2_ws.h" #include "h2_util.h" +#if AP_HAS_RESPONSE_BUCKETS + +apr_status_t h2_c2_filter_notes_out(ap_filter_t *f, apr_bucket_brigade *bb) +{ + apr_bucket *b; + request_rec *r_prev; + ap_bucket_response *resp; + const char *err; + + if (!f->r) { + goto pass; + } + + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) + { + if (AP_BUCKET_IS_RESPONSE(b)) { + resp = b->data; + if (resp->status >= 400 && f->r->prev) { + /* Error responses are commonly handled via internal + * redirects to error documents. That creates a new + * request_rec with 'prev' set to the original. + * Each of these has its onw 'notes'. + * We'd like to copy interesting ones into the current 'r->notes' + * as we reset HTTP/2 stream with H2 specific error codes then. + */ + for (r_prev = f->r; r_prev != NULL; r_prev = r_prev->prev) { + if ((err = apr_table_get(r_prev->notes, "ssl-renegotiate-forbidden"))) { + if (r_prev != f->r) { + apr_table_setn(resp->notes, "ssl-renegotiate-forbidden", err); + } + break; + } + } + } + else if (h2_config_rgeti(f->r, H2_CONF_PUSH) == 0 + && h2_config_sgeti(f->r->server, H2_CONF_PUSH) != 0) { + /* location configuration turns off H2 PUSH handling */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c, + "h2_c2_filter_notes_out, turning PUSH off"); + apr_table_setn(resp->notes, H2_PUSH_MODE_NOTE, "0"); + } + } + } +pass: + return ap_pass_brigade(f->next, bb); +} + +apr_status_t h2_c2_filter_request_in(ap_filter_t *f, + apr_bucket_brigade *bb, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes) +{ + h2_conn_ctx_t *conn_ctx; + apr_bucket *b; + + /* just get out of the way for things we don't want to handle. */ + if (mode != AP_MODE_READBYTES && mode != AP_MODE_GETLINE) { + return ap_get_brigade(f->next, bb, mode, block, readbytes); + } + + /* This filter is a one-time wonder */ + ap_remove_input_filter(f); + + if (f->c->master && (conn_ctx = h2_conn_ctx_get(f->c)) && + conn_ctx->stream_id) { + const h2_request *req = conn_ctx->request; + + if (req->http_status == H2_HTTP_STATUS_UNSET && + req->protocol && !strcmp("websocket", req->protocol)) { + req = h2_ws_rewrite_request(req, f->c, conn_ctx->beam_in == NULL); + if (!req) + return APR_EGENERAL; + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c, + "h2_c2_filter_request_in(%s): adding request bucket", + conn_ctx->id); + b = h2_request_create_bucket(req, f->r); + APR_BRIGADE_INSERT_TAIL(bb, b); + + if (req->http_status != H2_HTTP_STATUS_UNSET) { + /* error was encountered preparing this request */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c, + "h2_c2_filter_request_in(%s): adding error bucket %d", + conn_ctx->id, req->http_status); + b = ap_bucket_error_create(req->http_status, NULL, f->r->pool, + f->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + return APR_SUCCESS; + } + + if (!conn_ctx->beam_in) { + b = apr_bucket_eos_create(f->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + } + + return APR_SUCCESS; + } + + return ap_get_brigade(f->next, bb, mode, block, readbytes); +} + +#else /* AP_HAS_RESPONSE_BUCKETS */ + +#define H2_FILTER_LOG(name, c, level, rv, msg, bb) \ + do { \ + if (APLOG_C_IS_LEVEL((c),(level))) { \ + char buffer[4 * 1024]; \ + apr_size_t len, bmax = sizeof(buffer)/sizeof(buffer[0]); \ + len = h2_util_bb_print(buffer, bmax, "", "", (bb)); \ + ap_log_cerror(APLOG_MARK, (level), rv, (c), \ + "FILTER[%s]: %s %s", \ + (name), (msg), len? buffer : ""); \ + } \ + } while (0) + + /* This routine is called by apr_table_do and merges all instances of * the passed field values into a single array that will be further * processed by some later routine. Originally intended to help split @@ -49,15 +176,15 @@ static int uniq_field_values(void *d, const char *key, const char *val) char *e; char **strpp; int i; - + (void)key; values = (apr_array_header_t *)d; - + e = apr_pstrdup(values->pool, val); - + do { /* Find a non-empty fieldname */ - + while (*e == ',' || apr_isspace(*e)) { ++e; } @@ -71,7 +198,7 @@ static int uniq_field_values(void *d, const char *key, const char *val) if (*e != '\0') { *e++ = '\0'; } - + /* Now add it to values if it isn't already represented. * Could be replaced by a ap_array_strcasecmp() if we had one. */ @@ -85,7 +212,7 @@ static int uniq_field_values(void *d, const char *key, const char *val) *(char **)apr_array_push(values) = start; } } while (*e != '\0'); - + return 1; } @@ -97,82 +224,28 @@ static int uniq_field_values(void *d, const char *key, const char *val) static void fix_vary(request_rec *r) { apr_array_header_t *varies; - + varies = apr_array_make(r->pool, 5, sizeof(char *)); - + /* Extract all Vary fields from the headers_out, separate each into * its comma-separated fieldname values, and then add them to varies * if not already present in the array. */ apr_table_do(uniq_field_values, varies, r->headers_out, "Vary", NULL); - + /* If we found any, replace old Vary fields with unique-ified value */ - + if (varies->nelts > 0) { apr_table_setn(r->headers_out, "Vary", apr_array_pstrcat(r->pool, varies, ',')); } } -static void set_basic_http_header(apr_table_t *headers, request_rec *r, - apr_pool_t *pool) -{ - char *date = NULL; - const char *proxy_date = NULL; - const char *server = NULL; - const char *us = ap_get_server_banner(); - - /* - * keep the set-by-proxy server and date headers, otherwise - * generate a new server header / date header - */ - if (r && r->proxyreq != PROXYREQ_NONE) { - proxy_date = apr_table_get(r->headers_out, "Date"); - if (!proxy_date) { - /* - * proxy_date needs to be const. So use date for the creation of - * our own Date header and pass it over to proxy_date later to - * avoid a compiler warning. - */ - date = apr_palloc(pool, APR_RFC822_DATE_LEN); - ap_recent_rfc822_date(date, r->request_time); - } - server = apr_table_get(r->headers_out, "Server"); - } - else { - date = apr_palloc(pool, APR_RFC822_DATE_LEN); - ap_recent_rfc822_date(date, r? r->request_time : apr_time_now()); - } - - apr_table_setn(headers, "Date", proxy_date ? proxy_date : date ); - if (r) { - apr_table_unset(r->headers_out, "Date"); - } - - if (!server && *us) { - server = us; - } - if (server) { - apr_table_setn(headers, "Server", server); - if (r) { - apr_table_unset(r->headers_out, "Server"); - } - } -} - -static int copy_header(void *ctx, const char *name, const char *value) -{ - apr_table_t *headers = ctx; - - apr_table_add(headers, name, value); - return 1; -} - -static h2_headers *create_response(h2_task *task, request_rec *r) +static h2_headers *create_response(request_rec *r) { const char *clheader; const char *ctype; - apr_table_t *headers; + /* * Now that we are ready to send a response, we need to combine the two * header field tables into a single table. If we don't do this, our @@ -183,7 +256,7 @@ static h2_headers *create_response(h2_task *task, request_rec *r) r->headers_out); apr_table_clear(r->err_headers_out); } - + /* * Remove the 'Vary' header field if the client can't handle it. * Since this will have nasty effects on HTTP/1.1 caches, force @@ -197,7 +270,7 @@ static h2_headers *create_response(h2_task *task, request_rec *r) else { fix_vary(r); } - + /* * Now remove any ETag response header field if earlier processing * says so (such as a 'FileETag None' directive). @@ -205,10 +278,10 @@ static h2_headers *create_response(h2_task *task, request_rec *r) if (apr_table_get(r->notes, "no-etag") != NULL) { apr_table_unset(r->headers_out, "ETag"); } - + /* determine the protocol and whether we should use keepalives. */ ap_set_keepalive(r); - + if (AP_STATUS_IS_HEADER_ONLY(r->status)) { apr_table_unset(r->headers_out, "Transfer-Encoding"); apr_table_unset(r->headers_out, "Content-Length"); @@ -225,18 +298,18 @@ static h2_headers *create_response(h2_task *task, request_rec *r) if (ctype) { apr_table_setn(r->headers_out, "Content-Type", ctype); } - + if (r->content_encoding) { apr_table_setn(r->headers_out, "Content-Encoding", r->content_encoding); } - + if (!apr_is_empty_array(r->content_languages)) { - unsigned int i; + int i; char *token; char **languages = (char **)(r->content_languages->elts); const char *field = apr_table_get(r->headers_out, "Content-Language"); - + while (field && (token = ap_get_list_item(r->pool, &field)) != NULL) { for (i = 0; i < r->content_languages->nelts; ++i) { if (!apr_strnatcasecmp(token, languages[i])) @@ -246,11 +319,11 @@ static h2_headers *create_response(h2_task *task, request_rec *r) *((char **) apr_array_push(r->content_languages)) = token; } } - + field = apr_array_pstrcat(r->pool, r->content_languages, ','); apr_table_setn(r->headers_out, "Content-Language", field); } - + /* * Control cachability for non-cachable responses if not already set by * some other part of the server configuration. @@ -260,7 +333,7 @@ static h2_headers *create_response(h2_task *task, request_rec *r) ap_recent_rfc822_date(date, r->request_time); apr_table_add(r->headers_out, "Expires", date); } - + /* This is a hack, but I can't find anyway around it. The idea is that * we don't want to send out 0 Content-Lengths if it is a head request. * This happens when modules try to outsmart the server, and return @@ -278,29 +351,26 @@ static h2_headers *create_response(h2_task *task, request_rec *r) && !strcmp(clheader, "0")) { apr_table_unset(r->headers_out, "Content-Length"); } - - headers = apr_table_make(r->pool, 10); - - set_basic_http_header(headers, r, r->pool); - if (r->status == HTTP_NOT_MODIFIED) { - apr_table_do(copy_header, headers, r->headers_out, - "ETag", - "Content-Location", - "Expires", - "Cache-Control", - "Vary", - "Warning", - "WWW-Authenticate", - "Proxy-Authenticate", - "Set-Cookie", - "Set-Cookie2", - NULL); + + /* + * keep the set-by-proxy server and date headers, otherwise + * generate a new server header / date header + */ + if (r->proxyreq == PROXYREQ_NONE + || !apr_table_get(r->headers_out, "Date")) { + char *date = apr_palloc(r->pool, APR_RFC822_DATE_LEN); + ap_recent_rfc822_date(date, r->request_time); + apr_table_setn(r->headers_out, "Date", date ); } - else { - apr_table_do(copy_header, headers, r->headers_out, NULL); + if (r->proxyreq == PROXYREQ_NONE + || !apr_table_get(r->headers_out, "Server")) { + const char *us = ap_get_server_banner(); + if (us && *us) { + apr_table_setn(r->headers_out, "Server", us); + } } - - return h2_headers_rcreate(r, r->status, headers, r->pool); + + return h2_headers_rcreate(r, r->status, r->headers_out, r->pool); } typedef enum { @@ -309,13 +379,17 @@ typedef enum { H2_RP_DONE } h2_rp_state_t; -typedef struct h2_response_parser { +typedef struct h2_response_parser h2_response_parser; +struct h2_response_parser { + const char *id; h2_rp_state_t state; - h2_task *task; + conn_rec *c; + apr_pool_t *pool; int http_status; apr_array_header_t *hlines; apr_bucket_brigade *tmp; -} h2_response_parser; + apr_bucket_brigade *saveto; +}; static apr_status_t parse_header(h2_response_parser *parser, char *line) { const char *hline; @@ -325,52 +399,92 @@ static apr_status_t parse_header(h2_response_parser *parser, char *line) { while (line[0] == ' ' || line[0] == '\t') { ++line; } - + plast = apr_array_pop(parser->hlines); if (plast == NULL) { /* not well formed */ return APR_EINVAL; } - hline = apr_psprintf(parser->task->pool, "%s %s", *plast, line); + hline = apr_psprintf(parser->pool, "%s %s", *plast, line); } else { /* new header line */ - hline = apr_pstrdup(parser->task->pool, line); + hline = apr_pstrdup(parser->pool, line); } - APR_ARRAY_PUSH(parser->hlines, const char*) = hline; + APR_ARRAY_PUSH(parser->hlines, const char*) = hline; return APR_SUCCESS; } -static apr_status_t get_line(h2_response_parser *parser, apr_bucket_brigade *bb, +static apr_status_t get_line(h2_response_parser *parser, apr_bucket_brigade *bb, char *line, apr_size_t len) { - h2_task *task = parser->task; apr_status_t status; - + if (!parser->tmp) { - parser->tmp = apr_brigade_create(task->pool, task->c->bucket_alloc); + parser->tmp = apr_brigade_create(parser->pool, parser->c->bucket_alloc); } - status = apr_brigade_split_line(parser->tmp, bb, APR_BLOCK_READ, - HUGE_STRING_LEN); + status = apr_brigade_split_line(parser->tmp, bb, APR_BLOCK_READ, + len); if (status == APR_SUCCESS) { --len; status = apr_brigade_flatten(parser->tmp, line, &len); if (status == APR_SUCCESS) { /* we assume a non-0 containing line and remove trailing crlf. */ line[len] = '\0'; + /* + * XXX: What to do if there is an LF but no CRLF? + * Should we error out? + */ if (len >= 2 && !strcmp(H2_CRLF, line + len - 2)) { len -= 2; line[len] = '\0'; apr_brigade_cleanup(parser->tmp); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, - "h2_task(%s): read response line: %s", - task->id, line); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, parser->c, + "h2_c2(%s): read response line: %s", + parser->id, line); } else { + apr_off_t brigade_length; + + /* + * If the brigade parser->tmp becomes longer than our buffer + * for flattening we never have a chance to get a complete + * line. This can happen if we are called multiple times after + * previous calls did not find a H2_CRLF and we returned + * APR_EAGAIN. In this case parser->tmp (correctly) grows + * with each call to apr_brigade_split_line. + * + * XXX: Currently a stack based buffer of HUGE_STRING_LEN is + * used. This means we cannot cope with lines larger than + * HUGE_STRING_LEN which might be an issue. + */ + status = apr_brigade_length(parser->tmp, 0, &brigade_length); + if ((status != APR_SUCCESS) || (brigade_length > (apr_off_t)len)) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, parser->c, APLOGNO(10257) + "h2_c2(%s): read response, line too long", + parser->id); + return APR_ENOSPC; + } /* this does not look like a complete line yet */ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, - "h2_task(%s): read response, incomplete line: %s", - task->id, line); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, parser->c, + "h2_c2(%s): read response, incomplete line: %s", + parser->id, line); + if (!parser->saveto) { + parser->saveto = apr_brigade_create(parser->pool, + parser->c->bucket_alloc); + } + /* + * Be on the save side and save the parser->tmp brigade + * as it could contain transient buckets which could be + * invalid next time we are here. + * + * NULL for the filter parameter is ok since we + * provide our own brigade as second parameter + * and ap_save_brigade does not need to create one. + */ + ap_save_brigade(NULL, &(parser->saveto), &(parser->tmp), + parser->tmp->p); + APR_BRIGADE_CONCAT(parser->tmp, parser->saveto); return APR_EAGAIN; } } @@ -381,19 +495,18 @@ static apr_status_t get_line(h2_response_parser *parser, apr_bucket_brigade *bb, static apr_table_t *make_table(h2_response_parser *parser) { - h2_task *task = parser->task; apr_array_header_t *hlines = parser->hlines; if (hlines) { - apr_table_t *headers = apr_table_make(task->pool, hlines->nelts); + apr_table_t *headers = apr_table_make(parser->pool, hlines->nelts); int i; - + for (i = 0; i < hlines->nelts; ++i) { char *hline = ((char **)hlines->elts)[i]; char *sep = ap_strchr(hline, ':'); if (!sep) { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, task->c, - APLOGNO(02955) "h2_task(%s): invalid header[%d] '%s'", - task->id, i, (char*)hline); + ap_log_cerror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, parser->c, + APLOGNO(02955) "h2_c2(%s): invalid header[%d] '%s'", + parser->id, i, (char*)hline); /* not valid format, abort */ return NULL; } @@ -401,52 +514,51 @@ static apr_table_t *make_table(h2_response_parser *parser) while (*sep == ' ' || *sep == '\t') { ++sep; } - - if (!h2_util_ignore_header(hline)) { + + if (!h2_util_ignore_resp_header(hline)) { apr_table_merge(headers, hline, sep); } } return headers; } else { - return apr_table_make(task->pool, 0); + return apr_table_make(parser->pool, 0); } } -static apr_status_t pass_response(h2_task *task, ap_filter_t *f, - h2_response_parser *parser) +static apr_status_t pass_response(h2_conn_ctx_t *conn_ctx, ap_filter_t *f, + h2_response_parser *parser) { apr_bucket *b; apr_status_t status; - - h2_headers *response = h2_headers_create(parser->http_status, + h2_headers *response = h2_headers_create(parser->http_status, make_table(parser), - NULL, 0, task->pool); + parser->c->notes, + 0, parser->pool); apr_brigade_cleanup(parser->tmp); - b = h2_bucket_headers_create(task->c->bucket_alloc, response); + b = h2_bucket_headers_create(parser->c->bucket_alloc, response); + APR_BRIGADE_INSERT_TAIL(parser->tmp, b); + b = apr_bucket_flush_create(parser->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(parser->tmp, b); - b = apr_bucket_flush_create(task->c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(parser->tmp, b); status = ap_pass_brigade(f->next, parser->tmp); apr_brigade_cleanup(parser->tmp); - + /* reset parser for possible next response */ parser->state = H2_RP_STATUS_LINE; apr_array_clear(parser->hlines); if (response->status >= 200) { - task->output.sent_response = 1; + conn_ctx->has_final_response = 1; } - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, - APLOGNO(03197) "h2_task(%s): passed response %d", - task->id, response->status); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, parser->c, + APLOGNO(03197) "h2_c2(%s): passed response %d", + parser->id, response->status); return status; } -static apr_status_t parse_status(h2_task *task, char *line) +static apr_status_t parse_status(h2_response_parser *parser, char *line) { - h2_response_parser *parser = task->output.rparser; - int sindex = (apr_date_checkmask(line, "HTTP/#.# ###*")? 9 : + int sindex = (apr_date_checkmask(line, "HTTP/#.# ###*")? 9 : (apr_date_checkmask(line, "HTTP/# ###*")? 7 : 0)); if (sindex > 0) { int k = sindex + 3; @@ -455,37 +567,29 @@ static apr_status_t parse_status(h2_task *task, char *line) parser->http_status = atoi(&line[sindex]); line[k] = keepchar; parser->state = H2_RP_HEADER_LINE; - + return APR_SUCCESS; } /* Seems like there is garbage on the connection. May be a leftover - * from a previous proxy request. - * This should only happen if the H2_RESPONSE filter is not yet in + * from a previous proxy request. + * This should only happen if the H2_RESPONSE filter is not yet in * place (post_read_request has not been reached and the handler wants * to write something. Probably just the interim response we are * waiting for. But if there is other data hanging around before * that, this needs to fail. */ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, APLOGNO(03467) - "h2_task(%s): unable to parse status line: %s", - task->id, line); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, parser->c, APLOGNO(03467) + "h2_c2(%s): unable to parse status line: %s", + parser->id, line); return APR_EINVAL; } -apr_status_t h2_from_h1_parse_response(h2_task *task, ap_filter_t *f, - apr_bucket_brigade *bb) +static apr_status_t parse_response(h2_response_parser *parser, + h2_conn_ctx_t *conn_ctx, + ap_filter_t* f, apr_bucket_brigade *bb) { - h2_response_parser *parser = task->output.rparser; char line[HUGE_STRING_LEN]; apr_status_t status = APR_SUCCESS; - if (!parser) { - parser = apr_pcalloc(task->pool, sizeof(*parser)); - parser->task = task; - parser->state = H2_RP_STATUS_LINE; - parser->hlines = apr_array_make(task->pool, 10, sizeof(char *)); - task->output.rparser = parser; - } - while (!APR_BRIGADE_EMPTY(bb) && status == APR_SUCCESS) { switch (parser->state) { case H2_RP_STATUS_LINE: @@ -500,21 +604,21 @@ apr_status_t h2_from_h1_parse_response(h2_task *task, ap_filter_t *f, } if (parser->state == H2_RP_STATUS_LINE) { /* instead of parsing, just take it directly */ - status = parse_status(task, line); + status = parse_status(parser, line); } else if (line[0] == '\0') { /* end of headers, pass response onward */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_task(%s): end of response", task->id); - return pass_response(task, f, parser); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, parser->c, + "h2_c2(%s): end of response", parser->id); + return pass_response(conn_ctx, f, parser); } else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_task(%s): response header %s", task->id, line); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, parser->c, + "h2_c2(%s): response header %s", parser->id, line); status = parse_header(parser, line); } break; - + default: return status; } @@ -522,18 +626,74 @@ apr_status_t h2_from_h1_parse_response(h2_task *task, ap_filter_t *f, return status; } -apr_status_t h2_filter_headers_out(ap_filter_t *f, apr_bucket_brigade *bb) +apr_status_t h2_c2_filter_catch_h1_out(ap_filter_t* f, apr_bucket_brigade* bb) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); + h2_response_parser *parser = f->ctx; + apr_status_t rv; + + ap_assert(conn_ctx); + H2_FILTER_LOG("c2_catch_h1_out", f->c, APLOG_TRACE2, 0, "check", bb); + + if (!f->c->aborted && !conn_ctx->has_final_response) { + if (!parser) { + parser = apr_pcalloc(f->c->pool, sizeof(*parser)); + parser->id = apr_psprintf(f->c->pool, "%s-%d", conn_ctx->id, conn_ctx->stream_id); + parser->pool = f->c->pool; + parser->c = f->c; + parser->state = H2_RP_STATUS_LINE; + parser->hlines = apr_array_make(parser->pool, 10, sizeof(char *)); + f->ctx = parser; + } + + if (!APR_BRIGADE_EMPTY(bb)) { + apr_bucket *b = APR_BRIGADE_FIRST(bb); + if (AP_BUCKET_IS_EOR(b)) { + /* TODO: Yikes, this happens when errors are encountered on input + * before anything from the repsonse has been processed. The + * ap_die_r() call will do nothing in certain conditions. + */ + int result = ap_map_http_request_error(conn_ctx->last_err, + HTTP_INTERNAL_SERVER_ERROR); + request_rec *r = h2_create_request_rec(conn_ctx->request, f->c, 1); + if (r) { + ap_die((result >= 400)? result : HTTP_INTERNAL_SERVER_ERROR, r); + b = ap_bucket_eor_create(f->c->bucket_alloc, r); + APR_BRIGADE_INSERT_TAIL(bb, b); + } + } + } + /* There are cases where we need to parse a serialized http/1.1 response. + * One example is a 100-continue answer via a mod_proxy setup. */ + while (bb && !f->c->aborted && !conn_ctx->has_final_response) { + rv = parse_response(parser, conn_ctx, f, bb); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, f->c, + "h2_c2(%s): parsed response", parser->id); + if (APR_BRIGADE_EMPTY(bb) || APR_SUCCESS != rv) { + return rv; + } + } + } + + return ap_pass_brigade(f->next, bb); +} + +apr_status_t h2_c2_filter_response_out(ap_filter_t *f, apr_bucket_brigade *bb) { - h2_task *task = f->ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); request_rec *r = f->r; apr_bucket *b, *bresp, *body_bucket = NULL, *next; ap_bucket_error *eb = NULL; h2_headers *response = NULL; int headers_passing = 0; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_task(%s): output_filter called", task->id); - - if (!task->output.sent_response && !f->c->aborted) { + + H2_FILTER_LOG("c2_response_out", f->c, APLOG_TRACE1, 0, "called with", bb); + + if (f->c->aborted || !conn_ctx || conn_ctx->has_final_response) { + return ap_pass_brigade(f->next, bb); + } + + if (!conn_ctx->has_final_response) { /* check, if we need to send the response now. Until we actually * see a DATA bucket or some EOS/EOR, we do not do so. */ for (b = APR_BRIGADE_FIRST(bb); @@ -549,39 +709,40 @@ apr_status_t h2_filter_headers_out(ap_filter_t *f, apr_bucket_brigade *bb) */ ap_remove_output_filter(f); ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c, - "h2_task(%s): eoc bucket passed", task->id); + "h2_c2(%s): eoc bucket passed", conn_ctx->id); return ap_pass_brigade(f->next, bb); } else if (H2_BUCKET_IS_HEADERS(b)) { headers_passing = 1; } - else if (!APR_BUCKET_IS_FLUSH(b)) { + else if (!APR_BUCKET_IS_FLUSH(b)) { body_bucket = b; break; } } - + if (eb) { int st = eb->status; ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c, APLOGNO(03047) - "h2_task(%s): err bucket status=%d", task->id, st); + "h2_c2(%s): err bucket status=%d", + conn_ctx->id, st); /* throw everything away and replace it with the error response * generated by ap_die() */ apr_brigade_cleanup(bb); ap_die(st, r); return AP_FILTER_ERROR; } - + if (body_bucket || !headers_passing) { /* time to insert the response bucket before the body or if * no h2_headers is passed, e.g. the response is empty */ - response = create_response(task, r); + response = create_response(r); if (response == NULL) { ap_log_cerror(APLOG_MARK, APLOG_NOTICE, 0, f->c, APLOGNO(03048) - "h2_task(%s): unable to create response", task->id); + "h2_c2(%s): unable to create response", conn_ctx->id); return APR_ENOMEM; } - + bresp = h2_bucket_headers_create(f->c->bucket_alloc, response); if (body_bucket) { APR_BUCKET_INSERT_BEFORE(body_bucket, bresp); @@ -589,200 +750,219 @@ apr_status_t h2_filter_headers_out(ap_filter_t *f, apr_bucket_brigade *bb) else { APR_BRIGADE_INSERT_HEAD(bb, bresp); } - task->output.sent_response = 1; + conn_ctx->has_final_response = 1; r->sent_bodyct = 1; + ap_remove_output_filter_byhandle(f->r->output_filters, "H2_C2_NET_CATCH_H1"); } } - - if (r->header_only) { + + if (r->header_only || AP_STATUS_IS_HEADER_ONLY(r->status)) { ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_task(%s): header_only, cleanup output brigade", - task->id); + "h2_c2(%s): headers only, cleanup output brigade", conn_ctx->id); b = body_bucket? body_bucket : APR_BRIGADE_FIRST(bb); while (b != APR_BRIGADE_SENTINEL(bb)) { next = APR_BUCKET_NEXT(b); if (APR_BUCKET_IS_EOS(b) || AP_BUCKET_IS_EOR(b)) { break; - } - APR_BUCKET_REMOVE(b); - apr_bucket_destroy(b); + } + if (!H2_BUCKET_IS_HEADERS(b)) { + APR_BUCKET_REMOVE(b); + apr_bucket_destroy(b); + } b = next; } } - else if (task->output.sent_response) { + if (conn_ctx->has_final_response) { /* lets get out of the way, our task is done */ ap_remove_output_filter(f); } return ap_pass_brigade(f->next, bb); } -static void make_chunk(h2_task *task, apr_bucket_brigade *bb, - apr_bucket *first, apr_off_t chunk_len, + +struct h2_chunk_filter_t { + const char *id; + int eos_chunk_added; + apr_bucket_brigade *bbchunk; + apr_off_t chunked_total; +}; +typedef struct h2_chunk_filter_t h2_chunk_filter_t; + + +static void make_chunk(conn_rec *c, h2_chunk_filter_t *fctx, apr_bucket_brigade *bb, + apr_bucket *first, apr_off_t chunk_len, apr_bucket *tail) { /* Surround the buckets [first, tail[ with new buckets carrying the * HTTP/1.1 chunked encoding format. If tail is NULL, the chunk extends * to the end of the brigade. */ char buffer[128]; - apr_bucket *c; - int len; - - len = apr_snprintf(buffer, H2_ALEN(buffer), - "%"APR_UINT64_T_HEX_FMT"\r\n", (apr_uint64_t)chunk_len); - c = apr_bucket_heap_create(buffer, len, NULL, bb->bucket_alloc); - APR_BUCKET_INSERT_BEFORE(first, c); - c = apr_bucket_heap_create("\r\n", 2, NULL, bb->bucket_alloc); + apr_bucket *b; + apr_size_t len; + + len = (apr_size_t)apr_snprintf(buffer, H2_ALEN(buffer), + "%"APR_UINT64_T_HEX_FMT"\r\n", (apr_uint64_t)chunk_len); + b = apr_bucket_heap_create(buffer, len, NULL, bb->bucket_alloc); + APR_BUCKET_INSERT_BEFORE(first, b); + b = apr_bucket_immortal_create("\r\n", 2, bb->bucket_alloc); if (tail) { - APR_BUCKET_INSERT_BEFORE(tail, c); + APR_BUCKET_INSERT_BEFORE(tail, b); } else { - APR_BRIGADE_INSERT_TAIL(bb, c); + APR_BRIGADE_INSERT_TAIL(bb, b); } - task->input.chunked_total += chunk_len; - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, task->c, - "h2_task(%s): added chunk %ld, total %ld", - task->id, (long)chunk_len, (long)task->input.chunked_total); + fctx->chunked_total += chunk_len; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "h2_c2(%s): added chunk %ld, total %ld", + fctx->id, (long)chunk_len, (long)fctx->chunked_total); } -static int ser_header(void *ctx, const char *name, const char *value) +static int ser_header(void *ctx, const char *name, const char *value) { apr_bucket_brigade *bb = ctx; apr_brigade_printf(bb, NULL, NULL, "%s: %s\r\n", name, value); return 1; } -static apr_status_t read_and_chunk(ap_filter_t *f, h2_task *task, +static apr_status_t read_and_chunk(ap_filter_t *f, h2_conn_ctx_t *conn_ctx, apr_read_type_e block) { + h2_chunk_filter_t *fctx = f->ctx; request_rec *r = f->r; apr_status_t status = APR_SUCCESS; - apr_bucket_brigade *bb = task->input.bbchunk; - - if (!bb) { - bb = apr_brigade_create(r->pool, f->c->bucket_alloc); - task->input.bbchunk = bb; - } - - if (APR_BRIGADE_EMPTY(bb)) { + + if (!fctx->bbchunk) { + fctx->bbchunk = apr_brigade_create(r->pool, f->c->bucket_alloc); + } + + if (APR_BRIGADE_EMPTY(fctx->bbchunk)) { apr_bucket *b, *next, *first_data = NULL; apr_bucket_brigade *tmp; apr_off_t bblen = 0; /* get more data from the lower layer filters. Always do this * in larger pieces, since we handle the read modes ourself. */ - status = ap_get_brigade(f->next, bb, - AP_MODE_READBYTES, block, 32*1024); - if (status == APR_EOF) { - if (!task->input.eos) { - status = apr_brigade_puts(bb, NULL, NULL, "0\r\n\r\n"); - task->input.eos = 1; - return APR_SUCCESS; - } - ap_remove_input_filter(f); - return status; - - } - else if (status != APR_SUCCESS) { + status = ap_get_brigade(f->next, fctx->bbchunk, + AP_MODE_READBYTES, block, conn_ctx->mplx->stream_max_mem); + if (status != APR_SUCCESS) { return status; } - for (b = APR_BRIGADE_FIRST(bb); - b != APR_BRIGADE_SENTINEL(bb) && !task->input.eos; + for (b = APR_BRIGADE_FIRST(fctx->bbchunk); + b != APR_BRIGADE_SENTINEL(fctx->bbchunk); b = next) { next = APR_BUCKET_NEXT(b); if (APR_BUCKET_IS_METADATA(b)) { if (first_data) { - make_chunk(task, bb, first_data, bblen, b); + make_chunk(f->c, fctx, fctx->bbchunk, first_data, bblen, b); first_data = NULL; } - + if (H2_BUCKET_IS_HEADERS(b)) { h2_headers *headers = h2_bucket_headers_get(b); - + ap_assert(headers); ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, - "h2_task(%s): receiving trailers", task->id); - tmp = apr_brigade_split_ex(bb, b, NULL); + "h2_c2(%s-%d): receiving trailers", + conn_ctx->id, conn_ctx->stream_id); + tmp = apr_brigade_split_ex(fctx->bbchunk, b, NULL); if (!apr_is_empty_table(headers->headers)) { - status = apr_brigade_puts(bb, NULL, NULL, "0\r\n"); - apr_table_do(ser_header, bb, headers->headers, NULL); - status = apr_brigade_puts(bb, NULL, NULL, "\r\n"); + status = apr_brigade_puts(fctx->bbchunk, NULL, NULL, "0\r\n"); + apr_table_do(ser_header, fctx->bbchunk, headers->headers, NULL); + status = apr_brigade_puts(fctx->bbchunk, NULL, NULL, "\r\n"); } else { - status = apr_brigade_puts(bb, NULL, NULL, "0\r\n\r\n"); + status = apr_brigade_puts(fctx->bbchunk, NULL, NULL, "0\r\n\r\n"); } r->trailers_in = apr_table_clone(r->pool, headers->headers); APR_BUCKET_REMOVE(b); apr_bucket_destroy(b); - APR_BRIGADE_CONCAT(bb, tmp); + APR_BRIGADE_CONCAT(fctx->bbchunk, tmp); apr_brigade_destroy(tmp); - task->input.eos = 1; + fctx->eos_chunk_added = 1; } else if (APR_BUCKET_IS_EOS(b)) { - tmp = apr_brigade_split_ex(bb, b, NULL); - status = apr_brigade_puts(bb, NULL, NULL, "0\r\n\r\n"); - APR_BRIGADE_CONCAT(bb, tmp); - apr_brigade_destroy(tmp); - task->input.eos = 1; + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "h2_c2(%s-%d): receiving eos", + conn_ctx->id, conn_ctx->stream_id); + if (!fctx->eos_chunk_added) { + tmp = apr_brigade_split_ex(fctx->bbchunk, b, NULL); + status = apr_brigade_puts(fctx->bbchunk, NULL, NULL, "0\r\n\r\n"); + APR_BRIGADE_CONCAT(fctx->bbchunk, tmp); + apr_brigade_destroy(tmp); + } + fctx->eos_chunk_added = 0; } } else if (b->length == 0) { APR_BUCKET_REMOVE(b); apr_bucket_destroy(b); - } + } else { if (!first_data) { first_data = b; bblen = 0; } bblen += b->length; - } + } } - + if (first_data) { - make_chunk(task, bb, first_data, bblen, NULL); - } + make_chunk(f->c, fctx, fctx->bbchunk, first_data, bblen, NULL); + } } return status; } -apr_status_t h2_filter_request_in(ap_filter_t* f, - apr_bucket_brigade* bb, - ap_input_mode_t mode, - apr_read_type_e block, - apr_off_t readbytes) +apr_status_t h2_c2_filter_request_in(ap_filter_t* f, + apr_bucket_brigade* bb, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes) { - h2_task *task = f->ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); + h2_chunk_filter_t *fctx = f->ctx; request_rec *r = f->r; apr_status_t status = APR_SUCCESS; apr_bucket *b, *next; core_server_config *conf = (core_server_config *) ap_get_module_config(r->server->module_config, &core_module); + ap_assert(conn_ctx); + + if (!fctx) { + fctx = apr_pcalloc(r->pool, sizeof(*fctx)); + fctx->id = apr_psprintf(r->pool, "%s-%d", conn_ctx->id, conn_ctx->stream_id); + f->ctx = fctx; + } ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, f->r, - "h2_task(%s): request filter, exp=%d", task->id, r->expecting_100); - if (!task->request->chunked) { + "h2_c2(%s-%d): request input, mode=%d, block=%d, " + "readbytes=%ld, exp=%d", + conn_ctx->id, conn_ctx->stream_id, mode, block, + (long)readbytes, r->expecting_100); + if (!conn_ctx->input_chunked) { status = ap_get_brigade(f->next, bb, mode, block, readbytes); /* pipe data through, just take care of trailers */ - for (b = APR_BRIGADE_FIRST(bb); + for (b = APR_BRIGADE_FIRST(bb); b != APR_BRIGADE_SENTINEL(bb); b = next) { next = APR_BUCKET_NEXT(b); if (H2_BUCKET_IS_HEADERS(b)) { h2_headers *headers = h2_bucket_headers_get(b); ap_assert(headers); ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, - "h2_task(%s): receiving trailers", task->id); + "h2_c2(%s-%d): receiving trailers", + conn_ctx->id, conn_ctx->stream_id); r->trailers_in = headers->headers; if (conf && conf->merge_trailers == AP_MERGE_TRAILERS_ENABLE) { r->headers_in = apr_table_overlay(r->pool, r->headers_in, - r->trailers_in); + r->trailers_in); } APR_BUCKET_REMOVE(b); apr_bucket_destroy(b); ap_remove_input_filter(f); - - if (headers->raw_bytes && h2_task_logio_add_bytes_in) { - h2_task_logio_add_bytes_in(task->c, headers->raw_bytes); + + if (headers->raw_bytes && h2_c_logio_add_bytes_in) { + h2_c_logio_add_bytes_in(f->c, headers->raw_bytes); } break; } @@ -795,56 +975,55 @@ apr_status_t h2_filter_request_in(ap_filter_t* f, * transfer encoding and trailers. * We need to simulate chunked encoding for it to be happy. */ - if ((status = read_and_chunk(f, task, block)) != APR_SUCCESS) { + if ((status = read_and_chunk(f, conn_ctx, block)) != APR_SUCCESS) { return status; } - + if (mode == AP_MODE_EXHAUSTIVE) { /* return all we have */ - APR_BRIGADE_CONCAT(bb, task->input.bbchunk); + APR_BRIGADE_CONCAT(bb, fctx->bbchunk); } else if (mode == AP_MODE_READBYTES) { - status = h2_brigade_concat_length(bb, task->input.bbchunk, readbytes); + status = h2_brigade_concat_length(bb, fctx->bbchunk, readbytes); } else if (mode == AP_MODE_SPECULATIVE) { - status = h2_brigade_copy_length(bb, task->input.bbchunk, readbytes); + status = h2_brigade_copy_length(bb, fctx->bbchunk, readbytes); } else if (mode == AP_MODE_GETLINE) { - /* we are reading a single LF line, e.g. the HTTP headers. + /* we are reading a single LF line, e.g. the HTTP headers. * this has the nasty side effect to split the bucket, even * though it ends with CRLF and creates a 0 length bucket */ - status = apr_brigade_split_line(bb, task->input.bbchunk, block, - HUGE_STRING_LEN); + status = apr_brigade_split_line(bb, fctx->bbchunk, block, HUGE_STRING_LEN); if (APLOGctrace1(f->c)) { char buffer[1024]; apr_size_t len = sizeof(buffer)-1; apr_brigade_flatten(bb, buffer, &len); buffer[len] = 0; ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, - "h2_task(%s): getline: %s", - task->id, buffer); + "h2_c2(%s-%d): getline: %s", + conn_ctx->id, conn_ctx->stream_id, buffer); } } else { /* Hmm, well. There is mode AP_MODE_EATCRLF, but we chose not * to support it. Seems to work. */ ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOTIMPL, f->c, - APLOGNO(02942) - "h2_task, unsupported READ mode %d", mode); + APLOGNO(02942) + "h2_c2, unsupported READ mode %d", mode); status = APR_ENOTIMPL; } - - h2_util_bb_log(f->c, task->stream_id, APLOG_TRACE2, "forwarding input", bb); + + h2_util_bb_log(f->c, conn_ctx->stream_id, APLOG_TRACE2, "returning input", bb); return status; } -apr_status_t h2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb) +apr_status_t h2_c2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb) { - h2_task *task = f->ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); request_rec *r = f->r; apr_bucket *b, *e; - - if (task && r) { + + if (conn_ctx && r) { /* Detect the EOS/EOR bucket and forward any trailers that may have * been set to our h2_headers. */ @@ -856,9 +1035,10 @@ apr_status_t h2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb) && r->trailers_out && !apr_is_empty_table(r->trailers_out)) { h2_headers *headers; apr_table_t *trailers; - + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c, APLOGNO(03049) - "h2_task(%s): sending trailers", task->id); + "h2_c2(%s-%d): sending trailers", + conn_ctx->id, conn_ctx->stream_id); trailers = apr_table_clone(r->pool, r->trailers_out); headers = h2_headers_rcreate(r, HTTP_OK, trailers, r->pool); e = h2_bucket_headers_create(bb->bucket_alloc, headers); @@ -867,9 +1047,10 @@ apr_status_t h2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb) ap_remove_output_filter(f); break; } - } + } } - + return ap_pass_brigade(f->next, bb); } +#endif /* else #if AP_HAS_RESPONSE_BUCKETS */ diff --git a/modules/http2/h2_from_h1.h b/modules/http2/h2_c2_filter.h index 68a24fd..c6f50dd 100644 --- a/modules/http2/h2_from_h1.h +++ b/modules/http2/h2_c2_filter.h @@ -14,8 +14,31 @@ * limitations under the License. */ -#ifndef __mod_h2__h2_from_h1__ -#define __mod_h2__h2_from_h1__ +#ifndef __mod_h2__h2_c2_filter__ +#define __mod_h2__h2_c2_filter__ + +#include "h2.h" + +/** + * Input filter on secondary connections that insert the REQUEST bucket + * with the request to perform and then removes itself. + */ +apr_status_t h2_c2_filter_request_in(ap_filter_t *f, + apr_bucket_brigade *bb, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t readbytes); + +#if AP_HAS_RESPONSE_BUCKETS + +/** + * Output filter that inspects the request_rec->notes of the request + * itself and possible internal redirects to detect conditions that + * merit specific HTTP/2 response codes, such as 421. + */ +apr_status_t h2_c2_filter_notes_out(ap_filter_t *f, apr_bucket_brigade *bb); + +#else /* AP_HAS_RESPONSE_BUCKETS */ /** * h2_from_h1 parses a HTTP/1.1 response into @@ -24,7 +47,7 @@ * - a series of bytes that represent the response body alone, without * any meta data, such as inserted by chunked transfer encoding. * - * All data is allocated from the stream memory pool. + * All data is allocated from the stream memory pool. * * Again, see comments in h2_request: ideally we would take the headers * and status from the httpd structures instead of parsing them here, but @@ -32,19 +55,14 @@ * processing, so this seems to be the way for now. */ struct h2_headers; -struct h2_task; +struct h2_response_parser; -apr_status_t h2_from_h1_parse_response(struct h2_task *task, ap_filter_t *f, - apr_bucket_brigade *bb); +apr_status_t h2_c2_filter_catch_h1_out(ap_filter_t* f, apr_bucket_brigade* bb); -apr_status_t h2_filter_headers_out(ap_filter_t *f, apr_bucket_brigade *bb); +apr_status_t h2_c2_filter_response_out(ap_filter_t *f, apr_bucket_brigade *bb); -apr_status_t h2_filter_request_in(ap_filter_t* f, - apr_bucket_brigade* brigade, - ap_input_mode_t mode, - apr_read_type_e block, - apr_off_t readbytes); +apr_status_t h2_c2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb); -apr_status_t h2_filter_trailers_out(ap_filter_t *f, apr_bucket_brigade *bb); +#endif /* else AP_HAS_RESPONSE_BUCKETS */ -#endif /* defined(__mod_h2__h2_from_h1__) */ +#endif /* defined(__mod_h2__h2_c2_filter__) */ diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c index 8766355..22653d4 100644 --- a/modules/http2/h2_config.c +++ b/modules/http2/h2_config.c @@ -30,11 +30,10 @@ #include <apr_strings.h> #include "h2.h" -#include "h2_alt_svc.h" -#include "h2_ctx.h" -#include "h2_conn.h" +#include "h2_conn_ctx.h" +#include "h2_c1.h" #include "h2_config.h" -#include "h2_h2.h" +#include "h2_protocol.h" #include "h2_private.h" #define DEF_VAL (-1) @@ -42,17 +41,65 @@ #define H2_CONFIG_GET(a, b, n) \ (((a)->n == DEF_VAL)? (b) : (a))->n +#define H2_CONFIG_SET(a, n, v) \ + ((a)->n = v) + +#define CONFIG_CMD_SET(cmd,dir,var,val) \ + h2_config_seti(((cmd)->path? (dir) : NULL), h2_config_sget((cmd)->server), var, val) + +#define CONFIG_CMD_SET64(cmd,dir,var,val) \ + h2_config_seti64(((cmd)->path? (dir) : NULL), h2_config_sget((cmd)->server), var, val) + +/* Apache httpd module configuration for h2. */ +typedef struct h2_config { + const char *name; + int h2_max_streams; /* max concurrent # streams (http2) */ + int h2_window_size; /* stream window size (http2) */ + int min_workers; /* min # of worker threads/child */ + int max_workers; /* max # of worker threads/child */ + apr_interval_time_t idle_limit; /* max duration for idle workers */ + int stream_max_mem_size; /* max # bytes held in memory/stream */ + int h2_direct; /* if mod_h2 is active directly */ + int modern_tls_only; /* Accept only modern TLS in HTTP/2 connections */ + int h2_upgrade; /* Allow HTTP/1 upgrade to h2/h2c */ + apr_int64_t tls_warmup_size; /* Amount of TLS data to send before going full write size */ + int tls_cooldown_secs; /* Seconds of idle time before going back to small TLS records */ + int h2_push; /* if HTTP/2 server push is enabled */ + struct apr_hash_t *priorities; /* map of content-type to h2_priority records */ + + int push_diary_size; /* # of entries in push diary */ + int copy_files; /* if files shall be copied vs setaside on output */ + apr_array_header_t *push_list; /* list of h2_push_res configurations */ + apr_table_t *early_headers; /* HTTP headers for a 103 response */ + int early_hints; /* support status code 103 */ + int padding_bits; + int padding_always; + 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; + +typedef struct h2_dir_config { + const char *name; + int h2_upgrade; /* Allow HTTP/1 upgrade to h2/h2c */ + int h2_push; /* if HTTP/2 server push is enabled */ + apr_array_header_t *push_list; /* list of h2_push_res configurations */ + apr_table_t *early_headers; /* HTTP headers for a 103 response */ + int early_hints; /* support status code 103 */ + apr_interval_time_t stream_timeout;/* beam timeout */ +} h2_dir_config; + + static h2_config defconf = { "default", 100, /* max_streams */ H2_INITIAL_WINDOW_SIZE, /* window_size */ -1, /* min workers */ -1, /* max workers */ - 10 * 60, /* max workers idle secs */ + apr_time_from_sec(10 * 60), /* workers idle limit */ 32 * 1024, /* stream max mem size */ - NULL, /* no alt-svcs */ - -1, /* alt-svc max age */ - 0, /* serialize headers */ -1, /* h2 direct mode */ 1, /* modern TLS only */ -1, /* HTTP/1 Upgrade support */ @@ -63,7 +110,25 @@ static h2_config defconf = { 256, /* push diary size */ 0, /* copy files across threads */ NULL, /* push list */ + NULL, /* early headers */ 0, /* early hints, http status 103 */ + 0, /* padding bits */ + 1, /* padding always */ + 1, /* stream output buffered */ + -1, /* beam timeout */ + 0, /* max DATA frame len, 0 == no extra limit */ + 0, /* forward proxy */ + 0, /* WebSockets negotiation, enabled */ +}; + +static h2_dir_config defdconf = { + "default", + -1, /* HTTP/1 Upgrade support */ + -1, /* HTTP/2 server push enabled */ + NULL, /* push list */ + NULL, /* early headers */ + -1, /* early hints, http status 103 */ + -1, /* beam timeout */ }; void h2_config_init(apr_pool_t *pool) @@ -71,22 +136,18 @@ void h2_config_init(apr_pool_t *pool) (void)pool; } -static void *h2_config_create(apr_pool_t *pool, - const char *prefix, const char *x) +void *h2_config_create_svr(apr_pool_t *pool, server_rec *s) { h2_config *conf = (h2_config *)apr_pcalloc(pool, sizeof(h2_config)); - const char *s = x? x : "unknown"; - char *name = apr_pstrcat(pool, prefix, "[", s, "]", NULL); + char *name = apr_pstrcat(pool, "srv[", s->defn_name, "]", NULL); conf->name = name; conf->h2_max_streams = DEF_VAL; conf->h2_window_size = DEF_VAL; conf->min_workers = DEF_VAL; conf->max_workers = DEF_VAL; - conf->max_worker_idle_secs = DEF_VAL; + conf->idle_limit = DEF_VAL; conf->stream_max_mem_size = DEF_VAL; - conf->alt_svc_max_age = DEF_VAL; - conf->serialize_headers = DEF_VAL; conf->h2_direct = DEF_VAL; conf->modern_tls_only = DEF_VAL; conf->h2_upgrade = DEF_VAL; @@ -97,20 +158,18 @@ static void *h2_config_create(apr_pool_t *pool, conf->push_diary_size = DEF_VAL; conf->copy_files = DEF_VAL; conf->push_list = NULL; + conf->early_headers = NULL; conf->early_hints = DEF_VAL; + conf->padding_bits = DEF_VAL; + conf->padding_always = DEF_VAL; + 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; } -void *h2_config_create_svr(apr_pool_t *pool, server_rec *s) -{ - return h2_config_create(pool, "srv", s->defn_name); -} - -void *h2_config_create_dir(apr_pool_t *pool, char *x) -{ - return h2_config_create(pool, "dir", x); -} - static void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv) { h2_config *base = (h2_config *)basev; @@ -123,11 +182,8 @@ static void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv) n->h2_window_size = H2_CONFIG_GET(add, base, h2_window_size); n->min_workers = H2_CONFIG_GET(add, base, min_workers); n->max_workers = H2_CONFIG_GET(add, base, max_workers); - n->max_worker_idle_secs = H2_CONFIG_GET(add, base, max_worker_idle_secs); + n->idle_limit = H2_CONFIG_GET(add, base, idle_limit); n->stream_max_mem_size = H2_CONFIG_GET(add, base, stream_max_mem_size); - n->alt_svcs = add->alt_svcs? add->alt_svcs : base->alt_svcs; - n->alt_svc_max_age = H2_CONFIG_GET(add, base, alt_svc_max_age); - n->serialize_headers = H2_CONFIG_GET(add, base, serialize_headers); n->h2_direct = H2_CONFIG_GET(add, base, h2_direct); n->modern_tls_only = H2_CONFIG_GET(add, base, modern_tls_only); n->h2_upgrade = H2_CONFIG_GET(add, base, h2_upgrade); @@ -142,32 +198,75 @@ static void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv) } n->push_diary_size = H2_CONFIG_GET(add, base, push_diary_size); n->copy_files = H2_CONFIG_GET(add, base, copy_files); + n->output_buffered = H2_CONFIG_GET(add, base, output_buffered); if (add->push_list && base->push_list) { n->push_list = apr_array_append(pool, base->push_list, add->push_list); } else { n->push_list = add->push_list? add->push_list : base->push_list; } + if (add->early_headers && base->early_headers) { + n->early_headers = apr_table_overlay(pool, add->early_headers, base->early_headers); + } + else { + n->early_headers = add->early_headers? add->early_headers : base->early_headers; + } n->early_hints = H2_CONFIG_GET(add, base, early_hints); + n->padding_bits = H2_CONFIG_GET(add, base, padding_bits); + 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; } -void *h2_config_merge_dir(apr_pool_t *pool, void *basev, void *addv) +void *h2_config_merge_svr(apr_pool_t *pool, void *basev, void *addv) { return h2_config_merge(pool, basev, addv); } -void *h2_config_merge_svr(apr_pool_t *pool, void *basev, void *addv) +void *h2_config_create_dir(apr_pool_t *pool, char *x) { - return h2_config_merge(pool, basev, addv); + h2_dir_config *conf = (h2_dir_config *)apr_pcalloc(pool, sizeof(h2_dir_config)); + const char *s = x? x : "unknown"; + char *name = apr_pstrcat(pool, "dir[", s, "]", NULL); + + conf->name = name; + conf->h2_upgrade = DEF_VAL; + conf->h2_push = DEF_VAL; + conf->early_hints = DEF_VAL; + conf->stream_timeout = DEF_VAL; + return conf; } -int h2_config_geti(const h2_config *conf, h2_config_var_t var) +void *h2_config_merge_dir(apr_pool_t *pool, void *basev, void *addv) { - return (int)h2_config_geti64(conf, var); + h2_dir_config *base = (h2_dir_config *)basev; + h2_dir_config *add = (h2_dir_config *)addv; + h2_dir_config *n = (h2_dir_config *)apr_pcalloc(pool, sizeof(h2_dir_config)); + + n->name = apr_pstrcat(pool, "merged[", add->name, ", ", base->name, "]", NULL); + n->h2_upgrade = H2_CONFIG_GET(add, base, h2_upgrade); + n->h2_push = H2_CONFIG_GET(add, base, h2_push); + if (add->push_list && base->push_list) { + n->push_list = apr_array_append(pool, base->push_list, add->push_list); + } + else { + n->push_list = add->push_list? add->push_list : base->push_list; + } + if (add->early_headers && base->early_headers) { + n->early_headers = apr_table_overlay(pool, add->early_headers, base->early_headers); + } + else { + n->early_headers = add->early_headers? add->early_headers : base->early_headers; + } + n->early_hints = H2_CONFIG_GET(add, base, early_hints); + n->stream_timeout = H2_CONFIG_GET(add, base, stream_timeout); + return n; } -apr_int64_t h2_config_geti64(const h2_config *conf, h2_config_var_t var) +static apr_int64_t h2_srv_config_geti64(const h2_config *conf, h2_config_var_t var) { switch(var) { case H2_CONF_MAX_STREAMS: @@ -178,14 +277,10 @@ apr_int64_t h2_config_geti64(const h2_config *conf, h2_config_var_t var) return H2_CONFIG_GET(conf, &defconf, min_workers); case H2_CONF_MAX_WORKERS: return H2_CONFIG_GET(conf, &defconf, max_workers); - case H2_CONF_MAX_WORKER_IDLE_SECS: - return H2_CONFIG_GET(conf, &defconf, max_worker_idle_secs); + case H2_CONF_MAX_WORKER_IDLE_LIMIT: + return H2_CONFIG_GET(conf, &defconf, idle_limit); case H2_CONF_STREAM_MAX_MEM: return H2_CONFIG_GET(conf, &defconf, stream_max_mem_size); - case H2_CONF_ALT_SVC_MAX_AGE: - return H2_CONFIG_GET(conf, &defconf, alt_svc_max_age); - case H2_CONF_SER_HEADERS: - return H2_CONFIG_GET(conf, &defconf, serialize_headers); case H2_CONF_MODERN_TLS_ONLY: return H2_CONFIG_GET(conf, &defconf, modern_tls_only); case H2_CONF_UPGRADE: @@ -204,12 +299,112 @@ apr_int64_t h2_config_geti64(const h2_config *conf, h2_config_var_t var) return H2_CONFIG_GET(conf, &defconf, copy_files); case H2_CONF_EARLY_HINTS: return H2_CONFIG_GET(conf, &defconf, early_hints); + case H2_CONF_PADDING_BITS: + return H2_CONFIG_GET(conf, &defconf, padding_bits); + case H2_CONF_PADDING_ALWAYS: + return H2_CONFIG_GET(conf, &defconf, padding_always); + case H2_CONF_OUTPUT_BUFFER: + return H2_CONFIG_GET(conf, &defconf, output_buffered); + case H2_CONF_STREAM_TIMEOUT: + 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: return DEF_VAL; } } -const h2_config *h2_config_sget(server_rec *s) +static void h2_srv_config_seti(h2_config *conf, h2_config_var_t var, int val) +{ + switch(var) { + case H2_CONF_MAX_STREAMS: + H2_CONFIG_SET(conf, h2_max_streams, val); + break; + case H2_CONF_WIN_SIZE: + H2_CONFIG_SET(conf, h2_window_size, val); + break; + case H2_CONF_MIN_WORKERS: + H2_CONFIG_SET(conf, min_workers, val); + break; + case H2_CONF_MAX_WORKERS: + H2_CONFIG_SET(conf, max_workers, val); + break; + case H2_CONF_STREAM_MAX_MEM: + H2_CONFIG_SET(conf, stream_max_mem_size, val); + break; + case H2_CONF_MODERN_TLS_ONLY: + H2_CONFIG_SET(conf, modern_tls_only, val); + break; + case H2_CONF_UPGRADE: + H2_CONFIG_SET(conf, h2_upgrade, val); + break; + case H2_CONF_DIRECT: + H2_CONFIG_SET(conf, h2_direct, val); + break; + case H2_CONF_TLS_WARMUP_SIZE: + H2_CONFIG_SET(conf, tls_warmup_size, val); + break; + case H2_CONF_TLS_COOLDOWN_SECS: + H2_CONFIG_SET(conf, tls_cooldown_secs, val); + break; + case H2_CONF_PUSH: + H2_CONFIG_SET(conf, h2_push, val); + break; + case H2_CONF_PUSH_DIARY_SIZE: + H2_CONFIG_SET(conf, push_diary_size, val); + break; + case H2_CONF_COPY_FILES: + H2_CONFIG_SET(conf, copy_files, val); + break; + case H2_CONF_EARLY_HINTS: + H2_CONFIG_SET(conf, early_hints, val); + break; + case H2_CONF_PADDING_BITS: + H2_CONFIG_SET(conf, padding_bits, val); + break; + case H2_CONF_PADDING_ALWAYS: + H2_CONFIG_SET(conf, padding_always, val); + break; + case H2_CONF_OUTPUT_BUFFER: + H2_CONFIG_SET(conf, output_buffered, val); + break; + 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); + break; + case H2_CONF_WEBSOCKETS: + H2_CONFIG_SET(conf, h2_websockets, val); + break; + default: + break; + } +} + +static void h2_srv_config_seti64(h2_config *conf, h2_config_var_t var, apr_int64_t val) +{ + switch(var) { + case H2_CONF_TLS_WARMUP_SIZE: + H2_CONFIG_SET(conf, tls_warmup_size, val); + break; + case H2_CONF_STREAM_TIMEOUT: + H2_CONFIG_SET(conf, stream_timeout, val); + break; + case H2_CONF_MAX_WORKER_IDLE_LIMIT: + H2_CONFIG_SET(conf, idle_limit, val); + break; + default: + h2_srv_config_seti(conf, var, (int)val); + break; + } +} + +static h2_config *h2_config_sget(server_rec *s) { h2_config *cfg = (h2_config *)ap_get_module_config(s->module_config, &http2_module); @@ -217,177 +412,310 @@ const h2_config *h2_config_sget(server_rec *s) return cfg; } -const struct h2_priority *h2_config_get_priority(const h2_config *conf, - const char *content_type) +static const h2_dir_config *h2_config_rget(request_rec *r) +{ + h2_dir_config *cfg = (h2_dir_config *)ap_get_module_config(r->per_dir_config, + &http2_module); + ap_assert(cfg); + return cfg; +} + +static apr_int64_t h2_dir_config_geti64(const h2_dir_config *conf, h2_config_var_t var) +{ + switch(var) { + case H2_CONF_UPGRADE: + return H2_CONFIG_GET(conf, &defdconf, h2_upgrade); + case H2_CONF_PUSH: + return H2_CONFIG_GET(conf, &defdconf, h2_push); + case H2_CONF_EARLY_HINTS: + return H2_CONFIG_GET(conf, &defdconf, early_hints); + case H2_CONF_STREAM_TIMEOUT: + return H2_CONFIG_GET(conf, &defdconf, stream_timeout); + + default: + return DEF_VAL; + } +} + +static void h2_config_seti(h2_dir_config *dconf, h2_config *conf, h2_config_var_t var, int val) +{ + int set_srv = !dconf; + if (dconf) { + switch(var) { + case H2_CONF_UPGRADE: + H2_CONFIG_SET(dconf, h2_upgrade, val); + break; + case H2_CONF_PUSH: + H2_CONFIG_SET(dconf, h2_push, val); + break; + case H2_CONF_EARLY_HINTS: + H2_CONFIG_SET(dconf, early_hints, val); + break; + default: + /* not handled in dir_conf */ + set_srv = 1; + break; + } + } + + if (set_srv) { + h2_srv_config_seti(conf, var, val); + } +} + +static void h2_config_seti64(h2_dir_config *dconf, h2_config *conf, h2_config_var_t var, apr_int64_t val) +{ + int set_srv = !dconf; + if (dconf) { + switch(var) { + case H2_CONF_STREAM_TIMEOUT: + H2_CONFIG_SET(dconf, stream_timeout, val); + break; + default: + /* not handled in dir_conf */ + set_srv = 1; + break; + } + } + + if (set_srv) { + h2_srv_config_seti64(conf, var, val); + } +} + +static const h2_config *h2_config_get(conn_rec *c) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + + if (conn_ctx && conn_ctx->server) { + return h2_config_sget(conn_ctx->server); + } + return h2_config_sget(c->base_server); +} + +int h2_config_cgeti(conn_rec *c, h2_config_var_t var) { + return (int)h2_srv_config_geti64(h2_config_get(c), var); +} + +apr_int64_t h2_config_cgeti64(conn_rec *c, h2_config_var_t var) +{ + return h2_srv_config_geti64(h2_config_get(c), var); +} + +int h2_config_sgeti(server_rec *s, h2_config_var_t var) +{ + return (int)h2_srv_config_geti64(h2_config_sget(s), var); +} + +apr_int64_t h2_config_sgeti64(server_rec *s, h2_config_var_t var) +{ + return h2_srv_config_geti64(h2_config_sget(s), var); +} + +int h2_config_geti(request_rec *r, server_rec *s, h2_config_var_t var) +{ + return (int)h2_config_geti64(r, s, var); +} + +apr_int64_t h2_config_geti64(request_rec *r, server_rec *s, h2_config_var_t var) +{ + apr_int64_t mode = r? (int)h2_dir_config_geti64(h2_config_rget(r), var) : DEF_VAL; + return (mode != DEF_VAL)? mode : h2_config_sgeti64(s, var); +} + +int h2_config_rgeti(request_rec *r, h2_config_var_t var) +{ + return h2_config_geti(r, r->server, var); +} + +apr_int64_t h2_config_rgeti64(request_rec *r, h2_config_var_t var) +{ + return h2_config_geti64(r, r->server, var); +} + +apr_array_header_t *h2_config_push_list(request_rec *r) +{ + const h2_config *sconf; + const h2_dir_config *conf = h2_config_rget(r); + + if (conf && conf->push_list) { + return conf->push_list; + } + sconf = h2_config_sget(r->server); + return sconf? sconf->push_list : NULL; +} + +apr_table_t *h2_config_early_headers(request_rec *r) +{ + const h2_config *sconf; + const h2_dir_config *conf = h2_config_rget(r); + + if (conf && conf->early_headers) { + return conf->early_headers; + } + sconf = h2_config_sget(r->server); + return sconf? sconf->early_headers : NULL; +} + +const struct h2_priority *h2_cconfig_get_priority(conn_rec *c, const char *content_type) +{ + const h2_config *conf = h2_config_get(c); if (content_type && conf->priorities) { - size_t len = strcspn(content_type, "; \t"); + apr_ssize_t len = (apr_ssize_t)strcspn(content_type, "; \t"); h2_priority *prio = apr_hash_get(conf->priorities, content_type, len); return prio? prio : apr_hash_get(conf->priorities, "*", 1); } return NULL; } -static const char *h2_conf_set_max_streams(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_max_streams(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->h2_max_streams = (int)apr_atoi64(value); - (void)arg; - if (cfg->h2_max_streams < 1) { + apr_int64_t ival = (int)apr_atoi64(value); + if (ival < 1) { return "value must be > 0"; } + CONFIG_CMD_SET64(cmd, dirconf, H2_CONF_MAX_STREAMS, ival); return NULL; } -static const char *h2_conf_set_window_size(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_window_size(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->h2_window_size = (int)apr_atoi64(value); - (void)arg; - if (cfg->h2_window_size < 1024) { + int val = (int)apr_atoi64(value); + if (val < 1024) { return "value must be >= 1024"; } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WIN_SIZE, val); return NULL; } -static const char *h2_conf_set_min_workers(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_min_workers(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->min_workers = (int)apr_atoi64(value); - (void)arg; - if (cfg->min_workers < 1) { + int val = (int)apr_atoi64(value); + if (val < 1) { return "value must be > 0"; } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_MIN_WORKERS, val); return NULL; } -static const char *h2_conf_set_max_workers(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_max_workers(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->max_workers = (int)apr_atoi64(value); - (void)arg; - if (cfg->max_workers < 1) { + int val = (int)apr_atoi64(value); + if (val < 1) { return "value must be > 0"; } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_MAX_WORKERS, val); return NULL; } -static const char *h2_conf_set_max_worker_idle_secs(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_max_worker_idle_limit(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->max_worker_idle_secs = (int)apr_atoi64(value); - (void)arg; - if (cfg->max_worker_idle_secs < 1) { - return "value must be > 0"; + apr_interval_time_t timeout; + apr_status_t rv = ap_timeout_parameter_parse(value, &timeout, "s"); + if (rv != APR_SUCCESS) { + return "Invalid idle limit value"; } + if (timeout <= 0) { + timeout = DEF_VAL; + } + CONFIG_CMD_SET64(cmd, dirconf, H2_CONF_MAX_WORKER_IDLE_LIMIT, timeout); return NULL; } -static const char *h2_conf_set_stream_max_mem_size(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_stream_max_mem_size(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - - - cfg->stream_max_mem_size = (int)apr_atoi64(value); - (void)arg; - if (cfg->stream_max_mem_size < 1024) { + int val = (int)apr_atoi64(value); + if (val < 1024) { return "value must be >= 1024"; } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_STREAM_MAX_MEM, val); return NULL; } -static const char *h2_add_alt_svc(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_max_data_frame_len(cmd_parms *cmd, + void *dirconf, const char *value) { - if (value && *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - h2_alt_svc *as = h2_alt_svc_parse(value, parms->pool); - if (!as) { - return "unable to parse alt-svc specifier"; - } - if (!cfg->alt_svcs) { - cfg->alt_svcs = apr_array_make(parms->pool, 5, sizeof(h2_alt_svc*)); - } - APR_ARRAY_PUSH(cfg->alt_svcs, h2_alt_svc*) = as; + int val = (int)apr_atoi64(value); + if (val < 0) { + return "value must be 0 or larger"; } - (void)arg; - return NULL; -} - -static const char *h2_conf_set_alt_svc_max_age(cmd_parms *parms, - void *arg, const char *value) -{ - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->alt_svc_max_age = (int)apr_atoi64(value); - (void)arg; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_MAX_DATA_FRAME_LEN, val); return NULL; } -static const char *h2_conf_set_session_extra_files(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_session_extra_files(cmd_parms *cmd, + void *dirconf, const char *value) { /* deprecated, ignore */ - (void)arg; + (void)dirconf; (void)value; - ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, parms->pool, /* NO LOGNO */ + ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, cmd->pool, /* NO LOGNO */ "H2SessionExtraFiles is obsolete and will be ignored"); return NULL; } static const char *h2_conf_set_serialize_headers(cmd_parms *parms, - void *arg, const char *value) + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); if (!strcasecmp(value, "On")) { - cfg->serialize_headers = 1; + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, parms->server, APLOGNO(10307) + "%s: this feature has been disabled and the directive " + "to enable it is ignored.", parms->cmd->name); + } + return NULL; +} + +static const char *h2_conf_set_direct(cmd_parms *cmd, + void *dirconf, const char *value) +{ + if (!strcasecmp(value, "On")) { + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_DIRECT, 1); return NULL; } else if (!strcasecmp(value, "Off")) { - cfg->serialize_headers = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_DIRECT, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } -static const char *h2_conf_set_direct(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_push(cmd_parms *cmd, void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); if (!strcasecmp(value, "On")) { - cfg->h2_direct = 1; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_PUSH, 1); return NULL; } else if (!strcasecmp(value, "Off")) { - cfg->h2_direct = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_PUSH, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } -static const char *h2_conf_set_push(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_websockets(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); if (!strcasecmp(value, "On")) { - cfg->h2_push = 1; +#if H2_USE_WEBSOCKETS + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 1); return NULL; +#elif !H2_USE_PIPES + return "HTTP/2 WebSockets are not supported on this platform"; +#else + return "HTTP/2 WebSockets are not supported in this server version"; +#endif } else if (!strcasecmp(value, "Off")) { - cfg->h2_push = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } @@ -400,7 +728,8 @@ static const char *h2_conf_add_push_priority(cmd_parms *cmd, void *_cfg, h2_dependency dependency; h2_priority *priority; int weight; - + + (void)_cfg; if (!*ctype) { return "1st argument must be a mime-type, like 'text/css' or '*'"; } @@ -419,7 +748,7 @@ static const char *h2_conf_add_push_priority(cmd_parms *cmd, void *_cfg, else if (!strcasecmp("BEFORE", sdependency)) { dependency = H2_DEPENDANT_BEFORE; if (sweight) { - return "dependecy 'Before' does not allow a weight"; + return "dependency 'Before' does not allow a weight"; } } else if (!strcasecmp("INTERLEAVED", sdependency)) { @@ -443,104 +772,92 @@ static const char *h2_conf_add_push_priority(cmd_parms *cmd, void *_cfg, if (!cfg->priorities) { cfg->priorities = apr_hash_make(cmd->pool); } - apr_hash_set(cfg->priorities, ctype, strlen(ctype), priority); + apr_hash_set(cfg->priorities, ctype, (apr_ssize_t)strlen(ctype), priority); return NULL; } -static const char *h2_conf_set_modern_tls_only(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_modern_tls_only(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); if (!strcasecmp(value, "On")) { - cfg->modern_tls_only = 1; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_MODERN_TLS_ONLY, 1); return NULL; } else if (!strcasecmp(value, "Off")) { - cfg->modern_tls_only = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_MODERN_TLS_ONLY, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } -static const char *h2_conf_set_upgrade(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_upgrade(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); if (!strcasecmp(value, "On")) { - cfg->h2_upgrade = 1; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_UPGRADE, 1); return NULL; } else if (!strcasecmp(value, "Off")) { - cfg->h2_upgrade = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_UPGRADE, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } -static const char *h2_conf_set_tls_warmup_size(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_tls_warmup_size(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->tls_warmup_size = apr_atoi64(value); - (void)arg; + apr_int64_t val = apr_atoi64(value); + CONFIG_CMD_SET64(cmd, dirconf, H2_CONF_TLS_WARMUP_SIZE, val); return NULL; } -static const char *h2_conf_set_tls_cooldown_secs(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_tls_cooldown_secs(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - cfg->tls_cooldown_secs = (int)apr_atoi64(value); - (void)arg; + apr_int64_t val = (int)apr_atoi64(value); + CONFIG_CMD_SET64(cmd, dirconf, H2_CONF_TLS_COOLDOWN_SECS, val); return NULL; } -static const char *h2_conf_set_push_diary_size(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_push_diary_size(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); - (void)arg; - cfg->push_diary_size = (int)apr_atoi64(value); - if (cfg->push_diary_size < 0) { + int val = (int)apr_atoi64(value); + if (val < 0) { return "value must be >= 0"; } - if (cfg->push_diary_size > 0 && (cfg->push_diary_size & (cfg->push_diary_size-1))) { + if (val > 0 && (val & (val-1))) { return "value must a power of 2"; } - if (cfg->push_diary_size > (1 << 15)) { + if (val > (1 << 15)) { return "value must <= 65536"; } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_PUSH_DIARY_SIZE, val); return NULL; } -static const char *h2_conf_set_copy_files(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_copy_files(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)arg; if (!strcasecmp(value, "On")) { - cfg->copy_files = 1; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_COPY_FILES, 1); return NULL; } else if (!strcasecmp(value, "Off")) { - cfg->copy_files = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_COPY_FILES, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } -static void add_push(apr_pool_t *pool, h2_config *conf, h2_push_res *push) +static void add_push(apr_array_header_t **plist, apr_pool_t *pool, h2_push_res *push) { h2_push_res *new; - if (!conf->push_list) { - conf->push_list = apr_array_make(pool, 10, sizeof(*push)); + if (!*plist) { + *plist = apr_array_make(pool, 10, sizeof(*push)); } - new = apr_array_push(conf->push_list); + new = apr_array_push(*plist); new->uri_ref = push->uri_ref; new->critical = push->critical; } @@ -549,8 +866,6 @@ static const char *h2_conf_add_push_res(cmd_parms *cmd, void *dirconf, const char *arg1, const char *arg2, const char *arg3) { - h2_config *dconf = (h2_config*)dirconf ; - h2_config *sconf = (h2_config*)h2_config_sget(cmd->server); h2_push_res push; const char *last = arg3; @@ -575,57 +890,136 @@ static const char *h2_conf_add_push_res(cmd_parms *cmd, void *dirconf, } } - /* server command? set both */ - if (cmd->path == NULL) { - add_push(cmd->pool, sconf, &push); - add_push(cmd->pool, dconf, &push); + if (cmd->path) { + add_push(&(((h2_dir_config*)dirconf)->push_list), cmd->pool, &push); } else { - add_push(cmd->pool, dconf, &push); + add_push(&(h2_config_sget(cmd->server)->push_list), cmd->pool, &push); } + return NULL; +} + +static const char *h2_conf_add_early_hint(cmd_parms *cmd, void *dirconf, + const char *name, const char *value) +{ + apr_table_t *hds, **phds; + + if(!name || !*name) + return "Early Hint header name must not be empty"; + if(!value) + return "Early Hint header value must not be empty"; + while (apr_isspace(*value)) + ++value; + if(!*value) + return "Early Hint header value must not be empty/only space"; + if (*ap_scan_http_field_content(value)) + return "Early Hint header value contains invalid characters"; + + if (cmd->path) { + phds = &((h2_dir_config*)dirconf)->early_headers; + } + else { + phds = &(h2_config_sget(cmd->server))->early_headers; + } + hds = *phds; + if (!hds) { + *phds = hds = apr_table_make(cmd->pool, 10); + } + apr_table_add(hds, name, value); return NULL; } -static const char *h2_conf_set_early_hints(cmd_parms *parms, - void *arg, const char *value) +static const char *h2_conf_set_early_hints(cmd_parms *cmd, + void *dirconf, const char *value) +{ + int val; + + if (!strcasecmp(value, "On")) val = 1; + else if (!strcasecmp(value, "Off")) val = 0; + else return "value must be On or Off"; + + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_EARLY_HINTS, val); + if (cmd->path) { + ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, cmd->pool, + "H2EarlyHints = %d on path %s", val, cmd->path); + } + return NULL; +} + +static const char *h2_conf_set_padding(cmd_parms *cmd, void *dirconf, const char *value) +{ + int val; + + val = (int)apr_atoi64(value); + if (val < 0) { + return "number of bits must be >= 0"; + } + if (val > 8) { + return "number of bits must be <= 8"; + } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_PADDING_BITS, val); + return NULL; +} + +static const char *h2_conf_set_output_buffer(cmd_parms *cmd, + void *dirconf, const char *value) { - h2_config *cfg = (h2_config *)h2_config_sget(parms->server); if (!strcasecmp(value, "On")) { - cfg->early_hints = 1; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_OUTPUT_BUFFER, 1); return NULL; } else if (!strcasecmp(value, "Off")) { - cfg->early_hints = 0; + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_OUTPUT_BUFFER, 0); return NULL; } - - (void)arg; return "value must be On or Off"; } -void h2_get_num_workers(server_rec *s, int *minw, int *maxw) +static const char *h2_conf_set_stream_timeout(cmd_parms *cmd, + void *dirconf, const char *value) +{ + apr_status_t rv; + apr_interval_time_t timeout; + + rv = ap_timeout_parameter_parse(value, &timeout, "s"); + if (rv != APR_SUCCESS) { + return "Invalid timeout value"; + } + CONFIG_CMD_SET64(cmd, dirconf, H2_CONF_STREAM_TIMEOUT, timeout); + 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) { int threads_per_child = 0; - const h2_config *config = h2_config_sget(s); - *minw = h2_config_geti(config, H2_CONF_MIN_WORKERS); - *maxw = h2_config_geti(config, H2_CONF_MAX_WORKERS); - ap_mpm_query(AP_MPMQ_MAX_THREADS, &threads_per_child); + *pminw = h2_config_sgeti(s, H2_CONF_MIN_WORKERS); + *pmaxw = h2_config_sgeti(s, H2_CONF_MAX_WORKERS); - if (*minw <= 0) { - *minw = threads_per_child; - } - if (*maxw <= 0) { - /* As a default, this seems to work quite well under mpm_event. - * For people enabling http2 under mpm_prefork, start 4 threads unless - * configured otherwise. People get unhappy if their http2 requests are - * blocking each other. */ - *maxw = 3 * (*minw) / 2; - if (*maxw < 4) { - *maxw = 4; - } + ap_mpm_query(AP_MPMQ_MAX_THREADS, &threads_per_child); + if (*pminw <= 0) { + *pminw = threads_per_child; + } + if (*pmaxw <= 0) { + *pmaxw = H2MAX(4, 3 * (*pminw) / 2); } + *pidle_limit = h2_config_sgeti64(s, H2_CONF_MAX_WORKER_IDLE_LIMIT); } #define AP_END_CMD AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) @@ -639,20 +1033,16 @@ const command_rec h2_cmds[] = { RSRC_CONF, "minimum number of worker threads per child"), AP_INIT_TAKE1("H2MaxWorkers", h2_conf_set_max_workers, NULL, RSRC_CONF, "maximum number of worker threads per child"), - AP_INIT_TAKE1("H2MaxWorkerIdleSeconds", h2_conf_set_max_worker_idle_secs, NULL, + AP_INIT_TAKE1("H2MaxWorkerIdleSeconds", h2_conf_set_max_worker_idle_limit, NULL, RSRC_CONF, "maximum number of idle seconds before a worker shuts down"), AP_INIT_TAKE1("H2StreamMaxMemSize", h2_conf_set_stream_max_mem_size, NULL, RSRC_CONF, "maximum number of bytes buffered in memory for a stream"), - AP_INIT_TAKE1("H2AltSvc", h2_add_alt_svc, NULL, - RSRC_CONF, "adds an Alt-Svc for this server"), - AP_INIT_TAKE1("H2AltSvcMaxAge", h2_conf_set_alt_svc_max_age, NULL, - RSRC_CONF, "set the maximum age (in seconds) that client can rely on alt-svc information"), AP_INIT_TAKE1("H2SerializeHeaders", h2_conf_set_serialize_headers, NULL, - RSRC_CONF, "on to enable header serialization for compatibility"), + RSRC_CONF, "disabled, this directive has no longer an effect."), AP_INIT_TAKE1("H2ModernTLSOnly", h2_conf_set_modern_tls_only, NULL, RSRC_CONF, "off to not impose RFC 7540 restrictions on TLS"), AP_INIT_TAKE1("H2Upgrade", h2_conf_set_upgrade, NULL, - RSRC_CONF, "on to allow HTTP/1 Upgrades to h2/h2c"), + RSRC_CONF|OR_AUTHCFG, "on to allow HTTP/1 Upgrades to h2/h2c"), AP_INIT_TAKE1("H2Direct", h2_conf_set_direct, NULL, RSRC_CONF, "on to enable direct HTTP/2 mode"), AP_INIT_TAKE1("H2SessionExtraFiles", h2_conf_set_session_extra_files, NULL, @@ -662,7 +1052,7 @@ const command_rec h2_cmds[] = { AP_INIT_TAKE1("H2TLSCoolDownSecs", h2_conf_set_tls_cooldown_secs, NULL, RSRC_CONF, "seconds of idle time on TLS before shrinking writes"), AP_INIT_TAKE1("H2Push", h2_conf_set_push, NULL, - RSRC_CONF, "off to disable HTTP/2 server push"), + RSRC_CONF|OR_AUTHCFG, "off to disable HTTP/2 server push"), AP_INIT_TAKE23("H2PushPriority", h2_conf_add_push_priority, NULL, RSRC_CONF, "define priority of PUSHed resources per content type"), AP_INIT_TAKE1("H2PushDiarySize", h2_conf_set_push_diary_size, NULL, @@ -670,33 +1060,24 @@ const command_rec h2_cmds[] = { AP_INIT_TAKE1("H2CopyFiles", h2_conf_set_copy_files, NULL, OR_FILEINFO, "on to perform copy of file data"), AP_INIT_TAKE123("H2PushResource", h2_conf_add_push_res, NULL, - OR_FILEINFO, "add a resource to be pushed in this location/on this server."), + OR_FILEINFO|OR_AUTHCFG, "add a resource to be pushed in this location/on this server."), AP_INIT_TAKE1("H2EarlyHints", h2_conf_set_early_hints, NULL, RSRC_CONF, "on to enable interim status 103 responses"), + AP_INIT_TAKE1("H2Padding", h2_conf_set_padding, NULL, + RSRC_CONF, "set payload padding"), + AP_INIT_TAKE1("H2OutputBuffering", h2_conf_set_output_buffer, NULL, + RSRC_CONF, "set stream output buffer on/off"), + AP_INIT_TAKE1("H2StreamTimeout", h2_conf_set_stream_timeout, NULL, + RSRC_CONF, "set stream timeout"), + AP_INIT_TAKE1("H2MaxDataFrameLen", h2_conf_set_max_data_frame_len, NULL, + 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 }; -const h2_config *h2_config_rget(request_rec *r) -{ - h2_config *cfg = (h2_config *)ap_get_module_config(r->per_dir_config, - &http2_module); - return cfg? cfg : h2_config_sget(r->server); -} - -const h2_config *h2_config_get(conn_rec *c) -{ - h2_ctx *ctx = h2_ctx_get(c, 0); - - if (ctx) { - if (ctx->config) { - return ctx->config; - } - else if (ctx->server) { - ctx->config = h2_config_sget(ctx->server); - return ctx->config; - } - } - - return h2_config_sget(c->base_server); -} diff --git a/modules/http2/h2_config.h b/modules/http2/h2_config.h index 17d75d6..15242db 100644 --- a/modules/http2/h2_config.h +++ b/modules/http2/h2_config.h @@ -28,11 +28,8 @@ typedef enum { H2_CONF_WIN_SIZE, H2_CONF_MIN_WORKERS, H2_CONF_MAX_WORKERS, - H2_CONF_MAX_WORKER_IDLE_SECS, + H2_CONF_MAX_WORKER_IDLE_LIMIT, H2_CONF_STREAM_MAX_MEM, - H2_CONF_ALT_SVCS, - H2_CONF_ALT_SVC_MAX_AGE, - H2_CONF_SER_HEADERS, H2_CONF_DIRECT, H2_CONF_MODERN_TLS_ONLY, H2_CONF_UPGRADE, @@ -42,6 +39,13 @@ typedef enum { H2_CONF_PUSH_DIARY_SIZE, H2_CONF_COPY_FILES, H2_CONF_EARLY_HINTS, + H2_CONF_PADDING_BITS, + H2_CONF_PADDING_ALWAYS, + 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; struct apr_hash_t; @@ -53,33 +57,6 @@ typedef struct h2_push_res { int critical; } h2_push_res; -/* Apache httpd module configuration for h2. */ -typedef struct h2_config { - const char *name; - int h2_max_streams; /* max concurrent # streams (http2) */ - int h2_window_size; /* stream window size (http2) */ - int min_workers; /* min # of worker threads/child */ - int max_workers; /* max # of worker threads/child */ - int max_worker_idle_secs; /* max # of idle seconds for worker */ - int stream_max_mem_size; /* max # bytes held in memory/stream */ - apr_array_header_t *alt_svcs; /* h2_alt_svc specs for this server */ - int alt_svc_max_age; /* seconds clients can rely on alt-svc info*/ - int serialize_headers; /* Use serialized HTTP/1.1 headers for - processing, better compatibility */ - int h2_direct; /* if mod_h2 is active directly */ - int modern_tls_only; /* Accept only modern TLS in HTTP/2 connections */ - int h2_upgrade; /* Allow HTTP/1 upgrade to h2/h2c */ - apr_int64_t tls_warmup_size; /* Amount of TLS data to send before going full write size */ - int tls_cooldown_secs; /* Seconds of idle time before going back to small TLS records */ - int h2_push; /* if HTTP/2 server push is enabled */ - struct apr_hash_t *priorities;/* map of content-type to h2_priority records */ - - int push_diary_size; /* # of entries in push diary */ - int copy_files; /* if files shall be copied vs setaside on output */ - apr_array_header_t *push_list;/* list of h2_push_res configurations */ - int early_hints; /* support status code 103 */ -} h2_config; - void *h2_config_create_dir(apr_pool_t *pool, char *x); void *h2_config_merge_dir(apr_pool_t *pool, void *basev, void *addv); @@ -88,19 +65,38 @@ void *h2_config_merge_svr(apr_pool_t *pool, void *basev, void *addv); extern const command_rec h2_cmds[]; -const h2_config *h2_config_get(conn_rec *c); -const h2_config *h2_config_sget(server_rec *s); -const h2_config *h2_config_rget(request_rec *r); +int h2_config_geti(request_rec *r, server_rec *s, h2_config_var_t var); +apr_int64_t h2_config_geti64(request_rec *r, server_rec *s, h2_config_var_t var); + +/** + * Get the configured value for variable <var> at the given connection. + */ +int h2_config_cgeti(conn_rec *c, h2_config_var_t var); +apr_int64_t h2_config_cgeti64(conn_rec *c, h2_config_var_t var); + +/** + * Get the configured value for variable <var> at the given server. + */ +int h2_config_sgeti(server_rec *s, h2_config_var_t var); +apr_int64_t h2_config_sgeti64(server_rec *s, h2_config_var_t var); + +/** + * Get the configured value for variable <var> at the given request, + * if configured for the request location. + * Fallback to request server config otherwise. + */ +int h2_config_rgeti(request_rec *r, h2_config_var_t var); +apr_int64_t h2_config_rgeti64(request_rec *r, h2_config_var_t var); -int h2_config_geti(const h2_config *conf, h2_config_var_t var); -apr_int64_t h2_config_geti64(const h2_config *conf, h2_config_var_t var); +apr_array_header_t *h2_config_push_list(request_rec *r); +apr_table_t *h2_config_early_headers(request_rec *r); -void h2_get_num_workers(server_rec *s, int *minw, int *maxw); +void h2_get_workers_config(server_rec *s, int *pminw, int *pmaxw, + apr_time_t *pidle_limit); void h2_config_init(apr_pool_t *pool); -const struct h2_priority *h2_config_get_priority(const h2_config *conf, - const char *content_type); +const struct h2_priority *h2_cconfig_get_priority(conn_rec *c, const char *content_type); #endif /* __mod_h2__h2_config_h__ */ diff --git a/modules/http2/h2_conn.c b/modules/http2/h2_conn.c deleted file mode 100644 index 88da2ba..0000000 --- a/modules/http2/h2_conn.c +++ /dev/null @@ -1,370 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <assert.h> -#include <apr_strings.h> - -#include <ap_mpm.h> - -#include <httpd.h> -#include <http_core.h> -#include <http_config.h> -#include <http_log.h> -#include <http_connection.h> -#include <http_protocol.h> -#include <http_request.h> - -#include <mpm_common.h> - -#include "h2_private.h" -#include "h2.h" -#include "h2_config.h" -#include "h2_ctx.h" -#include "h2_filter.h" -#include "h2_mplx.h" -#include "h2_session.h" -#include "h2_stream.h" -#include "h2_h2.h" -#include "h2_task.h" -#include "h2_workers.h" -#include "h2_conn.h" -#include "h2_version.h" - -static struct h2_workers *workers; - -static h2_mpm_type_t mpm_type = H2_MPM_UNKNOWN; -static module *mpm_module; -static int async_mpm; -static int mpm_supported = 1; -static apr_socket_t *dummy_socket; - -static void check_modules(int force) -{ - static int checked = 0; - int i; - - if (force || !checked) { - for (i = 0; ap_loaded_modules[i]; ++i) { - module *m = ap_loaded_modules[i]; - - if (!strcmp("event.c", m->name)) { - mpm_type = H2_MPM_EVENT; - mpm_module = m; - break; - } - else if (!strcmp("motorz.c", m->name)) { - mpm_type = H2_MPM_MOTORZ; - mpm_module = m; - break; - } - else if (!strcmp("mpm_netware.c", m->name)) { - mpm_type = H2_MPM_NETWARE; - mpm_module = m; - break; - } - else if (!strcmp("prefork.c", m->name)) { - mpm_type = H2_MPM_PREFORK; - mpm_module = m; - /* While http2 can work really well on prefork, it collides - * today's use case for prefork: runnning single-thread app engines - * like php. If we restrict h2_workers to 1 per process, php will - * work fine, but browser will be limited to 1 active request at a - * time. */ - mpm_supported = 0; - break; - } - else if (!strcmp("simple_api.c", m->name)) { - mpm_type = H2_MPM_SIMPLE; - mpm_module = m; - mpm_supported = 0; - break; - } - else if (!strcmp("mpm_winnt.c", m->name)) { - mpm_type = H2_MPM_WINNT; - mpm_module = m; - break; - } - else if (!strcmp("worker.c", m->name)) { - mpm_type = H2_MPM_WORKER; - mpm_module = m; - break; - } - } - checked = 1; - } -} - -apr_status_t h2_conn_child_init(apr_pool_t *pool, server_rec *s) -{ - const h2_config *config = h2_config_sget(s); - apr_status_t status = APR_SUCCESS; - int minw, maxw; - int max_threads_per_child = 0; - int idle_secs = 0; - - check_modules(1); - ap_mpm_query(AP_MPMQ_MAX_THREADS, &max_threads_per_child); - - status = ap_mpm_query(AP_MPMQ_IS_ASYNC, &async_mpm); - if (status != APR_SUCCESS) { - /* some MPMs do not implemnent this */ - async_mpm = 0; - status = APR_SUCCESS; - } - - h2_config_init(pool); - - h2_get_num_workers(s, &minw, &maxw); - - idle_secs = h2_config_geti(config, H2_CONF_MAX_WORKER_IDLE_SECS); - ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, s, - "h2_workers: min=%d max=%d, mthrpchild=%d, idle_secs=%d", - minw, maxw, max_threads_per_child, idle_secs); - workers = h2_workers_create(s, pool, minw, maxw, idle_secs); - - ap_register_input_filter("H2_IN", h2_filter_core_input, - NULL, AP_FTYPE_CONNECTION); - - status = h2_mplx_child_init(pool, s); - - if (status == APR_SUCCESS) { - status = apr_socket_create(&dummy_socket, APR_INET, SOCK_STREAM, - APR_PROTO_TCP, pool); - } - - return status; -} - -h2_mpm_type_t h2_conn_mpm_type(void) -{ - check_modules(0); - return mpm_type; -} - -const char *h2_conn_mpm_name(void) -{ - check_modules(0); - return mpm_module? mpm_module->name : "unknown"; -} - -int h2_mpm_supported(void) -{ - check_modules(0); - return mpm_supported; -} - -static module *h2_conn_mpm_module(void) -{ - check_modules(0); - return mpm_module; -} - -apr_status_t h2_conn_setup(h2_ctx *ctx, conn_rec *c, request_rec *r) -{ - h2_session *session; - apr_status_t status; - - if (!workers) { - ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, APLOGNO(02911) - "workers not initialized"); - return APR_EGENERAL; - } - - if (r) { - status = h2_session_rcreate(&session, r, ctx, workers); - } - else { - status = h2_session_create(&session, c, ctx, workers); - } - - if (status == APR_SUCCESS) { - h2_ctx_session_set(ctx, session); - } - return status; -} - -apr_status_t h2_conn_run(struct h2_ctx *ctx, conn_rec *c) -{ - apr_status_t status; - int mpm_state = 0; - h2_session *session = h2_ctx_session_get(ctx); - - ap_assert(session); - do { - if (c->cs) { - c->cs->sense = CONN_SENSE_DEFAULT; - c->cs->state = CONN_STATE_HANDLER; - } - - status = h2_session_process(session, async_mpm); - - if (APR_STATUS_IS_EOF(status)) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, - H2_SSSN_LOG(APLOGNO(03045), session, - "process, closing conn")); - c->keepalive = AP_CONN_CLOSE; - } - else { - c->keepalive = AP_CONN_KEEPALIVE; - } - - if (ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state)) { - break; - } - } while (!async_mpm - && c->keepalive == AP_CONN_KEEPALIVE - && mpm_state != AP_MPMQ_STOPPING); - - if (c->cs) { - switch (session->state) { - case H2_SESSION_ST_INIT: - case H2_SESSION_ST_IDLE: - case H2_SESSION_ST_BUSY: - case H2_SESSION_ST_WAIT: - c->cs->state = CONN_STATE_WRITE_COMPLETION; - break; - case H2_SESSION_ST_CLEANUP: - case H2_SESSION_ST_DONE: - default: - c->cs->state = CONN_STATE_LINGER; - break; - } - } - - return APR_SUCCESS; -} - -apr_status_t h2_conn_pre_close(struct h2_ctx *ctx, conn_rec *c) -{ - h2_session *session = h2_ctx_session_get(ctx); - if (session) { - apr_status_t status = h2_session_pre_close(session, async_mpm); - return (status == APR_SUCCESS)? DONE : status; - } - return DONE; -} - -conn_rec *h2_slave_create(conn_rec *master, int slave_id, apr_pool_t *parent) -{ - apr_allocator_t *allocator; - apr_status_t status; - apr_pool_t *pool; - conn_rec *c; - void *cfg; - module *mpm; - - ap_assert(master); - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, master, - "h2_stream(%ld-%d): create slave", master->id, slave_id); - - /* We create a pool with its own allocator to be used for - * processing a request. This is the only way to have the processing - * independant of its parent pool in the sense that it can work in - * another thread. Also, the new allocator needs its own mutex to - * synchronize sub-pools. - */ - apr_allocator_create(&allocator); - apr_allocator_max_free_set(allocator, ap_max_mem_free); - status = apr_pool_create_ex(&pool, parent, NULL, allocator); - if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_ERR, status, master, - APLOGNO(10004) "h2_session(%ld-%d): create slave pool", - master->id, slave_id); - return NULL; - } - apr_allocator_owner_set(allocator, pool); - apr_pool_tag(pool, "h2_slave_conn"); - - c = (conn_rec *) apr_palloc(pool, sizeof(conn_rec)); - if (c == NULL) { - ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOMEM, master, - APLOGNO(02913) "h2_session(%ld-%d): create slave", - master->id, slave_id); - apr_pool_destroy(pool); - return NULL; - } - - memcpy(c, master, sizeof(conn_rec)); - - c->master = master; - c->pool = pool; - c->conn_config = ap_create_conn_config(pool); - c->notes = apr_table_make(pool, 5); - c->input_filters = NULL; - c->output_filters = NULL; - c->bucket_alloc = apr_bucket_alloc_create(pool); - c->data_in_input_filters = 0; - c->data_in_output_filters = 0; - /* prevent mpm_event from making wrong assumptions about this connection, - * like e.g. using its socket for an async read check. */ - c->clogging_input_filters = 1; - c->log = NULL; - c->log_id = apr_psprintf(pool, "%ld-%d", - master->id, slave_id); - c->aborted = 0; - /* We cannot install the master connection socket on the slaves, as - * modules mess with timeouts/blocking of the socket, with - * unwanted side effects to the master connection processing. - * Fortunately, since we never use the slave socket, we can just install - * a single, process-wide dummy and everyone is happy. - */ - ap_set_module_config(c->conn_config, &core_module, dummy_socket); - /* TODO: these should be unique to this thread */ - c->sbh = master->sbh; - /* TODO: not all mpm modules have learned about slave connections yet. - * copy their config from master to slave. - */ - if ((mpm = h2_conn_mpm_module()) != NULL) { - cfg = ap_get_module_config(master->conn_config, mpm); - ap_set_module_config(c->conn_config, mpm, cfg); - } - - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, - "h2_stream(%ld-%d): created slave", master->id, slave_id); - return c; -} - -void h2_slave_destroy(conn_rec *slave) -{ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, slave, - "h2_stream(%s): destroy slave", - apr_table_get(slave->notes, H2_TASK_ID_NOTE)); - slave->sbh = NULL; - apr_pool_destroy(slave->pool); -} - -apr_status_t h2_slave_run_pre_connection(conn_rec *slave, apr_socket_t *csd) -{ - if (slave->keepalives == 0) { - /* Simulate that we had already a request on this connection. Some - * hooks trigger special behaviour when keepalives is 0. - * (Not necessarily in pre_connection, but later. Set it here, so it - * is in place.) */ - slave->keepalives = 1; - /* We signal that this connection will be closed after the request. - * Which is true in that sense that we throw away all traffic data - * on this slave connection after each requests. Although we might - * reuse internal structures like memory pools. - * The wanted effect of this is that httpd does not try to clean up - * any dangling data on this connection when a request is done. Which - * is unneccessary on a h2 stream. - */ - slave->keepalive = AP_CONN_CLOSE; - return ap_run_pre_connection(slave, csd); - } - return APR_SUCCESS; -} - diff --git a/modules/http2/h2_conn.h b/modules/http2/h2_conn.h deleted file mode 100644 index e45ff31..0000000 --- a/modules/http2/h2_conn.h +++ /dev/null @@ -1,77 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __mod_h2__h2_conn__ -#define __mod_h2__h2_conn__ - -struct h2_ctx; -struct h2_task; - -/** - * Setup the connection and our context for HTTP/2 processing - * - * @param ctx the http2 context to setup - * @param c the connection HTTP/2 is starting on - * @param r the upgrade request that still awaits an answer, optional - */ -apr_status_t h2_conn_setup(struct h2_ctx *ctx, conn_rec *c, request_rec *r); - -/** - * Run the HTTP/2 connection in synchronous fashion. - * Return when the HTTP/2 session is done - * and the connection will close or a fatal error occurred. - * - * @param ctx the http2 context to run - * @return APR_SUCCESS when session is done. - */ -apr_status_t h2_conn_run(struct h2_ctx *ctx, conn_rec *c); - -/** - * The connection is about to close. If we have not send a GOAWAY - * yet, this is the last chance. - */ -apr_status_t h2_conn_pre_close(struct h2_ctx *ctx, conn_rec *c); - -/* Initialize this child process for h2 connection work, - * to be called once during child init before multi processing - * starts. - */ -apr_status_t h2_conn_child_init(apr_pool_t *pool, server_rec *s); - - -typedef enum { - H2_MPM_UNKNOWN, - H2_MPM_WORKER, - H2_MPM_EVENT, - H2_MPM_PREFORK, - H2_MPM_MOTORZ, - H2_MPM_SIMPLE, - H2_MPM_NETWARE, - H2_MPM_WINNT, -} h2_mpm_type_t; - -/* Returns the type of MPM module detected */ -h2_mpm_type_t h2_conn_mpm_type(void); -const char *h2_conn_mpm_name(void); -int h2_mpm_supported(void); - -conn_rec *h2_slave_create(conn_rec *master, int slave_id, apr_pool_t *parent); -void h2_slave_destroy(conn_rec *slave); - -apr_status_t h2_slave_run_pre_connection(conn_rec *slave, apr_socket_t *csd); -void h2_slave_run_connection(conn_rec *slave); - -#endif /* defined(__mod_h2__h2_conn__) */ diff --git a/modules/http2/h2_conn_ctx.c b/modules/http2/h2_conn_ctx.c new file mode 100644 index 0000000..b8a0fb3 --- /dev/null +++ b/modules/http2/h2_conn_ctx.c @@ -0,0 +1,123 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <apr_strings.h> +#include <apr_atomic.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_config.h> +#include <http_log.h> +#include <http_protocol.h> + +#include "h2_private.h" +#include "h2_session.h" +#include "h2_bucket_beam.h" +#include "h2_c2.h" +#include "h2_mplx.h" +#include "h2_stream.h" +#include "h2_util.h" +#include "h2_conn_ctx.h" + + +void h2_conn_ctx_detach(conn_rec *c) +{ + ap_set_module_config(c->conn_config, &http2_module, NULL); +} + +static h2_conn_ctx_t *ctx_create(conn_rec *c, const char *id) +{ + h2_conn_ctx_t *conn_ctx = apr_pcalloc(c->pool, sizeof(*conn_ctx)); + conn_ctx->id = id; + conn_ctx->server = c->base_server; + apr_atomic_set32(&conn_ctx->started, 1); + conn_ctx->started_at = apr_time_now(); + + ap_set_module_config(c->conn_config, &http2_module, conn_ctx); + return conn_ctx; +} + +h2_conn_ctx_t *h2_conn_ctx_create_for_c1(conn_rec *c1, server_rec *s, const char *protocol) +{ + h2_conn_ctx_t *ctx; + + ctx = ctx_create(c1, apr_psprintf(c1->pool, "%ld", c1->id)); + ctx->server = s; + ctx->protocol = apr_pstrdup(c1->pool, protocol); + + ctx->pfd.desc_type = APR_POLL_SOCKET; + ctx->pfd.desc.s = ap_get_conn_socket(c1); + ctx->pfd.reqevents = APR_POLLIN | APR_POLLERR | APR_POLLHUP; + ctx->pfd.client_data = ctx; + apr_socket_opt_set(ctx->pfd.desc.s, APR_SO_NONBLOCK, 1); + + return ctx; +} + +void h2_conn_ctx_assign_session(h2_conn_ctx_t *ctx, struct h2_session *session) +{ + ctx->session = session; + ctx->id = apr_psprintf(session->pool, "%d-%lu", session->child_num, (unsigned long)session->id); +} + +apr_status_t h2_conn_ctx_init_for_c2(h2_conn_ctx_t **pctx, conn_rec *c2, + struct h2_mplx *mplx, struct h2_stream *stream, + struct h2_c2_transit *transit) +{ + h2_conn_ctx_t *conn_ctx; + apr_status_t rv = APR_SUCCESS; + + ap_assert(c2->master); + conn_ctx = h2_conn_ctx_get(c2); + if (!conn_ctx) { + h2_conn_ctx_t *c1_ctx; + + c1_ctx = h2_conn_ctx_get(c2->master); + ap_assert(c1_ctx); + ap_assert(c1_ctx->session); + + conn_ctx = ctx_create(c2, c1_ctx->id); + conn_ctx->server = c2->master->base_server; + } + + conn_ctx->mplx = mplx; + conn_ctx->transit = transit; + conn_ctx->stream_id = stream->id; + apr_pool_create(&conn_ctx->req_pool, c2->pool); + apr_pool_tag(conn_ctx->req_pool, "H2_C2_REQ"); + conn_ctx->request = stream->request; + apr_atomic_set32(&conn_ctx->started, 1); + conn_ctx->started_at = apr_time_now(); + conn_ctx->done = 0; + conn_ctx->done_at = 0; + + *pctx = conn_ctx; + return rv; +} + +void h2_conn_ctx_set_timeout(h2_conn_ctx_t *conn_ctx, apr_interval_time_t timeout) +{ + if (conn_ctx->beam_out) { + h2_beam_timeout_set(conn_ctx->beam_out, timeout); + } + if (conn_ctx->beam_in) { + h2_beam_timeout_set(conn_ctx->beam_in, timeout); + } + if (conn_ctx->pipe_in[H2_PIPE_OUT]) { + apr_file_pipe_timeout_set(conn_ctx->pipe_in[H2_PIPE_OUT], timeout); + } +} diff --git a/modules/http2/h2_conn_ctx.h b/modules/http2/h2_conn_ctx.h new file mode 100644 index 0000000..3b44856 --- /dev/null +++ b/modules/http2/h2_conn_ctx.h @@ -0,0 +1,100 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __mod_h2__h2_conn_ctx__ +#define __mod_h2__h2_conn_ctx__ + +#include "h2.h" + +struct h2_session; +struct h2_stream; +struct h2_mplx; +struct h2_bucket_beam; +struct h2_response_parser; +struct h2_c2_transit; + +#define H2_PIPE_OUT 0 +#define H2_PIPE_IN 1 + +/** + * The h2 module context associated with a connection. + * + * It keeps track of the different types of connections: + * - those from clients that use HTTP/2 protocol + * - those from clients that do not use HTTP/2 + * - those created by ourself to perform work on HTTP/2 streams + */ +struct h2_conn_ctx_t { + const char *id; /* c*: our identifier of this connection */ + server_rec *server; /* c*: httpd server selected. */ + const char *protocol; /* c1: the protocol negotiated */ + struct h2_session *session; /* c1: the h2 session established */ + struct h2_mplx *mplx; /* c2: the multiplexer */ + struct h2_c2_transit *transit; /* c2: transit pool and bucket_alloc */ + +#if !AP_HAS_RESPONSE_BUCKETS + int pre_conn_done; /* has pre_connection setup run? */ +#endif + int stream_id; /* c1: 0, c2: stream id processed */ + apr_pool_t *req_pool; /* c2: a c2 child pool for a request */ + const struct h2_request *request; /* c2: the request to process */ + struct h2_bucket_beam *beam_out; /* c2: data out, created from req_pool */ + struct h2_bucket_beam *beam_in; /* c2: data in or NULL, borrowed from request stream */ + unsigned input_chunked:1; /* c2: if input needs HTTP/1.1 chunking applied */ + unsigned is_upgrade:1; /* c2: if requst is a HTTP Upgrade */ + + apr_file_t *pipe_in[2]; /* c2: input produced notification pipe */ + apr_pollfd_t pfd; /* c1: poll socket input, c2: NUL */ + + int has_final_response; /* final HTTP response passed on out */ + apr_status_t last_err; /* APR_SUCCES or last error encountered in filters */ + + apr_off_t bytes_sent; /* c2: bytes acutaly sent via c1 */ + /* atomic */ apr_uint32_t started; /* c2: processing was started */ + apr_time_t started_at; /* c2: when processing started */ + /* atomic */ apr_uint32_t done; /* c2: processing has finished */ + apr_time_t done_at; /* c2: when processing was done */ +}; +typedef struct h2_conn_ctx_t h2_conn_ctx_t; + +/** + * Get the h2 connection context. + * @param c the connection to look at + * @return h2 context of this connection + */ +#define h2_conn_ctx_get(c) \ + ((c)? (h2_conn_ctx_t*)ap_get_module_config((c)->conn_config, &http2_module) : NULL) + +/** + * Create the h2 connection context. + * @param c the connection to create it at + * @param s the server in use + * @param protocol the protocol selected + * @return created h2 context of this connection + */ +h2_conn_ctx_t *h2_conn_ctx_create_for_c1(conn_rec *c, server_rec *s, const char *protocol); + +void h2_conn_ctx_assign_session(h2_conn_ctx_t *ctx, struct h2_session *session); + +apr_status_t h2_conn_ctx_init_for_c2(h2_conn_ctx_t **pctx, conn_rec *c, + struct h2_mplx *mplx, struct h2_stream *stream, + struct h2_c2_transit *transit); + +void h2_conn_ctx_detach(conn_rec *c); + +void h2_conn_ctx_set_timeout(h2_conn_ctx_t *conn_ctx, apr_interval_time_t timeout); + +#endif /* defined(__mod_h2__h2_conn_ctx__) */ diff --git a/modules/http2/h2_conn_io.c b/modules/http2/h2_conn_io.c deleted file mode 100644 index eb6ec92..0000000 --- a/modules/http2/h2_conn_io.c +++ /dev/null @@ -1,389 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <assert.h> -#include <apr_strings.h> -#include <ap_mpm.h> - -#include <httpd.h> -#include <http_core.h> -#include <http_log.h> -#include <http_connection.h> -#include <http_request.h> - -#include "h2_private.h" -#include "h2_bucket_eos.h" -#include "h2_config.h" -#include "h2_conn_io.h" -#include "h2_h2.h" -#include "h2_session.h" -#include "h2_util.h" - -#define TLS_DATA_MAX (16*1024) - -/* Calculated like this: assuming MTU 1500 bytes - * 1500 - 40 (IP) - 20 (TCP) - 40 (TCP options) - * - TLS overhead (60-100) - * ~= 1300 bytes */ -#define WRITE_SIZE_INITIAL 1300 - -/* Calculated like this: max TLS record size 16*1024 - * - 40 (IP) - 20 (TCP) - 40 (TCP options) - * - TLS overhead (60-100) - * which seems to create less TCP packets overall - */ -#define WRITE_SIZE_MAX (TLS_DATA_MAX - 100) - - -static void h2_conn_io_bb_log(conn_rec *c, int stream_id, int level, - const char *tag, apr_bucket_brigade *bb) -{ - char buffer[16 * 1024]; - const char *line = "(null)"; - apr_size_t bmax = sizeof(buffer)/sizeof(buffer[0]); - int off = 0; - apr_bucket *b; - - if (bb) { - memset(buffer, 0, bmax--); - for (b = APR_BRIGADE_FIRST(bb); - bmax && (b != APR_BRIGADE_SENTINEL(bb)); - b = APR_BUCKET_NEXT(b)) { - - if (APR_BUCKET_IS_METADATA(b)) { - if (APR_BUCKET_IS_EOS(b)) { - off += apr_snprintf(buffer+off, bmax-off, "eos "); - } - else if (APR_BUCKET_IS_FLUSH(b)) { - off += apr_snprintf(buffer+off, bmax-off, "flush "); - } - else if (AP_BUCKET_IS_EOR(b)) { - off += apr_snprintf(buffer+off, bmax-off, "eor "); - } - else if (H2_BUCKET_IS_H2EOS(b)) { - off += apr_snprintf(buffer+off, bmax-off, "h2eos "); - } - else { - off += apr_snprintf(buffer+off, bmax-off, "meta(unknown) "); - } - } - else { - const char *btype = "data"; - if (APR_BUCKET_IS_FILE(b)) { - btype = "file"; - } - else if (APR_BUCKET_IS_PIPE(b)) { - btype = "pipe"; - } - else if (APR_BUCKET_IS_SOCKET(b)) { - btype = "socket"; - } - else if (APR_BUCKET_IS_HEAP(b)) { - btype = "heap"; - } - else if (APR_BUCKET_IS_TRANSIENT(b)) { - btype = "transient"; - } - else if (APR_BUCKET_IS_IMMORTAL(b)) { - btype = "immortal"; - } -#if APR_HAS_MMAP - else if (APR_BUCKET_IS_MMAP(b)) { - btype = "mmap"; - } -#endif - else if (APR_BUCKET_IS_POOL(b)) { - btype = "pool"; - } - - off += apr_snprintf(buffer+off, bmax-off, "%s[%ld] ", - btype, - (long)(b->length == ((apr_size_t)-1)? - -1 : b->length)); - } - } - line = *buffer? buffer : "(empty)"; - } - /* Intentional no APLOGNO */ - ap_log_cerror(APLOG_MARK, level, 0, c, "h2_session(%ld)-%s: %s", - c->id, tag, line); - -} - -apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c, - const h2_config *cfg) -{ - io->c = c; - io->output = apr_brigade_create(c->pool, c->bucket_alloc); - io->is_tls = h2_h2_is_tls(c); - io->buffer_output = io->is_tls; - io->flush_threshold = (apr_size_t)h2_config_geti64(cfg, H2_CONF_STREAM_MAX_MEM); - - if (io->is_tls) { - /* This is what we start with, - * see https://issues.apache.org/jira/browse/TS-2503 - */ - io->warmup_size = h2_config_geti64(cfg, H2_CONF_TLS_WARMUP_SIZE); - io->cooldown_usecs = (h2_config_geti(cfg, H2_CONF_TLS_COOLDOWN_SECS) - * APR_USEC_PER_SEC); - io->write_size = (io->cooldown_usecs > 0? - WRITE_SIZE_INITIAL : WRITE_SIZE_MAX); - } - else { - io->warmup_size = 0; - io->cooldown_usecs = 0; - io->write_size = 0; - } - - if (APLOGctrace1(c)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, io->c, - "h2_conn_io(%ld): init, buffering=%d, warmup_size=%ld, " - "cd_secs=%f", io->c->id, io->buffer_output, - (long)io->warmup_size, - ((float)io->cooldown_usecs/APR_USEC_PER_SEC)); - } - - return APR_SUCCESS; -} - -static void append_scratch(h2_conn_io *io) -{ - if (io->scratch && io->slen > 0) { - apr_bucket *b = apr_bucket_heap_create(io->scratch, io->slen, - apr_bucket_free, - io->c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(io->output, b); - io->scratch = NULL; - io->slen = io->ssize = 0; - } -} - -static apr_size_t assure_scratch_space(h2_conn_io *io) { - apr_size_t remain = io->ssize - io->slen; - if (io->scratch && remain == 0) { - append_scratch(io); - } - if (!io->scratch) { - /* we control the size and it is larger than what buckets usually - * allocate. */ - io->scratch = apr_bucket_alloc(io->write_size, io->c->bucket_alloc); - io->ssize = io->write_size; - io->slen = 0; - remain = io->ssize; - } - return remain; -} - -static apr_status_t read_to_scratch(h2_conn_io *io, apr_bucket *b) -{ - apr_status_t status; - const char *data; - apr_size_t len; - - if (!b->length) { - return APR_SUCCESS; - } - - ap_assert(b->length <= (io->ssize - io->slen)); - if (APR_BUCKET_IS_FILE(b)) { - apr_bucket_file *f = (apr_bucket_file *)b->data; - apr_file_t *fd = f->fd; - apr_off_t offset = b->start; - apr_size_t len = b->length; - - /* file buckets will either mmap (which we do not want) or - * read 8000 byte chunks and split themself. However, we do - * know *exactly* how many bytes we need where. - */ - status = apr_file_seek(fd, APR_SET, &offset); - if (status != APR_SUCCESS) { - return status; - } - status = apr_file_read(fd, io->scratch + io->slen, &len); - if (status != APR_SUCCESS && status != APR_EOF) { - return status; - } - io->slen += len; - } - else { - status = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); - if (status == APR_SUCCESS) { - memcpy(io->scratch+io->slen, data, len); - io->slen += len; - } - } - return status; -} - -static void check_write_size(h2_conn_io *io) -{ - if (io->write_size > WRITE_SIZE_INITIAL - && (io->cooldown_usecs > 0) - && (apr_time_now() - io->last_write) >= io->cooldown_usecs) { - /* long time not written, reset write size */ - io->write_size = WRITE_SIZE_INITIAL; - io->bytes_written = 0; - } - else if (io->write_size < WRITE_SIZE_MAX - && io->bytes_written >= io->warmup_size) { - /* connection is hot, use max size */ - io->write_size = WRITE_SIZE_MAX; - } -} - -static apr_status_t pass_output(h2_conn_io *io, int flush) -{ - conn_rec *c = io->c; - apr_bucket_brigade *bb = io->output; - apr_bucket *b; - apr_off_t bblen; - apr_status_t status; - - append_scratch(io); - if (flush && !io->is_flushed) { - b = apr_bucket_flush_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - } - - if (APR_BRIGADE_EMPTY(bb)) { - return APR_SUCCESS; - } - - ap_update_child_status(c->sbh, SERVER_BUSY_WRITE, NULL); - apr_brigade_length(bb, 0, &bblen); - h2_conn_io_bb_log(c, 0, APLOG_TRACE2, "out", bb); - - status = ap_pass_brigade(c->output_filters, bb); - if (status == APR_SUCCESS) { - io->bytes_written += (apr_size_t)bblen; - io->last_write = apr_time_now(); - if (flush) { - io->is_flushed = 1; - } - } - apr_brigade_cleanup(bb); - - if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, APLOGNO(03044) - "h2_conn_io(%ld): pass_out brigade %ld bytes", - c->id, (long)bblen); - } - return status; -} - -int h2_conn_io_needs_flush(h2_conn_io *io) -{ - if (!io->is_flushed) { - apr_off_t len = h2_brigade_mem_size(io->output); - if (len > io->flush_threshold) { - return 1; - } - /* if we do not exceed flush length due to memory limits, - * we want at least flush when we have that amount of data. */ - apr_brigade_length(io->output, 0, &len); - return len > (4 * io->flush_threshold); - } - return 0; -} - -apr_status_t h2_conn_io_flush(h2_conn_io *io) -{ - apr_status_t status; - status = pass_output(io, 1); - check_write_size(io); - return status; -} - -apr_status_t h2_conn_io_write(h2_conn_io *io, const char *data, size_t length) -{ - apr_status_t status = APR_SUCCESS; - apr_size_t remain; - - if (length > 0) { - io->is_flushed = 0; - } - - if (io->buffer_output) { - while (length > 0) { - remain = assure_scratch_space(io); - if (remain >= length) { - memcpy(io->scratch + io->slen, data, length); - io->slen += length; - length = 0; - } - else { - memcpy(io->scratch + io->slen, data, remain); - io->slen += remain; - data += remain; - length -= remain; - } - } - } - else { - status = apr_brigade_write(io->output, NULL, NULL, data, length); - } - return status; -} - -apr_status_t h2_conn_io_pass(h2_conn_io *io, apr_bucket_brigade *bb) -{ - apr_bucket *b; - apr_status_t status = APR_SUCCESS; - - if (!APR_BRIGADE_EMPTY(bb)) { - io->is_flushed = 0; - } - - while (!APR_BRIGADE_EMPTY(bb) && status == APR_SUCCESS) { - b = APR_BRIGADE_FIRST(bb); - - if (APR_BUCKET_IS_METADATA(b)) { - /* need to finish any open scratch bucket, as meta data - * needs to be forward "in order". */ - append_scratch(io); - APR_BUCKET_REMOVE(b); - APR_BRIGADE_INSERT_TAIL(io->output, b); - } - else if (io->buffer_output) { - apr_size_t remain = assure_scratch_space(io); - if (b->length > remain) { - apr_bucket_split(b, remain); - if (io->slen == 0) { - /* complete write_size bucket, append unchanged */ - APR_BUCKET_REMOVE(b); - APR_BRIGADE_INSERT_TAIL(io->output, b); - continue; - } - } - else { - /* bucket fits in remain, copy to scratch */ - status = read_to_scratch(io, b); - apr_bucket_delete(b); - continue; - } - } - else { - /* no buffering, forward buckets setaside on flush */ - if (APR_BUCKET_IS_TRANSIENT(b)) { - apr_bucket_setaside(b, io->c->pool); - } - APR_BUCKET_REMOVE(b); - APR_BRIGADE_INSERT_TAIL(io->output, b); - } - } - return status; -} - diff --git a/modules/http2/h2_ctx.c b/modules/http2/h2_ctx.c deleted file mode 100644 index d5ccc24..0000000 --- a/modules/http2/h2_ctx.c +++ /dev/null @@ -1,121 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <assert.h> - -#include <httpd.h> -#include <http_core.h> -#include <http_config.h> - -#include "h2_private.h" -#include "h2_session.h" -#include "h2_task.h" -#include "h2_ctx.h" - -static h2_ctx *h2_ctx_create(const conn_rec *c) -{ - h2_ctx *ctx = apr_pcalloc(c->pool, sizeof(h2_ctx)); - ap_assert(ctx); - ap_set_module_config(c->conn_config, &http2_module, ctx); - h2_ctx_server_set(ctx, c->base_server); - return ctx; -} - -void h2_ctx_clear(const conn_rec *c) -{ - ap_assert(c); - ap_set_module_config(c->conn_config, &http2_module, NULL); -} - -h2_ctx *h2_ctx_create_for(const conn_rec *c, h2_task *task) -{ - h2_ctx *ctx = h2_ctx_create(c); - if (ctx) { - ctx->task = task; - } - return ctx; -} - -h2_ctx *h2_ctx_get(const conn_rec *c, int create) -{ - h2_ctx *ctx = (h2_ctx*)ap_get_module_config(c->conn_config, &http2_module); - if (ctx == NULL && create) { - ctx = h2_ctx_create(c); - } - return ctx; -} - -h2_ctx *h2_ctx_rget(const request_rec *r) -{ - return h2_ctx_get(r->connection, 0); -} - -const char *h2_ctx_protocol_get(const conn_rec *c) -{ - h2_ctx *ctx; - if (c->master) { - c = c->master; - } - ctx = (h2_ctx*)ap_get_module_config(c->conn_config, &http2_module); - return ctx? ctx->protocol : NULL; -} - -h2_ctx *h2_ctx_protocol_set(h2_ctx *ctx, const char *proto) -{ - ctx->protocol = proto; - return ctx; -} - -h2_session *h2_ctx_session_get(h2_ctx *ctx) -{ - return ctx? ctx->session : NULL; -} - -void h2_ctx_session_set(h2_ctx *ctx, struct h2_session *session) -{ - ctx->session = session; -} - -server_rec *h2_ctx_server_get(h2_ctx *ctx) -{ - return ctx? ctx->server : NULL; -} - -h2_ctx *h2_ctx_server_set(h2_ctx *ctx, server_rec *s) -{ - ctx->server = s; - return ctx; -} - -int h2_ctx_is_task(h2_ctx *ctx) -{ - return ctx && ctx->task; -} - -h2_task *h2_ctx_get_task(h2_ctx *ctx) -{ - return ctx? ctx->task : NULL; -} - -h2_task *h2_ctx_cget_task(conn_rec *c) -{ - return h2_ctx_get_task(h2_ctx_get(c, 0)); -} - -h2_task *h2_ctx_rget_task(request_rec *r) -{ - return h2_ctx_get_task(h2_ctx_rget(r)); -} diff --git a/modules/http2/h2_ctx.h b/modules/http2/h2_ctx.h deleted file mode 100644 index cb111c9..0000000 --- a/modules/http2/h2_ctx.h +++ /dev/null @@ -1,78 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __mod_h2__h2_ctx__ -#define __mod_h2__h2_ctx__ - -struct h2_session; -struct h2_task; -struct h2_config; - -/** - * The h2 module context associated with a connection. - * - * It keeps track of the different types of connections: - * - those from clients that use HTTP/2 protocol - * - those from clients that do not use HTTP/2 - * - those created by ourself to perform work on HTTP/2 streams - */ -typedef struct h2_ctx { - const char *protocol; /* the protocol negotiated */ - struct h2_session *session; /* the session established */ - struct h2_task *task; /* the h2_task executing or NULL */ - const char *hostname; /* hostname negotiated via SNI, optional */ - server_rec *server; /* httpd server config selected. */ - const struct h2_config *config; /* effective config in this context */ -} h2_ctx; - -/** - * Get (or create) a h2 context record for this connection. - * @param c the connection to look at - * @param create != 0 iff missing context shall be created - * @return h2 context of this connection - */ -h2_ctx *h2_ctx_get(const conn_rec *c, int create); -void h2_ctx_clear(const conn_rec *c); - -h2_ctx *h2_ctx_rget(const request_rec *r); -h2_ctx *h2_ctx_create_for(const conn_rec *c, struct h2_task *task); - - -/* Set the h2 protocol established on this connection context or - * NULL when other protocols are in place. - */ -h2_ctx *h2_ctx_protocol_set(h2_ctx *ctx, const char *proto); - -/* Set the server_rec relevant for this context. - */ -h2_ctx *h2_ctx_server_set(h2_ctx *ctx, server_rec *s); -server_rec *h2_ctx_server_get(h2_ctx *ctx); - -struct h2_session *h2_ctx_session_get(h2_ctx *ctx); -void h2_ctx_session_set(h2_ctx *ctx, struct h2_session *session); - -/** - * Get the h2 protocol negotiated for this connection, or NULL. - */ -const char *h2_ctx_protocol_get(const conn_rec *c); - -int h2_ctx_is_task(h2_ctx *ctx); - -struct h2_task *h2_ctx_get_task(h2_ctx *ctx); -struct h2_task *h2_ctx_cget_task(conn_rec *c); -struct h2_task *h2_ctx_rget_task(request_rec *r); - -#endif /* defined(__mod_h2__h2_ctx__) */ diff --git a/modules/http2/h2_filter.c b/modules/http2/h2_filter.c deleted file mode 100644 index 8b254b1..0000000 --- a/modules/http2/h2_filter.c +++ /dev/null @@ -1,568 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <assert.h> - -#include <apr_strings.h> -#include <httpd.h> -#include <http_core.h> -#include <http_protocol.h> -#include <http_log.h> -#include <http_connection.h> -#include <scoreboard.h> - -#include "h2_private.h" -#include "h2.h" -#include "h2_config.h" -#include "h2_conn_io.h" -#include "h2_ctx.h" -#include "h2_mplx.h" -#include "h2_push.h" -#include "h2_task.h" -#include "h2_stream.h" -#include "h2_request.h" -#include "h2_headers.h" -#include "h2_stream.h" -#include "h2_session.h" -#include "h2_util.h" -#include "h2_version.h" - -#include "h2_filter.h" - -#define UNSET -1 -#define H2MIN(x,y) ((x) < (y) ? (x) : (y)) - -static apr_status_t recv_RAW_DATA(conn_rec *c, h2_filter_cin *cin, - apr_bucket *b, apr_read_type_e block) -{ - h2_session *session = cin->session; - apr_status_t status = APR_SUCCESS; - apr_size_t len; - const char *data; - ssize_t n; - - status = apr_bucket_read(b, &data, &len, block); - - while (status == APR_SUCCESS && len > 0) { - n = nghttp2_session_mem_recv(session->ngh2, (const uint8_t *)data, len); - - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - H2_SSSN_MSG(session, "fed %ld bytes to nghttp2, %ld read"), - (long)len, (long)n); - if (n < 0) { - if (nghttp2_is_fatal((int)n)) { - h2_session_event(session, H2_SESSION_EV_PROTO_ERROR, - (int)n, nghttp2_strerror((int)n)); - status = APR_EGENERAL; - } - } - else { - session->io.bytes_read += n; - if (len <= n) { - break; - } - len -= n; - data += n; - } - } - - return status; -} - -static apr_status_t recv_RAW_brigade(conn_rec *c, h2_filter_cin *cin, - apr_bucket_brigade *bb, - apr_read_type_e block) -{ - apr_status_t status = APR_SUCCESS; - apr_bucket* b; - int consumed = 0; - - h2_util_bb_log(c, c->id, APLOG_TRACE2, "RAW_in", bb); - while (status == APR_SUCCESS && !APR_BRIGADE_EMPTY(bb)) { - b = APR_BRIGADE_FIRST(bb); - - if (APR_BUCKET_IS_METADATA(b)) { - /* nop */ - } - else { - status = recv_RAW_DATA(c, cin, b, block); - } - consumed = 1; - apr_bucket_delete(b); - } - - if (!consumed && status == APR_SUCCESS && block == APR_NONBLOCK_READ) { - return APR_EAGAIN; - } - return status; -} - -h2_filter_cin *h2_filter_cin_create(h2_session *session) -{ - h2_filter_cin *cin; - - cin = apr_pcalloc(session->pool, sizeof(*cin)); - if (!cin) { - return NULL; - } - cin->session = session; - return cin; -} - -void h2_filter_cin_timeout_set(h2_filter_cin *cin, apr_interval_time_t timeout) -{ - cin->timeout = timeout; -} - -apr_status_t h2_filter_core_input(ap_filter_t* f, - apr_bucket_brigade* brigade, - ap_input_mode_t mode, - apr_read_type_e block, - apr_off_t readbytes) -{ - h2_filter_cin *cin = f->ctx; - apr_status_t status = APR_SUCCESS; - apr_interval_time_t saved_timeout = UNSET; - const int trace1 = APLOGctrace1(f->c); - - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_session(%ld): read, %s, mode=%d, readbytes=%ld", - (long)f->c->id, (block == APR_BLOCK_READ)? - "BLOCK_READ" : "NONBLOCK_READ", mode, (long)readbytes); - } - - if (mode == AP_MODE_INIT || mode == AP_MODE_SPECULATIVE) { - return ap_get_brigade(f->next, brigade, mode, block, readbytes); - } - - if (mode != AP_MODE_READBYTES) { - return (block == APR_BLOCK_READ)? APR_SUCCESS : APR_EAGAIN; - } - - if (!cin->bb) { - cin->bb = apr_brigade_create(cin->session->pool, f->c->bucket_alloc); - } - - if (!cin->socket) { - cin->socket = ap_get_conn_socket(f->c); - } - - if (APR_BRIGADE_EMPTY(cin->bb)) { - /* We only do a blocking read when we have no streams to process. So, - * in httpd scoreboard lingo, we are in a KEEPALIVE connection state. - */ - if (block == APR_BLOCK_READ) { - if (cin->timeout > 0) { - apr_socket_timeout_get(cin->socket, &saved_timeout); - apr_socket_timeout_set(cin->socket, cin->timeout); - } - } - status = ap_get_brigade(f->next, cin->bb, AP_MODE_READBYTES, - block, readbytes); - if (saved_timeout != UNSET) { - apr_socket_timeout_set(cin->socket, saved_timeout); - } - } - - switch (status) { - case APR_SUCCESS: - status = recv_RAW_brigade(f->c, cin, cin->bb, block); - break; - case APR_EOF: - case APR_EAGAIN: - case APR_TIMEUP: - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, - "h2_session(%ld): read", f->c->id); - } - break; - default: - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, f->c, APLOGNO(03046) - "h2_session(%ld): error reading", f->c->id); - break; - } - return status; -} - -/******************************************************************************* - * http2 connection status handler + stream out source - ******************************************************************************/ - -typedef struct { - apr_bucket_refcount refcount; - h2_bucket_event_cb *cb; - void *ctx; -} h2_bucket_observer; - -static apr_status_t bucket_read(apr_bucket *b, const char **str, - apr_size_t *len, apr_read_type_e block) -{ - (void)b; - (void)block; - *str = NULL; - *len = 0; - return APR_SUCCESS; -} - -static void bucket_destroy(void *data) -{ - h2_bucket_observer *h = data; - if (apr_bucket_shared_destroy(h)) { - if (h->cb) { - h->cb(h->ctx, H2_BUCKET_EV_BEFORE_DESTROY, NULL); - } - apr_bucket_free(h); - } -} - -apr_bucket * h2_bucket_observer_make(apr_bucket *b, h2_bucket_event_cb *cb, - void *ctx) -{ - h2_bucket_observer *br; - - br = apr_bucket_alloc(sizeof(*br), b->list); - br->cb = cb; - br->ctx = ctx; - - b = apr_bucket_shared_make(b, br, 0, 0); - b->type = &h2_bucket_type_observer; - return b; -} - -apr_bucket * h2_bucket_observer_create(apr_bucket_alloc_t *list, - h2_bucket_event_cb *cb, void *ctx) -{ - apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); - - APR_BUCKET_INIT(b); - b->free = apr_bucket_free; - b->list = list; - b = h2_bucket_observer_make(b, cb, ctx); - return b; -} - -apr_status_t h2_bucket_observer_fire(apr_bucket *b, h2_bucket_event event) -{ - if (H2_BUCKET_IS_OBSERVER(b)) { - h2_bucket_observer *l = (h2_bucket_observer *)b->data; - return l->cb(l->ctx, event, b); - } - return APR_EINVAL; -} - -const apr_bucket_type_t h2_bucket_type_observer = { - "H2OBS", 5, APR_BUCKET_METADATA, - bucket_destroy, - bucket_read, - apr_bucket_setaside_noop, - apr_bucket_split_notimpl, - apr_bucket_shared_copy -}; - -apr_bucket *h2_bucket_observer_beam(struct h2_bucket_beam *beam, - apr_bucket_brigade *dest, - const apr_bucket *src) -{ - if (H2_BUCKET_IS_OBSERVER(src)) { - h2_bucket_observer *l = (h2_bucket_observer *)src->data; - apr_bucket *b = h2_bucket_observer_create(dest->bucket_alloc, - l->cb, l->ctx); - APR_BRIGADE_INSERT_TAIL(dest, b); - l->cb = NULL; - l->ctx = NULL; - h2_bucket_observer_fire(b, H2_BUCKET_EV_BEFORE_MASTER_SEND); - return b; - } - return NULL; -} - -static apr_status_t bbout(apr_bucket_brigade *bb, const char *fmt, ...) - __attribute__((format(printf,2,3))); -static apr_status_t bbout(apr_bucket_brigade *bb, const char *fmt, ...) -{ - va_list args; - apr_status_t rv; - - va_start(args, fmt); - rv = apr_brigade_vprintf(bb, NULL, NULL, fmt, args); - va_end(args); - - return rv; -} - -static void add_settings(apr_bucket_brigade *bb, h2_session *s, int last) -{ - h2_mplx *m = s->mplx; - - bbout(bb, " \"settings\": {\n"); - bbout(bb, " \"SETTINGS_MAX_CONCURRENT_STREAMS\": %d,\n", m->max_streams); - bbout(bb, " \"SETTINGS_MAX_FRAME_SIZE\": %d,\n", 16*1024); - bbout(bb, " \"SETTINGS_INITIAL_WINDOW_SIZE\": %d,\n", - h2_config_geti(s->config, H2_CONF_WIN_SIZE)); - bbout(bb, " \"SETTINGS_ENABLE_PUSH\": %d\n", h2_session_push_enabled(s)); - bbout(bb, " }%s\n", last? "" : ","); -} - -static void add_peer_settings(apr_bucket_brigade *bb, h2_session *s, int last) -{ - bbout(bb, " \"peerSettings\": {\n"); - bbout(bb, " \"SETTINGS_MAX_CONCURRENT_STREAMS\": %d,\n", - nghttp2_session_get_remote_settings(s->ngh2, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS)); - bbout(bb, " \"SETTINGS_MAX_FRAME_SIZE\": %d,\n", - nghttp2_session_get_remote_settings(s->ngh2, NGHTTP2_SETTINGS_MAX_FRAME_SIZE)); - bbout(bb, " \"SETTINGS_INITIAL_WINDOW_SIZE\": %d,\n", - nghttp2_session_get_remote_settings(s->ngh2, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE)); - bbout(bb, " \"SETTINGS_ENABLE_PUSH\": %d,\n", - nghttp2_session_get_remote_settings(s->ngh2, NGHTTP2_SETTINGS_ENABLE_PUSH)); - bbout(bb, " \"SETTINGS_HEADER_TABLE_SIZE\": %d,\n", - nghttp2_session_get_remote_settings(s->ngh2, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE)); - bbout(bb, " \"SETTINGS_MAX_HEADER_LIST_SIZE\": %d\n", - nghttp2_session_get_remote_settings(s->ngh2, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE)); - bbout(bb, " }%s\n", last? "" : ","); -} - -typedef struct { - apr_bucket_brigade *bb; - h2_session *s; - int idx; -} stream_ctx_t; - -static int add_stream(h2_stream *stream, void *ctx) -{ - stream_ctx_t *x = ctx; - int32_t flowIn, flowOut; - - flowIn = nghttp2_session_get_stream_effective_local_window_size(x->s->ngh2, stream->id); - flowOut = nghttp2_session_get_stream_remote_window_size(x->s->ngh2, stream->id); - bbout(x->bb, "%s\n \"%d\": {\n", (x->idx? "," : ""), stream->id); - bbout(x->bb, " \"state\": \"%s\",\n", h2_stream_state_str(stream)); - bbout(x->bb, " \"created\": %f,\n", ((double)stream->created)/APR_USEC_PER_SEC); - bbout(x->bb, " \"flowIn\": %d,\n", flowIn); - bbout(x->bb, " \"flowOut\": %d,\n", flowOut); - bbout(x->bb, " \"dataIn\": %"APR_OFF_T_FMT",\n", stream->in_data_octets); - bbout(x->bb, " \"dataOut\": %"APR_OFF_T_FMT"\n", stream->out_data_octets); - bbout(x->bb, " }"); - - ++x->idx; - return 1; -} - -static void add_streams(apr_bucket_brigade *bb, h2_session *s, int last) -{ - stream_ctx_t x; - - x.bb = bb; - x.s = s; - x.idx = 0; - bbout(bb, " \"streams\": {"); - h2_mplx_stream_do(s->mplx, add_stream, &x); - bbout(bb, "\n }%s\n", last? "" : ","); -} - -static void add_push(apr_bucket_brigade *bb, h2_session *s, - h2_stream *stream, int last) -{ - h2_push_diary *diary; - apr_status_t status; - - bbout(bb, " \"push\": {\n"); - diary = s->push_diary; - if (diary) { - const char *data; - const char *base64_digest; - apr_size_t len; - - status = h2_push_diary_digest_get(diary, bb->p, 256, - stream->request->authority, - &data, &len); - if (status == APR_SUCCESS) { - base64_digest = h2_util_base64url_encode(data, len, bb->p); - bbout(bb, " \"cacheDigest\": \"%s\",\n", base64_digest); - } - } - bbout(bb, " \"promises\": %d,\n", s->pushes_promised); - bbout(bb, " \"submits\": %d,\n", s->pushes_submitted); - bbout(bb, " \"resets\": %d\n", s->pushes_reset); - bbout(bb, " }%s\n", last? "" : ","); -} - -static void add_in(apr_bucket_brigade *bb, h2_session *s, int last) -{ - bbout(bb, " \"in\": {\n"); - bbout(bb, " \"requests\": %d,\n", s->remote.emitted_count); - bbout(bb, " \"resets\": %d, \n", s->streams_reset); - bbout(bb, " \"frames\": %ld,\n", (long)s->frames_received); - bbout(bb, " \"octets\": %"APR_UINT64_T_FMT"\n", s->io.bytes_read); - bbout(bb, " }%s\n", last? "" : ","); -} - -static void add_out(apr_bucket_brigade *bb, h2_session *s, int last) -{ - bbout(bb, " \"out\": {\n"); - bbout(bb, " \"responses\": %d,\n", s->responses_submitted); - bbout(bb, " \"frames\": %ld,\n", (long)s->frames_sent); - bbout(bb, " \"octets\": %"APR_UINT64_T_FMT"\n", s->io.bytes_written); - bbout(bb, " }%s\n", last? "" : ","); -} - -static void add_stats(apr_bucket_brigade *bb, h2_session *s, - h2_stream *stream, int last) -{ - bbout(bb, " \"stats\": {\n"); - add_in(bb, s, 0); - add_out(bb, s, 0); - add_push(bb, s, stream, 1); - bbout(bb, " }%s\n", last? "" : ","); -} - -static apr_status_t h2_status_insert(h2_task *task, apr_bucket *b) -{ - conn_rec *c = task->c->master; - h2_ctx *h2ctx = h2_ctx_get(c, 0); - h2_session *session; - h2_stream *stream; - apr_bucket_brigade *bb; - apr_bucket *e; - int32_t connFlowIn, connFlowOut; - - - if (!h2ctx || (session = h2_ctx_session_get(h2ctx)) == NULL) { - return APR_SUCCESS; - } - - stream = h2_session_stream_get(session, task->stream_id); - if (!stream) { - /* stream already done */ - return APR_SUCCESS; - } - - bb = apr_brigade_create(stream->pool, c->bucket_alloc); - - connFlowIn = nghttp2_session_get_effective_local_window_size(session->ngh2); - connFlowOut = nghttp2_session_get_remote_window_size(session->ngh2); - - bbout(bb, "{\n"); - bbout(bb, " \"version\": \"draft-01\",\n"); - add_settings(bb, session, 0); - add_peer_settings(bb, session, 0); - bbout(bb, " \"connFlowIn\": %d,\n", connFlowIn); - bbout(bb, " \"connFlowOut\": %d,\n", connFlowOut); - bbout(bb, " \"sentGoAway\": %d,\n", session->local.shutdown); - - add_streams(bb, session, 0); - - add_stats(bb, session, stream, 1); - bbout(bb, "}\n"); - - while ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) { - APR_BUCKET_REMOVE(e); - APR_BUCKET_INSERT_AFTER(b, e); - b = e; - } - apr_brigade_destroy(bb); - - return APR_SUCCESS; -} - -static apr_status_t status_event(void *ctx, h2_bucket_event event, - apr_bucket *b) -{ - h2_task *task = ctx; - - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, task->c->master, - "status_event(%s): %d", task->id, event); - switch (event) { - case H2_BUCKET_EV_BEFORE_MASTER_SEND: - h2_status_insert(task, b); - break; - default: - break; - } - return APR_SUCCESS; -} - -int h2_filter_h2_status_handler(request_rec *r) -{ - h2_ctx *ctx = h2_ctx_rget(r); - conn_rec *c = r->connection; - h2_task *task; - apr_bucket_brigade *bb; - apr_bucket *b; - apr_status_t status; - - if (strcmp(r->handler, "http2-status")) { - return DECLINED; - } - if (r->method_number != M_GET && r->method_number != M_POST) { - return DECLINED; - } - - task = ctx? h2_ctx_get_task(ctx) : NULL; - if (task) { - - if ((status = ap_discard_request_body(r)) != OK) { - return status; - } - - /* We need to handle the actual output on the main thread, as - * we need to access h2_session information. */ - r->status = 200; - r->clength = -1; - r->chunked = 1; - apr_table_unset(r->headers_out, "Content-Length"); - /* Discourage content-encodings */ - apr_table_unset(r->headers_out, "Content-Encoding"); - apr_table_setn(r->subprocess_env, "no-brotli", "1"); - apr_table_setn(r->subprocess_env, "no-gzip", "1"); - - ap_set_content_type(r, "application/json"); - apr_table_setn(r->notes, H2_FILTER_DEBUG_NOTE, "on"); - - bb = apr_brigade_create(r->pool, c->bucket_alloc); - b = h2_bucket_observer_create(c->bucket_alloc, status_event, task); - APR_BRIGADE_INSERT_TAIL(bb, b); - b = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - - ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, - "status_handler(%s): checking for incoming trailers", - task->id); - if (r->trailers_in && !apr_is_empty_table(r->trailers_in)) { - ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, - "status_handler(%s): seeing incoming trailers", - task->id); - apr_table_setn(r->trailers_out, "h2-trailers-in", - apr_itoa(r->pool, 1)); - } - - status = ap_pass_brigade(r->output_filters, bb); - if (status == APR_SUCCESS - || r->status != HTTP_OK - || c->aborted) { - return OK; - } - else { - /* no way to know what type of error occurred */ - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, status, r, - "status_handler(%s): ap_pass_brigade failed", - task->id); - return AP_FILTER_ERROR; - } - } - return DECLINED; -} - diff --git a/modules/http2/h2_filter.h b/modules/http2/h2_filter.h deleted file mode 100644 index 12810d8..0000000 --- a/modules/http2/h2_filter.h +++ /dev/null @@ -1,73 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __mod_h2__h2_filter__ -#define __mod_h2__h2_filter__ - -struct h2_bucket_beam; -struct h2_headers; -struct h2_stream; -struct h2_session; - -typedef struct h2_filter_cin { - apr_pool_t *pool; - apr_socket_t *socket; - apr_interval_time_t timeout; - apr_bucket_brigade *bb; - struct h2_session *session; - apr_bucket *cur; -} h2_filter_cin; - -h2_filter_cin *h2_filter_cin_create(struct h2_session *session); - -void h2_filter_cin_timeout_set(h2_filter_cin *cin, apr_interval_time_t timeout); - -apr_status_t h2_filter_core_input(ap_filter_t* filter, - apr_bucket_brigade* brigade, - ap_input_mode_t mode, - apr_read_type_e block, - apr_off_t readbytes); - -/******* observer bucket ******************************************************/ - -typedef enum { - H2_BUCKET_EV_BEFORE_DESTROY, - H2_BUCKET_EV_BEFORE_MASTER_SEND -} h2_bucket_event; - -extern const apr_bucket_type_t h2_bucket_type_observer; - -typedef apr_status_t h2_bucket_event_cb(void *ctx, h2_bucket_event event, apr_bucket *b); - -#define H2_BUCKET_IS_OBSERVER(e) (e->type == &h2_bucket_type_observer) - -apr_bucket * h2_bucket_observer_make(apr_bucket *b, h2_bucket_event_cb *cb, - void *ctx); - -apr_bucket * h2_bucket_observer_create(apr_bucket_alloc_t *list, - h2_bucket_event_cb *cb, void *ctx); - -apr_status_t h2_bucket_observer_fire(apr_bucket *b, h2_bucket_event event); - -apr_bucket *h2_bucket_observer_beam(struct h2_bucket_beam *beam, - apr_bucket_brigade *dest, - const apr_bucket *src); - -/******* /.well-known/h2/state handler ****************************************/ - -int h2_filter_h2_status_handler(request_rec *r); - -#endif /* __mod_h2__h2_filter__ */ diff --git a/modules/http2/h2_headers.c b/modules/http2/h2_headers.c index 8b7add6..d9b3fd0 100644 --- a/modules/http2/h2_headers.c +++ b/modules/http2/h2_headers.c @@ -27,11 +27,13 @@ #include <nghttp2/nghttp2.h> #include "h2_private.h" -#include "h2_h2.h" +#include "h2_protocol.h" +#include "h2_config.h" #include "h2_util.h" #include "h2_request.h" #include "h2_headers.h" +#if !AP_HAS_RESPONSE_BUCKETS static int is_unsafe(server_rec *s) { @@ -63,6 +65,7 @@ apr_bucket * h2_bucket_headers_make(apr_bucket *b, h2_headers *r) b = apr_bucket_shared_make(b, br, 0, 0); b->type = &h2_bucket_type_headers; + b->length = 0; return b; } @@ -87,32 +90,35 @@ h2_headers *h2_bucket_headers_get(apr_bucket *b) return NULL; } +static void bucket_destroy(void *data) +{ + h2_bucket_headers *h = data; + + if (apr_bucket_shared_destroy(h)) { + apr_bucket_free(h); + } +} + const apr_bucket_type_t h2_bucket_type_headers = { "H2HEADERS", 5, APR_BUCKET_METADATA, - apr_bucket_destroy_noop, + bucket_destroy, bucket_read, apr_bucket_setaside_noop, apr_bucket_split_notimpl, apr_bucket_shared_copy }; -apr_bucket *h2_bucket_headers_beam(struct h2_bucket_beam *beam, - apr_bucket_brigade *dest, - const apr_bucket *src) +apr_bucket *h2_bucket_headers_clone(apr_bucket *b, apr_pool_t *pool, + apr_bucket_alloc_t *list) { - if (H2_BUCKET_IS_HEADERS(src)) { - h2_headers *r = ((h2_bucket_headers *)src->data)->headers; - apr_bucket *b = h2_bucket_headers_create(dest->bucket_alloc, r); - APR_BRIGADE_INSERT_TAIL(dest, b); - return b; - } - return NULL; + h2_headers *hdrs = ((h2_bucket_headers *)b->data)->headers; + return h2_bucket_headers_create(list, h2_headers_clone(pool, hdrs)); } -h2_headers *h2_headers_create(int status, apr_table_t *headers_in, - apr_table_t *notes, apr_off_t raw_bytes, - apr_pool_t *pool) +h2_headers *h2_headers_create(int status, const apr_table_t *headers_in, + const apr_table_t *notes, apr_off_t raw_bytes, + apr_pool_t *pool) { h2_headers *headers = apr_pcalloc(pool, sizeof(h2_headers)); headers->status = status; @@ -123,26 +129,55 @@ h2_headers *h2_headers_create(int status, apr_table_t *headers_in, return headers; } +static int add_header_lengths(void *ctx, const char *name, const char *value) +{ + apr_size_t *plen = ctx; + *plen += strlen(name) + strlen(value); + return 1; +} + +apr_size_t h2_headers_length(h2_headers *headers) +{ + apr_size_t len = 0; + apr_table_do(add_header_lengths, &len, headers->headers, NULL); + return len; +} + +apr_size_t h2_bucket_headers_headers_length(apr_bucket *b) +{ + h2_headers *h = h2_bucket_headers_get(b); + return h? h2_headers_length(h) : 0; +} + h2_headers *h2_headers_rcreate(request_rec *r, int status, - apr_table_t *header, apr_pool_t *pool) + const apr_table_t *header, apr_pool_t *pool) { h2_headers *headers = h2_headers_create(status, header, r->notes, 0, pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, headers->status, r, + "h2_headers_rcreate(%ld): status=%d", + (long)r->connection->id, status); if (headers->status == HTTP_FORBIDDEN) { - const char *cause = apr_table_get(r->notes, "ssl-renegotiate-forbidden"); - if (cause) { - /* This request triggered a TLS renegotiation that is now allowed - * in HTTP/2. Tell the client that it should use HTTP/1.1 for this. - */ - ap_log_rerror(APLOG_MARK, APLOG_DEBUG, headers->status, r, - APLOGNO(03061) - "h2_headers(%ld): renegotiate forbidden, cause: %s", - (long)r->connection->id, cause); - headers->status = H2_ERR_HTTP_1_1_REQUIRED; + request_rec *r_prev; + for (r_prev = r; r_prev != NULL; r_prev = r_prev->prev) { + const char *cause = apr_table_get(r_prev->notes, "ssl-renegotiate-forbidden"); + if (cause) { + /* This request triggered a TLS renegotiation that is not allowed + * in HTTP/2. Tell the client that it should use HTTP/1.1 for this. + */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, headers->status, r, + APLOGNO(10399) + "h2_headers(%ld): renegotiate forbidden, cause: %s", + (long)r->connection->id, cause); + headers->status = H2_ERR_HTTP_1_1_REQUIRED; + break; + } } } if (is_unsafe(r->server)) { - apr_table_setn(headers->notes, H2_HDR_CONFORMANCE, - H2_HDR_CONFORMANCE_UNSAFE); + apr_table_setn(headers->notes, H2_HDR_CONFORMANCE, H2_HDR_CONFORMANCE_UNSAFE); + } + if (h2_config_rgeti(r, H2_CONF_PUSH) == 0 && h2_config_sgeti(r->server, H2_CONF_PUSH) != 0) { + apr_table_setn(headers->notes, H2_PUSH_MODE_NOTE, "0"); } return headers; } @@ -152,6 +187,11 @@ h2_headers *h2_headers_copy(apr_pool_t *pool, h2_headers *h) return h2_headers_create(h->status, h->headers, h->notes, h->raw_bytes, pool); } +h2_headers *h2_headers_clone(apr_pool_t *pool, h2_headers *h) +{ + return h2_headers_create(h->status, h->headers, h->notes, h->raw_bytes, pool); +} + h2_headers *h2_headers_die(apr_status_t type, const h2_request *req, apr_pool_t *pool) { @@ -171,8 +211,9 @@ h2_headers *h2_headers_die(apr_status_t type, return headers; } -int h2_headers_are_response(h2_headers *headers) +int h2_headers_are_final_response(h2_headers *headers) { return headers->status >= 200; } +#endif /* !AP_HAS_RESPONSE_BUCKETS */ diff --git a/modules/http2/h2_headers.h b/modules/http2/h2_headers.h index 840e8c4..3d78dc3 100644 --- a/modules/http2/h2_headers.h +++ b/modules/http2/h2_headers.h @@ -19,8 +19,19 @@ #include "h2.h" +#if !AP_HAS_RESPONSE_BUCKETS + struct h2_bucket_beam; +typedef struct h2_headers h2_headers; +struct h2_headers { + int status; + apr_table_t *headers; + apr_table_t *notes; + apr_off_t raw_bytes; /* RAW network bytes that generated this request - if known. */ +}; + + extern const apr_bucket_type_t h2_bucket_type_headers; #define H2_BUCKET_IS_HEADERS(e) (e->type == &h2_bucket_type_headers) @@ -32,10 +43,6 @@ apr_bucket * h2_bucket_headers_create(apr_bucket_alloc_t *list, h2_headers *h2_bucket_headers_get(apr_bucket *b); -apr_bucket *h2_bucket_headers_beam(struct h2_bucket_beam *beam, - apr_bucket_brigade *dest, - const apr_bucket *src); - /** * Create the headers from the given status and headers * @param status the headers status @@ -44,8 +51,8 @@ apr_bucket *h2_bucket_headers_beam(struct h2_bucket_beam *beam, * @param raw_bytes the raw network bytes (if known) used to transmit these * @param pool the memory pool to use */ -h2_headers *h2_headers_create(int status, apr_table_t *header, - apr_table_t *notes, apr_off_t raw_bytes, +h2_headers *h2_headers_create(int status, const apr_table_t *header, + const apr_table_t *notes, apr_off_t raw_bytes, apr_pool_t *pool); /** @@ -56,24 +63,45 @@ h2_headers *h2_headers_create(int status, apr_table_t *header, * @param pool the memory pool to use */ h2_headers *h2_headers_rcreate(request_rec *r, int status, - apr_table_t *header, apr_pool_t *pool); + const apr_table_t *header, apr_pool_t *pool); /** - * Clone the headers into another pool. This will not copy any + * Copy the headers into another pool. This will not copy any * header strings. */ h2_headers *h2_headers_copy(apr_pool_t *pool, h2_headers *h); /** + * Clone the headers into another pool. This will also clone any + * header strings. + */ +h2_headers *h2_headers_clone(apr_pool_t *pool, h2_headers *h); + +/** * Create the headers for the given error. - * @param stream_id id of the stream to create the headers for * @param type the error code * @param req the original h2_request * @param pool the memory pool to use */ h2_headers *h2_headers_die(apr_status_t type, - const struct h2_request *req, apr_pool_t *pool); + const struct h2_request *req, apr_pool_t *pool); + +int h2_headers_are_final_response(h2_headers *headers); + +/** + * Give the number of bytes of all contained header strings. + */ +apr_size_t h2_headers_length(h2_headers *headers); + +/** + * For H2HEADER buckets, return the length of all contained header strings. + * For all other buckets, return 0. + */ +apr_size_t h2_bucket_headers_headers_length(apr_bucket *b); + +apr_bucket *h2_bucket_headers_clone(apr_bucket *b, apr_pool_t *pool, + apr_bucket_alloc_t *list); -int h2_headers_are_response(h2_headers *headers); +#endif /* !AP_HAS_RESPONSE_BUCKETS */ #endif /* defined(__mod_h2__h2_headers__) */ diff --git a/modules/http2/h2_mplx.c b/modules/http2/h2_mplx.c index 0fae117..2aeea42 100644 --- a/modules/http2/h2_mplx.c +++ b/modules/http2/h2_mplx.c @@ -26,7 +26,9 @@ #include <httpd.h> #include <http_core.h> +#include <http_connection.h> #include <http_log.h> +#include <http_protocol.h> #include <mpm_common.h> @@ -36,15 +38,14 @@ #include "h2_private.h" #include "h2_bucket_beam.h" #include "h2_config.h" -#include "h2_conn.h" -#include "h2_ctx.h" -#include "h2_h2.h" +#include "h2_c1.h" +#include "h2_conn_ctx.h" +#include "h2_protocol.h" #include "h2_mplx.h" -#include "h2_ngn_shed.h" #include "h2_request.h" #include "h2_stream.h" #include "h2_session.h" -#include "h2_task.h" +#include "h2_c2.h" #include "h2_workers.h" #include "h2_util.h" @@ -54,16 +55,40 @@ typedef struct { h2_mplx *m; h2_stream *stream; apr_time_t now; + apr_size_t count; } stream_iter_ctx; -apr_status_t h2_mplx_child_init(apr_pool_t *pool, server_rec *s) +static conn_rec *c2_prod_next(void *baton, int *phas_more); +static void c2_prod_done(void *baton, conn_rec *c2); +static void workers_shutdown(void *baton, int graceful); + +static void s_mplx_be_happy(h2_mplx *m, conn_rec *c, h2_conn_ctx_t *conn_ctx); +static void m_be_annoyed(h2_mplx *m); + +static apr_status_t mplx_pollset_create(h2_mplx *m); +static apr_status_t mplx_pollset_poll(h2_mplx *m, apr_interval_time_t timeout, + stream_ev_callback *on_stream_input, + stream_ev_callback *on_stream_output, + void *on_ctx); + +static apr_pool_t *pchild; + +/* APR callback invoked if allocation fails. */ +static int abort_on_oom(int retcode) { + ap_abort_on_oom(); + return retcode; /* unreachable, hopefully. */ +} + +apr_status_t h2_mplx_c1_child_init(apr_pool_t *pool, server_rec *s) +{ + pchild = pool; return APR_SUCCESS; } #define H2_MPLX_ENTER(m) \ - do { apr_status_t rv; if ((rv = apr_thread_mutex_lock(m->lock)) != APR_SUCCESS) {\ - return rv;\ + do { apr_status_t rv_lock; if ((rv_lock = apr_thread_mutex_lock(m->lock)) != APR_SUCCESS) {\ + return rv_lock;\ } } while(0) #define H2_MPLX_LEAVE(m) \ @@ -72,71 +97,150 @@ apr_status_t h2_mplx_child_init(apr_pool_t *pool, server_rec *s) #define H2_MPLX_ENTER_ALWAYS(m) \ apr_thread_mutex_lock(m->lock) -#define H2_MPLX_ENTER_MAYBE(m, lock) \ - if (lock) apr_thread_mutex_lock(m->lock) - -#define H2_MPLX_LEAVE_MAYBE(m, lock) \ - if (lock) apr_thread_mutex_unlock(m->lock) +#define H2_MPLX_ENTER_MAYBE(m, dolock) \ + if (dolock) apr_thread_mutex_lock(m->lock) -static void check_data_for(h2_mplx *m, h2_stream *stream, int lock); +#define H2_MPLX_LEAVE_MAYBE(m, dolock) \ + if (dolock) apr_thread_mutex_unlock(m->lock) -static void stream_output_consumed(void *ctx, - h2_bucket_beam *beam, apr_off_t length) +static void c1_input_consumed(void *ctx, h2_bucket_beam *beam, apr_off_t length) { - h2_stream *stream = ctx; - h2_task *task = stream->task; - - if (length > 0 && task && task->assigned) { - h2_req_engine_out_consumed(task->assigned, task->c, length); - } + h2_stream_in_consumed(ctx, length); } -static void stream_input_ev(void *ctx, h2_bucket_beam *beam) +static int stream_is_running(h2_stream *stream) { - h2_stream *stream = ctx; - h2_mplx *m = stream->session->mplx; - apr_atomic_set32(&m->event_pending, 1); + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(stream->c2); + return conn_ctx && apr_atomic_read32(&conn_ctx->started) != 0 + && apr_atomic_read32(&conn_ctx->done) == 0; } -static void stream_input_consumed(void *ctx, h2_bucket_beam *beam, apr_off_t length) +int h2_mplx_c1_stream_is_running(h2_mplx *m, h2_stream *stream) { - h2_stream_in_consumed(ctx, length); + int rv; + + H2_MPLX_ENTER(m); + rv = stream_is_running(stream); + H2_MPLX_LEAVE(m); + return rv; } -static void stream_joined(h2_mplx *m, h2_stream *stream) +static void c1c2_stream_joined(h2_mplx *m, h2_stream *stream) { - ap_assert(!stream->task || stream->task->worker_done); + ap_assert(!stream_is_running(stream)); h2_ihash_remove(m->shold, stream->id); - h2_ihash_add(m->spurge, stream); + APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; } -static void stream_cleanup(h2_mplx *m, h2_stream *stream) +static void m_stream_cleanup(h2_mplx *m, h2_stream *stream) { - ap_assert(stream->state == H2_SS_CLEANUP); + h2_conn_ctx_t *c2_ctx = h2_conn_ctx_get(stream->c2); - if (stream->input) { - h2_beam_on_consumed(stream->input, NULL, NULL, NULL); - h2_beam_abort(stream->input); - } - if (stream->output) { - h2_beam_on_produced(stream->output, NULL, NULL); - h2_beam_leave(stream->output); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_STRM_MSG(stream, "cleanup, unsubscribing from beam events")); + if (c2_ctx) { + if (c2_ctx->beam_out) { + h2_beam_on_was_empty(c2_ctx->beam_out, NULL, NULL); + } + if (c2_ctx->beam_in) { + h2_beam_on_send(c2_ctx->beam_in, NULL, NULL); + h2_beam_on_received(c2_ctx->beam_in, NULL, NULL); + h2_beam_on_eagain(c2_ctx->beam_in, NULL, NULL); + h2_beam_on_consumed(c2_ctx->beam_in, NULL, NULL); + } } - - h2_stream_cleanup(stream); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_STRM_MSG(stream, "cleanup, removing from registries")); + ap_assert(stream->state == H2_SS_CLEANUP); + h2_stream_cleanup(stream); h2_ihash_remove(m->streams, stream->id); h2_iq_remove(m->q, stream->id); - h2_ififo_remove(m->readyq, stream->id); - h2_ihash_add(m->shold, stream); - - if (!stream->task || stream->task->worker_done) { - stream_joined(m, stream); + + if (c2_ctx) { + if (!stream_is_running(stream)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_STRM_MSG(stream, "cleanup, c2 is done, move to spurge")); + /* processing has finished */ + APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; + } + else { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_STRM_MSG(stream, "cleanup, c2 is running, abort")); + /* c2 is still running */ + h2_c2_abort(stream->c2, m->c1); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_STRM_MSG(stream, "cleanup, c2 is done, move to shold")); + h2_ihash_add(m->shold, stream); + } } - else if (stream->task) { - stream->task->c->aborted = 1; - apr_thread_cond_broadcast(m->task_thawed); + else { + /* never started */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_STRM_MSG(stream, "cleanup, never started, move to spurge")); + APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; + } +} + +static h2_c2_transit *c2_transit_create(h2_mplx *m) +{ + apr_allocator_t *allocator; + apr_pool_t *ptrans; + h2_c2_transit *transit; + apr_status_t rv; + + /* We create a pool with its own allocator to be used for + * processing a request. This is the only way to have the processing + * independent of its parent pool in the sense that it can work in + * another thread. + */ + + rv = apr_allocator_create(&allocator); + if (rv == APR_SUCCESS) { + apr_allocator_max_free_set(allocator, ap_max_mem_free); + rv = apr_pool_create_ex(&ptrans, m->pool, NULL, allocator); + } + if (rv != APR_SUCCESS) { + /* maybe the log goes through, maybe not. */ + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, m->c1, + APLOGNO(10004) "h2_mplx: create transit pool"); + ap_abort_on_oom(); + return NULL; /* should never be reached. */ + } + + apr_allocator_owner_set(allocator, ptrans); + apr_pool_abort_set(abort_on_oom, ptrans); + apr_pool_tag(ptrans, "h2_c2_transit"); + + transit = apr_pcalloc(ptrans, sizeof(*transit)); + transit->pool = ptrans; + transit->bucket_alloc = apr_bucket_alloc_create(ptrans); + return transit; +} + +static void c2_transit_destroy(h2_c2_transit *transit) +{ + apr_pool_destroy(transit->pool); +} + +static h2_c2_transit *c2_transit_get(h2_mplx *m) +{ + h2_c2_transit **ptransit = apr_array_pop(m->c2_transits); + if (ptransit) { + return *ptransit; + } + return c2_transit_create(m); +} + +static void c2_transit_recycle(h2_mplx *m, h2_c2_transit *transit) +{ + if (m->c2_transits->nelts >= APR_INT32_MAX || + (apr_uint32_t)m->c2_transits->nelts >= m->max_spare_transits) { + c2_transit_destroy(transit); + } + else { + APR_ARRAY_PUSH(m->c2_transits, h2_c2_transit*) = transit; } } @@ -151,208 +255,118 @@ static void stream_cleanup(h2_mplx *m, h2_stream *stream) * their HTTP/1 cousins, the separate allocator seems to work better * than protecting a shared h2_session one with an own lock. */ -h2_mplx *h2_mplx_create(conn_rec *c, apr_pool_t *parent, - const h2_config *conf, - h2_workers *workers) +h2_mplx *h2_mplx_c1_create(int child_num, apr_uint32_t id, h2_stream *stream0, + server_rec *s, apr_pool_t *parent, + h2_workers *workers) { + h2_conn_ctx_t *conn_ctx; apr_status_t status = APR_SUCCESS; apr_allocator_t *allocator; - apr_thread_mutex_t *mutex; - h2_mplx *m; - h2_ctx *ctx = h2_ctx_get(c, 0); - ap_assert(conf); + apr_thread_mutex_t *mutex = NULL; + h2_mplx *m = NULL; m = apr_pcalloc(parent, sizeof(h2_mplx)); - if (m) { - m->id = c->id; - m->c = c; - m->s = (ctx? h2_ctx_server_get(ctx) : NULL); - if (!m->s) { - m->s = c->base_server; - } - - /* We create a pool with its own allocator to be used for - * processing slave connections. This is the only way to have the - * processing independant of its parent pool in the sense that it - * can work in another thread. Also, the new allocator needs its own - * mutex to synchronize sub-pools. - */ - status = apr_allocator_create(&allocator); - if (status != APR_SUCCESS) { - return NULL; - } - apr_allocator_max_free_set(allocator, ap_max_mem_free); - apr_pool_create_ex(&m->pool, parent, NULL, allocator); - if (!m->pool) { - apr_allocator_destroy(allocator); - return NULL; - } - apr_pool_tag(m->pool, "h2_mplx"); - apr_allocator_owner_set(allocator, m->pool); - status = apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, - m->pool); - if (status != APR_SUCCESS) { - apr_pool_destroy(m->pool); - return NULL; - } - apr_allocator_mutex_set(allocator, mutex); - - status = apr_thread_mutex_create(&m->lock, APR_THREAD_MUTEX_DEFAULT, - m->pool); - if (status != APR_SUCCESS) { - apr_pool_destroy(m->pool); - return NULL; - } - - status = apr_thread_cond_create(&m->task_thawed, m->pool); - if (status != APR_SUCCESS) { - apr_pool_destroy(m->pool); - return NULL; - } - - m->max_streams = h2_config_geti(conf, H2_CONF_MAX_STREAMS); - m->stream_max_mem = h2_config_geti(conf, H2_CONF_STREAM_MAX_MEM); - - m->streams = h2_ihash_create(m->pool, offsetof(h2_stream,id)); - m->sredo = h2_ihash_create(m->pool, offsetof(h2_stream,id)); - m->shold = h2_ihash_create(m->pool, offsetof(h2_stream,id)); - m->spurge = h2_ihash_create(m->pool, offsetof(h2_stream,id)); - m->q = h2_iq_create(m->pool, m->max_streams); - - status = h2_ififo_set_create(&m->readyq, m->pool, m->max_streams); - if (status != APR_SUCCESS) { - apr_pool_destroy(m->pool); - return NULL; - } + m->stream0 = stream0; + m->c1 = stream0->c2; + m->s = s; + m->child_num = child_num; + m->id = id; + + /* We create a pool with its own allocator to be used for + * processing secondary connections. This is the only way to have the + * processing independent of its parent pool in the sense that it + * can work in another thread. Also, the new allocator needs its own + * mutex to synchronize sub-pools. + */ + status = apr_allocator_create(&allocator); + if (status != APR_SUCCESS) { + allocator = NULL; + goto failure; + } + + apr_allocator_max_free_set(allocator, ap_max_mem_free); + apr_pool_create_ex(&m->pool, parent, NULL, allocator); + if (!m->pool) goto failure; + + apr_pool_tag(m->pool, "h2_mplx"); + apr_allocator_owner_set(allocator, m->pool); + + status = apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, + m->pool); + if (APR_SUCCESS != status) goto failure; + apr_allocator_mutex_set(allocator, mutex); + + status = apr_thread_mutex_create(&m->lock, APR_THREAD_MUTEX_DEFAULT, + m->pool); + if (APR_SUCCESS != status) goto failure; + + m->max_streams = h2_config_sgeti(s, H2_CONF_MAX_STREAMS); + m->stream_max_mem = h2_config_sgeti(s, H2_CONF_STREAM_MAX_MEM); + + m->streams = h2_ihash_create(m->pool, offsetof(h2_stream,id)); + m->shold = h2_ihash_create(m->pool, offsetof(h2_stream,id)); + m->spurge = apr_array_make(m->pool, 10, sizeof(h2_stream*)); + m->q = h2_iq_create(m->pool, m->max_streams); + + m->workers = workers; + m->processing_max = H2MIN(h2_workers_get_max_workers(workers), m->max_streams); + m->processing_limit = 6; /* the original h1 max parallel connections */ + m->last_mood_change = apr_time_now(); + m->mood_update_interval = apr_time_from_msec(100); + + status = mplx_pollset_create(m); + if (APR_SUCCESS != status) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, m->c1, APLOGNO(10308) + "nghttp2: could not create pollset"); + goto failure; + } + m->streams_ev_in = apr_array_make(m->pool, 10, sizeof(h2_stream*)); + m->streams_ev_out = apr_array_make(m->pool, 10, sizeof(h2_stream*)); + + m->streams_input_read = h2_iq_create(m->pool, 10); + m->streams_output_written = h2_iq_create(m->pool, 10); + status = apr_thread_mutex_create(&m->poll_lock, APR_THREAD_MUTEX_DEFAULT, + m->pool); + if (APR_SUCCESS != status) goto failure; + + conn_ctx = h2_conn_ctx_get(m->c1); + if (conn_ctx->pfd.reqevents) { + apr_pollset_add(m->pollset, &conn_ctx->pfd); + } + + m->max_spare_transits = 3; + m->c2_transits = apr_array_make(m->pool, (int)m->max_spare_transits, + sizeof(h2_c2_transit*)); + + m->producer = h2_workers_register(workers, m->pool, + apr_psprintf(m->pool, "h2-%u", + (unsigned int)m->id), + c2_prod_next, c2_prod_done, + workers_shutdown, m); + return m; - m->workers = workers; - m->max_active = workers->max_workers; - m->limit_active = 6; /* the original h1 max parallel connections */ - m->last_limit_change = m->last_idle_block = apr_time_now(); - m->limit_change_interval = apr_time_from_msec(100); - - m->spare_slaves = apr_array_make(m->pool, 10, sizeof(conn_rec*)); - - m->ngn_shed = h2_ngn_shed_create(m->pool, m->c, m->max_streams, - m->stream_max_mem); - h2_ngn_shed_set_ctx(m->ngn_shed , m); +failure: + if (m->pool) { + apr_pool_destroy(m->pool); } - return m; + else if (allocator) { + apr_allocator_destroy(allocator); + } + return NULL; } -int h2_mplx_shutdown(h2_mplx *m) +int h2_mplx_c1_shutdown(h2_mplx *m) { - int max_stream_started = 0; + int max_stream_id_started = 0; H2_MPLX_ENTER(m); - max_stream_started = m->max_stream_started; + max_stream_id_started = m->max_stream_id_started; /* Clear schedule queue, disabling existing streams from starting */ h2_iq_clear(m->q); H2_MPLX_LEAVE(m); - return max_stream_started; -} - -static int input_consumed_signal(h2_mplx *m, h2_stream *stream) -{ - if (stream->input) { - return h2_beam_report_consumption(stream->input); - } - return 0; -} - -static int report_consumption_iter(void *ctx, void *val) -{ - h2_stream *stream = val; - h2_mplx *m = ctx; - - input_consumed_signal(m, stream); - if (stream->state == H2_SS_CLOSED_L - && (!stream->task || stream->task->worker_done)) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, - H2_STRM_LOG(APLOGNO(10026), stream, "remote close missing")); - nghttp2_submit_rst_stream(stream->session->ngh2, NGHTTP2_FLAG_NONE, - stream->id, NGHTTP2_NO_ERROR); - } - return 1; -} - -static int output_consumed_signal(h2_mplx *m, h2_task *task) -{ - if (task->output.beam) { - return h2_beam_report_consumption(task->output.beam); - } - return 0; -} - -static int stream_destroy_iter(void *ctx, void *val) -{ - h2_mplx *m = ctx; - h2_stream *stream = val; - - h2_ihash_remove(m->spurge, stream->id); - ap_assert(stream->state == H2_SS_CLEANUP); - - if (stream->input) { - /* Process outstanding events before destruction */ - input_consumed_signal(m, stream); - h2_beam_log(stream->input, m->c, APLOG_TRACE2, "stream_destroy"); - h2_beam_destroy(stream->input); - stream->input = NULL; - } - - if (stream->task) { - h2_task *task = stream->task; - conn_rec *slave; - int reuse_slave = 0; - - stream->task = NULL; - slave = task->c; - if (slave) { - /* On non-serialized requests, the IO logging has not accounted for any - * meta data send over the network: response headers and h2 frame headers. we - * counted this on the stream and need to add this now. - * This is supposed to happen before the EOR bucket triggers the - * logging of the transaction. *fingers crossed* */ - if (task->request && !task->request->serialize && h2_task_logio_add_bytes_out) { - apr_off_t unaccounted = stream->out_frame_octets - stream->out_data_octets; - if (unaccounted > 0) { - h2_task_logio_add_bytes_out(slave, unaccounted); - } - } - - if (m->s->keep_alive_max == 0 || slave->keepalives < m->s->keep_alive_max) { - reuse_slave = ((m->spare_slaves->nelts < (m->limit_active * 3 / 2)) - && !task->rst_error); - } - - if (reuse_slave && slave->keepalive == AP_CONN_KEEPALIVE) { - h2_beam_log(task->output.beam, m->c, APLOG_DEBUG, - APLOGNO(03385) "h2_task_destroy, reuse slave"); - h2_task_destroy(task); - APR_ARRAY_PUSH(m->spare_slaves, conn_rec*) = slave; - } - else { - h2_beam_log(task->output.beam, m->c, APLOG_TRACE1, - "h2_task_destroy, destroy slave"); - h2_slave_destroy(slave); - } - } - } - h2_stream_destroy(stream); - return 0; -} - -static void purge_streams(h2_mplx *m, int lock) -{ - if (!h2_ihash_empty(m->spurge)) { - H2_MPLX_ENTER_MAYBE(m, lock); - while (!h2_ihash_iter(m->spurge, stream_destroy_iter, m)) { - /* repeat until empty */ - } - H2_MPLX_LEAVE_MAYBE(m, lock); - } + return max_stream_id_started; } typedef struct { @@ -360,13 +374,13 @@ typedef struct { void *ctx; } stream_iter_ctx_t; -static int stream_iter_wrap(void *ctx, void *stream) +static int m_stream_iter_wrap(void *ctx, void *stream) { stream_iter_ctx_t *x = ctx; return x->cb(stream, x->ctx); } -apr_status_t h2_mplx_stream_do(h2_mplx *m, h2_mplx_stream_cb *cb, void *ctx) +apr_status_t h2_mplx_c1_streams_do(h2_mplx *m, h2_mplx_stream_cb *cb, void *ctx) { stream_iter_ctx_t x; @@ -374,276 +388,260 @@ apr_status_t h2_mplx_stream_do(h2_mplx *m, h2_mplx_stream_cb *cb, void *ctx) x.cb = cb; x.ctx = ctx; - h2_ihash_iter(m->streams, stream_iter_wrap, &x); + h2_ihash_iter(m->streams, m_stream_iter_wrap, &x); H2_MPLX_LEAVE(m); return APR_SUCCESS; } -static int report_stream_iter(void *ctx, void *val) { +typedef struct { + int stream_count; + int stream_want_send; +} stream_iter_aws_t; + +static int m_stream_want_send_data(void *ctx, void *stream) +{ + stream_iter_aws_t *x = ctx; + ++x->stream_count; + if (h2_stream_wants_send_data(stream)) + ++x->stream_want_send; + return 1; +} + +int h2_mplx_c1_all_streams_want_send_data(h2_mplx *m) +{ + stream_iter_aws_t x; + x.stream_count = 0; + x.stream_want_send = 0; + H2_MPLX_ENTER(m); + h2_ihash_iter(m->streams, m_stream_want_send_data, &x); + H2_MPLX_LEAVE(m); + return x.stream_count && (x.stream_count == x.stream_want_send); +} + +static int m_report_stream_iter(void *ctx, void *val) { h2_mplx *m = ctx; h2_stream *stream = val; - h2_task *task = stream->task; - if (APLOGctrace1(m->c)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - H2_STRM_MSG(stream, "started=%d, scheduled=%d, ready=%d, out_buffer=%ld"), - !!stream->task, stream->scheduled, h2_stream_is_ready(stream), - (long)h2_beam_get_buffered(stream->output)); - } - if (task) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, /* NO APLOGNO */ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(stream->c2); + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c1, + H2_STRM_MSG(stream, "started=%d, scheduled=%d, ready=%d, out_buffer=%ld"), + !!stream->c2, stream->scheduled, h2_stream_is_ready(stream), + (long)(stream->output? h2_beam_get_buffered(stream->output) : -1)); + if (conn_ctx) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c1, /* NO APLOGNO */ H2_STRM_MSG(stream, "->03198: %s %s %s" - "[started=%d/done=%d/frozen=%d]"), - task->request->method, task->request->authority, - task->request->path, task->worker_started, - task->worker_done, task->frozen); + "[started=%u/done=%u]"), + conn_ctx->request->method, conn_ctx->request->authority, + conn_ctx->request->path, + apr_atomic_read32(&conn_ctx->started), + apr_atomic_read32(&conn_ctx->done)); } else { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, /* NO APLOGNO */ - H2_STRM_MSG(stream, "->03198: no task")); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c1, /* NO APLOGNO */ + H2_STRM_MSG(stream, "->03198: not started")); } return 1; } -static int unexpected_stream_iter(void *ctx, void *val) { +static int m_unexpected_stream_iter(void *ctx, void *val) { h2_mplx *m = ctx; h2_stream *stream = val; - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c, /* NO APLOGNO */ + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c1, /* NO APLOGNO */ H2_STRM_MSG(stream, "unexpected, started=%d, scheduled=%d, ready=%d"), - !!stream->task, stream->scheduled, h2_stream_is_ready(stream)); + !!stream->c2, stream->scheduled, h2_stream_is_ready(stream)); return 1; } -static int stream_cancel_iter(void *ctx, void *val) { +static int m_stream_cancel_iter(void *ctx, void *val) { h2_mplx *m = ctx; h2_stream *stream = val; - /* disabled input consumed reporting */ - if (stream->input) { - h2_beam_on_consumed(stream->input, NULL, NULL, NULL); - } /* take over event monitoring */ h2_stream_set_monitor(stream, NULL); /* Reset, should transit to CLOSED state */ h2_stream_rst(stream, H2_ERR_NO_ERROR); /* All connection data has been sent, simulate cleanup */ h2_stream_dispatch(stream, H2_SEV_EOS_SENT); - stream_cleanup(m, stream); + m_stream_cleanup(m, stream); return 0; } -void h2_mplx_release_and_join(h2_mplx *m, apr_thread_cond_t *wait) +static void c1_purge_streams(h2_mplx *m); + +void h2_mplx_c1_destroy(h2_mplx *m) { apr_status_t status; - int i, wait_secs = 60; + unsigned int i, wait_secs = 60; + int old_aborted; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_MPLX_MSG(m, "start release")); /* How to shut down a h2 connection: - * 0. abort and tell the workers that no more tasks will come from us */ - m->aborted = 1; - h2_workers_unregister(m->workers, m); - + * 0. abort and tell the workers that no more work will come from us */ + m->shutdown = m->aborted = 1; + H2_MPLX_ENTER_ALWAYS(m); + /* While really terminating any c2 connections, treat the master + * connection as aborted. It's not as if we could send any more data + * at this point. */ + old_aborted = m->c1->aborted; + m->c1->aborted = 1; + /* How to shut down a h2 connection: * 1. cancel all streams still active */ - while (!h2_ihash_iter(m->streams, stream_cancel_iter, m)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_MPLX_MSG(m, "release, %u/%u/%d streams (total/hold/purge), %d streams"), + h2_ihash_count(m->streams), + h2_ihash_count(m->shold), + m->spurge->nelts, m->processing_count); + while (!h2_ihash_iter(m->streams, m_stream_cancel_iter, m)) { /* until empty */ } - /* 2. terminate ngn_shed, no more streams - * should be scheduled or in the active set */ - h2_ngn_shed_abort(m->ngn_shed); + /* 2. no more streams should be scheduled or in the active set */ ap_assert(h2_ihash_empty(m->streams)); ap_assert(h2_iq_empty(m->q)); /* 3. while workers are busy on this connection, meaning they - * are processing tasks from this connection, wait on them finishing + * are processing streams from this connection, wait on them finishing * in order to wake us and let us check again. - * Eventually, this has to succeed. */ - m->join_wait = wait; - for (i = 0; h2_ihash_count(m->shold) > 0; ++i) { - status = apr_thread_cond_timedwait(wait, m->lock, apr_time_from_sec(wait_secs)); + * Eventually, this has to succeed. */ + if (!m->join_wait) { + apr_thread_cond_create(&m->join_wait, m->pool); + } + + for (i = 0; h2_ihash_count(m->shold) > 0; ++i) { + status = apr_thread_cond_timedwait(m->join_wait, m->lock, apr_time_from_sec(wait_secs)); if (APR_STATUS_IS_TIMEUP(status)) { /* This can happen if we have very long running requests * that do not time out on IO. */ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, APLOGNO(03198) - "h2_mplx(%ld): waited %d sec for %d tasks", - m->id, i*wait_secs, (int)h2_ihash_count(m->shold)); - h2_ihash_iter(m->shold, report_stream_iter, m); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c1, APLOGNO(03198) + H2_MPLX_MSG(m, "waited %u sec for %u streams"), + i*wait_secs, h2_ihash_count(m->shold)); + h2_ihash_iter(m->shold, m_report_stream_iter, m); } } - ap_assert(m->tasks_active == 0); - m->join_wait = NULL; - - /* 4. close the h2_req_enginge shed */ - h2_ngn_shed_destroy(m->ngn_shed); - m->ngn_shed = NULL; - + + H2_MPLX_LEAVE(m); + h2_workers_join(m->workers, m->producer); + H2_MPLX_ENTER_ALWAYS(m); + /* 4. With all workers done, all streams should be in spurge */ + ap_assert(m->processing_count == 0); if (!h2_ihash_empty(m->shold)) { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c, APLOGNO(03516) - "h2_mplx(%ld): unexpected %d streams in hold", - m->id, (int)h2_ihash_count(m->shold)); - h2_ihash_iter(m->shold, unexpected_stream_iter, m); + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c1, APLOGNO(03516) + H2_MPLX_MSG(m, "unexpected %u streams in hold"), + h2_ihash_count(m->shold)); + h2_ihash_iter(m->shold, m_unexpected_stream_iter, m); } - + + c1_purge_streams(m); + + m->c1->aborted = old_aborted; H2_MPLX_LEAVE(m); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): released", m->id); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_MPLX_MSG(m, "released")); } -apr_status_t h2_mplx_stream_cleanup(h2_mplx *m, h2_stream *stream) +apr_status_t h2_mplx_c1_stream_cleanup(h2_mplx *m, h2_stream *stream, + unsigned int *pstream_count) { H2_MPLX_ENTER(m); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, H2_STRM_MSG(stream, "cleanup")); - stream_cleanup(m, stream); - + m_stream_cleanup(m, stream); + *pstream_count = h2_ihash_count(m->streams); H2_MPLX_LEAVE(m); return APR_SUCCESS; } -h2_stream *h2_mplx_stream_get(h2_mplx *m, int id) +const h2_stream *h2_mplx_c2_stream_get(h2_mplx *m, int stream_id) { h2_stream *s = NULL; H2_MPLX_ENTER_ALWAYS(m); - - s = h2_ihash_get(m->streams, id); - + s = h2_ihash_get(m->streams, stream_id); H2_MPLX_LEAVE(m); + return s; } -static void output_produced(void *ctx, h2_bucket_beam *beam, apr_off_t bytes) -{ - h2_stream *stream = ctx; - h2_mplx *m = stream->session->mplx; - - check_data_for(m, stream, 1); -} -static apr_status_t out_open(h2_mplx *m, int stream_id, h2_bucket_beam *beam) +static void c1_purge_streams(h2_mplx *m) { - apr_status_t status = APR_SUCCESS; - h2_stream *stream = h2_ihash_get(m->streams, stream_id); - - if (!stream || !stream->task || m->aborted) { - return APR_ECONNABORTED; - } - - ap_assert(stream->output == NULL); - stream->output = beam; - - if (APLOGctrace2(m->c)) { - h2_beam_log(beam, m->c, APLOG_TRACE2, "out_open"); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, m->c, - "h2_mplx(%s): out open", stream->task->id); - } - - h2_beam_on_consumed(stream->output, NULL, stream_output_consumed, stream); - h2_beam_on_produced(stream->output, output_produced, stream); - if (stream->task->output.copy_files) { - h2_beam_on_file_beam(stream->output, h2_beam_no_files, NULL); - } - - /* we might see some file buckets in the output, see - * if we have enough handles reserved. */ - check_data_for(m, stream, 0); - return status; -} + h2_stream *stream; + int i; -apr_status_t h2_mplx_out_open(h2_mplx *m, int stream_id, h2_bucket_beam *beam) -{ - apr_status_t status; - - H2_MPLX_ENTER(m); + for (i = 0; i < m->spurge->nelts; ++i) { + stream = APR_ARRAY_IDX(m->spurge, i, h2_stream*); + ap_assert(stream->state == H2_SS_CLEANUP); - if (m->aborted) { - status = APR_ECONNABORTED; - } - else { - status = out_open(m, stream_id, beam); + if (stream->input) { + h2_beam_destroy(stream->input, m->c1); + stream->input = NULL; + } + if (stream->c2) { + conn_rec *c2 = stream->c2; + h2_conn_ctx_t *c2_ctx = h2_conn_ctx_get(c2); + h2_c2_transit *transit; + + stream->c2 = NULL; + ap_assert(c2_ctx); + transit = c2_ctx->transit; + h2_c2_destroy(c2); /* c2_ctx is gone as well */ + if (transit) { + c2_transit_recycle(m, transit); + } + } + h2_stream_destroy(stream); } - - H2_MPLX_LEAVE(m); - return status; + apr_array_clear(m->spurge); } -static apr_status_t out_close(h2_mplx *m, h2_task *task) +void h2_mplx_c1_going_keepalive(h2_mplx *m) { - apr_status_t status = APR_SUCCESS; - h2_stream *stream; - - if (!task) { - return APR_ECONNABORTED; - } - if (task->c) { - ++task->c->keepalives; - } - - stream = h2_ihash_get(m->streams, task->stream_id); - if (!stream) { - return APR_ECONNABORTED; + H2_MPLX_ENTER_ALWAYS(m); + if (m->spurge->nelts) { + c1_purge_streams(m); } - - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, m->c, - "h2_mplx(%s): close", task->id); - status = h2_beam_close(task->output.beam); - h2_beam_log(task->output.beam, m->c, APLOG_TRACE2, "out_close"); - output_consumed_signal(m, task); - check_data_for(m, stream, 0); - return status; + H2_MPLX_LEAVE(m); } -apr_status_t h2_mplx_out_trywait(h2_mplx *m, apr_interval_time_t timeout, - apr_thread_cond_t *iowait) +apr_status_t h2_mplx_c1_poll(h2_mplx *m, apr_interval_time_t timeout, + stream_ev_callback *on_stream_input, + stream_ev_callback *on_stream_output, + void *on_ctx) { - apr_status_t status; - + apr_status_t rv; + H2_MPLX_ENTER(m); if (m->aborted) { - status = APR_ECONNABORTED; - } - else if (h2_mplx_has_master_events(m)) { - status = APR_SUCCESS; - } - else { - purge_streams(m, 0); - h2_ihash_iter(m->streams, report_consumption_iter, m); - m->added_output = iowait; - status = apr_thread_cond_timedwait(m->added_output, m->lock, timeout); - if (APLOGctrace2(m->c)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, - "h2_mplx(%ld): trywait on data for %f ms)", - m->id, timeout/1000.0); - } - m->added_output = NULL; + rv = APR_ECONNABORTED; + goto cleanup; + } + /* Purge (destroy) streams outside of pollset processing. + * Streams that are registered in the pollset, will be removed + * when they are destroyed, but the pollset works on copies + * of these registrations. So, if we destroy streams while + * processing pollset events, we might access freed memory. + */ + if (m->spurge->nelts) { + c1_purge_streams(m); } + rv = mplx_pollset_poll(m, timeout, on_stream_input, on_stream_output, on_ctx); +cleanup: H2_MPLX_LEAVE(m); - return status; -} - -static void check_data_for(h2_mplx *m, h2_stream *stream, int lock) -{ - if (h2_ififo_push(m->readyq, stream->id) == APR_SUCCESS) { - apr_atomic_set32(&m->event_pending, 1); - H2_MPLX_ENTER_MAYBE(m, lock); - if (m->added_output) { - apr_thread_cond_signal(m->added_output); - } - H2_MPLX_LEAVE_MAYBE(m, lock); - } + return rv; } -apr_status_t h2_mplx_reprioritize(h2_mplx *m, h2_stream_pri_cmp *cmp, void *ctx) +apr_status_t h2_mplx_c1_reprioritize(h2_mplx *m, h2_stream_pri_cmp_fn *cmp, + h2_session *session) { apr_status_t status; @@ -653,9 +651,9 @@ apr_status_t h2_mplx_reprioritize(h2_mplx *m, h2_stream_pri_cmp *cmp, void *ctx) status = APR_ECONNABORTED; } else { - h2_iq_sort(m->q, cmp, ctx); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): reprioritize tasks", m->id); + h2_iq_sort(m->q, cmp, session); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_MPLX_MSG(m, "reprioritize streams")); status = APR_SUCCESS; } @@ -663,560 +661,435 @@ apr_status_t h2_mplx_reprioritize(h2_mplx *m, h2_stream_pri_cmp *cmp, void *ctx) return status; } -static void register_if_needed(h2_mplx *m) -{ - if (!m->aborted && !m->is_registered && !h2_iq_empty(m->q)) { - apr_status_t status = h2_workers_register(m->workers, m); - if (status == APR_SUCCESS) { - m->is_registered = 1; - } - else { - ap_log_cerror(APLOG_MARK, APLOG_ERR, status, m->c, APLOGNO(10021) - "h2_mplx(%ld): register at workers", m->id); - } - } -} - -apr_status_t h2_mplx_process(h2_mplx *m, struct h2_stream *stream, - h2_stream_pri_cmp *cmp, void *ctx) +static apr_status_t c1_process_stream(h2_mplx *m, + h2_stream *stream, + h2_stream_pri_cmp_fn *cmp, + h2_session *session) { - apr_status_t status; - - H2_MPLX_ENTER(m); + apr_status_t rv = APR_SUCCESS; if (m->aborted) { - status = APR_ECONNABORTED; + rv = APR_ECONNABORTED; + goto cleanup; + } + if (!stream->request) { + rv = APR_EINVAL; + goto cleanup; + } + if (APLOGctrace1(m->c1)) { + const h2_request *r = stream->request; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_STRM_MSG(stream, "process %s%s%s %s%s%s%s"), + r->protocol? r->protocol : "", + r->protocol? " " : "", + r->method, r->scheme? r->scheme : "", + r->scheme? "://" : "", + r->authority, r->path? r->path: ""); + } + + stream->scheduled = 1; + h2_ihash_add(m->streams, stream); + if (h2_stream_is_ready(stream)) { + /* already have a response */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_STRM_MSG(stream, "process, ready already")); } else { - status = APR_SUCCESS; - h2_ihash_add(m->streams, stream); - if (h2_stream_is_ready(stream)) { - /* already have a response */ - check_data_for(m, stream, 0); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - H2_STRM_MSG(stream, "process, add to readyq")); - } - else { - h2_iq_add(m->q, stream->id, cmp, ctx); - register_if_needed(m); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - H2_STRM_MSG(stream, "process, added to q")); - } + /* last chance to set anything up before stream is processed + * by worker threads. */ + rv = h2_stream_prepare_processing(stream); + if (APR_SUCCESS != rv) goto cleanup; + h2_iq_add(m->q, stream->id, cmp, session); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_STRM_MSG(stream, "process, added to q")); } - H2_MPLX_LEAVE(m); - return status; +cleanup: + return rv; } -static h2_task *next_stream_task(h2_mplx *m) +void h2_mplx_c1_process(h2_mplx *m, + h2_iqueue *ready_to_process, + h2_stream_get_fn *get_stream, + h2_stream_pri_cmp_fn *stream_pri_cmp, + h2_session *session, + unsigned int *pstream_count) { - h2_stream *stream; + apr_status_t rv; int sid; - while (!m->aborted && (m->tasks_active < m->limit_active) - && (sid = h2_iq_shift(m->q)) > 0) { - - stream = h2_ihash_get(m->streams, sid); - if (stream) { - conn_rec *slave, **pslave; - pslave = (conn_rec **)apr_array_pop(m->spare_slaves); - if (pslave) { - slave = *pslave; - slave->aborted = 0; - } - else { - slave = h2_slave_create(m->c, stream->id, m->pool); - } - - if (!stream->task) { + H2_MPLX_ENTER_ALWAYS(m); - if (sid > m->max_stream_started) { - m->max_stream_started = sid; - } - if (stream->input) { - h2_beam_on_consumed(stream->input, stream_input_ev, - stream_input_consumed, stream); - } - - stream->task = h2_task_create(slave, stream->id, - stream->request, m, stream->input, - stream->session->s->timeout, - m->stream_max_mem); - if (!stream->task) { - ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOMEM, slave, - H2_STRM_LOG(APLOGNO(02941), stream, - "create task")); - return NULL; - } - + while ((sid = h2_iq_shift(ready_to_process)) > 0) { + h2_stream *stream = get_stream(session, sid); + if (stream) { + ap_assert(!stream->scheduled); + rv = c1_process_stream(session->mplx, stream, stream_pri_cmp, session); + if (APR_SUCCESS != rv) { + h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR); } - - ++m->tasks_active; - return stream->task; - } - } - return NULL; -} - -apr_status_t h2_mplx_pop_task(h2_mplx *m, h2_task **ptask) -{ - apr_status_t rv = APR_EOF; - - *ptask = NULL; - ap_assert(m); - ap_assert(m->lock); - - if (APR_SUCCESS != (rv = apr_thread_mutex_lock(m->lock))) { - return rv; - } - - if (m->aborted) { - rv = APR_EOF; - } - else { - *ptask = next_stream_task(m); - rv = (*ptask != NULL && !h2_iq_empty(m->q))? APR_EAGAIN : APR_SUCCESS; - } - if (APR_EAGAIN != rv) { - m->is_registered = 0; /* h2_workers will discard this mplx */ - } - H2_MPLX_LEAVE(m); - return rv; -} - -static void task_done(h2_mplx *m, h2_task *task, h2_req_engine *ngn) -{ - h2_stream *stream; - - if (task->frozen) { - /* this task was handed over to an engine for processing - * and the original worker has finished. That means the - * engine may start processing now. */ - h2_task_thaw(task); - apr_thread_cond_broadcast(m->task_thawed); - return; - } - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): task(%s) done", m->id, task->id); - out_close(m, task); - - if (ngn) { - apr_off_t bytes = 0; - h2_beam_send(task->output.beam, NULL, APR_NONBLOCK_READ); - bytes += h2_beam_get_buffered(task->output.beam); - if (bytes > 0) { - /* we need to report consumed and current buffered output - * to the engine. The request will be streamed out or cancelled, - * no more data is coming from it and the engine should update - * its calculations before we destroy this information. */ - h2_req_engine_out_consumed(ngn, task->c, bytes); - } - } - - if (task->engine) { - if (!m->aborted && !task->c->aborted - && !h2_req_engine_is_shutdown(task->engine)) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c, APLOGNO(10022) - "h2_mplx(%ld): task(%s) has not-shutdown " - "engine(%s)", m->id, task->id, - h2_req_engine_get_id(task->engine)); - } - h2_ngn_shed_done_ngn(m->ngn_shed, task->engine); - } - - task->worker_done = 1; - task->done_at = apr_time_now(); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, - "h2_mplx(%s): request done, %f ms elapsed", task->id, - (task->done_at - task->started_at) / 1000.0); - - if (task->started_at > m->last_idle_block) { - /* this task finished without causing an 'idle block', e.g. - * a block by flow control. - */ - if (task->done_at- m->last_limit_change >= m->limit_change_interval - && m->limit_active < m->max_active) { - /* Well behaving stream, allow it more workers */ - m->limit_active = H2MIN(m->limit_active * 2, - m->max_active); - m->last_limit_change = task->done_at; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): increase worker limit to %d", - m->id, m->limit_active); - } - } - - stream = h2_ihash_get(m->streams, task->stream_id); - if (stream) { - /* stream not done yet. */ - if (!m->aborted && h2_ihash_get(m->sredo, stream->id)) { - /* reset and schedule again */ - h2_task_redo(task); - h2_ihash_remove(m->sredo, stream->id); - h2_iq_add(m->q, stream->id, NULL, NULL); } else { - /* stream not cleaned up, stay around */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, - H2_STRM_MSG(stream, "task_done, stream open")); - if (stream->input) { - h2_beam_leave(stream->input); - } - - /* more data will not arrive, resume the stream */ - check_data_for(m, stream, 0); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_MPLX_MSG(m, "stream %d not found to process"), sid); } } - else if ((stream = h2_ihash_get(m->shold, task->stream_id)) != NULL) { - /* stream is done, was just waiting for this. */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, - H2_STRM_MSG(stream, "task_done, in hold")); - if (stream->input) { - h2_beam_leave(stream->input); + if ((m->processing_count < m->processing_limit) && !h2_iq_empty(m->q)) { + H2_MPLX_LEAVE(m); + rv = h2_workers_activate(m->workers, m->producer); + H2_MPLX_ENTER_ALWAYS(m); + if (rv != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, m->c1, APLOGNO(10021) + H2_MPLX_MSG(m, "activate at workers")); } - stream_joined(m, stream); - } - else if ((stream = h2_ihash_get(m->spurge, task->stream_id)) != NULL) { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c, - H2_STRM_LOG(APLOGNO(03517), stream, "already in spurge")); - ap_assert("stream should not be in spurge" == NULL); } - else { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, m->c, APLOGNO(03518) - "h2_mplx(%s): task_done, stream not found", - task->id); - ap_assert("stream should still be available" == NULL); - } -} + *pstream_count = h2_ihash_count(m->streams); -void h2_mplx_task_done(h2_mplx *m, h2_task *task, h2_task **ptask) -{ - H2_MPLX_ENTER_ALWAYS(m); +#if APR_POOL_DEBUG + do { + apr_size_t mem_g, mem_m, mem_s, mem_c1; - task_done(m, task, NULL); - --m->tasks_active; - - if (m->join_wait) { - apr_thread_cond_signal(m->join_wait); - } - if (ptask) { - /* caller wants another task */ - *ptask = next_stream_task(m); - } - register_if_needed(m); + mem_g = pchild? apr_pool_num_bytes(pchild, 1) : 0; + mem_m = apr_pool_num_bytes(m->pool, 1); + mem_s = apr_pool_num_bytes(session->pool, 1); + mem_c1 = apr_pool_num_bytes(m->c1->pool, 1); + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, m->c1, + H2_MPLX_MSG(m, "child mem=%ld, mplx mem=%ld, session mem=%ld, c1=%ld"), + (long)mem_g, (long)mem_m, (long)mem_s, (long)mem_c1); + + } while (0); +#endif H2_MPLX_LEAVE(m); } -/******************************************************************************* - * h2_mplx DoS protection - ******************************************************************************/ +static void c2_beam_input_write_notify(void *ctx, h2_bucket_beam *beam) +{ + conn_rec *c = ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); -static int latest_repeatable_unsubmitted_iter(void *data, void *val) + (void)beam; + if (conn_ctx && conn_ctx->stream_id && conn_ctx->pipe_in[H2_PIPE_IN]) { + apr_file_putc(1, conn_ctx->pipe_in[H2_PIPE_IN]); + } +} + +static void add_stream_poll_event(h2_mplx *m, int stream_id, h2_iqueue *q) { - stream_iter_ctx *ctx = data; - h2_stream *stream = val; - - if (stream->task && !stream->task->worker_done - && h2_task_can_redo(stream->task) - && !h2_ihash_get(ctx->m->sredo, stream->id)) { - if (!h2_stream_is_ready(stream)) { - /* this task occupies a worker, the response has not been submitted - * yet, not been cancelled and it is a repeatable request - * -> it can be re-scheduled later */ - if (!ctx->stream - || (ctx->stream->task->started_at < stream->task->started_at)) { - /* we did not have one or this one was started later */ - ctx->stream = stream; - } - } + apr_thread_mutex_lock(m->poll_lock); + if (h2_iq_append(q, stream_id) && h2_iq_count(q) == 1) { + /* newly added first */ + apr_pollset_wakeup(m->pollset); } - return 1; + apr_thread_mutex_unlock(m->poll_lock); } -static h2_stream *get_latest_repeatable_unsubmitted_stream(h2_mplx *m) +static void c2_beam_input_read_notify(void *ctx, h2_bucket_beam *beam) { - stream_iter_ctx ctx; - ctx.m = m; - ctx.stream = NULL; - h2_ihash_iter(m->streams, latest_repeatable_unsubmitted_iter, &ctx); - return ctx.stream; + conn_rec *c = ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + + if (conn_ctx && conn_ctx->stream_id) { + add_stream_poll_event(conn_ctx->mplx, conn_ctx->stream_id, + conn_ctx->mplx->streams_input_read); + } } -static int timed_out_busy_iter(void *data, void *val) +static void c2_beam_input_read_eagain(void *ctx, h2_bucket_beam *beam) { - stream_iter_ctx *ctx = data; - h2_stream *stream = val; - if (stream->task && !stream->task->worker_done - && (ctx->now - stream->task->started_at) > stream->task->timeout) { - /* timed out stream occupying a worker, found */ - ctx->stream = stream; - return 0; + conn_rec *c = ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + /* installed in the input bucket beams when we use pipes. + * Drain the pipe just before the beam returns APR_EAGAIN. + * A clean state for allowing polling on the pipe to rest + * when the beam is empty */ + if (conn_ctx && conn_ctx->pipe_in[H2_PIPE_OUT]) { + h2_util_drain_pipe(conn_ctx->pipe_in[H2_PIPE_OUT]); } - return 1; } -static h2_stream *get_timed_out_busy_stream(h2_mplx *m) +static void c2_beam_output_write_notify(void *ctx, h2_bucket_beam *beam) { - stream_iter_ctx ctx; - ctx.m = m; - ctx.stream = NULL; - ctx.now = apr_time_now(); - h2_ihash_iter(m->streams, timed_out_busy_iter, &ctx); - return ctx.stream; + conn_rec *c = ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + + if (conn_ctx && conn_ctx->stream_id) { + add_stream_poll_event(conn_ctx->mplx, conn_ctx->stream_id, + conn_ctx->mplx->streams_output_written); + } } -static apr_status_t unschedule_slow_tasks(h2_mplx *m) +static apr_status_t c2_setup_io(h2_mplx *m, conn_rec *c2, h2_stream *stream, h2_c2_transit *transit) { - h2_stream *stream; - int n; - - /* Try to get rid of streams that occupy workers. Look for safe requests - * that are repeatable. If none found, fail the connection. - */ - n = (m->tasks_active - m->limit_active - (int)h2_ihash_count(m->sredo)); - while (n > 0 && (stream = get_latest_repeatable_unsubmitted_stream(m))) { - h2_task_rst(stream->task, H2_ERR_CANCEL); - h2_ihash_add(m->sredo, stream); - --n; + h2_conn_ctx_t *conn_ctx; + apr_status_t rv = APR_SUCCESS; + const char *action = "init"; + + rv = h2_conn_ctx_init_for_c2(&conn_ctx, c2, m, stream, transit); + if (APR_SUCCESS != rv) goto cleanup; + + if (!conn_ctx->beam_out) { + action = "create output beam"; + rv = h2_beam_create(&conn_ctx->beam_out, c2, conn_ctx->req_pool, + stream->id, "output", 0, c2->base_server->timeout); + if (APR_SUCCESS != rv) goto cleanup; + + h2_beam_buffer_size_set(conn_ctx->beam_out, m->stream_max_mem); + h2_beam_on_was_empty(conn_ctx->beam_out, c2_beam_output_write_notify, c2); } - - if ((m->tasks_active - h2_ihash_count(m->sredo)) > m->limit_active) { - h2_stream *stream = get_timed_out_busy_stream(m); - if (stream) { - /* Too many busy workers, unable to cancel enough streams - * and with a busy, timed out stream, we tell the client - * to go away... */ - return APR_TIMEUP; - } + + memset(&conn_ctx->pipe_in, 0, sizeof(conn_ctx->pipe_in)); + if (stream->input) { + conn_ctx->beam_in = stream->input; + h2_beam_on_send(stream->input, c2_beam_input_write_notify, c2); + h2_beam_on_received(stream->input, c2_beam_input_read_notify, c2); + h2_beam_on_consumed(stream->input, c1_input_consumed, stream); +#if H2_USE_PIPES + action = "create input write pipe"; + rv = apr_file_pipe_create_pools(&conn_ctx->pipe_in[H2_PIPE_OUT], + &conn_ctx->pipe_in[H2_PIPE_IN], + APR_READ_BLOCK, + c2->pool, c2->pool); + if (APR_SUCCESS != rv) goto cleanup; +#endif + h2_beam_on_eagain(stream->input, c2_beam_input_read_eagain, c2); + if (!h2_beam_empty(stream->input)) + c2_beam_input_write_notify(c2, stream->input); + } + +cleanup: + stream->output = (APR_SUCCESS == rv)? conn_ctx->beam_out : NULL; + if (APR_SUCCESS != rv) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c2, + H2_STRM_LOG(APLOGNO(10309), stream, + "error %s"), action); } - return APR_SUCCESS; + return rv; } -apr_status_t h2_mplx_idle(h2_mplx *m) +static conn_rec *s_next_c2(h2_mplx *m) { - apr_status_t status = APR_SUCCESS; - apr_time_t now; - apr_size_t scount; - - H2_MPLX_ENTER(m); + h2_stream *stream = NULL; + apr_status_t rv = APR_SUCCESS; + apr_uint32_t sid; + conn_rec *c2 = NULL; + h2_c2_transit *transit = NULL; - scount = h2_ihash_count(m->streams); - if (scount > 0) { - if (m->tasks_active) { - /* If we have streams in connection state 'IDLE', meaning - * all streams are ready to sent data out, but lack - * WINDOW_UPDATEs. - * - * This is ok, unless we have streams that still occupy - * h2 workers. As worker threads are a scarce resource, - * we need to take measures that we do not get DoSed. - * - * This is what we call an 'idle block'. Limit the amount - * of busy workers we allow for this connection until it - * well behaves. - */ - now = apr_time_now(); - m->last_idle_block = now; - if (m->limit_active > 2 - && now - m->last_limit_change >= m->limit_change_interval) { - if (m->limit_active > 16) { - m->limit_active = 16; - } - else if (m->limit_active > 8) { - m->limit_active = 8; - } - else if (m->limit_active > 4) { - m->limit_active = 4; - } - else if (m->limit_active > 2) { - m->limit_active = 2; - } - m->last_limit_change = now; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): decrease worker limit to %d", - m->id, m->limit_active); - } - - if (m->tasks_active > m->limit_active) { - status = unschedule_slow_tasks(m); - } - } - else if (!h2_iq_empty(m->q)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): idle, but %d streams to process", - m->id, (int)h2_iq_count(m->q)); - status = APR_EAGAIN; - } - else { - /* idle, have streams, but no tasks active. what are we waiting for? - * WINDOW_UPDATEs from client? */ - h2_stream *stream = NULL; - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): idle, no tasks ongoing, %d streams", - m->id, (int)h2_ihash_count(m->streams)); - h2_ihash_shift(m->streams, (void**)&stream, 1); - if (stream) { - h2_ihash_add(m->streams, stream); - if (stream->output && !stream->out_checked) { - /* FIXME: this looks like a race between the session thinking - * it is idle and the EOF on a stream not being sent. - * Signal to caller to leave IDLE state. - */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, - H2_STRM_MSG(stream, "output closed=%d, mplx idle" - ", out has %ld bytes buffered"), - h2_beam_is_closed(stream->output), - (long)h2_beam_get_buffered(stream->output)); - h2_ihash_add(m->streams, stream); - check_data_for(m, stream, 0); - stream->out_checked = 1; - status = APR_EAGAIN; - } - } + while (!m->aborted && !stream && (m->processing_count < m->processing_limit) + && (sid = h2_iq_shift(m->q)) > 0) { + stream = h2_ihash_get(m->streams, sid); + } + + if (!stream) { + if (m->processing_count >= m->processing_limit && !h2_iq_empty(m->q)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c1, + H2_MPLX_MSG(m, "delaying request processing. " + "Current limit is %d and %d workers are in use."), + m->processing_limit, m->processing_count); } + goto cleanup; } - register_if_needed(m); - H2_MPLX_LEAVE(m); - return status; -} + if (sid > m->max_stream_id_started) { + m->max_stream_id_started = sid; + } -/******************************************************************************* - * HTTP/2 request engines - ******************************************************************************/ + transit = c2_transit_get(m); +#if AP_HAS_RESPONSE_BUCKETS + c2 = ap_create_secondary_connection(transit->pool, m->c1, transit->bucket_alloc); +#else + c2 = h2_c2_create(m->c1, transit->pool, transit->bucket_alloc); +#endif + if (!c2) goto cleanup; + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, m->c1, + H2_STRM_MSG(stream, "created new c2")); -typedef struct { - h2_mplx * m; - h2_req_engine *ngn; - int streams_updated; -} ngn_update_ctx; + rv = c2_setup_io(m, c2, stream, transit); + if (APR_SUCCESS != rv) goto cleanup; -static int ngn_update_window(void *ctx, void *val) -{ - ngn_update_ctx *uctx = ctx; - h2_stream *stream = val; - if (stream->task && stream->task->assigned == uctx->ngn - && output_consumed_signal(uctx->m, stream->task)) { - ++uctx->streams_updated; + stream->c2 = c2; + ++m->processing_count; + +cleanup: + if (APR_SUCCESS != rv && c2) { + h2_c2_destroy(c2); + c2 = NULL; } - return 1; + if (transit && !c2) { + c2_transit_recycle(m, transit); + } + return c2; } -static apr_status_t ngn_out_update_windows(h2_mplx *m, h2_req_engine *ngn) +static conn_rec *c2_prod_next(void *baton, int *phas_more) { - ngn_update_ctx ctx; - - ctx.m = m; - ctx.ngn = ngn; - ctx.streams_updated = 0; - h2_ihash_iter(m->streams, ngn_update_window, &ctx); - - return ctx.streams_updated? APR_SUCCESS : APR_EAGAIN; + h2_mplx *m = baton; + conn_rec *c = NULL; + + H2_MPLX_ENTER_ALWAYS(m); + if (!m->aborted) { + c = s_next_c2(m); + *phas_more = (c != NULL && !h2_iq_empty(m->q)); + } + H2_MPLX_LEAVE(m); + return c; } -apr_status_t h2_mplx_req_engine_push(const char *ngn_type, - request_rec *r, - http2_req_engine_init *einit) +static void s_c2_done(h2_mplx *m, conn_rec *c2, h2_conn_ctx_t *conn_ctx) { - apr_status_t status; - h2_mplx *m; - h2_task *task; h2_stream *stream; + + ap_assert(conn_ctx); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_mplx(%s-%d): c2 done", conn_ctx->id, conn_ctx->stream_id); + + AP_DEBUG_ASSERT(apr_atomic_read32(&conn_ctx->done) == 0); + apr_atomic_set32(&conn_ctx->done, 1); + conn_ctx->done_at = apr_time_now(); + ++c2->keepalives; + /* From here on, the final handling of c2 is done by c1 processing. + * Which means we can give it c1's scoreboard handle for updates. */ + c2->sbh = m->c1->sbh; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, + "h2_mplx(%s-%d): request done, %f ms elapsed", + conn_ctx->id, conn_ctx->stream_id, + (conn_ctx->done_at - conn_ctx->started_at) / 1000.0); - task = h2_ctx_rget_task(r); - if (!task) { - return APR_ECONNABORTED; + if (!conn_ctx->has_final_response) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, conn_ctx->last_err, c2, + "h2_c2(%s-%d): processing finished without final response", + conn_ctx->id, conn_ctx->stream_id); + c2->aborted = 1; + if (conn_ctx->beam_out) + h2_beam_abort(conn_ctx->beam_out, c2); + } + else if (!conn_ctx->beam_out || !h2_beam_is_complete(conn_ctx->beam_out)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, conn_ctx->last_err, c2, + "h2_c2(%s-%d): processing finished with incomplete output", + conn_ctx->id, conn_ctx->stream_id); + c2->aborted = 1; + h2_beam_abort(conn_ctx->beam_out, c2); + } + else if (!c2->aborted) { + s_mplx_be_happy(m, c2, conn_ctx); } - m = task->mplx; - H2_MPLX_ENTER(m); - - stream = h2_ihash_get(m->streams, task->stream_id); + stream = h2_ihash_get(m->streams, conn_ctx->stream_id); if (stream) { - status = h2_ngn_shed_push_request(m->ngn_shed, ngn_type, r, einit); + /* stream not done yet. trigger a potential polling on the output + * since nothing more will happening here. */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, + H2_STRM_MSG(stream, "c2_done, stream open")); + c2_beam_output_write_notify(c2, NULL); } - else { - status = APR_ECONNABORTED; + else if ((stream = h2_ihash_get(m->shold, conn_ctx->stream_id)) != NULL) { + /* stream is done, was just waiting for this. */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, + H2_STRM_MSG(stream, "c2_done, in hold")); + c1c2_stream_joined(m, stream); } + else { + int i; + + for (i = 0; i < m->spurge->nelts; ++i) { + if (stream == APR_ARRAY_IDX(m->spurge, i, h2_stream*)) { + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c2, + H2_STRM_LOG(APLOGNO(03517), stream, "already in spurge")); + ap_assert("stream should not be in spurge" == NULL); + return; + } + } - H2_MPLX_LEAVE(m); - return status; + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c2, APLOGNO(03518) + "h2_mplx(%s-%d): c2_done, stream not found", + conn_ctx->id, conn_ctx->stream_id); + ap_assert("stream should still be available" == NULL); + } } -apr_status_t h2_mplx_req_engine_pull(h2_req_engine *ngn, - apr_read_type_e block, - int capacity, - request_rec **pr) -{ - h2_ngn_shed *shed = h2_ngn_shed_get_shed(ngn); - h2_mplx *m = h2_ngn_shed_get_ctx(shed); - apr_status_t status; - int want_shutdown; - - H2_MPLX_ENTER(m); +static void c2_prod_done(void *baton, conn_rec *c2) +{ + h2_mplx *m = baton; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2); - want_shutdown = (block == APR_BLOCK_READ); + AP_DEBUG_ASSERT(conn_ctx); + H2_MPLX_ENTER_ALWAYS(m); - /* Take this opportunity to update output consummation - * for this engine */ - ngn_out_update_windows(m, ngn); - - if (want_shutdown && !h2_iq_empty(m->q)) { - /* For a blocking read, check first if requests are to be - * had and, if not, wait a short while before doing the - * blocking, and if unsuccessful, terminating read. - */ - status = h2_ngn_shed_pull_request(shed, ngn, capacity, 1, pr); - if (APR_STATUS_IS_EAGAIN(status)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c, - "h2_mplx(%ld): start block engine pull", m->id); - apr_thread_cond_timedwait(m->task_thawed, m->lock, - apr_time_from_msec(20)); - status = h2_ngn_shed_pull_request(shed, ngn, capacity, 1, pr); - } - } - else { - status = h2_ngn_shed_pull_request(shed, ngn, capacity, - want_shutdown, pr); - } + --m->processing_count; + s_c2_done(m, c2, conn_ctx); + if (m->join_wait) apr_thread_cond_signal(m->join_wait); H2_MPLX_LEAVE(m); - return status; } - -void h2_mplx_req_engine_done(h2_req_engine *ngn, conn_rec *r_conn, - apr_status_t status) + +static void workers_shutdown(void *baton, int graceful) { - h2_task *task = h2_ctx_cget_task(r_conn); - - if (task) { - h2_mplx *m = task->mplx; - h2_stream *stream; + h2_mplx *m = baton; + + apr_thread_mutex_lock(m->poll_lock); + /* time to wakeup and assess what to do */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_MPLX_MSG(m, "workers shutdown, waking pollset")); + m->shutdown = 1; + if (!graceful) { + m->aborted = 1; + } + apr_pollset_wakeup(m->pollset); + apr_thread_mutex_unlock(m->poll_lock); +} - H2_MPLX_ENTER_ALWAYS(m); +/******************************************************************************* + * h2_mplx DoS protection + ******************************************************************************/ - stream = h2_ihash_get(m->streams, task->stream_id); - - ngn_out_update_windows(m, ngn); - h2_ngn_shed_done_task(m->ngn_shed, ngn, task); - - if (status != APR_SUCCESS && stream - && h2_task_can_redo(task) - && !h2_ihash_get(m->sredo, stream->id)) { - h2_ihash_add(m->sredo, stream); - } +static void s_mplx_be_happy(h2_mplx *m, conn_rec *c, h2_conn_ctx_t *conn_ctx) +{ + apr_time_t now; - if (task->engine) { - /* cannot report that as done until engine returns */ - } - else { - task_done(m, task, ngn); + if (m->processing_limit < m->processing_max + && conn_ctx->started_at > m->last_mood_change) { + --m->irritations_since; + if (m->processing_limit < m->processing_max + && ((now = apr_time_now()) - m->last_mood_change >= m->mood_update_interval + || m->irritations_since < -m->processing_limit)) { + m->processing_limit = H2MIN(m->processing_limit * 2, m->processing_max); + m->last_mood_change = now; + m->irritations_since = 0; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + H2_MPLX_MSG(m, "mood update, increasing worker limit to %d"), + m->processing_limit); } + } +} - H2_MPLX_LEAVE(m); +static void m_be_annoyed(h2_mplx *m) +{ + apr_time_t now; + + if (m->processing_limit > 2) { + ++m->irritations_since; + if (((now = apr_time_now()) - m->last_mood_change >= m->mood_update_interval) + || (m->irritations_since >= m->processing_limit)) { + + if (m->processing_limit > 16) { + m->processing_limit = 16; + } + else if (m->processing_limit > 8) { + m->processing_limit = 8; + } + else if (m->processing_limit > 4) { + m->processing_limit = 4; + } + else if (m->processing_limit > 2) { + m->processing_limit = 2; + } + m->last_mood_change = now; + m->irritations_since = 0; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, m->c1, + H2_MPLX_MSG(m, "mood update, decreasing worker limit to %d"), + m->processing_limit); + } } } @@ -1224,59 +1097,168 @@ void h2_mplx_req_engine_done(h2_req_engine *ngn, conn_rec *r_conn, * mplx master events dispatching ******************************************************************************/ -int h2_mplx_has_master_events(h2_mplx *m) +static int reset_is_acceptable(h2_stream *stream) { - return apr_atomic_read32(&m->event_pending) > 0; + /* client may terminate a stream via H2 RST_STREAM message at any time. + * This is annyoing when we have committed resources (e.g. worker threads) + * to it, so our mood (e.g. willingness to commit resources on this + * connection in the future) goes down. + * + * This is a DoS protection. We do not want to make it too easy for + * a client to eat up server resources. + * + * However: there are cases where a RST_STREAM is the only way to end + * a request. This includes websockets and server-side-event streams (SSEs). + * The responses to such requests continue forever otherwise. + * + */ + if (!stream_is_running(stream)) return 1; + if (!(stream->id & 0x01)) return 1; /* stream initiated by us. acceptable. */ + if (!stream->response) return 0; /* no response headers produced yet. bad. */ + if (!stream->out_data_frames) return 0; /* no response body data sent yet. bad. */ + return 1; /* otherwise, be forgiving */ } -apr_status_t h2_mplx_dispatch_master_events(h2_mplx *m, - stream_ev_callback *on_resume, - void *on_ctx) +apr_status_t h2_mplx_c1_client_rst(h2_mplx *m, int stream_id, h2_stream *stream) { - h2_stream *stream; - int n, id; - - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c, - "h2_mplx(%ld): dispatch events", m->id); - apr_atomic_set32(&m->event_pending, 0); + apr_status_t status = APR_SUCCESS; + int registered; - /* update input windows for streams */ - h2_ihash_iter(m->streams, report_consumption_iter, m); - purge_streams(m, 1); - - n = h2_ififo_count(m->readyq); - while (n > 0 - && (h2_ififo_try_pull(m->readyq, &id) == APR_SUCCESS)) { - --n; - stream = h2_ihash_get(m->streams, id); - if (stream) { - on_resume(on_ctx, stream); - } + H2_MPLX_ENTER_ALWAYS(m); + registered = (h2_ihash_get(m->streams, stream_id) != NULL); + if (!stream) { + /* a RST might arrive so late, we have already forgotten + * about it. Seems ok. */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c1, + H2_MPLX_MSG(m, "RST on unknown stream %d"), stream_id); + AP_DEBUG_ASSERT(!registered); + } + else if (!registered) { + /* a RST on a stream that mplx has not been told about, but + * which the session knows. Very early and annoying. */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, m->c1, + H2_STRM_MSG(stream, "very early RST, drop")); + h2_stream_set_monitor(stream, NULL); + h2_stream_rst(stream, H2_ERR_STREAM_CLOSED); + h2_stream_dispatch(stream, H2_SEV_EOS_SENT); + m_stream_cleanup(m, stream); + m_be_annoyed(m); + } + else if (!reset_is_acceptable(stream)) { + m_be_annoyed(m); } - - return APR_SUCCESS; + H2_MPLX_LEAVE(m); + return status; } -apr_status_t h2_mplx_keep_active(h2_mplx *m, h2_stream *stream) +static apr_status_t mplx_pollset_create(h2_mplx *m) { - check_data_for(m, stream, 1); - return APR_SUCCESS; + /* stream0 output only */ + return apr_pollset_create(&m->pollset, 1, m->pool, + APR_POLLSET_WAKEABLE); } -int h2_mplx_awaits_data(h2_mplx *m) +static apr_status_t mplx_pollset_poll(h2_mplx *m, apr_interval_time_t timeout, + stream_ev_callback *on_stream_input, + stream_ev_callback *on_stream_output, + void *on_ctx) { - int waiting = 1; - - H2_MPLX_ENTER_ALWAYS(m); + apr_status_t rv; + const apr_pollfd_t *results, *pfd; + apr_int32_t nresults, i; + h2_conn_ctx_t *conn_ctx; + h2_stream *stream; - if (h2_ihash_empty(m->streams)) { - waiting = 0; - } - else if (!m->tasks_active && !h2_ififo_count(m->readyq) - && h2_iq_empty(m->q)) { - waiting = 0; - } + /* Make sure we are not called recursively. */ + ap_assert(!m->polling); + m->polling = 1; + do { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_MPLX_MSG(m, "enter polling timeout=%d"), + (int)apr_time_sec(timeout)); + + apr_array_clear(m->streams_ev_in); + apr_array_clear(m->streams_ev_out); + + do { + /* add streams we started processing in the meantime */ + apr_thread_mutex_lock(m->poll_lock); + if (!h2_iq_empty(m->streams_input_read) + || !h2_iq_empty(m->streams_output_written)) { + while ((i = h2_iq_shift(m->streams_input_read))) { + stream = h2_ihash_get(m->streams, i); + if (stream) { + APR_ARRAY_PUSH(m->streams_ev_in, h2_stream*) = stream; + } + } + while ((i = h2_iq_shift(m->streams_output_written))) { + stream = h2_ihash_get(m->streams, i); + if (stream) { + APR_ARRAY_PUSH(m->streams_ev_out, h2_stream*) = stream; + } + } + nresults = 0; + rv = APR_SUCCESS; + apr_thread_mutex_unlock(m->poll_lock); + break; + } + apr_thread_mutex_unlock(m->poll_lock); + + H2_MPLX_LEAVE(m); + rv = apr_pollset_poll(m->pollset, timeout >= 0? timeout : -1, &nresults, &results); + H2_MPLX_ENTER_ALWAYS(m); + if (APR_STATUS_IS_EINTR(rv) && m->shutdown) { + if (!m->aborted) { + rv = APR_SUCCESS; + } + goto cleanup; + } + } while (APR_STATUS_IS_EINTR(rv)); - H2_MPLX_LEAVE(m); - return waiting; + if (APR_SUCCESS != rv) { + if (APR_STATUS_IS_TIMEUP(rv)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, + H2_MPLX_MSG(m, "polling timed out ")); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, m->c1, APLOGNO(10310) \ + H2_MPLX_MSG(m, "polling failed")); + } + goto cleanup; + } + + for (i = 0; i < nresults; i++) { + pfd = &results[i]; + conn_ctx = pfd->client_data; + + AP_DEBUG_ASSERT(conn_ctx); + if (conn_ctx->stream_id == 0) { + if (on_stream_input) { + APR_ARRAY_PUSH(m->streams_ev_in, h2_stream*) = m->stream0; + } + continue; + } + } + + if (on_stream_input && m->streams_ev_in->nelts) { + H2_MPLX_LEAVE(m); + for (i = 0; i < m->streams_ev_in->nelts; ++i) { + on_stream_input(on_ctx, APR_ARRAY_IDX(m->streams_ev_in, i, h2_stream*)); + } + H2_MPLX_ENTER_ALWAYS(m); + } + if (on_stream_output && m->streams_ev_out->nelts) { + H2_MPLX_LEAVE(m); + for (i = 0; i < m->streams_ev_out->nelts; ++i) { + on_stream_output(on_ctx, APR_ARRAY_IDX(m->streams_ev_out, i, h2_stream*)); + } + H2_MPLX_ENTER_ALWAYS(m); + } + break; + } while(1); + +cleanup: + m->polling = 0; + return rv; } + diff --git a/modules/http2/h2_mplx.h b/modules/http2/h2_mplx.h index 2890b98..860f916 100644 --- a/modules/http2/h2_mplx.h +++ b/modules/http2/h2_mplx.h @@ -18,21 +18,16 @@ #define __mod_h2__h2_mplx__ /** - * The stream multiplexer. It pushes buckets from the connection - * thread to the stream threads and vice versa. It's thread-safe - * to use. + * The stream multiplexer. It performs communication between the + * primary HTTP/2 connection (c1) to the secondary connections (c2) + * that process the requests, aka. HTTP/2 streams. * - * There is one h2_mplx instance for each h2_session, which sits on top - * of a particular httpd conn_rec. Input goes from the connection to - * the stream tasks. Output goes from the stream tasks to the connection, - * e.g. the client. + * There is one h2_mplx instance for each h2_session. * - * For each stream, there can be at most "H2StreamMaxMemSize" output bytes - * queued in the multiplexer. If a task thread tries to write more - * data, it is blocked until space becomes available. - * - * Writing input is never blocked. In order to use flow control on the input, - * the mplx can be polled for input data consumption. + * Naming Convention: + * "h2_mplx_c1_" are methods only to be called by the primary connection + * "h2_mplx_c2_" are methods only to be called by a secondary connection + * "h2_mplx_worker_" are methods only to be called by a h2 worker thread */ struct apr_pool_t; @@ -41,108 +36,97 @@ struct apr_thread_cond_t; struct h2_bucket_beam; struct h2_config; struct h2_ihash_t; -struct h2_task; struct h2_stream; struct h2_request; struct apr_thread_cond_t; struct h2_workers; struct h2_iqueue; -struct h2_ngn_shed; -struct h2_req_engine; #include <apr_queue.h> +#include "h2_workers.h" + +typedef struct h2_c2_transit h2_c2_transit; + +struct h2_c2_transit { + apr_pool_t *pool; + apr_bucket_alloc_t *bucket_alloc; +}; + typedef struct h2_mplx h2_mplx; struct h2_mplx { - long id; - conn_rec *c; + int child_num; /* child this runs in */ + apr_uint32_t id; /* id unique per child */ + conn_rec *c1; /* the main connection */ apr_pool_t *pool; + struct h2_stream *stream0; /* HTTP/2's stream 0 */ server_rec *s; /* server for master conn */ - unsigned int event_pending; - unsigned int aborted; - unsigned int is_registered; /* is registered at h2_workers */ + int shutdown; /* we are shutting down */ + int aborted; /* we need to get out of here asap */ + int polling; /* is waiting/processing pollset events */ + ap_conn_producer_t *producer; /* registered producer at h2_workers */ - struct h2_ihash_t *streams; /* all streams currently processing */ - struct h2_ihash_t *sredo; /* all streams that need to be re-started */ - struct h2_ihash_t *shold; /* all streams done with task ongoing */ - struct h2_ihash_t *spurge; /* all streams done, ready for destroy */ + struct h2_ihash_t *streams; /* all streams active */ + struct h2_ihash_t *shold; /* all streams done with c2 processing ongoing */ + apr_array_header_t *spurge; /* all streams done, ready for destroy */ struct h2_iqueue *q; /* all stream ids that need to be started */ - struct h2_ififo *readyq; /* all stream ids ready for output */ - - struct h2_ihash_t *redo_tasks; /* all tasks that need to be redone */ + + apr_size_t stream_max_mem; /* max memory to buffer for a stream */ + apr_uint32_t max_streams; /* max # of concurrent streams */ + apr_uint32_t max_stream_id_started; /* highest stream id that started processing */ + + apr_uint32_t processing_count; /* # of c2 working for this mplx */ + apr_uint32_t processing_limit; /* current limit on processing c2s, dynamic */ + apr_uint32_t processing_max; /* max, hard limit of processing c2s */ - int max_streams; /* max # of concurrent streams */ - int max_stream_started; /* highest stream id that started processing */ - int tasks_active; /* # of tasks being processed from this mplx */ - int limit_active; /* current limit on active tasks, dynamic */ - int max_active; /* max, hard limit # of active tasks in a process */ - apr_time_t last_idle_block; /* last time, this mplx entered IDLE while - * streams were ready */ - apr_time_t last_limit_change; /* last time, worker limit changed */ - apr_interval_time_t limit_change_interval; + apr_time_t last_mood_change; /* last time, processing limit changed */ + apr_interval_time_t mood_update_interval; /* how frequent we update at most */ + apr_uint32_t irritations_since; /* irritations (>0) or happy events (<0) since last mood change */ apr_thread_mutex_t *lock; - struct apr_thread_cond_t *added_output; - struct apr_thread_cond_t *task_thawed; struct apr_thread_cond_t *join_wait; - apr_size_t stream_max_mem; - - apr_pool_t *spare_io_pool; - apr_array_header_t *spare_slaves; /* spare slave connections */ - - struct h2_workers *workers; - - struct h2_ngn_shed *ngn_shed; -}; + apr_pollset_t *pollset; /* pollset for c1/c2 IO events */ + apr_array_header_t *streams_ev_in; + apr_array_header_t *streams_ev_out; + apr_thread_mutex_t *poll_lock; /* protect modifications of queues below */ + struct h2_iqueue *streams_input_read; /* streams whose input has been read from */ + struct h2_iqueue *streams_output_written; /* streams whose output has been written to */ + struct h2_workers *workers; /* h2 workers process wide instance */ -/******************************************************************************* - * Object lifecycle and information. - ******************************************************************************/ + apr_uint32_t max_spare_transits; /* max number of transit pools idling */ + apr_array_header_t *c2_transits; /* base pools for running c2 connections */ +}; -apr_status_t h2_mplx_child_init(apr_pool_t *pool, server_rec *s); +apr_status_t h2_mplx_c1_child_init(apr_pool_t *pool, server_rec *s); /** * Create the multiplexer for the given HTTP2 session. * Implicitly has reference count 1. */ -h2_mplx *h2_mplx_create(conn_rec *c, apr_pool_t *master, - const struct h2_config *conf, - struct h2_workers *workers); +h2_mplx *h2_mplx_c1_create(int child_id, apr_uint32_t id, + struct h2_stream *stream0, + server_rec *s, apr_pool_t *master, + struct h2_workers *workers); /** - * Decreases the reference counter of this mplx and waits for it - * to reached 0, destroy the mplx afterwards. - * This is to be called from the thread that created the mplx in - * the first place. - * @param m the mplx to be released and destroyed + * Destroy the mplx, shutting down all ongoing processing. + * @param m the mplx destroyed * @param wait condition var to wait on for ref counter == 0 */ -void h2_mplx_release_and_join(h2_mplx *m, struct apr_thread_cond_t *wait); - -apr_status_t h2_mplx_pop_task(h2_mplx *m, struct h2_task **ptask); - -void h2_mplx_task_done(h2_mplx *m, struct h2_task *task, struct h2_task **ptask); +void h2_mplx_c1_destroy(h2_mplx *m); /** * Shut down the multiplexer gracefully. Will no longer schedule new streams * but let the ongoing ones finish normally. * @return the highest stream id being/been processed */ -int h2_mplx_shutdown(h2_mplx *m); - -int h2_mplx_is_busy(h2_mplx *m); - -/******************************************************************************* - * IO lifetime of streams. - ******************************************************************************/ - -struct h2_stream *h2_mplx_stream_get(h2_mplx *m, int id); +int h2_mplx_c1_shutdown(h2_mplx *m); /** * Notifies mplx that a stream has been completely handled on the main @@ -150,33 +134,28 @@ struct h2_stream *h2_mplx_stream_get(h2_mplx *m, int id); * * @param m the mplx itself * @param stream the stream ready for cleanup + * @param pstream_count return the number of streams active */ -apr_status_t h2_mplx_stream_cleanup(h2_mplx *m, struct h2_stream *stream); - -/** - * Waits on output data from any stream in this session to become available. - * Returns APR_TIMEUP if no data arrived in the given time. - */ -apr_status_t h2_mplx_out_trywait(h2_mplx *m, apr_interval_time_t timeout, - struct apr_thread_cond_t *iowait); +apr_status_t h2_mplx_c1_stream_cleanup(h2_mplx *m, struct h2_stream *stream, + unsigned int *pstream_count); -apr_status_t h2_mplx_keep_active(h2_mplx *m, struct h2_stream *stream); - -/******************************************************************************* - * Stream processing. - ******************************************************************************/ +int h2_mplx_c1_stream_is_running(h2_mplx *m, struct h2_stream *stream); /** * Process a stream request. * * @param m the multiplexer - * @param stream the identifier of the stream - * @param r the request to be processed + * @param read_to_process + * @param input_pending * @param cmp the stream priority compare function - * @param ctx context data for the compare function + * @param pstream_count on return the number of streams active in mplx */ -apr_status_t h2_mplx_process(h2_mplx *m, struct h2_stream *stream, - h2_stream_pri_cmp *cmp, void *ctx); +void h2_mplx_c1_process(h2_mplx *m, + struct h2_iqueue *read_to_process, + h2_stream_get_fn *get_stream, + h2_stream_pri_cmp_fn *cmp, + struct h2_session *session, + unsigned int *pstream_count); /** * Stream priorities have changed, reschedule pending requests. @@ -185,146 +164,67 @@ apr_status_t h2_mplx_process(h2_mplx *m, struct h2_stream *stream, * @param cmp the stream priority compare function * @param ctx context data for the compare function */ -apr_status_t h2_mplx_reprioritize(h2_mplx *m, h2_stream_pri_cmp *cmp, void *ctx); - -typedef apr_status_t stream_ev_callback(void *ctx, struct h2_stream *stream); +apr_status_t h2_mplx_c1_reprioritize(h2_mplx *m, h2_stream_pri_cmp_fn *cmp, + struct h2_session *session); -/** - * Check if the multiplexer has events for the master connection pending. - * @return != 0 iff there are events pending - */ -int h2_mplx_has_master_events(h2_mplx *m); +typedef void stream_ev_callback(void *ctx, struct h2_stream *stream); /** - * Dispatch events for the master connection, such as - ± @param m the multiplexer - * @param on_resume new output data has arrived for a suspended stream - * @param ctx user supplied argument to invocation. + * Poll the primary connection for input and the active streams for output. + * Invoke the callback for any stream where an event happened. */ -apr_status_t h2_mplx_dispatch_master_events(h2_mplx *m, - stream_ev_callback *on_resume, - void *ctx); - -int h2_mplx_awaits_data(h2_mplx *m); +apr_status_t h2_mplx_c1_poll(h2_mplx *m, apr_interval_time_t timeout, + stream_ev_callback *on_stream_input, + stream_ev_callback *on_stream_output, + void *on_ctx); -typedef int h2_mplx_stream_cb(struct h2_stream *s, void *ctx); +void h2_mplx_c2_input_read(h2_mplx *m, conn_rec *c2); +void h2_mplx_c2_output_written(h2_mplx *m, conn_rec *c2); -apr_status_t h2_mplx_stream_do(h2_mplx *m, h2_mplx_stream_cb *cb, void *ctx); - -/******************************************************************************* - * Output handling of streams. - ******************************************************************************/ +typedef int h2_mplx_stream_cb(struct h2_stream *s, void *userdata); /** - * Opens the output for the given stream with the specified response. + * Iterate over all streams known to mplx from the primary connection. + * @param m the mplx + * @param cb the callback to invoke on each stream + * @param ctx userdata passed to the callback */ -apr_status_t h2_mplx_out_open(h2_mplx *mplx, int stream_id, - struct h2_bucket_beam *beam); - -/******************************************************************************* - * h2_mplx list Manipulation. - ******************************************************************************/ +apr_status_t h2_mplx_c1_streams_do(h2_mplx *m, h2_mplx_stream_cb *cb, void *ctx); /** - * The magic pointer value that indicates the head of a h2_mplx list - * @param b The mplx list - * @return The magic pointer value + * Return != 0 iff all open streams want to send data */ -#define H2_MPLX_LIST_SENTINEL(b) APR_RING_SENTINEL((b), h2_mplx, link) +int h2_mplx_c1_all_streams_want_send_data(h2_mplx *m); /** - * Determine if the mplx list is empty - * @param b The list to check - * @return true or false + * A stream has been RST_STREAM by the client. Abort + * any processing going on and remove from processing + * queue. */ -#define H2_MPLX_LIST_EMPTY(b) APR_RING_EMPTY((b), h2_mplx, link) +apr_status_t h2_mplx_c1_client_rst(h2_mplx *m, int stream_id, + struct h2_stream *stream); /** - * Return the first mplx in a list - * @param b The list to query - * @return The first mplx in the list + * Get readonly access to a stream for a secondary connection. */ -#define H2_MPLX_LIST_FIRST(b) APR_RING_FIRST(b) +const struct h2_stream *h2_mplx_c2_stream_get(h2_mplx *m, int stream_id); /** - * Return the last mplx in a list - * @param b The list to query - * @return The last mplx int he list + * A h2 worker asks for a secondary connection to process. + * @param out_c2 non-NULL, a pointer where to reveive the next + * secondary connection to process. */ -#define H2_MPLX_LIST_LAST(b) APR_RING_LAST(b) +apr_status_t h2_mplx_worker_pop_c2(h2_mplx *m, conn_rec **out_c2); -/** - * Insert a single mplx at the front of a list - * @param b The list to add to - * @param e The mplx to insert - */ -#define H2_MPLX_LIST_INSERT_HEAD(b, e) do { \ -h2_mplx *ap__b = (e); \ -APR_RING_INSERT_HEAD((b), ap__b, h2_mplx, link); \ -} while (0) /** - * Insert a single mplx at the end of a list - * @param b The list to add to - * @param e The mplx to insert + * Session processing is entering KEEPALIVE, e.g. giving control + * to the MPM for monitoring incoming socket events only. + * Last chance for maintenance work before losing control. */ -#define H2_MPLX_LIST_INSERT_TAIL(b, e) do { \ -h2_mplx *ap__b = (e); \ -APR_RING_INSERT_TAIL((b), ap__b, h2_mplx, link); \ -} while (0) +void h2_mplx_c1_going_keepalive(h2_mplx *m); -/** - * Get the next mplx in the list - * @param e The current mplx - * @return The next mplx - */ -#define H2_MPLX_NEXT(e) APR_RING_NEXT((e), link) -/** - * Get the previous mplx in the list - * @param e The current mplx - * @return The previous mplx - */ -#define H2_MPLX_PREV(e) APR_RING_PREV((e), link) - -/** - * Remove a mplx from its list - * @param e The mplx to remove - */ -#define H2_MPLX_REMOVE(e) APR_RING_REMOVE((e), link) - -/******************************************************************************* - * h2_mplx DoS protection - ******************************************************************************/ - -/** - * Master connection has entered idle mode. - * @param m the mplx instance of the master connection - * @return != SUCCESS iff connection should be terminated - */ -apr_status_t h2_mplx_idle(h2_mplx *m); - -/******************************************************************************* - * h2_req_engine handling - ******************************************************************************/ - -typedef void h2_output_consumed(void *ctx, conn_rec *c, apr_off_t consumed); -typedef apr_status_t h2_mplx_req_engine_init(struct h2_req_engine *engine, - const char *id, - const char *type, - apr_pool_t *pool, - apr_size_t req_buffer_size, - request_rec *r, - h2_output_consumed **pconsumed, - void **pbaton); - -apr_status_t h2_mplx_req_engine_push(const char *ngn_type, - request_rec *r, - h2_mplx_req_engine_init *einit); -apr_status_t h2_mplx_req_engine_pull(struct h2_req_engine *ngn, - apr_read_type_e block, - int capacity, - request_rec **pr); -void h2_mplx_req_engine_done(struct h2_req_engine *ngn, conn_rec *r_conn, - apr_status_t status); +#define H2_MPLX_MSG(m, msg) \ + "h2_mplx(%d-%lu): "msg, m->child_num, (unsigned long)m->id #endif /* defined(__mod_h2__h2_mplx__) */ diff --git a/modules/http2/h2_ngn_shed.c b/modules/http2/h2_ngn_shed.c deleted file mode 100644 index fb85776..0000000 --- a/modules/http2/h2_ngn_shed.c +++ /dev/null @@ -1,392 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <assert.h> -#include <stddef.h> -#include <stdlib.h> - -#include <apr_thread_mutex.h> -#include <apr_thread_cond.h> -#include <apr_strings.h> -#include <apr_time.h> - -#include <httpd.h> -#include <http_core.h> -#include <http_log.h> - -#include "mod_http2.h" - -#include "h2_private.h" -#include "h2.h" -#include "h2_config.h" -#include "h2_conn.h" -#include "h2_ctx.h" -#include "h2_h2.h" -#include "h2_mplx.h" -#include "h2_request.h" -#include "h2_task.h" -#include "h2_util.h" -#include "h2_ngn_shed.h" - - -typedef struct h2_ngn_entry h2_ngn_entry; -struct h2_ngn_entry { - APR_RING_ENTRY(h2_ngn_entry) link; - h2_task *task; - request_rec *r; -}; - -#define H2_NGN_ENTRY_NEXT(e) APR_RING_NEXT((e), link) -#define H2_NGN_ENTRY_PREV(e) APR_RING_PREV((e), link) -#define H2_NGN_ENTRY_REMOVE(e) APR_RING_REMOVE((e), link) - -#define H2_REQ_ENTRIES_SENTINEL(b) APR_RING_SENTINEL((b), h2_ngn_entry, link) -#define H2_REQ_ENTRIES_EMPTY(b) APR_RING_EMPTY((b), h2_ngn_entry, link) -#define H2_REQ_ENTRIES_FIRST(b) APR_RING_FIRST(b) -#define H2_REQ_ENTRIES_LAST(b) APR_RING_LAST(b) - -#define H2_REQ_ENTRIES_INSERT_HEAD(b, e) do { \ -h2_ngn_entry *ap__b = (e); \ -APR_RING_INSERT_HEAD((b), ap__b, h2_ngn_entry, link); \ -} while (0) - -#define H2_REQ_ENTRIES_INSERT_TAIL(b, e) do { \ -h2_ngn_entry *ap__b = (e); \ -APR_RING_INSERT_TAIL((b), ap__b, h2_ngn_entry, link); \ -} while (0) - -struct h2_req_engine { - const char *id; /* identifier */ - const char *type; /* name of the engine type */ - apr_pool_t *pool; /* pool for engine specific allocations */ - conn_rec *c; /* connection this engine is assigned to */ - h2_task *task; /* the task this engine is based on, running in */ - h2_ngn_shed *shed; - - unsigned int shutdown : 1; /* engine is being shut down */ - unsigned int done : 1; /* engine has finished */ - - APR_RING_HEAD(h2_req_entries, h2_ngn_entry) entries; - int capacity; /* maximum concurrent requests */ - int no_assigned; /* # of assigned requests */ - int no_live; /* # of live */ - int no_finished; /* # of finished */ - - h2_output_consumed *out_consumed; - void *out_consumed_ctx; -}; - -const char *h2_req_engine_get_id(h2_req_engine *engine) -{ - return engine->id; -} - -int h2_req_engine_is_shutdown(h2_req_engine *engine) -{ - return engine->shutdown; -} - -void h2_req_engine_out_consumed(h2_req_engine *engine, conn_rec *c, - apr_off_t bytes) -{ - if (engine->out_consumed) { - engine->out_consumed(engine->out_consumed_ctx, c, bytes); - } -} - -h2_ngn_shed *h2_ngn_shed_create(apr_pool_t *pool, conn_rec *c, - int default_capacity, - apr_size_t req_buffer_size) -{ - h2_ngn_shed *shed; - - shed = apr_pcalloc(pool, sizeof(*shed)); - shed->c = c; - shed->pool = pool; - shed->default_capacity = default_capacity; - shed->req_buffer_size = req_buffer_size; - shed->ngns = apr_hash_make(pool); - - return shed; -} - -void h2_ngn_shed_set_ctx(h2_ngn_shed *shed, void *user_ctx) -{ - shed->user_ctx = user_ctx; -} - -void *h2_ngn_shed_get_ctx(h2_ngn_shed *shed) -{ - return shed->user_ctx; -} - -h2_ngn_shed *h2_ngn_shed_get_shed(h2_req_engine *ngn) -{ - return ngn->shed; -} - -void h2_ngn_shed_abort(h2_ngn_shed *shed) -{ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, APLOGNO(03394) - "h2_ngn_shed(%ld): abort", shed->c->id); - shed->aborted = 1; -} - -static void ngn_add_task(h2_req_engine *ngn, h2_task *task, request_rec *r) -{ - h2_ngn_entry *entry = apr_pcalloc(task->pool, sizeof(*entry)); - APR_RING_ELEM_INIT(entry, link); - entry->task = task; - entry->r = r; - H2_REQ_ENTRIES_INSERT_TAIL(&ngn->entries, entry); - ngn->no_assigned++; -} - - -apr_status_t h2_ngn_shed_push_request(h2_ngn_shed *shed, const char *ngn_type, - request_rec *r, - http2_req_engine_init *einit) -{ - h2_req_engine *ngn; - h2_task *task = h2_ctx_rget_task(r); - - ap_assert(task); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, - "h2_ngn_shed(%ld): PUSHing request (task=%s)", shed->c->id, - task->id); - if (task->request->serialize) { - /* Max compatibility, deny processing of this */ - return APR_EOF; - } - - if (task->assigned) { - --task->assigned->no_assigned; - --task->assigned->no_live; - task->assigned = NULL; - } - - if (task->engine) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, task->c, - "h2_ngn_shed(%ld): push task(%s) hosting engine %s " - "already with %d tasks", - shed->c->id, task->id, task->engine->id, - task->engine->no_assigned); - task->assigned = task->engine; - ngn_add_task(task->engine, task, r); - return APR_SUCCESS; - } - - ngn = apr_hash_get(shed->ngns, ngn_type, APR_HASH_KEY_STRING); - if (ngn && !ngn->shutdown) { - /* this task will be processed in another thread, - * freeze any I/O for the time being. */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, task->c, - "h2_ngn_shed(%ld): pushing request %s to %s", - shed->c->id, task->id, ngn->id); - if (!h2_task_has_thawed(task)) { - h2_task_freeze(task); - } - ngn_add_task(ngn, task, r); - return APR_SUCCESS; - } - - /* no existing engine or being shut down, start a new one */ - if (einit) { - apr_status_t status; - apr_pool_t *pool = task->pool; - h2_req_engine *newngn; - - newngn = apr_pcalloc(pool, sizeof(*ngn)); - newngn->pool = pool; - newngn->id = apr_psprintf(pool, "ngn-%s", task->id); - newngn->type = apr_pstrdup(pool, ngn_type); - newngn->c = task->c; - newngn->shed = shed; - newngn->capacity = shed->default_capacity; - newngn->no_assigned = 1; - newngn->no_live = 1; - APR_RING_INIT(&newngn->entries, h2_ngn_entry, link); - - status = einit(newngn, newngn->id, newngn->type, newngn->pool, - shed->req_buffer_size, r, - &newngn->out_consumed, &newngn->out_consumed_ctx); - - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, task->c, APLOGNO(03395) - "h2_ngn_shed(%ld): create engine %s (%s)", - shed->c->id, newngn->id, newngn->type); - if (status == APR_SUCCESS) { - newngn->task = task; - task->engine = newngn; - task->assigned = newngn; - apr_hash_set(shed->ngns, newngn->type, APR_HASH_KEY_STRING, newngn); - } - return status; - } - return APR_EOF; -} - -static h2_ngn_entry *pop_detached(h2_req_engine *ngn) -{ - h2_ngn_entry *entry; - for (entry = H2_REQ_ENTRIES_FIRST(&ngn->entries); - entry != H2_REQ_ENTRIES_SENTINEL(&ngn->entries); - entry = H2_NGN_ENTRY_NEXT(entry)) { - if (h2_task_has_thawed(entry->task) - || (entry->task->engine == ngn)) { - /* The task hosting this engine can always be pulled by it. - * For other task, they need to become detached, e.g. no longer - * assigned to another worker. */ - H2_NGN_ENTRY_REMOVE(entry); - return entry; - } - } - return NULL; -} - -apr_status_t h2_ngn_shed_pull_request(h2_ngn_shed *shed, - h2_req_engine *ngn, - int capacity, - int want_shutdown, - request_rec **pr) -{ - h2_ngn_entry *entry; - - ap_assert(ngn); - *pr = NULL; - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, shed->c, APLOGNO(03396) - "h2_ngn_shed(%ld): pull task for engine %s, shutdown=%d", - shed->c->id, ngn->id, want_shutdown); - if (shed->aborted) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, APLOGNO(03397) - "h2_ngn_shed(%ld): abort while pulling requests %s", - shed->c->id, ngn->id); - ngn->shutdown = 1; - return APR_ECONNABORTED; - } - - ngn->capacity = capacity; - if (H2_REQ_ENTRIES_EMPTY(&ngn->entries)) { - if (want_shutdown) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, - "h2_ngn_shed(%ld): emtpy queue, shutdown engine %s", - shed->c->id, ngn->id); - ngn->shutdown = 1; - } - return ngn->shutdown? APR_EOF : APR_EAGAIN; - } - - if ((entry = pop_detached(ngn))) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, entry->task->c, APLOGNO(03398) - "h2_ngn_shed(%ld): pulled request %s for engine %s", - shed->c->id, entry->task->id, ngn->id); - ngn->no_live++; - *pr = entry->r; - entry->task->assigned = ngn; - /* task will now run in ngn's own thread. Modules like lua - * seem to require the correct thread set in the conn_rec. - * See PR 59542. */ - if (entry->task->c && ngn->c) { - entry->task->c->current_thread = ngn->c->current_thread; - } - if (entry->task->engine == ngn) { - /* If an engine pushes its own base task, and then pulls - * it back to itself again, it needs to be thawed. - */ - h2_task_thaw(entry->task); - } - return APR_SUCCESS; - } - - if (1) { - h2_ngn_entry *entry = H2_REQ_ENTRIES_FIRST(&ngn->entries); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, shed->c, APLOGNO(03399) - "h2_ngn_shed(%ld): pull task, nothing, first task %s", - shed->c->id, entry->task->id); - } - return APR_EAGAIN; -} - -static apr_status_t ngn_done_task(h2_ngn_shed *shed, h2_req_engine *ngn, - h2_task *task, int waslive, int aborted) -{ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, shed->c, APLOGNO(03400) - "h2_ngn_shed(%ld): task %s %s by %s", - shed->c->id, task->id, aborted? "aborted":"done", ngn->id); - ngn->no_finished++; - if (waslive) ngn->no_live--; - ngn->no_assigned--; - task->assigned = NULL; - - return APR_SUCCESS; -} - -apr_status_t h2_ngn_shed_done_task(h2_ngn_shed *shed, - struct h2_req_engine *ngn, h2_task *task) -{ - return ngn_done_task(shed, ngn, task, 1, 0); -} - -void h2_ngn_shed_done_ngn(h2_ngn_shed *shed, struct h2_req_engine *ngn) -{ - if (ngn->done) { - return; - } - - if (!shed->aborted && !H2_REQ_ENTRIES_EMPTY(&ngn->entries)) { - h2_ngn_entry *entry; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, - "h2_ngn_shed(%ld): exit engine %s (%s), " - "has still requests queued, shutdown=%d," - "assigned=%ld, live=%ld, finished=%ld", - shed->c->id, ngn->id, ngn->type, - ngn->shutdown, - (long)ngn->no_assigned, (long)ngn->no_live, - (long)ngn->no_finished); - for (entry = H2_REQ_ENTRIES_FIRST(&ngn->entries); - entry != H2_REQ_ENTRIES_SENTINEL(&ngn->entries); - entry = H2_NGN_ENTRY_NEXT(entry)) { - h2_task *task = entry->task; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, - "h2_ngn_shed(%ld): engine %s has queued task %s, " - "frozen=%d, aborting", - shed->c->id, ngn->id, task->id, task->frozen); - ngn_done_task(shed, ngn, task, 0, 1); - task->engine = task->assigned = NULL; - } - } - if (!shed->aborted && (ngn->no_assigned > 1 || ngn->no_live > 1)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, - "h2_ngn_shed(%ld): exit engine %s (%s), " - "assigned=%ld, live=%ld, finished=%ld", - shed->c->id, ngn->id, ngn->type, - (long)ngn->no_assigned, (long)ngn->no_live, - (long)ngn->no_finished); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, shed->c, - "h2_ngn_shed(%ld): exit engine %s", - shed->c->id, ngn->id); - } - - apr_hash_set(shed->ngns, ngn->type, APR_HASH_KEY_STRING, NULL); - ngn->done = 1; -} - -void h2_ngn_shed_destroy(h2_ngn_shed *shed) -{ - ap_assert(apr_hash_count(shed->ngns) == 0); -} - diff --git a/modules/http2/h2_ngn_shed.h b/modules/http2/h2_ngn_shed.h deleted file mode 100644 index 7764c18..0000000 --- a/modules/http2/h2_ngn_shed.h +++ /dev/null @@ -1,79 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef h2_req_shed_h -#define h2_req_shed_h - -struct h2_req_engine; -struct h2_task; - -typedef struct h2_ngn_shed h2_ngn_shed; -struct h2_ngn_shed { - conn_rec *c; - apr_pool_t *pool; - apr_hash_t *ngns; - void *user_ctx; - - unsigned int aborted : 1; - - int default_capacity; - apr_size_t req_buffer_size; /* preferred buffer size for responses */ -}; - -const char *h2_req_engine_get_id(h2_req_engine *engine); -int h2_req_engine_is_shutdown(h2_req_engine *engine); - -void h2_req_engine_out_consumed(h2_req_engine *engine, conn_rec *c, - apr_off_t bytes); - -typedef apr_status_t h2_shed_ngn_init(h2_req_engine *engine, - const char *id, - const char *type, - apr_pool_t *pool, - apr_size_t req_buffer_size, - request_rec *r, - h2_output_consumed **pconsumed, - void **pbaton); - -h2_ngn_shed *h2_ngn_shed_create(apr_pool_t *pool, conn_rec *c, - int default_capactiy, - apr_size_t req_buffer_size); - -void h2_ngn_shed_destroy(h2_ngn_shed *shed); - -void h2_ngn_shed_set_ctx(h2_ngn_shed *shed, void *user_ctx); -void *h2_ngn_shed_get_ctx(h2_ngn_shed *shed); - -h2_ngn_shed *h2_ngn_shed_get_shed(struct h2_req_engine *ngn); - -void h2_ngn_shed_abort(h2_ngn_shed *shed); - -apr_status_t h2_ngn_shed_push_request(h2_ngn_shed *shed, const char *ngn_type, - request_rec *r, - h2_shed_ngn_init *init_cb); - -apr_status_t h2_ngn_shed_pull_request(h2_ngn_shed *shed, h2_req_engine *pub_ngn, - int capacity, - int want_shutdown, request_rec **pr); - -apr_status_t h2_ngn_shed_done_task(h2_ngn_shed *shed, - struct h2_req_engine *ngn, - struct h2_task *task); - -void h2_ngn_shed_done_ngn(h2_ngn_shed *shed, struct h2_req_engine *ngn); - - -#endif /* h2_req_shed_h */ diff --git a/modules/http2/h2_h2.c b/modules/http2/h2_protocol.c index 5580cef..874753e 100644 --- a/modules/http2/h2_h2.c +++ b/modules/http2/h2_protocol.c @@ -26,45 +26,36 @@ #include <http_connection.h> #include <http_protocol.h> #include <http_request.h> +#include <http_ssl.h> #include <http_log.h> -#include "mod_ssl.h" - #include "mod_http2.h" #include "h2_private.h" #include "h2_bucket_beam.h" #include "h2_stream.h" -#include "h2_task.h" +#include "h2_c2.h" #include "h2_config.h" -#include "h2_ctx.h" -#include "h2_conn.h" -#include "h2_filter.h" +#include "h2_conn_ctx.h" +#include "h2_c1.h" #include "h2_request.h" #include "h2_headers.h" #include "h2_session.h" #include "h2_util.h" -#include "h2_h2.h" +#include "h2_protocol.h" #include "mod_http2.h" -const char *h2_tls_protos[] = { +const char *h2_protocol_ids_tls[] = { "h2", NULL }; -const char *h2_clear_protos[] = { +const char *h2_protocol_ids_clear[] = { "h2c", NULL }; const char *H2_MAGIC_TOKEN = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; /******************************************************************************* - * The optional mod_ssl functions we need. - */ -static APR_OPTIONAL_FN_TYPE(ssl_is_https) *opt_ssl_is_https; -static APR_OPTIONAL_FN_TYPE(ssl_var_lookup) *opt_ssl_var_lookup; - - -/******************************************************************************* * HTTP/2 error stuff */ static const char *h2_err_descr[] = { @@ -84,7 +75,7 @@ static const char *h2_err_descr[] = { "http/1.1 required", }; -const char *h2_h2_err_description(unsigned int h2_error) +const char *h2_protocol_err_description(unsigned int h2_error) { if (h2_error < (sizeof(h2_err_descr)/sizeof(h2_err_descr[0]))) { return h2_err_descr[h2_error]; @@ -429,61 +420,30 @@ static int cipher_is_blacklisted(const char *cipher, const char **psource) return !!*psource; } -/******************************************************************************* - * Hooks for processing incoming connections: - * - process_conn take over connection in case of h2 - */ -static int h2_h2_process_conn(conn_rec* c); -static int h2_h2_pre_close_conn(conn_rec* c); -static int h2_h2_post_read_req(request_rec *r); -static int h2_h2_late_fixups(request_rec *r); - -/******************************************************************************* - * Once per lifetime init, retrieve optional functions - */ -apr_status_t h2_h2_init(apr_pool_t *pool, server_rec *s) +apr_status_t h2_protocol_init(apr_pool_t *pool, server_rec *s) { (void)pool; ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, "h2_h2, child_init"); - opt_ssl_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https); - opt_ssl_var_lookup = APR_RETRIEVE_OPTIONAL_FN(ssl_var_lookup); - - if (!opt_ssl_is_https || !opt_ssl_var_lookup) { - ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, - APLOGNO(02951) "mod_ssl does not seem to be enabled"); - } - cipher_init(pool); return APR_SUCCESS; } -int h2_h2_is_tls(conn_rec *c) -{ - return opt_ssl_is_https && opt_ssl_is_https(c); -} - -int h2_is_acceptable_connection(conn_rec *c, int require_all) +int h2_protocol_is_acceptable_c1(conn_rec *c, request_rec *r, int require_all) { - int is_tls = h2_h2_is_tls(c); - const h2_config *cfg = h2_config_get(c); + int is_tls = ap_ssl_conn_is_ssl(c); - if (is_tls && h2_config_geti(cfg, H2_CONF_MODERN_TLS_ONLY) > 0) { + if (is_tls && h2_config_cgeti(c, H2_CONF_MODERN_TLS_ONLY) > 0) { /* Check TLS connection for modern TLS parameters, as defined in * RFC 7540 and https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility */ apr_pool_t *pool = c->pool; server_rec *s = c->base_server; - char *val; - - if (!opt_ssl_var_lookup) { - /* unable to check */ - return 0; - } - + const char *val; + /* Need Tlsv1.2 or higher, rfc 7540, ch. 9.2 */ - val = opt_ssl_var_lookup(pool, s, c, NULL, (char*)"SSL_PROTOCOL"); + val = ap_ssl_var_lookup(pool, s, c, NULL, "SSL_PROTOCOL"); if (val && *val) { if (strncmp("TLS", val, 3) || !strcmp("TLSv1", val) @@ -500,266 +460,26 @@ int h2_is_acceptable_connection(conn_rec *c, int require_all) return 0; } - /* Check TLS cipher blacklist - */ - val = opt_ssl_var_lookup(pool, s, c, NULL, (char*)"SSL_CIPHER"); - if (val && *val) { - const char *source; - if (cipher_is_blacklisted(val, &source)) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03052) - "h2_h2(%ld): tls cipher %s blacklisted by %s", - (long)c->id, val, source); - return 0; - } - } - else if (require_all) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03053) - "h2_h2(%ld): tls cipher is indetermined", (long)c->id); - return 0; - } - } - return 1; -} - -int h2_allows_h2_direct(conn_rec *c) -{ - const h2_config *cfg = h2_config_get(c); - int is_tls = h2_h2_is_tls(c); - const char *needed_protocol = is_tls? "h2" : "h2c"; - int h2_direct = h2_config_geti(cfg, H2_CONF_DIRECT); - - if (h2_direct < 0) { - h2_direct = is_tls? 0 : 1; - } - return (h2_direct - && ap_is_allowed_protocol(c, NULL, NULL, needed_protocol)); -} - -int h2_allows_h2_upgrade(conn_rec *c) -{ - const h2_config *cfg = h2_config_get(c); - int h2_upgrade = h2_config_geti(cfg, H2_CONF_UPGRADE); - - return h2_upgrade > 0 || (h2_upgrade < 0 && !h2_h2_is_tls(c)); -} - -/******************************************************************************* - * Register various hooks - */ -static const char* const mod_ssl[] = { "mod_ssl.c", NULL}; -static const char* const mod_reqtimeout[] = { "mod_reqtimeout.c", NULL}; - -void h2_h2_register_hooks(void) -{ - /* Our main processing needs to run quite late. Definitely after mod_ssl, - * as we need its connection filters, but also before reqtimeout as its - * method of timeouts is specific to HTTP/1.1 (as of now). - * The core HTTP/1 processing run as REALLY_LAST, so we will have - * a chance to take over before it. - */ - ap_hook_process_connection(h2_h2_process_conn, - mod_ssl, mod_reqtimeout, APR_HOOK_LAST); - - /* One last chance to properly say goodbye if we have not done so - * already. */ - ap_hook_pre_close_connection(h2_h2_pre_close_conn, NULL, mod_ssl, APR_HOOK_LAST); - - /* With "H2SerializeHeaders On", we install the filter in this hook - * that parses the response. This needs to happen before any other post - * read function terminates the request with an error. Otherwise we will - * never see the response. - */ - ap_hook_post_read_request(h2_h2_post_read_req, NULL, NULL, APR_HOOK_REALLY_FIRST); - ap_hook_fixups(h2_h2_late_fixups, NULL, NULL, APR_HOOK_LAST); - - /* special bucket type transfer through a h2_bucket_beam */ - h2_register_bucket_beamer(h2_bucket_headers_beam); - h2_register_bucket_beamer(h2_bucket_observer_beam); -} - -int h2_h2_process_conn(conn_rec* c) -{ - apr_status_t status; - h2_ctx *ctx; - - if (c->master) { - return DECLINED; - } - - ctx = h2_ctx_get(c, 0); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn"); - if (h2_ctx_is_task(ctx)) { - /* our stream pseudo connection */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, task, declined"); - return DECLINED; - } - - if (!ctx && c->keepalives == 0) { - const char *proto = ap_get_protocol(c); - - if (APLOGctrace1(c)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, process_conn, " - "new connection using protocol '%s', direct=%d, " - "tls acceptable=%d", proto, h2_allows_h2_direct(c), - h2_is_acceptable_connection(c, 1)); - } - - if (!strcmp(AP_PROTOCOL_HTTP1, proto) - && h2_allows_h2_direct(c) - && h2_is_acceptable_connection(c, 1)) { - /* Fresh connection still is on http/1.1 and H2Direct is enabled. - * Otherwise connection is in a fully acceptable state. - * -> peek at the first 24 incoming bytes - */ - apr_bucket_brigade *temp; - char *s = NULL; - apr_size_t slen; - - temp = apr_brigade_create(c->pool, c->bucket_alloc); - status = ap_get_brigade(c->input_filters, temp, - AP_MODE_SPECULATIVE, APR_BLOCK_READ, 24); - - if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, APLOGNO(03054) - "h2_h2, error reading 24 bytes speculative"); - apr_brigade_destroy(temp); - return DECLINED; - } - - apr_brigade_pflatten(temp, &s, &slen, c->pool); - if ((slen >= 24) && !memcmp(H2_MAGIC_TOKEN, s, 24)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_h2, direct mode detected"); - if (!ctx) { - ctx = h2_ctx_get(c, 1); - } - h2_ctx_protocol_set(ctx, h2_h2_is_tls(c)? "h2" : "h2c"); - } - else if (APLOGctrace2(c)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, - "h2_h2, not detected in %d bytes(base64): %s", - (int)slen, h2_util_base64url_encode(s, slen, c->pool)); - } - - apr_brigade_destroy(temp); - } - } - - if (ctx) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "process_conn"); - if (!h2_ctx_session_get(ctx)) { - status = h2_conn_setup(ctx, c, NULL); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, c, "conn_setup"); - if (status != APR_SUCCESS) { - h2_ctx_clear(c); - return !OK; - } - } - h2_conn_run(ctx, c); - return OK; - } - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "h2_h2, declined"); - return DECLINED; -} - -static int h2_h2_pre_close_conn(conn_rec *c) -{ - h2_ctx *ctx; - - /* slave connection? */ - if (c->master) { - return DECLINED; - } - - ctx = h2_ctx_get(c, 0); - if (ctx) { - /* If the session has been closed correctly already, we will not - * find a h2_ctx here. The presence indicates that the session - * is still ongoing. */ - return h2_conn_pre_close(ctx, c); - } - return DECLINED; -} - -static void check_push(request_rec *r, const char *tag) -{ - const h2_config *conf = h2_config_rget(r); - if (!r->expecting_100 - && conf && conf->push_list && conf->push_list->nelts > 0) { - int i, old_status; - const char *old_line; - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, - "%s, early announcing %d resources for push", - tag, conf->push_list->nelts); - for (i = 0; i < conf->push_list->nelts; ++i) { - h2_push_res *push = &APR_ARRAY_IDX(conf->push_list, i, h2_push_res); - apr_table_add(r->headers_out, "Link", - apr_psprintf(r->pool, "<%s>; rel=preload%s", - push->uri_ref, push->critical? "; critical" : "")); - } - old_status = r->status; - old_line = r->status_line; - r->status = 103; - r->status_line = "103 Early Hints"; - ap_send_interim_response(r, 1); - r->status = old_status; - r->status_line = old_line; - } -} - -static int h2_h2_post_read_req(request_rec *r) -{ - /* slave connection? */ - if (r->connection->master) { - h2_ctx *ctx = h2_ctx_rget(r); - struct h2_task *task = h2_ctx_get_task(ctx); - /* This hook will get called twice on internal redirects. Take care - * that we manipulate filters only once. */ - if (task && !task->filters_set) { - ap_filter_t *f; - ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r, - "h2_task(%s): adding request filters", task->id); - - /* setup the correct filters to process the request for h2 */ - ap_add_input_filter("H2_REQUEST", task, r, r->connection); - - /* replace the core http filter that formats response headers - * in HTTP/1 with our own that collects status and headers */ - ap_remove_output_filter_byhandle(r->output_filters, "HTTP_HEADER"); - ap_add_output_filter("H2_RESPONSE", task, r, r->connection); - - for (f = r->input_filters; f; f = f->next) { - if (!strcmp("H2_SLAVE_IN", f->frec->name)) { - f->r = r; - break; + if (val && !strcmp("TLSv1.2", val)) { + /* Check TLS cipher blacklist, defined pre-TLSv1.3, so only + * checking for 1.2 */ + val = ap_ssl_var_lookup(pool, s, c, NULL, "SSL_CIPHER"); + if (val && *val) { + const char *source; + if (cipher_is_blacklisted(val, &source)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03052) + "h2_h2(%ld): tls cipher %s blacklisted by %s", + (long)c->id, val, source); + return 0; } } - ap_add_output_filter("H2_TRAILERS_OUT", task, r, r->connection); - task->filters_set = 1; - } - } - return DECLINED; -} - -static int h2_h2_late_fixups(request_rec *r) -{ - /* slave connection? */ - if (r->connection->master) { - h2_ctx *ctx = h2_ctx_rget(r); - struct h2_task *task = h2_ctx_get_task(ctx); - if (task) { - /* check if we copy vs. setaside files in this location */ - task->output.copy_files = h2_config_geti(h2_config_rget(r), - H2_CONF_COPY_FILES); - if (task->output.copy_files) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, task->c, - "h2_slave_out(%s): copy_files on", task->id); - h2_beam_on_file_beam(task->output.beam, h2_beam_no_files, NULL); + else if (require_all) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03053) + "h2_h2(%ld): tls cipher is indetermined", (long)c->id); + return 0; } - check_push(r, "late_fixup"); } } - return DECLINED; + return 1; } diff --git a/modules/http2/h2_h2.h b/modules/http2/h2_protocol.h index 367823d..ed48e89 100644 --- a/modules/http2/h2_h2.h +++ b/modules/http2/h2_protocol.h @@ -14,66 +14,43 @@ * limitations under the License. */ -#ifndef __mod_h2__h2_h2__ -#define __mod_h2__h2_h2__ +#ifndef __mod_h2__h2_protocol__ +#define __mod_h2__h2_protocol__ /** - * List of ALPN protocol identifiers that we support in cleartext + * List of protocol identifiers that we support in cleartext * negotiations. NULL terminated. */ -extern const char *h2_clear_protos[]; +extern const char *h2_protocol_ids_clear[]; /** - * List of ALPN protocol identifiers that we support in TLS encrypted - * negotiations. NULL terminated. + * List of protocol identifiers that we support in TLS encrypted + * negotiations (ALPN). NULL terminated. */ -extern const char *h2_tls_protos[]; +extern const char *h2_protocol_ids_tls[]; /** * Provide a user readable description of the HTTP/2 error code- * @param h2_error http/2 error code, as in rfc 7540, ch. 7 * @return textual description of code or that it is unknown. */ -const char *h2_h2_err_description(unsigned int h2_error); +const char *h2_protocol_err_description(unsigned int h2_error); /* * One time, post config initialization. */ -apr_status_t h2_h2_init(apr_pool_t *pool, server_rec *s); - -/* Is the connection a TLS connection? - */ -int h2_h2_is_tls(conn_rec *c); - -/* Register apache hooks for h2 protocol - */ -void h2_h2_register_hooks(void); +apr_status_t h2_protocol_init(apr_pool_t *pool, server_rec *s); /** - * Check if the given connection fulfills the requirements as configured. + * Check if the given primary connection fulfills the protocol + * requirements for HTTP/2. * @param c the connection * @param require_all != 0 iff any missing connection properties make * the test fail. For example, a cipher might not have been selected while * the handshake is still ongoing. - * @return != 0 iff connection requirements are met - */ -int h2_is_acceptable_connection(conn_rec *c, int require_all); - -/** - * Check if the "direct" HTTP/2 mode of protocol handling is enabled - * for the given connection. - * @param c the connection to check - * @return != 0 iff direct mode is enabled - */ -int h2_allows_h2_direct(conn_rec *c); - -/** - * Check if the "Upgrade" HTTP/1.1 mode of protocol switching is enabled - * for the given connection. - * @param c the connection to check - * @return != 0 iff Upgrade switching is enabled + * @return != 0 iff protocol requirements are met */ -int h2_allows_h2_upgrade(conn_rec *c); +int h2_protocol_is_acceptable_c1(conn_rec *c, request_rec *r, int require_all); -#endif /* defined(__mod_h2__h2_h2__) */ +#endif /* defined(__mod_h2__h2_protocol__) */ diff --git a/modules/http2/h2_proxy_session.c b/modules/http2/h2_proxy_session.c index 8389c7c..db22301 100644 --- a/modules/http2/h2_proxy_session.c +++ b/modules/http2/h2_proxy_session.c @@ -20,6 +20,7 @@ #include <mpm_common.h> #include <httpd.h> +#include <http_protocol.h> #include <mod_proxy.h> #include "mod_http2.h" @@ -36,6 +37,7 @@ typedef struct h2_proxy_stream { const char *url; request_rec *r; + conn_rec *cfront; h2_proxy_request *req; const char *real_server_uri; const char *p_server_uri; @@ -45,6 +47,7 @@ typedef struct h2_proxy_stream { unsigned int suspended : 1; unsigned int waiting_on_100 : 1; unsigned int waiting_on_ping : 1; + unsigned int headers_ended : 1; uint32_t error_code; apr_bucket_brigade *input; @@ -61,7 +64,123 @@ static void dispatch_event(h2_proxy_session *session, h2_proxys_event_t ev, static void ping_arrived(h2_proxy_session *session); static apr_status_t check_suspended(h2_proxy_session *session); static void stream_resume(h2_proxy_stream *stream); +static apr_status_t submit_trailers(h2_proxy_stream *stream); + +/* + * The H2_PING connection sub-state: a state independant of the H2_SESSION state + * of the connection: + * - H2_PING_ST_NONE: no interference with request handling, ProxyTimeout in effect. + * When entered, all suspended streams are unsuspended again. + * - H2_PING_ST_AWAIT_ANY: new requests are suspended, a possibly configured "ping" + * timeout is in effect. Any frame received transits to H2_PING_ST_NONE. + * - H2_PING_ST_AWAIT_PING: same as above, but only a PING frame transits + * to H2_PING_ST_NONE. + * + * An AWAIT state is entered on a new connection or when re-using a connection and + * the last frame received has been some time ago. The latter sends a PING frame + * and insists on an answer, the former is satisfied by any frame received from the + * backend. + * + * This works for new connections as there is always at least one SETTINGS frame + * that the backend sends. When re-using connection, we send a PING and insist on + * receiving one back, as there might be frames in our connection buffers from + * some time ago. Since some servers have protections against PING flooding, we + * only ever have one PING unanswered. + * + * Requests are suspended while in a PING state, as we do not want to send data + * before we can be reasonably sure that the connection is working (at least on + * the h2 protocol level). This also means that the session can do blocking reads + * when expecting PING answers. + */ +static void set_ping_timeout(h2_proxy_session *session) +{ + if (session->ping_timeout != -1 && session->save_timeout == -1) { + apr_socket_t *socket = NULL; + socket = ap_get_conn_socket(session->c); + if (socket) { + apr_socket_timeout_get(socket, &session->save_timeout); + apr_socket_timeout_set(socket, session->ping_timeout); + } + } +} + +static void unset_ping_timeout(h2_proxy_session *session) +{ + if (session->save_timeout != -1) { + apr_socket_t *socket = NULL; + + socket = ap_get_conn_socket(session->c); + if (socket) { + apr_socket_timeout_set(socket, session->save_timeout); + session->save_timeout = -1; + } + } +} + +static void enter_ping_state(h2_proxy_session *session, h2_ping_state_t state) +{ + if (session->ping_state == state) return; + switch (session->ping_state) { + case H2_PING_ST_NONE: + /* leaving NONE, enforce timeout, send frame maybe */ + if (H2_PING_ST_AWAIT_PING == state) { + unset_ping_timeout(session); + nghttp2_submit_ping(session->ngh2, 0, (const uint8_t *)"nevergonnagiveyouup"); + } + set_ping_timeout(session); + session->ping_state = state; + break; + default: + /* no switching between the != NONE states */ + if (H2_PING_ST_NONE == state) { + session->ping_state = state; + unset_ping_timeout(session); + ping_arrived(session); + } + break; + } +} + +static void ping_new_session(h2_proxy_session *session, proxy_conn_rec *p_conn) +{ + session->save_timeout = -1; + session->ping_timeout = (p_conn->worker->s->ping_timeout_set? + p_conn->worker->s->ping_timeout : -1); + session->ping_state = H2_PING_ST_NONE; + enter_ping_state(session, H2_PING_ST_AWAIT_ANY); +} + +static void ping_reuse_session(h2_proxy_session *session) +{ + if (H2_PING_ST_NONE == session->ping_state) { + apr_interval_time_t age = apr_time_now() - session->last_frame_received; + if (age > apr_time_from_sec(1)) { + enter_ping_state(session, H2_PING_ST_AWAIT_PING); + } + } +} + +static void ping_ev_frame_received(h2_proxy_session *session, const nghttp2_frame *frame) +{ + session->last_frame_received = apr_time_now(); + switch (session->ping_state) { + case H2_PING_ST_NONE: + /* nop */ + break; + case H2_PING_ST_AWAIT_ANY: + enter_ping_state(session, H2_PING_ST_NONE); + break; + case H2_PING_ST_AWAIT_PING: + if (NGHTTP2_PING == frame->hd.type) { + enter_ping_state(session, H2_PING_ST_NONE); + } + /* we may receive many other frames while we are waiting for the + * PING answer. They may come all from our connection buffers and + * say nothing about the current state of the backend. */ + break; + } +} static apr_status_t proxy_session_pre_close(void *theconn) { @@ -152,7 +271,8 @@ static int on_frame_recv(nghttp2_session *ngh2, const nghttp2_frame *frame, session->id, buffer); } - session->last_frame_received = apr_time_now(); + ping_ev_frame_received(session, frame); + /* Action for frame types: */ switch (frame->hd.type) { case NGHTTP2_HEADERS: stream = nghttp2_session_get_stream_user_data(ngh2, frame->hd.stream_id); @@ -193,10 +313,6 @@ static int on_frame_recv(nghttp2_session *ngh2, const nghttp2_frame *frame, stream_resume(stream); break; case NGHTTP2_PING: - if (session->check_ping) { - session->check_ping = 0; - ping_arrived(session); - } break; case NGHTTP2_PUSH_PROMISE: break; @@ -241,7 +357,8 @@ static int add_header(void *table, const char *n, const char *v) return 1; } -static void process_proxy_header(h2_proxy_stream *stream, const char *n, const char *v) +static void process_proxy_header(apr_table_t *headers, h2_proxy_stream *stream, + const char *n, const char *v) { static const struct { const char *name; @@ -262,20 +379,18 @@ static void process_proxy_header(h2_proxy_stream *stream, const char *n, const c if (!dconf->preserve_host) { for (i = 0; transform_hdrs[i].name; ++i) { if (!ap_cstr_casecmp(transform_hdrs[i].name, n)) { - apr_table_add(r->headers_out, n, - (*transform_hdrs[i].func)(r, dconf, v)); + apr_table_add(headers, n, (*transform_hdrs[i].func)(r, dconf, v)); return; } } if (!ap_cstr_casecmp("Link", n)) { dconf = ap_get_module_config(r->per_dir_config, &proxy_module); - apr_table_add(r->headers_out, n, - h2_proxy_link_reverse_map(r, dconf, - stream->real_server_uri, stream->p_server_uri, v)); + apr_table_add(headers, n, h2_proxy_link_reverse_map(r, dconf, + stream->real_server_uri, stream->p_server_uri, v)); return; } } - apr_table_add(r->headers_out, n, v); + apr_table_add(headers, n, v); } static apr_status_t h2_proxy_stream_add_header_out(h2_proxy_stream *stream, @@ -287,7 +402,7 @@ static apr_status_t h2_proxy_stream_add_header_out(h2_proxy_stream *stream, 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, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->cfront, "h2_proxy_stream(%s-%d): got status %s", stream->session->id, stream->id, s); stream->r->status = (int)apr_atoi64(s); @@ -299,17 +414,22 @@ static apr_status_t h2_proxy_stream_add_header_out(h2_proxy_stream *stream, return APR_SUCCESS; } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->cfront, + "h2_proxy_stream(%s-%d): on_header %s: %s", + stream->session->id, stream->id, n, v); if (!h2_proxy_res_ignore_header(n, nlen)) { char *hname, *hvalue; + apr_table_t *headers = (stream->headers_ended? + stream->r->trailers_out : stream->r->headers_out); hname = apr_pstrndup(stream->pool, n, nlen); h2_proxy_util_camel_case_header(hname, nlen); hvalue = apr_pstrndup(stream->pool, v, vlen); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->cfront, "h2_proxy_stream(%s-%d): got header %s: %s", stream->session->id, stream->id, hname, hvalue); - process_proxy_header(stream, hname, hvalue); + process_proxy_header(headers, stream, hname, hvalue); } return APR_SUCCESS; } @@ -328,6 +448,7 @@ 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; + const char *buf; /* 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); @@ -337,6 +458,10 @@ static void h2_proxy_stream_end_headers_out(h2_proxy_stream *stream) apr_table_unset(r->headers_out, "Set-Cookie"); r->headers_out = apr_table_overlay(p, r->headers_out, stream->saves); } + + if ((buf = apr_table_get(r->headers_out, "Content-Type"))) { + ap_set_content_type(r, apr_pstrdup(p, buf)); + } /* handle Via header in response */ if (session->conf->viaopt != via_off @@ -374,6 +499,7 @@ static void h2_proxy_stream_end_headers_out(h2_proxy_stream *stream) server_name, portstr) ); } + if (r->status >= 200) stream->headers_ended = 1; if (APLOGrtrace2(stream->r)) { ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, stream->r, @@ -407,34 +533,27 @@ static int stream_response_data(nghttp2_session *ngh2, uint8_t flags, h2_proxy_stream_end_headers_out(stream); } stream->data_received += len; - - b = apr_bucket_transient_create((const char*)data, len, - stream->r->connection->bucket_alloc); + b = apr_bucket_transient_create((const char*)data, len, + stream->cfront->bucket_alloc); APR_BRIGADE_INSERT_TAIL(stream->output, b); /* always flush after a DATA frame, as we have no other indication * of buffer use */ - b = apr_bucket_flush_create(stream->r->connection->bucket_alloc); + b = apr_bucket_flush_create(stream->cfront->bucket_alloc); APR_BRIGADE_INSERT_TAIL(stream->output, b); - + status = ap_pass_brigade(stream->r->output_filters, stream->output); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, stream->r, APLOGNO(03359) "h2_proxy_session(%s): stream=%d, response DATA %ld, %ld" " total", session->id, stream_id, (long)len, (long)stream->data_received); if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, APLOGNO(03344) + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, stream->r, 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; } - if (stream->standalone) { - nghttp2_session_consume(ngh2, stream_id, len); - ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, stream->r, - "h2_proxy_session(%s): stream %d, win_update %d bytes", - session->id, stream_id, (int)len); - } return 0; } @@ -493,12 +612,12 @@ static ssize_t stream_request_data(nghttp2_session *ngh2, int32_t stream_id, stream = nghttp2_session_get_stream_user_data(ngh2, stream_id); if (!stream) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, ap_server_conf, APLOGNO(03361) - "h2_proxy_stream(%s): data_read, stream %d not found", - stream->session->id, stream_id); + "h2_proxy_stream(NULL): data_read, stream %d not found", + stream_id); return NGHTTP2_ERR_CALLBACK_FAILURE; } - if (stream->session->check_ping) { + if (stream->session->ping_state != H2_PING_ST_NONE) { /* suspend until we hear from the other side */ stream->waiting_on_ping = 1; status = APR_EAGAIN; @@ -518,7 +637,7 @@ static ssize_t stream_request_data(nghttp2_session *ngh2, int32_t stream_id, } if (status == APR_SUCCESS) { - ssize_t readlen = 0; + size_t readlen = 0; while (status == APR_SUCCESS && (readlen < length) && !APR_BRIGADE_EMPTY(stream->input)) { @@ -537,7 +656,7 @@ static ssize_t stream_request_data(nghttp2_session *ngh2, int32_t stream_id, status = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ); if (status == APR_SUCCESS && blen > 0) { - ssize_t copylen = H2MIN(length - readlen, blen); + size_t copylen = H2MIN(length - readlen, blen); memcpy(buf, bdata, copylen); buf += copylen; readlen += copylen; @@ -553,9 +672,14 @@ static ssize_t stream_request_data(nghttp2_session *ngh2, int32_t stream_id, stream->data_sent += readlen; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, stream->r, APLOGNO(03468) "h2_proxy_stream(%d): request DATA %ld, %ld" - " total, flags=%d", - stream->id, (long)readlen, (long)stream->data_sent, + " total, flags=%d", stream->id, (long)readlen, (long)stream->data_sent, (int)*data_flags); + if ((*data_flags & NGHTTP2_DATA_FLAG_EOF) && !apr_is_empty_table(stream->r->trailers_in)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, stream->r, APLOGNO(10179) + "h2_proxy_stream(%d): submit trailers", stream->id); + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + submit_trailers(stream); + } return readlen; } else if (APR_STATUS_IS_EAGAIN(status)) { @@ -575,7 +699,7 @@ static ssize_t stream_request_data(nghttp2_session *ngh2, int32_t stream_id, } #ifdef H2_NG2_INVALID_HEADER_CB -static int on_invalid_header_cb(nghttp2_session *ngh2, +static int on_invalid_header_cb(nghttp2_session *ngh2, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, @@ -638,26 +762,22 @@ h2_proxy_session *h2_proxy_session_setup(const char *id, proxy_conn_rec *p_conn, #ifdef H2_NG2_INVALID_HEADER_CB nghttp2_session_callbacks_set_on_invalid_header_callback(cbs, on_invalid_header_cb); #endif - nghttp2_option_new(&option); nghttp2_option_set_peer_max_concurrent_streams(option, 100); - nghttp2_option_set_no_auto_window_update(option, 1); + nghttp2_option_set_no_auto_window_update(option, 0); nghttp2_session_client_new2(&session->ngh2, cbs, session, option); nghttp2_option_del(option); nghttp2_session_callbacks_del(cbs); + ping_new_session(session, p_conn); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03362) "setup session for %s", p_conn->hostname); } else { h2_proxy_session *session = p_conn->data; - apr_interval_time_t age = apr_time_now() - session->last_frame_received; - if (age > apr_time_from_sec(1)) { - session->check_ping = 1; - nghttp2_submit_ping(session->ngh2, 0, (const uint8_t *)"nevergonnagiveyouup"); - } + ping_reuse_session(session); } return p_conn->data; } @@ -698,7 +818,7 @@ static apr_status_t open_stream(h2_proxy_session *session, const char *url, { h2_proxy_stream *stream; apr_uri_t puri; - const char *authority, *scheme, *path; + const char *authority, *scheme, *path, *orig_host; apr_status_t status; proxy_dir_conf *dconf; @@ -707,24 +827,29 @@ static apr_status_t open_stream(h2_proxy_session *session, const char *url, stream->pool = r->pool; stream->url = url; stream->r = r; + stream->cfront = r->connection; stream->standalone = standalone; 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->input = apr_brigade_create(stream->pool, stream->cfront->bucket_alloc); + stream->output = apr_brigade_create(stream->pool, stream->cfront->bucket_alloc); - stream->req = h2_proxy_req_create(1, stream->pool, 0); + stream->req = h2_proxy_req_create(1, stream->pool); status = apr_uri_parse(stream->pool, url, &puri); if (status != APR_SUCCESS) return status; scheme = (strcmp(puri.scheme, "h2")? "http" : "https"); - + orig_host = apr_table_get(r->headers_in, "Host"); + if (orig_host == NULL) { + orig_host = r->hostname; + } + dconf = ap_get_module_config(r->per_dir_config, &proxy_module); if (dconf->preserve_host) { - authority = r->hostname; + authority = orig_host; } else { authority = puri.hostname; @@ -733,20 +858,27 @@ static apr_status_t open_stream(h2_proxy_session *session, const char *url, /* port info missing and port is not default for scheme: append */ authority = apr_psprintf(stream->pool, "%s:%d", authority, puri.port); } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->cfront, + "authority=%s from uri.hostname=%s and uri.port=%d", + authority, puri.hostname, puri.port); } - + /* See #235, we use only :authority when available and remove Host: + * since differing values are not acceptable, see RFC 9113 ch. 8.3.1 */ + if (authority && strlen(authority)) { + apr_table_unset(r->headers_in, "Host"); + } + /* we need this for mapping relative uris in headers ("Link") back * to local uris */ stream->real_server_uri = apr_psprintf(stream->pool, "%s://%s", scheme, authority); stream->p_server_uri = apr_psprintf(stream->pool, "%s://%s", puri.scheme, authority); path = apr_uri_unparse(stream->pool, &puri, APR_URI_UNP_OMITSITEPART); + h2_proxy_req_make(stream->req, stream->pool, r->method, scheme, authority, path, r->headers_in); if (dconf->add_forwarded_headers) { if (PROXYREQ_REVERSE == r->proxyreq) { - const char *buf; - /* Add X-Forwarded-For: so that the upstream has a chance to * determine, where the original request came from. */ @@ -756,8 +888,9 @@ static apr_status_t open_stream(h2_proxy_session *session, const char *url, /* Add X-Forwarded-Host: so that upstream knows what the * original request hostname was. */ - if ((buf = apr_table_get(r->headers_in, "Host"))) { - apr_table_mergen(stream->req->headers, "X-Forwarded-Host", buf); + if (orig_host) { + apr_table_mergen(stream->req->headers, "X-Forwarded-Host", + orig_host); } /* Add X-Forwarded-Server: so that upstream knows what the @@ -768,7 +901,7 @@ static apr_status_t open_stream(h2_proxy_session *session, const char *url, r->server->server_hostname); } } - + /* 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); @@ -811,7 +944,7 @@ static apr_status_t submit_stream(h2_proxy_session *session, h2_proxy_stream *st rv = nghttp2_submit_request(session->ngh2, NULL, hd->nv, hd->nvlen, pp, stream); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03363) + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->cfront, APLOGNO(03363) "h2_proxy_session(%s): submit %s%s -> %d", session->id, stream->req->authority, stream->req->path, rv); @@ -826,12 +959,22 @@ static apr_status_t submit_stream(h2_proxy_session *session, h2_proxy_stream *st return APR_EGENERAL; } +static apr_status_t submit_trailers(h2_proxy_stream *stream) +{ + h2_proxy_ngheader *hd; + int rv; + + hd = h2_proxy_util_nghd_make(stream->pool, stream->r->trailers_in); + rv = nghttp2_submit_trailer(stream->session->ngh2, stream->id, hd->nv, hd->nvlen); + return rv == 0? APR_SUCCESS: 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); @@ -854,9 +997,10 @@ static apr_status_t feed_brigade(h2_proxy_session *session, apr_bucket_brigade * } } else { - readlen += n; - if (n < blen) { - apr_bucket_split(b, n); + size_t rlen = (size_t)n; + readlen += rlen; + if (rlen < blen) { + apr_bucket_split(b, rlen); } } } @@ -882,7 +1026,7 @@ static apr_status_t h2_proxy_session_read(h2_proxy_session *session, int block, apr_socket_t *socket = NULL; apr_time_t save_timeout = -1; - if (block) { + if (block && timeout > 0) { socket = ap_get_conn_socket(session->c); if (socket) { apr_socket_timeout_get(socket, &save_timeout); @@ -945,7 +1089,7 @@ apr_status_t h2_proxy_session_submit(h2_proxy_session *session, static void stream_resume(h2_proxy_stream *stream) { h2_proxy_session *session = stream->session; - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->cfront, "h2_proxy_stream(%s-%d): resuming", session->id, stream->id); stream->suspended = 0; @@ -954,6 +1098,14 @@ static void stream_resume(h2_proxy_stream *stream) dispatch_event(session, H2_PROXYS_EV_STREAM_RESUMED, 0, NULL); } +static int is_waiting_for_backend(h2_proxy_session *session) +{ + return ((session->ping_state != H2_PING_ST_NONE) + || ((session->suspended->nelts <= 0) + && !nghttp2_session_want_write(session->ngh2) + && nghttp2_session_want_read(session->ngh2))); +} + static apr_status_t check_suspended(h2_proxy_session *session) { h2_proxy_stream *stream; @@ -978,7 +1130,7 @@ static apr_status_t check_suspended(h2_proxy_session *session) return APR_SUCCESS; } else if (status != APR_SUCCESS && !APR_STATUS_IS_EAGAIN(status)) { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, stream->cfront, APLOGNO(03382) "h2_proxy_stream(%s-%d): check input", session->id, stream_id); stream_resume(stream); @@ -1006,7 +1158,7 @@ static apr_status_t session_shutdown(h2_proxy_session *session, int reason, if (!err && reason) { err = nghttp2_strerror(reason); } - nghttp2_submit_goaway(session->ngh2, NGHTTP2_FLAG_NONE, 0, + 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); @@ -1208,39 +1360,56 @@ static void ev_stream_done(h2_proxy_session *session, int stream_id, const char *msg) { h2_proxy_stream *stream; - + apr_bucket *b; + stream = nghttp2_session_get_stream_user_data(session->ngh2, stream_id); if (stream) { - int touched = (stream->data_sent || - stream_id <= session->last_stream_id); + /* if the stream's connection is aborted, do not send anything + * more on it. */ apr_status_t status = (stream->error_code == 0)? APR_SUCCESS : APR_EINVAL; - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03364) - "h2_proxy_sesssion(%s): stream(%d) closed " - "(touched=%d, error=%d)", - session->id, stream_id, touched, stream->error_code); - - if (status != APR_SUCCESS) { - stream->r->status = 500; - } - else 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); + int touched = (stream->data_sent || stream->data_received || + stream_id <= session->last_stream_id); + if (!stream->cfront->aborted) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->cfront, APLOGNO(03364) + "h2_proxy_sesssion(%s): stream(%d) closed " + "(touched=%d, error=%d)", + session->id, stream_id, touched, stream->error_code); + + if (status != APR_SUCCESS) { + /* stream failed. If we have received (and forwarded) response + * data already, we need to append an error buckt to inform + * consumers. + * Otherwise, we have an early fail on the connection and may + * retry this request on a new one. In that case, keep the + * output virgin so that a new attempt can be made. */ + if (stream->data_received) { + int http_status = ap_map_http_request_error(status, HTTP_BAD_REQUEST); + b = ap_bucket_error_create(http_status, NULL, stream->r->pool, + stream->cfront->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + b = apr_bucket_eos_create(stream->cfront->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + ap_pass_brigade(stream->r->output_filters, stream->output); + } + } + else if (!stream->data_received) { + /* if the response had no body, this is the time to flush + * an empty brigade which will also write the response headers */ + h2_proxy_stream_end_headers_out(stream); + stream->data_received = 1; + b = apr_bucket_flush_create(stream->cfront->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + b = apr_bucket_eos_create(stream->cfront->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_proxy_ihash_remove(session->streams, stream_id); h2_proxy_iq_remove(session->suspended, stream_id); if (session->done) { - session->done(session, stream->r, status, touched); + session->done(session, stream->r, status, touched, stream->error_code); } } @@ -1408,7 +1577,22 @@ run_loop: break; case H2_PROXYS_ST_WAIT: - if (check_suspended(session) == APR_EAGAIN) { + if (is_waiting_for_backend(session)) { + /* we can do a blocking read with the default timeout (as + * configured via ProxyTimeout in our socket. There is + * nothing we want to send or check until we get more data + * from the backend. */ + status = h2_proxy_session_read(session, 1, 0); + if (status == APR_SUCCESS) { + have_read = 1; + dispatch_event(session, H2_PROXYS_EV_DATA_READ, 0, NULL); + } + else { + dispatch_event(session, H2_PROXYS_EV_CONN_ERROR, status, NULL); + return status; + } + } + else if (check_suspended(session) == APR_EAGAIN) { /* no stream has become resumed. Do a blocking read with * ever increasing timeouts... */ if (session->wait_timeout < 25) { @@ -1423,7 +1607,7 @@ run_loop: ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, session->c, APLOGNO(03365) "h2_proxy_session(%s): WAIT read, timeout=%fms", - session->id, (float)session->wait_timeout/1000.0); + session->id, session->wait_timeout/1000.0); if (status == APR_SUCCESS) { have_read = 1; dispatch_event(session, H2_PROXYS_EV_DATA_READ, 0, NULL); @@ -1495,9 +1679,19 @@ static int done_iter(void *udata, void *val) { cleanup_iter_ctx *ctx = udata; h2_proxy_stream *stream = val; - int touched = (stream->data_sent || + int touched = (stream->data_sent || stream->data_received || stream->id <= ctx->session->last_stream_id); - ctx->done(ctx->session, stream->r, APR_ECONNABORTED, touched); + if (touched && stream->output) { + apr_bucket *b = ap_bucket_error_create(HTTP_BAD_GATEWAY, NULL, + stream->r->pool, + stream->cfront->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + b = apr_bucket_eos_create(stream->cfront->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(stream->output, b); + ap_pass_brigade(stream->r->output_filters, stream->output); + } + ctx->done(ctx->session, stream->r, APR_ECONNABORTED, touched, + stream->error_code); return 1; } @@ -1516,6 +1710,12 @@ void h2_proxy_session_cleanup(h2_proxy_session *session, } } +int h2_proxy_session_is_reusable(h2_proxy_session *session) +{ + return (session->state != H2_PROXYS_ST_DONE) && + h2_proxy_ihash_empty(session->streams); +} + static int ping_arrived_iter(void *udata, void *val) { h2_proxy_stream *stream = val; @@ -1543,42 +1743,3 @@ typedef struct { 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 (!h2_proxy_ihash_empty(session->streams)) { - win_update_ctx ctx; - ctx.session = session; - ctx.c = c; - ctx.bytes = bytes; - ctx.updated = 0; - h2_proxy_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 index ecebb61..3bc16d7 100644 --- a/modules/http2/h2_proxy_session.h +++ b/modules/http2/h2_proxy_session.h @@ -60,10 +60,16 @@ typedef enum { H2_PROXYS_EV_PRE_CLOSE, /* connection will close after this */ } h2_proxys_event_t; +typedef enum { + H2_PING_ST_NONE, /* normal connection mode, ProxyTimeout rules */ + H2_PING_ST_AWAIT_ANY, /* waiting for any frame from backend */ + H2_PING_ST_AWAIT_PING, /* waiting for PING frame from backend */ +} h2_ping_state_t; typedef struct h2_proxy_session h2_proxy_session; typedef void h2_proxy_request_done(h2_proxy_session *s, request_rec *r, - apr_status_t status, int touched); + apr_status_t status, int touched, + int error_code); struct h2_proxy_session { const char *id; @@ -74,7 +80,6 @@ struct h2_proxy_session { nghttp2_session *ngh2; /* the nghttp2 session itself */ unsigned int aborted : 1; - unsigned int check_ping : 1; unsigned int h2_front : 1; /* if front-end connection is HTTP/2 */ h2_proxy_request_done *done; @@ -94,6 +99,10 @@ struct h2_proxy_session { apr_bucket_brigade *input; apr_bucket_brigade *output; + + h2_ping_state_t ping_state; + apr_time_t ping_timeout; + apr_time_t save_timeout; }; h2_proxy_session *h2_proxy_session_setup(const char *id, proxy_conn_rec *p_conn, @@ -120,9 +129,8 @@ void h2_proxy_session_cancel_all(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" +int h2_proxy_session_is_reusable(h2_proxy_session *s); + #endif /* h2_proxy_session_h */ diff --git a/modules/http2/h2_proxy_util.c b/modules/http2/h2_proxy_util.c index bd45294..dc69ec0 100644 --- a/modules/http2/h2_proxy_util.c +++ b/modules/http2/h2_proxy_util.c @@ -452,6 +452,22 @@ h2_proxy_ngheader *h2_proxy_util_nghd_make_req(apr_pool_t *p, return ngh; } +h2_proxy_ngheader *h2_proxy_util_nghd_make(apr_pool_t *p, apr_table_t *headers) +{ + + h2_proxy_ngheader *ngh; + size_t n; + + n = 0; + apr_table_do(count_header, &n, headers, NULL); + + ngh = apr_pcalloc(p, sizeof(h2_proxy_ngheader)); + ngh->nv = apr_pcalloc(p, n * sizeof(nghttp2_nv)); + apr_table_do(add_table_header, ngh, headers, NULL); + + return ngh; +} + /******************************************************************************* * header HTTP/1 <-> HTTP/2 conversions ******************************************************************************/ @@ -480,7 +496,7 @@ static int ignore_header(const literal *lits, size_t llen, const char *name, size_t nlen) { const literal *lit; - int i; + size_t i; for (i = 0; i < llen; ++i) { lit = &lits[i]; @@ -567,8 +583,7 @@ static apr_status_t h2_headers_add_h1(apr_table_t *headers, apr_pool_t *pool, static h2_proxy_request *h2_proxy_req_createn(int id, apr_pool_t *pool, const char *method, const char *scheme, const char *authority, - const char *path, apr_table_t *header, - int serialize) + const char *path, apr_table_t *header) { h2_proxy_request *req = apr_pcalloc(pool, sizeof(h2_proxy_request)); @@ -578,14 +593,13 @@ static h2_proxy_request *h2_proxy_req_createn(int id, apr_pool_t *pool, const ch req->path = path; req->headers = header? header : apr_table_make(pool, 10); req->request_time = apr_time_now(); - req->serialize = serialize; - + return req; } -h2_proxy_request *h2_proxy_req_create(int id, apr_pool_t *pool, int serialize) +h2_proxy_request *h2_proxy_req_create(int id, apr_pool_t *pool) { - return h2_proxy_req_createn(id, pool, NULL, NULL, NULL, NULL, NULL, serialize); + return h2_proxy_req_createn(id, pool, NULL, NULL, NULL, NULL, NULL); } typedef struct { @@ -609,6 +623,7 @@ apr_status_t h2_proxy_req_make(h2_proxy_request *req, apr_pool_t *pool, apr_table_t *headers) { h1_ctx x; + const char *val; req->method = method; req->scheme = scheme; @@ -623,6 +638,11 @@ apr_status_t h2_proxy_req_make(h2_proxy_request *req, apr_pool_t *pool, x.pool = pool; x.headers = req->headers; apr_table_do(set_h1_header, &x, headers, NULL); + if ((val = apr_table_get(headers, "TE")) && ap_find_token(pool, val, "trailers")) { + /* client accepts trailers, forward this information */ + apr_table_addn(req->headers, "TE", "trailers"); + } + apr_table_setn(req->headers, "te", "trailers"); return APR_SUCCESS; } @@ -915,12 +935,12 @@ static size_t subst_str(link_ctx *ctx, int start, int end, const char *ns) nlen = (int)strlen(ns); delta = nlen - olen; plen = ctx->slen + delta + 1; - p = apr_pcalloc(ctx->pool, plen); + p = apr_palloc(ctx->pool, plen); memcpy(p, ctx->s, start); memcpy(p + start, ns, nlen); strcpy(p + start + nlen, ctx->s + end); ctx->s = p; - ctx->slen = (int)strlen(p); + ctx->slen = plen - 1; /* (int)strlen(p) */ if (ctx->i >= end) { ctx->i += delta; } @@ -931,7 +951,7 @@ static void map_link(link_ctx *ctx) { if (ctx->link_start < ctx->link_end) { char buffer[HUGE_STRING_LEN]; - int need_len, link_len, buffer_len, prepend_p_server; + size_t need_len, link_len, buffer_len, prepend_p_server; const char *mapped; buffer[0] = '\0'; diff --git a/modules/http2/h2_proxy_util.h b/modules/http2/h2_proxy_util.h index a88fb7e..202363d 100644 --- a/modules/http2/h2_proxy_util.h +++ b/modules/http2/h2_proxy_util.h @@ -168,6 +168,8 @@ typedef struct h2_proxy_ngheader { h2_proxy_ngheader *h2_proxy_util_nghd_make_req(apr_pool_t *p, const struct h2_proxy_request *req); +h2_proxy_ngheader *h2_proxy_util_nghd_make(apr_pool_t *p, apr_table_t *headers); + /******************************************************************************* * h2_proxy_request helpers ******************************************************************************/ @@ -183,11 +185,10 @@ struct h2_proxy_request { apr_time_t request_time; - unsigned int chunked : 1; /* iff requst body needs to be forwarded as chunked */ - unsigned int serialize : 1; /* iff this request is written in HTTP/1.1 serialization */ + int chunked; /* iff request body needs to be forwarded as chunked */ }; -h2_proxy_request *h2_proxy_req_create(int id, apr_pool_t *pool, int serialize); +h2_proxy_request *h2_proxy_req_create(int id, apr_pool_t *pool); apr_status_t h2_proxy_req_make(h2_proxy_request *req, apr_pool_t *pool, const char *method, const char *scheme, const char *authority, const char *path, diff --git a/modules/http2/h2_push.c b/modules/http2/h2_push.c index 9a3b19b..e6a10c5 100644 --- a/modules/http2/h2_push.c +++ b/modules/http2/h2_push.c @@ -23,19 +23,19 @@ #include <apr_time.h> #ifdef H2_OPENSSL -#include <openssl/sha.h> +#include <openssl/evp.h> #endif #include <httpd.h> #include <http_core.h> #include <http_log.h> +#include <http_protocol.h> #include "h2_private.h" -#include "h2_h2.h" +#include "h2_protocol.h" #include "h2_util.h" #include "h2_push.h" #include "h2_request.h" -#include "h2_headers.h" #include "h2_session.h" #include "h2_stream.h" @@ -59,7 +59,7 @@ static const char *policy_str(h2_push_policy policy) typedef struct { const h2_request *req; - int push_policy; + apr_uint32_t push_policy; apr_pool_t *pool; apr_array_header_t *pushes; const char *s; @@ -348,11 +348,10 @@ static int add_push(link_ctx *ctx) } headers = apr_table_make(ctx->pool, 5); apr_table_do(set_push_header, headers, ctx->req->headers, NULL); - req = h2_req_create(0, ctx->pool, method, ctx->req->scheme, - ctx->req->authority, path, headers, - ctx->req->serialize); + req = h2_request_create(0, ctx->pool, method, ctx->req->scheme, + ctx->req->authority, path, headers); /* atm, we do not push on pushes */ - h2_request_end_headers(req, ctx->pool, 1, 0); + h2_request_end_headers(req, ctx->pool, 0); push->req = req; if (has_param(ctx, "critical")) { h2_priority *prio = apr_pcalloc(ctx->pool, sizeof(*prio)); @@ -427,14 +426,23 @@ static void inspect_link(link_ctx *ctx, const char *s, size_t slen) static int head_iter(void *ctx, const char *key, const char *value) { - if (!apr_strnatcasecmp("link", key)) { + if (!ap_cstr_casecmp("link", key)) { inspect_link(ctx, value, strlen(value)); } return 1; } -apr_array_header_t *h2_push_collect(apr_pool_t *p, const h2_request *req, - int push_policy, const h2_headers *res) +#if AP_HAS_RESPONSE_BUCKETS +apr_array_header_t *h2_push_collect(apr_pool_t *p, + const struct h2_request *req, + apr_uint32_t push_policy, + const ap_bucket_response *res) +#else +apr_array_header_t *h2_push_collect(apr_pool_t *p, + const struct h2_request *req, + apr_uint32_t push_policy, + const struct h2_headers *res) +#endif { if (req && push_policy != H2_PUSH_NONE) { /* Collect push candidates from the request/response pair. @@ -464,33 +472,6 @@ apr_array_header_t *h2_push_collect(apr_pool_t *p, const h2_request *req, return NULL; } -/******************************************************************************* - * push diary - * - * - The push diary keeps track of resources already PUSHed via HTTP/2 on this - * connection. It records a hash value from the absolute URL of the resource - * pushed. - * - Lacking openssl, it uses 'apr_hashfunc_default' for the value - * - with openssl, it uses SHA256 to calculate the hash value - * - whatever the method to generate the hash, the diary keeps a maximum of 64 - * bits per hash, limiting the memory consumption to about - * H2PushDiarySize * 8 - * bytes. Entries are sorted by most recently used and oldest entries are - * forgotten first. - * - Clients can initialize/replace the push diary by sending a 'Cache-Digest' - * header. Currently, this is the base64url encoded value of the cache digest - * as specified in https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/ - * This draft can be expected to evolve and the definition of the header - * will be added there and refined. - * - The cache digest header is a Golomb Coded Set of hash values, but it may - * limit the amount of bits per hash value even further. For a good description - * of GCS, read here: - * http://giovanni.bajo.it/post/47119962313/golomb-coded-sets-smaller-than-bloom-filters - * - The means that the push diary might be initialized with hash values of much - * less than 64 bits, leading to more false positives, but smaller digest size. - ******************************************************************************/ - - #define GCSLOG_LEVEL APLOG_TRACE1 typedef struct h2_push_diary_entry { @@ -499,27 +480,32 @@ typedef struct h2_push_diary_entry { #ifdef H2_OPENSSL -static void sha256_update(SHA256_CTX *ctx, const char *s) +static void sha256_update(EVP_MD_CTX *ctx, const char *s) { - SHA256_Update(ctx, s, strlen(s)); + EVP_DigestUpdate(ctx, s, strlen(s)); } static void calc_sha256_hash(h2_push_diary *diary, apr_uint64_t *phash, h2_push *push) { - SHA256_CTX sha256; + EVP_MD_CTX *md; apr_uint64_t val; - unsigned char hash[SHA256_DIGEST_LENGTH]; - int i; - - SHA256_Init(&sha256); - sha256_update(&sha256, push->req->scheme); - sha256_update(&sha256, "://"); - sha256_update(&sha256, push->req->authority); - sha256_update(&sha256, push->req->path); - SHA256_Final(hash, &sha256); + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned len, i; + + md = EVP_MD_CTX_create(); + ap_assert(md != NULL); + + i = EVP_DigestInit_ex(md, EVP_sha256(), NULL); + ap_assert(i == 1); + sha256_update(md, push->req->scheme); + sha256_update(md, "://"); + sha256_update(md, push->req->authority); + sha256_update(md, push->req->path); + EVP_DigestFinal(md, hash, &len); + EVP_MD_CTX_destroy(md); val = 0; - for (i = 0; i != sizeof(val); ++i) + for (i = 0; i != len; ++i) val = val * 256 + hash[i]; *phash = val >> (64 - diary->mask_bits); } @@ -528,13 +514,14 @@ static void calc_sha256_hash(h2_push_diary *diary, apr_uint64_t *phash, h2_push static unsigned int val_apr_hash(const char *str) { - apr_ssize_t len = strlen(str); + apr_ssize_t len = (apr_ssize_t)strlen(str); return apr_hashfunc_default(str, &len); } static void calc_apr_hash(h2_push_diary *diary, apr_uint64_t *phash, h2_push *push) { apr_uint64_t val; + (void)diary; #if APR_UINT64_MAX > UINT_MAX val = ((apr_uint64_t)(val_apr_hash(push->req->scheme)) << 32); val ^= ((apr_uint64_t)(val_apr_hash(push->req->authority)) << 16); @@ -617,38 +604,48 @@ static int h2_push_diary_find(h2_push_diary *diary, apr_uint64_t hash) return -1; } -static h2_push_diary_entry *move_to_last(h2_push_diary *diary, apr_size_t idx) +static void move_to_last(h2_push_diary *diary, apr_size_t idx) { h2_push_diary_entry *entries = (h2_push_diary_entry*)diary->entries->elts; h2_push_diary_entry e; - apr_size_t lastidx = diary->entries->nelts-1; + apr_size_t lastidx; + /* Move an existing entry to the last place */ + if (diary->entries->nelts <= 0) + return; + /* move entry[idx] to the end */ + lastidx = diary->entries->nelts - 1; if (idx < lastidx) { e = entries[idx]; - memmove(entries+idx, entries+idx+1, sizeof(e) * (lastidx - idx)); + memmove(entries+idx, entries+idx+1, sizeof(h2_push_diary_entry) * (lastidx - idx)); entries[lastidx] = e; } - return &entries[lastidx]; } -static void h2_push_diary_append(h2_push_diary *diary, h2_push_diary_entry *e) +static void remove_first(h2_push_diary *diary) { - h2_push_diary_entry *ne; + h2_push_diary_entry *entries = (h2_push_diary_entry*)diary->entries->elts; + int lastidx; - if (diary->entries->nelts < diary->N) { - /* append a new diary entry at the end */ - APR_ARRAY_PUSH(diary->entries, h2_push_diary_entry) = *e; - ne = &APR_ARRAY_IDX(diary->entries, diary->entries->nelts-1, h2_push_diary_entry); + /* move remaining entries to index 0 */ + lastidx = diary->entries->nelts - 1; + if (lastidx > 0) { + --diary->entries->nelts; + memmove(entries, entries+1, sizeof(h2_push_diary_entry) * diary->entries->nelts); } - else { - /* replace content with new digest. keeps memory usage constant once diary is full */ - ne = move_to_last(diary, 0); - *ne = *e; +} + +static void h2_push_diary_append(h2_push_diary *diary, h2_push_diary_entry *e) +{ + while (diary->entries->nelts >= diary->N) { + remove_first(diary); } + /* append a new diary entry at the end */ + APR_ARRAY_PUSH(diary->entries, h2_push_diary_entry) = *e; /* Intentional no APLOGNO */ ap_log_perror(APLOG_MARK, GCSLOG_LEVEL, 0, diary->entries->pool, - "push_diary_append: %"APR_UINT64_T_HEX_FMT, ne->hash); + "push_diary_append: %"APR_UINT64_T_HEX_FMT, e->hash); } apr_array_header_t *h2_push_diary_update(h2_session *session, apr_array_header_t *pushes) @@ -668,13 +665,13 @@ apr_array_header_t *h2_push_diary_update(h2_session *session, apr_array_header_t idx = h2_push_diary_find(session->push_diary, e.hash); if (idx >= 0) { /* Intentional no APLOGNO */ - ap_log_cerror(APLOG_MARK, GCSLOG_LEVEL, 0, session->c, + ap_log_cerror(APLOG_MARK, GCSLOG_LEVEL, 0, session->c1, "push_diary_update: already there PUSH %s", push->req->path); - move_to_last(session->push_diary, idx); + move_to_last(session->push_diary, (apr_size_t)idx); } else { /* Intentional no APLOGNO */ - ap_log_cerror(APLOG_MARK, GCSLOG_LEVEL, 0, session->c, + ap_log_cerror(APLOG_MARK, GCSLOG_LEVEL, 0, session->c1, "push_diary_update: adding PUSH %s", push->req->path); if (!npushes) { npushes = apr_array_make(pushes->pool, 5, sizeof(h2_push_diary_entry*)); @@ -687,34 +684,22 @@ apr_array_header_t *h2_push_diary_update(h2_session *session, apr_array_header_t return npushes; } -apr_array_header_t *h2_push_collect_update(h2_stream *stream, - const struct h2_request *req, +#if AP_HAS_RESPONSE_BUCKETS +apr_array_header_t *h2_push_collect_update(struct h2_stream *stream, + const struct h2_request *req, + const ap_bucket_response *res) +#else +apr_array_header_t *h2_push_collect_update(struct h2_stream *stream, + const struct h2_request *req, const struct h2_headers *res) +#endif { - h2_session *session = stream->session; - const char *cache_digest = apr_table_get(req->headers, "Cache-Digest"); apr_array_header_t *pushes; - apr_status_t status; - if (cache_digest && session->push_diary) { - status = h2_push_diary_digest64_set(session->push_diary, req->authority, - cache_digest, stream->pool); - if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, - H2_SSSN_LOG(APLOGNO(03057), session, - "push diary set from Cache-Digest: %s"), cache_digest); - } - } pushes = h2_push_collect(stream->pool, req, stream->push_policy, res); return h2_push_diary_update(stream->session, pushes); } -static apr_int32_t h2_log2inv(unsigned char log2) -{ - return log2? (1 << log2) : 1; -} - - typedef struct { h2_push_diary *diary; unsigned char log2p; @@ -822,23 +807,18 @@ apr_status_t h2_push_diary_digest_get(h2_push_diary *diary, apr_pool_t *pool, int maxP, const char *authority, const char **pdata, apr_size_t *plen) { - int nelts, N, i; + int nelts, N; unsigned char log2n, log2pmax; gset_encoder encoder; apr_uint64_t *hashes; - apr_size_t hash_count; + apr_size_t hash_count, i; nelts = diary->entries->nelts; - - if (nelts > APR_UINT32_MAX) { - /* should not happen */ - return APR_ENOTIMPL; - } N = ceil_power_of_2(nelts); log2n = h2_log2(N); /* Now log2p is the max number of relevant bits, so that - * log2p + log2n == mask_bits. We can uise a lower log2p + * log2p + log2n == mask_bits. We can use a lower log2p * and have a shorter set encoding... */ log2pmax = h2_log2(ceil_power_of_2(maxP)); @@ -895,166 +875,3 @@ apr_status_t h2_push_diary_digest_get(h2_push_diary *diary, apr_pool_t *pool, return APR_SUCCESS; } -typedef struct { - h2_push_diary *diary; - apr_pool_t *pool; - unsigned char log2p; - const unsigned char *data; - apr_size_t datalen; - apr_size_t offset; - unsigned int bit; - apr_uint64_t last_val; -} gset_decoder; - -static int gset_decode_next_bit(gset_decoder *decoder) -{ - if (++decoder->bit >= 8) { - if (++decoder->offset >= decoder->datalen) { - return -1; - } - decoder->bit = 0; - } - return (decoder->data[decoder->offset] & cbit_mask[decoder->bit])? 1 : 0; -} - -static apr_status_t gset_decode_next(gset_decoder *decoder, apr_uint64_t *phash) -{ - apr_uint64_t flex = 0, fixed = 0, delta; - int i; - - /* read 1 bits until we encounter 0, then read log2n(diary-P) bits. - * On a malformed bit-string, this will not fail, but produce results - * which are pbly too large. Luckily, the diary will modulo the hash. - */ - while (1) { - int bit = gset_decode_next_bit(decoder); - if (bit == -1) { - return APR_EINVAL; - } - if (!bit) { - break; - } - ++flex; - } - - for (i = 0; i < decoder->log2p; ++i) { - int bit = gset_decode_next_bit(decoder); - if (bit == -1) { - return APR_EINVAL; - } - fixed = (fixed << 1) | bit; - } - - delta = (flex << decoder->log2p) | fixed; - *phash = delta + decoder->last_val; - decoder->last_val = *phash; - - /* Intentional no APLOGNO */ - ap_log_perror(APLOG_MARK, GCSLOG_LEVEL, 0, decoder->pool, - "h2_push_diary_digest_dec: val=%"APR_UINT64_T_HEX_FMT", delta=%" - APR_UINT64_T_HEX_FMT", flex=%d, fixed=%"APR_UINT64_T_HEX_FMT, - *phash, delta, (int)flex, fixed); - - return APR_SUCCESS; -} - -/** - * Initialize the push diary by a cache digest as described in - * https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/ - * . - * @param diary the diary to set the digest into - * @param data the binary cache digest - * @param len the length of the cache digest - * @return APR_EINVAL if digest was not successfully parsed - */ -apr_status_t h2_push_diary_digest_set(h2_push_diary *diary, const char *authority, - const char *data, apr_size_t len) -{ - gset_decoder decoder; - unsigned char log2n, log2p; - int N, i; - apr_pool_t *pool = diary->entries->pool; - h2_push_diary_entry e; - apr_status_t status = APR_SUCCESS; - - if (len < 2) { - /* at least this should be there */ - return APR_EINVAL; - } - log2n = data[0]; - log2p = data[1]; - diary->mask_bits = log2n + log2p; - if (diary->mask_bits > 64) { - /* cannot handle */ - return APR_ENOTIMPL; - } - - /* whatever is in the digest, it replaces the diary entries */ - apr_array_clear(diary->entries); - if (!authority || !strcmp("*", authority)) { - diary->authority = NULL; - } - else if (!diary->authority || strcmp(diary->authority, authority)) { - diary->authority = apr_pstrdup(diary->entries->pool, authority); - } - - N = h2_log2inv(log2n + log2p); - - decoder.diary = diary; - decoder.pool = pool; - decoder.log2p = log2p; - decoder.data = (const unsigned char*)data; - decoder.datalen = len; - decoder.offset = 1; - decoder.bit = 8; - decoder.last_val = 0; - - diary->N = N; - /* Determine effective N we use for storage */ - if (!N) { - /* a totally empty cache digest. someone tells us that she has no - * entries in the cache at all. Use our own preferences for N+mask - */ - diary->N = diary->NMax; - return APR_SUCCESS; - } - else if (N > diary->NMax) { - /* Store not more than diary is configured to hold. We open us up - * to DOS attacks otherwise. */ - diary->N = diary->NMax; - } - - /* Intentional no APLOGNO */ - ap_log_perror(APLOG_MARK, GCSLOG_LEVEL, 0, pool, - "h2_push_diary_digest_set: N=%d, log2n=%d, " - "diary->mask_bits=%d, dec.log2p=%d", - (int)diary->N, (int)log2n, diary->mask_bits, - (int)decoder.log2p); - - for (i = 0; i < diary->N; ++i) { - if (gset_decode_next(&decoder, &e.hash) != APR_SUCCESS) { - /* the data may have less than N values */ - break; - } - h2_push_diary_append(diary, &e); - } - - /* Intentional no APLOGNO */ - ap_log_perror(APLOG_MARK, GCSLOG_LEVEL, 0, pool, - "h2_push_diary_digest_set: diary now with %d entries, mask_bits=%d", - (int)diary->entries->nelts, diary->mask_bits); - return status; -} - -apr_status_t h2_push_diary_digest64_set(h2_push_diary *diary, const char *authority, - const char *data64url, apr_pool_t *pool) -{ - const char *data; - apr_size_t len = h2_util_base64url_decode(&data, data64url, pool); - /* Intentional no APLOGNO */ - ap_log_perror(APLOG_MARK, GCSLOG_LEVEL, 0, pool, - "h2_push_diary_digest64_set: digest=%s, dlen=%d", - data64url, (int)len); - return h2_push_diary_digest_set(diary, authority, data, len); -} - diff --git a/modules/http2/h2_push.h b/modules/http2/h2_push.h index bc24e68..947b73b 100644 --- a/modules/http2/h2_push.h +++ b/modules/http2/h2_push.h @@ -17,10 +17,12 @@ #ifndef __mod_h2__h2_push__ #define __mod_h2__h2_push__ +#include <http_protocol.h> + #include "h2.h" +#include "h2_headers.h" struct h2_request; -struct h2_headers; struct h2_ngheader; struct h2_session; struct h2_stream; @@ -35,6 +37,44 @@ typedef enum { H2_PUSH_DIGEST_SHA256 } h2_push_digest_type; +/******************************************************************************* + * push diary + * + * - The push diary keeps track of resources already PUSHed via HTTP/2 on this + * connection. It records a hash value from the absolute URL of the resource + * pushed. + * - Lacking openssl, + * - with openssl, it uses SHA256 to calculate the hash value, otherwise it + * falls back to apr_hashfunc_default() + * - whatever the method to generate the hash, the diary keeps a maximum of 64 + * bits per hash, limiting the memory consumption to about + * H2PushDiarySize * 8 + * bytes. Entries are sorted by most recently used and oldest entries are + * forgotten first. + * - While useful by itself to avoid duplicated PUSHes on the same connection, + * the original idea was that clients provided a 'Cache-Digest' header with + * the values of *their own* cached resources. This was described in + * <https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/> + * and some subsequent revisions that tweaked values but kept the overall idea. + * - The draft was abandoned by the IETF http-wg, as support from major clients, + * e.g. browsers, was lacking for various reasons. + * - For these reasons, mod_h2 abandoned its support for client supplied values + * but keeps the diary. It seems to provide value for applications using PUSH, + * is configurable in size and defaults to a very moderate amount of memory + * used. + * - The cache digest header is a Golomb Coded Set of hash values, but it may + * limit the amount of bits per hash value even further. For a good description + * of GCS, read here: + * <http://giovanni.bajo.it/post/47119962313/golomb-coded-sets-smaller-than-bloom-filters> + ******************************************************************************/ + + +/* + * The push diary is based on the abandoned draft + * <https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/> + * that describes how to use golomb filters. + */ + typedef struct h2_push_diary h2_push_diary; typedef void h2_push_digest_calc(h2_push_diary *diary, apr_uint64_t *phash, h2_push *push); @@ -59,14 +99,21 @@ struct h2_push_diary { * @param res the response from the server * @return array of h2_push addresses or NULL */ -apr_array_header_t *h2_push_collect(apr_pool_t *p, - const struct h2_request *req, - int push_policy, +#if AP_HAS_RESPONSE_BUCKETS +apr_array_header_t *h2_push_collect(apr_pool_t *p, + const struct h2_request *req, + apr_uint32_t push_policy, + const ap_bucket_response *res); +#else +apr_array_header_t *h2_push_collect(apr_pool_t *p, + const struct h2_request *req, + apr_uint32_t push_policy, const struct h2_headers *res); +#endif /** * Create a new push diary for the given maximum number of entries. - * + * * @param p the pool to use * @param N the max number of entries, rounded up to 2^x * @return the created diary, might be NULL of max_entries is 0 @@ -83,14 +130,21 @@ apr_array_header_t *h2_push_diary_update(struct h2_session *session, apr_array_h * Collect pushes for the given request/response pair, enter them into the * diary and return those pushes newly entered. */ -apr_array_header_t *h2_push_collect_update(struct h2_stream *stream, - const struct h2_request *req, +#if AP_HAS_RESPONSE_BUCKETS +apr_array_header_t *h2_push_collect_update(struct h2_stream *stream, + const struct h2_request *req, + const ap_bucket_response *res); +#else +apr_array_header_t *h2_push_collect_update(struct h2_stream *stream, + const struct h2_request *req, const struct h2_headers *res); +#endif + /** * Get a cache digest as described in * https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/ * from the contents of the push diary. - * + * * @param diary the diary to calculdate the digest from * @param p the pool to use * @param authority the authority to get the data for, use NULL/"*" for all @@ -101,20 +155,4 @@ apr_status_t h2_push_diary_digest_get(h2_push_diary *diary, apr_pool_t *p, int maxP, const char *authority, const char **pdata, apr_size_t *plen); -/** - * Initialize the push diary by a cache digest as described in - * https://datatracker.ietf.org/doc/draft-kazuho-h2-cache-digest/ - * . - * @param diary the diary to set the digest into - * @param authority the authority to set the data for - * @param data the binary cache digest - * @param len the length of the cache digest - * @return APR_EINVAL if digest was not successfully parsed - */ -apr_status_t h2_push_diary_digest_set(h2_push_diary *diary, const char *authority, - const char *data, apr_size_t len); - -apr_status_t h2_push_diary_digest64_set(h2_push_diary *diary, const char *authority, - const char *data64url, apr_pool_t *pool); - #endif /* defined(__mod_h2__h2_push__) */ diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c index 8899c4f..2713947 100644 --- a/modules/http2/h2_request.c +++ b/modules/http2/h2_request.c @@ -16,7 +16,12 @@ #include <assert.h> -#include <apr_strings.h> +#include "apr.h" +#include "apr_strings.h" +#include "apr_lib.h" +#include "apr_strmatch.h" + +#include <ap_mmn.h> #include <httpd.h> #include <http_core.h> @@ -24,6 +29,7 @@ #include <http_protocol.h> #include <http_request.h> #include <http_log.h> +#include <http_ssl.h> #include <http_vhost.h> #include <util_filter.h> #include <ap_mpm.h> @@ -32,11 +38,28 @@ #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" +h2_request *h2_request_create(int id, apr_pool_t *pool, const char *method, + const char *scheme, const char *authority, + const char *path, apr_table_t *header) +{ + h2_request *req = apr_pcalloc(pool, sizeof(h2_request)); + + req->method = method; + req->scheme = scheme; + req->authority = authority; + req->path = path; + req->headers = header? header : apr_table_make(pool, 10); + req->request_time = apr_time_now(); + + return req; +} + typedef struct { apr_table_t *headers; apr_pool_t *pool; @@ -46,9 +69,9 @@ typedef struct { static int set_h1_header(void *ctx, const char *key, const char *value) { h1_ctx *x = ctx; - x->status = h2_req_add_header(x->headers, x->pool, key, strlen(key), - value, strlen(value)); - return (x->status == APR_SUCCESS)? 1 : 0; + int was_added; + h2_req_add_header(x->headers, x->pool, key, strlen(key), value, strlen(value), 0, &was_added); + return 1; } apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, @@ -68,54 +91,67 @@ apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, return APR_EINVAL; } - if (!ap_strchr_c(authority, ':') && r->server && r->server->port) { - apr_port_t defport = apr_uri_port_of_scheme(scheme); - if (defport != r->server->port) { - /* port info missing and port is not default for scheme: append */ - authority = apr_psprintf(pool, "%s:%d", authority, - (int)r->server->port); + /* The authority we carry in h2_request is the 'authority' part of + * the URL for the request. r->hostname has stripped any port info that + * might have been present. Do we need to add it? + */ + if (!ap_strchr_c(authority, ':')) { + if (r->parsed_uri.port_str) { + /* Yes, it was there, add it again. */ + authority = apr_pstrcat(pool, authority, ":", r->parsed_uri.port_str, NULL); + } + else if (!r->parsed_uri.hostname && r->server && r->server->port) { + /* If there was no hostname in the parsed URL, the URL was relative. + * In that case, we restore port from our server->port, if it + * is known and not the default port for the scheme. */ + apr_port_t defport = apr_uri_port_of_scheme(scheme); + if (defport != r->server->port) { + /* port info missing and port is not default for scheme: append */ + authority = apr_psprintf(pool, "%s:%d", authority, + (int)r->server->port); + } } } - + req = apr_pcalloc(pool, sizeof(*req)); - req->method = apr_pstrdup(pool, r->method); - req->scheme = scheme; - req->authority = authority; - req->path = path; - req->headers = apr_table_make(pool, 10); - if (r->server) { - req->serialize = h2_config_geti(h2_config_sget(r->server), - H2_CONF_SER_HEADERS); - } + req->method = apr_pstrdup(pool, r->method); + req->scheme = scheme; + req->authority = authority; + req->path = path; + req->headers = apr_table_make(pool, 10); + req->http_status = H2_HTTP_STATUS_UNSET; + req->request_time = apr_time_now(); x.pool = pool; x.headers = req->headers; x.status = APR_SUCCESS; apr_table_do(set_h1_header, &x, r->headers_in, NULL); - + *preq = req; return x.status; } -apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, +apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, const char *name, size_t nlen, - const char *value, size_t vlen) + const char *value, size_t vlen, + size_t max_field_len, int *pwas_added) { apr_status_t status = APR_SUCCESS; - + + *pwas_added = 0; if (nlen <= 0) { return status; } - + if (name[0] == ':') { /* pseudo header, see ch. 8.1.2.3, always should come first */ if (!apr_is_empty_table(req->headers)) { ap_log_perror(APLOG_MARK, APLOG_ERR, 0, pool, - APLOGNO(02917) + APLOGNO(02917) "h2_request: pseudo header after request start"); return APR_EGENERAL; } - + if (H2_HEADER_METHOD_LEN == nlen && !strncmp(H2_HEADER_METHOD, name, nlen)) { req->method = apr_pstrndup(pool, value, vlen); @@ -132,32 +168,36 @@ apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, && !strncmp(H2_HEADER_AUTH, name, nlen)) { req->authority = apr_pstrndup(pool, value, vlen); } + else if (H2_HEADER_PROTO_LEN == nlen + && !strncmp(H2_HEADER_PROTO, name, nlen)) { + req->protocol = apr_pstrndup(pool, value, vlen); + } else { char buffer[32]; memset(buffer, 0, 32); strncpy(buffer, name, (nlen > 31)? 31 : nlen); ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, pool, - APLOGNO(02954) + APLOGNO(02954) "h2_request: ignoring unknown pseudo header %s", buffer); } } else { - /* non-pseudo header, append to work bucket of stream */ - status = h2_req_add_header(req->headers, pool, name, nlen, value, vlen); + /* non-pseudo header, add to table */ + status = h2_req_add_header(req->headers, pool, name, nlen, value, vlen, + max_field_len, pwas_added); } - + return status; } -apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos, size_t raw_bytes) +apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, + size_t raw_bytes) { - const char *s; - - /* rfc7540, ch. 8.1.2.3: - * - if we have :authority, it overrides any Host header - * - :authority MUST be ommited when converting h1->h2, so we - * might get a stream without, but then Host needs to be there */ + /* rfc7540, ch. 8.1.2.3: without :authority, Host: must be there */ + if (req->authority && !strlen(req->authority)) { + req->authority = NULL; + } if (!req->authority) { const char *host = apr_table_get(req->headers, "Host"); if (!host) { @@ -168,30 +208,8 @@ apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos, else { apr_table_setn(req->headers, "Host", req->authority); } - - s = apr_table_get(req->headers, "Content-Length"); - if (!s) { - /* HTTP/2 does not need a Content-Length for framing, but our - * internal request processing is used to HTTP/1.1, so we - * need to either add a Content-Length or a Transfer-Encoding - * if any content can be expected. */ - if (!eos) { - /* We have not seen a content-length and have no eos, - * simulate a chunked encoding for our HTTP/1.1 infrastructure, - * in case we have "H2SerializeHeaders on" here - */ - req->chunked = 1; - apr_table_mergen(req->headers, "Transfer-Encoding", "chunked"); - } - else if (apr_table_get(req->headers, "Content-Type")) { - /* If we have a content-type, but already seen eos, no more - * data will come. Signal a zero content length explicitly. - */ - apr_table_setn(req->headers, "Content-Length", "0"); - } - } req->raw_bytes += raw_bytes; - + return APR_SUCCESS; } @@ -202,17 +220,16 @@ h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src) dst->scheme = apr_pstrdup(p, src->scheme); dst->authority = apr_pstrdup(p, src->authority); dst->path = apr_pstrdup(p, src->path); + dst->protocol = apr_pstrdup(p, src->protocol); dst->headers = apr_table_clone(p, src->headers); return dst; } -request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) +#if !AP_MODULE_MAGIC_AT_LEAST(20120211, 106) +static request_rec *my_ap_create_request(conn_rec *c) { - int access_status = HTTP_OK; - const char *rpath; apr_pool_t *p; request_rec *r; - const char *s; apr_pool_create(&p, c->pool); apr_pool_tag(p, "request"); @@ -221,86 +238,294 @@ request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) r->pool = p; r->connection = c; r->server = c->base_server; - + r->user = NULL; r->ap_auth_type = NULL; - + r->allowed_methods = ap_make_method_list(p, 2); - - r->headers_in = apr_table_clone(r->pool, req->headers); + + r->headers_in = apr_table_make(r->pool, 5); r->trailers_in = apr_table_make(r->pool, 5); r->subprocess_env = apr_table_make(r->pool, 25); r->headers_out = apr_table_make(r->pool, 12); r->err_headers_out = apr_table_make(r->pool, 5); r->trailers_out = apr_table_make(r->pool, 5); r->notes = apr_table_make(r->pool, 5); - + r->request_config = ap_create_request_config(r->pool); /* Must be set before we run create request hook */ - + r->proto_output_filters = c->output_filters; r->output_filters = r->proto_output_filters; r->proto_input_filters = c->input_filters; r->input_filters = r->proto_input_filters; ap_run_create_request(r); r->per_dir_config = r->server->lookup_defaults; - + r->sent_bodyct = 0; /* bytect isn't for body */ - + r->read_length = 0; r->read_body = REQUEST_NO_BODY; - + r->status = HTTP_OK; /* Until further notice */ r->header_only = 0; r->the_request = NULL; - + /* Begin by presuming any module can make its own path_info assumptions, * until some module interjects and changes the value. */ r->used_path_info = AP_REQ_DEFAULT_PATH_INFO; - + r->useragent_addr = c->client_addr; r->useragent_ip = c->client_ip; - + return r; +} +#endif + +#if AP_HAS_RESPONSE_BUCKETS +apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r) +{ + conn_rec *c = r->connection; + apr_table_t *headers = apr_table_clone(r->pool, req->headers); + const char *uri = req->path; + + AP_DEBUG_ASSERT(req->method); + AP_DEBUG_ASSERT(req->authority); + if (!ap_cstr_casecmp("CONNECT", req->method)) { + uri = req->authority; + } + 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 : ""); + } + + return ap_bucket_request_create(req->method, uri, "HTTP/2.0", headers, + r->pool, c->bucket_alloc); +} +#endif + +static void assign_headers(request_rec *r, const h2_request *req, + int no_body, int is_connect) +{ + const char *cl; + + r->headers_in = apr_table_clone(r->pool, req->headers); + + if (req->authority && !is_connect) { + /* for internal handling, we have to simulate that :authority + * came in as Host:, RFC 9113 ch. says that mismatches between + * :authority and Host: SHOULD be rejected as malformed. However, + * we are more lenient and just replace any Host: if we have + * an :authority. + */ + const char *orig_host = apr_table_get(req->headers, "Host"); + if (orig_host && strcmp(req->authority, orig_host)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(10401) + "overwriting 'Host: %s' with :authority: %s'", + orig_host, req->authority); + apr_table_setn(r->subprocess_env, "H2_ORIGINAL_HOST", orig_host); + } + apr_table_setn(r->headers_in, "Host", req->authority); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "set 'Host: %s' from :authority", req->authority); + } + + /* Unless we open a byte stream via CONNECT, apply content-length guards. */ + if (!is_connect) { + cl = apr_table_get(req->headers, "Content-Length"); + if (no_body) { + if (!cl && apr_table_get(req->headers, "Content-Type")) { + /* If we have a content-type, but already seen eos, no more + * data will come. Signal a zero content length explicitly. + */ + apr_table_setn(req->headers, "Content-Length", "0"); + } + } +#if !AP_HAS_RESPONSE_BUCKETS + else if (!cl) { + /* there may be a body and we have internal HTTP/1.1 processing. + * If the Content-Length is unspecified, we MUST simulate + * chunked Transfer-Encoding. + * + * HTTP/2 does not need a Content-Length for framing. Ideally + * all clients set the EOS flag on the header frame if they + * do not intent to send a body. However, forwarding proxies + * might just no know at the time and send an empty DATA + * frame with EOS much later. + */ + apr_table_mergen(r->headers_in, "Transfer-Encoding", "chunked"); + } +#endif /* else AP_HAS_RESPONSE_BUCKETS */ + } +} + +request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c, + int no_body) +{ + int access_status = HTTP_OK; + int is_connect = !ap_cstr_casecmp("CONNECT", req->method); + +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 106) + request_rec *r = ap_create_request(c); +#else + request_rec *r = my_ap_create_request(c); +#endif + +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 107) + assign_headers(r, req, no_body, is_connect); ap_run_pre_read_request(r, c); - + /* Time to populate r with the data we have. */ r->request_time = req->request_time; - r->method = req->method; - /* Provide quick information about the request method as soon as known */ - r->method_number = ap_method_number_of(r->method); - if (r->method_number == M_GET && r->method[0] == 'H') { - r->header_only = 1; + AP_DEBUG_ASSERT(req->authority); + if (req->http_status != H2_HTTP_STATUS_UNSET) { + access_status = req->http_status; + goto die; + } + else if (is_connect) { + /* CONNECT MUST NOT have scheme or path */ + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->authority); + if (req->scheme) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458) + "':scheme: %s' header present in CONNECT request", + req->scheme); + access_status = HTTP_BAD_REQUEST; + goto die; + } + else if (req->path) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459) + "':path: %s' header present in CONNECT request", + req->path); + access_status = HTTP_BAD_REQUEST; + goto die; + } + } + else if (req->protocol) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10470) + "':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(10468) + "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(10469) + "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 + * than what we have on this connection. Make an absolute URI. */ + r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0", + req->method, req->scheme, req->authority, + req->path ? req->path : ""); + } + else if (req->path) { + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->path); + } + else { + /* We should only come here on a request that is errored already. + * create a request line that passes parsing, we'll die anyway. + */ + AP_DEBUG_ASSERT(req->http_status != H2_HTTP_STATUS_UNSET); + r->the_request = apr_psprintf(r->pool, "%s / HTTP/2.0", req->method); } - rpath = (req->path ? req->path : ""); - ap_parse_uri(r, rpath); - r->protocol = (char*)"HTTP/2.0"; - r->proto_num = HTTP_VERSION(2, 0); - - r->the_request = apr_psprintf(r->pool, "%s %s %s", - r->method, rpath, r->protocol); - - /* update what we think the virtual host is based on the headers we've - * now read. may update status. - * Leave r->hostname empty, vhost will parse if form our Host: header, - * otherwise we get complains about port numbers. + /* Start with r->hostname = NULL, ap_check_request_header() will get it + * form Host: header, otherwise we get complains about port numbers. */ r->hostname = NULL; - ap_update_vhost_from_headers(r); - - /* we may have switched to another server */ - r->per_dir_config = r->server->lookup_defaults; - - s = apr_table_get(r->headers_in, "Expect"); - if (s && s[0]) { - if (ap_cstr_casecmp(s, "100-continue") == 0) { - r->expecting_100 = 1; + + /* Validate HTTP/1 request and select vhost. */ + if (!ap_parse_request_line(r) || !ap_check_request_header(r)) { + /* we may have switched to another server still */ + r->per_dir_config = r->server->lookup_defaults; + if (req->http_status != H2_HTTP_STATUS_UNSET) { + access_status = req->http_status; + /* Be safe and close the connection */ + c->keepalive = AP_CONN_CLOSE; } else { - r->status = HTTP_EXPECTATION_FAILED; - ap_send_error_response(r, 0); + access_status = r->status; + } + r->status = HTTP_OK; + goto die; + } +#else + { + const char *s; + + assign_headers(r, req, no_body, is_connect); + ap_run_pre_read_request(r, c); + + /* Time to populate r with the data we have. */ + r->request_time = req->request_time; + r->method = apr_pstrdup(r->pool, req->method); + /* Provide quick information about the request method as soon as known */ + r->method_number = ap_method_number_of(r->method); + if (r->method_number == M_GET && r->method[0] == 'H') { + r->header_only = 1; } + ap_parse_uri(r, req->path ? req->path : ""); + r->protocol = (char*)"HTTP/2.0"; + r->proto_num = HTTP_VERSION(2, 0); + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + r->method, req->path ? req->path : ""); + + /* Start with r->hostname = NULL, ap_check_request_header() will get it + * form Host: header, otherwise we get complains about port numbers. + */ + r->hostname = NULL; + ap_update_vhost_from_headers(r); + + /* we may have switched to another server */ + r->per_dir_config = r->server->lookup_defaults; + + s = apr_table_get(r->headers_in, "Expect"); + if (s && s[0]) { + if (ap_cstr_casecmp(s, "100-continue") == 0) { + r->expecting_100 = 1; + } + else { + r->status = HTTP_EXPECTATION_FAILED; + access_status = r->status; + goto die; + } + } + } +#endif + + /* we may have switched to another server */ + r->per_dir_config = r->server->lookup_defaults; + + if (req->http_status != H2_HTTP_STATUS_UNSET) { + access_status = req->http_status; + r->status = HTTP_OK; + /* Be safe and close the connection */ + c->keepalive = AP_CONN_CLOSE; + goto die; } /* @@ -311,29 +536,58 @@ request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) */ ap_add_input_filter_handle(ap_http_input_filter_handle, NULL, r, r->connection); - - if (access_status != HTTP_OK - || (access_status = ap_run_post_read_request(r))) { + + if ((access_status = ap_post_read_request(r))) { /* Request check post hooks failed. An example of this would be a * request for a vhost where h2 is disabled --> 421. */ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03367) "h2_request: access_status=%d, request_create failed", access_status); - ap_die(access_status, r); - ap_update_child_status(c->sbh, SERVER_BUSY_LOG, r); - ap_run_log_transaction(r); - r = NULL; - goto traceout; + goto die; } - AP_READ_REQUEST_SUCCESS((uintptr_t)r, (char *)r->method, - (char *)r->uri, (char *)r->server->defn_name, + AP_READ_REQUEST_SUCCESS((uintptr_t)r, (char *)r->method, + (char *)r->uri, (char *)r->server->defn_name, r->status); return r; -traceout: - AP_READ_REQUEST_FAILURE((uintptr_t)r); - return r; -} +die: + if (!r->method) { + /* if we fail early, `r` is not properly initialized for error + * processing which accesses fields in message generation. + * Make a best effort. */ + if (!r->the_request) { + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->path); + } + ap_parse_request_line(r); + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "ap_die(%d) for %s", access_status, r->the_request); + ap_die(access_status, r); + /* ap_die() sent the response through the output filters, we must now + * end the request with an EOR bucket for stream/pipeline accounting. + */ + { + apr_bucket_brigade *eor_bb; +#if AP_MODULE_MAGIC_AT_LEAST(20180905, 1) + eor_bb = ap_acquire_brigade(c); + APR_BRIGADE_INSERT_TAIL(eor_bb, + ap_bucket_eor_create(c->bucket_alloc, r)); + ap_pass_brigade(c->output_filters, eor_bb); + ap_release_brigade(c, eor_bb); +#else + eor_bb = apr_brigade_create(c->pool, c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(eor_bb, + ap_bucket_eor_create(c->bucket_alloc, r)); + ap_pass_brigade(c->output_filters, eor_bb); + apr_brigade_destroy(eor_bb); +#endif + } + + r = NULL; + AP_READ_REQUEST_FAILURE((uintptr_t)r); + return NULL; +} diff --git a/modules/http2/h2_request.h b/modules/http2/h2_request.h index 48aee09..7e20b69 100644 --- a/modules/http2/h2_request.h +++ b/modules/http2/h2_request.h @@ -19,18 +19,24 @@ #include "h2.h" -apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, +h2_request *h2_request_create(int id, apr_pool_t *pool, const char *method, + const char *scheme, const char *authority, + const char *path, apr_table_t *header); + +apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, request_rec *r); apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, const char *name, size_t nlen, - const char *value, size_t vlen); + const char *value, size_t vlen, + size_t max_field_len, int *pwas_added); apr_status_t h2_request_add_trailer(h2_request *req, apr_pool_t *pool, const char *name, size_t nlen, const char *value, size_t vlen); -apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos, size_t raw_bytes); +apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, + size_t raw_bytes); h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src); @@ -40,9 +46,14 @@ h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src); * * @param req the h2 request to process * @param conn the connection to process the request on + * @param no_body != 0 iff the request is known to have no body * @return the request_rec representing the request */ -request_rec *h2_request_create_rec(const h2_request *req, conn_rec *conn); +request_rec *h2_create_request_rec(const h2_request *req, conn_rec *conn, + int no_body); +#if AP_HAS_RESPONSE_BUCKETS +apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r); +#endif #endif /* defined(__mod_h2__h2_request__) */ diff --git a/modules/http2/h2_session.c b/modules/http2/h2_session.c index ed96cf0..5724fda 100644 --- a/modules/http2/h2_session.c +++ b/modules/http2/h2_session.c @@ -17,6 +17,7 @@ #include <assert.h> #include <stddef.h> #include <apr_thread_cond.h> +#include <apr_atomic.h> #include <apr_base64.h> #include <apr_strings.h> @@ -26,33 +27,35 @@ #include <http_core.h> #include <http_config.h> #include <http_log.h> +#include <http_protocol.h> #include <scoreboard.h> #include <mpm_common.h> +#if APR_HAVE_UNISTD_H +#include <unistd.h> /* for getpid() */ +#endif + #include "h2_private.h" #include "h2.h" #include "h2_bucket_beam.h" #include "h2_bucket_eos.h" #include "h2_config.h" -#include "h2_ctx.h" -#include "h2_filter.h" -#include "h2_h2.h" +#include "h2_conn_ctx.h" +#include "h2_protocol.h" #include "h2_mplx.h" #include "h2_push.h" #include "h2_request.h" #include "h2_headers.h" #include "h2_stream.h" -#include "h2_task.h" +#include "h2_c2.h" #include "h2_session.h" #include "h2_util.h" #include "h2_version.h" #include "h2_workers.h" -static apr_status_t dispatch_master(h2_session *session); -static apr_status_t h2_session_read(h2_session *session, int block); -static void transit(h2_session *session, const char *action, +static void transit(h2_session *session, const char *action, h2_session_state nstate); static void on_stream_state_enter(void *ctx, h2_stream *stream); @@ -73,23 +76,20 @@ static int h2_session_status_from_apr_status(apr_status_t rv) return NGHTTP2_ERR_PROTO; } -h2_stream *h2_session_stream_get(h2_session *session, int stream_id) +static h2_stream *get_stream(h2_session *session, int stream_id) { return nghttp2_session_get_stream_user_data(session->ngh2, stream_id); } -static void dispatch_event(h2_session *session, h2_session_event_t ev, - int err, const char *msg); - -void h2_session_event(h2_session *session, h2_session_event_t ev, +void h2_session_event(h2_session *session, h2_session_event_t ev, int err, const char *msg) { - dispatch_event(session, ev, err, msg); + h2_session_dispatch_event(session, ev, err, msg); } static int rst_unprocessed_stream(h2_stream *stream, void *ctx) { - int unprocessed = (!h2_stream_was_closed(stream) + int unprocessed = (!h2_stream_is_at_or_past(stream, H2_SS_CLOSED) && (H2_STREAM_CLIENT_INITIATED(stream->id)? (!stream->session->local.accepting && stream->id > stream->session->local.accepted_max) @@ -106,7 +106,7 @@ static int rst_unprocessed_stream(h2_stream *stream, void *ctx) static void cleanup_unprocessed_streams(h2_session *session) { - h2_mplx_stream_do(session->mplx, rst_unprocessed_stream, session); + h2_mplx_c1_streams_do(session->mplx, rst_unprocessed_stream, session); } static h2_stream *h2_session_open_stream(h2_session *session, int stream_id, @@ -127,7 +127,7 @@ static h2_stream *h2_session_open_stream(h2_session *session, int stream_id, } /** - * Determine the importance of streams when scheduling tasks. + * Determine the priority order of streams. * - if both stream depend on the same one, compare weights * - if one stream is closer to the root, prioritize that one * - if both are on the same level, use the weight of their root @@ -187,20 +187,26 @@ static ssize_t send_cb(nghttp2_session *ngh2, int flags, void *userp) { h2_session *session = (h2_session *)userp; - apr_status_t status; + apr_status_t rv; (void)ngh2; (void)flags; - - status = h2_conn_io_write(&session->io, (const char *)data, length); - if (status == APR_SUCCESS) { + + if (h2_c1_io_needs_flush(&session->io)) { + return NGHTTP2_ERR_WOULDBLOCK; + } + + rv = h2_c1_io_add_data(&session->io, (const char *)data, length); + if (APR_SUCCESS == rv) { return length; } - if (APR_STATUS_IS_EAGAIN(status)) { + else if (APR_STATUS_IS_EAGAIN(rv)) { return NGHTTP2_ERR_WOULDBLOCK; } - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, APLOGNO(03062) - "h2_session: send error"); - return h2_session_status_from_apr_status(status); + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, session->c1, + APLOGNO(03062) "h2_session: send error"); + return h2_session_status_from_apr_status(rv); + } } static int on_invalid_frame_recv_cb(nghttp2_session *ngh2, @@ -210,11 +216,11 @@ static int on_invalid_frame_recv_cb(nghttp2_session *ngh2, h2_session *session = (h2_session *)userp; (void)ngh2; - if (APLOGcdebug(session->c)) { + if (APLOGcdebug(session->c1)) { char buffer[256]; h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_SSSN_LOG(APLOGNO(03063), session, "recv invalid FRAME[%s], frames=%ld/%ld (r/s)"), buffer, (long)session->frames_received, @@ -232,15 +238,17 @@ static int on_data_chunk_recv_cb(nghttp2_session *ngh2, uint8_t flags, h2_stream * stream; int rv = 0; - stream = h2_session_stream_get(session, stream_id); + stream = get_stream(session, stream_id); if (stream) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_SSSN_STRM_MSG(session, stream_id, "write %ld bytes of DATA"), + (long)len); status = h2_stream_recv_DATA(stream, flags, data, len); - dispatch_event(session, H2_SESSION_EV_STREAM_CHANGE, 0, "stream data rcvd"); } else { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03064) - "h2_stream(%ld-%d): on_data_chunk for unknown stream", - session->id, (int)stream_id); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, APLOGNO(03064) + H2_SSSN_STRM_MSG(session, stream_id, + "on_data_chunk for unknown stream")); rv = NGHTTP2_ERR_CALLBACK_FAILURE; } @@ -258,13 +266,13 @@ static int on_stream_close_cb(nghttp2_session *ngh2, int32_t stream_id, h2_stream *stream; (void)ngh2; - stream = h2_session_stream_get(session, stream_id); + stream = get_stream(session, stream_id); if (stream) { if (error_code) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_STRM_LOG(APLOGNO(03065), stream, "closing with err=%d %s"), - (int)error_code, h2_h2_err_description(error_code)); + (int)error_code, h2_protocol_err_description(error_code)); h2_stream_rst(stream, error_code); } } @@ -275,16 +283,16 @@ static int on_begin_headers_cb(nghttp2_session *ngh2, const nghttp2_frame *frame, void *userp) { h2_session *session = (h2_session *)userp; - h2_stream *s; + h2_stream *s = NULL; /* We may see HEADERs at the start of a stream or after all DATA * streams to carry trailers. */ (void)ngh2; - s = h2_session_stream_get(session, frame->hd.stream_id); + s = get_stream(session, frame->hd.stream_id); if (s) { /* nop */ } - else { + else if (session->local.accepting) { s = h2_session_open_stream(userp, frame->hd.stream_id, 0); } return s? 0 : NGHTTP2_ERR_START_STREAM_NOT_ALLOWED; @@ -301,17 +309,23 @@ static int on_header_cb(nghttp2_session *ngh2, const nghttp2_frame *frame, apr_status_t status; (void)flags; - stream = h2_session_stream_get(session, frame->hd.stream_id); + stream = get_stream(session, frame->hd.stream_id); if (!stream) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(02920) - "h2_stream(%ld-%d): on_header unknown stream", - session->id, (int)frame->hd.stream_id); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, APLOGNO(02920) + H2_SSSN_STRM_MSG(session, frame->hd.stream_id, + "on_header unknown stream")); return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } status = h2_stream_add_header(stream, (const char *)name, namelen, (const char *)value, valuelen); - if (status != APR_SUCCESS && !h2_stream_is_ready(stream)) { + if (status != APR_SUCCESS && + (!stream->rtmp || + stream->rtmp->http_status == H2_HTTP_STATUS_UNSET || + /* We accept a certain amount of failures in order to reply + * with an informative HTTP error response like 413. But if the + * client is too wrong, we fail the request a RESET of the stream */ + stream->request_headers_failed > 100)) { return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } return 0; @@ -330,15 +344,25 @@ static int on_frame_recv_cb(nghttp2_session *ng2s, h2_stream *stream; apr_status_t rv = APR_SUCCESS; - if (APLOGcdebug(session->c)) { + stream = frame->hd.stream_id? get_stream(session, frame->hd.stream_id) : NULL; + if (APLOGcdebug(session->c1)) { char buffer[256]; - + h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - H2_SSSN_LOG(APLOGNO(03066), session, - "recv FRAME[%s], frames=%ld/%ld (r/s)"), - buffer, (long)session->frames_received, - (long)session->frames_sent); + if (stream) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_STRM_LOG(APLOGNO(10302), stream, + "recv FRAME[%s], frames=%ld/%ld (r/s)"), + buffer, (long)session->frames_received, + (long)session->frames_sent); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_SSSN_LOG(APLOGNO(03066), session, + "recv FRAME[%s], frames=%ld/%ld (r/s)"), + buffer, (long)session->frames_received, + (long)session->frames_sent); + } } ++session->frames_received; @@ -347,16 +371,14 @@ static int on_frame_recv_cb(nghttp2_session *ng2s, /* This can be HEADERS for a new stream, defining the request, * or HEADER may come after DATA at the end of a stream as in * trailers */ - stream = h2_session_stream_get(session, frame->hd.stream_id); if (stream) { rv = h2_stream_recv_frame(stream, NGHTTP2_HEADERS, frame->hd.flags, frame->hd.length + H2_FRAME_HDR_LEN); } break; case NGHTTP2_DATA: - stream = h2_session_stream_get(session, frame->hd.stream_id); if (stream) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_STRM_LOG(APLOGNO(02923), stream, "DATA, len=%ld, flags=%d"), (long)frame->hd.length, frame->hd.flags); @@ -366,35 +388,40 @@ static int on_frame_recv_cb(nghttp2_session *ng2s, break; case NGHTTP2_PRIORITY: session->reprioritize = 1; - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - "h2_stream(%ld-%d): PRIORITY frame " - " weight=%d, dependsOn=%d, exclusive=%d", - session->id, (int)frame->hd.stream_id, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_SSSN_STRM_MSG(session, frame->hd.stream_id, "PRIORITY frame " + " weight=%d, dependsOn=%d, exclusive=%d"), frame->priority.pri_spec.weight, frame->priority.pri_spec.stream_id, frame->priority.pri_spec.exclusive); break; case NGHTTP2_WINDOW_UPDATE: - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - "h2_stream(%ld-%d): WINDOW_UPDATE incr=%d", - session->id, (int)frame->hd.stream_id, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_SSSN_STRM_MSG(session, frame->hd.stream_id, + "WINDOW_UPDATE incr=%d"), frame->window_update.window_size_increment); - if (nghttp2_session_want_write(session->ngh2)) { - dispatch_event(session, H2_SESSION_EV_FRAME_RCVD, 0, "window update"); - } break; case NGHTTP2_RST_STREAM: - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03067) - "h2_stream(%ld-%d): RST_STREAM by client, errror=%d", - session->id, (int)frame->hd.stream_id, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, APLOGNO(03067) + H2_SSSN_STRM_MSG(session, frame->hd.stream_id, + "RST_STREAM by client, error=%d"), (int)frame->rst_stream.error_code); - stream = h2_session_stream_get(session, frame->hd.stream_id); + if (stream) { + rv = h2_stream_recv_frame(stream, NGHTTP2_RST_STREAM, frame->hd.flags, + frame->hd.length + H2_FRAME_HDR_LEN); + } if (stream && stream->initiated_on) { + /* A stream reset on a request we sent it. Normal, when the + * client does not want it. */ ++session->pushes_reset; } else { - ++session->streams_reset; + /* A stream reset on a request it sent us. Could happen in a browser + * when the user navigates away or cancels loading - maybe. */ + h2_mplx_c1_client_rst(session->mplx, frame->hd.stream_id, + stream); } + ++session->streams_reset; break; case NGHTTP2_GOAWAY: if (frame->goaway.error_code == 0 @@ -404,23 +431,21 @@ static int on_frame_recv_cb(nghttp2_session *ng2s, } else { session->remote.accepted_max = frame->goaway.last_stream_id; - dispatch_event(session, H2_SESSION_EV_REMOTE_GOAWAY, + h2_session_dispatch_event(session, H2_SESSION_EV_REMOTE_GOAWAY, frame->goaway.error_code, NULL); } break; case NGHTTP2_SETTINGS: - if (APLOGctrace2(session->c)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - H2_SSSN_MSG(session, "SETTINGS, len=%ld"), (long)frame->hd.length); - } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_SSSN_MSG(session, "SETTINGS, len=%ld"), (long)frame->hd.length); break; default: - if (APLOGctrace2(session->c)) { + if (APLOGctrace2(session->c1)) { char buffer[256]; h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, H2_SSSN_MSG(session, "on_frame_rcv %s"), buffer); } break; @@ -436,7 +461,7 @@ static int on_frame_recv_cb(nghttp2_session *ng2s, * become in serving this connection. This is expressed in increasing "idle_delays". * Eventually, the connection will timeout and we'll close it. */ session->idle_frames = H2MIN(session->idle_frames + 1, session->frames_received); - ap_log_cerror( APLOG_MARK, APLOG_TRACE2, 0, session->c, + ap_log_cerror( APLOG_MARK, APLOG_TRACE2, 0, session->c1, H2_SSSN_MSG(session, "session has %ld idle frames"), (long)session->idle_frames); if (session->idle_frames > 10) { @@ -461,16 +486,6 @@ static int on_frame_recv_cb(nghttp2_session *ng2s, return 0; } -static int h2_session_continue_data(h2_session *session) { - if (h2_mplx_has_master_events(session->mplx)) { - return 0; - } - if (h2_conn_io_needs_flush(&session->io)) { - return 0; - } - return 1; -} - static char immortal_zeros[H2_MAX_PADLEN]; static int on_send_data_cb(nghttp2_session *ngh2, @@ -491,48 +506,42 @@ static int on_send_data_cb(nghttp2_session *ngh2, (void)ngh2; (void)source; - if (!h2_session_continue_data(session)) { - return NGHTTP2_ERR_WOULDBLOCK; - } - - if (frame->data.padlen > H2_MAX_PADLEN) { - return NGHTTP2_ERR_PROTO; - } + ap_assert(frame->data.padlen <= (H2_MAX_PADLEN+1)); padlen = (unsigned char)frame->data.padlen; - stream = h2_session_stream_get(session, stream_id); + stream = get_stream(session, stream_id); if (!stream) { - ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_NOTFOUND, session->c, + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_NOTFOUND, session->c1, APLOGNO(02924) - "h2_stream(%ld-%d): send_data, stream not found", - session->id, (int)stream_id); + H2_SSSN_STRM_MSG(session, stream_id, "send_data, stream not found")); return NGHTTP2_ERR_CALLBACK_FAILURE; } - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, H2_STRM_MSG(stream, "send_data_cb for %ld bytes"), (long)length); - status = h2_conn_io_write(&session->io, (const char *)framehd, 9); + status = h2_c1_io_add_data(&session->io, (const char *)framehd, H2_FRAME_HDR_LEN); if (padlen && status == APR_SUCCESS) { - status = h2_conn_io_write(&session->io, (const char *)&padlen, 1); + --padlen; + status = h2_c1_io_add_data(&session->io, (const char *)&padlen, 1); } if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c1, H2_STRM_MSG(stream, "writing frame header")); return NGHTTP2_ERR_CALLBACK_FAILURE; } status = h2_stream_read_to(stream, session->bbtmp, &len, &eos); if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c1, H2_STRM_MSG(stream, "send_data_cb, reading stream")); apr_brigade_cleanup(session->bbtmp); return NGHTTP2_ERR_CALLBACK_FAILURE; } - else if (len != length) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c, + else if (len != (apr_off_t)length) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, session->c1, H2_STRM_MSG(stream, "send_data_cb, wanted %ld bytes, " "got %ld from stream"), (long)length, (long)len); apr_brigade_cleanup(session->bbtmp); @@ -541,20 +550,23 @@ static int on_send_data_cb(nghttp2_session *ngh2, if (padlen) { b = apr_bucket_immortal_create(immortal_zeros, padlen, - session->c->bucket_alloc); + session->c1->bucket_alloc); APR_BRIGADE_INSERT_TAIL(session->bbtmp, b); } - status = h2_conn_io_pass(&session->io, session->bbtmp); + status = h2_c1_io_append(&session->io, session->bbtmp); apr_brigade_cleanup(session->bbtmp); if (status == APR_SUCCESS) { stream->out_data_frames++; stream->out_data_octets += length; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_STRM_MSG(stream, "sent data length=%ld, total=%ld"), + (long)length, (long)stream->out_data_octets); return 0; } else { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c1, H2_STRM_LOG(APLOGNO(02925), stream, "failed send_data_cb")); return NGHTTP2_ERR_CALLBACK_FAILURE; } @@ -578,18 +590,27 @@ static int on_frame_send_cb(nghttp2_session *ngh2, break; } - if (APLOGcdebug(session->c)) { + stream = get_stream(session, stream_id); + if (APLOGcdebug(session->c1)) { char buffer[256]; h2_util_frame_print(frame, buffer, sizeof(buffer)/sizeof(buffer[0])); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - H2_SSSN_LOG(APLOGNO(03068), session, - "sent FRAME[%s], frames=%ld/%ld (r/s)"), - buffer, (long)session->frames_received, - (long)session->frames_sent); + if (stream) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_STRM_LOG(APLOGNO(10303), stream, + "sent FRAME[%s], frames=%ld/%ld (r/s)"), + buffer, (long)session->frames_received, + (long)session->frames_sent); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_SSSN_LOG(APLOGNO(03068), session, + "sent FRAME[%s], frames=%ld/%ld (r/s)"), + buffer, (long)session->frames_received, + (long)session->frames_sent); + } } - stream = h2_session_stream_get(session, stream_id); if (stream) { h2_stream_send_frame(stream, frame->hd.type, frame->hd.flags, frame->hd.length + H2_FRAME_HDR_LEN); @@ -598,7 +619,7 @@ static int on_frame_send_cb(nghttp2_session *ngh2, } #ifdef H2_NG2_INVALID_HEADER_CB -static int on_invalid_header_cb(nghttp2_session *ngh2, +static int on_invalid_header_cb(nghttp2_session *ngh2, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, @@ -607,14 +628,11 @@ static int on_invalid_header_cb(nghttp2_session *ngh2, h2_session *session = user_data; h2_stream *stream; - if (APLOGcdebug(session->c)) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03456) - "h2_stream(%ld-%d): invalid header '%s: %s'", - session->id, (int)frame->hd.stream_id, - apr_pstrndup(session->pool, (const char *)name, namelen), - apr_pstrndup(session->pool, (const char *)value, valuelen)); - } - stream = h2_session_stream_get(session, frame->hd.stream_id); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, APLOGNO(03456) + H2_SSSN_STRM_MSG(session, frame->hd.stream_id, + "invalid header '%.*s: %.*s'"), + (int)namelen, name, (int)valuelen, value); + stream = get_stream(session, frame->hd.stream_id); if (stream) { h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR); } @@ -622,6 +640,37 @@ static int on_invalid_header_cb(nghttp2_session *ngh2, } #endif +static ssize_t select_padding_cb(nghttp2_session *ngh2, + const nghttp2_frame *frame, + size_t max_payloadlen, void *user_data) +{ + h2_session *session = user_data; + size_t frame_len = frame->hd.length + H2_FRAME_HDR_LEN; /* the total length without padding */ + size_t padded_len = frame_len; + + /* Determine # of padding bytes to append to frame. Unless session->padding_always + * the number my be capped by the ui.write_size that currently applies. + */ + if (session->padding_max) { + int n = ap_random_pick(0, session->padding_max); + padded_len = H2MIN(max_payloadlen + H2_FRAME_HDR_LEN, frame_len + n); + } + + if (padded_len != frame_len) { + if (!session->padding_always && session->io.write_size + && (padded_len > session->io.write_size) + && (frame_len <= session->io.write_size)) { + padded_len = session->io.write_size; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + "select padding from [%d, %d]: %d (frame length: 0x%04x, write size: %d)", + (int)frame_len, (int)max_payloadlen+H2_FRAME_HDR_LEN, + (int)(padded_len - frame_len), (int)padded_len, (int)session->io.write_size); + return padded_len - H2_FRAME_HDR_LEN; + } + return frame->hd.length; +} + #define NGH2_SET_CALLBACK(callbacks, name, fn)\ nghttp2_session_callbacks_set_##name##_callback(callbacks, fn) @@ -647,9 +696,37 @@ static apr_status_t init_callbacks(conn_rec *c, nghttp2_session_callbacks **pcb) #ifdef H2_NG2_INVALID_HEADER_CB NGH2_SET_CALLBACK(*pcb, on_invalid_header, on_invalid_header_cb); #endif + NGH2_SET_CALLBACK(*pcb, select_padding, select_padding_cb); return APR_SUCCESS; } +static void update_child_status(h2_session *session, int status, + const char *msg, const h2_stream *stream) +{ + /* Assume that we also change code/msg when something really happened and + * avoid updating the scoreboard in between */ + if (session->last_status_code != status + || session->last_status_msg != msg) { + char sbuffer[1024]; + sbuffer[0] = '\0'; + if (stream) { + apr_snprintf(sbuffer, sizeof(sbuffer), + ": stream %d, %s %s", + stream->id, + stream->request? stream->request->method : "", + stream->request? stream->request->path : ""); + } + apr_snprintf(session->status, sizeof(session->status), + "[%d/%d] %s%s", + (int)(session->remote.emitted_count + session->pushes_submitted), + (int)session->streams_done, + msg? msg : "-", sbuffer); + ap_update_child_status_from_server(session->c1->sbh, status, + session->c1, session->s); + ap_update_child_status_descr(session->c1->sbh, status, session->status); + } +} + static apr_status_t h2_session_shutdown_notice(h2_session *session) { apr_status_t status; @@ -663,9 +740,9 @@ static apr_status_t h2_session_shutdown_notice(h2_session *session) session->local.accepting = 0; status = nghttp2_session_send(session->ngh2); if (status == APR_SUCCESS) { - status = h2_conn_io_flush(&session->io); + status = h2_c1_io_assure_flushed(&session->io); } - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_SSSN_LOG(APLOGNO(03457), session, "sent shutdown notice")); return status; } @@ -679,10 +756,13 @@ static apr_status_t h2_session_shutdown(h2_session *session, int error, if (session->local.shutdown) { return APR_SUCCESS; } - if (!msg && error) { - msg = nghttp2_strerror(error); + + if (error && !msg) { + if (APR_STATUS_IS_EPIPE(error)) { + msg = "remote close"; + } } - + if (error || force_close) { /* not a graceful shutdown, we want to leave... * Do not start further streams that are waiting to be scheduled. @@ -691,8 +771,9 @@ static apr_status_t h2_session_shutdown(h2_session *session, int error, * Remove all streams greater than this number without submitting * a RST_STREAM frame, since that should be clear from the GOAWAY * we send. */ - session->local.accepted_max = h2_mplx_shutdown(session->mplx); + session->local.accepted_max = h2_mplx_c1_shutdown(session->mplx); session->local.error = error; + session->local.error_msg = msg; } else { /* graceful shutdown. we will continue processing all streams @@ -702,25 +783,25 @@ static apr_status_t h2_session_shutdown(h2_session *session, int error, session->local.accepting = 0; session->local.shutdown = 1; - if (!session->c->aborted) { + if (!session->c1->aborted) { nghttp2_submit_goaway(session->ngh2, NGHTTP2_FLAG_NONE, session->local.accepted_max, error, (uint8_t*)msg, msg? strlen(msg):0); status = nghttp2_session_send(session->ngh2); if (status == APR_SUCCESS) { - status = h2_conn_io_flush(&session->io); + status = h2_c1_io_assure_flushed(&session->io); } - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_SSSN_LOG(APLOGNO(03069), session, "sent GOAWAY, err=%d, msg=%s"), error, msg? msg : ""); } - dispatch_event(session, H2_SESSION_EV_LOCAL_GOAWAY, error, msg); + h2_session_dispatch_event(session, H2_SESSION_EV_LOCAL_GOAWAY, error, msg); return status; } static apr_status_t session_cleanup(h2_session *session, const char *trigger) { - conn_rec *c = session->c; + conn_rec *c = session->c1; ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, H2_SSSN_MSG(session, "pool_cleanup")); @@ -734,40 +815,54 @@ static apr_status_t session_cleanup(h2_session *session, const char *trigger) * connection when sending the next request, this has the effect * that at least this one request will fail. */ - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, H2_SSSN_LOG(APLOGNO(03199), session, "connection disappeared without proper " "goodbye, clients will be confused, should not happen")); } + if (!h2_iq_empty(session->ready_to_process)) { + int sid; + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, + H2_SSSN_LOG(APLOGNO(10485), session, + "cleanup, resetting %d streams in ready-to-process"), + h2_iq_count(session->ready_to_process)); + while ((sid = h2_iq_shift(session->ready_to_process)) > 0) { + h2_mplx_c1_client_rst(session->mplx, sid, get_stream(session, sid)); + } + } + transit(session, trigger, H2_SESSION_ST_CLEANUP); - h2_mplx_release_and_join(session->mplx, session->iowait); + h2_mplx_c1_destroy(session->mplx); session->mplx = NULL; ap_assert(session->ngh2); nghttp2_session_del(session->ngh2); session->ngh2 = NULL; - h2_ctx_clear(c); - - + h2_conn_ctx_detach(c); + return APR_SUCCESS; } static apr_status_t session_pool_cleanup(void *data) { conn_rec *c = data; - h2_session *session; - h2_ctx *ctx = h2_ctx_get(c, 0); - - if (ctx && (session = h2_ctx_session_get(ctx))) { + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + h2_session *session = conn_ctx? conn_ctx->session : NULL; + + if (session) { + int mpm_state = 0; + int level; + + ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state); + level = (AP_MPMQ_STOPPING == mpm_state)? APLOG_DEBUG : APLOG_WARNING; /* if the session is still there, now is the last chance * to perform cleanup. Normally, cleanup should have happened - * earlier in the connection pre_close. Main reason is that - * any ongoing requests on slave connections might still access - * data which has, at this time, already been freed. An example - * is mod_ssl that uses request hooks. */ - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, - H2_SSSN_LOG(APLOGNO(10020), session, + * earlier in the connection pre_close. + * However, when the server is stopping, it may shutdown connections + * without running the pre_close hooks. Do not want about that. */ + ap_log_cerror(APLOG_MARK, level, 0, c, + H2_SSSN_LOG(APLOGNO(10020), session, "session cleanup triggered by pool cleanup. " "this should have happened earlier already.")); return session_cleanup(session, "pool cleanup"); @@ -775,101 +870,83 @@ static apr_status_t session_pool_cleanup(void *data) return APR_SUCCESS; } -static apr_status_t h2_session_create_int(h2_session **psession, - conn_rec *c, - request_rec *r, - h2_ctx *ctx, - h2_workers *workers) +static /* atomic */ apr_uint32_t next_id; + +apr_status_t h2_session_create(h2_session **psession, conn_rec *c, request_rec *r, + server_rec *s, h2_workers *workers) { nghttp2_session_callbacks *callbacks = NULL; nghttp2_option *options = NULL; - apr_allocator_t *allocator; - apr_thread_mutex_t *mutex; uint32_t n; + int thread_num; apr_pool_t *pool = NULL; h2_session *session; + h2_stream *stream0; apr_status_t status; int rv; *psession = NULL; - status = apr_allocator_create(&allocator); - if (status != APR_SUCCESS) { - return status; - } - apr_allocator_max_free_set(allocator, ap_max_mem_free); - apr_pool_create_ex(&pool, c->pool, NULL, allocator); - if (!pool) { - apr_allocator_destroy(allocator); - return APR_ENOMEM; - } + apr_pool_create(&pool, c->pool); apr_pool_tag(pool, "h2_session"); - apr_allocator_owner_set(allocator, pool); - status = apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, pool); - if (status != APR_SUCCESS) { - apr_pool_destroy(pool); - return APR_ENOMEM; - } - apr_allocator_mutex_set(allocator, mutex); - session = apr_pcalloc(pool, sizeof(h2_session)); if (!session) { return APR_ENOMEM; } *psession = session; - session->id = c->id; - session->c = c; + /* c->id does not give a unique id for the lifetime of the session. + * mpms like event change c->id when re-activating a keepalive + * connection based on the child_num+thread_num of the worker + * processing it. + * We'd like to have an id that remains constant and unique bc + * h2 streams can live through keepalive periods. While double id + * will not lead to processing failures, it will confuse log analysis. + */ +#if AP_MODULE_MAGIC_AT_LEAST(20211221, 8) + ap_sb_get_child_thread(c->sbh, &session->child_num, &thread_num); +#else + (void)thread_num; + session->child_num = (int)getpid(); +#endif + session->id = apr_atomic_inc32(&next_id); + session->c1 = c; session->r = r; - session->s = h2_ctx_server_get(ctx); + session->s = s; session->pool = pool; - session->config = h2_config_sget(session->s); session->workers = workers; session->state = H2_SESSION_ST_INIT; session->local.accepting = 1; session->remote.accepting = 1; - session->max_stream_count = h2_config_geti(session->config, - H2_CONF_MAX_STREAMS); - session->max_stream_mem = h2_config_geti(session->config, - H2_CONF_STREAM_MAX_MEM); - - status = apr_thread_cond_create(&session->iowait, session->pool); - if (status != APR_SUCCESS) { - apr_pool_destroy(pool); - return status; - } - - session->in_pending = h2_iq_create(session->pool, (int)session->max_stream_count); - if (session->in_pending == NULL) { - apr_pool_destroy(pool); - return APR_ENOMEM; - } + session->max_stream_count = h2_config_sgeti(s, H2_CONF_MAX_STREAMS); + session->max_stream_mem = h2_config_sgeti(s, H2_CONF_STREAM_MAX_MEM); + session->max_data_frame_len = h2_config_sgeti(s, H2_CONF_MAX_DATA_FRAME_LEN); + + session->out_c1_blocked = h2_iq_create(session->pool, (int)session->max_stream_count); + session->ready_to_process = h2_iq_create(session->pool, (int)session->max_stream_count); - session->in_process = h2_iq_create(session->pool, (int)session->max_stream_count); - if (session->in_process == NULL) { - apr_pool_destroy(pool); - return APR_ENOMEM; - } - session->monitor = apr_pcalloc(pool, sizeof(h2_stream_monitor)); - if (session->monitor == NULL) { - apr_pool_destroy(pool); - return APR_ENOMEM; - } session->monitor->ctx = session; session->monitor->on_state_enter = on_stream_state_enter; session->monitor->on_state_event = on_stream_state_event; session->monitor->on_event = on_stream_event; - - session->mplx = h2_mplx_create(c, session->pool, session->config, - workers); - - /* connection input filter that feeds the session */ - session->cin = h2_filter_cin_create(session); - ap_add_input_filter("H2_IN", session->cin, r, c); - - h2_conn_io_init(&session->io, c, session->config); + + stream0 = h2_stream_create(0, session->pool, session, NULL, 0); + stream0->c2 = session->c1; /* stream0's connection is the main connection */ + session->mplx = h2_mplx_c1_create(session->child_num, session->id, + stream0, s, session->pool, workers); + if (!session->mplx) { + apr_pool_destroy(pool); + return APR_ENOTIMPL; + } + + h2_c1_io_init(&session->io, session); + session->padding_max = h2_config_sgeti(s, H2_CONF_PADDING_BITS); + if (session->padding_max) { + session->padding_max = (0x01 << session->padding_max) - 1; + } + session->padding_always = h2_config_sgeti(s, H2_CONF_PADDING_ALWAYS); session->bbtmp = apr_brigade_create(session->pool, c->bucket_alloc); status = init_callbacks(c, &callbacks); @@ -888,12 +965,23 @@ static apr_status_t h2_session_create_int(h2_session **psession, apr_pool_destroy(pool); return status; } - nghttp2_option_set_peer_max_concurrent_streams( - options, (uint32_t)session->max_stream_count); + nghttp2_option_set_peer_max_concurrent_streams(options, (uint32_t)session->max_stream_count); /* We need to handle window updates ourself, otherwise we * get flooded by nghttp2. */ nghttp2_option_set_no_auto_window_update(options, 1); - +#ifdef H2_NG2_NO_CLOSED_STREAMS + /* We do not want nghttp2 to keep information about closed streams as + * that accumulates memory on long connections. This makes PRIORITY + * setting in relation to older streams non-working. */ + nghttp2_option_set_no_closed_streams(options, 1); +#endif +#ifdef H2_NG2_RFC9113_STRICTNESS + /* nghttp2 v1.50.0 introduces the strictness checks on leading/trailing + * whitespace of RFC 9113 for fields. But, by default, it RST streams + * carrying such. We do not want that. We want to strip the ws and + * handle them, just like the HTTP/1.1 parser does. */ + nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(options, 1); +#endif rv = nghttp2_session_server_new2(&session->ngh2, callbacks, session, options); nghttp2_session_callbacks_del(callbacks); @@ -907,7 +995,7 @@ static apr_status_t h2_session_create_int(h2_session **psession, return APR_ENOMEM; } - n = h2_config_geti(session->config, H2_CONF_PUSH_DIARY_SIZE); + n = h2_config_sgeti(s, H2_CONF_PUSH_DIARY_SIZE); session->push_diary = h2_push_diary_create(session->pool, n); if (APLOGcdebug(c)) { @@ -915,35 +1003,26 @@ static apr_status_t h2_session_create_int(h2_session **psession, H2_SSSN_LOG(APLOGNO(03200), session, "created, max_streams=%d, stream_mem=%d, " "workers_limit=%d, workers_max=%d, " - "push_diary(type=%d,N=%d)"), + "push_diary(type=%d,N=%d), " + "max_data_frame_len=%d"), (int)session->max_stream_count, (int)session->max_stream_mem, - session->mplx->limit_active, - session->mplx->max_active, + session->mplx->processing_limit, + session->mplx->processing_max, session->push_diary->dtype, - (int)session->push_diary->N); + (int)session->push_diary->N, + (int)session->max_data_frame_len); } - apr_pool_pre_cleanup_register(pool, c, session_pool_cleanup); + apr_pool_pre_cleanup_register(pool, c, session_pool_cleanup); + return APR_SUCCESS; } -apr_status_t h2_session_create(h2_session **psession, - conn_rec *c, h2_ctx *ctx, h2_workers *workers) -{ - return h2_session_create_int(psession, c, NULL, ctx, workers); -} - -apr_status_t h2_session_rcreate(h2_session **psession, - request_rec *r, h2_ctx *ctx, h2_workers *workers) -{ - return h2_session_create_int(psession, r->connection, r, ctx, workers); -} - static apr_status_t h2_session_start(h2_session *session, int *rv) { apr_status_t status = APR_SUCCESS; - nghttp2_settings_entry settings[3]; + nghttp2_settings_entry settings[4]; size_t slen; int win_size; @@ -1004,14 +1083,21 @@ static apr_status_t h2_session_start(h2_session *session, int *rv) settings[slen].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; settings[slen].value = (uint32_t)session->max_stream_count; ++slen; - win_size = h2_config_geti(session->config, H2_CONF_WIN_SIZE); + win_size = h2_config_sgeti(session->s, H2_CONF_WIN_SIZE); if (win_size != H2_INITIAL_WINDOW_SIZE) { settings[slen].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; settings[slen].value = win_size; ++slen; } - - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, +#if H2_USE_WEBSOCKETS + if (h2_config_sgeti(session->s, H2_CONF_WEBSOCKETS)) { + settings[slen].settings_id = NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL; + settings[slen].value = 1; + ++slen; + } +#endif + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c1, H2_SSSN_LOG(APLOGNO(03201), session, "start, INITIAL_WINDOW_SIZE=%ld, MAX_CONCURRENT_STREAMS=%d"), (long)win_size, (int)session->max_stream_count); @@ -1019,7 +1105,7 @@ static apr_status_t h2_session_start(h2_session *session, int *rv) settings, slen); if (*rv != 0) { status = APR_EGENERAL; - ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c1, H2_SSSN_LOG(APLOGNO(02935), session, "nghttp2_submit_settings: %s"), nghttp2_strerror(*rv)); } @@ -1037,7 +1123,7 @@ static apr_status_t h2_session_start(h2_session *session, int *rv) 0, NGHTTP2_MAX_WINDOW_SIZE - win_size); if (*rv != 0) { status = APR_EGENERAL; - ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c1, H2_SSSN_LOG(APLOGNO(02970), session, "nghttp2_submit_window_update: %s"), nghttp2_strerror(*rv)); @@ -1047,87 +1133,6 @@ static apr_status_t h2_session_start(h2_session *session, int *rv) return status; } -static apr_status_t on_stream_headers(h2_session *session, h2_stream *stream, - h2_headers *headers, apr_off_t len, - int eos); - -static ssize_t stream_data_cb(nghttp2_session *ng2s, - int32_t stream_id, - uint8_t *buf, - size_t length, - uint32_t *data_flags, - nghttp2_data_source *source, - void *puser) -{ - h2_session *session = (h2_session *)puser; - apr_off_t nread = length; - int eos = 0; - apr_status_t status; - h2_stream *stream; - ap_assert(session); - - /* The session wants to send more DATA for the stream. We need - * to find out how much of the requested length we can send without - * blocking. - * Indicate EOS when we encounter it or DEFERRED if the stream - * should be suspended. Beware of trailers. - */ - - (void)ng2s; - (void)buf; - (void)source; - stream = h2_session_stream_get(session, stream_id); - if (!stream) { - ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, session->c, - APLOGNO(02937) - "h2_stream(%ld-%d): data_cb, stream not found", - session->id, (int)stream_id); - return NGHTTP2_ERR_CALLBACK_FAILURE; - } - - status = h2_stream_out_prepare(stream, &nread, &eos, NULL); - if (nread) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - H2_STRM_MSG(stream, "prepared no_copy, len=%ld, eos=%d"), - (long)nread, eos); - *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; - } - - switch (status) { - case APR_SUCCESS: - break; - - case APR_EOF: - eos = 1; - break; - - case APR_ECONNRESET: - case APR_ECONNABORTED: - return NGHTTP2_ERR_CALLBACK_FAILURE; - - case APR_EAGAIN: - /* If there is no data available, our session will automatically - * suspend this stream and not ask for more data until we resume - * it. Remember at our h2_stream that we need to do this. - */ - nread = 0; - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - H2_STRM_LOG(APLOGNO(03071), stream, "suspending")); - return NGHTTP2_ERR_DEFERRED; - - default: - nread = 0; - ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, - H2_STRM_LOG(APLOGNO(02938), stream, "reading data")); - return NGHTTP2_ERR_CALLBACK_FAILURE; - } - - if (eos) { - *data_flags |= NGHTTP2_DATA_FLAG_EOF; - } - return (ssize_t)nread; -} - struct h2_stream *h2_session_push(h2_session *session, h2_stream *is, h2_push *push) { @@ -1142,21 +1147,21 @@ struct h2_stream *h2_session_push(h2_session *session, h2_stream *is, ngh->nv, ngh->nvlen, NULL); } if (status != APR_SUCCESS || nid <= 0) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c1, H2_STRM_LOG(APLOGNO(03075), is, "submitting push promise fail: %s"), nghttp2_strerror(nid)); return NULL; } ++session->pushes_promised; - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_STRM_LOG(APLOGNO(03076), is, "SERVER_PUSH %d for %s %s on %d"), nid, push->req->method, push->req->path, is->id); stream = h2_session_open_stream(session, nid, is->id); if (!stream) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - H2_STRM_LOG(APLOGNO(03077), stream, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_STRM_LOG(APLOGNO(03077), is, "failed to create stream obj %d"), nid); /* kill the push_promise */ nghttp2_submit_rst_stream(session->ngh2, NGHTTP2_FLAG_NONE, nid, @@ -1166,7 +1171,6 @@ struct h2_stream *h2_session_push(h2_session *session, h2_stream *is, h2_session_set_prio(session, stream, push->priority); h2_stream_set_request(stream, push->req); - ++session->unsent_promises; return stream; } @@ -1181,7 +1185,6 @@ apr_status_t h2_session_set_prio(h2_session *session, h2_stream *stream, const h2_priority *prio) { apr_status_t status = APR_SUCCESS; -#ifdef H2_NG2_CHANGE_PRIO nghttp2_stream *s_grandpa, *s_parent, *s; if (prio == NULL) { @@ -1190,7 +1193,7 @@ apr_status_t h2_session_set_prio(h2_session *session, h2_stream *stream, } s = nghttp2_session_find_stream(session->ngh2, stream->id); if (!s) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c1, H2_STRM_MSG(stream, "lookup of nghttp2_stream failed")); return APR_EINVAL; } @@ -1239,10 +1242,10 @@ apr_status_t h2_session_set_prio(h2_session *session, h2_stream *stream, id_grandpa = nghttp2_stream_get_stream_id(s_grandpa); rv = nghttp2_session_change_stream_priority(session->ngh2, id_parent, &ps); if (rv < 0) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, APLOGNO(03202) - "h2_stream(%ld-%d): PUSH BEFORE, weight=%d, " - "depends=%d, returned=%d", - session->id, id_parent, ps.weight, ps.stream_id, rv); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, APLOGNO(03202) + H2_SSSN_STRM_MSG(session, id_parent, + "PUSH BEFORE, weight=%d, depends=%d, returned=%d"), + ps.weight, ps.stream_id, rv); return APR_EGENERAL; } nghttp2_priority_spec_init(&ps, id_grandpa, w, 0); @@ -1261,18 +1264,13 @@ apr_status_t h2_session_set_prio(h2_session *session, h2_stream *stream, rv = nghttp2_session_change_stream_priority(session->ngh2, stream->id, &ps); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - ""H2_STRM_LOG(APLOGNO(03203), stream, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_STRM_LOG(APLOGNO(03203), stream, "PUSH %s, weight=%d, depends=%d, returned=%d"), ptype, ps.weight, ps.stream_id, rv); status = (rv < 0)? APR_EGENERAL : APR_SUCCESS; } -#else - (void)session; - (void)stream; - (void)prio; - (void)valid_weight; -#endif + return status; } @@ -1280,338 +1278,89 @@ int h2_session_push_enabled(h2_session *session) { /* iff we can and they can and want */ return (session->remote.accepting /* remote GOAWAY received */ - && h2_config_geti(session->config, H2_CONF_PUSH) + && h2_config_sgeti(session->s, H2_CONF_PUSH) && nghttp2_session_get_remote_settings(session->ngh2, NGHTTP2_SETTINGS_ENABLE_PUSH)); } -static apr_status_t h2_session_send(h2_session *session) +static int h2_session_want_send(h2_session *session) { - apr_interval_time_t saved_timeout; - int rv; - apr_socket_t *socket; - - socket = ap_get_conn_socket(session->c); - if (socket) { - apr_socket_timeout_get(socket, &saved_timeout); - apr_socket_timeout_set(socket, session->s->timeout); - } - - rv = nghttp2_session_send(session->ngh2); - - if (socket) { - apr_socket_timeout_set(socket, saved_timeout); - } - session->have_written = 1; - if (rv != 0 && rv != NGHTTP2_ERR_WOULDBLOCK) { - if (nghttp2_is_fatal(rv)) { - dispatch_event(session, H2_SESSION_EV_PROTO_ERROR, rv, nghttp2_strerror(rv)); - return APR_EGENERAL; - } - } - - session->unsent_promises = 0; - session->unsent_submits = 0; - - return APR_SUCCESS; + return nghttp2_session_want_write(session->ngh2) + || h2_c1_io_pending(&session->io); } -/** - * headers for the stream are ready. - */ -static apr_status_t on_stream_headers(h2_session *session, h2_stream *stream, - h2_headers *headers, apr_off_t len, - int eos) +static apr_status_t h2_session_send(h2_session *session) { - apr_status_t status = APR_SUCCESS; - int rv = 0; + int ngrv, pending = 0; + apr_status_t rv = APR_SUCCESS; - ap_assert(session); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - H2_STRM_MSG(stream, "on_headers")); - if (headers->status < 100) { - h2_stream_rst(stream, headers->status); - goto leave; - } - else if (stream->has_response) { - h2_ngheader *nh; - - status = h2_res_create_ngtrailer(&nh, stream->pool, headers); - - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, - H2_STRM_LOG(APLOGNO(03072), stream, "submit %d trailers"), - (int)nh->nvlen); - if (status == APR_SUCCESS) { - rv = nghttp2_submit_trailer(session->ngh2, stream->id, - nh->nv, nh->nvlen); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, - H2_STRM_LOG(APLOGNO(10024), stream, "invalid trailers")); - h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR); - } - goto leave; - } - else { - nghttp2_data_provider provider, *pprovider = NULL; - h2_ngheader *ngh; - const char *note; - - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - H2_STRM_LOG(APLOGNO(03073), stream, "submit response %d, REMOTE_WINDOW_SIZE=%u"), - headers->status, - (unsigned int)nghttp2_session_get_stream_remote_window_size(session->ngh2, stream->id)); - - if (!eos || len > 0) { - memset(&provider, 0, sizeof(provider)); - provider.source.fd = stream->id; - provider.read_callback = stream_data_cb; - pprovider = &provider; - } - - /* If this stream is not a pushed one itself, - * and HTTP/2 server push is enabled here, - * and the response HTTP status is not sth >= 400, - * and the remote side has pushing enabled, - * -> find and perform any pushes on this stream - * *before* we submit the stream response itself. - * This helps clients avoid opening new streams on Link - * headers that get pushed right afterwards. - * - * *) the response code is relevant, as we do not want to - * make pushes on 401 or 403 codes and friends. - * And if we see a 304, we do not push either - * as the client, having this resource in its cache, might - * also have the pushed ones as well. - */ - if (!stream->initiated_on - && !stream->has_response - && stream->request && stream->request->method - && !strcmp("GET", stream->request->method) - && (headers->status < 400) - && (headers->status != 304) - && h2_session_push_enabled(session)) { - - h2_stream_submit_pushes(stream, headers); - } - - if (!stream->pref_priority) { - stream->pref_priority = h2_stream_get_priority(stream, headers); - } - h2_session_set_prio(session, stream, stream->pref_priority); - - note = apr_table_get(headers->notes, H2_FILTER_DEBUG_NOTE); - if (note && !strcmp("on", note)) { - int32_t connFlowIn, connFlowOut; - - connFlowIn = nghttp2_session_get_effective_local_window_size(session->ngh2); - connFlowOut = nghttp2_session_get_remote_window_size(session->ngh2); - headers = h2_headers_copy(stream->pool, headers); - apr_table_setn(headers->headers, "conn-flow-in", - apr_itoa(stream->pool, connFlowIn)); - apr_table_setn(headers->headers, "conn-flow-out", - apr_itoa(stream->pool, connFlowOut)); - } - - if (headers->status == 103 - && !h2_config_geti(session->config, H2_CONF_EARLY_HINTS)) { - /* suppress sending this to the client, it might have triggered - * pushes and served its purpose nevertheless */ - rv = 0; - goto leave; - } - - status = h2_res_create_ngheader(&ngh, stream->pool, headers); - if (status == APR_SUCCESS) { - rv = nghttp2_submit_response(session->ngh2, stream->id, - ngh->nv, ngh->nvlen, pprovider); - stream->has_response = h2_headers_are_response(headers); - session->have_written = 1; - - if (stream->initiated_on) { - ++session->pushes_submitted; - } - else { - ++session->responses_submitted; + while (nghttp2_session_want_write(session->ngh2)) { + ngrv = nghttp2_session_send(session->ngh2); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + "nghttp2_session_send: %d", (int)ngrv); + pending = 1; + if (ngrv != 0 && ngrv != NGHTTP2_ERR_WOULDBLOCK) { + if (nghttp2_is_fatal(ngrv)) { + h2_session_dispatch_event(session, H2_SESSION_EV_PROTO_ERROR, + ngrv, nghttp2_strerror(ngrv)); + rv = APR_EGENERAL; + goto cleanup; } } - else { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c, - H2_STRM_LOG(APLOGNO(10025), stream, "invalid response")); - h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR); + if (h2_c1_io_needs_flush(&session->io) || + ngrv == NGHTTP2_ERR_WOULDBLOCK) { + rv = h2_c1_io_assure_flushed(&session->io); + if (rv != APR_SUCCESS) + goto cleanup; + pending = 0; } } - -leave: - if (nghttp2_is_fatal(rv)) { - status = APR_EGENERAL; - dispatch_event(session, H2_SESSION_EV_PROTO_ERROR, rv, nghttp2_strerror(rv)); - ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, - APLOGNO(02940) "submit_response: %s", - nghttp2_strerror(rv)); + if (pending) { + rv = h2_c1_io_pass(&session->io); } - - ++session->unsent_submits; - - /* Unsent push promises are written immediately, as nghttp2 - * 1.5.0 realizes internal stream data structures only on - * send and we might need them for other submits. - * Also, to conserve memory, we send at least every 10 submits - * so that nghttp2 does not buffer all outbound items too - * long. - */ - if (status == APR_SUCCESS - && (session->unsent_promises || session->unsent_submits > 10)) { - status = h2_session_send(session); +cleanup: + if (rv != APR_SUCCESS) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, rv, NULL); } - return status; + return rv; } /** - * A stream was resumed as new response/output data arrived. + * A streams input state has changed. */ -static apr_status_t on_stream_resume(void *ctx, h2_stream *stream) +static void on_stream_input(void *ctx, h2_stream *stream) { h2_session *session = ctx; - apr_status_t status = APR_EAGAIN; - int rv; - apr_off_t len = 0; - int eos = 0; - h2_headers *headers; - - ap_assert(stream); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - H2_STRM_MSG(stream, "on_resume")); - -send_headers: - headers = NULL; - status = h2_stream_out_prepare(stream, &len, &eos, &headers); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c, - H2_STRM_MSG(stream, "prepared len=%ld, eos=%d"), - (long)len, eos); - if (headers) { - status = on_stream_headers(session, stream, headers, len, eos); - if (status != APR_SUCCESS || stream->rst_error) { - return status; - } - goto send_headers; - } - else if (status != APR_EAGAIN) { - /* we have DATA to send */ - if (!stream->has_response) { - /* but no response */ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, - H2_STRM_LOG(APLOGNO(03466), stream, - "no response, RST_STREAM")); - h2_stream_rst(stream, H2_ERR_PROTOCOL_ERROR); - return APR_SUCCESS; - } - rv = nghttp2_session_resume_data(session->ngh2, stream->id); - session->have_written = 1; - ap_log_cerror(APLOG_MARK, nghttp2_is_fatal(rv)? - APLOG_ERR : APLOG_DEBUG, 0, session->c, - H2_STRM_LOG(APLOGNO(02936), stream, "resumed")); - } - return status; -} -static void h2_session_in_flush(h2_session *session) -{ - int id; - - while ((id = h2_iq_shift(session->in_process)) > 0) { - h2_stream *stream = h2_session_stream_get(session, id); - if (stream) { - ap_assert(!stream->scheduled); - if (h2_stream_prep_processing(stream) == APR_SUCCESS) { - h2_mplx_process(session->mplx, stream, stream_pri_cmp, session); - } - else { - h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR); - } - } + ap_assert(stream); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_STRM_MSG(stream, "on_input change")); + update_child_status(session, SERVER_BUSY_READ, "read", stream); + if (stream->id == 0) { + /* input on primary connection available? read */ + h2_c1_read(session); } - - while ((id = h2_iq_shift(session->in_pending)) > 0) { - h2_stream *stream = h2_session_stream_get(session, id); - if (stream) { - h2_stream_flush_input(stream); - } + else { + h2_stream_on_input_change(stream); } } -static apr_status_t session_read(h2_session *session, apr_size_t readlen, int block) +/** + * A streams output state has changed. + */ +static void on_stream_output(void *ctx, h2_stream *stream) { - apr_status_t status, rstatus = APR_EAGAIN; - conn_rec *c = session->c; - apr_off_t read_start = session->io.bytes_read; - - while (1) { - /* H2_IN filter handles all incoming data against the session. - * We just pull at the filter chain to make it happen */ - status = ap_get_brigade(c->input_filters, - session->bbtmp, AP_MODE_READBYTES, - block? APR_BLOCK_READ : APR_NONBLOCK_READ, - H2MAX(APR_BUCKET_BUFF_SIZE, readlen)); - /* get rid of any possible data we do not expect to get */ - apr_brigade_cleanup(session->bbtmp); - - switch (status) { - case APR_SUCCESS: - /* successful read, reset our idle timers */ - rstatus = APR_SUCCESS; - if (block) { - /* successful blocked read, try unblocked to - * get more. */ - block = 0; - } - break; - case APR_EAGAIN: - return rstatus; - case APR_TIMEUP: - return status; - default: - if (session->io.bytes_read == read_start) { - /* first attempt failed */ - if (APR_STATUS_IS_ETIMEDOUT(status) - || APR_STATUS_IS_ECONNABORTED(status) - || APR_STATUS_IS_ECONNRESET(status) - || APR_STATUS_IS_EOF(status) - || APR_STATUS_IS_EBADF(status)) { - /* common status for a client that has left */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, c, - H2_SSSN_MSG(session, "input gone")); - } - else { - /* uncommon status, log on INFO so that we see this */ - ap_log_cerror( APLOG_MARK, APLOG_DEBUG, status, c, - H2_SSSN_LOG(APLOGNO(02950), session, - "error reading, terminating")); - } - return status; - } - /* subsequent failure after success(es), return initial - * status. */ - return rstatus; - } - if ((session->io.bytes_read - read_start) > readlen) { - /* read enough in one go, give write a chance */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, c, - H2_SSSN_MSG(session, "read enough, returning")); - break; - } + h2_session *session = ctx; + + ap_assert(stream); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_STRM_MSG(stream, "on_output change")); + if (stream->id != 0) { + update_child_status(session, SERVER_BUSY_WRITE, "write", stream); + h2_stream_on_output_change(stream); } - return rstatus; } -static apr_status_t h2_session_read(h2_session *session, int block) -{ - apr_status_t status = session_read(session, session->max_stream_mem - * H2MAX(2, session->open_streams), - block); - h2_session_in_flush(session); - return status; -} static const char *StateNames[] = { "INIT", /* H2_SESSION_ST_INIT */ @@ -1630,40 +1379,14 @@ const char *h2_session_state_str(h2_session_state state) return StateNames[state]; } -static void update_child_status(h2_session *session, int status, const char *msg) -{ - /* Assume that we also change code/msg when something really happened and - * avoid updating the scoreboard in between */ - if (session->last_status_code != status - || session->last_status_msg != msg) { - apr_snprintf(session->status, sizeof(session->status), - "%s, streams: %d/%d/%d/%d/%d (open/recv/resp/push/rst)", - msg? msg : "-", - (int)session->open_streams, - (int)session->remote.emitted_count, - (int)session->responses_submitted, - (int)session->pushes_submitted, - (int)session->pushes_reset + session->streams_reset); - ap_update_child_status_descr(session->c->sbh, status, session->status); - } -} - static void transit(h2_session *session, const char *action, h2_session_state nstate) { - apr_time_t timeout; - int ostate, loglvl; - const char *s; - + int ostate; + if (session->state != nstate) { ostate = session->state; - session->state = nstate; - - loglvl = APLOG_DEBUG; - if ((ostate == H2_SESSION_ST_BUSY && nstate == H2_SESSION_ST_WAIT) - || (ostate == H2_SESSION_ST_WAIT && nstate == H2_SESSION_ST_BUSY)){ - loglvl = APLOG_TRACE1; - } - ap_log_cerror(APLOG_MARK, loglvl, 0, session->c, + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_SSSN_LOG(APLOGNO(03078), session, "transit [%s] -- %s --> [%s]"), h2_session_state_str(ostate), action, @@ -1678,35 +1401,21 @@ static void transit(h2_session *session, const char *action, h2_session_state ns * If we return to mpm right away, this connection has the * same chance of being cleaned up by the mpm as connections * that already served requests - not fair. */ - session->idle_sync_until = apr_time_now() + apr_time_from_sec(1); - s = "timeout"; - timeout = H2MAX(session->s->timeout, session->s->keep_alive_timeout); - update_child_status(session, SERVER_BUSY_READ, "idle"); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, - H2_SSSN_LOG("", session, "enter idle, timeout = %d sec"), - (int)apr_time_sec(H2MAX(session->s->timeout, session->s->keep_alive_timeout))); - } - else if (session->open_streams) { - s = "timeout"; - timeout = session->s->keep_alive_timeout; - update_child_status(session, SERVER_BUSY_KEEPALIVE, "idle"); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c1, + H2_SSSN_LOG("", session, "enter idle")); } else { /* normal keepalive setup */ - s = "keepalive"; - timeout = session->s->keep_alive_timeout; - update_child_status(session, SERVER_BUSY_KEEPALIVE, "idle"); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c1, + H2_SSSN_LOG("", session, "enter keepalive")); } - session->idle_until = apr_time_now() + timeout; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, - H2_SSSN_LOG("", session, "enter idle, %s = %d sec"), - s, (int)apr_time_sec(timeout)); + session->state = nstate; break; case H2_SESSION_ST_DONE: - update_child_status(session, SERVER_CLOSING, "done"); break; default: /* nop */ + session->state = nstate; break; } } @@ -1724,12 +1433,45 @@ static void h2_session_ev_init(h2_session *session, int arg, const char *msg) } } +static void h2_session_ev_input_pending(h2_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_SESSION_ST_INIT: + case H2_SESSION_ST_IDLE: + case H2_SESSION_ST_WAIT: + transit(session, "input read", H2_SESSION_ST_BUSY); + break; + default: + break; + } +} + +static void h2_session_ev_input_exhausted(h2_session *session, int arg, const char *msg) +{ + switch (session->state) { + case H2_SESSION_ST_BUSY: + if (!h2_session_want_send(session)) { + if (session->open_streams == 0) { + transit(session, "input exhausted, no streams", H2_SESSION_ST_IDLE); + } + else { + transit(session, "input exhausted", H2_SESSION_ST_WAIT); + } + } + break; + case H2_SESSION_ST_WAIT: + if (session->open_streams == 0) { + transit(session, "input exhausted, no streams", H2_SESSION_ST_IDLE); + } + break; + default: + break; + } +} + static void h2_session_ev_local_goaway(h2_session *session, int arg, const char *msg) { cleanup_unprocessed_streams(session); - if (!session->remote.shutdown) { - update_child_status(session, SERVER_CLOSING, "local goaway"); - } transit(session, "local goaway", H2_SESSION_ST_DONE); } @@ -1740,7 +1482,6 @@ static void h2_session_ev_remote_goaway(h2_session *session, int arg, const char session->remote.accepting = 0; session->remote.shutdown = 1; cleanup_unprocessed_streams(session); - update_child_status(session, SERVER_CLOSING, "remote goaway"); transit(session, "remote goaway", H2_SESSION_ST_DONE); } } @@ -1755,7 +1496,7 @@ static void h2_session_ev_conn_error(h2_session *session, int arg, const char *m break; default: - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_SSSN_LOG(APLOGNO(03401), session, "conn error -> shutdown")); h2_session_shutdown(session, arg, msg, 0); @@ -1766,7 +1507,7 @@ static void h2_session_ev_conn_error(h2_session *session, int arg, const char *m static void h2_session_ev_proto_error(h2_session *session, int arg, const char *msg) { if (!session->local.shutdown) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_SSSN_LOG(APLOGNO(03402), session, "proto error -> shutdown")); h2_session_shutdown(session, arg, msg, 0); @@ -1781,115 +1522,91 @@ static void h2_session_ev_conn_timeout(h2_session *session, int arg, const char } } -static void h2_session_ev_no_io(h2_session *session, int arg, const char *msg) +static void h2_session_ev_ngh2_done(h2_session *session, int arg, const char *msg) { switch (session->state) { - case H2_SESSION_ST_BUSY: - /* Nothing to READ, nothing to WRITE on the master connection. - * Possible causes: - * - we wait for the client to send us sth - * - we wait for started tasks to produce output - * - we have finished all streams and the client has sent GO_AWAY - */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - H2_SSSN_MSG(session, "NO_IO event, %d streams open"), - session->open_streams); - h2_conn_io_flush(&session->io); - if (session->open_streams > 0) { - if (h2_mplx_awaits_data(session->mplx)) { - /* waiting for at least one stream to produce data */ - transit(session, "no io", H2_SESSION_ST_WAIT); - } - else { - /* we have streams open, and all are submitted and none - * is suspended. The only thing keeping us from WRITEing - * more must be the flow control. - * This means we only wait for WINDOW_UPDATE from the - * client and can block on READ. */ - transit(session, "no io (flow wait)", H2_SESSION_ST_IDLE); - /* Make sure we have flushed all previously written output - * so that the client will react. */ - if (h2_conn_io_flush(&session->io) != APR_SUCCESS) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); - return; - } - } - } - else if (session->local.accepting) { - /* When we have no streams, but accept new, switch to idle */ - transit(session, "no io (keepalive)", H2_SESSION_ST_IDLE); - } - else { - /* We are no longer accepting new streams and there are - * none left. Time to leave. */ - h2_session_shutdown(session, arg, msg, 0); - transit(session, "no io", H2_SESSION_ST_DONE); - } - break; - default: + case H2_SESSION_ST_DONE: /* nop */ break; - } -} - -static void h2_session_ev_frame_rcvd(h2_session *session, int arg, const char *msg) -{ - switch (session->state) { - case H2_SESSION_ST_IDLE: - case H2_SESSION_ST_WAIT: - transit(session, "frame received", H2_SESSION_ST_BUSY); - break; default: - /* nop */ + transit(session, "nghttp2 done", H2_SESSION_ST_DONE); break; } } -static void h2_session_ev_stream_change(h2_session *session, int arg, const char *msg) +static void h2_session_ev_mpm_stopping(h2_session *session, int arg, const char *msg) { switch (session->state) { - case H2_SESSION_ST_IDLE: - case H2_SESSION_ST_WAIT: - transit(session, "stream change", H2_SESSION_ST_BUSY); + case H2_SESSION_ST_DONE: + /* nop */ break; default: - /* nop */ + h2_session_shutdown_notice(session); +#if !AP_MODULE_MAGIC_AT_LEAST(20120211, 110) + h2_workers_graceful_shutdown(session->workers); +#endif break; } } -static void h2_session_ev_ngh2_done(h2_session *session, int arg, const char *msg) +static void h2_session_ev_pre_close(h2_session *session, int arg, const char *msg) { - switch (session->state) { - case H2_SESSION_ST_DONE: - /* nop */ - break; - default: - transit(session, "nghttp2 done", H2_SESSION_ST_DONE); - break; - } + h2_session_shutdown(session, arg, msg, 1); } -static void h2_session_ev_mpm_stopping(h2_session *session, int arg, const char *msg) +static void h2_session_ev_no_more_streams(h2_session *session) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_SSSN_LOG(APLOGNO(10304), session, "no more streams")); switch (session->state) { - case H2_SESSION_ST_DONE: - /* nop */ + case H2_SESSION_ST_BUSY: + case H2_SESSION_ST_WAIT: + if (!h2_session_want_send(session)) { + if (session->local.accepting) { + /* We wait for new frames on c1 only. */ + transit(session, "all streams done", H2_SESSION_ST_IDLE); + } + else { + /* We are no longer accepting new streams. + * Time to leave. */ + h2_session_shutdown(session, 0, "done", 0); + transit(session, "c1 done after goaway", H2_SESSION_ST_DONE); + } + } + else { + transit(session, "no more streams", H2_SESSION_ST_WAIT); + } break; default: - h2_session_shutdown_notice(session); + /* nop */ break; } } -static void h2_session_ev_pre_close(h2_session *session, int arg, const char *msg) +static void ev_stream_created(h2_session *session, h2_stream *stream) { - h2_session_shutdown(session, arg, msg, 1); + /* nop */ } static void ev_stream_open(h2_session *session, h2_stream *stream) { - h2_iq_append(session->in_process, stream->id); + if (H2_STREAM_CLIENT_INITIATED(stream->id)) { + ++session->remote.emitted_count; + if (stream->id > session->remote.emitted_max) { + session->remote.emitted_max = stream->id; + session->local.accepted_max = stream->id; + } + } + else { + if (stream->id > session->local.emitted_max) { + ++session->local.emitted_count; + session->remote.emitted_max = stream->id; + } + } + /* Stream state OPEN means we have received all request headers + * and can start processing the stream. */ + h2_iq_append(session->ready_to_process, stream->id); + update_child_status(session, SERVER_BUSY_READ, "schedule", stream); } static void ev_stream_closed(h2_session *session, h2_stream *stream) @@ -1900,75 +1617,72 @@ static void ev_stream_closed(h2_session *session, h2_stream *stream) && (stream->id > session->local.completed_max)) { session->local.completed_max = stream->id; } - switch (session->state) { - case H2_SESSION_ST_IDLE: - break; - default: - break; - } - /* The stream might have data in the buffers of the main connection. * We can only free the allocated resources once all had been written. * Send a special buckets on the connection that gets destroyed when * all preceding data has been handled. On its destruction, it is safe * to purge all resources of the stream. */ - b = h2_bucket_eos_create(session->c->bucket_alloc, stream); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_STRM_MSG(stream, "adding h2_eos to c1 out")); + b = h2_bucket_eos_create(session->c1->bucket_alloc, stream); APR_BRIGADE_INSERT_TAIL(session->bbtmp, b); - h2_conn_io_pass(&session->io, session->bbtmp); + h2_c1_io_append(&session->io, session->bbtmp); apr_brigade_cleanup(session->bbtmp); } static void on_stream_state_enter(void *ctx, h2_stream *stream) { h2_session *session = ctx; - /* stream entered a new state */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, H2_STRM_MSG(stream, "entered state")); switch (stream->state) { case H2_SS_IDLE: /* stream was created */ - ++session->open_streams; - if (H2_STREAM_CLIENT_INITIATED(stream->id)) { - ++session->remote.emitted_count; - if (stream->id > session->remote.emitted_max) { - session->remote.emitted_max = stream->id; - session->local.accepted_max = stream->id; - } - } - else { - if (stream->id > session->local.emitted_max) { - ++session->local.emitted_count; - session->remote.emitted_max = stream->id; - } - } + ev_stream_created(session, stream); break; case H2_SS_OPEN: /* stream has request headers */ - case H2_SS_RSVD_L: /* stream has request headers */ + case H2_SS_RSVD_L: ev_stream_open(session, stream); break; - case H2_SS_CLOSED_L: /* stream output was closed */ + case H2_SS_CLOSED_L: /* stream output was closed, but remote end is not */ + /* If the stream is still being processed, it could still be reading + * its input (theoretically, http request hangling does not normally). + * But when processing is done, we need to cancel the stream as no + * one is consuming the input any longer. + * This happens, for example, on a large POST when the response + * is ready early due to the POST being denied. */ + if (!h2_mplx_c1_stream_is_running(session->mplx, stream)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, + H2_STRM_LOG(APLOGNO(10305), stream, "remote close missing")); + nghttp2_submit_rst_stream(session->ngh2, NGHTTP2_FLAG_NONE, + stream->id, H2_ERR_NO_ERROR); + } break; case H2_SS_CLOSED_R: /* stream input was closed */ break; case H2_SS_CLOSED: /* stream in+out were closed */ - --session->open_streams; ev_stream_closed(session, stream); break; case H2_SS_CLEANUP: - h2_mplx_stream_cleanup(session->mplx, stream); + nghttp2_session_set_stream_user_data(session->ngh2, stream->id, NULL); + h2_mplx_c1_stream_cleanup(session->mplx, stream, &session->open_streams); + ++session->streams_done; + update_child_status(session, SERVER_BUSY_WRITE, "done", stream); break; default: break; } - dispatch_event(session, H2_SESSION_EV_STREAM_CHANGE, 0, "stream state change"); } -static void on_stream_event(void *ctx, h2_stream *stream, - h2_stream_event_t ev) +static void on_stream_event(void *ctx, h2_stream *stream, h2_stream_event_t ev) { h2_session *session = ctx; switch (ev) { case H2_SEV_IN_DATA_PENDING: - h2_iq_append(session->in_pending, stream->id); + session->input_flushed = 1; + break; + case H2_SEV_OUT_C1_BLOCK: + h2_iq_append(session->out_c1_blocked, stream->id); break; default: /* NOP */ @@ -1993,13 +1707,19 @@ static void on_stream_state_event(void *ctx, h2_stream *stream, } } -static void dispatch_event(h2_session *session, h2_session_event_t ev, - int arg, const char *msg) +void h2_session_dispatch_event(h2_session *session, h2_session_event_t ev, + apr_status_t arg, const char *msg) { switch (ev) { case H2_SESSION_EV_INIT: h2_session_ev_init(session, arg, msg); break; + case H2_SESSION_EV_INPUT_PENDING: + h2_session_ev_input_pending(session, arg, msg); + break; + case H2_SESSION_EV_INPUT_EXHAUSTED: + h2_session_ev_input_exhausted(session, arg, msg); + break; case H2_SESSION_EV_LOCAL_GOAWAY: h2_session_ev_local_goaway(session, arg, msg); break; @@ -2015,12 +1735,6 @@ static void dispatch_event(h2_session *session, h2_session_event_t ev, case H2_SESSION_EV_CONN_TIMEOUT: h2_session_ev_conn_timeout(session, arg, msg); break; - case H2_SESSION_EV_NO_IO: - h2_session_ev_no_io(session, arg, msg); - break; - case H2_SESSION_EV_FRAME_RCVD: - h2_session_ev_frame_rcvd(session, arg, msg); - break; case H2_SESSION_EV_NGH2_DONE: h2_session_ev_ngh2_done(session, arg, msg); break; @@ -2030,311 +1744,265 @@ static void dispatch_event(h2_session *session, h2_session_event_t ev, case H2_SESSION_EV_PRE_CLOSE: h2_session_ev_pre_close(session, arg, msg); break; - case H2_SESSION_EV_STREAM_CHANGE: - h2_session_ev_stream_change(session, arg, msg); + case H2_SESSION_EV_NO_MORE_STREAMS: + h2_session_ev_no_more_streams(session); break; default: - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c1, H2_SSSN_MSG(session, "unknown event %d"), ev); break; } } -/* trigger window updates, stream resumes and submits */ -static apr_status_t dispatch_master(h2_session *session) { - apr_status_t status; - - status = h2_mplx_dispatch_master_events(session->mplx, - on_stream_resume, session); - if (status == APR_EAGAIN) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, session->c, - H2_SSSN_MSG(session, "no master event available")); - } - else if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, session->c, - H2_SSSN_MSG(session, "dispatch error")); - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, - H2_ERR_INTERNAL_ERROR, "dispatch error"); +static void unblock_c1_out(h2_session *session) { + int sid; + + while ((sid = h2_iq_shift(session->out_c1_blocked)) > 0) { + nghttp2_session_resume_data(session->ngh2, sid); } - return status; } -static const int MAX_WAIT_MICROS = 200 * 1000; - apr_status_t h2_session_process(h2_session *session, int async) { apr_status_t status = APR_SUCCESS; - conn_rec *c = session->c; + conn_rec *c = session->c1; int rv, mpm_state, trace = APLOGctrace3(c); - apr_time_t now; - + if (trace) { ap_log_cerror( APLOG_MARK, APLOG_TRACE3, status, c, H2_SSSN_MSG(session, "process start, async=%d"), async); } - + + if (H2_SESSION_ST_INIT == session->state) { + if (!h2_protocol_is_acceptable_c1(c, session->r, 1)) { + const char *msg = nghttp2_strerror(NGHTTP2_INADEQUATE_SECURITY); + update_child_status(session, SERVER_BUSY_READ, msg, NULL); + h2_session_shutdown(session, APR_EINVAL, msg, 1); + } + else { + update_child_status(session, SERVER_BUSY_READ, "init", NULL); + status = h2_session_start(session, &rv); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, + H2_SSSN_LOG(APLOGNO(03079), session, + "started on %s:%d"), + session->s->server_hostname, + c->local_addr->port); + if (status != APR_SUCCESS) { + h2_session_dispatch_event(session, + H2_SESSION_EV_CONN_ERROR, status, NULL); + } + else { + h2_session_dispatch_event(session, H2_SESSION_EV_INIT, 0, NULL); + } + } + } + while (session->state != H2_SESSION_ST_DONE) { - now = apr_time_now(); - session->have_read = session->have_written = 0; - if (session->local.accepting + /* PR65731: we may get a new connection to process while the + * MPM already is stopping. For example due to having reached + * MaxRequestsPerChild limit. + * Since this is supposed to handle things gracefully, we need to: + * a) fully initialize the session before GOAWAYing + * b) give the client the chance to submit at least one request + */ + if (session->state != H2_SESSION_ST_INIT /* no longer intializing */ + && session->local.accepted_max > 0 /* have gotten at least one stream */ + && session->local.accepting /* have not already locally shut down */ && !ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state)) { if (mpm_state == AP_MPMQ_STOPPING) { - dispatch_event(session, H2_SESSION_EV_MPM_STOPPING, 0, NULL); + h2_session_dispatch_event(session, H2_SESSION_EV_MPM_STOPPING, 0, NULL); } } - + session->status[0] = '\0'; + if (h2_session_want_send(session)) { + h2_session_send(session); + } + else if (!nghttp2_session_want_read(session->ngh2)) { + h2_session_dispatch_event(session, H2_SESSION_EV_NGH2_DONE, 0, NULL); + } + + if (!h2_iq_empty(session->ready_to_process)) { + h2_mplx_c1_process(session->mplx, session->ready_to_process, + get_stream, stream_pri_cmp, session, + &session->open_streams); + transit(session, "scheduled stream", H2_SESSION_ST_BUSY); + } + + if (session->input_flushed) { + transit(session, "forwarded input", H2_SESSION_ST_BUSY); + session->input_flushed = 0; + } + + if (!h2_iq_empty(session->out_c1_blocked)) { + unblock_c1_out(session); + transit(session, "unblocked output", H2_SESSION_ST_BUSY); + } + + if (session->reprioritize) { + h2_mplx_c1_reprioritize(session->mplx, stream_pri_cmp, session); + session->reprioritize = 0; + } + + if (h2_session_want_send(session)) { + h2_session_send(session); + } + + status = h2_c1_io_assure_flushed(&session->io); + if (APR_SUCCESS != status) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, status, NULL); + } + switch (session->state) { - case H2_SESSION_ST_INIT: - ap_update_child_status_from_conn(c->sbh, SERVER_BUSY_READ, c); - if (!h2_is_acceptable_connection(c, 1)) { - update_child_status(session, SERVER_BUSY_READ, - "inadequate security"); - h2_session_shutdown(session, - NGHTTP2_INADEQUATE_SECURITY, NULL, 1); - } - else { - update_child_status(session, SERVER_BUSY_READ, "init"); - status = h2_session_start(session, &rv); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, - H2_SSSN_LOG(APLOGNO(03079), session, - "started on %s:%d"), - session->s->server_hostname, - c->local_addr->port); - if (status != APR_SUCCESS) { - dispatch_event(session, - H2_SESSION_EV_CONN_ERROR, 0, NULL); - } - dispatch_event(session, H2_SESSION_EV_INIT, 0, NULL); - } - break; - - case H2_SESSION_ST_IDLE: - if (session->idle_until && (apr_time_now() + session->idle_delay) > session->idle_until) { - ap_log_cerror( APLOG_MARK, APLOG_TRACE1, status, c, - H2_SSSN_MSG(session, "idle, timeout reached, closing")); - if (session->idle_delay) { - apr_table_setn(session->c->notes, "short-lingering-close", "1"); - } - dispatch_event(session, H2_SESSION_EV_CONN_TIMEOUT, 0, "timeout"); - goto out; - } - - if (session->idle_delay) { - /* we are less interested in spending time on this connection */ - ap_log_cerror( APLOG_MARK, APLOG_TRACE2, status, c, - H2_SSSN_MSG(session, "session is idle (%ld ms), idle wait %ld sec left"), - (long)apr_time_as_msec(session->idle_delay), - (long)apr_time_sec(session->idle_until - now)); - apr_sleep(session->idle_delay); - session->idle_delay = 0; - } + case H2_SESSION_ST_INIT: + ap_assert(0); + h2_c1_read(session); + break; - h2_conn_io_flush(&session->io); - if (async && !session->r && (now > session->idle_sync_until)) { - if (trace) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, c, - H2_SSSN_MSG(session, - "nonblock read, %d streams open"), - session->open_streams); - } - status = h2_session_read(session, 0); - - if (status == APR_SUCCESS) { - session->have_read = 1; - } - else if (APR_STATUS_IS_EAGAIN(status) || APR_STATUS_IS_TIMEUP(status)) { - status = APR_EAGAIN; - goto out; - } - else { + case H2_SESSION_ST_IDLE: + ap_assert(session->open_streams == 0); + ap_assert(nghttp2_session_want_read(session->ngh2)); + if (!h2_session_want_send(session)) { + /* Give any new incoming request a short grace period to + * arrive while we are still hot and return to the mpm + * connection handling when nothing really happened. */ + h2_c1_read(session); + if (H2_SESSION_ST_IDLE == session->state) { + if (async) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, c, - H2_SSSN_LOG(APLOGNO(03403), session, - "no data, error")); - dispatch_event(session, - H2_SESSION_EV_CONN_ERROR, 0, "timeout"); - } - } - else { - /* make certain, we send everything before we idle */ - if (trace) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, c, - H2_SSSN_MSG(session, - "sync, stutter 1-sec, %d streams open"), - session->open_streams); - } - /* We wait in smaller increments, using a 1 second timeout. - * That gives us the chance to check for MPMQ_STOPPING often. - */ - status = h2_mplx_idle(session->mplx); - if (status == APR_EAGAIN) { - break; - } - else if (status != APR_SUCCESS) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, - H2_ERR_ENHANCE_YOUR_CALM, "less is more"); - } - h2_filter_cin_timeout_set(session->cin, apr_time_from_sec(1)); - status = h2_session_read(session, 1); - if (status == APR_SUCCESS) { - session->have_read = 1; - } - else if (status == APR_EAGAIN) { - /* nothing to read */ - } - else if (APR_STATUS_IS_TIMEUP(status)) { - /* continue reading handling */ - } - else if (APR_STATUS_IS_ECONNABORTED(status) - || APR_STATUS_IS_ECONNRESET(status) - || APR_STATUS_IS_EOF(status) - || APR_STATUS_IS_EBADF(status)) { - ap_log_cerror( APLOG_MARK, APLOG_TRACE3, status, c, - H2_SSSN_MSG(session, "input gone")); - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); + H2_SSSN_LOG(APLOGNO(10306), session, + "returning to mpm c1 monitoring")); + goto leaving; } else { - ap_log_cerror( APLOG_MARK, APLOG_TRACE3, status, c, - H2_SSSN_MSG(session, - "(1 sec timeout) read failed")); - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, "error"); - } - } - if (nghttp2_session_want_write(session->ngh2)) { - ap_update_child_status(session->c->sbh, SERVER_BUSY_WRITE, NULL); - status = h2_session_send(session); - if (status == APR_SUCCESS) { - status = h2_conn_io_flush(&session->io); - } - if (status != APR_SUCCESS) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, - H2_ERR_INTERNAL_ERROR, "writing"); - break; + /* Not an async mpm, we must continue waiting + * for client data to arrive until the configured + * server Timeout/KeepAliveTimeout happens */ + apr_time_t timeout = (session->open_streams == 0)? + session->s->keep_alive_timeout : + session->s->timeout; + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, c, + H2_SSSN_MSG(session, "polling timeout=%d"), + (int)apr_time_sec(timeout)); + status = h2_mplx_c1_poll(session->mplx, timeout, + on_stream_input, + on_stream_output, session); + if (APR_STATUS_IS_TIMEUP(status)) { + if (session->open_streams == 0) { + h2_session_dispatch_event(session, + H2_SESSION_EV_CONN_TIMEOUT, status, NULL); + break; + } + } + else if (APR_SUCCESS != status) { + h2_session_dispatch_event(session, + H2_SESSION_EV_CONN_ERROR, status, NULL); + break; + } } } + } + else { + transit(session, "c1 io pending", H2_SESSION_ST_BUSY); + } + break; + + case H2_SESSION_ST_BUSY: + /* IO happening in and out. Make sure we react to c2 events + * inbetween send and receive. */ + status = h2_mplx_c1_poll(session->mplx, 0, + on_stream_input, on_stream_output, session); + if (APR_SUCCESS != status && !APR_STATUS_IS_TIMEUP(status)) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, status, NULL); break; - - case H2_SESSION_ST_BUSY: - if (nghttp2_session_want_read(session->ngh2)) { - ap_update_child_status(session->c->sbh, SERVER_BUSY_READ, NULL); - h2_filter_cin_timeout_set(session->cin, session->s->timeout); - status = h2_session_read(session, 0); - if (status == APR_SUCCESS) { - session->have_read = 1; - } - else if (status == APR_EAGAIN) { - /* nothing to read */ - } - else if (APR_STATUS_IS_TIMEUP(status)) { - dispatch_event(session, H2_SESSION_EV_CONN_TIMEOUT, 0, NULL); - break; - } - else { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); - } - } + } + h2_c1_read(session); + break; - status = dispatch_master(session); - if (status != APR_SUCCESS && status != APR_EAGAIN) { + case H2_SESSION_ST_WAIT: + status = h2_c1_io_assure_flushed(&session->io); + if (APR_SUCCESS != status) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, status, NULL); + break; + } + if (session->open_streams == 0) { + h2_session_dispatch_event(session, H2_SESSION_EV_NO_MORE_STREAMS, + 0, "streams really done"); + if (session->state != H2_SESSION_ST_WAIT) { break; } - - if (nghttp2_session_want_write(session->ngh2)) { - ap_update_child_status(session->c->sbh, SERVER_BUSY_WRITE, NULL); - status = h2_session_send(session); - if (status == APR_SUCCESS) { - status = h2_conn_io_flush(&session->io); - } - if (status != APR_SUCCESS) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, - H2_ERR_INTERNAL_ERROR, "writing"); - break; - } - } - - if (session->have_read || session->have_written) { - if (session->wait_us) { - session->wait_us = 0; - } - } - else if (!nghttp2_session_want_write(session->ngh2)) { - dispatch_event(session, H2_SESSION_EV_NO_IO, 0, NULL); + } + /* No IO happening and input is exhausted. Make sure we have + * flushed any possibly pending output and then wait with + * the c1 connection timeout for sth to happen in our c1/c2 sockets/pipes */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, c, + H2_SSSN_MSG(session, "polling timeout=%d, open_streams=%d"), + (int)apr_time_sec(session->s->timeout), session->open_streams); + status = h2_mplx_c1_poll(session->mplx, session->s->timeout, + on_stream_input, on_stream_output, session); + if (APR_STATUS_IS_TIMEUP(status)) { + /* If we timeout without streams open, no new request from client + * arrived. + * If we timeout without nghttp2 wanting to write something, but + * all open streams have something to send, it means we are + * blocked on HTTP/2 flow control and the client did not send + * WINDOW_UPDATEs to us. */ + if (session->open_streams == 0 || + (!h2_session_want_send(session) && + h2_mplx_c1_all_streams_want_send_data(session->mplx))) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_TIMEOUT, status, NULL); + break; } + } + else if (APR_SUCCESS != status) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, status, NULL); break; - - case H2_SESSION_ST_WAIT: - if (session->wait_us <= 0) { - session->wait_us = 10; - if (h2_conn_io_flush(&session->io) != APR_SUCCESS) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); - break; - } - } - else { - /* repeating, increase timer for graceful backoff */ - session->wait_us = H2MIN(session->wait_us*2, MAX_WAIT_MICROS); - } + } + break; - if (trace) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, c, - "h2_session: wait for data, %ld micros", - (long)session->wait_us); - } - status = h2_mplx_out_trywait(session->mplx, session->wait_us, - session->iowait); - if (status == APR_SUCCESS) { - session->wait_us = 0; - dispatch_event(session, H2_SESSION_EV_STREAM_CHANGE, 0, NULL); - } - else if (APR_STATUS_IS_TIMEUP(status)) { - /* go back to checking all inputs again */ - transit(session, "wait cycle", session->local.shutdown? - H2_SESSION_ST_DONE : H2_SESSION_ST_BUSY); - } - else if (APR_STATUS_IS_ECONNRESET(status) - || APR_STATUS_IS_ECONNABORTED(status)) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, c, - H2_SSSN_LOG(APLOGNO(03404), session, - "waiting on conditional")); - h2_session_shutdown(session, H2_ERR_INTERNAL_ERROR, - "cond wait error", 0); - } - break; - - default: - ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, c, - H2_SSSN_LOG(APLOGNO(03080), session, - "unknown state")); - dispatch_event(session, H2_SESSION_EV_PROTO_ERROR, 0, NULL); - break; - } + case H2_SESSION_ST_DONE: + h2_c1_read(session); + break; - if (!nghttp2_session_want_read(session->ngh2) - && !nghttp2_session_want_write(session->ngh2)) { - dispatch_event(session, H2_SESSION_EV_NGH2_DONE, 0, NULL); - } - if (session->reprioritize) { - h2_mplx_reprioritize(session->mplx, stream_pri_cmp, session); - session->reprioritize = 0; + default: + ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, c, + H2_SSSN_LOG(APLOGNO(03080), session, + "unknown state")); + h2_session_dispatch_event(session, H2_SESSION_EV_PROTO_ERROR, APR_EGENERAL, NULL); + break; } } - -out: + +leaving: if (trace) { ap_log_cerror( APLOG_MARK, APLOG_TRACE3, status, c, H2_SSSN_MSG(session, "process returns")); } - - if ((session->state != H2_SESSION_ST_DONE) - && (APR_STATUS_IS_EOF(status) + h2_mplx_c1_going_keepalive(session->mplx); + + if (session->state == H2_SESSION_ST_DONE) { + if (session->local.error) { + char buffer[128]; + const char *msg; + if (session->local.error_msg) { + msg = session->local.error_msg; + } + else { + msg = apr_strerror(session->local.error, buffer, sizeof(buffer)); + } + update_child_status(session, SERVER_CLOSING, msg, NULL); + } + else { + update_child_status(session, SERVER_CLOSING, "done", NULL); + } + } + else if (APR_STATUS_IS_EOF(status) || APR_STATUS_IS_ECONNRESET(status) - || APR_STATUS_IS_ECONNABORTED(status))) { - dispatch_event(session, H2_SESSION_EV_CONN_ERROR, 0, NULL); + || APR_STATUS_IS_ECONNABORTED(status)) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, status, NULL); + update_child_status(session, SERVER_CLOSING, "error", NULL); } return (session->state == H2_SESSION_ST_DONE)? APR_EOF : APR_SUCCESS; @@ -2344,14 +2012,14 @@ apr_status_t h2_session_pre_close(h2_session *session, int async) { apr_status_t status; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c1, H2_SSSN_MSG(session, "pre_close")); - dispatch_event(session, H2_SESSION_EV_PRE_CLOSE, 0, + h2_session_dispatch_event(session, H2_SESSION_EV_PRE_CLOSE, 0, (session->state == H2_SESSION_ST_IDLE)? "timeout" : NULL); status = session_cleanup(session, "pre_close"); if (status == APR_SUCCESS) { /* no one should hold a reference to this session any longer and - * the h2_ctx was removed from the connection. + * the h2_conn_ctx_twas removed from the connection. * Take the pool (and thus all subpools etc. down now, instead of * during cleanup of main connection pool. */ apr_pool_destroy(session->pool); diff --git a/modules/http2/h2_session.h b/modules/http2/h2_session.h index df2a862..3328509 100644 --- a/modules/http2/h2_session.h +++ b/modules/http2/h2_session.h @@ -17,25 +17,15 @@ #ifndef __mod_h2__h2_session__ #define __mod_h2__h2_session__ -#include "h2_conn_io.h" +#include "h2_c1_io.h" /** * A HTTP/2 connection, a session with a specific client. * * h2_session sits on top of a httpd conn_rec* instance and takes complete * control of the connection data. It receives protocol frames from the - * client. For new HTTP/2 streams it creates h2_task(s) that are sent - * via callback to a dispatcher (see h2_conn.c). - * h2_session keeps h2_io's for each ongoing stream which buffer the - * payload for that stream. - * - * New incoming HEADER frames are converted into a h2_stream+h2_task instance - * that both represent a HTTP/2 stream, but may have separate lifetimes. This - * allows h2_task to be scheduled in other threads without semaphores - * all over the place. It allows task memory to be freed independent of - * session lifetime and sessions may close down while tasks are still running. - * - * + * client. For new HTTP/2 streams it creates secondary connections + * to execute the requests in h2 workers. */ #include "h2.h" @@ -44,7 +34,6 @@ struct apr_thread_mutext_t; struct apr_thread_cond_t; struct h2_ctx; struct h2_config; -struct h2_filter_cin; struct h2_ihash_t; struct h2_mplx; struct h2_priority; @@ -53,39 +42,39 @@ struct h2_push_diary; struct h2_session; struct h2_stream; struct h2_stream_monitor; -struct h2_task; struct h2_workers; struct nghttp2_session; typedef enum { H2_SESSION_EV_INIT, /* session was initialized */ + H2_SESSION_EV_INPUT_PENDING, /* c1 input may have data pending */ + H2_SESSION_EV_INPUT_EXHAUSTED, /* c1 input exhausted */ H2_SESSION_EV_LOCAL_GOAWAY, /* we send a GOAWAY */ H2_SESSION_EV_REMOTE_GOAWAY, /* remote send us a GOAWAY */ H2_SESSION_EV_CONN_ERROR, /* connection error */ H2_SESSION_EV_PROTO_ERROR, /* protocol error */ H2_SESSION_EV_CONN_TIMEOUT, /* connection timeout */ - H2_SESSION_EV_NO_IO, /* nothing has been read or written */ - H2_SESSION_EV_FRAME_RCVD, /* a frame has been received */ H2_SESSION_EV_NGH2_DONE, /* nghttp2 wants neither read nor write anything */ H2_SESSION_EV_MPM_STOPPING, /* the process is stopping */ H2_SESSION_EV_PRE_CLOSE, /* connection will close after this */ - H2_SESSION_EV_STREAM_CHANGE, /* a stream (state/input/output) changed */ + H2_SESSION_EV_NO_MORE_STREAMS, /* no more streams to process */ } h2_session_event_t; typedef struct h2_session { - long id; /* identifier of this session, unique - * inside a httpd process */ - conn_rec *c; /* the connection this session serves */ + int child_num; /* child number this session runs in */ + apr_uint32_t id; /* identifier of this session, unique per child */ + conn_rec *c1; /* the main connection this session serves */ request_rec *r; /* the request that started this in case * of 'h2c', NULL otherwise */ server_rec *s; /* server/vhost we're starting on */ - const struct h2_config *config; /* Relevant config for this session */ apr_pool_t *pool; /* pool to use in session */ struct h2_mplx *mplx; /* multiplexer for stream data */ - struct h2_workers *workers; /* for executing stream tasks */ - struct h2_filter_cin *cin; /* connection input filter context */ - h2_conn_io io; /* io on httpd conn filters */ + struct h2_workers *workers; /* for executing streams */ + struct h2_c1_io_in_ctx_t *cin; /* connection input filter context */ + h2_c1_io io; /* io on httpd conn filters */ + unsigned int padding_max; /* max number of padding bytes */ + int padding_always; /* padding has precedence over I/O optimizations */ struct nghttp2_session *ngh2; /* the nghttp2 session (internal use) */ h2_session_state state; /* state session is in */ @@ -95,43 +84,39 @@ typedef struct h2_session { unsigned int reprioritize : 1; /* scheduled streams priority changed */ unsigned int flush : 1; /* flushing output necessary */ - unsigned int have_read : 1; /* session has read client data */ - unsigned int have_written : 1; /* session did write data to client */ apr_interval_time_t wait_us; /* timeout during BUSY_WAIT state, micro secs */ struct h2_push_diary *push_diary; /* remember pushes, avoid duplicates */ struct h2_stream_monitor *monitor;/* monitor callbacks for streams */ - int open_streams; /* number of client streams open */ - int unsent_submits; /* number of submitted, but not yet written responses. */ - int unsent_promises; /* number of submitted, but not yet written push promises */ - - int responses_submitted; /* number of http/2 responses submitted */ - int streams_reset; /* number of http/2 streams reset by client */ - int pushes_promised; /* number of http/2 push promises submitted */ - int pushes_submitted; /* number of http/2 pushed responses submitted */ - int pushes_reset; /* number of http/2 pushed reset by client */ + unsigned int open_streams; /* number of streams processing */ + + unsigned int streams_done; /* number of http/2 streams handled */ + unsigned int responses_submitted; /* number of http/2 responses submitted */ + unsigned int streams_reset; /* number of http/2 streams reset by client */ + unsigned int pushes_promised; /* number of http/2 push promises submitted */ + unsigned int pushes_submitted; /* number of http/2 pushed responses submitted */ + unsigned int pushes_reset; /* number of http/2 pushed reset by client */ apr_size_t frames_received; /* number of http/2 frames received */ apr_size_t frames_sent; /* number of http/2 frames sent */ apr_size_t max_stream_count; /* max number of open streams */ apr_size_t max_stream_mem; /* max buffer memory for a single stream */ - - apr_time_t idle_until; /* Time we shut down due to sheer boredom */ - apr_time_t idle_sync_until; /* Time we sync wait until keepalive handling kicks in */ + apr_size_t max_data_frame_len; /* max amount of bytes for a single DATA frame */ + apr_size_t idle_frames; /* number of rcvd frames that kept session in idle state */ apr_interval_time_t idle_delay; /* Time we delay processing rcvd frames in idle state */ apr_bucket_brigade *bbtmp; /* brigade for keeping temporary data */ - struct apr_thread_cond_t *iowait; /* our cond when trywaiting for data */ - + char status[64]; /* status message for scoreboard */ int last_status_code; /* the one already reported */ const char *last_status_msg; /* the one already reported */ - - struct h2_iqueue *in_pending; /* all streams with input pending */ - struct h2_iqueue *in_process; /* all streams ready for processing on slave */ + + int input_flushed; /* stream input was flushed */ + struct h2_iqueue *out_c1_blocked; /* all streams with output blocked on c1 buffer full */ + struct h2_iqueue *ready_to_process; /* all streams ready for processing */ } h2_session; @@ -142,29 +127,17 @@ const char *h2_session_state_str(h2_session_state state); * The session will apply the configured parameter. * @param psession pointer receiving the created session on success or NULL * @param c the connection to work on + * @param r optional request when protocol was upgraded * @param cfg the module config to apply * @param workers the worker pool to use * @return the created session */ apr_status_t h2_session_create(h2_session **psession, - conn_rec *c, struct h2_ctx *ctx, + conn_rec *c, request_rec *r, server_rec *, struct h2_workers *workers); -/** - * Create a new h2_session for the given request. - * The session will apply the configured parameter. - * @param psession pointer receiving the created session on success or NULL - * @param r the request that was upgraded - * @param cfg the module config to apply - * @param workers the worker pool to use - * @return the created session - */ -apr_status_t h2_session_rcreate(h2_session **psession, - request_rec *r, struct h2_ctx *ctx, - struct h2_workers *workers); - void h2_session_event(h2_session *session, h2_session_event_t ev, - int err, const char *msg); + int err, const char *msg); /** * Process the given HTTP/2 session until it is ended or a fatal @@ -188,22 +161,12 @@ apr_status_t h2_session_pre_close(h2_session *session, int async); void h2_session_abort(h2_session *session, apr_status_t reason); /** - * Close and deallocate the given session. - */ -void h2_session_close(h2_session *session); - -/** * Returns if client settings have push enabled. * @param != 0 iff push is enabled in client settings */ int h2_session_push_enabled(h2_session *session); /** - * Look up the stream in this session with the given id. - */ -struct h2_stream *h2_session_stream_get(h2_session *session, int stream_id); - -/** * Submit a push promise on the stream and schedule the new steam for * processing.. * @@ -219,10 +182,25 @@ apr_status_t h2_session_set_prio(h2_session *session, struct h2_stream *stream, const struct h2_priority *prio); +/** + * Dispatch a event happending during session processing. + * @param session the sessiont + * @param ev the event that happened + * @param arg integer argument (event type dependant) + * @param msg destriptive message + */ +void h2_session_dispatch_event(h2_session *session, h2_session_event_t ev, + int arg, const char *msg); + + #define H2_SSSN_MSG(s, msg) \ - "h2_session(%ld,%s,%d): "msg, s->id, h2_session_state_str(s->state), \ + "h2_session(%d-%lu,%s,%d): "msg, s->child_num, (unsigned long)s->id, \ + h2_session_state_str(s->state), \ s->open_streams #define H2_SSSN_LOG(aplogno, s, msg) aplogno H2_SSSN_MSG(s, msg) +#define H2_SSSN_STRM_MSG(s, stream_id, msg) \ + "h2_stream(%d-%lu-%d): "msg, s->child_num, (unsigned long)s->id, stream_id + #endif /* defined(__mod_h2__h2_session__) */ diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c index 24ebc56..ee87555 100644 --- a/modules/http2/h2_stream.c +++ b/modules/http2/h2_stream.c @@ -17,34 +17,39 @@ #include <assert.h> #include <stddef.h> -#include <apr_strings.h> +#include "apr.h" +#include "apr_strings.h" +#include "apr_lib.h" +#include "apr_strmatch.h" #include <httpd.h> #include <http_core.h> #include <http_connection.h> #include <http_log.h> +#include <http_protocol.h> +#include <http_ssl.h> #include <nghttp2/nghttp2.h> #include "h2_private.h" #include "h2.h" #include "h2_bucket_beam.h" -#include "h2_conn.h" +#include "h2_c1.h" #include "h2_config.h" -#include "h2_h2.h" +#include "h2_protocol.h" #include "h2_mplx.h" #include "h2_push.h" #include "h2_request.h" #include "h2_headers.h" #include "h2_session.h" #include "h2_stream.h" -#include "h2_task.h" -#include "h2_ctx.h" -#include "h2_task.h" +#include "h2_c2.h" +#include "h2_conn_ctx.h" +#include "h2_c2.h" #include "h2_util.h" -static const char *h2_ss_str(h2_stream_state_t state) +static const char *h2_ss_str(const h2_stream_state_t state) { switch (state) { case H2_SS_IDLE: @@ -68,7 +73,7 @@ static const char *h2_ss_str(h2_stream_state_t state) } } -const char *h2_stream_state_str(h2_stream *stream) +const char *h2_stream_state_str(const h2_stream *stream) { return h2_ss_str(stream->state); } @@ -120,7 +125,8 @@ static int trans_on_event[][H2_SS_MAX] = { { S_XXX, S_ERR, S_ERR, S_CL_L, S_CLS, S_XXX, S_XXX, S_XXX, },/* EV_CLOSED_L*/ { S_ERR, S_ERR, S_ERR, S_CL_R, S_ERR, S_CLS, S_NOP, S_NOP, },/* EV_CLOSED_R*/ { S_CLS, S_CLS, S_CLS, S_CLS, S_CLS, S_CLS, S_NOP, S_NOP, },/* EV_CANCELLED*/ -{ S_NOP, S_XXX, S_XXX, S_XXX, S_XXX, S_CLS, S_CLN, S_XXX, },/* EV_EOS_SENT*/ +{ S_NOP, S_XXX, S_XXX, S_XXX, S_XXX, S_CLS, S_CLN, S_NOP, },/* EV_EOS_SENT*/ +{ S_NOP, S_XXX, S_CLS, S_XXX, S_XXX, S_CLS, S_XXX, S_XXX, },/* EV_IN_ERROR*/ }; static int on_map(h2_stream_state_t state, int map[H2_SS_MAX]) @@ -142,7 +148,7 @@ static int on_frame(h2_stream_state_t state, int frame_type, { ap_assert(frame_type >= 0); ap_assert(state >= 0); - if (frame_type >= maxlen) { + if ((apr_size_t)frame_type >= maxlen) { return state; /* NOP, ignore unknown frame types */ } return on_map(state, frame_map[frame_type]); @@ -160,6 +166,7 @@ static int on_frame_recv(h2_stream_state_t state, int frame_type) static int on_event(h2_stream* stream, h2_stream_event_t ev) { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); if (stream->monitor && stream->monitor->on_event) { stream->monitor->on_event(stream->monitor->ctx, stream, ev); } @@ -169,10 +176,18 @@ static int on_event(h2_stream* stream, h2_stream_event_t ev) return stream->state; } +static ssize_t stream_data_cb(nghttp2_session *ng2s, + int32_t stream_id, + uint8_t *buf, + size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, + void *puser); + static void H2_STREAM_OUT_LOG(int lvl, h2_stream *s, const char *tag) { - if (APLOG_C_IS_LEVEL(s->session->c, lvl)) { - conn_rec *c = s->session->c; + if (APLOG_C_IS_LEVEL(s->session->c1, lvl)) { + conn_rec *c = s->session->c1; char buffer[4 * 1024]; apr_size_t len, bmax = sizeof(buffer)/sizeof(buffer[0]); @@ -182,76 +197,116 @@ static void H2_STREAM_OUT_LOG(int lvl, h2_stream *s, const char *tag) } } -static apr_status_t setup_input(h2_stream *stream) { - if (stream->input == NULL) { - int empty = (stream->input_eof - && (!stream->in_buffer - || APR_BRIGADE_EMPTY(stream->in_buffer))); - if (!empty) { - h2_beam_create(&stream->input, stream->pool, stream->id, - "input", H2_BEAM_OWNER_SEND, 0, - stream->session->s->timeout); - h2_beam_send_from(stream->input, stream->pool); - } +static void stream_setup_input(h2_stream *stream) +{ + if (stream->input != NULL) return; + ap_assert(!stream->input_closed); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c1, + H2_STRM_MSG(stream, "setup input beam")); + h2_beam_create(&stream->input, stream->session->c1, + stream->pool, stream->id, + "input", 0, stream->session->s->timeout); +} + +apr_status_t h2_stream_prepare_processing(h2_stream *stream) +{ + /* Right before processing starts, last chance to decide if + * there is need to an input beam. */ + if (!stream->input_closed) { + stream_setup_input(stream); } return APR_SUCCESS; } -static apr_status_t close_input(h2_stream *stream) +static int input_buffer_is_empty(h2_stream *stream) +{ + return !stream->in_buffer || APR_BRIGADE_EMPTY(stream->in_buffer); +} + +static apr_status_t input_flush(h2_stream *stream) { - conn_rec *c = stream->session->c; apr_status_t status = APR_SUCCESS; + apr_off_t written; - stream->input_eof = 1; - if (stream->input && h2_beam_is_closed(stream->input)) { - return APR_SUCCESS; - } - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, - H2_STRM_MSG(stream, "closing input")); - if (stream->rst_error) { - return APR_ECONNRESET; + if (input_buffer_is_empty(stream)) goto cleanup; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c1, + H2_STRM_MSG(stream, "flush input")); + status = h2_beam_send(stream->input, stream->session->c1, + stream->in_buffer, APR_BLOCK_READ, &written); + stream->in_last_write = apr_time_now(); + if (APR_SUCCESS != status && h2_stream_is_at(stream, H2_SS_CLOSED_L)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, stream->session->c1, + H2_STRM_MSG(stream, "send input error")); + h2_stream_dispatch(stream, H2_SEV_IN_ERROR); } - - if (stream->trailers && !apr_is_empty_table(stream->trailers)) { - apr_bucket *b; - h2_headers *r; - - if (!stream->in_buffer) { - stream->in_buffer = apr_brigade_create(stream->pool, c->bucket_alloc); - } - - r = h2_headers_create(HTTP_OK, stream->trailers, NULL, - stream->in_trailer_octets, stream->pool); - stream->trailers = NULL; - b = h2_bucket_headers_create(c->bucket_alloc, r); - APR_BRIGADE_INSERT_TAIL(stream->in_buffer, b); - - b = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(stream->in_buffer, b); - - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, - H2_STRM_MSG(stream, "added trailers")); - h2_stream_dispatch(stream, H2_SEV_IN_DATA_PENDING); +cleanup: + return status; +} + +static void input_append_bucket(h2_stream *stream, apr_bucket *b) +{ + if (!stream->in_buffer) { + stream_setup_input(stream); + stream->in_buffer = apr_brigade_create( + stream->pool, stream->session->c1->bucket_alloc); } - if (stream->input) { - h2_stream_flush_input(stream); - return h2_beam_close(stream->input); + APR_BRIGADE_INSERT_TAIL(stream->in_buffer, b); +} + +static void input_append_data(h2_stream *stream, const char *data, apr_size_t len) +{ + if (!stream->in_buffer) { + stream_setup_input(stream); + stream->in_buffer = apr_brigade_create( + stream->pool, stream->session->c1->bucket_alloc); } - return status; + apr_brigade_write(stream->in_buffer, NULL, NULL, data, len); } -static apr_status_t close_output(h2_stream *stream) + +static apr_status_t close_input(h2_stream *stream) { - if (!stream->output || h2_beam_is_closed(stream->output)) { - return APR_SUCCESS; + conn_rec *c = stream->session->c1; + apr_status_t rv = APR_SUCCESS; + apr_bucket *b; + + if (stream->input_closed) goto cleanup; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, + H2_STRM_MSG(stream, "closing input")); + if (!stream->rst_error + && stream->trailers_in + && !apr_is_empty_table(stream->trailers_in)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c1, + H2_STRM_MSG(stream, "adding trailers")); +#if AP_HAS_RESPONSE_BUCKETS + b = ap_bucket_headers_create(stream->trailers_in, + stream->pool, c->bucket_alloc); +#else + b = h2_bucket_headers_create(c->bucket_alloc, + h2_headers_create(HTTP_OK, stream->trailers_in, NULL, + stream->in_trailer_octets, stream->pool)); +#endif + input_append_bucket(stream, b); + stream->trailers_in = NULL; + } + + stream->input_closed = 1; + if (stream->input) { + b = apr_bucket_eos_create(c->bucket_alloc); + input_append_bucket(stream, b); + input_flush(stream); + h2_stream_dispatch(stream, H2_SEV_IN_DATA_PENDING); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c1, + H2_STRM_MSG(stream, "input flush + EOS")); } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, - H2_STRM_MSG(stream, "closing output")); - return h2_beam_leave(stream->output); + +cleanup: + return rv; } -static void on_state_enter(h2_stream *stream) +static void on_state_enter(h2_stream *stream) { if (stream->monitor && stream->monitor->on_state_enter) { stream->monitor->on_state_enter(stream->monitor->ctx, stream); @@ -271,7 +326,7 @@ static void on_state_invalid(h2_stream *stream) stream->monitor->on_state_invalid(stream->monitor->ctx, stream); } /* stream got an event/frame invalid in its state */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, H2_STRM_MSG(stream, "invalid state event")); switch (stream->state) { case H2_SS_OPEN: @@ -288,17 +343,17 @@ static void on_state_invalid(h2_stream *stream) static apr_status_t transit(h2_stream *stream, int new_state) { - if (new_state == stream->state) { + if ((h2_stream_state_t)new_state == stream->state) { return APR_SUCCESS; } else if (new_state < 0) { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, stream->session->c1, H2_STRM_LOG(APLOGNO(03081), stream, "invalid transition")); on_state_invalid(stream); return APR_EINVAL; } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, H2_STRM_MSG(stream, "transit to [%s]"), h2_ss_str(new_state)); stream->state = new_state; switch (new_state) { @@ -312,14 +367,12 @@ static apr_status_t transit(h2_stream *stream, int new_state) case H2_SS_OPEN: break; case H2_SS_CLOSED_L: - close_output(stream); break; case H2_SS_CLOSED_R: close_input(stream); break; case H2_SS_CLOSED: close_input(stream); - close_output(stream); if (stream->out_buffer) { apr_brigade_cleanup(stream->out_buffer); } @@ -340,19 +393,20 @@ void h2_stream_dispatch(h2_stream *stream, h2_stream_event_t ev) { int new_state; - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c1, H2_STRM_MSG(stream, "dispatch event %d"), ev); new_state = on_event(stream, ev); if (new_state < 0) { - ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, stream->session->c1, H2_STRM_LOG(APLOGNO(10002), stream, "invalid event %d"), ev); on_state_invalid(stream); AP_DEBUG_ASSERT(new_state > S_XXX); return; } - else if (new_state == stream->state) { + else if ((h2_stream_state_t)new_state == stream->state) { /* nop */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c1, H2_STRM_MSG(stream, "non-state event %d"), ev); return; } @@ -365,9 +419,7 @@ void h2_stream_dispatch(h2_stream *stream, h2_stream_event_t ev) static void set_policy_for(h2_stream *stream, h2_request *r) { int enabled = h2_session_push_enabled(stream->session); - stream->push_policy = h2_push_policy_determine(r->headers, stream->pool, - enabled); - r->serialize = h2_config_geti(stream->session->config, H2_CONF_SER_HEADERS); + stream->push_policy = h2_push_policy_determine(r->headers, stream->pool, enabled); } apr_status_t h2_stream_send_frame(h2_stream *stream, int ftype, int flags, size_t frame_len) @@ -375,9 +427,10 @@ apr_status_t h2_stream_send_frame(h2_stream *stream, int ftype, int flags, size_ apr_status_t status = APR_SUCCESS; int new_state, eos = 0; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); new_state = on_frame_send(stream->state, ftype); if (new_state < 0) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, H2_STRM_MSG(stream, "invalid frame %d send"), ftype); AP_DEBUG_ASSERT(new_state > S_XXX); return transit(stream, new_state); @@ -385,6 +438,12 @@ apr_status_t h2_stream_send_frame(h2_stream *stream, int ftype, int flags, size_ ++stream->out_frames; stream->out_frame_octets += frame_len; + if(stream->c2) { + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(stream->c2); + if(conn_ctx) + conn_ctx->bytes_sent = stream->out_frame_octets; + } + switch (ftype) { case NGHTTP2_DATA: eos = (flags & NGHTTP2_FLAG_END_STREAM); @@ -398,24 +457,18 @@ apr_status_t h2_stream_send_frame(h2_stream *stream, int ftype, int flags, size_ /* start pushed stream */ ap_assert(stream->request == NULL); ap_assert(stream->rtmp != NULL); - status = h2_request_end_headers(stream->rtmp, stream->pool, 1, 0); - if (status != APR_SUCCESS) { - return status; - } - set_policy_for(stream, stream->rtmp); - stream->request = stream->rtmp; - stream->rtmp = NULL; + status = h2_stream_end_headers(stream, 1, 0); + if (status != APR_SUCCESS) goto leave; break; default: break; } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, - H2_STRM_MSG(stream, "send frame %d, eos=%d"), ftype, eos); status = transit(stream, new_state); if (status == APR_SUCCESS && eos) { status = transit(stream, on_event(stream, H2_SEV_CLOSED_L)); } +leave: return status; } @@ -424,9 +477,10 @@ apr_status_t h2_stream_recv_frame(h2_stream *stream, int ftype, int flags, size_ apr_status_t status = APR_SUCCESS; int new_state, eos = 0; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); new_state = on_frame_recv(stream->state, ftype); if (new_state < 0) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, H2_STRM_MSG(stream, "invalid frame %d recv"), ftype); AP_DEBUG_ASSERT(new_state > S_XXX); return transit(stream, new_state); @@ -439,7 +493,7 @@ apr_status_t h2_stream_recv_frame(h2_stream *stream, int ftype, int flags, size_ case NGHTTP2_HEADERS: eos = (flags & NGHTTP2_FLAG_END_STREAM); - if (stream->state == H2_SS_OPEN) { + if (h2_stream_is_at_or_past(stream, H2_SS_OPEN)) { /* trailer HEADER */ if (!eos) { h2_stream_rst(stream, H2_ERR_PROTOCOL_ERROR); @@ -451,18 +505,13 @@ apr_status_t h2_stream_recv_frame(h2_stream *stream, int ftype, int flags, size_ ap_assert(stream->request == NULL); if (stream->rtmp == NULL) { /* This can only happen, if the stream has received no header - * name/value pairs at all. The lastest nghttp2 version have become + * name/value pairs at all. The latest nghttp2 version have become * pretty good at detecting this early. In any case, we have * to abort the connection here, since this is clearly a protocol error */ return APR_EINVAL; } - status = h2_request_end_headers(stream->rtmp, stream->pool, eos, frame_len); - if (status != APR_SUCCESS) { - return status; - } - set_policy_for(stream, stream->rtmp); - stream->request = stream->rtmp; - stream->rtmp = NULL; + status = h2_stream_end_headers(stream, eos, frame_len); + if (status != APR_SUCCESS) goto leave; } break; @@ -473,22 +522,7 @@ apr_status_t h2_stream_recv_frame(h2_stream *stream, int ftype, int flags, size_ if (status == APR_SUCCESS && eos) { status = transit(stream, on_event(stream, H2_SEV_CLOSED_R)); } - return status; -} - -apr_status_t h2_stream_flush_input(h2_stream *stream) -{ - apr_status_t status = APR_SUCCESS; - - if (stream->in_buffer && !APR_BRIGADE_EMPTY(stream->in_buffer)) { - setup_input(stream); - status = h2_beam_send(stream->input, stream->in_buffer, APR_BLOCK_READ); - stream->in_last_write = apr_time_now(); - } - if (stream->input_eof - && stream->input && !h2_beam_is_closed(stream->input)) { - status = h2_beam_close(stream->input); - } +leave: return status; } @@ -498,41 +532,59 @@ apr_status_t h2_stream_recv_DATA(h2_stream *stream, uint8_t flags, h2_session *session = stream->session; apr_status_t status = APR_SUCCESS; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); stream->in_data_frames++; if (len > 0) { - if (APLOGctrace3(session->c)) { + if (APLOGctrace3(session->c1)) { const char *load = apr_pstrndup(stream->pool, (const char *)data, len); - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, session->c1, H2_STRM_MSG(stream, "recv DATA, len=%d: -->%s<--"), (int)len, load); } else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c1, H2_STRM_MSG(stream, "recv DATA, len=%d"), (int)len); } stream->in_data_octets += len; - if (!stream->in_buffer) { - stream->in_buffer = apr_brigade_create(stream->pool, - session->c->bucket_alloc); - } - apr_brigade_write(stream->in_buffer, NULL, NULL, (const char *)data, len); + input_append_data(stream, (const char*)data, len); + input_flush(stream); h2_stream_dispatch(stream, H2_SEV_IN_DATA_PENDING); } return status; } -static void prep_output(h2_stream *stream) { - conn_rec *c = stream->session->c; - if (!stream->out_buffer) { - stream->out_buffer = apr_brigade_create(stream->pool, c->bucket_alloc); +#ifdef AP_DEBUG +static apr_status_t stream_pool_destroy(void *data) +{ + h2_stream *stream = data; + switch (stream->magic) { + case H2_STRM_MAGIC_OK: + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, stream->session->c1, + H2_STRM_MSG(stream, "was not destroyed explicitly")); + AP_DEBUG_ASSERT(0); + break; + case H2_STRM_MAGIC_SDEL: + /* stream has been explicitly destroyed, as it should */ + H2_STRM_ASSIGN_MAGIC(stream, H2_STRM_MAGIC_PDEL); + break; + case H2_STRM_MAGIC_PDEL: + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, stream->session->c1, + H2_STRM_MSG(stream, "already pool destroyed")); + AP_DEBUG_ASSERT(0); + break; + default: + AP_DEBUG_ASSERT(0); } + return APR_SUCCESS; } +#endif h2_stream *h2_stream_create(int id, apr_pool_t *pool, h2_session *session, h2_stream_monitor *monitor, int initiated_on) { h2_stream *stream = apr_pcalloc(pool, sizeof(h2_stream)); - + + H2_STRM_ASSIGN_MAGIC(stream, H2_STRM_MAGIC_OK); stream->id = id; stream->initiated_on = initiated_on; stream->created = apr_time_now(); @@ -540,15 +592,21 @@ h2_stream *h2_stream_create(int id, apr_pool_t *pool, h2_session *session, stream->pool = pool; stream->session = session; stream->monitor = monitor; - stream->max_mem = session->max_stream_mem; - -#ifdef H2_NG2_LOCAL_WIN_SIZE - stream->in_window_size = - nghttp2_session_get_stream_local_window_size( - stream->session->ngh2, stream->id); +#ifdef AP_DEBUG + if (id) { /* stream 0 has special lifetime */ + apr_pool_cleanup_register(pool, stream, stream_pool_destroy, + apr_pool_cleanup_null); + } #endif - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c, +#ifdef H2_NG2_LOCAL_WIN_SIZE + if (id) { + stream->in_window_size = + nghttp2_session_get_stream_local_window_size( + stream->session->ngh2, stream->id); + } +#endif + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1, H2_STRM_LOG(APLOGNO(03082), stream, "created")); on_state_enter(stream); return stream; @@ -556,59 +614,35 @@ h2_stream *h2_stream_create(int id, apr_pool_t *pool, h2_session *session, void h2_stream_cleanup(h2_stream *stream) { - apr_status_t status; - + /* Stream is done on c1. There might still be processing on a c2 + * going on. The input/output beams get aborted and the stream's + * end of the in/out notifications get closed. + */ ap_assert(stream); + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); if (stream->out_buffer) { - /* remove any left over output buckets that may still have - * references into request pools */ apr_brigade_cleanup(stream->out_buffer); } - if (stream->input) { - h2_beam_abort(stream->input); - status = h2_beam_wait_empty(stream->input, APR_NONBLOCK_READ); - if (status == APR_EAGAIN) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, stream->session->c, - H2_STRM_MSG(stream, "wait on input drain")); - status = h2_beam_wait_empty(stream->input, APR_BLOCK_READ); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, stream->session->c, - H2_STRM_MSG(stream, "input drain returned")); - } - } } void h2_stream_destroy(h2_stream *stream) { ap_assert(stream); - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, stream->session->c, + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + ap_log_cerror(APLOG_MARK, APLOG_TRACE3, 0, stream->session->c1, H2_STRM_MSG(stream, "destroy")); + H2_STRM_ASSIGN_MAGIC(stream, H2_STRM_MAGIC_SDEL); apr_pool_destroy(stream->pool); } -apr_status_t h2_stream_prep_processing(h2_stream *stream) -{ - if (stream->request) { - const h2_request *r = stream->request; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, - H2_STRM_MSG(stream, "schedule %s %s://%s%s chunked=%d"), - r->method, r->scheme, r->authority, r->path, r->chunked); - setup_input(stream); - stream->scheduled = 1; - return APR_SUCCESS; - } - return APR_EINVAL; -} - void h2_stream_rst(h2_stream *stream, int error_code) { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); stream->rst_error = error_code; - if (stream->input) { - h2_beam_abort(stream->input); - } - if (stream->output) { - h2_beam_leave(stream->output); + if (stream->c2) { + h2_c2_abort(stream->c2, stream->session->c1); } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, H2_STRM_MSG(stream, "reset, error=%d"), error_code); h2_stream_dispatch(stream, H2_SEV_CANCELLED); } @@ -619,6 +653,7 @@ apr_status_t h2_stream_set_request_rec(h2_stream *stream, h2_request *req; apr_status_t status; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); ap_assert(stream->request == NULL); ap_assert(stream->rtmp == NULL); if (stream->rst_error) { @@ -640,6 +675,7 @@ apr_status_t h2_stream_set_request_rec(h2_stream *stream, void h2_stream_set_request(h2_stream *stream, const h2_request *r) { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); ap_assert(stream->request == NULL); ap_assert(stream->rtmp == NULL); stream->rtmp = h2_request_clone(stream->pool, r); @@ -647,43 +683,46 @@ void h2_stream_set_request(h2_stream *stream, const h2_request *r) static void set_error_response(h2_stream *stream, int http_status) { - if (!h2_stream_is_ready(stream)) { - conn_rec *c = stream->session->c; - apr_bucket *b; - h2_headers *response; - - response = h2_headers_die(http_status, stream->request, stream->pool); - prep_output(stream); - b = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_HEAD(stream->out_buffer, b); - b = h2_bucket_headers_create(c->bucket_alloc, response); - APR_BRIGADE_INSERT_HEAD(stream->out_buffer, b); + if (!h2_stream_is_ready(stream) && stream->rtmp) { + stream->rtmp->http_status = http_status; } } static apr_status_t add_trailer(h2_stream *stream, const char *name, size_t nlen, - const char *value, size_t vlen) + const char *value, size_t vlen, + size_t max_field_len, int *pwas_added) { - conn_rec *c = stream->session->c; + conn_rec *c = stream->session->c1; char *hname, *hvalue; + const char *existing; + *pwas_added = 0; if (nlen == 0 || name[0] == ':') { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, APR_EINVAL, c, H2_STRM_LOG(APLOGNO(03060), stream, "pseudo header in trailer")); return APR_EINVAL; } - if (h2_req_ignore_trailer(name, nlen)) { + if (h2_ignore_req_trailer(name, nlen)) { return APR_SUCCESS; } - if (!stream->trailers) { - stream->trailers = apr_table_make(stream->pool, 5); + if (!stream->trailers_in) { + stream->trailers_in = apr_table_make(stream->pool, 5); } hname = apr_pstrndup(stream->pool, name, nlen); - hvalue = apr_pstrndup(stream->pool, value, vlen); h2_util_camel_case_header(hname, nlen); - apr_table_mergen(stream->trailers, hname, hvalue); + existing = apr_table_get(stream->trailers_in, hname); + if (max_field_len + && ((existing? strlen(existing)+2 : 0) + vlen + nlen + 2 > max_field_len)) { + /* "key: (oldval, )?nval" is too long */ + return APR_EINVAL; + } + if (!existing) *pwas_added = 1; + hvalue = apr_pstrndup(stream->pool, value, vlen); + apr_table_mergen(stream->trailers_in, hname, hvalue); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + H2_STRM_MSG(stream, "added trailer '%s: %s'"), hname, hvalue); return APR_SUCCESS; } @@ -693,274 +732,487 @@ apr_status_t h2_stream_add_header(h2_stream *stream, const char *value, size_t vlen) { h2_session *session = stream->session; - int error = 0; - apr_status_t status; + int error = 0, was_added = 0; + apr_status_t status = APR_SUCCESS; - if (stream->has_response) { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + if (stream->response) { return APR_EINVAL; } - ++stream->request_headers_added; + if (name[0] == ':') { - if ((vlen) > session->s->limit_req_line) { + if (vlen > APR_INT32_MAX || (int)vlen > session->s->limit_req_line) { /* pseudo header: approximation of request line size check */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, - H2_STRM_MSG(stream, "pseudo %s too long"), name); + if (!h2_stream_is_ready(stream)) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, session->c1, + H2_STRM_LOG(APLOGNO(10178), stream, + "Request pseudo header exceeds " + "LimitRequestFieldSize: %s"), name); + } error = HTTP_REQUEST_URI_TOO_LARGE; + goto cleanup; } } - else if ((nlen + 2 + vlen) > session->s->limit_req_fieldsize) { + + if (session->s->limit_req_fields > 0 + && stream->request_headers_added > session->s->limit_req_fields) { + /* already over limit, count this attempt, but do not take it in */ + ++stream->request_headers_added; + } + else if (H2_SS_IDLE == stream->state) { + if (!stream->rtmp) { + stream->rtmp = h2_request_create(stream->id, stream->pool, + NULL, NULL, NULL, NULL, NULL); + } + status = h2_request_add_header(stream->rtmp, stream->pool, + name, nlen, value, vlen, + session->s->limit_req_fieldsize, &was_added); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, session->c1, + H2_STRM_MSG(stream, "add_header: '%.*s: %.*s"), + (int)nlen, name, (int)vlen, value); + if (was_added) ++stream->request_headers_added; + } + else if (H2_SS_OPEN == stream->state) { + status = add_trailer(stream, name, nlen, value, vlen, + session->s->limit_req_fieldsize, &was_added); + if (was_added) ++stream->request_headers_added; + } + else { + status = APR_EINVAL; + goto cleanup; + } + + if (APR_EINVAL == status) { /* header too long */ - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, - H2_STRM_MSG(stream, "header %s too long"), name); + if (!h2_stream_is_ready(stream)) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, session->c1, + H2_STRM_LOG(APLOGNO(10180), stream,"Request header exceeds " + "LimitRequestFieldSize: %.*s"), + (int)H2MIN(nlen, 80), name); + } error = HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE; + goto cleanup; } - if (stream->request_headers_added > session->s->limit_req_fields + 4) { - /* too many header lines, include 4 pseudo headers */ - if (stream->request_headers_added - > session->s->limit_req_fields + 4 + 100) { - /* yeah, right */ + if (session->s->limit_req_fields > 0 + && stream->request_headers_added > session->s->limit_req_fields) { + /* too many header lines */ + if (stream->request_headers_added > session->s->limit_req_fields + 100) { + /* yeah, right, this request is way over the limit, say goodbye */ h2_stream_rst(stream, H2_ERR_ENHANCE_YOUR_CALM); return APR_ECONNRESET; } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, - H2_STRM_MSG(stream, "too many header lines")); + if (!h2_stream_is_ready(stream)) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, session->c1, + H2_STRM_LOG(APLOGNO(10181), stream, "Number of request headers " + "exceeds LimitRequestFields")); + } error = HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE; + goto cleanup; } +cleanup: if (error) { + ++stream->request_headers_failed; set_error_response(stream, error); return APR_EINVAL; } - else if (H2_SS_IDLE == stream->state) { - if (!stream->rtmp) { - stream->rtmp = h2_req_create(stream->id, stream->pool, - NULL, NULL, NULL, NULL, NULL, 0); - } - status = h2_request_add_header(stream->rtmp, stream->pool, - name, nlen, value, vlen); + else if (status != APR_SUCCESS) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c1, + H2_STRM_MSG(stream, "header %s not accepted"), name); + h2_stream_dispatch(stream, H2_SEV_CANCELLED); } - else if (H2_SS_OPEN == stream->state) { - status = add_trailer(stream, name, nlen, value, vlen); + return status; +} + +typedef struct { + apr_size_t maxlen; + const char *failed_key; +} val_len_check_ctx; + +static int table_check_val_len(void *baton, const char *key, const char *value) +{ + val_len_check_ctx *ctx = baton; + + if (strlen(value) <= ctx->maxlen) return 1; + ctx->failed_key = key; + return 0; +} + +apr_status_t h2_stream_end_headers(h2_stream *stream, int eos, size_t raw_bytes) +{ + apr_status_t status; + val_len_check_ctx ctx; + int is_http_or_https; + h2_request *req = stream->rtmp; + + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + status = h2_request_end_headers(req, stream->pool, raw_bytes); + if (APR_SUCCESS != status || req->http_status != H2_HTTP_STATUS_UNSET) { + goto cleanup; + } + + /* keep on returning APR_SUCCESS for error responses, so that we + * send it and do not RST the stream. + */ + set_policy_for(stream, req); + + ctx.maxlen = stream->session->s->limit_req_fieldsize; + ctx.failed_key = NULL; + apr_table_do(table_check_val_len, &ctx, req->headers, NULL); + if (ctx.failed_key) { + if (!h2_stream_is_ready(stream)) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10230), stream,"Request header exceeds " + "LimitRequestFieldSize: %.*s"), + (int)H2MIN(strlen(ctx.failed_key), 80), ctx.failed_key); + } + set_error_response(stream, HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE); + goto cleanup; + } + + /* http(s) scheme. rfc7540, ch. 8.1.2.3: + * This [:path] pseudo-header field MUST NOT be empty for "http" or "https" + * URIs; "http" or "https" URIs that do not contain a path component + * MUST include a value of '/'. The exception to this rule is an + * OPTIONS request for an "http" or "https" URI that does not include + * a path component; these MUST include a ":path" pseudo-header field + * with a value of '*' + * + * All HTTP/2 requests MUST include exactly one valid value for the + * ":method", ":scheme", and ":path" pseudo-header fields, unless it is + * a CONNECT request. + */ + is_http_or_https = (!req->scheme + || !(ap_cstr_casecmpn(req->scheme, "http", 4) != 0 + || (req->scheme[4] != '\0' + && (apr_tolower(req->scheme[4]) != 's' + || req->scheme[5] != '\0')))); + + /* CONNECT. rfc7540, ch. 8.3: + * In HTTP/2, the CONNECT method is used to establish a tunnel over a + * single HTTP/2 stream to a remote host for similar purposes. The HTTP + * header field mapping works as defined in Section 8.1.2.3 ("Request + * Pseudo-Header Fields"), with a few differences. Specifically: + * o The ":method" pseudo-header field is set to "CONNECT". + * o The ":scheme" and ":path" pseudo-header fields MUST be omitted. + * o The ":authority" pseudo-header field contains the host and port to + * connect to (equivalent to the authority-form of the request-target + * of CONNECT requests (see [RFC7230], Section 5.3)). + */ + if (!ap_cstr_casecmp(req->method, "CONNECT")) { + if (req->protocol) { + if (!strcmp("websocket", req->protocol)) { + if (!req->scheme || !req->path) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10457), stream, "Request to websocket CONNECT " + "without :scheme or :path, sending 400 answer")); + set_error_response(stream, HTTP_BAD_REQUEST); + goto cleanup; + } + } + else { + /* do not know that protocol */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->session->c1, APLOGNO(10460) + "':protocol: %s' header present in %s request", + req->protocol, req->method); + set_error_response(stream, HTTP_NOT_IMPLEMENTED); + goto cleanup; + } + } + else if (req->scheme || req->path) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10384), stream, "Request to CONNECT " + "with :scheme or :path specified, sending 400 answer")); + set_error_response(stream, HTTP_BAD_REQUEST); + goto cleanup; + } } - else { - status = APR_EINVAL; + else if (is_http_or_https) { + if (!req->path) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10385), stream, "Request for http(s) " + "resource without :path, sending 400 answer")); + set_error_response(stream, HTTP_BAD_REQUEST); + goto cleanup; + } + if (!req->scheme) { + req->scheme = ap_ssl_conn_is_ssl(stream->session->c1)? "https" : "http"; + } } - - if (status != APR_SUCCESS) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c, - H2_STRM_MSG(stream, "header %s not accepted"), name); - h2_stream_dispatch(stream, H2_SEV_CANCELLED); + + if (req->scheme && (req->path && req->path[0] != '/')) { + /* We still have a scheme, which means we need to pass an absolute URI into + * our HTTP protocol handling and the missing '/' at the start will prevent + * us from doing so (as it then confuses path and authority). */ + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10379), stream, "Request :scheme '%s' and " + "path '%s' do not allow creating an absolute URL. Failing " + "request with 400."), req->scheme, req->path); + set_error_response(stream, HTTP_BAD_REQUEST); + goto cleanup; + } + +cleanup: + if (APR_SUCCESS == status) { + stream->request = req; + stream->rtmp = NULL; + + if (APLOGctrace4(stream->session->c1)) { + int i; + const apr_array_header_t *t_h = apr_table_elts(req->headers); + const apr_table_entry_t *t_elt = (apr_table_entry_t *)t_h->elts; + ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, stream->session->c1, + H2_STRM_MSG(stream,"headers received from client:")); + for (i = 0; i < t_h->nelts; i++, t_elt++) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, stream->session->c1, + H2_STRM_MSG(stream, " %s: %s"), + ap_escape_logitem(stream->pool, t_elt->key), + ap_escape_logitem(stream->pool, t_elt->val)); + } + } } return status; } -static apr_bucket *get_first_headers_bucket(apr_bucket_brigade *bb) +static apr_bucket *get_first_response_bucket(apr_bucket_brigade *bb) { if (bb) { apr_bucket *b = APR_BRIGADE_FIRST(bb); while (b != APR_BRIGADE_SENTINEL(bb)) { +#if AP_HAS_RESPONSE_BUCKETS + if (AP_BUCKET_IS_RESPONSE(b)) { + return b; + } +#else if (H2_BUCKET_IS_HEADERS(b)) { return b; } +#endif b = APR_BUCKET_NEXT(b); } } return NULL; } -static apr_status_t add_buffered_data(h2_stream *stream, apr_off_t requested, - apr_off_t *plen, int *peos, int *is_all, - h2_headers **pheaders) +static void stream_do_error_bucket(h2_stream *stream, apr_bucket *b) { - apr_bucket *b, *e; - - *peos = 0; - *plen = 0; - *is_all = 0; - if (pheaders) { - *pheaders = NULL; + int err = ((ap_bucket_error *)(b->data))->status; + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, + H2_STRM_MSG(stream, "error bucket received, err=%d"), err); + if (err >= 500) { + err = NGHTTP2_INTERNAL_ERROR; } - - H2_STREAM_OUT_LOG(APLOG_TRACE2, stream, "add_buffered_data"); - b = APR_BRIGADE_FIRST(stream->out_buffer); - while (b != APR_BRIGADE_SENTINEL(stream->out_buffer)) { - e = APR_BUCKET_NEXT(b); - if (APR_BUCKET_IS_METADATA(b)) { - if (APR_BUCKET_IS_FLUSH(b)) { - APR_BUCKET_REMOVE(b); - apr_bucket_destroy(b); - } - else if (APR_BUCKET_IS_EOS(b)) { - *peos = 1; - return APR_SUCCESS; - } - else if (H2_BUCKET_IS_HEADERS(b)) { - if (*plen > 0) { - /* data before the response, can only return up to here */ - return APR_SUCCESS; - } - else if (pheaders) { - *pheaders = h2_bucket_headers_get(b); - APR_BUCKET_REMOVE(b); - apr_bucket_destroy(b); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, - H2_STRM_MSG(stream, "prep, -> response %d"), - (*pheaders)->status); - return APR_SUCCESS; - } - else { - return APR_EAGAIN; - } - } - } - else if (b->length == 0) { - APR_BUCKET_REMOVE(b); - apr_bucket_destroy(b); - } - else { - ap_assert(b->length != (apr_size_t)-1); - *plen += b->length; - if (*plen >= requested) { - *plen = requested; - return APR_SUCCESS; - } - } - b = e; + else if (err >= 400) { + err = NGHTTP2_STREAM_CLOSED; } - *is_all = 1; - return APR_SUCCESS; + else { + err = NGHTTP2_PROTOCOL_ERROR; + } + h2_stream_rst(stream, err); } -apr_status_t h2_stream_out_prepare(h2_stream *stream, apr_off_t *plen, - int *peos, h2_headers **pheaders) +static apr_status_t buffer_output_receive(h2_stream *stream) { - apr_status_t status = APR_SUCCESS; - apr_off_t requested, missing, max_chunk = H2_DATA_CHUNK_SIZE; - conn_rec *c; - int complete; + apr_status_t rv = APR_EAGAIN; + apr_off_t buf_len; + conn_rec *c1 = stream->session->c1; + apr_bucket *b, *e; - ap_assert(stream); - + if (!stream->output) { + goto cleanup; + } if (stream->rst_error) { - *plen = 0; - *peos = 1; - return APR_ECONNRESET; + rv = APR_ECONNRESET; + goto cleanup; } - - c = stream->session->c; - prep_output(stream); - /* determine how much we'd like to send. We cannot send more than - * is requested. But we can reduce the size in case the master - * connection operates in smaller chunks. (TSL warmup) */ - if (stream->session->io.write_size > 0) { - max_chunk = stream->session->io.write_size - 9; /* header bits */ + if (!stream->out_buffer) { + stream->out_buffer = apr_brigade_create(stream->pool, c1->bucket_alloc); + buf_len = 0; } - requested = (*plen > 0)? H2MIN(*plen, max_chunk) : max_chunk; - - /* count the buffered data until eos or a headers bucket */ - status = add_buffered_data(stream, requested, plen, peos, &complete, pheaders); - - if (status == APR_EAGAIN) { - /* TODO: ugly, someone needs to retrieve the response first */ - h2_mplx_keep_active(stream->session->mplx, stream); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - H2_STRM_MSG(stream, "prep, response eagain")); - return status; + else { + /* if the brigade contains a file bucket, its normal report length + * might be megabytes, but the memory used is tiny. For buffering, + * we are only interested in the memory footprint. */ + buf_len = h2_brigade_mem_size(stream->out_buffer); } - else if (status != APR_SUCCESS) { - return status; + + if (buf_len > APR_INT32_MAX + || (apr_size_t)buf_len >= stream->session->max_stream_mem) { + /* we have buffered enough. No need to read more. + * However, we have now output pending for which we may not + * receive another poll event. We need to make sure that this + * stream is not suspended so we keep on processing output. + */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1, + H2_STRM_MSG(stream, "out_buffer, already has %ld length"), + (long)buf_len); + rv = APR_SUCCESS; + goto cleanup; } - - if (pheaders && *pheaders) { - return APR_SUCCESS; + + if (stream->output_eos) { + rv = APR_BRIGADE_EMPTY(stream->out_buffer)? APR_EOF : APR_SUCCESS; } - - /* If there we do not have enough buffered data to satisfy the requested - * length *and* we counted the _complete_ buffer (and did not stop in the middle - * because of meta data there), lets see if we can read more from the - * output beam */ - missing = H2MIN(requested, stream->max_mem) - *plen; - if (complete && !*peos && missing > 0) { - apr_status_t rv = APR_EOF; - - if (stream->output) { - H2_STREAM_OUT_LOG(APLOG_TRACE2, stream, "pre"); - rv = h2_beam_receive(stream->output, stream->out_buffer, - APR_NONBLOCK_READ, stream->max_mem - *plen); - H2_STREAM_OUT_LOG(APLOG_TRACE2, stream, "post"); - } - - if (rv == APR_SUCCESS) { - /* count the buffer again, now that we have read output */ - status = add_buffered_data(stream, requested, plen, peos, &complete, pheaders); - } - else if (APR_STATUS_IS_EOF(rv)) { - apr_bucket *eos = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(stream->out_buffer, eos); - *peos = 1; - } - else if (APR_STATUS_IS_EAGAIN(rv)) { - /* we set this is the status of this call only if there - * is no buffered data, see check below */ - } - else { - /* real error reading. Give this back directly, even though - * we may have something buffered. */ - status = rv; + else { + H2_STREAM_OUT_LOG(APLOG_TRACE2, stream, "pre"); + rv = h2_beam_receive(stream->output, stream->session->c1, stream->out_buffer, + APR_NONBLOCK_READ, stream->session->max_stream_mem - buf_len); + if (APR_SUCCESS != rv) { + if (APR_EAGAIN != rv) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1, + H2_STRM_MSG(stream, "out_buffer, receive unsuccessful")); + } } } - - if (status == APR_SUCCESS) { - if (*peos || *plen) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - H2_STRM_MSG(stream, "prepare, len=%ld eos=%d"), - (long)*plen, *peos); - } - else { - status = (stream->output && h2_beam_is_closed(stream->output))? APR_EOF : APR_EAGAIN; - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - H2_STRM_MSG(stream, "prepare, no data")); + + /* get rid of buckets we have no need for */ + if (!APR_BRIGADE_EMPTY(stream->out_buffer)) { + b = APR_BRIGADE_FIRST(stream->out_buffer); + while (b != APR_BRIGADE_SENTINEL(stream->out_buffer)) { + e = APR_BUCKET_NEXT(b); + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_FLUSH(b)) { /* we flush any c1 data already */ + APR_BUCKET_REMOVE(b); + apr_bucket_destroy(b); + } + else if (APR_BUCKET_IS_EOS(b)) { + stream->output_eos = 1; + } + else if (AP_BUCKET_IS_ERROR(b)) { + stream_do_error_bucket(stream, b); + break; + } + } + else if (b->length == 0) { /* zero length data */ + APR_BUCKET_REMOVE(b); + apr_bucket_destroy(b); + } + b = e; } } - return status; + H2_STREAM_OUT_LOG(APLOG_TRACE2, stream, "out_buffer, after receive"); + +cleanup: + return rv; } -static int is_not_headers(apr_bucket *b) +static int bucket_pass_to_c1(apr_bucket *b) { - return !H2_BUCKET_IS_HEADERS(b); +#if AP_HAS_RESPONSE_BUCKETS + return !AP_BUCKET_IS_RESPONSE(b) + && !AP_BUCKET_IS_HEADERS(b) + && !APR_BUCKET_IS_EOS(b); +#else + return !H2_BUCKET_IS_HEADERS(b) && !APR_BUCKET_IS_EOS(b); +#endif } apr_status_t h2_stream_read_to(h2_stream *stream, apr_bucket_brigade *bb, apr_off_t *plen, int *peos) { - conn_rec *c = stream->session->c; - apr_status_t status = APR_SUCCESS; + apr_status_t rv = APR_SUCCESS; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); if (stream->rst_error) { return APR_ECONNRESET; } - status = h2_append_brigade(bb, stream->out_buffer, plen, peos, is_not_headers); - if (status == APR_SUCCESS && !*peos && !*plen) { - status = APR_EAGAIN; + rv = h2_append_brigade(bb, stream->out_buffer, plen, peos, bucket_pass_to_c1); + if (APR_SUCCESS == rv && !*peos && !*plen) { + rv = APR_EAGAIN; } - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, c, - H2_STRM_MSG(stream, "read_to, len=%ld eos=%d"), - (long)*plen, *peos); - return status; + return rv; } +static apr_status_t stream_do_trailers(h2_stream *stream) +{ + conn_rec *c1 = stream->session->c1; + int ngrv; + h2_ngheader *nh = NULL; + apr_bucket *b, *e; +#if AP_HAS_RESPONSE_BUCKETS + ap_bucket_headers *headers = NULL; +#else + h2_headers *headers = NULL; +#endif + apr_status_t rv; + + ap_assert(stream->response); + ap_assert(stream->out_buffer); + + b = APR_BRIGADE_FIRST(stream->out_buffer); + while (b != APR_BRIGADE_SENTINEL(stream->out_buffer)) { + e = APR_BUCKET_NEXT(b); + if (APR_BUCKET_IS_METADATA(b)) { +#if AP_HAS_RESPONSE_BUCKETS + if (AP_BUCKET_IS_HEADERS(b)) { + headers = b->data; +#else /* AP_HAS_RESPONSE_BUCKETS */ + if (H2_BUCKET_IS_HEADERS(b)) { + headers = h2_bucket_headers_get(b); +#endif /* else AP_HAS_RESPONSE_BUCKETS */ + APR_BUCKET_REMOVE(b); + apr_bucket_destroy(b); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c1, + H2_STRM_MSG(stream, "process trailers")); + break; + } + else if (APR_BUCKET_IS_EOS(b)) { + break; + } + } + else { + break; + } + b = e; + } + if (!headers) { + rv = APR_EAGAIN; + goto cleanup; + } + + rv = h2_res_create_ngtrailer(&nh, stream->pool, headers); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c1, + H2_STRM_LOG(APLOGNO(03072), stream, "submit %d trailers"), + (int)nh->nvlen); + if (APR_SUCCESS != rv) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c1, + H2_STRM_LOG(APLOGNO(10024), stream, "invalid trailers")); + h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR); + goto cleanup; + } + + ngrv = nghttp2_submit_trailer(stream->session->ngh2, stream->id, nh->nv, nh->nvlen); + if (nghttp2_is_fatal(ngrv)) { + rv = APR_EGENERAL; + h2_session_dispatch_event(stream->session, + H2_SESSION_EV_PROTO_ERROR, ngrv, nghttp2_strerror(rv)); + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c1, + APLOGNO(02940) "submit_response: %s", + nghttp2_strerror(rv)); + } + stream->sent_trailers = 1; + +cleanup: + return rv; +} + +#if AP_HAS_RESPONSE_BUCKETS +apr_status_t h2_stream_submit_pushes(h2_stream *stream, ap_bucket_response *response) +#else apr_status_t h2_stream_submit_pushes(h2_stream *stream, h2_headers *response) +#endif { apr_status_t status = APR_SUCCESS; apr_array_header_t *pushes; int i; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); pushes = h2_push_collect_update(stream, stream->request, response); if (pushes && !apr_is_empty_array(pushes)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c, + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c1, H2_STRM_MSG(stream, "found %d push candidates"), pushes->nelts); for (i = 0; i < pushes->nelts; ++i) { @@ -977,17 +1229,24 @@ apr_status_t h2_stream_submit_pushes(h2_stream *stream, h2_headers *response) apr_table_t *h2_stream_get_trailers(h2_stream *stream) { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); return NULL; } -const h2_priority *h2_stream_get_priority(h2_stream *stream, +#if AP_HAS_RESPONSE_BUCKETS +const h2_priority *h2_stream_get_priority(h2_stream *stream, + ap_bucket_response *response) +#else +const h2_priority *h2_stream_get_priority(h2_stream *stream, h2_headers *response) +#endif { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); if (response && stream->initiated_on) { const char *ctype = apr_table_get(response->headers, "content-type"); if (ctype) { /* FIXME: Not good enough, config needs to come from request->server */ - return h2_config_get_priority(stream->session->config, ctype); + return h2_cconfig_get_priority(stream->session->c1, ctype); } } return NULL; @@ -995,21 +1254,47 @@ const h2_priority *h2_stream_get_priority(h2_stream *stream, int h2_stream_is_ready(h2_stream *stream) { - if (stream->has_response) { + /* Have we sent a response or do we have the response in our buffer? */ + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + if (stream->response) { return 1; } - else if (stream->out_buffer && get_first_headers_bucket(stream->out_buffer)) { + else if (stream->out_buffer && get_first_response_bucket(stream->out_buffer)) { return 1; } return 0; } -int h2_stream_was_closed(const h2_stream *stream) +int h2_stream_wants_send_data(h2_stream *stream) { - switch (stream->state) { + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + return h2_stream_is_ready(stream) && + ((stream->out_buffer && !APR_BRIGADE_EMPTY(stream->out_buffer)) || + (stream->output && !h2_beam_empty(stream->output))); +} + +int h2_stream_is_at(const h2_stream *stream, h2_stream_state_t state) +{ + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + return stream->state == state; +} + +int h2_stream_is_at_or_past(const h2_stream *stream, h2_stream_state_t state) +{ + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + switch (state) { + case H2_SS_IDLE: + return 1; /* by definition */ + case H2_SS_RSVD_R: /*fall through*/ + case H2_SS_RSVD_L: /*fall through*/ + case H2_SS_OPEN: + return stream->state == state || stream->state >= H2_SS_OPEN; + case H2_SS_CLOSED_R: /*fall through*/ + case H2_SS_CLOSED_L: /*fall through*/ case H2_SS_CLOSED: + return stream->state == state || stream->state >= H2_SS_CLOSED; case H2_SS_CLEANUP: - return 1; + return stream->state == state; default: return 0; } @@ -1019,6 +1304,7 @@ apr_status_t h2_stream_in_consumed(h2_stream *stream, apr_off_t amount) { h2_session *session = stream->session; + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); if (amount > 0) { apr_off_t consumed = amount; @@ -1066,13 +1352,477 @@ apr_status_t h2_stream_in_consumed(h2_stream *stream, apr_off_t amount) nghttp2_session_set_local_window_size(session->ngh2, NGHTTP2_FLAG_NONE, stream->id, win); } - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c, - "h2_stream(%ld-%d): consumed %ld bytes, window now %d/%d", - session->id, stream->id, (long)amount, - cur_size, stream->in_window_size); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c1, + H2_STRM_MSG(stream, "consumed %ld bytes, window now %d/%d"), + (long)amount, cur_size, stream->in_window_size); } -#endif +#endif /* #ifdef H2_NG2_LOCAL_WIN_SIZE */ } return APR_SUCCESS; } +static apr_off_t output_data_buffered(h2_stream *stream, int *peos, int *pheader_blocked) +{ + /* How much data do we have in our buffers that we can write? */ + apr_off_t buf_len = 0; + apr_bucket *b; + + *peos = *pheader_blocked = 0; + if (stream->out_buffer) { + b = APR_BRIGADE_FIRST(stream->out_buffer); + while (b != APR_BRIGADE_SENTINEL(stream->out_buffer)) { + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + *peos = 1; + break; + } +#if AP_HAS_RESPONSE_BUCKETS + else if (AP_BUCKET_IS_RESPONSE(b)) { + break; + } + else if (AP_BUCKET_IS_HEADERS(b)) { + *pheader_blocked = 1; + break; + } +#else + else if (H2_BUCKET_IS_HEADERS(b)) { + *pheader_blocked = 1; + break; + } +#endif + } + else { + buf_len += b->length; + } + b = APR_BUCKET_NEXT(b); + } + } + return buf_len; +} + +static ssize_t stream_data_cb(nghttp2_session *ng2s, + int32_t stream_id, + uint8_t *buf, + size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, + void *puser) +{ + h2_session *session = (h2_session *)puser; + conn_rec *c1 = session->c1; + apr_off_t buf_len; + int eos, header_blocked; + apr_status_t rv; + h2_stream *stream; + + /* nghttp2 wants to send more DATA for the stream. + * we should have submitted the final response at this time + * after receiving output via stream_do_responses() */ + ap_assert(session); + (void)ng2s; + (void)buf; + (void)source; + stream = nghttp2_session_get_stream_user_data(session->ngh2, stream_id); + + if (!stream) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c1, + APLOGNO(02937) + H2_SSSN_STRM_MSG(session, stream_id, "data_cb, stream not found")); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + if (!stream->output || !stream->response || !stream->out_buffer) { + return NGHTTP2_ERR_DEFERRED; + } + if (stream->rst_error) { + return NGHTTP2_ERR_DEFERRED; + } + if (h2_c1_io_needs_flush(&session->io)) { + rv = h2_c1_io_pass(&session->io); + if (APR_STATUS_IS_EAGAIN(rv)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c1, + H2_SSSN_STRM_MSG(session, stream_id, "suspending on c1 out needs flush")); + h2_stream_dispatch(stream, H2_SEV_OUT_C1_BLOCK); + return NGHTTP2_ERR_DEFERRED; + } + else if (rv) { + h2_session_dispatch_event(session, H2_SESSION_EV_CONN_ERROR, rv, NULL); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + + /* determine how much we'd like to send. We cannot send more than + * is requested. But we can reduce the size in case the master + * connection operates in smaller chunks. (TSL warmup) */ + if (stream->session->io.write_size > 0) { + apr_size_t chunk_len = stream->session->io.write_size - H2_FRAME_HDR_LEN; + if (length > chunk_len) { + length = chunk_len; + } + } + /* We allow configurable max DATA frame length. */ + if (stream->session->max_data_frame_len > 0 + && length > stream->session->max_data_frame_len) { + length = stream->session->max_data_frame_len; + } + + /* How much data do we have in our buffers that we can write? + * if not enough, receive more. */ + buf_len = output_data_buffered(stream, &eos, &header_blocked); + if (buf_len < (apr_off_t)length && !eos + && !header_blocked && !stream->rst_error) { + /* read more? */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c1, + H2_SSSN_STRM_MSG(session, stream_id, + "need more (read len=%ld, %ld in buffer)"), + (long)length, (long)buf_len); + rv = buffer_output_receive(stream); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1, + H2_SSSN_STRM_MSG(session, stream_id, + "buffer_output_received")); + if (APR_STATUS_IS_EAGAIN(rv)) { + /* currently, no more is available */ + } + else if (APR_SUCCESS == rv) { + /* got some, re-assess */ + buf_len = output_data_buffered(stream, &eos, &header_blocked); + } + else if (APR_EOF == rv) { + if (!stream->output_eos) { + /* Seeing APR_EOF without an EOS bucket received before indicates + * that stream output is incomplete. Commonly, we expect to see + * an ERROR bucket to have been generated. But faulty handlers + * may not have generated one. + * We need to RST the stream bc otherwise the client thinks + * it is all fine. */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1, + H2_SSSN_STRM_MSG(session, stream_id, "rst stream")); + h2_stream_rst(stream, H2_ERR_STREAM_CLOSED); + return NGHTTP2_ERR_DEFERRED; + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1, + H2_SSSN_STRM_MSG(session, stream_id, + "eof on receive (read len=%ld, %ld in buffer)"), + (long)length, (long)buf_len); + eos = 1; + rv = APR_SUCCESS; + } + else if (APR_ECONNRESET == rv || APR_ECONNABORTED == rv) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c1, + H2_STRM_LOG(APLOGNO(10471), stream, "data_cb, reading data")); + h2_stream_rst(stream, H2_ERR_STREAM_CLOSED); + return NGHTTP2_ERR_DEFERRED; + } + else { + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c1, + H2_STRM_LOG(APLOGNO(02938), stream, "data_cb, reading data")); + h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR); + return NGHTTP2_ERR_DEFERRED; + } + } + + if (stream->rst_error) { + return NGHTTP2_ERR_DEFERRED; + } + + if (buf_len == 0 && header_blocked) { + rv = stream_do_trailers(stream); + if (APR_SUCCESS != rv && !APR_STATUS_IS_EAGAIN(rv)) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c1, + H2_STRM_LOG(APLOGNO(10300), stream, + "data_cb, error processing trailers")); + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + length = 0; + eos = 0; + } + else if (buf_len > (apr_off_t)length) { + eos = 0; /* Any EOS we have in the buffer does not apply yet */ + } + else { + length = (size_t)buf_len; + } + + if (length) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c1, + H2_STRM_MSG(stream, "data_cb, sending len=%ld, eos=%d"), + (long)length, eos); + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + } + else if (!eos && !stream->sent_trailers) { + /* We have not reached the end of DATA yet, DEFER sending */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c1, + H2_STRM_LOG(APLOGNO(03071), stream, "data_cb, suspending")); + return NGHTTP2_ERR_DEFERRED; + } + + if (eos) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + return length; +} + +static apr_status_t stream_do_response(h2_stream *stream) +{ + conn_rec *c1 = stream->session->c1; + apr_status_t rv = APR_EAGAIN; + int ngrv, is_empty = 0; + h2_ngheader *nh = NULL; + apr_bucket *b, *e; +#if AP_HAS_RESPONSE_BUCKETS + ap_bucket_response *resp = NULL; +#else + h2_headers *resp = NULL; +#endif + nghttp2_data_provider provider, *pprovider = NULL; + + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + ap_assert(!stream->response); + ap_assert(stream->out_buffer); + + b = APR_BRIGADE_FIRST(stream->out_buffer); + while (b != APR_BRIGADE_SENTINEL(stream->out_buffer)) { + e = APR_BUCKET_NEXT(b); + if (APR_BUCKET_IS_METADATA(b)) { +#if AP_HAS_RESPONSE_BUCKETS + if (AP_BUCKET_IS_RESPONSE(b)) { + resp = b->data; +#else /* AP_HAS_RESPONSE_BUCKETS */ + if (H2_BUCKET_IS_HEADERS(b)) { + resp = h2_bucket_headers_get(b); +#endif /* else AP_HAS_RESPONSE_BUCKETS */ + APR_BUCKET_REMOVE(b); + apr_bucket_destroy(b); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c1, + H2_STRM_MSG(stream, "process response %d"), + resp->status); + is_empty = (e != APR_BRIGADE_SENTINEL(stream->out_buffer) + && APR_BUCKET_IS_EOS(e)); + break; + } + else if (APR_BUCKET_IS_EOS(b)) { + h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR); + rv = APR_EINVAL; + goto cleanup; + } + else if (AP_BUCKET_IS_ERROR(b)) { + stream_do_error_bucket(stream, b); + rv = APR_EINVAL; + goto cleanup; + } + } + else { + /* data buckets before response headers, an error */ + h2_stream_rst(stream, H2_ERR_INTERNAL_ERROR); + rv = APR_EINVAL; + goto cleanup; + } + b = e; + } + + if (!resp) { + rv = APR_EAGAIN; + goto cleanup; + } + + if (resp->status < 100) { + h2_stream_rst(stream, resp->status); + goto cleanup; + } + + if (resp->status == HTTP_FORBIDDEN && resp->notes) { + const char *cause = apr_table_get(resp->notes, "ssl-renegotiate-forbidden"); + if (cause) { + /* This request triggered a TLS renegotiation that is not allowed + * in HTTP/2. Tell the client that it should use HTTP/1.1 for this. + */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, resp->status, c1, + H2_STRM_LOG(APLOGNO(03061), stream, + "renegotiate forbidden, cause: %s"), cause); + h2_stream_rst(stream, H2_ERR_HTTP_1_1_REQUIRED); + goto cleanup; + } + } + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c1, + H2_STRM_LOG(APLOGNO(03073), stream, + "submit response %d"), resp->status); + + /* If this stream is not a pushed one itself, + * and HTTP/2 server push is enabled here, + * and the response HTTP status is not sth >= 400, + * and the remote side has pushing enabled, + * -> find and perform any pushes on this stream + * *before* we submit the stream response itself. + * This helps clients avoid opening new streams on Link + * resp that get pushed right afterwards. + * + * *) the response code is relevant, as we do not want to + * make pushes on 401 or 403 codes and friends. + * And if we see a 304, we do not push either + * as the client, having this resource in its cache, might + * also have the pushed ones as well. + */ + if (!stream->initiated_on + && !stream->response + && stream->request && stream->request->method + && !strcmp("GET", stream->request->method) + && (resp->status < 400) + && (resp->status != 304) + && h2_session_push_enabled(stream->session)) { + /* PUSH is possible and enabled on server, unless the request + * denies it, submit resources to push */ + const char *s = apr_table_get(resp->notes, H2_PUSH_MODE_NOTE); + if (!s || strcmp(s, "0")) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c1, + H2_STRM_MSG(stream, "submit pushes, note=%s"), s); + h2_stream_submit_pushes(stream, resp); + } + } + + if (!stream->pref_priority) { + stream->pref_priority = h2_stream_get_priority(stream, resp); + } + h2_session_set_prio(stream->session, stream, stream->pref_priority); + + if (resp->status == 103 + && !h2_config_sgeti(stream->session->s, H2_CONF_EARLY_HINTS)) { + /* suppress sending this to the client, it might have triggered + * pushes and served its purpose nevertheless */ + rv = APR_SUCCESS; + goto cleanup; + } + if (resp->status >= 200) { + stream->response = resp; + } + + if (!is_empty) { + memset(&provider, 0, sizeof(provider)); + provider.source.fd = stream->id; + provider.read_callback = stream_data_cb; + pprovider = &provider; + } + + rv = h2_res_create_ngheader(&nh, stream->pool, resp); + if (APR_SUCCESS != rv) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c1, + H2_STRM_LOG(APLOGNO(10025), stream, "invalid response")); + h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR); + goto cleanup; + } + + ngrv = nghttp2_submit_response(stream->session->ngh2, stream->id, + nh->nv, nh->nvlen, pprovider); + if (nghttp2_is_fatal(ngrv)) { + rv = APR_EGENERAL; + h2_session_dispatch_event(stream->session, + H2_SESSION_EV_PROTO_ERROR, ngrv, nghttp2_strerror(rv)); + ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c1, + APLOGNO(10402) "submit_response: %s", + nghttp2_strerror(rv)); + goto cleanup; + } + + if (stream->initiated_on) { + ++stream->session->pushes_submitted; + } + else { + ++stream->session->responses_submitted; + } + +cleanup: + return rv; +} + +static void stream_do_responses(h2_stream *stream) +{ + h2_session *session = stream->session; + conn_rec *c1 = session->c1; + apr_status_t rv; + + ap_assert(!stream->response); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c1, + H2_STRM_MSG(stream, "do_response")); + rv = buffer_output_receive(stream); + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, rv, c1, + H2_SSSN_STRM_MSG(session, stream->id, + "buffer_output_received2")); + if (APR_SUCCESS != rv && APR_EAGAIN != rv) { + h2_stream_rst(stream, NGHTTP2_PROTOCOL_ERROR); + } + else { + /* process all headers sitting at the buffer head. */ + do { + rv = stream_do_response(stream); + } while (APR_SUCCESS == rv + && !stream->rst_error + && !stream->response); + } +} + +void h2_stream_on_output_change(h2_stream *stream) +{ + conn_rec *c1 = stream->session->c1; + apr_status_t rv = APR_EAGAIN; + + /* stream->pout_recv_write signalled a change. Check what has happend, read + * from it and act on seeing a response/data. */ + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + if (!stream->output) { + /* c2 has not assigned the output beam to the stream (yet). */ + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c1, + H2_STRM_MSG(stream, "read_output, no output beam registered")); + } + else if (h2_stream_is_at_or_past(stream, H2_SS_CLOSED)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, c1, + H2_STRM_LOG(APLOGNO(10301), stream, "already closed")); + } + else if (h2_stream_is_at(stream, H2_SS_CLOSED_L)) { + /* We have delivered a response to a stream that was not closed + * by the client. This could be a POST with body that we negate + * and we need to RST_STREAM to end if. */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c1, + H2_STRM_LOG(APLOGNO(10313), stream, "remote close missing")); + h2_stream_rst(stream, H2_ERR_NO_ERROR); + } + else { + /* stream is not closed, a change in output happened. There are + * two modes of operation here: + * 1) the final response has been submitted. nghttp2 is invoking + * stream_data_cb() to progress the stream. This handles DATA, + * trailers, EOS and ERRORs. + * When stream_data_cb() runs out of things to send, it returns + * NGHTTP2_ERR_DEFERRED and nghttp2 *suspends* further processing + * until we tell it to resume. + * 2) We have not seen the *final* response yet. The stream can not + * send any response DATA. The nghttp2 stream_data_cb() is not + * invoked. We need to receive output, expecting not DATA but + * RESPONSEs (intermediate may arrive) and submit those. On + * the final response, nghttp2 will start calling stream_data_cb(). + */ + if (stream->response) { + nghttp2_session_resume_data(stream->session->ngh2, stream->id); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c1, + H2_STRM_MSG(stream, "resumed")); + } + else { + stream_do_responses(stream); + if (!stream->rst_error) { + nghttp2_session_resume_data(stream->session->ngh2, stream->id); + } + } + } +} + +void h2_stream_on_input_change(h2_stream *stream) +{ + H2_STRM_ASSERT_MAGIC(stream, H2_STRM_MAGIC_OK); + ap_assert(stream->input); + h2_beam_report_consumption(stream->input); + if (h2_stream_is_at(stream, H2_SS_CLOSED_L) + && !h2_mplx_c1_stream_is_running(stream->session->mplx, stream)) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->session->c1, + H2_STRM_LOG(APLOGNO(10026), stream, "remote close missing")); + h2_stream_rst(stream, H2_ERR_NO_ERROR); + } +} diff --git a/modules/http2/h2_stream.h b/modules/http2/h2_stream.h index 7ecc0ad..405978a 100644 --- a/modules/http2/h2_stream.h +++ b/modules/http2/h2_stream.h @@ -17,7 +17,10 @@ #ifndef __mod_h2__h2_stream__ #define __mod_h2__h2_stream__ +#include <http_protocol.h> + #include "h2.h" +#include "h2_headers.h" /** * A HTTP/2 stream, e.g. a client request+response in HTTP/1.1 terms. @@ -26,8 +29,8 @@ * connection to the client. The h2_session writes to the h2_stream, * adding HEADERS and DATA and finally an EOS. When headers are done, * h2_stream is scheduled for handling, which is expected to produce - * a response h2_headers at least. - * + * h2_headers/RESPONSE buckets. + * * The h2_headers may be followed by more h2_headers (interim responses) and * by DATA frames read from the h2_stream until EOS is reached. Trailers * are send when a last h2_headers is received. This always closes the stream @@ -37,9 +40,7 @@ struct h2_mplx; struct h2_priority; struct h2_request; -struct h2_headers; struct h2_session; -struct h2_task; struct h2_bucket_beam; typedef struct h2_stream h2_stream; @@ -62,7 +63,22 @@ typedef struct h2_stream_monitor { trigger a state change */ } h2_stream_monitor; +#ifdef AP_DEBUG +#define H2_STRM_MAGIC_OK 0x5354524d +#define H2_STRM_MAGIC_SDEL 0x5344454c +#define H2_STRM_MAGIC_PDEL 0x5044454c + +#define H2_STRM_ASSIGN_MAGIC(s,m) ((s)->magic = m) +#define H2_STRM_ASSERT_MAGIC(s,m) ap_assert((s)->magic == m) +#else +#define H2_STRM_ASSIGN_MAGIC(s,m) ((void)0) +#define H2_STRM_ASSERT_MAGIC(s,m) ((void)0) +#endif + struct h2_stream { +#ifdef AP_DEBUG + uint32_t magic; +#endif int id; /* http2 stream identifier */ int initiated_on; /* initiating stream id (PUSH) or 0 */ apr_pool_t *pool; /* the memory pool for this stream */ @@ -73,9 +89,16 @@ struct h2_stream { const struct h2_request *request; /* the request made in this stream */ struct h2_request *rtmp; /* request being assembled */ - apr_table_t *trailers; /* optional incoming trailers */ + apr_table_t *trailers_in; /* optional, incoming trailers */ int request_headers_added; /* number of request headers added */ - + int request_headers_failed; /* number of request headers failed to add */ + +#if AP_HAS_RESPONSE_BUCKETS + ap_bucket_response *response; /* the final, non-interim response or NULL */ +#else + struct h2_headers *response; /* the final, non-interim response or NULL */ +#endif + struct h2_bucket_beam *input; apr_bucket_brigade *in_buffer; int in_window_size; @@ -83,17 +106,16 @@ struct h2_stream { struct h2_bucket_beam *output; apr_bucket_brigade *out_buffer; - apr_size_t max_mem; /* maximum amount of data buffered */ int rst_error; /* stream error for RST_STREAM */ unsigned int aborted : 1; /* was aborted */ unsigned int scheduled : 1; /* stream has been scheduled */ - unsigned int has_response : 1; /* response headers are known */ - unsigned int input_eof : 1; /* no more request data coming */ - unsigned int out_checked : 1; /* output eof was double checked */ + unsigned int input_closed : 1; /* no more request data/trailers coming */ unsigned int push_policy; /* which push policy to use for this request */ - - struct h2_task *task; /* assigned task to fullfill request */ + unsigned int sent_trailers : 1; /* trailers have been submitted */ + unsigned int output_eos : 1; /* output EOS in buffer/sent */ + + conn_rec *c2; /* connection processing stream */ const h2_priority *pref_priority; /* preferred priority for this stream */ apr_off_t out_frames; /* # of frames sent out */ @@ -132,13 +154,9 @@ h2_stream *h2_stream_create(int id, apr_pool_t *pool, void h2_stream_destroy(h2_stream *stream); /** - * Prepare the stream so that processing may start. - * - * This is the time to allocated resources not needed before. - * - * @param stream the stream to prep + * Perform any late initialization before stream starts processing. */ -apr_status_t h2_stream_prep_processing(h2_stream *stream); +apr_status_t h2_stream_prepare_processing(h2_stream *stream); /* * Set a new monitor for this stream, replacing any existing one. Can @@ -154,6 +172,22 @@ void h2_stream_set_monitor(h2_stream *stream, h2_stream_monitor *monitor); void h2_stream_dispatch(h2_stream *stream, h2_stream_event_t ev); /** + * Determine if stream is at given state. + * @param stream the stream to check + * @param state the state to look for + * @return != 0 iff stream is at given state. + */ +int h2_stream_is_at(const h2_stream *stream, h2_stream_state_t state); + +/** + * Determine if stream is reached given state or is past this state. + * @param stream the stream to check + * @param state the state to look for + * @return != 0 iff stream is at or past given state. + */ +int h2_stream_is_at_or_past(const h2_stream *stream, h2_stream_state_t state); + +/** * Cleanup references into requst processing. * * @param stream the stream to cleanup @@ -198,6 +232,10 @@ apr_status_t h2_stream_set_request_rec(h2_stream *stream, apr_status_t h2_stream_add_header(h2_stream *stream, const char *name, size_t nlen, const char *value, size_t vlen); + +/* End the construction of request headers */ +apr_status_t h2_stream_end_headers(h2_stream *stream, int eos, size_t raw_bytes); + apr_status_t h2_stream_send_frame(h2_stream *stream, int frame_type, int flags, size_t frame_len); apr_status_t h2_stream_recv_frame(h2_stream *stream, int frame_type, int flags, size_t frame_len); @@ -213,8 +251,6 @@ apr_status_t h2_stream_recv_frame(h2_stream *stream, int frame_type, int flags, apr_status_t h2_stream_recv_DATA(h2_stream *stream, uint8_t flags, const uint8_t *data, size_t len); -apr_status_t h2_stream_flush_input(h2_stream *stream); - /** * Reset the stream. Stream write/reads will return errors afterwards. * @@ -224,31 +260,16 @@ apr_status_t h2_stream_flush_input(h2_stream *stream); void h2_stream_rst(h2_stream *stream, int error_code); /** - * Determine if stream was closed already. This is true for - * states H2_SS_CLOSED, H2_SS_CLEANUP. But not true - * for H2_SS_CLOSED_L and H2_SS_CLOSED_R. - * - * @param stream the stream to check on - * @return != 0 iff stream has been closed + * Stream input signals change. Take necessary actions. + * @param stream the stream to read output for */ -int h2_stream_was_closed(const h2_stream *stream); +void h2_stream_on_input_change(h2_stream *stream); /** - * Do a speculative read on the stream output to determine the - * amount of data that can be read. - * - * @param stream the stream to speculatively read from - * @param plen (in-/out) number of bytes requested and on return amount of bytes that - * may be read without blocking - * @param peos (out) != 0 iff end of stream will be reached when reading plen - * bytes (out value). - * @param presponse (out) the response of one became available - * @return APR_SUCCESS if out information was computed successfully. - * APR_EAGAIN if not data is available and end of stream has not been - * reached yet. + * Stream output signals change. Take necessary actions. + * @param stream the stream to read output for */ -apr_status_t h2_stream_out_prepare(h2_stream *stream, apr_off_t *plen, - int *peos, h2_headers **presponse); +void h2_stream_on_output_change(h2_stream *stream); /** * Read a maximum number of bytes into the bucket brigade. @@ -277,23 +298,34 @@ apr_table_t *h2_stream_get_trailers(h2_stream *stream); /** * Submit any server push promises on this stream and schedule - * the tasks connection with these. + * the streams for these. * * @param stream the stream for which to submit */ -apr_status_t h2_stream_submit_pushes(h2_stream *stream, h2_headers *response); +#if AP_HAS_RESPONSE_BUCKETS +apr_status_t h2_stream_submit_pushes(h2_stream *stream, + ap_bucket_response *response); +#else +apr_status_t h2_stream_submit_pushes(h2_stream *stream, + struct h2_headers *response); +#endif /** * Get priority information set for this stream. */ -const struct h2_priority *h2_stream_get_priority(h2_stream *stream, - h2_headers *response); +#if AP_HAS_RESPONSE_BUCKETS +const struct h2_priority *h2_stream_get_priority(h2_stream *stream, + ap_bucket_response *response); +#else +const struct h2_priority *h2_stream_get_priority(h2_stream *stream, + struct h2_headers *response); +#endif /** * Return a textual representation of the stream state as in RFC 7540 * nomenclator, all caps, underscores. */ -const char *h2_stream_state_str(h2_stream *stream); +const char *h2_stream_state_str(const h2_stream *stream); /** * Determine if stream is ready for submitting a response or a RST @@ -301,8 +333,11 @@ const char *h2_stream_state_str(h2_stream *stream); */ int h2_stream_is_ready(h2_stream *stream); +int h2_stream_wants_send_data(h2_stream *stream); + #define H2_STRM_MSG(s, msg) \ - "h2_stream(%ld-%d,%s): "msg, s->session->id, s->id, h2_stream_state_str(s) + "h2_stream(%d-%lu-%d,%s): "msg, s->session->child_num, \ + (unsigned long)s->session->id, s->id, h2_stream_state_str(s) #define H2_STRM_LOG(aplogno, s, msg) aplogno H2_STRM_MSG(s, msg) diff --git a/modules/http2/h2_switch.c b/modules/http2/h2_switch.c index 5e73568..3799701 100644 --- a/modules/http2/h2_switch.c +++ b/modules/http2/h2_switch.c @@ -25,14 +25,17 @@ #include <http_config.h> #include <http_connection.h> #include <http_protocol.h> +#include <http_ssl.h> #include <http_log.h> #include "h2_private.h" +#include "h2.h" #include "h2_config.h" -#include "h2_ctx.h" -#include "h2_conn.h" -#include "h2_h2.h" +#include "h2_conn_ctx.h" +#include "h2_c1.h" +#include "h2_c2.h" +#include "h2_protocol.h" #include "h2_switch.h" /******************************************************************************* @@ -52,10 +55,9 @@ static int h2_protocol_propose(conn_rec *c, request_rec *r, apr_array_header_t *proposals) { int proposed = 0; - int is_tls = h2_h2_is_tls(c); - const char **protos = is_tls? h2_tls_protos : h2_clear_protos; + int is_tls = ap_ssl_conn_is_ssl(c); + const char **protos = is_tls? h2_protocol_ids_tls : h2_protocol_ids_clear; - (void)s; if (!h2_mpm_supported()) { return DECLINED; } @@ -68,7 +70,7 @@ static int h2_protocol_propose(conn_rec *c, request_rec *r, return DECLINED; } - if (!h2_is_acceptable_connection(c, 0)) { + if (!h2_protocol_is_acceptable_c1(c, r, 0)) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03084) "protocol propose: connection requirements not met"); return DECLINED; @@ -81,7 +83,7 @@ static int h2_protocol_propose(conn_rec *c, request_rec *r, */ const char *p; - if (!h2_allows_h2_upgrade(c)) { + if (!h2_c1_can_upgrade(r)) { return DECLINED; } @@ -102,9 +104,10 @@ static int h2_protocol_propose(conn_rec *c, request_rec *r, /* We also allow switching only for requests that have no body. */ p = apr_table_get(r->headers_in, "Content-Length"); - if (p && strcmp(p, "0")) { + if ((p && strcmp(p, "0")) + || (!p && apr_table_get(r->headers_in, "Transfer-Encoding"))) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03087) - "upgrade with content-length: %s, declined", p); + "upgrade with body declined"); return DECLINED; } } @@ -124,11 +127,35 @@ static int h2_protocol_propose(conn_rec *c, request_rec *r, return proposed? DECLINED : OK; } +#if AP_HAS_RESPONSE_BUCKETS +static void remove_output_filters_below(ap_filter_t *f, ap_filter_type ftype) +{ + ap_filter_t *fnext; + + while (f && f->frec->ftype < ftype) { + fnext = f->next; + ap_remove_output_filter(f); + f = fnext; + } +} + +static void remove_input_filters_below(ap_filter_t *f, ap_filter_type ftype) +{ + ap_filter_t *fnext; + + while (f && f->frec->ftype < ftype) { + fnext = f->next; + ap_remove_input_filter(f); + f = fnext; + } +} +#endif + static int h2_protocol_switch(conn_rec *c, request_rec *r, server_rec *s, const char *protocol) { int found = 0; - const char **protos = h2_h2_is_tls(c)? h2_tls_protos : h2_clear_protos; + const char **protos = ap_ssl_conn_is_ssl(c)? h2_protocol_ids_tls : h2_protocol_ids_clear; const char **p = protos; (void)s; @@ -145,35 +172,41 @@ static int h2_protocol_switch(conn_rec *c, request_rec *r, server_rec *s, } if (found) { - h2_ctx *ctx = h2_ctx_get(c, 1); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "switching protocol to '%s'", protocol); - h2_ctx_protocol_set(ctx, protocol); - h2_ctx_server_set(ctx, s); - + h2_conn_ctx_create_for_c1(c, s, protocol); + if (r != NULL) { apr_status_t status; +#if AP_HAS_RESPONSE_BUCKETS + /* Switching in the middle of a request means that + * we have to send out the response to this one in h2 + * format. So we need to take over the connection + * and remove all old filters with type up to the + * CONNEDCTION/NETWORK ones. + */ + remove_input_filters_below(r->input_filters, AP_FTYPE_CONNECTION); + remove_output_filters_below(r->output_filters, AP_FTYPE_CONNECTION); +#else /* Switching in the middle of a request means that * we have to send out the response to this one in h2 * format. So we need to take over the connection * right away. */ ap_remove_input_filter_byhandle(r->input_filters, "http_in"); - ap_remove_input_filter_byhandle(r->input_filters, "reqtimeout"); ap_remove_output_filter_byhandle(r->output_filters, "HTTP_HEADER"); - +#endif /* Ok, start an h2_conn on this one. */ - h2_ctx_server_set(ctx, r->server); - status = h2_conn_setup(ctx, r->connection, r); + status = h2_c1_setup(c, r, s); + if (status != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r, APLOGNO(03088) "session setup"); - h2_ctx_clear(c); + h2_conn_ctx_detach(c); return !OK; } - h2_conn_run(ctx, c); + h2_c1_run(c); } return OK; } @@ -183,7 +216,13 @@ static int h2_protocol_switch(conn_rec *c, request_rec *r, server_rec *s, static const char *h2_protocol_get(const conn_rec *c) { - return h2_ctx_protocol_get(c); + h2_conn_ctx_t *ctx; + + if (c->master) { + c = c->master; + } + ctx = h2_conn_ctx_get(c); + return ctx? ctx->protocol : NULL; } void h2_switch_register_hooks(void) diff --git a/modules/http2/h2_task.c b/modules/http2/h2_task.c deleted file mode 100644 index 86fb026..0000000 --- a/modules/http2/h2_task.c +++ /dev/null @@ -1,769 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <assert.h> -#include <stddef.h> - -#include <apr_atomic.h> -#include <apr_strings.h> - -#include <httpd.h> -#include <http_core.h> -#include <http_connection.h> -#include <http_protocol.h> -#include <http_request.h> -#include <http_log.h> -#include <http_vhost.h> -#include <util_filter.h> -#include <ap_mpm.h> -#include <mod_core.h> -#include <scoreboard.h> - -#include "h2_private.h" -#include "h2.h" -#include "h2_bucket_beam.h" -#include "h2_conn.h" -#include "h2_config.h" -#include "h2_ctx.h" -#include "h2_from_h1.h" -#include "h2_h2.h" -#include "h2_mplx.h" -#include "h2_request.h" -#include "h2_headers.h" -#include "h2_session.h" -#include "h2_stream.h" -#include "h2_task.h" -#include "h2_util.h" - -static void H2_TASK_OUT_LOG(int lvl, h2_task *task, apr_bucket_brigade *bb, - const char *tag) -{ - if (APLOG_C_IS_LEVEL(task->c, lvl)) { - conn_rec *c = task->c; - char buffer[4 * 1024]; - const char *line = "(null)"; - apr_size_t len, bmax = sizeof(buffer)/sizeof(buffer[0]); - - len = h2_util_bb_print(buffer, bmax, tag, "", bb); - ap_log_cerror(APLOG_MARK, lvl, 0, c, "bb_dump(%s): %s", - task->id, len? buffer : line); - } -} - -/******************************************************************************* - * task input handling - ******************************************************************************/ - -static int input_ser_header(void *ctx, const char *name, const char *value) -{ - h2_task *task = ctx; - apr_brigade_printf(task->input.bb, NULL, NULL, "%s: %s\r\n", name, value); - return 1; -} - -/******************************************************************************* - * task output handling - ******************************************************************************/ - -static apr_status_t open_output(h2_task *task) -{ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, APLOGNO(03348) - "h2_task(%s): open output to %s %s %s", - task->id, task->request->method, - task->request->authority, - task->request->path); - task->output.opened = 1; - return h2_mplx_out_open(task->mplx, task->stream_id, task->output.beam); -} - -static apr_status_t send_out(h2_task *task, apr_bucket_brigade* bb, int block) -{ - apr_off_t written, left; - apr_status_t status; - - apr_brigade_length(bb, 0, &written); - H2_TASK_OUT_LOG(APLOG_TRACE2, task, bb, "h2_task send_out"); - h2_beam_log(task->output.beam, task->c, APLOG_TRACE2, "send_out(before)"); - /* engines send unblocking */ - status = h2_beam_send(task->output.beam, bb, - block? APR_BLOCK_READ : APR_NONBLOCK_READ); - h2_beam_log(task->output.beam, task->c, APLOG_TRACE2, "send_out(after)"); - - if (APR_STATUS_IS_EAGAIN(status)) { - apr_brigade_length(bb, 0, &left); - written -= left; - status = APR_SUCCESS; - } - if (status == APR_SUCCESS) { - if (h2_task_logio_add_bytes_out) { - h2_task_logio_add_bytes_out(task->c, written); - } - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, task->c, - "h2_task(%s): send_out done", task->id); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, task->c, - "h2_task(%s): send_out (%ld bytes)", - task->id, (long)written); - } - return status; -} - -/* Bring the data from the brigade (which represents the result of the - * request_rec out filter chain) into the h2_mplx for further sending - * on the master connection. - */ -static apr_status_t slave_out(h2_task *task, ap_filter_t* f, - apr_bucket_brigade* bb) -{ - apr_bucket *b; - apr_status_t rv = APR_SUCCESS; - int flush = 0, blocking; - - if (task->frozen) { - h2_util_bb_log(task->c, task->stream_id, APLOG_TRACE2, - "frozen task output write, ignored", bb); - while (!APR_BRIGADE_EMPTY(bb)) { - b = APR_BRIGADE_FIRST(bb); - if (AP_BUCKET_IS_EOR(b)) { - APR_BUCKET_REMOVE(b); - task->eor = b; - } - else { - apr_bucket_delete(b); - } - } - return APR_SUCCESS; - } - -send: - /* we send block once we opened the output, so someone is there - * reading it *and* the task is not assigned to a h2_req_engine */ - blocking = (!task->assigned && task->output.opened); - for (b = APR_BRIGADE_FIRST(bb); - b != APR_BRIGADE_SENTINEL(bb); - b = APR_BUCKET_NEXT(b)) { - if (APR_BUCKET_IS_FLUSH(b) || APR_BUCKET_IS_EOS(b) || AP_BUCKET_IS_EOR(b)) { - flush = 1; - break; - } - } - - if (task->output.bb && !APR_BRIGADE_EMPTY(task->output.bb)) { - /* still have data buffered from previous attempt. - * setaside and append new data and try to pass the complete data */ - if (!APR_BRIGADE_EMPTY(bb)) { - if (APR_SUCCESS != (rv = ap_save_brigade(f, &task->output.bb, &bb, task->pool))) { - goto out; - } - } - rv = send_out(task, task->output.bb, blocking); - } - else { - /* no data buffered previously, pass brigade directly */ - rv = send_out(task, bb, blocking); - - if (APR_SUCCESS == rv && !APR_BRIGADE_EMPTY(bb)) { - /* output refused to buffer it all, time to open? */ - if (!task->output.opened && APR_SUCCESS == (rv = open_output(task))) { - /* Make another attempt to send the data. With the output open, - * the call might be blocking and send all data, so we do not need - * to save the brigade */ - goto send; - } - else if (blocking && flush) { - /* Need to keep on doing this. */ - goto send; - } - - if (APR_SUCCESS == rv) { - /* could not write all, buffer the rest */ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, task->c, APLOGNO(03405) - "h2_slave_out(%s): saving brigade", task->id); - ap_assert(NULL); - rv = ap_save_brigade(f, &task->output.bb, &bb, task->pool); - flush = 1; - } - } - } - - if (APR_SUCCESS == rv && !task->output.opened && flush) { - /* got a flush or could not write all, time to tell someone to read */ - rv = open_output(task); - } -out: - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, task->c, - "h2_slave_out(%s): slave_out leave", task->id); - return rv; -} - -static apr_status_t output_finish(h2_task *task) -{ - if (!task->output.opened) { - return open_output(task); - } - return APR_SUCCESS; -} - -/******************************************************************************* - * task slave connection filters - ******************************************************************************/ - -static apr_status_t h2_filter_slave_in(ap_filter_t* f, - apr_bucket_brigade* bb, - ap_input_mode_t mode, - apr_read_type_e block, - apr_off_t readbytes) -{ - h2_task *task; - apr_status_t status = APR_SUCCESS; - apr_bucket *b, *next; - apr_off_t bblen; - const int trace1 = APLOGctrace1(f->c); - apr_size_t rmax = ((readbytes <= APR_SIZE_MAX)? - (apr_size_t)readbytes : APR_SIZE_MAX); - - task = h2_ctx_cget_task(f->c); - ap_assert(task); - - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_slave_in(%s): read, mode=%d, block=%d, readbytes=%ld", - task->id, mode, block, (long)readbytes); - } - - if (mode == AP_MODE_INIT) { - return ap_get_brigade(f->c->input_filters, bb, mode, block, readbytes); - } - - if (f->c->aborted) { - return APR_ECONNABORTED; - } - - if (!task->input.bb) { - return APR_EOF; - } - - /* Cleanup brigades from those nasty 0 length non-meta buckets - * that apr_brigade_split_line() sometimes produces. */ - for (b = APR_BRIGADE_FIRST(task->input.bb); - b != APR_BRIGADE_SENTINEL(task->input.bb); b = next) { - next = APR_BUCKET_NEXT(b); - if (b->length == 0 && !APR_BUCKET_IS_METADATA(b)) { - apr_bucket_delete(b); - } - } - - while (APR_BRIGADE_EMPTY(task->input.bb)) { - /* Get more input data for our request. */ - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, - "h2_slave_in(%s): get more data from mplx, block=%d, " - "readbytes=%ld", task->id, block, (long)readbytes); - } - if (task->input.beam) { - status = h2_beam_receive(task->input.beam, task->input.bb, block, - 128*1024); - } - else { - status = APR_EOF; - } - - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, f->c, - "h2_slave_in(%s): read returned", task->id); - } - if (APR_STATUS_IS_EAGAIN(status) - && (mode == AP_MODE_GETLINE || block == APR_BLOCK_READ)) { - /* chunked input handling does not seem to like it if we - * return with APR_EAGAIN from a GETLINE read... - * upload 100k test on test-ser.example.org hangs */ - status = APR_SUCCESS; - } - else if (APR_STATUS_IS_EOF(status)) { - break; - } - else if (status != APR_SUCCESS) { - return status; - } - - if (trace1) { - h2_util_bb_log(f->c, task->stream_id, APLOG_TRACE2, - "input.beam recv raw", task->input.bb); - } - if (h2_task_logio_add_bytes_in) { - apr_brigade_length(bb, 0, &bblen); - h2_task_logio_add_bytes_in(f->c, bblen); - } - } - - /* Nothing there, no more data to get. Return APR_EAGAIN on - * speculative reads, this is ap_check_pipeline()'s trick to - * see if the connection needs closing. */ - if (status == APR_EOF && APR_BRIGADE_EMPTY(task->input.bb)) { - return (mode == AP_MODE_SPECULATIVE)? APR_EAGAIN : APR_EOF; - } - - if (trace1) { - h2_util_bb_log(f->c, task->stream_id, APLOG_TRACE2, - "task_input.bb", task->input.bb); - } - - if (APR_BRIGADE_EMPTY(task->input.bb)) { - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c, - "h2_slave_in(%s): no data", task->id); - } - return (block == APR_NONBLOCK_READ)? APR_EAGAIN : APR_EOF; - } - - if (mode == AP_MODE_EXHAUSTIVE) { - /* return all we have */ - APR_BRIGADE_CONCAT(bb, task->input.bb); - } - else if (mode == AP_MODE_READBYTES) { - status = h2_brigade_concat_length(bb, task->input.bb, rmax); - } - else if (mode == AP_MODE_SPECULATIVE) { - status = h2_brigade_copy_length(bb, task->input.bb, rmax); - } - else if (mode == AP_MODE_GETLINE) { - /* we are reading a single LF line, e.g. the HTTP headers. - * this has the nasty side effect to split the bucket, even - * though it ends with CRLF and creates a 0 length bucket */ - status = apr_brigade_split_line(bb, task->input.bb, block, - HUGE_STRING_LEN); - if (APLOGctrace1(f->c)) { - char buffer[1024]; - apr_size_t len = sizeof(buffer)-1; - apr_brigade_flatten(bb, buffer, &len); - buffer[len] = 0; - if (trace1) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, - "h2_slave_in(%s): getline: %s", - task->id, buffer); - } - } - } - else { - /* Hmm, well. There is mode AP_MODE_EATCRLF, but we chose not - * to support it. Seems to work. */ - ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_ENOTIMPL, f->c, - APLOGNO(03472) - "h2_slave_in(%s), unsupported READ mode %d", - task->id, mode); - status = APR_ENOTIMPL; - } - - if (trace1) { - apr_brigade_length(bb, 0, &bblen); - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, f->c, - "h2_slave_in(%s): %ld data bytes", task->id, (long)bblen); - } - return status; -} - -static apr_status_t h2_filter_slave_output(ap_filter_t* filter, - apr_bucket_brigade* brigade) -{ - h2_task *task = h2_ctx_cget_task(filter->c); - apr_status_t status; - - ap_assert(task); - status = slave_out(task, filter, brigade); - if (status != APR_SUCCESS) { - h2_task_rst(task, H2_ERR_INTERNAL_ERROR); - } - return status; -} - -static apr_status_t h2_filter_parse_h1(ap_filter_t* f, apr_bucket_brigade* bb) -{ - h2_task *task = h2_ctx_cget_task(f->c); - apr_status_t status; - - ap_assert(task); - /* There are cases where we need to parse a serialized http/1.1 - * response. One example is a 100-continue answer in serialized mode - * or via a mod_proxy setup */ - while (bb && !task->output.sent_response) { - status = h2_from_h1_parse_response(task, f, bb); - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, f->c, - "h2_task(%s): parsed response", task->id); - if (APR_BRIGADE_EMPTY(bb) || status != APR_SUCCESS) { - return status; - } - } - - return ap_pass_brigade(f->next, bb); -} - -/******************************************************************************* - * task things - ******************************************************************************/ - -int h2_task_can_redo(h2_task *task) { - if (task->input.beam && h2_beam_was_received(task->input.beam)) { - /* cannot repeat that. */ - return 0; - } - return (!strcmp("GET", task->request->method) - || !strcmp("HEAD", task->request->method) - || !strcmp("OPTIONS", task->request->method)); -} - -void h2_task_redo(h2_task *task) -{ - task->rst_error = 0; -} - -void h2_task_rst(h2_task *task, int error) -{ - task->rst_error = error; - if (task->input.beam) { - h2_beam_leave(task->input.beam); - } - if (!task->worker_done) { - h2_beam_abort(task->output.beam); - } - if (task->c) { - task->c->aborted = 1; - } -} - -/******************************************************************************* - * Register various hooks - */ -static const char *const mod_ssl[] = { "mod_ssl.c", NULL}; -static int h2_task_pre_conn(conn_rec* c, void *arg); -static int h2_task_process_conn(conn_rec* c); - -APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_in) *h2_task_logio_add_bytes_in; -APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_out) *h2_task_logio_add_bytes_out; - -void h2_task_register_hooks(void) -{ - /* This hook runs on new connections before mod_ssl has a say. - * Its purpose is to prevent mod_ssl from touching our pseudo-connections - * for streams. - */ - ap_hook_pre_connection(h2_task_pre_conn, - NULL, mod_ssl, APR_HOOK_FIRST); - /* When the connection processing actually starts, we might - * take over, if the connection is for a task. - */ - ap_hook_process_connection(h2_task_process_conn, - NULL, NULL, APR_HOOK_FIRST); - - ap_register_input_filter("H2_SLAVE_IN", h2_filter_slave_in, - NULL, AP_FTYPE_NETWORK); - ap_register_output_filter("H2_SLAVE_OUT", h2_filter_slave_output, - NULL, AP_FTYPE_NETWORK); - ap_register_output_filter("H2_PARSE_H1", h2_filter_parse_h1, - NULL, AP_FTYPE_NETWORK); - - ap_register_input_filter("H2_REQUEST", h2_filter_request_in, - NULL, AP_FTYPE_PROTOCOL); - ap_register_output_filter("H2_RESPONSE", h2_filter_headers_out, - NULL, AP_FTYPE_PROTOCOL); - ap_register_output_filter("H2_TRAILERS_OUT", h2_filter_trailers_out, - NULL, AP_FTYPE_PROTOCOL); -} - -/* post config init */ -apr_status_t h2_task_init(apr_pool_t *pool, server_rec *s) -{ - h2_task_logio_add_bytes_in = APR_RETRIEVE_OPTIONAL_FN(ap_logio_add_bytes_in); - h2_task_logio_add_bytes_out = APR_RETRIEVE_OPTIONAL_FN(ap_logio_add_bytes_out); - - return APR_SUCCESS; -} - -static int h2_task_pre_conn(conn_rec* c, void *arg) -{ - h2_ctx *ctx; - - if (!c->master) { - return OK; - } - - ctx = h2_ctx_get(c, 0); - (void)arg; - if (h2_ctx_is_task(ctx)) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, - "h2_h2, pre_connection, found stream task"); - ap_add_input_filter("H2_SLAVE_IN", NULL, NULL, c); - ap_add_output_filter("H2_PARSE_H1", NULL, NULL, c); - ap_add_output_filter("H2_SLAVE_OUT", NULL, NULL, c); - } - return OK; -} - -h2_task *h2_task_create(conn_rec *slave, int stream_id, - const h2_request *req, h2_mplx *m, - h2_bucket_beam *input, - apr_interval_time_t timeout, - apr_size_t output_max_mem) -{ - apr_pool_t *pool; - h2_task *task; - - ap_assert(slave); - ap_assert(req); - - apr_pool_create(&pool, slave->pool); - task = apr_pcalloc(pool, sizeof(h2_task)); - if (task == NULL) { - return NULL; - } - task->id = "000"; - task->stream_id = stream_id; - task->c = slave; - task->mplx = m; - task->pool = pool; - task->request = req; - task->timeout = timeout; - task->input.beam = input; - task->output.max_buffer = output_max_mem; - - return task; -} - -void h2_task_destroy(h2_task *task) -{ - if (task->output.beam) { - h2_beam_log(task->output.beam, task->c, APLOG_TRACE2, "task_destroy"); - h2_beam_destroy(task->output.beam); - task->output.beam = NULL; - } - - if (task->eor) { - apr_bucket_destroy(task->eor); - } - if (task->pool) { - apr_pool_destroy(task->pool); - } -} - -apr_status_t h2_task_do(h2_task *task, apr_thread_t *thread, int worker_id) -{ - conn_rec *c; - - ap_assert(task); - c = task->c; - task->worker_started = 1; - task->started_at = apr_time_now(); - - if (c->master) { - /* Each conn_rec->id is supposed to be unique at a point in time. Since - * some modules (and maybe external code) uses this id as an identifier - * for the request_rec they handle, it needs to be unique for slave - * connections also. - * The connection id is generated by the MPM and most MPMs use the formula - * id := (child_num * max_threads) + thread_num - * which means that there is a maximum id of about - * idmax := max_child_count * max_threads - * If we assume 2024 child processes with 2048 threads max, we get - * idmax ~= 2024 * 2048 = 2 ** 22 - * On 32 bit systems, we have not much space left, but on 64 bit systems - * (and higher?) we can use the upper 32 bits without fear of collision. - * 32 bits is just what we need, since a connection can only handle so - * many streams. - */ - int slave_id, free_bits; - - task->id = apr_psprintf(task->pool, "%ld-%d", c->master->id, - task->stream_id); - if (sizeof(unsigned long) >= 8) { - free_bits = 32; - slave_id = task->stream_id; - } - else { - /* Assume we have a more limited number of threads/processes - * and h2 workers on a 32-bit system. Use the worker instead - * of the stream id. */ - free_bits = 8; - slave_id = worker_id; - } - task->c->id = (c->master->id << free_bits)^slave_id; - c->keepalive = AP_CONN_KEEPALIVE; - } - - h2_beam_create(&task->output.beam, c->pool, task->stream_id, "output", - H2_BEAM_OWNER_SEND, 0, task->timeout); - if (!task->output.beam) { - return APR_ENOMEM; - } - - h2_beam_buffer_size_set(task->output.beam, task->output.max_buffer); - h2_beam_send_from(task->output.beam, task->pool); - - h2_ctx_create_for(c, task); - apr_table_setn(c->notes, H2_TASK_ID_NOTE, task->id); - - h2_slave_run_pre_connection(c, ap_get_conn_socket(c)); - - task->input.bb = apr_brigade_create(task->pool, c->bucket_alloc); - if (task->request->serialize) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): serialize request %s %s", - task->id, task->request->method, task->request->path); - apr_brigade_printf(task->input.bb, NULL, - NULL, "%s %s HTTP/1.1\r\n", - task->request->method, task->request->path); - apr_table_do(input_ser_header, task, task->request->headers, NULL); - apr_brigade_puts(task->input.bb, NULL, NULL, "\r\n"); - } - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): process connection", task->id); - - task->c->current_thread = thread; - ap_run_process_connection(c); - - if (task->frozen) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): process_conn returned frozen task", - task->id); - /* cleanup delayed */ - return APR_EAGAIN; - } - else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): processing done", task->id); - return output_finish(task); - } -} - -static apr_status_t h2_task_process_request(h2_task *task, conn_rec *c) -{ - const h2_request *req = task->request; - conn_state_t *cs = c->cs; - request_rec *r; - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): create request_rec", task->id); - r = h2_request_create_rec(req, c); - if (r && (r->status == HTTP_OK)) { - /* set timeouts for virtual host of request */ - if (task->timeout != r->server->timeout) { - task->timeout = r->server->timeout; - h2_beam_timeout_set(task->output.beam, task->timeout); - if (task->input.beam) { - h2_beam_timeout_set(task->input.beam, task->timeout); - } - } - - ap_update_child_status(c->sbh, SERVER_BUSY_WRITE, r); - - if (cs) { - cs->state = CONN_STATE_HANDLER; - } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): start process_request", task->id); - - /* Add the raw bytes of the request (e.g. header frame lengths to - * the logio for this request. */ - if (req->raw_bytes && h2_task_logio_add_bytes_in) { - h2_task_logio_add_bytes_in(c, req->raw_bytes); - } - - ap_process_request(r); - - if (task->frozen) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): process_request frozen", task->id); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): process_request done", task->id); - } - - /* After the call to ap_process_request, the - * request pool may have been deleted. We set - * r=NULL here to ensure that any dereference - * of r that might be added later in this function - * will result in a segfault immediately instead - * of nondeterministic failures later. - */ - if (cs) - cs->state = CONN_STATE_WRITE_COMPLETION; - r = NULL; - } - else if (!r) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): create request_rec failed, r=NULL", task->id); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s): create request_rec failed, r->status=%d", - task->id, r->status); - } - - return APR_SUCCESS; -} - -static int h2_task_process_conn(conn_rec* c) -{ - h2_ctx *ctx; - - if (!c->master) { - return DECLINED; - } - - ctx = h2_ctx_get(c, 0); - if (h2_ctx_is_task(ctx)) { - if (!ctx->task->request->serialize) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_h2, processing request directly"); - h2_task_process_request(ctx->task, c); - return DONE; - } - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "h2_task(%s), serialized handling", ctx->task->id); - } - else { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, - "slave_conn(%ld): has no task", c->id); - } - return DECLINED; -} - -apr_status_t h2_task_freeze(h2_task *task) -{ - if (!task->frozen) { - task->frozen = 1; - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, APLOGNO(03406) - "h2_task(%s), frozen", task->id); - } - return APR_SUCCESS; -} - -apr_status_t h2_task_thaw(h2_task *task) -{ - if (task->frozen) { - task->frozen = 0; - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c, APLOGNO(03407) - "h2_task(%s), thawed", task->id); - } - task->thawed = 1; - return APR_SUCCESS; -} - -int h2_task_has_thawed(h2_task *task) -{ - return task->thawed; -} diff --git a/modules/http2/h2_task.h b/modules/http2/h2_task.h deleted file mode 100644 index ab6a746..0000000 --- a/modules/http2/h2_task.h +++ /dev/null @@ -1,127 +0,0 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __mod_h2__h2_task__ -#define __mod_h2__h2_task__ - -#include <http_core.h> - -/** - * A h2_task fakes a HTTP/1.1 request from the data in a HTTP/2 stream - * (HEADER+CONT.+DATA) the module recieves. - * - * In order to answer a HTTP/2 stream, we want all Apache httpd infrastructure - * to be involved as usual, as if this stream can as a separate HTTP/1.1 - * request. The basic trickery to do so was derived from google's mod_spdy - * source. Basically, we fake a new conn_rec object, even with its own - * socket and give it to ap_process_connection(). - * - * Since h2_task instances are executed in separate threads, we may have - * different lifetimes than our h2_stream or h2_session instances. Basically, - * we would like to be as standalone as possible. - * - * Finally, to keep certain connection level filters, such as ourselves and - * especially mod_ssl ones, from messing with our data, we need a filter - * of our own to disble those. - */ - -struct h2_bucket_beam; -struct h2_conn; -struct h2_mplx; -struct h2_task; -struct h2_req_engine; -struct h2_request; -struct h2_response_parser; -struct h2_stream; -struct h2_worker; - -typedef struct h2_task h2_task; - -struct h2_task { - const char *id; - int stream_id; - conn_rec *c; - apr_pool_t *pool; - - const struct h2_request *request; - apr_interval_time_t timeout; - int rst_error; /* h2 related stream abort error */ - - struct { - struct h2_bucket_beam *beam; - unsigned int eos : 1; - apr_bucket_brigade *bb; - apr_bucket_brigade *bbchunk; - apr_off_t chunked_total; - } input; - struct { - struct h2_bucket_beam *beam; - unsigned int opened : 1; - unsigned int sent_response : 1; - unsigned int copy_files : 1; - struct h2_response_parser *rparser; - apr_bucket_brigade *bb; - apr_size_t max_buffer; - } output; - - struct h2_mplx *mplx; - - unsigned int filters_set : 1; - unsigned int frozen : 1; - unsigned int thawed : 1; - unsigned int worker_started : 1; /* h2_worker started processing */ - unsigned int worker_done : 1; /* h2_worker finished */ - - apr_time_t started_at; /* when processing started */ - apr_time_t done_at; /* when processing was done */ - apr_bucket *eor; - - struct h2_req_engine *engine; /* engine hosted by this task */ - struct h2_req_engine *assigned; /* engine that task has been assigned to */ -}; - -h2_task *h2_task_create(conn_rec *slave, int stream_id, - const h2_request *req, struct h2_mplx *m, - struct h2_bucket_beam *input, - apr_interval_time_t timeout, - apr_size_t output_max_mem); - -void h2_task_destroy(h2_task *task); - -apr_status_t h2_task_do(h2_task *task, apr_thread_t *thread, int worker_id); - -void h2_task_redo(h2_task *task); -int h2_task_can_redo(h2_task *task); - -/** - * Reset the task with the given error code, resets all input/output. - */ -void h2_task_rst(h2_task *task, int error); - -void h2_task_register_hooks(void); -/* - * One time, post config intialization. - */ -apr_status_t h2_task_init(apr_pool_t *pool, server_rec *s); - -extern APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_in) *h2_task_logio_add_bytes_in; -extern APR_OPTIONAL_FN_TYPE(ap_logio_add_bytes_out) *h2_task_logio_add_bytes_out; - -apr_status_t h2_task_freeze(h2_task *task); -apr_status_t h2_task_thaw(h2_task *task); -int h2_task_has_thawed(h2_task *task); - -#endif /* defined(__mod_h2__h2_task__) */ diff --git a/modules/http2/h2_util.c b/modules/http2/h2_util.c index 9dacd8b..8e53ceb 100644 --- a/modules/http2/h2_util.c +++ b/modules/http2/h2_util.c @@ -22,11 +22,13 @@ #include <httpd.h> #include <http_core.h> #include <http_log.h> +#include <http_protocol.h> #include <http_request.h> #include <nghttp2/nghttp2.h> #include "h2.h" +#include "h2_headers.h" #include "h2_util.h" /* h2_log2(n) iff n is a power of 2 */ @@ -55,7 +57,7 @@ unsigned char h2_log2(int n) if (!(n & 0x80000000u)) { lz += 1; } - + return 31 - lz; } @@ -75,26 +77,6 @@ size_t h2_util_hex_dump(char *buffer, size_t maxlen, return strlen(buffer); } -size_t h2_util_header_print(char *buffer, size_t maxlen, - const char *name, size_t namelen, - const char *value, size_t valuelen) -{ - size_t offset = 0; - size_t i; - for (i = 0; i < namelen && offset < maxlen; ++i, ++offset) { - buffer[offset] = name[i]; - } - for (i = 0; i < 2 && offset < maxlen; ++i, ++offset) { - buffer[offset] = ": "[i]; - } - for (i = 0; i < valuelen && offset < maxlen; ++i, ++offset) { - buffer[offset] = value[i]; - } - buffer[offset] = '\0'; - return offset; -} - - void h2_util_camel_case_header(char *s, size_t len) { size_t start = 1; @@ -104,7 +86,7 @@ void h2_util_camel_case_header(char *s, size_t len) if (s[i] >= 'a' && s[i] <= 'z') { s[i] -= 'a' - 'A'; } - + start = 0; } else if (s[i] == '-') { @@ -113,16 +95,16 @@ void h2_util_camel_case_header(char *s, size_t len) } } -/* base64 url encoding ****************************************************************************/ +/* base64 url encoding */ #define N6 (unsigned int)-1 static const unsigned int BASE64URL_UINT6[] = { /* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 0 */ - N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 1 */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 1 */ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, 62, N6, N6, /* 2 */ - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, N6, N6, N6, N6, N6, N6, /* 3 */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, N6, N6, N6, N6, N6, N6, /* 3 */ N6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 4 */ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, N6, N6, N6, N6, 63, /* 5 */ N6, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 6 */ @@ -148,7 +130,7 @@ static const unsigned char BASE64URL_CHARS[] = { #define BASE64URL_CHAR(x) BASE64URL_CHARS[ (unsigned int)(x) & 0x3fu ] -apr_size_t h2_util_base64url_decode(const char **decoded, const char *encoded, +apr_size_t h2_util_base64url_decode(const char **decoded, const char *encoded, apr_pool_t *pool) { const unsigned char *e = (const unsigned char *)encoded; @@ -156,14 +138,14 @@ apr_size_t h2_util_base64url_decode(const char **decoded, const char *encoded, unsigned char *d; unsigned int n; long len, mlen, remain, i; - + while (*p && BASE64URL_UINT6[ *p ] != N6) { ++p; } len = (int)(p - e); mlen = (len/4)*4; *decoded = apr_pcalloc(pool, (apr_size_t)len + 1); - + i = 0; d = (unsigned char*)*decoded; for (; i < mlen; i += 4) { @@ -197,14 +179,14 @@ apr_size_t h2_util_base64url_decode(const char **decoded, const char *encoded, return (apr_size_t)(mlen/4*3 + remain); } -const char *h2_util_base64url_encode(const char *data, +const char *h2_util_base64url_encode(const char *data, apr_size_t dlen, apr_pool_t *pool) { int i, len = (int)dlen; apr_size_t slen = ((dlen+2)/3)*4 + 1; /* 0 terminated */ const unsigned char *udata = (const unsigned char*)data; unsigned char *enc, *p = apr_pcalloc(pool, slen); - + enc = p; for (i = 0; i < len-2; i+= 3) { *p++ = BASE64URL_CHAR( (udata[i] >> 2) ); @@ -212,7 +194,7 @@ const char *h2_util_base64url_encode(const char *data, *p++ = BASE64URL_CHAR( (udata[i+1] << 2) + (udata[i+2] >> 6) ); *p++ = BASE64URL_CHAR( (udata[i+2]) ); } - + if (i < len) { *p++ = BASE64URL_CHAR( (udata[i] >> 2) ); if (i == (len - 1)) { @@ -248,7 +230,7 @@ h2_ihash_t *h2_ihash_create(apr_pool_t *pool, size_t offset_of_int) return ih; } -size_t h2_ihash_count(h2_ihash_t *ih) +unsigned int h2_ihash_count(h2_ihash_t *ih) { return apr_hash_count(ih->hash); } @@ -268,7 +250,7 @@ typedef struct { void *ctx; } iter_ctx; -static int ihash_iter(void *ctx, const void *key, apr_ssize_t klen, +static int ihash_iter(void *ctx, const void *key, apr_ssize_t klen, const void *val) { iter_ctx *ictx = ctx; @@ -326,7 +308,7 @@ size_t h2_ihash_shift(h2_ihash_t *ih, void **buffer, size_t max) { collect_ctx ctx; size_t i; - + ctx.ih = ih; ctx.buffer = buffer; ctx.max = max; @@ -344,19 +326,17 @@ size_t h2_ihash_shift(h2_ihash_t *ih, void **buffer, size_t max) static void iq_grow(h2_iqueue *q, int nlen); static void iq_swap(h2_iqueue *q, int i, int j); -static int iq_bubble_up(h2_iqueue *q, int i, int top, +static int iq_bubble_up(h2_iqueue *q, int i, int top, h2_iq_cmp *cmp, void *ctx); -static int iq_bubble_down(h2_iqueue *q, int i, int bottom, +static int iq_bubble_down(h2_iqueue *q, int i, int bottom, h2_iq_cmp *cmp, void *ctx); h2_iqueue *h2_iq_create(apr_pool_t *pool, int capacity) { h2_iqueue *q = apr_pcalloc(pool, sizeof(h2_iqueue)); - if (q) { - q->pool = pool; - iq_grow(q, capacity); - q->nelts = 0; - } + q->pool = pool; + iq_grow(q, capacity); + q->nelts = 0; return q; } @@ -374,7 +354,7 @@ int h2_iq_count(h2_iqueue *q) int h2_iq_add(h2_iqueue *q, int sid, h2_iq_cmp *cmp, void *ctx) { int i; - + if (h2_iq_contains(q, sid)) { return 0; } @@ -384,7 +364,7 @@ int h2_iq_add(h2_iqueue *q, int sid, h2_iq_cmp *cmp, void *ctx) i = (q->head + q->nelts) % q->nalloc; q->elts[i] = sid; ++q->nelts; - + if (cmp) { /* bubble it to the front of the queue */ iq_bubble_up(q, i, q->head, cmp, ctx); @@ -405,7 +385,7 @@ int h2_iq_remove(h2_iqueue *q, int sid) break; } } - + if (i < q->nelts) { ++i; for (; i < q->nelts; ++i) { @@ -425,23 +405,23 @@ void h2_iq_clear(h2_iqueue *q) void h2_iq_sort(h2_iqueue *q, h2_iq_cmp *cmp, void *ctx) { /* Assume that changes in ordering are minimal. This needs, - * best case, q->nelts - 1 comparisions to check that nothing + * best case, q->nelts - 1 comparisons to check that nothing * changed. */ if (q->nelts > 0) { int i, ni, prev, last; - + /* Start at the end of the queue and create a tail of sorted * entries. Make that tail one element longer in each iteration. */ last = i = (q->head + q->nelts - 1) % q->nalloc; while (i != q->head) { prev = (q->nalloc + i - 1) % q->nalloc; - + ni = iq_bubble_up(q, i, prev, cmp, ctx); if (ni == prev) { /* i bubbled one up, bubble the new i down, which - * keeps all tasks below i sorted. */ + * keeps all ints below i sorted. */ iq_bubble_down(q, i, last, cmp, ctx); } i = prev; @@ -453,21 +433,21 @@ void h2_iq_sort(h2_iqueue *q, h2_iq_cmp *cmp, void *ctx) int h2_iq_shift(h2_iqueue *q) { int sid; - + if (q->nelts <= 0) { return 0; } - + sid = q->elts[q->head]; q->head = (q->head + 1) % q->nalloc; q->nelts--; - + return sid; } size_t h2_iq_mshift(h2_iqueue *q, int *pint, size_t max) { - int i; + size_t i; for (i = 0; i < max; ++i) { pint[i] = h2_iq_shift(q); if (pint[i] == 0) { @@ -483,7 +463,7 @@ static void iq_grow(h2_iqueue *q, int nlen) int *nq = apr_pcalloc(q->pool, sizeof(int) * nlen); if (q->nelts > 0) { int l = ((q->head + q->nelts) % q->nalloc) - q->head; - + memmove(nq, q->elts + q->head, sizeof(int) * l); if (l < q->nelts) { /* elts wrapped, append elts in [0, remain] to nq */ @@ -504,11 +484,11 @@ static void iq_swap(h2_iqueue *q, int i, int j) q->elts[j] = x; } -static int iq_bubble_up(h2_iqueue *q, int i, int top, - h2_iq_cmp *cmp, void *ctx) +static int iq_bubble_up(h2_iqueue *q, int i, int top, + h2_iq_cmp *cmp, void *ctx) { int prev; - while (((prev = (q->nalloc + i - 1) % q->nalloc), i != top) + while (((prev = (q->nalloc + i - 1) % q->nalloc), i != top) && (*cmp)(q->elts[i], q->elts[prev], ctx) < 0) { iq_swap(q, prev, i); i = prev; @@ -516,11 +496,11 @@ static int iq_bubble_up(h2_iqueue *q, int i, int top, return i; } -static int iq_bubble_down(h2_iqueue *q, int i, int bottom, +static int iq_bubble_down(h2_iqueue *q, int i, int bottom, h2_iq_cmp *cmp, void *ctx) { int next; - while (((next = (q->nalloc + i + 1) % q->nalloc), i != bottom) + while (((next = (q->nalloc + i + 1) % q->nalloc), i != bottom) && (*cmp)(q->elts[i], q->elts[next], ctx) > 0) { iq_swap(q, next, i); i = next; @@ -545,9 +525,10 @@ int h2_iq_contains(h2_iqueue *q, int sid) struct h2_fifo { void **elems; - int nelems; + int capacity; int set; - int head; + int in; + int out; int count; int aborted; apr_thread_mutex_t *lock; @@ -555,12 +536,7 @@ struct h2_fifo { apr_thread_cond_t *not_full; }; -static int nth_index(h2_fifo *fifo, int n) -{ - return (fifo->head + n) % fifo->nelems; -} - -static apr_status_t fifo_destroy(void *data) +static apr_status_t fifo_destroy(void *data) { h2_fifo *fifo = data; @@ -574,21 +550,21 @@ static apr_status_t fifo_destroy(void *data) static int index_of(h2_fifo *fifo, void *elem) { int i; - - for (i = 0; i < fifo->count; ++i) { - if (elem == fifo->elems[nth_index(fifo, i)]) { + + for (i = fifo->out; i != fifo->in; i = (i + 1) % fifo->capacity) { + if (elem == fifo->elems[i]) { return i; } } return -1; } -static apr_status_t create_int(h2_fifo **pfifo, apr_pool_t *pool, +static apr_status_t create_int(h2_fifo **pfifo, apr_pool_t *pool, int capacity, int as_set) { apr_status_t rv; h2_fifo *fifo; - + fifo = apr_pcalloc(pool, sizeof(*fifo)); if (fifo == NULL) { return APR_ENOMEM; @@ -614,9 +590,9 @@ static apr_status_t create_int(h2_fifo **pfifo, apr_pool_t *pool, if (fifo->elems == NULL) { return APR_ENOMEM; } - fifo->nelems = capacity; + fifo->capacity = capacity; fifo->set = as_set; - + *pfifo = fifo; apr_pool_cleanup_register(pool, fifo, fifo_destroy, apr_pool_cleanup_null); @@ -638,15 +614,6 @@ apr_status_t h2_fifo_term(h2_fifo *fifo) apr_status_t rv; if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { fifo->aborted = 1; - apr_thread_mutex_unlock(fifo->lock); - } - return rv; -} - -apr_status_t h2_fifo_interrupt(h2_fifo *fifo) -{ - apr_status_t rv; - if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { apr_thread_cond_broadcast(fifo->not_empty); apr_thread_cond_broadcast(fifo->not_full); apr_thread_mutex_unlock(fifo->lock); @@ -656,7 +623,12 @@ apr_status_t h2_fifo_interrupt(h2_fifo *fifo) int h2_fifo_count(h2_fifo *fifo) { - return fifo->count; + int n; + + apr_thread_mutex_lock(fifo->lock); + n = fifo->count; + apr_thread_mutex_unlock(fifo->lock); + return n; } static apr_status_t check_not_empty(h2_fifo *fifo, int block) @@ -683,9 +655,9 @@ static apr_status_t fifo_push_int(h2_fifo *fifo, void *elem, int block) /* set mode, elem already member */ return APR_EEXIST; } - else if (fifo->count == fifo->nelems) { + else if (fifo->count == fifo->capacity) { if (block) { - while (fifo->count == fifo->nelems) { + while (fifo->count == fifo->capacity) { if (fifo->aborted) { return APR_EOF; } @@ -696,12 +668,14 @@ static apr_status_t fifo_push_int(h2_fifo *fifo, void *elem, int block) return APR_EAGAIN; } } - - ap_assert(fifo->count < fifo->nelems); - fifo->elems[nth_index(fifo, fifo->count)] = elem; + + fifo->elems[fifo->in++] = elem; + if (fifo->in >= fifo->capacity) { + fifo->in -= fifo->capacity; + } ++fifo->count; if (fifo->count == 1) { - apr_thread_cond_broadcast(fifo->not_empty); + apr_thread_cond_signal(fifo->not_empty); } return APR_SUCCESS; } @@ -709,10 +683,6 @@ static apr_status_t fifo_push_int(h2_fifo *fifo, void *elem, int block) static apr_status_t fifo_push(h2_fifo *fifo, void *elem, int block) { apr_status_t rv; - - if (fifo->aborted) { - return APR_EOF; - } if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { rv = fifo_push_int(fifo, elem, block); @@ -734,18 +704,20 @@ apr_status_t h2_fifo_try_push(h2_fifo *fifo, void *elem) static apr_status_t pull_head(h2_fifo *fifo, void **pelem, int block) { apr_status_t rv; - + int was_full; + if ((rv = check_not_empty(fifo, block)) != APR_SUCCESS) { *pelem = NULL; return rv; } - *pelem = fifo->elems[fifo->head]; + *pelem = fifo->elems[fifo->out++]; + if (fifo->out >= fifo->capacity) { + fifo->out -= fifo->capacity; + } + was_full = (fifo->count == fifo->capacity); --fifo->count; - if (fifo->count > 0) { - fifo->head = nth_index(fifo, 1); - if (fifo->count+1 == fifo->nelems) { - apr_thread_cond_broadcast(fifo->not_full); - } + if (was_full) { + apr_thread_cond_broadcast(fifo->not_full); } return APR_SUCCESS; } @@ -753,11 +725,7 @@ static apr_status_t pull_head(h2_fifo *fifo, void **pelem, int block) static apr_status_t fifo_pull(h2_fifo *fifo, void **pelem, int block) { apr_status_t rv; - - if (fifo->aborted) { - return APR_EOF; - } - + if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { rv = pull_head(fifo, pelem, block); apr_thread_mutex_unlock(fifo->lock); @@ -779,11 +747,11 @@ static apr_status_t fifo_peek(h2_fifo *fifo, h2_fifo_peek_fn *fn, void *ctx, int { apr_status_t rv; void *elem; - + if (fifo->aborted) { return APR_EOF; } - + if (APR_SUCCESS == (rv = apr_thread_mutex_lock(fifo->lock))) { if (APR_SUCCESS == (rv = pull_head(fifo, &elem, block))) { switch (fn(elem, ctx)) { @@ -812,28 +780,58 @@ apr_status_t h2_fifo_try_peek(h2_fifo *fifo, h2_fifo_peek_fn *fn, void *ctx) apr_status_t h2_fifo_remove(h2_fifo *fifo, void *elem) { apr_status_t rv; - + if (fifo->aborted) { return APR_EOF; } if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { - int i, rc; - void *e; - - rc = 0; - for (i = 0; i < fifo->count; ++i) { - e = fifo->elems[nth_index(fifo, i)]; - if (e == elem) { - ++rc; - } - else if (rc) { - fifo->elems[nth_index(fifo, i-rc)] = e; + int i, last_count = fifo->count; + + for (i = fifo->out; i != fifo->in; i = (i + 1) % fifo->capacity) { + if (fifo->elems[i] == elem) { + --fifo->count; + if (fifo->count == 0) { + fifo->out = fifo->in = 0; + } + else if (i == fifo->out) { + /* first element */ + ++fifo->out; + if (fifo->out >= fifo->capacity) { + fifo->out -= fifo->capacity; + } + } + else if (((i + 1) % fifo->capacity) == fifo->in) { + /* last element */ + --fifo->in; + if (fifo->in < 0) { + fifo->in += fifo->capacity; + } + } + else if (i > fifo->out) { + /* between out and in/capacity, move elements below up */ + memmove(&fifo->elems[fifo->out+1], &fifo->elems[fifo->out], + (i - fifo->out) * sizeof(void*)); + ++fifo->out; + if (fifo->out >= fifo->capacity) { + fifo->out -= fifo->capacity; + } + } + else { + /* we wrapped around, move elements above down */ + AP_DEBUG_ASSERT((fifo->in - i - 1) > 0); + AP_DEBUG_ASSERT((fifo->in - i - 1) < fifo->capacity); + memmove(&fifo->elems[i], &fifo->elems[i + 1], + (fifo->in - i - 1) * sizeof(void*)); + --fifo->in; + if (fifo->in < 0) { + fifo->in += fifo->capacity; + } + } } } - if (rc) { - fifo->count -= rc; - if (fifo->count + rc == fifo->nelems) { + if (fifo->count != last_count) { + if (last_count == fifo->capacity) { apr_thread_cond_broadcast(fifo->not_full); } rv = APR_SUCCESS; @@ -841,7 +839,7 @@ apr_status_t h2_fifo_remove(h2_fifo *fifo, void *elem) else { rv = APR_EAGAIN; } - + apr_thread_mutex_unlock(fifo->lock); } return rv; @@ -853,7 +851,7 @@ apr_status_t h2_fifo_remove(h2_fifo *fifo, void *elem) struct h2_ififo { int *elems; - int nelems; + int capacity; int set; int head; int count; @@ -863,12 +861,12 @@ struct h2_ififo { apr_thread_cond_t *not_full; }; -static int inth_index(h2_ififo *fifo, int n) +static int inth_index(h2_ififo *fifo, int n) { - return (fifo->head + n) % fifo->nelems; + return (fifo->head + n) % fifo->capacity; } -static apr_status_t ififo_destroy(void *data) +static apr_status_t ififo_destroy(void *data) { h2_ififo *fifo = data; @@ -882,7 +880,7 @@ static apr_status_t ififo_destroy(void *data) static int iindex_of(h2_ififo *fifo, int id) { int i; - + for (i = 0; i < fifo->count; ++i) { if (id == fifo->elems[inth_index(fifo, i)]) { return i; @@ -891,12 +889,12 @@ static int iindex_of(h2_ififo *fifo, int id) return -1; } -static apr_status_t icreate_int(h2_ififo **pfifo, apr_pool_t *pool, +static apr_status_t icreate_int(h2_ififo **pfifo, apr_pool_t *pool, int capacity, int as_set) { apr_status_t rv; h2_ififo *fifo; - + fifo = apr_pcalloc(pool, sizeof(*fifo)); if (fifo == NULL) { return APR_ENOMEM; @@ -922,9 +920,9 @@ static apr_status_t icreate_int(h2_ififo **pfifo, apr_pool_t *pool, if (fifo->elems == NULL) { return APR_ENOMEM; } - fifo->nelems = capacity; + fifo->capacity = capacity; fifo->set = as_set; - + *pfifo = fifo; apr_pool_cleanup_register(pool, fifo, ififo_destroy, apr_pool_cleanup_null); @@ -946,15 +944,6 @@ apr_status_t h2_ififo_term(h2_ififo *fifo) apr_status_t rv; if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { fifo->aborted = 1; - apr_thread_mutex_unlock(fifo->lock); - } - return rv; -} - -apr_status_t h2_ififo_interrupt(h2_ififo *fifo) -{ - apr_status_t rv; - if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { apr_thread_cond_broadcast(fifo->not_empty); apr_thread_cond_broadcast(fifo->not_full); apr_thread_mutex_unlock(fifo->lock); @@ -991,9 +980,9 @@ static apr_status_t ififo_push_int(h2_ififo *fifo, int id, int block) /* set mode, elem already member */ return APR_EEXIST; } - else if (fifo->count == fifo->nelems) { + else if (fifo->count == fifo->capacity) { if (block) { - while (fifo->count == fifo->nelems) { + while (fifo->count == fifo->capacity) { if (fifo->aborted) { return APR_EOF; } @@ -1004,8 +993,8 @@ static apr_status_t ififo_push_int(h2_ififo *fifo, int id, int block) return APR_EAGAIN; } } - - ap_assert(fifo->count < fifo->nelems); + + ap_assert(fifo->count < fifo->capacity); fifo->elems[inth_index(fifo, fifo->count)] = id; ++fifo->count; if (fifo->count == 1) { @@ -1017,10 +1006,6 @@ static apr_status_t ififo_push_int(h2_ififo *fifo, int id, int block) static apr_status_t ififo_push(h2_ififo *fifo, int id, int block) { apr_status_t rv; - - if (fifo->aborted) { - return APR_EOF; - } if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { rv = ififo_push_int(fifo, id, block); @@ -1042,7 +1027,7 @@ apr_status_t h2_ififo_try_push(h2_ififo *fifo, int id) static apr_status_t ipull_head(h2_ififo *fifo, int *pi, int block) { apr_status_t rv; - + if ((rv = icheck_not_empty(fifo, block)) != APR_SUCCESS) { *pi = 0; return rv; @@ -1051,7 +1036,7 @@ static apr_status_t ipull_head(h2_ififo *fifo, int *pi, int block) --fifo->count; if (fifo->count > 0) { fifo->head = inth_index(fifo, 1); - if (fifo->count+1 == fifo->nelems) { + if (fifo->count+1 == fifo->capacity) { apr_thread_cond_broadcast(fifo->not_full); } } @@ -1061,11 +1046,7 @@ static apr_status_t ipull_head(h2_ififo *fifo, int *pi, int block) static apr_status_t ififo_pull(h2_ififo *fifo, int *pi, int block) { apr_status_t rv; - - if (fifo->aborted) { - return APR_EOF; - } - + if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { rv = ipull_head(fifo, pi, block); apr_thread_mutex_unlock(fifo->lock); @@ -1087,11 +1068,7 @@ static apr_status_t ififo_peek(h2_ififo *fifo, h2_ififo_peek_fn *fn, void *ctx, { apr_status_t rv; int id; - - if (fifo->aborted) { - return APR_EOF; - } - + if (APR_SUCCESS == (rv = apr_thread_mutex_lock(fifo->lock))) { if (APR_SUCCESS == (rv = ipull_head(fifo, &id, block))) { switch (fn(id, ctx)) { @@ -1117,39 +1094,40 @@ apr_status_t h2_ififo_try_peek(h2_ififo *fifo, h2_ififo_peek_fn *fn, void *ctx) return ififo_peek(fifo, fn, ctx, 0); } -apr_status_t h2_ififo_remove(h2_ififo *fifo, int id) +static apr_status_t ififo_remove(h2_ififo *fifo, int id) { - apr_status_t rv; - + int rc, i; + if (fifo->aborted) { return APR_EOF; } - if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { - int i, rc; - int e; - - rc = 0; - for (i = 0; i < fifo->count; ++i) { - e = fifo->elems[inth_index(fifo, i)]; - if (e == id) { - ++rc; - } - else if (rc) { - fifo->elems[inth_index(fifo, i-rc)] = e; - } - } - if (rc) { - fifo->count -= rc; - if (fifo->count + rc == fifo->nelems) { - apr_thread_cond_broadcast(fifo->not_full); - } - rv = APR_SUCCESS; + rc = 0; + for (i = 0; i < fifo->count; ++i) { + int e = fifo->elems[inth_index(fifo, i)]; + if (e == id) { + ++rc; } - else { - rv = APR_EAGAIN; + else if (rc) { + fifo->elems[inth_index(fifo, i-rc)] = e; } - + } + if (!rc) { + return APR_EAGAIN; + } + fifo->count -= rc; + if (fifo->count + rc == fifo->capacity) { + apr_thread_cond_broadcast(fifo->not_full); + } + return APR_SUCCESS; +} + +apr_status_t h2_ififo_remove(h2_ififo *fifo, int id) +{ + apr_status_t rv; + + if ((rv = apr_thread_mutex_lock(fifo->lock)) == APR_SUCCESS) { + rv = ififo_remove(fifo, id); apr_thread_mutex_unlock(fifo->lock); } return rv; @@ -1158,7 +1136,7 @@ apr_status_t h2_ififo_remove(h2_ififo *fifo, int id) /******************************************************************************* * h2_util for apt_table_t ******************************************************************************/ - + typedef struct { apr_size_t bytes; apr_size_t pair_extra; @@ -1180,7 +1158,7 @@ static int count_bytes(void *x, const char *key, const char *value) apr_size_t h2_util_table_bytes(apr_table_t *t, apr_size_t pair_extra) { table_bytes_ctx ctx; - + ctx.bytes = 0; ctx.pair_extra = pair_extra; apr_table_do(count_bytes, &ctx, t, NULL); @@ -1192,287 +1170,108 @@ apr_size_t h2_util_table_bytes(apr_table_t *t, apr_size_t pair_extra) * h2_util for bucket brigades ******************************************************************************/ -static apr_status_t last_not_included(apr_bucket_brigade *bb, - apr_off_t maxlen, - int same_alloc, - apr_size_t *pfile_buckets_allowed, - apr_bucket **pend) +static void fit_bucket_into(apr_bucket *b, apr_off_t *plen) { - apr_bucket *b; - apr_status_t status = APR_SUCCESS; - int files_allowed = pfile_buckets_allowed? (int)*pfile_buckets_allowed : 0; - - if (maxlen >= 0) { - /* Find the bucket, up to which we reach maxlen/mem bytes */ - for (b = APR_BRIGADE_FIRST(bb); - (b != APR_BRIGADE_SENTINEL(bb)); - b = APR_BUCKET_NEXT(b)) { - - if (APR_BUCKET_IS_METADATA(b)) { - /* included */ - } - else { - if (b->length == ((apr_size_t)-1)) { - const char *ign; - apr_size_t ilen; - status = apr_bucket_read(b, &ign, &ilen, APR_BLOCK_READ); - if (status != APR_SUCCESS) { - return status; - } - } - - if (maxlen == 0 && b->length > 0) { - *pend = b; - return status; - } - - if (same_alloc && APR_BUCKET_IS_FILE(b)) { - /* we like it move it, always */ - } - else if (files_allowed > 0 && APR_BUCKET_IS_FILE(b)) { - /* this has no memory footprint really unless - * it is read, disregard it in length count, - * unless we do not move the file buckets */ - --files_allowed; - } - else if (maxlen < (apr_off_t)b->length) { - apr_bucket_split(b, (apr_size_t)maxlen); - maxlen = 0; - } - else { - maxlen -= b->length; - } - } - } + /* signed apr_off_t is at least as large as unsigned apr_size_t. + * Problems may arise when they are both the same size. Then + * the bucket length *may* be larger than a value we can hold + * in apr_off_t. Before casting b->length to apr_off_t we must + * check the limitations. + * After we resized the bucket, it is safe to cast and substract. + */ + if ((sizeof(apr_off_t) == sizeof(apr_int64_t) + && b->length > APR_INT64_MAX) + || (sizeof(apr_off_t) == sizeof(apr_int32_t) + && b->length > APR_INT32_MAX) + || *plen < (apr_off_t)b->length) { + /* bucket is longer the *plen */ + apr_bucket_split(b, *plen); } - *pend = APR_BRIGADE_SENTINEL(bb); - return status; + *plen -= (apr_off_t)b->length; } -apr_status_t h2_brigade_concat_length(apr_bucket_brigade *dest, +apr_status_t h2_brigade_concat_length(apr_bucket_brigade *dest, apr_bucket_brigade *src, apr_off_t length) { apr_bucket *b; apr_off_t remain = length; apr_status_t status = APR_SUCCESS; - + while (!APR_BRIGADE_EMPTY(src)) { - b = APR_BRIGADE_FIRST(src); - + b = APR_BRIGADE_FIRST(src); + if (APR_BUCKET_IS_METADATA(b)) { APR_BUCKET_REMOVE(b); APR_BRIGADE_INSERT_TAIL(dest, b); } else { - if (remain == b->length) { - /* fall through */ - } - else if (remain <= 0) { + if (remain <= 0) { return status; } - else { - if (b->length == ((apr_size_t)-1)) { - const char *ign; - apr_size_t ilen; - status = apr_bucket_read(b, &ign, &ilen, APR_BLOCK_READ); - if (status != APR_SUCCESS) { - return status; - } - } - - if (remain < b->length) { - apr_bucket_split(b, remain); + if (b->length == ((apr_size_t)-1)) { + const char *ign; + apr_size_t ilen; + status = apr_bucket_read(b, &ign, &ilen, APR_BLOCK_READ); + if (status != APR_SUCCESS) { + return status; } } + fit_bucket_into(b, &remain); APR_BUCKET_REMOVE(b); APR_BRIGADE_INSERT_TAIL(dest, b); - remain -= b->length; } } return status; } -apr_status_t h2_brigade_copy_length(apr_bucket_brigade *dest, +apr_status_t h2_brigade_copy_length(apr_bucket_brigade *dest, apr_bucket_brigade *src, apr_off_t length) { apr_bucket *b, *next; apr_off_t remain = length; apr_status_t status = APR_SUCCESS; - - for (b = APR_BRIGADE_FIRST(src); + + for (b = APR_BRIGADE_FIRST(src); b != APR_BRIGADE_SENTINEL(src); b = next) { next = APR_BUCKET_NEXT(b); - + if (APR_BUCKET_IS_METADATA(b)) { /* fall through */ } else { - if (remain == b->length) { - /* fall through */ - } - else if (remain <= 0) { + if (remain <= 0) { return status; } - else { - if (b->length == ((apr_size_t)-1)) { - const char *ign; - apr_size_t ilen; - status = apr_bucket_read(b, &ign, &ilen, APR_BLOCK_READ); - if (status != APR_SUCCESS) { - return status; - } - } - - if (remain < b->length) { - apr_bucket_split(b, remain); + if (b->length == ((apr_size_t)-1)) { + const char *ign; + apr_size_t ilen; + status = apr_bucket_read(b, &ign, &ilen, APR_BLOCK_READ); + if (status != APR_SUCCESS) { + return status; } } + fit_bucket_into(b, &remain); } status = apr_bucket_copy(b, &b); if (status != APR_SUCCESS) { return status; } APR_BRIGADE_INSERT_TAIL(dest, b); - remain -= b->length; - } - return status; -} - -int h2_util_has_eos(apr_bucket_brigade *bb, apr_off_t len) -{ - apr_bucket *b, *end; - - apr_status_t status = last_not_included(bb, len, 0, 0, &end); - if (status != APR_SUCCESS) { - return status; - } - - for (b = APR_BRIGADE_FIRST(bb); - b != APR_BRIGADE_SENTINEL(bb) && b != end; - b = APR_BUCKET_NEXT(b)) - { - if (APR_BUCKET_IS_EOS(b)) { - return 1; - } - } - return 0; -} - -apr_status_t h2_util_bb_avail(apr_bucket_brigade *bb, - apr_off_t *plen, int *peos) -{ - apr_status_t status; - apr_off_t blen = 0; - - /* test read to determine available length */ - status = apr_brigade_length(bb, 1, &blen); - if (status != APR_SUCCESS) { - return status; - } - else if (blen == 0) { - /* brigade without data, does it have an EOS bucket somwhere? */ - *plen = 0; - *peos = h2_util_has_eos(bb, -1); - } - else { - /* data in the brigade, limit the length returned. Check for EOS - * bucket only if we indicate data. This is required since plen == 0 - * means "the whole brigade" for h2_util_hash_eos() - */ - if (blen < *plen || *plen < 0) { - *plen = blen; - } - *peos = h2_util_has_eos(bb, *plen); - } - return APR_SUCCESS; -} - -apr_status_t h2_util_bb_readx(apr_bucket_brigade *bb, - h2_util_pass_cb *cb, void *ctx, - apr_off_t *plen, int *peos) -{ - apr_status_t status = APR_SUCCESS; - int consume = (cb != NULL); - apr_off_t written = 0; - apr_off_t avail = *plen; - apr_bucket *next, *b; - - /* Pass data in our brigade through the callback until the length - * is satisfied or we encounter an EOS. - */ - *peos = 0; - for (b = APR_BRIGADE_FIRST(bb); - (status == APR_SUCCESS) && (b != APR_BRIGADE_SENTINEL(bb)); - b = next) { - - if (APR_BUCKET_IS_METADATA(b)) { - if (APR_BUCKET_IS_EOS(b)) { - *peos = 1; - } - else { - /* ignore */ - } - } - else if (avail <= 0) { - break; - } - else { - const char *data = NULL; - apr_size_t data_len; - - if (b->length == ((apr_size_t)-1)) { - /* read to determine length */ - status = apr_bucket_read(b, &data, &data_len, APR_NONBLOCK_READ); - } - else { - data_len = b->length; - } - - if (data_len > avail) { - apr_bucket_split(b, avail); - data_len = (apr_size_t)avail; - } - - if (consume) { - if (!data) { - status = apr_bucket_read(b, &data, &data_len, - APR_NONBLOCK_READ); - } - if (status == APR_SUCCESS) { - status = cb(ctx, data, data_len); - } - } - else { - data_len = b->length; - } - avail -= data_len; - written += data_len; - } - - next = APR_BUCKET_NEXT(b); - if (consume) { - apr_bucket_delete(b); - } - } - - *plen = written; - if (status == APR_SUCCESS && !*peos && !*plen) { - return APR_EAGAIN; } return status; } -apr_size_t h2_util_bucket_print(char *buffer, apr_size_t bmax, +apr_size_t h2_util_bucket_print(char *buffer, apr_size_t bmax, apr_bucket *b, const char *sep) { apr_size_t off = 0; if (sep && *sep) { off += apr_snprintf(buffer+off, bmax-off, "%s", sep); } - + if (bmax <= off) { return off; } @@ -1480,30 +1279,30 @@ apr_size_t h2_util_bucket_print(char *buffer, apr_size_t bmax, off += apr_snprintf(buffer+off, bmax-off, "%s", b->type->name); } else if (bmax > off) { - off += apr_snprintf(buffer+off, bmax-off, "%s[%ld]", - b->type->name, - (long)(b->length == ((apr_size_t)-1)? - -1 : b->length)); + off += apr_snprintf(buffer+off, bmax-off, "%s[%ld]", + b->type->name, + (b->length == ((apr_size_t)-1)? + -1 : (long)b->length)); } return off; } -apr_size_t h2_util_bb_print(char *buffer, apr_size_t bmax, - const char *tag, const char *sep, +apr_size_t h2_util_bb_print(char *buffer, apr_size_t bmax, + const char *tag, const char *sep, apr_bucket_brigade *bb) { apr_size_t off = 0; const char *sp = ""; apr_bucket *b; - + if (bmax > 1) { if (bb) { memset(buffer, 0, bmax--); off += apr_snprintf(buffer+off, bmax-off, "%s(", tag); - for (b = APR_BRIGADE_FIRST(bb); + for (b = APR_BRIGADE_FIRST(bb); (bmax > off) && (b != APR_BRIGADE_SENTINEL(bb)); b = APR_BUCKET_NEXT(b)) { - + off += h2_util_bucket_print(buffer+off, bmax-off, b, sp); sp = " "; } @@ -1519,20 +1318,21 @@ apr_size_t h2_util_bb_print(char *buffer, apr_size_t bmax, } apr_status_t h2_append_brigade(apr_bucket_brigade *to, - apr_bucket_brigade *from, + apr_bucket_brigade *from, apr_off_t *plen, int *peos, h2_bucket_gate *should_append) { apr_bucket *e; - apr_off_t len = 0, remain = *plen; + apr_off_t start, remain; apr_status_t rv; *peos = 0; - + start = remain = *plen; + while (!APR_BRIGADE_EMPTY(from)) { e = APR_BRIGADE_FIRST(from); - + if (!should_append(e)) { goto leave; } @@ -1543,8 +1343,11 @@ apr_status_t h2_append_brigade(apr_bucket_brigade *to, continue; } } - else { - if (remain > 0 && e->length == ((apr_size_t)-1)) { + else { + if (remain <= 0) { + goto leave; + } + if (e->length == ((apr_size_t)-1)) { const char *ign; apr_size_t ilen; rv = apr_bucket_read(e, &ign, &ilen, APR_BLOCK_READ); @@ -1552,22 +1355,13 @@ apr_status_t h2_append_brigade(apr_bucket_brigade *to, return rv; } } - - if (remain < e->length) { - if (remain <= 0) { - goto leave; - } - apr_bucket_split(e, (apr_size_t)remain); - } + fit_bucket_into(e, &remain); } - APR_BUCKET_REMOVE(e); APR_BRIGADE_INSERT_TAIL(to, e); - len += e->length; - remain -= e->length; } leave: - *plen = len; + *plen = start - remain; return APR_SUCCESS; } @@ -1595,20 +1389,10 @@ apr_off_t h2_brigade_mem_size(apr_bucket_brigade *bb) /******************************************************************************* * h2_ngheader ******************************************************************************/ - -int h2_util_ignore_header(const char *name) -{ - /* never forward, ch. 8.1.2.2 */ - return (H2_HD_MATCH_LIT_CS("connection", name) - || H2_HD_MATCH_LIT_CS("proxy-connection", name) - || H2_HD_MATCH_LIT_CS("upgrade", name) - || H2_HD_MATCH_LIT_CS("keep-alive", name) - || H2_HD_MATCH_LIT_CS("transfer-encoding", name)); -} static int count_header(void *ctx, const char *key, const char *value) { - if (!h2_util_ignore_header(key)) { + if (!h2_util_ignore_resp_header(key)) { (*((size_t*)ctx))++; } return 1; @@ -1629,6 +1413,17 @@ static const char *inv_field_value_chr(const char *token) return (p && *p)? p : NULL; } +static void strip_field_value_ws(nghttp2_nv *nv) +{ + while(nv->valuelen && (nv->value[0] == ' ' || nv->value[0] == '\t')) { + nv->value++; nv->valuelen--; + } + while(nv->valuelen && (nv->value[nv->valuelen-1] == ' ' + || nv->value[nv->valuelen-1] == '\t')) { + nv->valuelen--; + } +} + typedef struct ngh_ctx { apr_pool_t *p; int unsafe; @@ -1644,14 +1439,14 @@ static int add_header(ngh_ctx *ctx, const char *key, const char *value) if (!ctx->unsafe) { if ((p = inv_field_name_chr(key))) { ap_log_perror(APLOG_MARK, APLOG_TRACE1, APR_EINVAL, ctx->p, - "h2_request: head field '%s: %s' has invalid char %s", + "h2_request: head field '%s: %s' has invalid char %s", key, value, p); ctx->status = APR_EINVAL; return 0; } if ((p = inv_field_value_chr(value))) { ap_log_perror(APLOG_MARK, APLOG_TRACE1, APR_EINVAL, ctx->p, - "h2_request: head field '%s: %s' has invalid char %s", + "h2_request: head field '%s: %s' has invalid char %s", key, value, p); ctx->status = APR_EINVAL; return 0; @@ -1661,69 +1456,100 @@ static int add_header(ngh_ctx *ctx, const char *key, const char *value) nv->namelen = strlen(key); nv->value = (uint8_t*)value; nv->valuelen = strlen(value); - + strip_field_value_ws(nv); + return 1; } static int add_table_header(void *ctx, const char *key, const char *value) { - if (!h2_util_ignore_header(key)) { + if (!h2_util_ignore_resp_header(key)) { add_header(ctx, key, value); } return 1; } -static apr_status_t ngheader_create(h2_ngheader **ph, apr_pool_t *p, - int unsafe, size_t key_count, +static apr_status_t ngheader_create(h2_ngheader **ph, apr_pool_t *p, + int unsafe, size_t key_count, const char *keys[], const char *values[], apr_table_t *headers) { ngh_ctx ctx; size_t n, i; - + ctx.p = p; ctx.unsafe = unsafe; - + n = key_count; apr_table_do(count_header, &n, headers, NULL); - + *ph = ctx.ngh = apr_pcalloc(p, sizeof(h2_ngheader)); if (!ctx.ngh) { return APR_ENOMEM; } - - ctx.ngh->nv = apr_pcalloc(p, n * sizeof(nghttp2_nv)); + + ctx.ngh->nv = apr_pcalloc(p, n * sizeof(nghttp2_nv)); if (!ctx.ngh->nv) { return APR_ENOMEM; } - + ctx.status = APR_SUCCESS; for (i = 0; i < key_count; ++i) { if (!add_header(&ctx, keys[i], values[i])) { return ctx.status; } } - + apr_table_do(add_table_header, &ctx, headers, NULL); return ctx.status; } +#if AP_HAS_RESPONSE_BUCKETS + +static int is_unsafe(ap_bucket_response *h) +{ + const char *v = h->notes? apr_table_get(h->notes, H2_HDR_CONFORMANCE) : NULL; + return (v && !strcmp(v, H2_HDR_CONFORMANCE_UNSAFE)); +} + +apr_status_t h2_res_create_ngtrailer(h2_ngheader **ph, apr_pool_t *p, + ap_bucket_headers *headers) +{ + return ngheader_create(ph, p, 0, + 0, NULL, NULL, headers->headers); +} + +apr_status_t h2_res_create_ngheader(h2_ngheader **ph, apr_pool_t *p, + ap_bucket_response *response) +{ + const char *keys[] = { + ":status" + }; + const char *values[] = { + apr_psprintf(p, "%d", response->status) + }; + return ngheader_create(ph, p, is_unsafe(response), + H2_ALEN(keys), keys, values, response->headers); +} + +#else /* AP_HAS_RESPONSE_BUCKETS */ + static int is_unsafe(h2_headers *h) { - const char *v = apr_table_get(h->notes, H2_HDR_CONFORMANCE); + const char *v = h->notes? apr_table_get(h->notes, H2_HDR_CONFORMANCE) : NULL; return (v && !strcmp(v, H2_HDR_CONFORMANCE_UNSAFE)); } -apr_status_t h2_res_create_ngtrailer(h2_ngheader **ph, apr_pool_t *p, +apr_status_t h2_res_create_ngtrailer(h2_ngheader **ph, apr_pool_t *p, h2_headers *headers) { - return ngheader_create(ph, p, is_unsafe(headers), + return ngheader_create(ph, p, is_unsafe(headers), 0, NULL, NULL, headers->headers); } - + apr_status_t h2_res_create_ngheader(h2_ngheader **ph, apr_pool_t *p, - h2_headers *headers) + h2_headers *headers) { const char *keys[] = { ":status" @@ -1731,27 +1557,29 @@ apr_status_t h2_res_create_ngheader(h2_ngheader **ph, apr_pool_t *p, const char *values[] = { apr_psprintf(p, "%d", headers->status) }; - return ngheader_create(ph, p, is_unsafe(headers), + return ngheader_create(ph, p, is_unsafe(headers), H2_ALEN(keys), keys, values, headers->headers); } -apr_status_t h2_req_create_ngheader(h2_ngheader **ph, apr_pool_t *p, +#endif /* else AP_HAS_RESPONSE_BUCKETS */ + +apr_status_t h2_req_create_ngheader(h2_ngheader **ph, apr_pool_t *p, const struct h2_request *req) { - + const char *keys[] = { - ":scheme", - ":authority", - ":path", - ":method", + ":scheme", + ":authority", + ":path", + ":method", }; const char *values[] = { req->scheme, - req->authority, - req->path, - req->method, + req->authority, + req->path, + req->method, }; - + ap_assert(req->scheme); ap_assert(req->authority); ap_assert(req->path); @@ -1763,7 +1591,7 @@ apr_status_t h2_req_create_ngheader(h2_ngheader **ph, apr_pool_t *p, /******************************************************************************* * header HTTP/1 <-> HTTP/2 conversions ******************************************************************************/ - + typedef struct { const char *name; @@ -1791,9 +1619,15 @@ static literal IgnoredRequestTrailers[] = { /* Ignore, see rfc7230, ch. 4.1.2 */ H2_DEF_LITERAL("max-forwards"), H2_DEF_LITERAL("cache-control"), H2_DEF_LITERAL("authorization"), - H2_DEF_LITERAL("content-length"), + H2_DEF_LITERAL("content-length"), H2_DEF_LITERAL("proxy-authorization"), -}; +}; +static literal IgnoredResponseHeaders[] = { + H2_DEF_LITERAL("upgrade"), + H2_DEF_LITERAL("connection"), + H2_DEF_LITERAL("keep-alive"), + H2_DEF_LITERAL("transfer-encoding"), +}; static literal IgnoredResponseTrailers[] = { H2_DEF_LITERAL("age"), H2_DEF_LITERAL("date"), @@ -1808,93 +1642,124 @@ static literal IgnoredResponseTrailers[] = { H2_DEF_LITERAL("proxy-authenticate"), }; -static int ignore_header(const literal *lits, size_t llen, - const char *name, size_t nlen) +static int contains_name(const literal *lits, size_t llen, nghttp2_nv *nv) { const literal *lit; size_t i; - + for (i = 0; i < llen; ++i) { lit = &lits[i]; - if (lit->len == nlen && !apr_strnatcasecmp(lit->name, name)) { + if (lit->len == nv->namelen + && !ap_cstr_casecmp(lit->name, (const char *)nv->name)) { return 1; } } return 0; } -int h2_req_ignore_header(const char *name, size_t len) +int h2_util_ignore_resp_header(const char *name) { - return ignore_header(H2_LIT_ARGS(IgnoredRequestHeaders), name, len); + nghttp2_nv nv; + + nv.name = (uint8_t*)name; + nv.namelen = strlen(name); + return contains_name(H2_LIT_ARGS(IgnoredResponseHeaders), &nv); } -int h2_req_ignore_trailer(const char *name, size_t len) + +static int h2_req_ignore_header(nghttp2_nv *nv) { - return (h2_req_ignore_header(name, len) - || ignore_header(H2_LIT_ARGS(IgnoredRequestTrailers), name, len)); + return contains_name(H2_LIT_ARGS(IgnoredRequestHeaders), nv); } -int h2_res_ignore_trailer(const char *name, size_t len) +int h2_ignore_req_trailer(const char *name, size_t len) { - return ignore_header(H2_LIT_ARGS(IgnoredResponseTrailers), name, len); + nghttp2_nv nv; + + nv.name = (uint8_t*)name; + nv.namelen = strlen(name); + return (h2_req_ignore_header(&nv) + || contains_name(H2_LIT_ARGS(IgnoredRequestTrailers), &nv)); } -apr_status_t h2_req_add_header(apr_table_t *headers, apr_pool_t *pool, - const char *name, size_t nlen, - const char *value, size_t vlen) +int h2_ignore_resp_trailer(const char *name, size_t len) +{ + nghttp2_nv nv; + + nv.name = (uint8_t*)name; + nv.namelen = strlen(name); + return (contains_name(H2_LIT_ARGS(IgnoredResponseHeaders), &nv) + || contains_name(H2_LIT_ARGS(IgnoredResponseTrailers), &nv)); +} + +static apr_status_t req_add_header(apr_table_t *headers, apr_pool_t *pool, + nghttp2_nv *nv, size_t max_field_len, + int *pwas_added) { char *hname, *hvalue; - - if (h2_req_ignore_header(name, nlen)) { + const char *existing; + + *pwas_added = 0; + strip_field_value_ws(nv); + + if (h2_req_ignore_header(nv)) { return APR_SUCCESS; } - else if (H2_HD_MATCH_LIT("cookie", name, nlen)) { - const char *existing = apr_table_get(headers, "cookie"); + else if (nv->namelen == sizeof("cookie")-1 + && !ap_cstr_casecmp("cookie", (const char *)nv->name)) { + existing = apr_table_get(headers, "cookie"); if (existing) { - char *nval; - /* Cookie header come separately in HTTP/2, but need * to be merged by "; " (instead of default ", ") */ - hvalue = apr_pstrndup(pool, value, vlen); - nval = apr_psprintf(pool, "%s; %s", existing, hvalue); - apr_table_setn(headers, "Cookie", nval); + if (max_field_len + && strlen(existing) + nv->valuelen + nv->namelen + 4 + > max_field_len) { + /* "key: oldval, nval" is too long */ + return APR_EINVAL; + } + hvalue = apr_pstrndup(pool, (const char*)nv->value, nv->valuelen); + apr_table_setn(headers, "Cookie", + apr_psprintf(pool, "%s; %s", existing, hvalue)); return APR_SUCCESS; } } - else if (H2_HD_MATCH_LIT("host", name, nlen)) { + else if (nv->namelen == sizeof("host")-1 + && !ap_cstr_casecmp("host", (const char *)nv->name)) { if (apr_table_get(headers, "Host")) { return APR_SUCCESS; /* ignore duplicate */ } } - - hname = apr_pstrndup(pool, name, nlen); - hvalue = apr_pstrndup(pool, value, vlen); - h2_util_camel_case_header(hname, nlen); + + hname = apr_pstrndup(pool, (const char*)nv->name, nv->namelen); + h2_util_camel_case_header(hname, nv->namelen); + existing = apr_table_get(headers, hname); + if (max_field_len) { + if ((existing? strlen(existing)+2 : 0) + nv->valuelen + nv->namelen + 2 + > max_field_len) { + /* "key: (oldval, )?nval" is too long */ + return APR_EINVAL; + } + } + if (!existing) *pwas_added = 1; + hvalue = apr_pstrndup(pool, (const char*)nv->value, nv->valuelen); apr_table_mergen(headers, hname, hvalue); - + return APR_SUCCESS; } -/******************************************************************************* - * h2 request handling - ******************************************************************************/ - -h2_request *h2_req_create(int id, apr_pool_t *pool, const char *method, - const char *scheme, const char *authority, - const char *path, apr_table_t *header, int serialize) +apr_status_t h2_req_add_header(apr_table_t *headers, apr_pool_t *pool, + const char *name, size_t nlen, + const char *value, size_t vlen, + size_t max_field_len, int *pwas_added) { - h2_request *req = apr_pcalloc(pool, sizeof(h2_request)); - - req->method = method; - req->scheme = scheme; - req->authority = authority; - req->path = path; - req->headers = header? header : apr_table_make(pool, 10); - req->request_time = apr_time_now(); - req->serialize = serialize; - - return req; + nghttp2_nv nv; + + nv.name = (uint8_t*)name; + nv.namelen = nlen; + nv.value = (uint8_t*)value; + nv.valuelen = vlen; + return req_add_header(headers, pool, &nv, max_field_len, pwas_added); } /******************************************************************************* @@ -1905,7 +1770,7 @@ int h2_util_frame_print(const nghttp2_frame *frame, char *buffer, size_t maxlen) { char scratch[128]; size_t s_len = sizeof(scratch)/sizeof(scratch[0]); - + switch (frame->hd.type) { case NGHTTP2_DATA: { return apr_snprintf(buffer, maxlen, @@ -1960,16 +1825,17 @@ int h2_util_frame_print(const nghttp2_frame *frame, char *buffer, size_t maxlen) case NGHTTP2_GOAWAY: { size_t len = (frame->goaway.opaque_data_len < s_len)? frame->goaway.opaque_data_len : s_len-1; - memcpy(scratch, frame->goaway.opaque_data, len); + if (len) + memcpy(scratch, frame->goaway.opaque_data, len); scratch[len] = '\0'; return apr_snprintf(buffer, maxlen, "GOAWAY[error=%d, reason='%s', " - "last_stream=%d]", frame->goaway.error_code, + "last_stream=%d]", frame->goaway.error_code, scratch, frame->goaway.last_stream_id); } case NGHTTP2_WINDOW_UPDATE: { return apr_snprintf(buffer, maxlen, "WINDOW_UPDATE[stream=%d, incr=%d]", - frame->hd.stream_id, + frame->hd.stream_id, frame->window_update.window_size_increment); } default: @@ -2013,3 +1879,60 @@ int h2_push_policy_determine(apr_table_t *headers, apr_pool_t *p, int push_enabl return policy; } +void h2_util_drain_pipe(apr_file_t *pipe) +{ + char rb[512]; + apr_size_t nr = sizeof(rb); + apr_interval_time_t timeout; + apr_status_t trv; + + /* Make the pipe non-blocking if we can */ + trv = apr_file_pipe_timeout_get(pipe, &timeout); + if (trv == APR_SUCCESS) + apr_file_pipe_timeout_set(pipe, 0); + + while (apr_file_read(pipe, rb, &nr) == APR_SUCCESS) { + /* Although we write just one byte to the other end of the pipe + * during wakeup, multiple threads could call the wakeup. + * So simply drain out from the input side of the pipe all + * the data. + */ + if (nr != sizeof(rb)) + break; + } + if (trv == APR_SUCCESS) + apr_file_pipe_timeout_set(pipe, timeout); +} + +apr_status_t h2_util_wait_on_pipe(apr_file_t *pipe) +{ + char rb[512]; + apr_size_t nr = sizeof(rb); + + return apr_file_read(pipe, rb, &nr); +} + +#if AP_HAS_RESPONSE_BUCKETS + +static int add_header_lengths(void *ctx, const char *name, const char *value) +{ + apr_size_t *plen = ctx; + *plen += strlen(name) + strlen(value); + return 1; +} + +apr_size_t headers_length_estimate(ap_bucket_headers *hdrs) +{ + apr_size_t len = 0; + apr_table_do(add_header_lengths, &len, hdrs->headers, NULL); + return len; +} + +apr_size_t response_length_estimate(ap_bucket_response *resp) +{ + apr_size_t len = 3 + 1 + 8 + (resp->reason? strlen(resp->reason) : 10); + apr_table_do(add_header_lengths, &len, resp->headers, NULL); + return len; +} + +#endif /* AP_HAS_RESPONSE_BUCKETS */ diff --git a/modules/http2/h2_util.h b/modules/http2/h2_util.h index 1eb262d..d2e6548 100644 --- a/modules/http2/h2_util.h +++ b/modules/http2/h2_util.h @@ -18,6 +18,10 @@ #define __mod_h2__h2_util__ #include <nghttp2/nghttp2.h> +#include <http_protocol.h> + +#include "h2.h" +#include "h2_headers.h" /******************************************************************************* * some debugging/format helpers @@ -28,10 +32,6 @@ struct nghttp2_frame; size_t h2_util_hex_dump(char *buffer, size_t maxlen, const char *data, size_t datalen); -size_t h2_util_header_print(char *buffer, size_t maxlen, - const char *name, size_t namelen, - const char *value, size_t valuelen); - void h2_util_camel_case_header(char *s, size_t len); int h2_util_frame_print(const nghttp2_frame *frame, char *buffer, size_t maxlen); @@ -49,7 +49,7 @@ typedef int h2_ihash_iter_t(void *ctx, void *val); */ h2_ihash_t *h2_ihash_create(apr_pool_t *pool, size_t offset_of_int); -size_t h2_ihash_count(h2_ihash_t *ih); +unsigned int h2_ihash_count(h2_ihash_t *ih); int h2_ihash_empty(h2_ihash_t *ih); void *h2_ihash_get(h2_ihash_t *ih, int id); @@ -96,13 +96,13 @@ typedef int h2_iq_cmp(int i1, int i2, void *ctx); /** * Allocate a new queue from the pool and initialize. - * @param id the identifier of the queue * @param pool the memory pool + * @param capacity the initial capacity of the queue */ h2_iqueue *h2_iq_create(apr_pool_t *pool, int capacity); /** - * Return != 0 iff there are no tasks in the queue. + * Return != 0 iff there are no ints in the queue. * @param q the queue to check */ int h2_iq_empty(h2_iqueue *q); @@ -134,11 +134,10 @@ int h2_iq_add(h2_iqueue *q, int sid, h2_iq_cmp *cmp, void *ctx); int h2_iq_append(h2_iqueue *q, int sid); /** - * Remove the stream id from the queue. Return != 0 iff task - * was found in queue. - * @param q the task queue + * Remove the int from the queue. Return != 0 iff it was found. + * @param q the queue * @param sid the stream id to remove - * @return != 0 iff task was found in queue + * @return != 0 iff int was found in queue */ int h2_iq_remove(h2_iqueue *q, int sid); @@ -148,7 +147,7 @@ int h2_iq_remove(h2_iqueue *q, int sid); void h2_iq_clear(h2_iqueue *q); /** - * Sort the stream idqueue again. Call if the task ordering + * Sort the stream idqueue again. Call if the int ordering * has changed. * * @param q the queue to sort @@ -169,7 +168,7 @@ int h2_iq_shift(h2_iqueue *q); /** * Get the first max ids from the queue. All these ids will be removed. * - * @param q the queue to get the first task from + * @param q the queue to get the first ids from * @param pint the int array to receive the values * @param max the maximum number of ids to shift * @return the actual number of ids shifted @@ -179,7 +178,7 @@ size_t h2_iq_mshift(h2_iqueue *q, int *pint, size_t max); /** * Determine if int is in the queue already * - * @parm q the queue + * @param q the queue * @param sid the integer id to check for * @return != 0 iff sid is already in the queue */ @@ -209,7 +208,6 @@ apr_status_t h2_fifo_create(h2_fifo **pfifo, apr_pool_t *pool, int capacity); apr_status_t h2_fifo_set_create(h2_fifo **pfifo, apr_pool_t *pool, int capacity); apr_status_t h2_fifo_term(h2_fifo *fifo); -apr_status_t h2_fifo_interrupt(h2_fifo *fifo); int h2_fifo_count(h2_fifo *fifo); @@ -229,7 +227,7 @@ apr_status_t h2_fifo_try_pull(h2_fifo *fifo, void **pelem); typedef enum { H2_FIFO_OP_PULL, /* pull the element from the queue, ie discard it */ - H2_FIFO_OP_REPUSH, /* pull and immediatley re-push it */ + H2_FIFO_OP_REPUSH, /* pull and immediately re-push it */ } h2_fifo_op_t; typedef h2_fifo_op_t h2_fifo_peek_fn(void *head, void *ctx); @@ -280,7 +278,6 @@ apr_status_t h2_ififo_create(h2_ififo **pfifo, apr_pool_t *pool, int capacity); apr_status_t h2_ififo_set_create(h2_ififo **pfifo, apr_pool_t *pool, int capacity); apr_status_t h2_ififo_term(h2_ififo *fifo); -apr_status_t h2_ififo_interrupt(h2_ififo *fifo); int h2_ififo_count(h2_ififo *fifo); @@ -345,9 +342,8 @@ apr_size_t h2_util_table_bytes(apr_table_t *t, apr_size_t pair_extra); /******************************************************************************* * HTTP/2 header helpers ******************************************************************************/ -int h2_req_ignore_header(const char *name, size_t len); -int h2_req_ignore_trailer(const char *name, size_t len); -int h2_res_ignore_trailer(const char *name, size_t len); +int h2_ignore_req_trailer(const char *name, size_t len); +int h2_ignore_resp_trailer(const char *name, size_t len); /** * Set the push policy for the given request. Takes request headers into @@ -378,52 +374,37 @@ const char *h2_util_base64url_encode(const char *data, * nghttp2 helpers ******************************************************************************/ -#define H2_HD_MATCH_LIT_CS(l, name) \ - ((strlen(name) == sizeof(l) - 1) && !apr_strnatcasecmp(l, name)) - -#define H2_CREATE_NV_LIT_CS(nv, NAME, VALUE) nv->name = (uint8_t *)NAME; \ - nv->namelen = sizeof(NAME) - 1; \ - nv->value = (uint8_t *)VALUE; \ - nv->valuelen = strlen(VALUE) - -#define H2_CREATE_NV_CS_LIT(nv, NAME, VALUE) nv->name = (uint8_t *)NAME; \ - nv->namelen = strlen(NAME); \ - nv->value = (uint8_t *)VALUE; \ - nv->valuelen = sizeof(VALUE) - 1 - -#define H2_CREATE_NV_CS_CS(nv, NAME, VALUE) nv->name = (uint8_t *)NAME; \ - nv->namelen = strlen(NAME); \ - nv->value = (uint8_t *)VALUE; \ - nv->valuelen = strlen(VALUE) - -int h2_util_ignore_header(const char *name); - -struct h2_headers; +int h2_util_ignore_resp_header(const char *name); typedef struct h2_ngheader { nghttp2_nv *nv; apr_size_t nvlen; } h2_ngheader; +#if AP_HAS_RESPONSE_BUCKETS +apr_status_t h2_res_create_ngtrailer(h2_ngheader **ph, apr_pool_t *p, + ap_bucket_headers *headers); +apr_status_t h2_res_create_ngheader(h2_ngheader **ph, apr_pool_t *p, + ap_bucket_response *response); +apr_status_t h2_req_create_ngheader(h2_ngheader **ph, apr_pool_t *p, + const struct h2_request *req); +#else apr_status_t h2_res_create_ngtrailer(h2_ngheader **ph, apr_pool_t *p, struct h2_headers *headers); apr_status_t h2_res_create_ngheader(h2_ngheader **ph, apr_pool_t *p, struct h2_headers *headers); apr_status_t h2_req_create_ngheader(h2_ngheader **ph, apr_pool_t *p, const struct h2_request *req); +#endif +/** + * Add a HTTP/2 header and return the table key if it really was added + * and not ignored. + */ apr_status_t h2_req_add_header(apr_table_t *headers, apr_pool_t *pool, const char *name, size_t nlen, - const char *value, size_t vlen); - -/******************************************************************************* - * h2_request helpers - ******************************************************************************/ - -struct h2_request *h2_req_create(int id, apr_pool_t *pool, const char *method, - const char *scheme, const char *authority, - const char *path, apr_table_t *header, - int serialize); + const char *value, size_t vlen, + size_t max_field_len, int *pwas_added); /******************************************************************************* * apr brigade helpers @@ -445,43 +426,10 @@ apr_status_t h2_brigade_copy_length(apr_bucket_brigade *dest, apr_bucket_brigade *src, apr_off_t length); -/** - * Return != 0 iff there is a FLUSH or EOS bucket in the brigade. - * @param bb the brigade to check on - * @return != 0 iff brigade holds FLUSH or EOS bucket (or both) - */ -int h2_util_has_eos(apr_bucket_brigade *bb, apr_off_t len); - -/** - * Check how many bytes of the desired amount are available and if the - * end of stream is reached by that amount. - * @param bb the brigade to check - * @param plen the desired length and, on return, the available length - * @param on return, if eos has been reached - */ -apr_status_t h2_util_bb_avail(apr_bucket_brigade *bb, - apr_off_t *plen, int *peos); - -typedef apr_status_t h2_util_pass_cb(void *ctx, +typedef apr_status_t h2_util_pass_cb(void *ctx, const char *data, apr_off_t len); /** - * Read at most *plen bytes from the brigade and pass them into the - * given callback. If cb is NULL, just return the amount of data that - * could have been read. - * If an EOS was/would be encountered, set *peos != 0. - * @param bb the brigade to read from - * @param cb the callback to invoke for the read data - * @param ctx optional data passed to callback - * @param plen inout, as input gives the maximum number of bytes to read, - * on return specifies the actual/would be number of bytes - * @param peos != 0 iff an EOS bucket was/would be encountered. - */ -apr_status_t h2_util_bb_readx(apr_bucket_brigade *bb, - h2_util_pass_cb *cb, void *ctx, - apr_off_t *plen, int *peos); - -/** * Print a bucket's meta data (type and length) to the buffer. * @return number of characters printed */ @@ -506,14 +454,16 @@ apr_size_t h2_util_bb_print(char *buffer, apr_size_t bmax, * @param bb the brigade to log */ #define h2_util_bb_log(c, sid, level, tag, bb) \ -do { \ - char buffer[4 * 1024]; \ - const char *line = "(null)"; \ - apr_size_t len, bmax = sizeof(buffer)/sizeof(buffer[0]); \ - len = h2_util_bb_print(buffer, bmax, (tag), "", (bb)); \ - ap_log_cerror(APLOG_MARK, level, 0, (c), "bb_dump(%ld): %s", \ - ((c)->master? (c)->master->id : (c)->id), (len? buffer : line)); \ -} while(0) +if (APLOG_C_IS_LEVEL(c, level)) { \ + do { \ + char buffer[4 * 1024]; \ + const char *line = "(null)"; \ + apr_size_t len, bmax = sizeof(buffer)/sizeof(buffer[0]); \ + len = h2_util_bb_print(buffer, bmax, (tag), "", (bb)); \ + ap_log_cerror(APLOG_MARK, level, 0, (c), "bb_dump(%ld): %s", \ + ((c)->master? (c)->master->id : (c)->id), (len? buffer : line)); \ + } while(0); \ +} typedef int h2_bucket_gate(apr_bucket *b); @@ -541,4 +491,29 @@ apr_status_t h2_append_brigade(apr_bucket_brigade *to, */ apr_off_t h2_brigade_mem_size(apr_bucket_brigade *bb); +/** + * Drain a pipe used for notification. + */ +void h2_util_drain_pipe(apr_file_t *pipe); + +/** + * Wait on data arriving on a pipe. + */ +apr_status_t h2_util_wait_on_pipe(apr_file_t *pipe); + + +#if AP_HAS_RESPONSE_BUCKETS +/** + * Give an estimate of the length of the header fields, + * without compression or other formatting decorations. + */ +apr_size_t headers_length_estimate(ap_bucket_headers *hdrs); + +/** + * Give an estimate of the length of the response meta data size, + * without compression or other formatting decorations. + */ +apr_size_t response_length_estimate(ap_bucket_response *resp); +#endif /* AP_HAS_RESPONSE_BUCKETS */ + #endif /* defined(__mod_h2__h2_util__) */ diff --git a/modules/http2/h2_version.h b/modules/http2/h2_version.h index 7079437..7e7da21 100644 --- a/modules/http2/h2_version.h +++ b/modules/http2/h2_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the http2 module as c string */ -#define MOD_HTTP2_VERSION "1.11.4" +#define MOD_HTTP2_VERSION "2.0.22" /** * @macro @@ -35,7 +35,7 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define MOD_HTTP2_VERSION_NUM 0x010b04 +#define MOD_HTTP2_VERSION_NUM 0x020016 #endif /* mod_h2_h2_version_h */ diff --git a/modules/http2/h2_workers.c b/modules/http2/h2_workers.c index 699f533..e7e2039 100644 --- a/modules/http2/h2_workers.c +++ b/modules/http2/h2_workers.c @@ -15,285 +15,440 @@ */ #include <assert.h> -#include <apr_atomic.h> +#include <apr_ring.h> #include <apr_thread_mutex.h> #include <apr_thread_cond.h> #include <mpm_common.h> #include <httpd.h> +#include <http_connection.h> #include <http_core.h> #include <http_log.h> +#include <http_protocol.h> #include "h2.h" #include "h2_private.h" #include "h2_mplx.h" -#include "h2_task.h" +#include "h2_c2.h" #include "h2_workers.h" #include "h2_util.h" +typedef enum { + PROD_IDLE, + PROD_ACTIVE, + PROD_JOINED, +} prod_state_t; + +struct ap_conn_producer_t { + APR_RING_ENTRY(ap_conn_producer_t) link; + const char *name; + void *baton; + ap_conn_producer_next *fn_next; + ap_conn_producer_done *fn_done; + ap_conn_producer_shutdown *fn_shutdown; + volatile prod_state_t state; + volatile int conns_active; +}; + + +typedef enum { + H2_SLOT_FREE, + H2_SLOT_RUN, + H2_SLOT_ZOMBIE, +} h2_slot_state_t; + typedef struct h2_slot h2_slot; struct h2_slot { - int id; - h2_slot *next; + APR_RING_ENTRY(h2_slot) link; + apr_uint32_t id; + apr_pool_t *pool; + h2_slot_state_t state; + volatile int should_shutdown; + volatile int is_idle; h2_workers *workers; - int aborted; - int sticks; - h2_task *task; + ap_conn_producer_t *prod; apr_thread_t *thread; - apr_thread_mutex_t *lock; - apr_thread_cond_t *not_idle; + struct apr_thread_cond_t *more_work; + int activations; }; -static h2_slot *pop_slot(h2_slot **phead) -{ - /* Atomically pop a slot from the list */ - for (;;) { - h2_slot *first = *phead; - if (first == NULL) { - return NULL; - } - if (apr_atomic_casptr((void*)phead, first->next, first) == first) { - first->next = NULL; - return first; - } - } -} +struct h2_workers { + server_rec *s; + apr_pool_t *pool; + + apr_uint32_t max_slots; + apr_uint32_t min_active; + volatile apr_time_t idle_limit; + volatile int aborted; + volatile int shutdown; + int dynamic; + + volatile apr_uint32_t active_slots; + volatile apr_uint32_t idle_slots; + + apr_threadattr_t *thread_attr; + h2_slot *slots; + + APR_RING_HEAD(h2_slots_free, h2_slot) free; + APR_RING_HEAD(h2_slots_idle, h2_slot) idle; + APR_RING_HEAD(h2_slots_busy, h2_slot) busy; + APR_RING_HEAD(h2_slots_zombie, h2_slot) zombie; + + APR_RING_HEAD(ap_conn_producer_active, ap_conn_producer_t) prod_active; + APR_RING_HEAD(ap_conn_producer_idle, ap_conn_producer_t) prod_idle; + + struct apr_thread_mutex_t *lock; + struct apr_thread_cond_t *prod_done; + struct apr_thread_cond_t *all_done; +}; -static void push_slot(h2_slot **phead, h2_slot *slot) -{ - /* Atomically push a slot to the list */ - ap_assert(!slot->next); - for (;;) { - h2_slot *next = slot->next = *phead; - if (apr_atomic_casptr((void*)phead, slot, next) == next) { - return; - } - } -} static void* APR_THREAD_FUNC slot_run(apr_thread_t *thread, void *wctx); -static apr_status_t activate_slot(h2_workers *workers, h2_slot *slot) +static apr_status_t activate_slot(h2_workers *workers) { - apr_status_t status; - - slot->workers = workers; - slot->aborted = 0; - slot->task = NULL; - - if (!slot->lock) { - status = apr_thread_mutex_create(&slot->lock, - APR_THREAD_MUTEX_DEFAULT, - workers->pool); - if (status != APR_SUCCESS) { - push_slot(&workers->free, slot); - return status; - } - } + h2_slot *slot; + apr_pool_t *pool; + apr_status_t rv; - if (!slot->not_idle) { - status = apr_thread_cond_create(&slot->not_idle, workers->pool); - if (status != APR_SUCCESS) { - push_slot(&workers->free, slot); - return status; - } + if (APR_RING_EMPTY(&workers->free, h2_slot, link)) { + return APR_EAGAIN; } - - ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, workers->s, - "h2_workers: new thread for slot %d", slot->id); - /* thread will either immediately start work or add itself - * to the idle queue */ - apr_thread_create(&slot->thread, workers->thread_attr, slot_run, slot, - workers->pool); - if (!slot->thread) { - push_slot(&workers->free, slot); - return APR_ENOMEM; - } - - apr_atomic_inc32(&workers->worker_count); - return APR_SUCCESS; -} + slot = APR_RING_FIRST(&workers->free); + ap_assert(slot->state == H2_SLOT_FREE); + APR_RING_REMOVE(slot, link); -static apr_status_t add_worker(h2_workers *workers) -{ - h2_slot *slot = pop_slot(&workers->free); - if (slot) { - return activate_slot(workers, slot); - } - return APR_EAGAIN; -} + ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, workers->s, + "h2_workers: activate slot %d", slot->id); -static void wake_idle_worker(h2_workers *workers) -{ - h2_slot *slot = pop_slot(&workers->idle); - if (slot) { - apr_thread_mutex_lock(slot->lock); - apr_thread_cond_signal(slot->not_idle); - apr_thread_mutex_unlock(slot->lock); - } - else if (workers->dynamic) { - add_worker(workers); + slot->state = H2_SLOT_RUN; + slot->should_shutdown = 0; + slot->is_idle = 0; + slot->pool = NULL; + ++workers->active_slots; + rv = apr_pool_create(&pool, workers->pool); + if (APR_SUCCESS != rv) goto cleanup; + apr_pool_tag(pool, "h2_worker_slot"); + slot->pool = pool; + + rv = ap_thread_create(&slot->thread, workers->thread_attr, + slot_run, slot, slot->pool); + +cleanup: + if (rv != APR_SUCCESS) { + AP_DEBUG_ASSERT(0); + slot->state = H2_SLOT_FREE; + if (slot->pool) { + apr_pool_destroy(slot->pool); + slot->pool = NULL; + } + APR_RING_INSERT_TAIL(&workers->free, slot, h2_slot, link); + --workers->active_slots; } + return rv; } -static void cleanup_zombies(h2_workers *workers) +static void join_zombies(h2_workers *workers) { h2_slot *slot; - while ((slot = pop_slot(&workers->zombies))) { - if (slot->thread) { - apr_status_t status; - apr_thread_join(&status, slot->thread); - slot->thread = NULL; + apr_status_t status; + + while (!APR_RING_EMPTY(&workers->zombie, h2_slot, link)) { + slot = APR_RING_FIRST(&workers->zombie); + APR_RING_REMOVE(slot, link); + ap_assert(slot->state == H2_SLOT_ZOMBIE); + ap_assert(slot->thread != NULL); + + apr_thread_mutex_unlock(workers->lock); + apr_thread_join(&status, slot->thread); + apr_thread_mutex_lock(workers->lock); + + slot->thread = NULL; + slot->state = H2_SLOT_FREE; + if (slot->pool) { + apr_pool_destroy(slot->pool); + slot->pool = NULL; } - apr_atomic_dec32(&workers->worker_count); - slot->next = NULL; - push_slot(&workers->free, slot); + APR_RING_INSERT_TAIL(&workers->free, slot, h2_slot, link); } } -static apr_status_t slot_pull_task(h2_slot *slot, h2_mplx *m) +static void wake_idle_worker(h2_workers *workers, ap_conn_producer_t *prod) { - apr_status_t rv; - - rv = h2_mplx_pop_task(m, &slot->task); - if (slot->task) { - /* Ok, we got something to give back to the worker for execution. - * If we still have idle workers, we let the worker be sticky, - * e.g. making it poll the task's h2_mplx instance for more work - * before asking back here. */ - slot->sticks = slot->workers->max_workers; - return rv; + if (!APR_RING_EMPTY(&workers->idle, h2_slot, link)) { + h2_slot *slot; + for (slot = APR_RING_FIRST(&workers->idle); + slot != APR_RING_SENTINEL(&workers->idle, h2_slot, link); + slot = APR_RING_NEXT(slot, link)) { + if (slot->is_idle && !slot->should_shutdown) { + apr_thread_cond_signal(slot->more_work); + slot->is_idle = 0; + return; + } + } + } + if (workers->dynamic && !workers->shutdown + && (workers->active_slots < workers->max_slots)) { + activate_slot(workers); } - slot->sticks = 0; - return APR_EOF; -} - -static h2_fifo_op_t mplx_peek(void *head, void *ctx) -{ - h2_mplx *m = head; - h2_slot *slot = ctx; - - if (slot_pull_task(slot, m) == APR_EAGAIN) { - wake_idle_worker(slot->workers); - return H2_FIFO_OP_REPUSH; - } - return H2_FIFO_OP_PULL; } /** - * Get the next task for the given worker. Will block until a task arrives - * or the max_wait timer expires and more than min workers exist. + * Get the next connection to work on. */ -static apr_status_t get_next(h2_slot *slot) +static conn_rec *get_next(h2_slot *slot) { h2_workers *workers = slot->workers; - apr_status_t status; - - slot->task = NULL; - while (!slot->aborted) { - if (!slot->task) { - status = h2_fifo_try_peek(workers->mplxs, mplx_peek, slot); - if (status == APR_EOF) { - return status; - } + conn_rec *c = NULL; + ap_conn_producer_t *prod; + int has_more; + + slot->prod = NULL; + if (!APR_RING_EMPTY(&workers->prod_active, ap_conn_producer_t, link)) { + slot->prod = prod = APR_RING_FIRST(&workers->prod_active); + APR_RING_REMOVE(prod, link); + AP_DEBUG_ASSERT(PROD_ACTIVE == prod->state); + + c = prod->fn_next(prod->baton, &has_more); + if (c && has_more) { + APR_RING_INSERT_TAIL(&workers->prod_active, prod, ap_conn_producer_t, link); + wake_idle_worker(workers, slot->prod); } - - if (slot->task) { - return APR_SUCCESS; + else { + prod->state = PROD_IDLE; + APR_RING_INSERT_TAIL(&workers->prod_idle, prod, ap_conn_producer_t, link); + } + if (c) { + ++prod->conns_active; } - - cleanup_zombies(workers); - - apr_thread_mutex_lock(slot->lock); - push_slot(&workers->idle, slot); - apr_thread_cond_wait(slot->not_idle, slot->lock); - apr_thread_mutex_unlock(slot->lock); } - return APR_EOF; -} -static void slot_done(h2_slot *slot) -{ - push_slot(&(slot->workers->zombies), slot); + return c; } - static void* APR_THREAD_FUNC slot_run(apr_thread_t *thread, void *wctx) { h2_slot *slot = wctx; - - while (!slot->aborted) { - - /* Get a h2_task from the mplxs queue. */ - get_next(slot); - while (slot->task) { - - h2_task_do(slot->task, thread, slot->id); - - /* Report the task as done. If stickyness is left, offer the - * mplx the opportunity to give us back a new task right away. - */ - if (!slot->aborted && (--slot->sticks > 0)) { - h2_mplx_task_done(slot->task->mplx, slot->task, &slot->task); - } - else { - h2_mplx_task_done(slot->task->mplx, slot->task, NULL); - slot->task = NULL; + h2_workers *workers = slot->workers; + conn_rec *c; + apr_status_t rv; + + apr_thread_mutex_lock(workers->lock); + slot->state = H2_SLOT_RUN; + ++slot->activations; + APR_RING_ELEM_INIT(slot, link); + for(;;) { + if (APR_RING_NEXT(slot, link) != slot) { + /* slot is part of the idle ring from the last loop */ + APR_RING_REMOVE(slot, link); + --workers->idle_slots; + } + slot->is_idle = 0; + + if (!workers->aborted && !slot->should_shutdown) { + APR_RING_INSERT_TAIL(&workers->busy, slot, h2_slot, link); + do { + c = get_next(slot); + if (!c) { + break; + } + apr_thread_mutex_unlock(workers->lock); + /* See the discussion at <https://github.com/icing/mod_h2/issues/195> + * + * Each conn_rec->id is supposed to be unique at a point in time. Since + * some modules (and maybe external code) uses this id as an identifier + * for the request_rec they handle, it needs to be unique for secondary + * connections also. + * + * The MPM module assigns the connection ids and mod_unique_id is using + * that one to generate identifier for requests. While the implementation + * works for HTTP/1.x, the parallel execution of several requests per + * connection will generate duplicate identifiers on load. + * + * The original implementation for secondary connection identifiers used + * to shift the master connection id up and assign the stream id to the + * lower bits. This was cramped on 32 bit systems, but on 64bit there was + * enough space. + * + * As issue 195 showed, mod_unique_id only uses the lower 32 bit of the + * connection id, even on 64bit systems. Therefore collisions in request ids. + * + * The way master connection ids are generated, there is some space "at the + * top" of the lower 32 bits on allmost all systems. If you have a setup + * with 64k threads per child and 255 child processes, you live on the edge. + * + * The new implementation shifts 8 bits and XORs in the worker + * id. This will experience collisions with > 256 h2 workers and heavy + * load still. There seems to be no way to solve this in all possible + * configurations by mod_h2 alone. + */ + if (c->master) { + c->id = (c->master->id << 8)^slot->id; + } + c->current_thread = thread; + AP_DEBUG_ASSERT(slot->prod); + +#if AP_HAS_RESPONSE_BUCKETS + ap_process_connection(c, ap_get_conn_socket(c)); +#else + h2_c2_process(c, thread, slot->id); +#endif + slot->prod->fn_done(slot->prod->baton, c); + + apr_thread_mutex_lock(workers->lock); + if (--slot->prod->conns_active <= 0) { + apr_thread_cond_broadcast(workers->prod_done); + } + if (slot->prod->state == PROD_IDLE) { + APR_RING_REMOVE(slot->prod, link); + slot->prod->state = PROD_ACTIVE; + APR_RING_INSERT_TAIL(&workers->prod_active, slot->prod, ap_conn_producer_t, link); + } + + } while (!workers->aborted && !slot->should_shutdown); + APR_RING_REMOVE(slot, link); /* no longer busy */ + } + + if (workers->aborted || slot->should_shutdown) { + break; + } + + join_zombies(workers); + + /* we are idle */ + APR_RING_INSERT_TAIL(&workers->idle, slot, h2_slot, link); + ++workers->idle_slots; + slot->is_idle = 1; + if (slot->id >= workers->min_active && workers->idle_limit > 0) { + rv = apr_thread_cond_timedwait(slot->more_work, workers->lock, + workers->idle_limit); + if (APR_TIMEUP == rv) { + APR_RING_REMOVE(slot, link); + --workers->idle_slots; + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s, + "h2_workers: idle timeout slot %d in state %d (%d activations)", + slot->id, slot->state, slot->activations); + break; } } + else { + apr_thread_cond_wait(slot->more_work, workers->lock); + } + } + + ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, workers->s, + "h2_workers: terminate slot %d in state %d (%d activations)", + slot->id, slot->state, slot->activations); + slot->is_idle = 0; + slot->state = H2_SLOT_ZOMBIE; + slot->should_shutdown = 0; + APR_RING_INSERT_TAIL(&workers->zombie, slot, h2_slot, link); + --workers->active_slots; + if (workers->active_slots <= 0) { + apr_thread_cond_broadcast(workers->all_done); } + apr_thread_mutex_unlock(workers->lock); - slot_done(slot); + apr_thread_exit(thread, APR_SUCCESS); return NULL; } +static void wake_all_idles(h2_workers *workers) +{ + h2_slot *slot; + for (slot = APR_RING_FIRST(&workers->idle); + slot != APR_RING_SENTINEL(&workers->idle, h2_slot, link); + slot = APR_RING_NEXT(slot, link)) + { + apr_thread_cond_signal(slot->more_work); + } +} + static apr_status_t workers_pool_cleanup(void *data) { h2_workers *workers = data; - h2_slot *slot; - - if (!workers->aborted) { - workers->aborted = 1; - /* abort all idle slots */ - for (;;) { - slot = pop_slot(&workers->idle); - if (slot) { - apr_thread_mutex_lock(slot->lock); - slot->aborted = 1; - apr_thread_cond_signal(slot->not_idle); - apr_thread_mutex_unlock(slot->lock); - } - else { - break; - } - } + apr_time_t end, timeout = apr_time_from_sec(1); + apr_status_t rv; + int n = 0, wait_sec = 5; - h2_fifo_term(workers->mplxs); - h2_fifo_interrupt(workers->mplxs); + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + "h2_workers: cleanup %d workers (%d idle)", + workers->active_slots, workers->idle_slots); + apr_thread_mutex_lock(workers->lock); + workers->shutdown = 1; + workers->aborted = 1; + wake_all_idles(workers); + apr_thread_mutex_unlock(workers->lock); + + /* wait for all the workers to become zombies and join them. + * this gets called after the mpm shuts down and all connections + * have either been handled (graceful) or we are forced exiting + * (ungrateful). Either way, we show limited patience. */ + end = apr_time_now() + apr_time_from_sec(wait_sec); + while (apr_time_now() < end) { + apr_thread_mutex_lock(workers->lock); + if (!(n = workers->active_slots)) { + apr_thread_mutex_unlock(workers->lock); + break; + } + wake_all_idles(workers); + rv = apr_thread_cond_timedwait(workers->all_done, workers->lock, timeout); + apr_thread_mutex_unlock(workers->lock); - cleanup_zombies(workers); + if (APR_TIMEUP == rv) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + APLOGNO(10290) "h2_workers: waiting for workers to close, " + "still seeing %d workers (%d idle) living", + workers->active_slots, workers->idle_slots); + } + } + if (n) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, workers->s, + APLOGNO(10291) "h2_workers: cleanup, %d workers (%d idle) " + "did not exit after %d seconds.", + n, workers->idle_slots, wait_sec); } + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + "h2_workers: cleanup all workers terminated"); + apr_thread_mutex_lock(workers->lock); + join_zombies(workers); + apr_thread_mutex_unlock(workers->lock); + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s, + "h2_workers: cleanup zombie workers joined"); + return APR_SUCCESS; } -h2_workers *h2_workers_create(server_rec *s, apr_pool_t *server_pool, - int min_workers, int max_workers, - int idle_secs) +h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pchild, + int max_slots, int min_active, + apr_time_t idle_limit) { - apr_status_t status; + apr_status_t rv; h2_workers *workers; apr_pool_t *pool; - int i, n; + apr_allocator_t *allocator; + int locked = 0; + apr_uint32_t i; ap_assert(s); - ap_assert(server_pool); + ap_assert(pchild); + ap_assert(idle_limit > 0); /* let's have our own pool that will be parent to all h2_worker * instances we create. This happens in various threads, but always * guarded by our lock. Without this pool, all subpool creations would * happen on the pool handed to us, which we do not guard. */ - apr_pool_create(&pool, server_pool); + rv = apr_allocator_create(&allocator); + if (rv != APR_SUCCESS) { + goto cleanup; + } + rv = apr_pool_create_ex(&pool, pchild, NULL, allocator); + if (rv != APR_SUCCESS) { + apr_allocator_destroy(allocator); + goto cleanup; + } + apr_allocator_owner_set(allocator, pool); apr_pool_tag(pool, "h2_workers"); workers = apr_pcalloc(pool, sizeof(h2_workers)); if (!workers) { @@ -302,31 +457,27 @@ h2_workers *h2_workers_create(server_rec *s, apr_pool_t *server_pool, workers->s = s; workers->pool = pool; - workers->min_workers = min_workers; - workers->max_workers = max_workers; - workers->max_idle_secs = (idle_secs > 0)? idle_secs : 10; - - /* FIXME: the fifo set we use here has limited capacity. Once the - * set is full, connections with new requests do a wait. Unfortunately, - * we have optimizations in place there that makes such waiting "unfair" - * in the sense that it may take connections a looong time to get scheduled. - * - * Need to rewrite this to use one of our double-linked lists and a mutex - * to have unlimited capacity and fair scheduling. - * - * For now, we just make enough room to have many connections inside one - * process. - */ - status = h2_fifo_set_create(&workers->mplxs, pool, 8 * 1024); - if (status != APR_SUCCESS) { - return NULL; - } - - status = apr_threadattr_create(&workers->thread_attr, workers->pool); - if (status != APR_SUCCESS) { - return NULL; - } - + workers->min_active = min_active; + workers->max_slots = max_slots; + workers->idle_limit = idle_limit; + workers->dynamic = (workers->min_active < workers->max_slots); + + ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, + "h2_workers: created with min=%d max=%d idle_ms=%d", + workers->min_active, workers->max_slots, + (int)apr_time_as_msec(idle_limit)); + + APR_RING_INIT(&workers->idle, h2_slot, link); + APR_RING_INIT(&workers->busy, h2_slot, link); + APR_RING_INIT(&workers->free, h2_slot, link); + APR_RING_INIT(&workers->zombie, h2_slot, link); + + APR_RING_INIT(&workers->prod_active, ap_conn_producer_t, link); + APR_RING_INIT(&workers->prod_idle, ap_conn_producer_t, link); + + rv = apr_threadattr_create(&workers->thread_attr, workers->pool); + if (rv != APR_SUCCESS) goto cleanup; + if (ap_thread_stacksize != 0) { apr_threadattr_stacksize_set(workers->thread_attr, ap_thread_stacksize); @@ -335,49 +486,141 @@ h2_workers *h2_workers_create(server_rec *s, apr_pool_t *server_pool, (long)ap_thread_stacksize); } - status = apr_thread_mutex_create(&workers->lock, - APR_THREAD_MUTEX_DEFAULT, - workers->pool); - if (status == APR_SUCCESS) { - n = workers->nslots = workers->max_workers; - workers->slots = apr_pcalloc(workers->pool, n * sizeof(h2_slot)); - if (workers->slots == NULL) { - workers->nslots = 0; - status = APR_ENOMEM; - } - for (i = 0; i < n; ++i) { - workers->slots[i].id = i; - } + rv = apr_thread_mutex_create(&workers->lock, + APR_THREAD_MUTEX_DEFAULT, + workers->pool); + if (rv != APR_SUCCESS) goto cleanup; + rv = apr_thread_cond_create(&workers->all_done, workers->pool); + if (rv != APR_SUCCESS) goto cleanup; + rv = apr_thread_cond_create(&workers->prod_done, workers->pool); + if (rv != APR_SUCCESS) goto cleanup; + + apr_thread_mutex_lock(workers->lock); + locked = 1; + + /* create the slots and put them on the free list */ + workers->slots = apr_pcalloc(workers->pool, workers->max_slots * sizeof(h2_slot)); + + for (i = 0; i < workers->max_slots; ++i) { + workers->slots[i].id = i; + workers->slots[i].state = H2_SLOT_FREE; + workers->slots[i].workers = workers; + APR_RING_ELEM_INIT(&workers->slots[i], link); + APR_RING_INSERT_TAIL(&workers->free, &workers->slots[i], h2_slot, link); + rv = apr_thread_cond_create(&workers->slots[i].more_work, workers->pool); + if (rv != APR_SUCCESS) goto cleanup; } - if (status == APR_SUCCESS) { - /* we activate all for now, TODO: support min_workers again. - * do this in reverse for vanity reasons so slot 0 will most - * likely be at head of idle queue. */ - n = workers->max_workers; - for (i = n-1; i >= 0; --i) { - status = activate_slot(workers, &workers->slots[i]); - } - /* the rest of the slots go on the free list */ - for(i = n; i < workers->nslots; ++i) { - push_slot(&workers->free, &workers->slots[i]); - } - workers->dynamic = (workers->worker_count < workers->max_workers); + + /* activate the min amount of workers */ + for (i = 0; i < workers->min_active; ++i) { + rv = activate_slot(workers); + if (rv != APR_SUCCESS) goto cleanup; } - if (status == APR_SUCCESS) { - apr_pool_pre_cleanup_register(pool, workers, workers_pool_cleanup); + +cleanup: + if (locked) { + apr_thread_mutex_unlock(workers->lock); + } + if (rv == APR_SUCCESS) { + /* Stop/join the workers threads when the MPM child exits (pchild is + * destroyed), and as a pre_cleanup of pchild thus before the threads + * pools (children of workers->pool) so that they are not destroyed + * before/under us. + */ + apr_pool_pre_cleanup_register(pchild, workers, workers_pool_cleanup); return workers; } + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, + "h2_workers: errors initializing"); return NULL; } -apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m) +apr_uint32_t h2_workers_get_max_workers(h2_workers *workers) { - apr_status_t status = h2_fifo_push(workers->mplxs, m); - wake_idle_worker(workers); - return status; + return workers->max_slots; } -apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m) +void h2_workers_shutdown(h2_workers *workers, int graceful) { - return h2_fifo_remove(workers->mplxs, m); + ap_conn_producer_t *prod; + + apr_thread_mutex_lock(workers->lock); + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s, + "h2_workers: shutdown graceful=%d", graceful); + workers->shutdown = 1; + workers->idle_limit = apr_time_from_sec(1); + wake_all_idles(workers); + for (prod = APR_RING_FIRST(&workers->prod_idle); + prod != APR_RING_SENTINEL(&workers->prod_idle, ap_conn_producer_t, link); + prod = APR_RING_NEXT(prod, link)) { + if (prod->fn_shutdown) { + prod->fn_shutdown(prod->baton, graceful); + } + } + apr_thread_mutex_unlock(workers->lock); +} + +ap_conn_producer_t *h2_workers_register(h2_workers *workers, + apr_pool_t *producer_pool, + const char *name, + ap_conn_producer_next *fn_next, + ap_conn_producer_done *fn_done, + ap_conn_producer_shutdown *fn_shutdown, + void *baton) +{ + ap_conn_producer_t *prod; + + prod = apr_pcalloc(producer_pool, sizeof(*prod)); + APR_RING_ELEM_INIT(prod, link); + prod->name = name; + prod->fn_next = fn_next; + prod->fn_done = fn_done; + prod->fn_shutdown = fn_shutdown; + prod->baton = baton; + + apr_thread_mutex_lock(workers->lock); + prod->state = PROD_IDLE; + APR_RING_INSERT_TAIL(&workers->prod_idle, prod, ap_conn_producer_t, link); + apr_thread_mutex_unlock(workers->lock); + + return prod; +} + +apr_status_t h2_workers_join(h2_workers *workers, ap_conn_producer_t *prod) +{ + apr_status_t rv = APR_SUCCESS; + + apr_thread_mutex_lock(workers->lock); + if (PROD_JOINED == prod->state) { + AP_DEBUG_ASSERT(APR_RING_NEXT(prod, link) == prod); /* should be in no ring */ + rv = APR_EINVAL; + } + else { + AP_DEBUG_ASSERT(PROD_ACTIVE == prod->state || PROD_IDLE == prod->state); + APR_RING_REMOVE(prod, link); + prod->state = PROD_JOINED; /* prevent further activations */ + while (prod->conns_active > 0) { + apr_thread_cond_wait(workers->prod_done, workers->lock); + } + APR_RING_ELEM_INIT(prod, link); /* make it link to itself */ + } + apr_thread_mutex_unlock(workers->lock); + return rv; +} + +apr_status_t h2_workers_activate(h2_workers *workers, ap_conn_producer_t *prod) +{ + apr_status_t rv = APR_SUCCESS; + apr_thread_mutex_lock(workers->lock); + if (PROD_IDLE == prod->state) { + APR_RING_REMOVE(prod, link); + prod->state = PROD_ACTIVE; + APR_RING_INSERT_TAIL(&workers->prod_active, prod, ap_conn_producer_t, link); + wake_idle_worker(workers, prod); + } + else if (PROD_JOINED == prod->state) { + rv = APR_EINVAL; + } + apr_thread_mutex_unlock(workers->lock); + return rv; } diff --git a/modules/http2/h2_workers.h b/modules/http2/h2_workers.h index 3561582..c219304 100644 --- a/modules/http2/h2_workers.h +++ b/modules/http2/h2_workers.h @@ -17,66 +17,113 @@ #ifndef __mod_h2__h2_workers__ #define __mod_h2__h2_workers__ -/* Thread pool specific to executing h2_tasks. Has a minimum and maximum - * number of workers it creates. Starts with minimum workers and adds - * some on load, reduces the number again when idle. - * +/* Thread pool specific to executing secondary connections. + * Has a minimum and maximum number of workers it creates. + * Starts with minimum workers and adds some on load, + * reduces the number again when idle. */ struct apr_thread_mutex_t; struct apr_thread_cond_t; struct h2_mplx; struct h2_request; -struct h2_task; struct h2_fifo; -struct h2_slot; - typedef struct h2_workers h2_workers; -struct h2_workers { - server_rec *s; - apr_pool_t *pool; - - int next_worker_id; - int min_workers; - int max_workers; - int max_idle_secs; - - int aborted; - int dynamic; - apr_threadattr_t *thread_attr; - int nslots; - struct h2_slot *slots; - - volatile apr_uint32_t worker_count; - - struct h2_slot *free; - struct h2_slot *idle; - struct h2_slot *zombies; - - struct h2_fifo *mplxs; - - struct apr_thread_mutex_t *lock; -}; +/** + * Create a worker set with a maximum number of 'slots', e.g. worker + * threads to run. Always keep `min_active` workers running. Shutdown + * any additional workers after `idle_secs` seconds of doing nothing. + * + * @oaram s the base server + * @param pool for allocations + * @param min_active minimum number of workers to run + * @param max_slots maximum number of worker slots + * @param idle_limit upper duration of idle after a non-minimal slots shuts down + */ +h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pool, + int max_slots, int min_active, apr_time_t idle_limit); +/** + * Shut down processing. + */ +void h2_workers_shutdown(h2_workers *workers, int graceful); -/* Create a worker pool with the given minimum and maximum number of - * threads. +/** + * Get the maximum number of workers. */ -h2_workers *h2_workers_create(server_rec *s, apr_pool_t *pool, - int min_size, int max_size, int idle_secs); +apr_uint32_t h2_workers_get_max_workers(h2_workers *workers); + +/** + * ap_conn_producer_t is the source of connections (conn_rec*) to run. + * + * Active producers are queried by idle workers for connections. + * If they do not hand one back, they become inactive and are not + * queried further. `h2_workers_activate()` places them on the active + * list again. + * + * A producer finishing MUST call `h2_workers_join()` which removes + * it completely from workers processing and waits for all ongoing + * work for this producer to be done. + */ +typedef struct ap_conn_producer_t ap_conn_producer_t; + +/** + * Ask a producer for the next connection to process. + * @param baton value from producer registration + * @param pconn holds the connection to process on return + * @param pmore if the producer has more connections that may be retrieved + * @return APR_SUCCESS for a connection to process, APR_EAGAIN for no + * connection being available at the time. + */ +typedef conn_rec *ap_conn_producer_next(void *baton, int *pmore); + +/** + * Tell the producer that processing the connection is done. + * @param baton value from producer registration + * @param conn the connection that has been processed. + */ +typedef void ap_conn_producer_done(void *baton, conn_rec *conn); + +/** + * Tell the producer that the workers are shutting down. + * @param baton value from producer registration + * @param graceful != 0 iff shutdown is graceful + */ +typedef void ap_conn_producer_shutdown(void *baton, int graceful); + +/** + * Register a new producer with the given `baton` and callback functions. + * Will allocate internal structures from the given pool (but make no use + * of the pool after registration). + * Producers are inactive on registration. See `h2_workers_activate()`. + * @param producer_pool to allocate the producer from + * @param name descriptive name of the producer, must not be unique + * @param fn_next callback for retrieving connections to process + * @param fn_done callback for processed connections + * @param baton provided value passed on in callbacks + * @return the producer instance created + */ +ap_conn_producer_t *h2_workers_register(h2_workers *workers, + apr_pool_t *producer_pool, + const char *name, + ap_conn_producer_next *fn_next, + ap_conn_producer_done *fn_done, + ap_conn_producer_shutdown *fn_shutdown, + void *baton); /** - * Registers a h2_mplx for task scheduling. If this h2_mplx runs - * out of tasks, it will be automatically be unregistered. Should - * new tasks arrive, it needs to be registered again. + * Stop retrieving more connection from the producer and wait + * for all ongoing for from that producer to be done. */ -apr_status_t h2_workers_register(h2_workers *workers, struct h2_mplx *m); +apr_status_t h2_workers_join(h2_workers *workers, ap_conn_producer_t *producer); /** - * Remove a h2_mplx from the worker registry. + * Activate a producer. A worker will query the producer for a connection + * to process, once a worker is available. + * This may be called, irregardless of the producers active/inactive. */ -apr_status_t h2_workers_unregister(h2_workers *workers, struct h2_mplx *m); +apr_status_t h2_workers_activate(h2_workers *workers, ap_conn_producer_t *producer); #endif /* defined(__mod_h2__h2_workers__) */ diff --git a/modules/http2/h2_ws.c b/modules/http2/h2_ws.c new file mode 100644 index 0000000..396e6e1 --- /dev/null +++ b/modules/http2/h2_ws.c @@ -0,0 +1,362 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> + +#include "apr.h" +#include "apr_strings.h" +#include "apr_lib.h" +#include "apr_sha1.h" +#include "apr_strmatch.h" + +#include <ap_mmn.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_connection.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_log.h> +#include <http_ssl.h> +#include <http_vhost.h> +#include <util_filter.h> +#include <ap_mpm.h> + +#include "h2_private.h" +#include "h2_config.h" +#include "h2_conn_ctx.h" +#include "h2_headers.h" +#include "h2_request.h" +#include "h2_ws.h" + +#if H2_USE_WEBSOCKETS + +#include "apr_encode.h" /* H2_USE_WEBSOCKETS is conditional on APR 1.6+ */ + +static ap_filter_rec_t *c2_ws_out_filter_handle; + +struct ws_filter_ctx { + const char *ws_accept_base64; + int has_final_response; + int override_body; +}; + +/** + * Generate the "Sec-WebSocket-Accept" header field for the given key + * (base64 encoded) as defined in RFC 6455 ch. 4.2.2 step 5.3 + */ +static const char *gen_ws_accept(conn_rec *c, const char *key_base64) +{ + apr_byte_t dgst[APR_SHA1_DIGESTSIZE]; + const char ws_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + apr_sha1_ctx_t sha1_ctx; + + apr_sha1_init(&sha1_ctx); + apr_sha1_update(&sha1_ctx, key_base64, (unsigned int)strlen(key_base64)); + apr_sha1_update(&sha1_ctx, ws_guid, (unsigned int)strlen(ws_guid)); + apr_sha1_final(dgst, &sha1_ctx); + + return apr_pencode_base64_binary(c->pool, dgst, sizeof(dgst), + APR_ENCODE_NONE, NULL); +} + +const h2_request *h2_ws_rewrite_request(const h2_request *req, + conn_rec *c2, int no_body) +{ + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2); + h2_request *wsreq; + unsigned char key_raw[16]; + const char *key_base64, *accept_base64; + struct ws_filter_ctx *ws_ctx; + apr_status_t rv; + + if (!conn_ctx || !req->protocol || strcmp("websocket", req->protocol)) + return req; + + if (ap_cstr_casecmp("CONNECT", req->method)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): websocket request with method %s", + conn_ctx->id, conn_ctx->stream_id, req->method); + return req; + } + if (!req->scheme) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): websocket CONNECT without :scheme", + conn_ctx->id, conn_ctx->stream_id); + return req; + } + if (!req->path) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): websocket CONNECT without :path", + conn_ctx->id, conn_ctx->stream_id); + return req; + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): websocket CONNECT for %s", + conn_ctx->id, conn_ctx->stream_id, req->path); + /* Transform the HTTP/2 extended CONNECT to an internal GET using + * the HTTP/1.1 version of websocket connection setup. */ + wsreq = h2_request_clone(c2->pool, req); + wsreq->method = "GET"; + wsreq->protocol = NULL; + apr_table_set(wsreq->headers, "Upgrade", "websocket"); + apr_table_add(wsreq->headers, "Connection", "Upgrade"); + /* add Sec-WebSocket-Key header */ + rv = apr_generate_random_bytes(key_raw, sizeof(key_raw)); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_CRIT, rv, NULL, APLOGNO(10461) + "error generating secret"); + return NULL; + } + key_base64 = apr_pencode_base64_binary(c2->pool, key_raw, sizeof(key_raw), + APR_ENCODE_NONE, NULL); + apr_table_set(wsreq->headers, "Sec-WebSocket-Key", key_base64); + /* This is now the request to process internally */ + + /* When this request gets processed and delivers a 101 response, + * we expect it to carry a "Sec-WebSocket-Accept" header with + * exactly the following value, as per RFC 6455. */ + accept_base64 = gen_ws_accept(c2, key_base64); + /* Add an output filter that intercepts generated responses: + * - if a valid WebSocket negotiation happens, transform the + * 101 response to a 200 + * - if a 2xx response happens, that does not pass the Accept test, + * return a 502 indicating that the URI seems not support the websocket + * protocol (RFC 8441 does not define this, but it seems the best + * choice) + * - if a 3xx, 4xx or 5xx response happens, forward this unchanged. + */ + ws_ctx = apr_pcalloc(c2->pool, sizeof(*ws_ctx)); + ws_ctx->ws_accept_base64 = accept_base64; + /* insert our filter just before the C2 core filter */ + ap_remove_output_filter_byhandle(c2->output_filters, "H2_C2_NET_OUT"); + ap_add_output_filter("H2_C2_WS_OUT", ws_ctx, NULL, c2); + ap_add_output_filter("H2_C2_NET_OUT", NULL, NULL, c2); + /* Mark the connection as being an Upgrade, with some special handling + * since the request needs an EOS, without the stream being closed */ + conn_ctx->is_upgrade = 1; + + return wsreq; +} + +static apr_bucket *make_valid_resp(conn_rec *c2, int status, + apr_table_t *headers, apr_table_t *notes) +{ + apr_table_t *nheaders, *nnotes; + + ap_assert(headers); + nheaders = apr_table_clone(c2->pool, headers); + apr_table_unset(nheaders, "Connection"); + apr_table_unset(nheaders, "Upgrade"); + apr_table_unset(nheaders, "Sec-WebSocket-Accept"); + nnotes = notes? apr_table_clone(c2->pool, notes) : + apr_table_make(c2->pool, 10); +#if AP_HAS_RESPONSE_BUCKETS + return ap_bucket_response_create(status, NULL, nheaders, nnotes, + c2->pool, c2->bucket_alloc); +#else + return h2_bucket_headers_create(c2->bucket_alloc, + h2_headers_create(status, nheaders, + nnotes, 0, c2->pool)); +#endif +} + +static apr_bucket *make_invalid_resp(conn_rec *c2, int status, + apr_table_t *notes) +{ + apr_table_t *nheaders, *nnotes; + + nheaders = apr_table_make(c2->pool, 10); + apr_table_setn(nheaders, "Content-Length", "0"); + nnotes = notes? apr_table_clone(c2->pool, notes) : + apr_table_make(c2->pool, 10); +#if AP_HAS_RESPONSE_BUCKETS + return ap_bucket_response_create(status, NULL, nheaders, nnotes, + c2->pool, c2->bucket_alloc); +#else + return h2_bucket_headers_create(c2->bucket_alloc, + h2_headers_create(status, nheaders, + nnotes, 0, c2->pool)); +#endif +} + +static void ws_handle_resp(conn_rec *c2, h2_conn_ctx_t *conn_ctx, + struct ws_filter_ctx *ws_ctx, apr_bucket *b) +{ +#if AP_HAS_RESPONSE_BUCKETS + ap_bucket_response *resp = b->data; +#else /* AP_HAS_RESPONSE_BUCKETS */ + h2_headers *resp = h2_bucket_headers_get(b); +#endif /* !AP_HAS_RESPONSE_BUCKETS */ + apr_bucket *b_override = NULL; + int is_final = 0; + int override_body = 0; + + if (ws_ctx->has_final_response) { + /* already did, nop */ + return; + } + + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, + "h2_c2(%s-%d): H2_C2_WS_OUT inspecting response %d", + conn_ctx->id, conn_ctx->stream_id, resp->status); + if (resp->status == HTTP_SWITCHING_PROTOCOLS) { + /* The resource agreed to switch protocol. But this is only valid + * if it send back the correct Sec-WebSocket-Accept header value */ + const char *hd = apr_table_get(resp->headers, "Sec-WebSocket-Accept"); + if (hd && !strcmp(ws_ctx->ws_accept_base64, hd)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): websocket CONNECT, valid 101 Upgrade" + ", converting to 200 response", + conn_ctx->id, conn_ctx->stream_id); + b_override = make_valid_resp(c2, HTTP_OK, resp->headers, resp->notes); + is_final = 1; + } + else { + if (!hd) { + /* This points to someone being confused */ + ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c2, APLOGNO(10462) + "h2_c2(%s-%d): websocket CONNECT, got 101 response " + "without Sec-WebSocket-Accept header", + conn_ctx->id, conn_ctx->stream_id); + } + else { + /* This points to a bug, either in our WebSockets negotiation + * or in the request processings implementation of WebSockets */ + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c2, APLOGNO(10463) + "h2_c2(%s-%d): websocket CONNECT, 101 response " + "with 'Sec-WebSocket-Accept: %s' but expected %s", + conn_ctx->id, conn_ctx->stream_id, hd, + ws_ctx->ws_accept_base64); + } + b_override = make_invalid_resp(c2, HTTP_BAD_GATEWAY, resp->notes); + override_body = is_final = 1; + } + } + else if (resp->status < 200) { + /* other intermediate response, pass through */ + } + else if (resp->status < 300) { + /* Failure, we might be talking to a plain http resource */ + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, + "h2_c2(%s-%d): websocket CONNECT, invalid response %d", + conn_ctx->id, conn_ctx->stream_id, resp->status); + b_override = make_invalid_resp(c2, HTTP_BAD_GATEWAY, resp->notes); + override_body = is_final = 1; + } + else { + /* error response, pass through. */ + ws_ctx->has_final_response = 1; + } + + if (b_override) { + APR_BUCKET_INSERT_BEFORE(b, b_override); + apr_bucket_delete(b); + b = b_override; + } + if (override_body) { + APR_BUCKET_INSERT_AFTER(b, apr_bucket_eos_create(c2->bucket_alloc)); + ws_ctx->override_body = 1; + } + if (is_final) { + ws_ctx->has_final_response = 1; + conn_ctx->has_final_response = 1; + } +} + +static apr_status_t h2_c2_ws_filter_out(ap_filter_t* f, apr_bucket_brigade* bb) +{ + struct ws_filter_ctx *ws_ctx = f->ctx; + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); + apr_bucket *b, *bnext; + + ap_assert(conn_ctx); + if (ws_ctx->override_body) { + /* We have overridden the original response and also its body. + * If this filter is called again, we signal a hard abort to + * allow processing to terminate at the earliest. */ + f->c->aborted = 1; + return APR_ECONNABORTED; + } + + /* Inspect the brigade, looking for RESPONSE/HEADER buckets. + * Remember, this filter is only active for client websocket CONNECT + * requests that we translated to an internal GET with websocket + * headers. + * We inspect the repsone to see if the internal resource actually + * agrees to talk websocket or is "just" a normal HTTP resource that + * ignored the websocket request headers. */ + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = bnext) + { + bnext = APR_BUCKET_NEXT(b); + if (APR_BUCKET_IS_METADATA(b)) { +#if AP_HAS_RESPONSE_BUCKETS + if (AP_BUCKET_IS_RESPONSE(b)) { +#else + if (H2_BUCKET_IS_HEADERS(b)) { +#endif /* !AP_HAS_RESPONSE_BUCKETS */ + ws_handle_resp(f->c, conn_ctx, ws_ctx, b); + continue; + } + } + else if (ws_ctx->override_body) { + apr_bucket_delete(b); + } + } + return ap_pass_brigade(f->next, bb); +} + +static int ws_post_read(request_rec *r) +{ + + if (r->connection->master) { + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(r->connection); + if (conn_ctx && conn_ctx->is_upgrade && + !h2_config_sgeti(r->server, H2_CONF_WEBSOCKETS)) { + return HTTP_NOT_IMPLEMENTED; + } + } + return DECLINED; +} + +void h2_ws_register_hooks(void) +{ + ap_hook_post_read_request(ws_post_read, NULL, NULL, APR_HOOK_MIDDLE); + c2_ws_out_filter_handle = + ap_register_output_filter("H2_C2_WS_OUT", h2_c2_ws_filter_out, + NULL, AP_FTYPE_NETWORK); +} + +#else /* H2_USE_WEBSOCKETS */ + +const h2_request *h2_ws_rewrite_request(const h2_request *req, + conn_rec *c2, int no_body) +{ + (void)c2; + (void)no_body; + /* no rewriting */ + return req; +} + +void h2_ws_register_hooks(void) +{ + /* NOP */ +} + +#endif /* H2_USE_WEBSOCKETS (else part) */ diff --git a/modules/http2/h2_alt_svc.h b/modules/http2/h2_ws.h index 479e4d1..a94d300 100644 --- a/modules/http2/h2_alt_svc.h +++ b/modules/http2/h2_ws.h @@ -14,27 +14,22 @@ * limitations under the License. */ -#ifndef __mod_h2__h2_alt_svc__ -#define __mod_h2__h2_alt_svc__ +#ifndef __mod_h2__h2_ws__ +#define __mod_h2__h2_ws__ -typedef struct h2_alt_svc h2_alt_svc; - -struct h2_alt_svc { - const char *alpn; - const char *host; - int port; -}; - -void h2_alt_svc_register_hooks(void); +#include "h2.h" /** - * Parse an Alt-Svc specifier as described in "HTTP Alternative Services" - * (https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-04) - * with the following changes: - * - do not percent encode token values - * - do not use quotation marks + * Rewrite a websocket request. + * + * @param req the h2 request to rewrite + * @param c2 the connection to process the request on + * @param no_body != 0 iff the request is known to have no body + * @return the websocket request for internal submit */ -h2_alt_svc *h2_alt_svc_parse(const char *s, apr_pool_t *pool); +const h2_request *h2_ws_rewrite_request(const h2_request *req, + conn_rec *c2, int no_body); +void h2_ws_register_hooks(void); -#endif /* defined(__mod_h2__h2_alt_svc__) */ +#endif /* defined(__mod_h2__h2_ws__) */ diff --git a/modules/http2/mod_http2.c b/modules/http2/mod_http2.c index 3d278e9..1bd34b2 100644 --- a/modules/http2/mod_http2.c +++ b/modules/http2/mod_http2.c @@ -24,24 +24,25 @@ #include <http_protocol.h> #include <http_request.h> #include <http_log.h> +#include <mpm_common.h> #include "mod_http2.h" #include <nghttp2/nghttp2.h> #include "h2_stream.h" -#include "h2_alt_svc.h" -#include "h2_conn.h" -#include "h2_filter.h" -#include "h2_task.h" +#include "h2_c1.h" +#include "h2_c2.h" #include "h2_session.h" #include "h2_config.h" -#include "h2_ctx.h" -#include "h2_h2.h" +#include "h2_conn_ctx.h" +#include "h2_protocol.h" #include "h2_mplx.h" #include "h2_push.h" #include "h2_request.h" #include "h2_switch.h" #include "h2_version.h" +#include "h2_bucket_beam.h" +#include "h2_ws.h" static void h2_hooks(apr_pool_t *pool); @@ -125,27 +126,6 @@ static int h2_post_config(apr_pool_t *p, apr_pool_t *plog, myfeats.dyn_windows? "+DWINS" : "", ngh2? ngh2->version_str : "unknown"); - switch (h2_conn_mpm_type()) { - case H2_MPM_SIMPLE: - case H2_MPM_MOTORZ: - case H2_MPM_NETWARE: - case H2_MPM_WINNT: - /* not sure we need something extra for those. */ - break; - case H2_MPM_EVENT: - case H2_MPM_WORKER: - /* all fine, we know these ones */ - break; - case H2_MPM_PREFORK: - /* ok, we now know how to handle that one */ - break; - case H2_MPM_UNKNOWN: - /* ??? */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03091) - "post_config: mpm type unknown"); - break; - } - if (!h2_mpm_supported() && !mpm_warned) { mpm_warned = 1; ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10034) @@ -157,14 +137,11 @@ static int h2_post_config(apr_pool_t *p, apr_pool_t *plog, h2_conn_mpm_name()); } - status = h2_h2_init(p, s); + status = h2_protocol_init(p, s); if (status == APR_SUCCESS) { status = h2_switch_init(p, s); } - if (status == APR_SUCCESS) { - status = h2_task_init(p, s); - } - + return status; } @@ -172,44 +149,29 @@ static char *http2_var_lookup(apr_pool_t *, server_rec *, conn_rec *, request_rec *, char *name); static int http2_is_h2(conn_rec *); -static apr_status_t http2_req_engine_push(const char *ngn_type, - request_rec *r, - http2_req_engine_init *einit) -{ - return h2_mplx_req_engine_push(ngn_type, r, einit); -} - -static apr_status_t http2_req_engine_pull(h2_req_engine *ngn, - apr_read_type_e block, - int capacity, - request_rec **pr) -{ - return h2_mplx_req_engine_pull(ngn, block, capacity, pr); -} - -static void http2_req_engine_done(h2_req_engine *ngn, conn_rec *r_conn, - apr_status_t status) -{ - h2_mplx_req_engine_done(ngn, r_conn, status); -} - static void http2_get_num_workers(server_rec *s, int *minw, int *maxw) { - h2_get_num_workers(s, minw, maxw); + apr_time_t tdummy; + + h2_get_workers_config(s, minw, maxw, &tdummy); } /* Runs once per created child process. Perform any process * related initionalization here. */ -static void h2_child_init(apr_pool_t *pool, server_rec *s) +static void h2_child_init(apr_pool_t *pchild, server_rec *s) { + apr_status_t rv; + /* Set up our connection processing */ - apr_status_t status = h2_conn_child_init(pool, s); - if (status != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_ERR, status, s, + rv = h2_c1_child_init(pchild, s); + if (APR_SUCCESS == rv) { + rv = h2_c2_child_init(pchild, s); + } + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(02949) "initializing connection handling"); } - } /* Install this module into the apache2 infrastructure. @@ -220,9 +182,6 @@ static void h2_hooks(apr_pool_t *pool) APR_REGISTER_OPTIONAL_FN(http2_is_h2); APR_REGISTER_OPTIONAL_FN(http2_var_lookup); - APR_REGISTER_OPTIONAL_FN(http2_req_engine_push); - APR_REGISTER_OPTIONAL_FN(http2_req_engine_pull); - APR_REGISTER_OPTIONAL_FN(http2_req_engine_done); APR_REGISTER_OPTIONAL_FN(http2_get_num_workers); ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks"); @@ -234,47 +193,45 @@ static void h2_hooks(apr_pool_t *pool) /* Run once after a child process has been created. */ ap_hook_child_init(h2_child_init, NULL, NULL, APR_HOOK_MIDDLE); +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 110) + ap_hook_child_stopping(h2_c1_child_stopping, NULL, NULL, APR_HOOK_MIDDLE); +#endif - h2_h2_register_hooks(); + h2_c1_register_hooks(); h2_switch_register_hooks(); - h2_task_register_hooks(); + h2_c2_register_hooks(); + h2_ws_register_hooks(); - h2_alt_svc_register_hooks(); - - /* Setup subprocess env for certain variables + /* Setup subprocess env for certain variables */ ap_hook_fixups(h2_h2_fixups, NULL,NULL, APR_HOOK_MIDDLE); - - /* test http2 connection status handler */ - ap_hook_handler(h2_filter_h2_status_handler, NULL, NULL, APR_HOOK_MIDDLE); } static const char *val_HTTP2(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx) + conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx) { return ctx? "on" : "off"; } static const char *val_H2_PUSH(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx) + conn_rec *c, request_rec *r, + h2_conn_ctx_t *conn_ctx) { - if (ctx) { + if (conn_ctx) { if (r) { - h2_task *task = h2_ctx_get_task(ctx); - if (task) { - h2_stream *stream = h2_mplx_stream_get(task->mplx, task->stream_id); + if (conn_ctx->stream_id) { + const h2_stream *stream = h2_mplx_c2_stream_get(conn_ctx->mplx, conn_ctx->stream_id); if (stream && stream->push_policy != H2_PUSH_NONE) { return "on"; } } } - else if (c && h2_session_push_enabled(ctx->session)) { + else if (c && h2_session_push_enabled(conn_ctx->session)) { return "on"; } } else if (s) { - const h2_config *cfg = h2_config_sget(s); - if (cfg && h2_config_geti(cfg, H2_CONF_PUSH)) { + if (h2_config_geti(r, s, H2_CONF_PUSH)) { return "on"; } } @@ -282,11 +239,11 @@ static const char *val_H2_PUSH(apr_pool_t *p, server_rec *s, } static const char *val_H2_PUSHED(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx) + conn_rec *c, request_rec *r, + h2_conn_ctx_t *conn_ctx) { - if (ctx) { - h2_task *task = h2_ctx_get_task(ctx); - if (task && !H2_STREAM_CLIENT_INITIATED(task->stream_id)) { + if (conn_ctx) { + if (conn_ctx->stream_id && !H2_STREAM_CLIENT_INITIATED(conn_ctx->stream_id)) { return "PUSHED"; } } @@ -294,12 +251,12 @@ static const char *val_H2_PUSHED(apr_pool_t *p, server_rec *s, } static const char *val_H2_PUSHED_ON(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx) + conn_rec *c, request_rec *r, + h2_conn_ctx_t *conn_ctx) { - if (ctx) { - h2_task *task = h2_ctx_get_task(ctx); - if (task && !H2_STREAM_CLIENT_INITIATED(task->stream_id)) { - h2_stream *stream = h2_mplx_stream_get(task->mplx, task->stream_id); + if (conn_ctx) { + if (conn_ctx->stream_id && !H2_STREAM_CLIENT_INITIATED(conn_ctx->stream_id)) { + const h2_stream *stream = h2_mplx_c2_stream_get(conn_ctx->mplx, conn_ctx->stream_id); if (stream) { return apr_itoa(p, stream->initiated_on); } @@ -309,29 +266,30 @@ static const char *val_H2_PUSHED_ON(apr_pool_t *p, server_rec *s, } static const char *val_H2_STREAM_TAG(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx) + conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx) { - if (ctx) { - h2_task *task = h2_ctx_get_task(ctx); - if (task) { - return task->id; + if (c) { + h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c); + if (conn_ctx) { + return conn_ctx->stream_id == 0? conn_ctx->id + : apr_psprintf(p, "%s-%d", conn_ctx->id, conn_ctx->stream_id); } } return ""; } static const char *val_H2_STREAM_ID(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx) + conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx) { const char *cp = val_H2_STREAM_TAG(p, s, c, r, ctx); - if (cp && (cp = ap_strchr_c(cp, '-'))) { + if (cp && (cp = ap_strrchr_c(cp, '-'))) { return ++cp; } return NULL; } typedef const char *h2_var_lookup(apr_pool_t *p, server_rec *s, - conn_rec *c, request_rec *r, h2_ctx *ctx); + conn_rec *c, request_rec *r, h2_conn_ctx_t *ctx); typedef struct h2_var_def { const char *name; h2_var_lookup *lookup; @@ -355,19 +313,19 @@ static h2_var_def H2_VARS[] = { static int http2_is_h2(conn_rec *c) { - return h2_ctx_get(c->master? c->master : c, 0) != NULL; + return h2_conn_ctx_get(c->master? c->master : c) != NULL; } static char *http2_var_lookup(apr_pool_t *p, server_rec *s, conn_rec *c, request_rec *r, char *name) { - int i; + unsigned int i; /* If the # of vars grow, we need to put definitions in a hash */ for (i = 0; i < H2_ALEN(H2_VARS); ++i) { h2_var_def *vdef = &H2_VARS[i]; if (!strcmp(vdef->name, name)) { - h2_ctx *ctx = (r? h2_ctx_rget(r) : - h2_ctx_get(c->master? c->master : c, 0)); + h2_conn_ctx_t *ctx = (r? h2_conn_ctx_get(c) : + h2_conn_ctx_get(c->master? c->master : c)); return (char *)vdef->lookup(p, s, c, r, ctx); } } @@ -377,9 +335,9 @@ static char *http2_var_lookup(apr_pool_t *p, server_rec *s, static int h2_h2_fixups(request_rec *r) { if (r->connection->master) { - h2_ctx *ctx = h2_ctx_rget(r); - int i; - + h2_conn_ctx_t *ctx = h2_conn_ctx_get(r->connection); + unsigned int i; + for (i = 0; ctx && i < H2_ALEN(H2_VARS); ++i) { h2_var_def *vdef = &H2_VARS[i]; if (vdef->subprocess) { diff --git a/modules/http2/mod_http2.dep b/modules/http2/mod_http2.dep index 52f2286..25c0ede 100644 --- a/modules/http2/mod_http2.dep +++ b/modules/http2/mod_http2.dep @@ -694,7 +694,6 @@ ".\h2_ctx.h"\ ".\h2_h2.h"\ ".\h2_mplx.h"\ - ".\h2_ngn_shed.h"\ ".\h2_private.h"\ ".\h2_request.h"\ ".\h2_stream.h"\ @@ -754,7 +753,6 @@ ".\h2_ctx.h"\ ".\h2_h2.h"\ ".\h2_mplx.h"\ - ".\h2_ngn_shed.h"\ ".\h2_private.h"\ ".\h2_request.h"\ ".\h2_task.h"\ diff --git a/modules/http2/mod_http2.dsp b/modules/http2/mod_http2.dsp index d1c4322..9775534 100644 --- a/modules/http2/mod_http2.dsp +++ b/modules/http2/mod_http2.dsp @@ -101,10 +101,6 @@ PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).ma # Name "mod_http2 - Win32 Debug" # Begin Source File -SOURCE=./h2_alt_svc.c -# End Source File -# Begin Source File - SOURCE=./h2_bucket_beam.c # End Source File # Begin Source File @@ -113,31 +109,31 @@ SOURCE=./h2_bucket_eos.c # End Source File # Begin Source File -SOURCE=./h2_config.c +SOURCE=./h2_c1.c # End Source File # Begin Source File -SOURCE=./h2_conn.c +SOURCE=./h2_c1_io.c # End Source File # Begin Source File -SOURCE=./h2_conn_io.c +SOURCE=./h2_c2.c # End Source File # Begin Source File -SOURCE=./h2_ctx.c +SOURCE=./h2_c2_filter.c # End Source File # Begin Source File -SOURCE=./h2_filter.c +SOURCE=./h2_config.c # End Source File # Begin Source File -SOURCE=./h2_from_h1.c +SOURCE=./h2_conn_ctx.c # End Source File # Begin Source File -SOURCE=./h2_h2.c +SOURCE=./h2_headers.c # End Source File # Begin Source File @@ -145,7 +141,7 @@ SOURCE=./h2_mplx.c # End Source File # Begin Source File -SOURCE=./h2_ngn_shed.c +SOURCE=./h2_protocol.c # End Source File # Begin Source File @@ -157,10 +153,6 @@ SOURCE=./h2_request.c # End Source File # Begin Source File -SOURCE=./h2_headers.c -# End Source File -# Begin Source File - SOURCE=./h2_session.c # End Source File # Begin Source File @@ -173,15 +165,19 @@ SOURCE=./h2_switch.c # End Source File # Begin Source File -SOURCE=./h2_task.c +SOURCE=./h2_util.c # End Source File # Begin Source File -SOURCE=./h2_util.c +SOURCE=./h2_workers.c # End Source File # Begin Source File -SOURCE=./h2_workers.c +SOURCE=./h2_ws.c +# End Source File +# Begin Source File + +SOURCE=./mod_http2.c # End Source File # Begin Source File diff --git a/modules/http2/mod_http2.h b/modules/http2/mod_http2.h index 7a1b49a..9cb04a6 100644 --- a/modules/http2/mod_http2.h +++ b/modules/http2/mod_http2.h @@ -28,24 +28,48 @@ APR_DECLARE_OPTIONAL_FN(char *, APR_DECLARE_OPTIONAL_FN(int, http2_is_h2, (conn_rec *)); +APR_DECLARE_OPTIONAL_FN(void, + http2_get_num_workers, (server_rec *s, + int *minw, int *max)); + +#define AP_HTTP2_HAS_GET_POLLFD + +/** + * Get a apr_pollfd_t populated for a h2 connection where + * (c->master != NULL) is true and pipes are supported. + * To be used in Apache modules implementing WebSockets in Apache httpd + * versions that do not support the corresponding `ap_get_pollfd_from_conn()` + * function. + * When available, use `ap_get_pollfd_from_conn()` instead of this function. + * + * How it works: pass in a `apr_pollfd_t` which gets populated for + * monitoring the input of connection `c`. If `c` is not a HTTP/2 + * stream connection, the function will return `APR_ENOTIMPL`. + * `ptimeout` is optional and, if passed, will get the timeout in effect + * + * On platforms without support for pipes (e.g. Windows), this function + * will return `APR_ENOTIMPL`. + */ +APR_DECLARE_OPTIONAL_FN(apr_status_t, + http2_get_pollfd_from_conn, + (conn_rec *c, struct apr_pollfd_t *pfd, + apr_interval_time_t *ptimeout)); /******************************************************************************* - * HTTP/2 request engines + * START HTTP/2 request engines (DEPRECATED) ******************************************************************************/ + +/* The following functions were introduced for the experimental mod_proxy_http2 + * support, but have been abandoned since. + * They are still declared here for backward compatibility, in case someone + * tries to build an old mod_proxy_http2 against it, but will disappear + * completely sometime in the future. + */ struct apr_thread_cond_t; - typedef struct h2_req_engine h2_req_engine; - typedef void http2_output_consumed(void *ctx, conn_rec *c, apr_off_t consumed); -/** - * Initialize a h2_req_engine. The structure will be passed in but - * only the name and master are set. The function should initialize - * all fields. - * @param engine the allocated, partially filled structure - * @param r the first request to process, or NULL - */ typedef apr_status_t http2_req_engine_init(h2_req_engine *engine, const char *id, const char *type, @@ -55,35 +79,11 @@ typedef apr_status_t http2_req_engine_init(h2_req_engine *engine, http2_output_consumed **pconsumed, void **pbaton); -/** - * Push a request to an engine with the specified name for further processing. - * If no such engine is available, einit is not NULL, einit is called - * with a new engine record and the caller is responsible for running the - * new engine instance. - * @param engine_type the type of the engine to add the request to - * @param r the request to push to an engine for processing - * @param einit an optional initialization callback for a new engine - * of the requested type, should no instance be available. - * By passing a non-NULL callback, the caller is willing - * to init and run a new engine itself. - * @return APR_SUCCESS iff slave was successfully added to an engine - */ APR_DECLARE_OPTIONAL_FN(apr_status_t, http2_req_engine_push, (const char *engine_type, request_rec *r, http2_req_engine_init *einit)); -/** - * Get a new request for processing in this engine. - * @param engine the engine which is done processing the slave - * @param block if call should block waiting for request to come - * @param capacity how many parallel requests are acceptable - * @param pr the request that needs processing or NULL - * @return APR_SUCCESS if new request was assigned - * APR_EAGAIN if no new request is available - * APR_EOF if engine may shut down, as no more request will be scheduled - * APR_ECONNABORTED if the engine needs to shut down immediately - */ APR_DECLARE_OPTIONAL_FN(apr_status_t, http2_req_engine_pull, (h2_req_engine *engine, apr_read_type_e block, @@ -94,8 +94,9 @@ APR_DECLARE_OPTIONAL_FN(void, conn_rec *rconn, apr_status_t status)); -APR_DECLARE_OPTIONAL_FN(void, - http2_get_num_workers, (server_rec *s, - int *minw, int *max)); + +/******************************************************************************* + * END HTTP/2 request engines (DEPRECATED) + ******************************************************************************/ #endif diff --git a/modules/http2/mod_http2.mak b/modules/http2/mod_http2.mak index 10ae887..26611c7 100644 --- a/modules/http2/mod_http2.mak +++ b/modules/http2/mod_http2.mak @@ -61,7 +61,6 @@ CLEAN : -@erase "$(INTDIR)\h2_h2.obj" -@erase "$(INTDIR)\h2_headers.obj" -@erase "$(INTDIR)\h2_mplx.obj" - -@erase "$(INTDIR)\h2_ngn_shed.obj" -@erase "$(INTDIR)\h2_push.obj" -@erase "$(INTDIR)\h2_request.obj" -@erase "$(INTDIR)\h2_session.obj" @@ -138,7 +137,6 @@ LINK32_OBJS= \ "$(INTDIR)\h2_h2.obj" \ "$(INTDIR)\h2_headers.obj" \ "$(INTDIR)\h2_mplx.obj" \ - "$(INTDIR)\h2_ngn_shed.obj" \ "$(INTDIR)\h2_push.obj" \ "$(INTDIR)\h2_request.obj" \ "$(INTDIR)\h2_session.obj" \ @@ -207,7 +205,6 @@ CLEAN : -@erase "$(INTDIR)\h2_h2.obj" -@erase "$(INTDIR)\h2_headers.obj" -@erase "$(INTDIR)\h2_mplx.obj" - -@erase "$(INTDIR)\h2_ngn_shed.obj" -@erase "$(INTDIR)\h2_push.obj" -@erase "$(INTDIR)\h2_request.obj" -@erase "$(INTDIR)\h2_session.obj" @@ -284,7 +281,6 @@ LINK32_OBJS= \ "$(INTDIR)\h2_h2.obj" \ "$(INTDIR)\h2_headers.obj" \ "$(INTDIR)\h2_mplx.obj" \ - "$(INTDIR)\h2_ngn_shed.obj" \ "$(INTDIR)\h2_push.obj" \ "$(INTDIR)\h2_request.obj" \ "$(INTDIR)\h2_session.obj" \ @@ -469,11 +465,6 @@ SOURCE=./h2_mplx.c "$(INTDIR)\h2_mplx.obj" : $(SOURCE) "$(INTDIR)" -SOURCE=./h2_ngn_shed.c - -"$(INTDIR)\h2_ngn_shed.obj" : $(SOURCE) "$(INTDIR)" - - SOURCE=./h2_push.c "$(INTDIR)\h2_push.obj" : $(SOURCE) "$(INTDIR)" diff --git a/modules/http2/mod_proxy_http2.c b/modules/http2/mod_proxy_http2.c index a7e0dcd..ebf8f61 100644 --- a/modules/http2/mod_proxy_http2.c +++ b/modules/http2/mod_proxy_http2.c @@ -16,13 +16,14 @@ #include <nghttp2/nghttp2.h> +#include <ap_mmn.h> #include <httpd.h> #include <mod_proxy.h> #include "mod_http2.h" #include "mod_proxy_http2.h" -#include "h2_request.h" +#include "h2.h" #include "h2_proxy_util.h" #include "h2_version.h" #include "h2_proxy_session.h" @@ -46,19 +47,11 @@ AP_DECLARE_MODULE(proxy_http2) = { /* 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, - int capacity, - request_rec **pr); -static void (*req_engine_done)(h2_req_engine *engine, conn_rec *r_conn, - apr_status_t status); - + typedef struct h2_proxy_ctx { - conn_rec *owner; + const char *id; + conn_rec *cfront; apr_pool_t *pool; - request_rec *rbase; server_rec *server; const char *proxy_func; char server_portstr[32]; @@ -66,20 +59,16 @@ typedef struct h2_proxy_ctx { 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_size_t req_buffer_size; - h2_proxy_fifo *requests; int 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 */ + request_rec *r; /* the request processed in this ctx */ + int r_status; /* status of request work */ + int r_done; /* request was processed, not necessarily successfully */ + int r_may_retry; /* request may be retried */ + int has_reusable_session; /* http2 session is live and clean */ } h2_proxy_ctx; static int h2_proxy_post_config(apr_pool_t *p, apr_pool_t *plog, @@ -104,16 +93,6 @@ static int h2_proxy_post_config(apr_pool_t *p, apr_pool_t *plog, 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; } @@ -174,9 +153,24 @@ static int proxy_http2_canon(request_rec *r, char *url) if (apr_table_get(r->notes, "proxy-nocanon")) { path = url; /* this is the raw path */ } + else if (apr_table_get(r->notes, "proxy-noencode")) { + path = url; /* this is the encoded path already */ + search = r->args; + } else { +#ifdef PROXY_CANONENC_NOENCODEDSLASHENCODING + core_dir_config *d = ap_get_core_module_config(r->per_dir_config); + int flags = d->allow_encoded_slashes && !d->decode_encoded_slashes ? PROXY_CANONENC_NOENCODEDSLASHENCODING : 0; + + path = ap_proxy_canonenc_ex(r->pool, url, (int)strlen(url), + enc_path, flags, r->proxyreq); +#else path = ap_proxy_canonenc(r->pool, url, (int)strlen(url), enc_path, 0, r->proxyreq); +#endif + if (!path) { + return HTTP_BAD_REQUEST; + } search = r->args; } break; @@ -184,9 +178,21 @@ static int proxy_http2_canon(request_rec *r, char *url) path = url; break; } - - if (path == NULL) { - return HTTP_BAD_REQUEST; + /* + * If we have a raw control character or a ' ' in nocanon path or + * r->args, correct encoding was missed. + */ + if (path == url && *ap_scan_vchar_obstext(path)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10420) + "To be forwarded path contains control " + "characters or spaces"); + return HTTP_FORBIDDEN; + } + if (search && *ap_scan_vchar_obstext(search)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10412) + "To be forwarded query string contains control " + "characters or spaces"); + return HTTP_FORBIDDEN; } if (port != def_port) { @@ -204,45 +210,6 @@ static int proxy_http2_canon(request_rec *r, char *url) 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_size_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) { - ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(03368) - "h2_proxy_session, engine init, no ctx found"); - return APR_ENOTIMPL; - } - - 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 = H2MIN(100, h2_proxy_fifo_capacity(ctx->requests)); - - *pconsumed = out_consumed; - *pctx = ctx; - return APR_SUCCESS; -} - static apr_status_t add_request(h2_proxy_session *session, request_rec *r) { h2_proxy_ctx *ctx = session->user_data; @@ -252,7 +219,7 @@ static apr_status_t add_request(h2_proxy_session *session, request_rec *r) 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, ctx->standalone); + status = h2_proxy_session_submit(session, url, r, 1); if (status != APR_SUCCESS) { ap_log_cerror(APLOG_MARK, APLOG_ERR, status, r->connection, APLOGNO(03351) "pass request body failed to %pI (%s) from %s (%s)", @@ -264,201 +231,84 @@ static apr_status_t add_request(h2_proxy_session *session, request_rec *r) } static void request_done(h2_proxy_ctx *ctx, request_rec *r, - apr_status_t status, int touched) + apr_status_t status, int touched, int error_code) { - const char *task_id = apr_table_get(r->connection->notes, H2_TASK_ID_NOTE); - - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, r->connection, - "h2_proxy_session(%s): request done %s, touched=%d", - ctx->engine_id, task_id, touched); - if (status != APR_SUCCESS) { - if (!touched) { - /* untouched request, need rescheduling */ - status = h2_proxy_fifo_push(ctx->requests, r); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, r->connection, - APLOGNO(03369) - "h2_proxy_session(%s): rescheduled request %s", - ctx->engine_id, task_id); - return; - } - else { - const char *uri; - uri = apr_uri_unparse(r->pool, &r->parsed_uri, 0); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, r->connection, - APLOGNO(03471) "h2_proxy_session(%s): request %s -> %s " - "not complete, cannot repeat", - ctx->engine_id, task_id, uri); - } - } - - if (r == ctx->rbase) { - ctx->r_status = ((status == APR_SUCCESS)? APR_SUCCESS - : HTTP_SERVICE_UNAVAILABLE); - } - - if (req_engine_done && ctx->engine) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, r->connection, - APLOGNO(03370) - "h2_proxy_session(%s): finished request %s", - ctx->engine_id, task_id); - req_engine_done(ctx->engine, r->connection, status); + if (r == ctx->r) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, r->connection, + "h2_proxy_session(%s): request done, touched=%d, error=%d", + ctx->id, touched, error_code); + ctx->r_done = 1; + if (touched) ctx->r_may_retry = 0; + ctx->r_status = error_code? HTTP_BAD_GATEWAY : + ((status == APR_SUCCESS)? OK : + ap_map_http_request_error(status, HTTP_SERVICE_UNAVAILABLE)); } } static void session_req_done(h2_proxy_session *session, request_rec *r, - apr_status_t status, int touched) -{ - request_done(session->user_data, r, status, touched); -} - -static apr_status_t next_request(h2_proxy_ctx *ctx, int before_leave) + apr_status_t status, int touched, int error_code) { - if (h2_proxy_fifo_count(ctx->requests) > 0) { - return APR_SUCCESS; - } - else if (req_engine_pull && ctx->engine) { - apr_status_t status; - request_rec *r = NULL; - - status = req_engine_pull(ctx->engine, before_leave? - APR_BLOCK_READ: APR_NONBLOCK_READ, - ctx->capacity, &r); - if (status == APR_SUCCESS && r) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE3, status, ctx->owner, - "h2_proxy_engine(%s): pulled request (%s) %s", - ctx->engine_id, - before_leave? "before leave" : "regular", - r->the_request); - h2_proxy_fifo_push(ctx->requests, r); - } - return APR_STATUS_IS_EAGAIN(status)? APR_SUCCESS : status; - } - return APR_EOF; + request_done(session->user_data, r, status, touched, error_code); } -static apr_status_t proxy_engine_run(h2_proxy_ctx *ctx) { +static apr_status_t ctx_run(h2_proxy_ctx *ctx) { apr_status_t status = OK; + h2_proxy_session *session; int h2_front; - request_rec *r; /* 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); - h2_front = is_h2? is_h2(ctx->owner) : 0; - ctx->session = h2_proxy_session_setup(ctx->engine_id, ctx->p_conn, ctx->conf, - h2_front, 30, - h2_proxy_log2((int)ctx->req_buffer_size), - session_req_done); - if (!ctx->session) { - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, + ctx->has_reusable_session = 0; /* don't know yet */ + h2_front = is_h2? is_h2(ctx->cfront) : 0; + session = h2_proxy_session_setup(ctx->id, ctx->p_conn, ctx->conf, + h2_front, 30, + h2_proxy_log2((int)ctx->req_buffer_size), + session_req_done); + if (!session) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->cfront, 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; + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->cfront, APLOGNO(03373) + "eng(%s): run session %s", ctx->id, session->id); + session->user_data = ctx; - while (!ctx->owner->aborted) { - if (APR_SUCCESS == h2_proxy_fifo_try_pull(ctx->requests, (void**)&r)) { - add_request(ctx->session, r); - } - - 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 = H2MIN((int)ctx->session->remote_max_concurrent, - h2_proxy_fifo_capacity(ctx->requests)); - } - 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); - /* give notice that we're leaving and cancel all ongoing - * streams. */ - next_request(ctx, 1); - h2_proxy_session_cancel_all(ctx->session); - h2_proxy_session_process(ctx->session); - status = ctx->r_status = APR_SUCCESS; - break; - } - if ((h2_proxy_fifo_count(ctx->requests) == 0) - && h2_proxy_ihash_empty(ctx->session->streams)) { - break; - } - } - else { - /* end of processing, maybe error */ - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, + ctx->r_done = 0; + add_request(session, ctx->r); + + while (!ctx->cfront->aborted && !ctx->r_done) { + + status = h2_proxy_session_process(session); + if (status != APR_SUCCESS) { + /* Encountered an error during session processing */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->cfront, APLOGNO(03375) "eng(%s): end of session %s", - ctx->engine_id, ctx->session->id); - /* - * Any open stream of that session needs to + ctx->id, session->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, session_req_done); - break; + h2_proxy_session_cleanup(session, session_req_done); + goto out; } } - ctx->session->user_data = NULL; - ctx->session = NULL; - +out: + if (ctx->cfront->aborted) { + /* master connection gone */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->cfront, + APLOGNO(03374) "eng(%s): master connection gone", ctx->id); + /* cancel all ongoing requests */ + h2_proxy_session_cancel_all(session); + h2_proxy_session_process(session); + } + ctx->has_reusable_session = h2_proxy_session_is_reusable(session); + session->user_data = NULL; return status; } -static apr_status_t push_request_somewhere(h2_proxy_ctx *ctx, request_rec *r) -{ - 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 && r && 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, r, proxy_engine_init) == APR_SUCCESS) { - if (ctx->engine == NULL) { - /* request has been assigned to an engine in another thread */ - return SUSPENDED; - } - } - } - - 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 h2_proxy_fifo_push(ctx->requests, r); -} - static int proxy_http2_handler(request_rec *r, proxy_worker *worker, proxy_server_conf *conf, @@ -498,29 +348,32 @@ static int proxy_http2_handler(request_rec *r, 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->id = apr_psprintf(r->pool, "%ld", (long)r->connection->id); + ctx->cfront = r->connection; + ctx->pool = r->pool; + 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->is_ssl = is_ssl; + ctx->worker = worker; + ctx->conf = conf; + ctx->req_buffer_size = (32*1024); + ctx->r = r; + ctx->r_status = status = HTTP_SERVICE_UNAVAILABLE; + ctx->r_done = 0; + ctx->r_may_retry = 1; - h2_proxy_fifo_set_create(&ctx->requests, ctx->pool, 100); - - ap_set_module_config(ctx->owner->conn_config, &proxy_http2_module, ctx); + ap_set_module_config(ctx->cfront->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, + apr_table_setn(ctx->r->notes, H2_PROXY_REQ_URL_NOTE, url); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, ctx->r, "H2: serving URL %s", url); run_connect: + if (ctx->cfront->aborted) goto cleanup; + /* 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. */ @@ -530,25 +383,11 @@ run_connect: } ctx->p_conn->is_ssl = ctx->is_ssl; - if (ctx->is_ssl && ctx->p_conn->connection) { - /* If there are some metadata on the connection (e.g. TLS alert), - * let mod_ssl detect them, and create a new connection below. - */ - apr_bucket_brigade *tmp_bb; - tmp_bb = apr_brigade_create(ctx->rbase->pool, - ctx->rbase->connection->bucket_alloc); - status = ap_get_brigade(ctx->p_conn->connection->input_filters, tmp_bb, - AP_MODE_SPECULATIVE, APR_NONBLOCK_READ, 1); - if (status != APR_SUCCESS && !APR_STATUS_IS_EAGAIN(status)) { - ctx->p_conn->close = 1; - } - apr_brigade_cleanup(tmp_bb); - } /* 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, + if ((status = ap_proxy_determine_connection(ctx->pool, ctx->r, conf, worker, ctx->p_conn, &uri, &locurl, proxyname, proxyport, ctx->server_portstr, @@ -556,111 +395,80 @@ run_connect: 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 (r && !ctx->engine) { - ctx->r_status = push_request_somewhere(ctx, r); - r = NULL; - if (ctx->r_status == SUSPENDED) { - /* request was pushed to another thread, leave processing here */ - 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_DEBUG, 0, ctx->owner, APLOGNO(03352) + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->cfront, APLOGNO(03352) "H2: failed to make connection to backend: %s", ctx->p_conn->hostname); - goto reconnect; + goto cleanup; } /* Step Three: Create conn_rec for the socket we have open now. */ - if (!ctx->p_conn->connection) { - status = ap_proxy_connection_create_ex(ctx->proxy_func, - ctx->p_conn, ctx->rbase); - if (status != OK) { - 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); - goto reconnect; - } - - if (!ctx->p_conn->data) { - /* New conection: set a note on the connection what CN is - * requested and what protocol we want */ - if (ctx->p_conn->ssl_hostname) { - ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, ctx->owner, - "set SNI to %s for (%s)", - ctx->p_conn->ssl_hostname, - ctx->p_conn->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"); - } - } + status = ap_proxy_connection_create_ex(ctx->proxy_func, ctx->p_conn, ctx->r); + if (status != OK) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->cfront, 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); + ctx->r_status = ap_map_http_request_error(status, HTTP_SERVICE_UNAVAILABLE); + goto cleanup; } - -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; + + if (!ctx->p_conn->data && ctx->is_ssl) { + /* New SSL connection: set a note on the connection about what + * protocol we need. */ + apr_table_setn(ctx->p_conn->connection->notes, + "proxy-request-alpn-protos", "h2"); } -reconnect: - if (next_request(ctx, 1) == APR_SUCCESS) { - /* Still more to do, tear down old conn and start over */ + if (ctx->cfront->aborted) goto cleanup; + status = ctx_run(ctx); + + if (ctx->r_status != OK && ctx->r_may_retry && !ctx->cfront->aborted) { + /* Not successfully processed, but may retry, tear down old conn and start over */ if (ctx->p_conn) { ctx->p_conn->close = 1; - /*only in trunk so far */ - /*proxy_run_detach_backend(r, ctx->p_conn);*/ +#if AP_MODULE_MAGIC_AT_LEAST(20140207, 2) + proxy_run_detach_backend(r, ctx->p_conn); +#endif ap_proxy_release_connection(ctx->proxy_func, ctx->p_conn, ctx->server); ctx->p_conn = NULL; } ++reconnects; - if (reconnects < 5 && !ctx->owner->aborted) { + if (reconnects < 2) { goto run_connect; } - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->owner, APLOGNO(10023) - "giving up after %d reconnects, %d requests todo", - reconnects, h2_proxy_fifo_count(ctx->requests)); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->cfront, APLOGNO(10023) + "giving up after %d reconnects, request-done=%d", + reconnects, ctx->r_done); } cleanup: if (ctx->p_conn) { - if (status != APR_SUCCESS) { - /* close socket when errors happened or session shut down (EOF) */ + if (status != APR_SUCCESS || !ctx->has_reusable_session) { + /* close socket when errors happened or session is not "clean", + * meaning in a working condition with no open streams */ ctx->p_conn->close = 1; } - /*only in trunk so far */ - /*proxy_run_detach_backend(ctx->rbase, ctx->p_conn);*/ +#if AP_MODULE_MAGIC_AT_LEAST(20140207, 2) + proxy_run_detach_backend(ctx->r, ctx->p_conn); +#endif ap_proxy_release_connection(ctx->proxy_func, ctx->p_conn, ctx->server); ctx->p_conn = NULL; } - /* Any requests will still have need to fail */ - while (APR_SUCCESS == h2_proxy_fifo_try_pull(ctx->requests, (void**)&r)) { - request_done(ctx, r, HTTP_SERVICE_UNAVAILABLE, 1); - } - - ap_set_module_config(ctx->owner->conn_config, &proxy_http2_module, NULL); - ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->owner, + ap_set_module_config(ctx->cfront->conn_config, &proxy_http2_module, NULL); + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, ctx->cfront, APLOGNO(03377) "leaving handler"); + if (ctx->r_status != OK) { + ap_die(ctx->r_status, r); + } + else if (status != APR_SUCCESS) { + ap_die(ap_map_http_request_error(status, HTTP_SERVICE_UNAVAILABLE), r); + } return ctx->r_status; } |