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)); }
int h2_is_acceptable_connection(conn_rec *c, int require_all) { int is_tls = h2_h2_is_tls(c); const h2_config *cfg = h2_config_get(c); if (is_tls && h2_config_geti(cfg, 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; } /* Need Tlsv1.2 or higher, rfc 7540, ch. 9.2 */ val = opt_ssl_var_lookup(pool, s, c, NULL, (char*)"SSL_PROTOCOL"); if (val && *val) { if (strncmp("TLS", val, 3) || !strcmp("TLSv1", val) || !strcmp("TLSv1.1", val)) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03050) "h2_h2(%ld): tls protocol not suitable: %s", (long)c->id, val); return 0; } } else if (require_all) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03051) "h2_h2(%ld): tls protocol is indetermined", (long)c->id); 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_h2_pre_conn(conn_rec* c, void *arg) { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, start"); h2_ctx *ctx = h2_ctx_get(c, 0); if (!ctx) { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, no ctx"); /* We have not seen this one yet, are we active? */ h2_config *cfg = h2_config_get(c); if (!h2_config_geti(cfg, H2_CONF_ENABLED)) { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, h2 not enabled"); return DECLINED; } /* Are we using TLS on this connection? */ if (!h2_h2_is_tls(c)) { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, no TLS"); return DECLINED; } /* Does mod_ssl offer ALPN/NPN support? */ if (opt_ssl_register_alpn == NULL && opt_ssl_register_npn == NULL) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "h2_h2, pre_connection, no ALPN/NPN support in mod_ssl"); return DECLINED; } ctx = h2_ctx_get(c, 1); if (opt_ssl_register_alpn) { opt_ssl_register_alpn(c, h2_h2_alpn_propose, h2_h2_alpn_negotiated); } if (opt_ssl_register_npn) { opt_ssl_register_npn(c, h2_h2_npn_advertise, h2_h2_npn_negotiated); } ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, ALPN callback registered"); ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, end"); } else if (h2_ctx_is_task(c)) { /* A connection that represents a http2 stream from another connection. */ ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "h2_h2, pre_connection, found stream task"); h2_task *task = h2_ctx_get_task(ctx); return h2_task_pre_conn(task, c); } return DECLINED; }
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)); }
static int h2_h2_npn_advertise(conn_rec *c, apr_array_header_t *protos) { check_sni_host(c); h2_config *cfg = h2_config_get(c); if (!h2_config_geti(cfg, H2_CONF_ENABLED)) { return DECLINED; } for (int i = 0; i < h2_protos_len; ++i) { const char *proto = h2_protos[i]; ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "NPN proposing %s from client selection", proto); APR_ARRAY_PUSH(protos, const char*) = proto; } return OK; }
int h2_allows_h2_direct(conn_rec *c) { const h2_config *cfg = h2_config_get(c); int h2_direct = h2_config_geti(cfg, H2_CONF_DIRECT); if (h2_direct < 0) { if (h2_h2_is_tls(c)) { /* disabled by default on TLS */ h2_direct = 0; } else { /* enabled if "Protocols h2c" is configured */ h2_direct = ap_is_allowed_protocol(c, NULL, NULL, "h2c"); } } return !!h2_direct; }
/** * A h2_mplx needs to be thread-safe *and* if will be called by * the h2_session thread *and* the h2_worker threads. Therefore: * - calls are protected by a mutex lock, m->lock * - the pool needs its own allocator, since apr_allocator_t are * not re-entrant. The separate allocator works without a * separate lock since we already protect h2_mplx itself. * Since HTTP/2 connections can be expected to live longer than * 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, h2_workers *workers) { apr_status_t status = APR_SUCCESS; h2_config *conf = h2_config_get(c); apr_allocator_t *allocator = NULL; h2_mplx *m; AP_DEBUG_ASSERT(conf); status = apr_allocator_create(&allocator); if (status != APR_SUCCESS) { return NULL; } m = apr_pcalloc(parent, sizeof(h2_mplx)); if (m) { m->id = c->id; APR_RING_ELEM_INIT(m, link); apr_atomic_set32(&m->refs, 1); m->c = c; apr_pool_create_ex(&m->pool, parent, NULL, allocator); if (!m->pool) { return NULL; } apr_allocator_owner_set(allocator, m->pool); status = apr_thread_mutex_create(&m->lock, APR_THREAD_MUTEX_DEFAULT, m->pool); if (status != APR_SUCCESS) { h2_mplx_destroy(m); return NULL; } m->bucket_alloc = apr_bucket_alloc_create(m->pool); m->q = h2_tq_create(m->id, m->pool); m->stream_ios = h2_io_set_create(m->pool); m->ready_ios = h2_io_set_create(m->pool); m->closed = h2_stream_set_create(m->pool); m->stream_max_mem = h2_config_geti(conf, H2_CONF_STREAM_MAX_MEM); m->workers = workers; m->file_handles_allowed = h2_config_geti(conf, H2_CONF_SESSION_FILES); } return m; }
apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c) { h2_config *cfg = h2_config_get(c); io->connection = c; io->input = apr_brigade_create(c->pool, c->bucket_alloc); io->output = apr_brigade_create(c->pool, c->bucket_alloc); io->buflen = 0; io->is_tls = h2_h2_is_tls(c); io->buffer_output = io->is_tls; if (io->buffer_output) { io->bufsize = WRITE_BUFFER_SIZE; io->buffer = apr_pcalloc(c->pool, io->bufsize); } else { io->bufsize = 0; } if (io->is_tls) { /* That is where 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 = WRITE_SIZE_INITIAL; } else { io->warmup_size = 0; io->cooldown_usecs = 0; io->write_size = (int)io->bufsize; } if (APLOGctrace1(c)) { ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection, "h2_conn_io(%ld): init, buffering=%d, warmup_size=%ld, cd_secs=%f", io->connection->id, io->buffer_output, (long)io->warmup_size, ((float)io->cooldown_usecs/APR_USEC_PER_SEC)); } return APR_SUCCESS; }
static int h2_h2_alpn_propose(conn_rec *c, apr_array_header_t *client_protos, apr_array_header_t *protos) { check_sni_host(c); h2_config *cfg = h2_config_get(c); if (!h2_config_geti(cfg, H2_CONF_ENABLED)) { return DECLINED; } for (int i = 0; i < h2_protos_len; ++i) { const char *proto = h2_protos[i]; if (h2_util_array_index(client_protos, proto) >= 0) { ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "ALPN proposing %s", proto); APR_ARRAY_PUSH(protos, const char*) = proto; return OK; /* propose only one, the first match from our list */ } } return OK; }
static int h2_h2_alpn_negotiated(conn_rec *c, const char *proto_name, apr_size_t proto_name_len) { if (APLOGctrace1(c)) { ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, "ALPN negotiated is %s", apr_pstrndup(c->pool, proto_name, proto_name_len)); } h2_config *cfg = h2_config_get(c); if (!h2_config_geti(cfg, H2_CONF_ENABLED)) { return DECLINED; } if (!h2_ctx_is_session(c) ) { return DECLINED; } if (h2_ctx_is_negotiated(c)) { // called twice? maybe alpn+npn overlap... return DECLINED; } for (int i = 0; i < h2_protos_len; ++i) { const char *proto = h2_protos[i]; if (proto_name_len == strlen(proto) && strncmp(proto, proto_name, proto_name_len) == 0) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "protocol set va ALPN to %s", proto); h2_ctx_set_protocol(c, proto); break; } } return OK; }
apr_status_t h2_task_do(h2_task *task, h2_worker *worker) { apr_status_t status = APR_SUCCESS; h2_config *cfg = h2_config_get(task->mplx->c); h2_task_env env; AP_DEBUG_ASSERT(task); memset(&env, 0, sizeof(env)); env.id = task->id; env.stream_id = task->stream_id; env.mplx = task->mplx; task->mplx = NULL; env.input_eos = task->input_eos; env.serialize_headers = !!h2_config_geti(cfg, H2_CONF_SER_HEADERS); /* Create a subpool from the worker one to be used for all things * with life-time of this task_env execution. */ apr_pool_create(&env.pool, h2_worker_get_pool(worker)); /* Link the env to the worker which provides useful things such * as mutex, a socket etc. */ env.io = h2_worker_get_cond(worker); /* Clone fields, so that lifetimes become (more) independent. */ env.method = apr_pstrdup(env.pool, task->method); env.path = apr_pstrdup(env.pool, task->path); env.authority = apr_pstrdup(env.pool, task->authority); env.headers = apr_table_clone(env.pool, task->headers); /* Setup the pseudo connection to use our own pool and bucket_alloc */ if (task->c) { env.c = *task->c; task->c = NULL; status = h2_conn_setup(&env, worker); } else { status = h2_conn_init(&env, worker); } /* save in connection that this one is a pseudo connection, prevents * other hooks from messing with it. */ h2_ctx_create_for(&env.c, &env); if (status == APR_SUCCESS) { env.input = h2_task_input_create(&env, env.pool, env.c.bucket_alloc); env.output = h2_task_output_create(&env, env.pool, env.c.bucket_alloc); status = h2_conn_process(&env.c, h2_worker_get_socket(worker)); ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, &env.c, "h2_task(%s): processing done", env.id); } else { ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, &env.c, "h2_task(%s): error setting up h2_task_env", env.id); } if (env.input) { h2_task_input_destroy(env.input); env.input = NULL; } if (env.output) { h2_task_output_close(env.output); h2_task_output_destroy(env.output); env.output = NULL; } h2_task_set_finished(task); if (env.io) { apr_thread_cond_signal(env.io); } if (env.pool) { apr_pool_destroy(env.pool); env.pool = NULL; } if (env.c.id) { h2_conn_post(&env.c, worker); } h2_mplx_task_done(env.mplx, env.stream_id); return status; }
apr_status_t h2_session_start(h2_session *session, int *rv) { apr_status_t status = APR_SUCCESS; h2_config *config; nghttp2_settings_entry settings[3]; AP_DEBUG_ASSERT(session); /* Start the conversation by submitting our SETTINGS frame */ *rv = 0; config = h2_config_get(session->c); if (session->r) { const char *s, *cs; apr_size_t dlen; h2_stream * stream; /* better for vhost matching */ config = h2_config_rget(session->r); /* 'h2c' mode: we should have a 'HTTP2-Settings' header with * base64 encoded client settings. */ s = apr_table_get(session->r->headers_in, "HTTP2-Settings"); if (!s) { ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EINVAL, session->r, APLOGNO(02931) "HTTP2-Settings header missing in request"); return APR_EINVAL; } cs = NULL; dlen = h2_util_base64url_decode(&cs, s, session->pool); if (APLOGrdebug(session->r)) { char buffer[128]; h2_util_hex_dump(buffer, 128, (char*)cs, dlen); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, session->r, "upgrading h2c session with HTTP2-Settings: %s -> %s (%d)", s, buffer, (int)dlen); } *rv = nghttp2_session_upgrade(session->ngh2, (uint8_t*)cs, dlen, NULL); if (*rv != 0) { status = APR_EINVAL; ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, APLOGNO(02932) "nghttp2_session_upgrade: %s", nghttp2_strerror(*rv)); return status; } /* Now we need to auto-open stream 1 for the request we got. */ *rv = stream_open(session, 1); if (*rv != 0) { status = APR_EGENERAL; ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, APLOGNO(02933) "open stream 1: %s", nghttp2_strerror(*rv)); return status; } stream = h2_stream_set_get(session->streams, 1); if (stream == NULL) { status = APR_EGENERAL; ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, APLOGNO(02934) "lookup of stream 1"); return status; } status = h2_stream_rwrite(stream, session->r); if (status != APR_SUCCESS) { return status; } status = stream_end_headers(session, stream, 1); if (status != APR_SUCCESS) { return status; } } settings[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; settings[0].value = (uint32_t)session->max_stream_count; settings[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; settings[1].value = h2_config_geti(config, H2_CONF_WIN_SIZE); settings[2].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE; settings[2].value = 64*1024; *rv = nghttp2_submit_settings(session->ngh2, NGHTTP2_FLAG_NONE, settings, sizeof(settings)/sizeof(settings[0])); if (*rv != 0) { status = APR_EGENERAL; ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, APLOGNO(02935) "nghttp2_submit_settings: %s", nghttp2_strerror(*rv)); } return status; }
apr_status_t h2_task_do(h2_task *task, h2_worker *worker) { apr_status_t status = APR_SUCCESS; h2_config *cfg = h2_config_get(task->mplx->c); h2_task_env env; AP_DEBUG_ASSERT(task); memset(&env, 0, sizeof(env)); env.id = task->id; env.stream_id = task->stream_id; env.mplx = task->mplx; /* Not cloning these task fields: * If the stream is destroyed before the task is done, this might * be a problem. However that should never happen as stream destruction * explicitly checks if task processing has finished. */ env.method = task->method; env.path = task->path; env.authority = task->authority; env.headers = task->headers; env.input_eos = task->input_eos; task->io = env.io = h2_worker_get_cond(worker); env.conn = task->conn; task->conn = NULL; env.serialize_headers = !!h2_config_geti(cfg, H2_CONF_SER_HEADERS); status = h2_conn_prep(env.conn, task->mplx->c, worker); /* save in connection that this one is for us, prevents * other hooks from messing with it. */ h2_ctx_create_for(env.conn->c, &env); if (status == APR_SUCCESS) { apr_pool_t *pool = env.conn->pool; apr_bucket_alloc_t *bucket_alloc = env.conn->bucket_alloc; env.input = h2_task_input_create(&env, pool, bucket_alloc); env.output = h2_task_output_create(&env, pool, bucket_alloc); status = h2_conn_process(env.conn); } ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, env.conn->c, "h2_task(%s):processing done", task->id); if (env.input) { h2_task_input_destroy(env.input); env.input = NULL; } if (env.conn) { h2_conn_post(env.conn, worker); env.conn = NULL; } if (env.output) { h2_task_output_close(env.output); h2_task_output_destroy(env.output); env.output = NULL; } h2_task_set_finished(task); if (env.io) { apr_thread_cond_signal(env.io); } return status; }
apr_status_t h2_task_do(h2_task *task, h2_worker *worker) { apr_status_t status = APR_SUCCESS; h2_config *cfg = h2_config_get(task->mplx->c); AP_DEBUG_ASSERT(task); task->serialize_headers = h2_config_geti(cfg, H2_CONF_SER_HEADERS); /* Create a subpool from the worker one to be used for all things * with life-time of this task execution. */ apr_pool_create(&task->pool, h2_worker_get_pool(worker)); /* Link the task to the worker which provides useful things such * as mutex, a socket etc. */ task->io = h2_worker_get_cond(worker); status = h2_conn_setup(task, worker); /* save in connection that this one is a pseudo connection, prevents * other hooks from messing with it. */ h2_ctx_create_for(task->c, task); if (status == APR_SUCCESS) { task->input = h2_task_input_create(task, task->pool, task->c->bucket_alloc); task->output = h2_task_output_create(task, task->pool, task->c->bucket_alloc); status = h2_conn_process(task->c, h2_worker_get_socket(worker)); ap_log_cerror(APLOG_MARK, APLOG_TRACE1, status, task->c, "h2_task(%s): processing done", task->id); } else { ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, task->c, APLOGNO(02957) "h2_task(%s): error setting up h2_task", task->id); } if (task->input) { h2_task_input_destroy(task->input); task->input = NULL; } if (task->output) { h2_task_output_close(task->output); h2_task_output_destroy(task->output); task->output = NULL; } if (task->io) { apr_thread_cond_signal(task->io); } if (task->pool) { apr_pool_destroy(task->pool); task->pool = NULL; } if (task->c->id) { h2_conn_post(task->c, worker); } h2_mplx_task_done(task->mplx, task->stream_id); return status; }
apr_status_t h2_session_start(h2_session *session) { assert(session); /* Start the conversation by submitting our SETTINGS frame */ apr_status_t status = APR_SUCCESS; h2_config *config = h2_config_get(session->c); int rv = 0; if (session->r) { /* 'h2c' mode: we should have a 'HTTP2-Settings' header with * base64 encoded client settings. */ const char *s = apr_table_get(session->r->headers_in, "HTTP2-Settings"); if (!s) { ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EINVAL, session->r, "HTTP2-Settings header missing in request"); return APR_EINVAL; } int cslen = apr_base64_decode_len(s); char *cs = apr_pcalloc(session->r->pool, cslen); --cslen; /* apr also counts the terminating 0 */ apr_base64_decode(cs, s); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, session->r, "upgrading h2c session with nghttp2 from %s (%d)", s, cslen); rv = nghttp2_session_upgrade(session->ngh2, (uint8_t*)cs, cslen, NULL); if (rv != 0) { status = APR_EGENERAL; ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, "nghttp2_session_upgrade: %s", nghttp2_strerror(rv)); return status; } /* Now we need to auto-open stream 1 for the request we got. */ rv = stream_open(session, 1); if (rv != 0) { status = APR_EGENERAL; ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, "open stream 1: %s", nghttp2_strerror(rv)); return status; } h2_stream * stream = h2_stream_set_get(session->streams, 1); if (stream == NULL) { status = APR_EGENERAL; ap_log_rerror(APLOG_MARK, APLOG_ERR, status, session->r, "lookup of stream 1"); return status; } status = h2_stream_rwrite(stream, session->r); if (status != APR_SUCCESS) { return status; } status = stream_end_headers(session, stream, 1); if (status != APR_SUCCESS) { return status; } status = h2_stream_write_eos(stream); if (status != APR_SUCCESS) { return status; } } nghttp2_settings_entry settings[] = { { NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, h2_config_geti(config, H2_CONF_MAX_HL_SIZE) }, { NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, h2_config_geti(config, H2_CONF_WIN_SIZE) }, {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, h2_config_geti(config, H2_CONF_MAX_STREAMS) }, }; rv = nghttp2_submit_settings(session->ngh2, NGHTTP2_FLAG_NONE, settings, sizeof(settings)/sizeof(settings[0])); if (rv != 0) { status = APR_EGENERAL; ap_log_cerror(APLOG_MARK, APLOG_ERR, status, session->c, "nghttp2_submit_settings: %s", nghttp2_strerror(rv)); } return status; }