</usage>
</directivesynopsis>
+ <directivesynopsis>
+ <name>H2Push</name>
+ <description>H2 Server Push Switch</description>
+ <syntax>H2Push on|off</syntax>
+ <default>H2Push on</default>
+ <contextlist>
+ <context>server config</context>
+ <context>virtual host</context>
+ </contextlist>
+
+ <usage>
+ <p>
+ This directive toggles the usage of the HTTP/2 server push
+ protocol feature. This should be used inside a
+ <directive module="core" type="section">VirtualHost</directive>
+ section to enable direct HTTP/2 communication for that virtual host.
+ </p>
+ <p>
+ The HTTP/2 protocol allows the server to push other resources to
+ a client when it asked for a particular one. This is helpful
+ if those resources are connected in some way and the client can
+ be expected to ask for it anyway. The pushing then saves the
+ time it takes the client to ask for the resources itself. On the
+ other hand, pushing resources the client never needs or already
+ has is a waste of bandwidth.
+ </p>
+ <p>
+ Server pushes are detected by inspecting the <code>Link</code> headers of
+ responses (see https://tools.ietf.org/html/rfc5988 for the
+ specification). When a link thus specified has the <code>rel=preload</code>
+ attribute, it is treated as a resource to be pushed.
+ </p>
+ <p>
+ Link headers in responses are either set by the application or
+ can be configured via <module>mod_headers</module> as:
+ </p>
+ <example><title>mod_headers example</title>
+ <highlight language="config">
+ <Location /index.html>
+ Header add Link "</css/site.css>;rel=preload"
+ Header add Link "</images/logo.jpg>;rel=preload"
+ </Location>
+ </highlight>
+ </example>
+ <p>
+ As the example shows, there can be several link headers added
+ to a response, resulting in several pushes being triggered. There
+ are no checks in the module to avoid pushing the same resource
+ twice or more to one client. Use with care.
+ </p>
+ <p>
+ HTTP/2 server pushes are enabled by default. This directive
+ allows it to be switch off on all resources of this server/virtual
+ host.
+ </p>
+ <example><title>Example</title>
+ <highlight language="config">
+ H2Push off
+ </highlight>
+ </example>
+ <p>
+ Last but not least, pushes happen only when the client signals
+ its willingness to accept those. Most browsers do, some, like Safari 9,
+ do not.
+ </p>
+ </usage>
+ </directivesynopsis>
+
<directivesynopsis>
<name>H2Upgrade</name>
<description>H2 Upgrade Protocol Switch</description>
<p>
This directive sets maximum number of <em>extra</em> file handles
a HTTP/2 session is allowed to use. A file handle is counted as
- <em>extra</em> when it is transfered from a h2 worker thread to
+ <em>extra</em> when it is transferred from a h2 worker thread to
the main HTTP/2 connection handling. This commonly happens when
serving static files.
</p><p>
<p>
The name stems from the
<a href="https://wiki.mozilla.org/Security/Server_Side_TLS">Security/Server Side TLS</a>
- definitions at mozilla where "modern compatiblity" is defined. Mozilla Firefox and
- other browsers require modern compatiblity for HTTP/2 connections. As everything
+ definitions at mozilla where "modern compatibility" is defined. Mozilla Firefox and
+ other browsers require modern compatibility for HTTP/2 connections. As everything
in OpSec, this is a moving target and can be expected to evolve in the future.
</p>
<p>
<p>
In deployments where servers are reached locally or over reliable
connections only, the value might be decreased with 0 disabling
- any warmup phase alltogether.
+ any warmup phase altogether.
</p>
<p>
The following example sets the size to zero, effectively disabling
<p>
See <directive type="section">H2TLSWarmUpSize</directive> for a
description of TLS warmup. H2TLSCoolDownSecs reflects the fact
- that connections may detoriate over time (and TCP flow adjusts)
+ that connections may deteriorate over time (and TCP flow adjusts)
for idle connections as well. It is beneficial to overall performance
to fall back to the pre-warmup phase after a number of seconds that
no data has been sent.
</p>
<p>
The following example sets the seconds to zero, effectively disabling
- any cooldown. Warmed up TLS connections stay on maximum record
+ any cool down. Warmed up TLS connections stay on maximum record
size.
</p>
<example><title>Example</title>
-1, /* HTTP/1 Upgrade support */
1024*1024, /* TLS warmup size */
1, /* TLS cooldown secs */
+ 1, /* HTTP/2 server push enabled */
};
static int files_per_session = 0;
conf->h2_upgrade = DEF_VAL;
conf->tls_warmup_size = DEF_VAL;
conf->tls_cooldown_secs = DEF_VAL;
+ conf->h2_push = DEF_VAL;
return conf;
}
n->h2_upgrade = H2_CONFIG_GET(add, base, h2_upgrade);
n->tls_warmup_size = H2_CONFIG_GET(add, base, tls_warmup_size);
n->tls_cooldown_secs = H2_CONFIG_GET(add, base, tls_cooldown_secs);
+ n->h2_push = H2_CONFIG_GET(add, base, h2_push);
return n;
}
return H2_CONFIG_GET(conf, &defconf, tls_warmup_size);
case H2_CONF_TLS_COOLDOWN_SECS:
return H2_CONFIG_GET(conf, &defconf, tls_cooldown_secs);
+ case H2_CONF_PUSH:
+ return H2_CONFIG_GET(conf, &defconf, h2_push);
default:
return DEF_VAL;
}
return "value must be On or Off";
}
+static const char *h2_conf_set_push(cmd_parms *parms,
+ void *arg, const char *value)
+{
+ h2_config *cfg = h2_config_sget(parms->server);
+ if (!strcasecmp(value, "On")) {
+ cfg->h2_push = 1;
+ return NULL;
+ }
+ else if (!strcasecmp(value, "Off")) {
+ cfg->h2_push = 0;
+ return NULL;
+ }
+
+ (void)arg;
+ return "value must be On or Off";
+}
+
static const char *h2_conf_set_modern_tls_only(cmd_parms *parms,
void *arg, const char *value)
{
RSRC_CONF, "number of bytes on TLS connection before doing max writes"),
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"),
AP_END_CMD
};
H2_CONF_UPGRADE,
H2_CONF_TLS_WARMUP_SIZE,
H2_CONF_TLS_COOLDOWN_SECS,
+ H2_CONF_PUSH,
} h2_config_var_t;
/* Apache httpd module configuration for h2. */
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 */
} h2_config;
struct h2_task *task = h2_ctx_get_task(ctx);
if (task) {
/* h2_task connection for a stream, not for h2c */
- ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, r,
"adding h1_to_h2_resp output filter");
if (task->serialize_headers) {
ap_remove_output_filter_byhandle(r->output_filters, "H1_TO_H2_RESP");
h2_io *io = h2_io_set_get(m->stream_ios, stream_id);
if (io) {
if (f) {
- ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c,
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c,
"h2_mplx(%ld-%d): open response: %d, rst=%d",
m->id, stream_id, response->http_status,
response->rst_error);
#include <stdio.h>
#include <apr_strings.h>
+#include <apr_lib.h>
#include <httpd.h>
#include <http_core.h>
typedef struct {
- apr_array_header_t *pushes;
- apr_pool_t *pool;
const h2_request *req;
+ apr_pool_t *pool;
+ apr_array_header_t *pushes;
+ const char *s;
+ size_t slen;
+ size_t i;
+
+ const char *link;
+ apr_table_t *params;
+ char b[4096];
} link_ctx;
-static size_t skip_ws(const char *s, size_t i, size_t max)
+static int attr_char(char c)
+{
+ switch (c) {
+ case '!':
+ case '#':
+ case '$':
+ case '&':
+ case '+':
+ case '-':
+ case '.':
+ case '^':
+ case '_':
+ case '`':
+ case '|':
+ case '~':
+ return 1;
+ default:
+ return apr_isalnum(c);
+ }
+}
+
+static int ptoken_char(char c)
+{
+ switch (c) {
+ case '!':
+ case '#':
+ case '$':
+ case '&':
+ case '\'':
+ case '(':
+ case ')':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '/':
+ case ':':
+ case '<':
+ case '=':
+ case '>':
+ case '?':
+ case '@':
+ case '[':
+ case ']':
+ case '^':
+ case '_':
+ case '`':
+ case '{':
+ case '|':
+ case '}':
+ case '~':
+ return 1;
+ default:
+ return apr_isalnum(c);
+ }
+}
+
+static int skip_ws(link_ctx *ctx)
{
char c;
- while (i < max && (((c = s[i]) == ' ') || (c == '\t'))) {
- ++i;
+ while (ctx->i < ctx->slen
+ && (((c = ctx->s[ctx->i]) == ' ') || (c == '\t'))) {
+ ++ctx->i;
+ }
+ return (ctx->i < ctx->slen);
+}
+
+static int find_chr(link_ctx *ctx, char c, size_t *pidx)
+{
+ size_t j;
+ for (j = ctx->i; j < ctx->slen; ++j) {
+ if (ctx->s[j] == c) {
+ *pidx = j;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int read_chr(link_ctx *ctx, char c)
+{
+ if (ctx->i < ctx->slen && ctx->s[ctx->i] == c) {
+ ++ctx->i;
+ return 1;
+ }
+ return 0;
+}
+
+static char *mk_str(link_ctx *ctx, size_t end)
+{
+ if (ctx->i < end) {
+ return apr_pstrndup(ctx->pool, ctx->s + ctx->i, end - ctx->i);
+ }
+ return "";
+}
+
+static int read_qstring(link_ctx *ctx, char **ps)
+{
+ if (skip_ws(ctx) && read_chr(ctx, '\"')) {
+ size_t end;
+ if (find_chr(ctx, '\"', &end)) {
+ *ps = mk_str(ctx, end);
+ ctx->i = end + 1;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int read_ptoken(link_ctx *ctx, char **ps)
+{
+ if (skip_ws(ctx)) {
+ size_t i;
+ for (i = ctx->i; i < ctx->slen && ptoken_char(ctx->s[i]); ++i) {
+ /* nop */
+ }
+ if (i > ctx->i) {
+ *ps = mk_str(ctx, i);
+ ctx->i = i;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+static int read_link(link_ctx *ctx)
+{
+ if (skip_ws(ctx) && read_chr(ctx, '<')) {
+ size_t end;
+ if (find_chr(ctx, '>', &end)) {
+ ctx->link = mk_str(ctx, end);
+ ctx->i = end + 1;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int read_pname(link_ctx *ctx, char **pname)
+{
+ if (skip_ws(ctx)) {
+ size_t i;
+ for (i = ctx->i; i < ctx->slen && attr_char(ctx->s[i]); ++i) {
+ /* nop */
+ }
+ if (i > ctx->i) {
+ *pname = mk_str(ctx, i);
+ ctx->i = i;
+ return 1;
+ }
}
- return i;
+ return 0;
+}
+
+static int read_pvalue(link_ctx *ctx, char **pvalue)
+{
+ if (skip_ws(ctx) && read_chr(ctx, '=')) {
+ if (read_qstring(ctx, pvalue) || read_ptoken(ctx, pvalue)) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int read_param(link_ctx *ctx)
+{
+ if (skip_ws(ctx) && read_chr(ctx, ';')) {
+ char *name, *value = "";
+ if (read_pname(ctx, &name)) {
+ read_pvalue(ctx, &value); /* value is optional */
+ apr_table_setn(ctx->params, name, value);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int read_sep(link_ctx *ctx)
+{
+ if (skip_ws(ctx) && read_chr(ctx, ',')) {
+ return 1;
+ }
+ return 0;
+}
+
+static void init_params(link_ctx *ctx)
+{
+ if (!ctx->params) {
+ ctx->params = apr_table_make(ctx->pool, 5);
+ }
+ else {
+ apr_table_clear(ctx->params);
+ }
+}
+
+static int same_authority(const h2_request *req, const apr_uri_t *uri)
+{
+ if (uri->scheme != NULL && strcmp(uri->scheme, req->scheme)) {
+ return 0;
+ }
+ if (uri->hostinfo != NULL && strcmp(uri->hostinfo, req->authority)) {
+ return 0;
+ }
+ return 1;
+}
+
+static int set_header(void *ctx, const char *key, const char *value)
+{
+ apr_table_setn(ctx, key, value);
+ return 1;
+}
+
+
+static int add_push(link_ctx *ctx)
+{
+ /* so, we have read a Link header and need to decide
+ * if we transform it into a push.
+ */
+ const char *rel = apr_table_get(ctx->params, "rel");
+ if (rel && !strcmp("preload", rel)) {
+ apr_uri_t uri;
+ if (apr_uri_parse(ctx->pool, ctx->link, &uri) == APR_SUCCESS) {
+ if (uri.path && same_authority(ctx->req, &uri)) {
+ char *path;
+ apr_table_t *headers;
+ h2_request *req;
+ h2_push *push;
+
+ /* We only want to generate pushes for resources in the
+ * same authority than the original request.
+ * icing: i think that is wise, otherwise we really need to
+ * check that the vhost/server is available and uses the same
+ * TLS (if any) parameters.
+ */
+ path = apr_uri_unparse(ctx->pool, &uri, APR_URI_UNP_OMITSITEPART);
+
+ push = apr_pcalloc(ctx->pool, sizeof(*push));
+ push->initiating_id = ctx->req->id;
+
+ headers = apr_table_make(ctx->pool, 5);
+ apr_table_do(set_header, headers, ctx->req->headers,
+ "User-Agent",
+ "Cache-Control",
+ "Accept-Language",
+ NULL);
+ /* TODO: which headers do we add here?
+ */
+
+ req = h2_request_createn(0, ctx->pool,
+ ctx->req->method,
+ ctx->req->scheme,
+ ctx->req->authority,
+ path, headers);
+ h2_request_end_headers(req, ctx->pool, 1);
+ push->req = req;
+
+ if (!ctx->pushes) {
+ ctx->pushes = apr_array_make(ctx->pool, 5, sizeof(h2_push*));
+ }
+ APR_ARRAY_PUSH(ctx->pushes, h2_push*) = push;
+ }
+ }
+ }
+ return 0;
}
static void inspect_link(link_ctx *ctx, const char *s, size_t slen)
relation-type = reg-rel-type | ext-rel-type
reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" )
ext-rel-type = URI
+
+ and from <https://tools.ietf.org/html/rfc5987>
+ parmname = 1*attr-char
+ attr-char = ALPHA / DIGIT
+ / "!" / "#" / "$" / "&" / "+" / "-" / "."
+ / "^" / "_" / "`" / "|" / "~"
*/
- /* TODO */
- (void)skip_ws;
+
+ ctx->s = s;
+ ctx->slen = slen;
+ ctx->i = 0;
+
+ while (read_link(ctx)) {
+ init_params(ctx);
+ while (read_param(ctx)) {
+ /* nop */
+ }
+ add_push(ctx);
+ if (!read_sep(ctx)) {
+ break;
+ }
+ }
+}
+
+static int head_iter(void *ctx, const char *key, const char *value)
+{
+ if (!apr_strnatcasecmp("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,
const h2_response *res)
{
- link_ctx ctx;
-
- ctx.pushes = NULL;
- ctx.pool = p;
- ctx.req = req;
-
/* Collect push candidates from the request/response pair.
*
* One source for pushes are "rel=preload" link headers
* TODO: This may be extended in the future by hooks or callbacks
* where other modules can provide push information directly.
*/
- if (res->ngheader) {
- int i;
- for (i = 0; i < res->ngheader->nvlen; ++i) {
- nghttp2_nv *nv = &res->ngheader->nv[i];
- if (nv->namelen == 4
- && apr_strnatcasecmp("link", (const char *)nv->name)) {
- inspect_link(&ctx, (const char *)nv->value, nv->valuelen);
- }
- }
- }
+ if (res->header) {
+ link_ctx ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.req = req;
+ ctx.pool = p;
- return ctx.pushes;
+ apr_table_do(head_iter, &ctx, res->header, NULL);
+ return ctx.pushes;
+ }
+ return NULL;
}
typedef struct h2_push {
int initiating_id;
const struct h2_request *req;
- const struct h2_ngheader *promise;
const char *as;
} h2_push;
h2_request *h2_request_create(int id, apr_pool_t *pool)
+{
+ return h2_request_createn(id, pool, NULL, NULL, NULL, NULL, NULL);
+}
+
+h2_request *h2_request_createn(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->id = id;
- req->headers = apr_table_make(pool, 10);
- req->content_length = -1;
+ req->id = id;
+ req->method = method;
+ req->scheme = scheme;
+ req->authority = authority;
+ req->path = path;
+ req->headers = header? header : apr_table_make(pool, 10);
return req;
}
{
}
+static apr_status_t inspect_clen(h2_request *req, const char *s)
+{
+ char *end;
+ req->content_length = apr_strtoi64(s, &end, 10);
+ return (s == end)? APR_EINVAL : APR_SUCCESS;
+}
+
static apr_status_t add_h1_header(h2_request *req, apr_pool_t *pool,
const char *name, size_t nlen,
const char *value, size_t vlen)
{
char *hname, *hvalue;
- if (H2_HD_MATCH_LIT("transfer-encoding", name, nlen)) {
- if (!apr_strnatcasecmp("chunked", value)) {
- /* This should never arrive here in a HTTP/2 request */
- ap_log_perror(APLOG_MARK, APLOG_ERR, APR_BADARG, pool,
- APLOGNO(02945)
- "h2_request: 'transfer-encoding: chunked' received");
- return APR_BADARG;
- }
- }
- else if (H2_HD_MATCH_LIT("content-length", name, nlen)) {
- char *end;
- req->content_length = apr_strtoi64(value, &end, 10);
- if (value == end) {
- ap_log_perror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, pool,
- APLOGNO(02959)
- "h2_request(%d): content-length value not parsed: %s",
- req->id, value);
- return APR_EINVAL;
- }
- req->chunked = 0;
- }
- else if (H2_HD_MATCH_LIT("content-type", name, nlen)) {
- /* If we see a content-type and have no length (yet),
- * we need to chunk. */
- req->chunked = (req->content_length == -1);
- }
- else if ((req->seen_host && H2_HD_MATCH_LIT("host", name, nlen))
- || H2_HD_MATCH_LIT("expect", name, nlen)
- || H2_HD_MATCH_LIT("upgrade", name, nlen)
- || H2_HD_MATCH_LIT("connection", name, nlen)
- || H2_HD_MATCH_LIT("proxy-connection", name, nlen)
- || H2_HD_MATCH_LIT("keep-alive", name, nlen)
- || H2_HD_MATCH_LIT("http2-settings", name, nlen)) {
+ if (H2_HD_MATCH_LIT("expect", name, nlen)
+ || H2_HD_MATCH_LIT("upgrade", name, nlen)
+ || H2_HD_MATCH_LIT("connection", name, nlen)
+ || H2_HD_MATCH_LIT("proxy-connection", name, nlen)
+ || H2_HD_MATCH_LIT("transfer-encoding", name, nlen)
+ || H2_HD_MATCH_LIT("keep-alive", name, nlen)
+ || H2_HD_MATCH_LIT("http2-settings", name, nlen)) {
/* ignore these. */
return APR_SUCCESS;
}
if (existing) {
char *nval;
- /* Cookie headers come separately in HTTP/2, but need
+ /* Cookie header come separately in HTTP/2, but need
* to be merged by "; " (instead of default ", ")
*/
hvalue = apr_pstrndup(pool, value, vlen);
}
}
else if (H2_HD_MATCH_LIT("host", name, nlen)) {
- req->seen_host = 1;
+ if (apr_table_get(req->headers, "Host")) {
+ return APR_SUCCESS; /* ignore duplicate */
+ }
}
hname = apr_pstrndup(pool, name, nlen);
return 1;
}
-static apr_status_t add_h1_headers(h2_request *req, apr_pool_t *pool,
- apr_table_t *headers)
+static apr_status_t add_all_h1_header(h2_request *req, apr_pool_t *pool,
+ apr_table_t *header)
{
h1_ctx x;
x.req = req;
x.pool = pool;
- apr_table_do(set_h1_header, &x, headers, NULL);
+ apr_table_do(set_h1_header, &x, header, NULL);
return APR_SUCCESS;
}
r->parsed_uri.port_str);
}
- status = add_h1_headers(req, r->pool, r->headers_in);
+ status = add_all_h1_header(req, r->pool, r->headers_in);
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r,
"h2_request(%d): rwrite %s host=%s://%s%s",
apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos)
{
+ const char *s;
+
if (req->eoh) {
return APR_EINVAL;
}
-
- if (!req->seen_host) {
+
+ /* be safe, some header we do not accept on h2(c) */
+ apr_table_unset(req->headers, "expect");
+ apr_table_unset(req->headers, "upgrade");
+ apr_table_unset(req->headers, "connection");
+ apr_table_unset(req->headers, "proxy-connection");
+ apr_table_unset(req->headers, "transfer-encoding");
+ apr_table_unset(req->headers, "keep-alive");
+ apr_table_unset(req->headers, "http2-settings");
+
+ if (!apr_table_get(req->headers, "Host")) {
/* Need to add a "Host" header if not already there to
* make virtual hosts work correctly. */
if (!req->authority) {
apr_table_set(req->headers, "Host", req->authority);
}
- if (eos && req->chunked) {
- /* We had chunking figured out, but the EOS is already there.
- * unmark chunking and set a definitive content-length.
- */
+ s = apr_table_get(req->headers, "Content-Length");
+ if (s) {
+ if (inspect_clen(req, s) != APR_SUCCESS) {
+ ap_log_perror(APLOG_MARK, APLOG_WARNING, APR_EINVAL, pool,
+ APLOGNO(02959)
+ "h2_request(%d): content-length value not parsed: %s",
+ req->id, s);
+ return APR_EINVAL;
+ }
req->chunked = 0;
- apr_table_setn(req->headers, "Content-Length", "0");
}
- else if (req->chunked) {
- /* We have not seen a content-length. We therefore must
- * pass any request content in chunked form.
- */
- apr_table_mergen(req->headers, "Transfer-Encoding", "chunked");
+ else {
+ /* no content-length given */
+ req->content_length = -1;
+ s = apr_table_get(req->headers, "Content-Type");
+ if (eos && s) {
+ req->chunked = 0;
+ apr_table_setn(req->headers, "Content-Length", "0");
+ }
+ else if (s) {
+ /* We have not seen a content-length, but a content-type.
+ * must pass any request content in chunked form.
+ */
+ req->chunked = 1;
+ apr_table_mergen(req->headers, "Transfer-Encoding", "chunked");
+ }
}
-
+
req->eoh = 1;
return APR_SUCCESS;
{
/* keep the dst id */
dst->method = OPT_COPY(p, src->method);
- dst->scheme = OPT_COPY(p, src->method);
- dst->authority = OPT_COPY(p, src->method);
- dst->path = OPT_COPY(p, src->method);
+ dst->scheme = OPT_COPY(p, src->scheme);
+ dst->authority = OPT_COPY(p, src->authority);
+ dst->path = OPT_COPY(p, src->path);
dst->headers = apr_table_clone(p, src->headers);
dst->content_length = src->content_length;
dst->chunked = src->chunked;
dst->eoh = src->eoh;
- dst->seen_host = src->seen_host;
}
apr_off_t content_length;
int chunked;
int eoh;
-
- int seen_host;
};
h2_request *h2_request_create(int id, apr_pool_t *pool);
+h2_request *h2_request_createn(int id, apr_pool_t *pool,
+ const char *method, const char *scheme,
+ const char *authority, const char *path,
+ apr_table_t *headers);
+
void h2_request_destroy(h2_request *req);
apr_status_t h2_request_rwrite(h2_request *req, request_rec *r);
#include "h2_private.h"
#include "h2_h2.h"
#include "h2_util.h"
-#include "h2_push.h"
#include "h2_response.h"
-static void make_ngheader(apr_pool_t *pool, h2_response *to,
- apr_table_t *header);
-
-static int ignore_header(const char *name)
-{
- 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));
-}
h2_response *h2_response_create(int stream_id,
int rst_error,
while (*sep == ' ' || *sep == '\t') {
++sep;
}
- if (ignore_header(hline)) {
- /* never forward, ch. 8.1.2.2 */
- }
- else {
+
+ if (!h2_util_ignore_header(hline)) {
apr_table_merge(header, hline, sep);
if (*sep && H2_HD_MATCH_LIT_CS("content-length", hline)) {
char *end;
header = apr_table_make(pool, 0);
}
- response->rheader = header;
+ response->header = header;
return response;
}
response->stream_id = stream_id;
response->http_status = r->status;
response->content_length = -1;
- response->rheader = header;
+ response->header = header;
if (response->http_status == HTTP_FORBIDDEN) {
const char *cause = apr_table_get(r->notes, "ssl-renegotiate-forbidden");
to->stream_id = from->stream_id;
to->http_status = from->http_status;
to->content_length = from->content_length;
- if (from->rheader) {
- make_ngheader(pool, to, from->rheader);
+ if (from->header) {
+ to->header = apr_table_clone(pool, from->header);
}
return to;
}
-typedef struct {
- nghttp2_nv *nv;
- size_t nvlen;
- size_t nvstrlen;
- size_t offset;
- char *strbuf;
- apr_pool_t *pool;
- apr_array_header_t *pushes;
-} nvctx_t;
-
-static int count_header(void *ctx, const char *key, const char *value)
-{
- if (!ignore_header(key)) {
- nvctx_t *nvctx = (nvctx_t*)ctx;
- nvctx->nvlen++;
- nvctx->nvstrlen += strlen(key) + strlen(value) + 2;
- }
- return 1;
-}
-
-#define NV_ADD_LIT_CS(nv, k, v) addnv_lit_cs(nv, k, sizeof(k) - 1, v, strlen(v))
-#define NV_ADD_CS_CS(nv, k, v) addnv_cs_cs(nv, k, strlen(k), v, strlen(v))
-#define NV_BUF_ADD(nv, s, len) memcpy(nv->strbuf, s, len); \
- s = nv->strbuf; \
- nv->strbuf += len + 1
-
-static void addnv_cs_cs(nvctx_t *ctx, const char *key, size_t key_len,
- const char *value, size_t val_len)
-{
- nghttp2_nv *nv = &ctx->nv[ctx->offset];
-
- NV_BUF_ADD(ctx, key, key_len);
- NV_BUF_ADD(ctx, value, val_len);
-
- nv->name = (uint8_t*)key;
- nv->namelen = key_len;
- nv->value = (uint8_t*)value;
- nv->valuelen = val_len;
-
- ctx->offset++;
-}
-
-static void addnv_lit_cs(nvctx_t *ctx, const char *key, size_t key_len,
- const char *value, size_t val_len)
-{
- nghttp2_nv *nv = &ctx->nv[ctx->offset];
-
- NV_BUF_ADD(ctx, value, val_len);
-
- nv->name = (uint8_t*)key;
- nv->namelen = key_len;
- nv->value = (uint8_t*)value;
- nv->valuelen = val_len;
-
- ctx->offset++;
-}
-
-static h2_push *h2_parse_preload(apr_pool_t *pool, const char *str)
-{
- /* TODO: implement this */
- return NULL;
-}
-
-static int add_header(void *ctx, const char *key, const char *value)
-{
- if (!ignore_header(key)) {
- nvctx_t *nvctx = (nvctx_t*)ctx;
- NV_ADD_CS_CS(nvctx, key, value);
-
- if (apr_strnatcasecmp("link", key)
- && ap_strstr_c("preload", value)) {
- /* Detect link headers with rel=preload to use as possible
- * server push candidates. */
- h2_push *push;
- if ((push = h2_parse_preload(nvctx->pool, value))) {
- if (!nvctx->pushes) {
- nvctx->pushes = apr_array_make(nvctx->pool, 5,
- sizeof(h2_push*));
- }
- APR_ARRAY_PUSH(nvctx->pushes, h2_push*) = push;
- }
- }
- }
- return 1;
-}
-
-static void make_ngheader(apr_pool_t *pool, h2_response *to,
- apr_table_t *header)
-{
- size_t n;
- h2_ngheader *h;
- nvctx_t ctx;
- char *status;
-
- to->ngheader = NULL;
- to->pushes = NULL;
-
- status = apr_psprintf(pool, "%d", to->http_status);
- ctx.nv = NULL;
- ctx.nvlen = 1;
- ctx.nvstrlen = strlen(status) + 1;
- ctx.offset = 0;
- ctx.strbuf = NULL;
- ctx.pool = pool;
- ctx.pushes = NULL;
-
- apr_table_do(count_header, &ctx, header, NULL);
-
- n = (sizeof(h2_ngheader)
- + (ctx.nvlen * sizeof(nghttp2_nv)) + ctx.nvstrlen);
- h = apr_pcalloc(pool, n);
- if (h) {
- ctx.nv = (nghttp2_nv*)(h + 1);
- ctx.strbuf = (char*)&ctx.nv[ctx.nvlen];
-
- NV_ADD_LIT_CS(&ctx, ":status", status);
- apr_table_do(add_header, &ctx, header, NULL);
-
- h->nv = ctx.nv;
- h->nvlen = ctx.nvlen;
-
- to->ngheader = h;
- to->pushes = ctx.pushes;
- }
-}
-
-int h2_response_push_count(h2_response *response)
-{
- return response->pushes? response->pushes->nelts : 0;
-}
-
-h2_push *h2_response_get_push(h2_response *response, size_t i)
-{
- if (response->pushes && i < response->pushes->nelts) {
- return APR_ARRAY_IDX(response->pushes, i, h2_push*);
- }
- return NULL;
-}
-
struct h2_push;
-/* h2_response is just the data belonging the the head of a HTTP response,
- * suitable prepared to be fed to nghttp2 for response submit.
- */
-typedef struct h2_ngheader {
- nghttp2_nv *nv;
- apr_size_t nvlen;
-} h2_ngheader;
-
-struct h2_push;
-
typedef struct h2_response {
int stream_id;
int rst_error;
int http_status;
apr_off_t content_length;
- apr_table_t *rheader;
- h2_ngheader *ngheader;
- apr_array_header_t *pushes;
+ apr_table_t *header;
} h2_response;
h2_response *h2_response_create(int stream_id,
h2_response *h2_response_copy(apr_pool_t *pool, h2_response *from);
-/**
- * Get the number of push proposals included with the response.
- * @return number of push proposals in this response
- */
-int h2_response_push_count(h2_response *response);
-
-/**
- * Get the ith h2_push contained in this response.
- *
- * @param response the response
- * @param i the index of the push to get
- * @return the ith h2_push or NULL if out of bounds
- */
-struct h2_push *h2_response_get_push(h2_response *response, size_t i);
-
-
#endif /* defined(__mod_h2__h2_response__) */
#include "h2_h2.h"
#include "h2_mplx.h"
#include "h2_push.h"
+#include "h2_request.h"
#include "h2_response.h"
#include "h2_stream.h"
#include "h2_stream_set.h"
session->max_stream_received = stream->id;
}
- ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
- "h2_session: stream(%ld-%d): opened",
- session->id, stream_id);
-
return stream;
}
return 0;
}
-static apr_status_t stream_destroy(h2_session *session,
+static apr_status_t stream_release(h2_session *session,
h2_stream *stream,
uint32_t error_code)
{
}
stream = h2_session_get_stream(session, stream_id);
if (stream) {
- stream_destroy(session, stream, error_code);
+ stream_release(session, stream, error_code);
}
if (error_code) {
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
- "h2_stream(%ld-%d): close error %d",
+ "h2_stream(%ld-%d): closed, error=%d",
session->id, (int)stream_id, error_code);
}
if (stream->submitted) {
rv = NGHTTP2_PROTOCOL_ERROR;
}
- else if (stream->response && stream->response->ngheader) {
+ else if (stream->response && stream->response->header) {
nghttp2_data_provider provider;
h2_response *response = stream->response;
+ h2_ngheader *ngh;
memset(&provider, 0, sizeof(provider));
provider.source.fd = stream->id;
provider.read_callback = stream_data_cb;
- ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c,
- "h2_stream(%ld-%d): submitting response %d",
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
+ "h2_stream(%ld-%d): submit response %d",
session->id, stream->id, response->http_status);
+ ngh = h2_util_ngheader_make_res(stream->pool, response->http_status,
+ response->header);
rv = nghttp2_submit_response(session->ngh2, response->stream_id,
- response->ngheader->nv,
- response->ngheader->nvlen, &provider);
-
+ ngh->nv, ngh->nvlen, &provider);
+
+ /* If the submit worked,
+ * and this stream is not a pushed one itself,
+ * and HTTP/2 server push is enabled here,
+ * and the response is in the range 200-299 *),
+ * and the remote side has pushing enabled,
+ * -> find and perform any pushes on this stream
+ *
+ * *) the response code is relevant, as we do not want to
+ * make pushes on 401 or 403 codes, neiterh on 301/302
+ * 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 (!rv
- && !stream->promised_on
+ && !stream->initiated_on
+ && h2_config_geti(h2_config_get(session->c), H2_CONF_PUSH)
&& H2_HTTP_2XX(response->http_status)
&& h2_session_push_enabled(session)) {
else {
int err = H2_STREAM_RST(stream, H2_ERR_PROTOCOL_ERROR);
- ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, session->c,
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
"h2_stream(%ld-%d): RST_STREAM, err=%d",
session->id, stream->id, err);
return status;
}
-struct h2_stream *h2_session_push(h2_session *session, h2_push *push)
+struct h2_stream *h2_session_push(h2_session *session, h2_stream *is,
+ h2_push *push)
{
apr_status_t status;
h2_stream *stream;
+ h2_ngheader *ngh;
int nid;
+ ngh = h2_util_ngheader_make_req(is->pool, push->req);
nid = nghttp2_submit_push_promise(session->ngh2, 0, push->initiating_id,
- push->promise->nv, push->promise->nvlen,
- NULL);
+ ngh->nv, ngh->nvlen, NULL);
+
if (nid <= 0) {
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
"h2_stream(%ld-%d): submitting push promise fail: %s",
return NULL;
}
- ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, session->c,
- "h2_stream(%ld-%d): promised new stream %d",
- session->id, push->initiating_id, nid);
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
+ "h2_stream(%ld-%d): promised new stream %d for %s %s",
+ session->id, push->initiating_id, nid,
+ push->req->method, push->req->path);
stream = h2_session_open_stream(session, nid);
if (stream) {
- h2_stream_set_h2_request(stream, push->req);
+ h2_stream_set_h2_request(stream, is->id, push->req);
status = stream_end_headers(session, stream, 1);
if (status != APR_SUCCESS) {
- ap_log_cerror(APLOG_MARK, APLOG_WARNING, status, session->c,
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c,
"h2_stream(%ld-%d): scheduling push stream",
session->id, stream->id);
h2_stream_cleanup(stream);
}
}
else {
- ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, session->c,
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c,
"h2_stream(%ld-%d): failed to create stream obj %d",
session->id, push->initiating_id, nid);
}
* processing..
*
* @param session the session to work in
- * @param stream the stream on which the push depends
+ * @param is the stream initiating the push
* @param push the push to promise
* @return the new promised stream or NULL
*/
-struct h2_stream *h2_session_push(h2_session *session, struct h2_push *push);
+struct h2_stream *h2_session_push(h2_session *session,
+ struct h2_stream *is, struct h2_push *push);
#endif /* defined(__mod_h2__h2_session__) */
apr_status_t h2_stream_destroy(h2_stream *stream)
{
AP_DEBUG_ASSERT(stream);
-
- ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->session->c,
- "h2_stream(%ld-%d): destroy", stream->session->id, stream->id);
if (stream->request) {
h2_request_destroy(stream->request);
stream->request = NULL;
return status;
}
-void h2_stream_set_h2_request(h2_stream *stream, const h2_request *req)
+void h2_stream_set_h2_request(h2_stream *stream, int initiated_on,
+ const h2_request *req)
{
h2_request_copy(stream->pool, stream->request, req);
+ stream->initiated_on = initiated_on;
+ stream->request->eoh = 0;
}
apr_status_t h2_stream_add_header(h2_stream *stream,
}
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, stream->session->c,
- "h2_mplx(%ld-%d): start stream, task %s %s (%s)",
+ "h2_stream(%ld-%d): scheduled %s %s://%s%s",
stream->session->id, stream->id,
- stream->request->method, stream->request->path,
- stream->request->authority);
+ stream->request->method, stream->request->scheme,
+ stream->request->authority, stream->request->path);
return status;
}
pushes = h2_push_collect(stream->pool, stream->request, stream->response);
if (pushes && !apr_is_empty_array(pushes)) {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, stream->session->c,
+ "h2_stream(%ld-%d): found %d push candidates",
+ stream->session->id, stream->id, pushes->nelts);
for (i = 0; i < pushes->nelts; ++i) {
h2_push *push = APR_ARRAY_IDX(pushes, i, h2_push*);
- h2_stream *s = h2_session_push(stream->session, push);
+ h2_stream *s = h2_session_push(stream->session, stream, push);
if (!s) {
status = APR_ECONNRESET;
break;
struct h2_stream {
int id; /* http2 stream id */
- int promised_on; /* http2 stream this was a promise on, or 0 */
+ int initiated_on; /* http2 stream id this was initiated on or 0 */
h2_stream_state_t state; /* http/2 state of this stream */
struct h2_session *session; /* the session this stream belongs to */
* @param stream the stream to init the request for
* @param req the request for initializing, will be copied
*/
-void h2_stream_set_h2_request(h2_stream *stream, const struct h2_request *req);
+void h2_stream_set_h2_request(h2_stream *stream, int initiated_on,
+ const struct h2_request *req);
/*
* Add a HTTP/2 header (including pseudo headers) to the given stream.
/* We do not serialize and have eos already, no need to
* create a bucket brigade. */
}
-
- if (APLOGcdebug(task->c)) {
- char buffer[1024];
- apr_size_t len = sizeof(buffer)-1;
- if (input->bb) {
- apr_brigade_flatten(input->bb, buffer, &len);
- }
- else {
- len = 0;
- }
- buffer[len] = 0;
- ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, task->c,
- "h2_task_input(%s): request is: %s",
- task->id, buffer);
- }
}
return input;
}
apr_status_t status = APR_SUCCESS;
apr_off_t bblen = 0;
- ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, f->c,
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c,
"h2_task_input(%s): read, block=%d, mode=%d, readbytes=%ld",
input->task->id, block, mode, (long)readbytes);
+ if (mode == AP_MODE_INIT) {
+ return ap_get_brigade(f->c->input_filters, bb, mode, block, readbytes);
+ }
+
if (is_aborted(f)) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, f->c,
- "h2_task_input(%s): is aborted",
- input->task->id);
+ "h2_task_input(%s): is aborted", input->task->id);
return APR_ECONNABORTED;
}
- if (mode == AP_MODE_INIT) {
- return ap_get_brigade(f->c->input_filters, bb, mode, block, readbytes);
- }
-
if (input->bb) {
status = apr_brigade_length(input->bb, 1, &bblen);
if (status != APR_SUCCESS) {
#include <nghttp2/nghttp2.h>
#include "h2_private.h"
+#include "h2_request.h"
#include "h2_util.h"
size_t h2_util_hex_dump(char *buffer, size_t maxlen,
return NULL;
}
+/*******************************************************************************
+ * h2_util for bucket brigades
+ ******************************************************************************/
+
/* DEEP_COPY==0 crashes under load. I think the setaside is fine,
* however buckets moved to another thread will still be
* free'd against the old bucket_alloc. *And* if the old
return APR_SUCCESS;
}
+/*******************************************************************************
+ * 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)) {
+ (*((size_t*)ctx))++;
+ }
+ return 1;
+}
+
+#define NV_ADD_LIT_CS(nv, k, v) add_header(nv, k, sizeof(k) - 1, v, strlen(v))
+#define NV_ADD_CS_CS(nv, k, v) add_header(nv, k, strlen(k), v, strlen(v))
+
+static int add_header(h2_ngheader *ngh,
+ const char *key, size_t key_len,
+ const char *value, size_t val_len)
+{
+ nghttp2_nv *nv = &ngh->nv[ngh->nvlen++];
+
+ nv->name = (uint8_t*)key;
+ nv->namelen = key_len;
+ nv->value = (uint8_t*)value;
+ nv->valuelen = val_len;
+ return 1;
+}
+
+static int add_table_header(void *ctx, const char *key, const char *value)
+{
+ if (!h2_util_ignore_header(key)) {
+ add_header(ctx, key, strlen(key), value, strlen(value));
+ }
+ return 1;
+}
+
+
+h2_ngheader *h2_util_ngheader_make_res(apr_pool_t *p,
+ int http_status,
+ apr_table_t *header)
+{
+ h2_ngheader *ngh;
+ size_t n;
+
+ n = 1;
+ apr_table_do(count_header, &n, header, NULL);
+
+ ngh = apr_pcalloc(p, sizeof(h2_ngheader));
+ ngh->nv = apr_pcalloc(p, n * sizeof(nghttp2_nv));
+ NV_ADD_LIT_CS(ngh, ":status", apr_psprintf(p, "%d", http_status));
+ apr_table_do(add_table_header, ngh, header, NULL);
+
+ return ngh;
+}
+
+h2_ngheader *h2_util_ngheader_make_req(apr_pool_t *p,
+ const struct h2_request *req)
+{
+
+ h2_ngheader *ngh;
+ size_t n;
+
+ AP_DEBUG_ASSERT(req);
+
+ n = 4;
+ apr_table_do(count_header, &n, req->headers, NULL);
+
+ ngh = apr_pcalloc(p, sizeof(h2_ngheader));
+ ngh->nv = apr_pcalloc(p, n * sizeof(nghttp2_nv));
+ NV_ADD_LIT_CS(ngh, ":scheme", req->scheme);
+ NV_ADD_LIT_CS(ngh, ":authority", req->authority);
+ NV_ADD_LIT_CS(ngh, ":path", req->path);
+ NV_ADD_LIT_CS(ngh, ":method", req->method);
+ apr_table_do(add_table_header, ngh, req->headers, NULL);
+
+ return ngh;
+}
+
#ifndef __mod_h2__h2_util__
#define __mod_h2__h2_util__
+struct h2_request;
struct nghttp2_frame;
size_t h2_util_hex_dump(char *buffer, size_t maxlen,
nv->value = (uint8_t *)VALUE; \
nv->valuelen = strlen(VALUE)
+int h2_util_ignore_header(const char *name);
+
+typedef struct h2_ngheader {
+ nghttp2_nv *nv;
+ apr_size_t nvlen;
+} h2_ngheader;
+
+h2_ngheader *h2_util_ngheader_make_res(apr_pool_t *p,
+ int http_status,
+ apr_table_t *header);
+h2_ngheader *h2_util_ngheader_make_req(apr_pool_t *p,
+ const struct h2_request *req);
+
/**
* Moves data from one brigade into another. If maxlen > 0, it only
* moves up to maxlen bytes into the target brigade, making bucket splits
* needed to give up with more than enough workers.
*/
if (task) {
- ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, workers->s,
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, workers->s,
"h2_worker(%d): start task(%s)",
h2_worker_get_id(worker), task->id);
/* Since we hand out a reference to the worker, we increase
{
apr_status_t status = apr_thread_mutex_lock(workers->lock);
if (status == APR_SUCCESS) {
- ap_log_error(APLOG_MARK, APLOG_DEBUG, status, workers->s,
+ ap_log_error(APLOG_MARK, APLOG_TRACE2, status, workers->s,
"h2_workers: register mplx(%ld)", m->id);
if (in_list(workers, m)) {
status = APR_EAGAIN;