</usage>
</directivesynopsis>
+ <directivesynopsis>
+ <name>H2PushDiarySize</name>
+ <description>H2 Server Push Diary Size</description>
+ <syntax>H2PushDiarySize n</syntax>
+ <default>H2PushDiarySize 128</default>
+ <contextlist>
+ <context>server config</context>
+ <context>virtual host</context>
+ </contextlist>
+ <compatibility>Available in version 2.4.19 and later.</compatibility>
+
+ <usage>
+ <p>
+ This directive toggles the maximum number of HTTP/2 server pushes
+ that are remembered per HTTP/2 connection. This can be used inside the
+ <directive module="core" type="section">VirtualHost</directive>
+ section to influence the number for all connections to that virtual host.
+ </p>
+ <p>
+ The push diary records a digest (currently using SHA256) of pushed
+ resources (their URL) to avoid duplicate pushes on the same connection.
+ These value are not persisted, so clients openeing a new connection
+ will experience known pushes again. There is ongoing work to enable
+ a client to disclose a digest of the resources it already has, so
+ the diary maybe initialized by the client on each connection setup.
+ </p>
+ <p>
+ If the maximum size is reached, newer entries replace the oldest
+ ones. Using SHA256, each diary entry uses 32 bytes, letting a
+ default diary with 128 entries consume around 4 KB of memory.
+ </p>
+ <p>
+ A size of 0 will effectively disable the push diary.
+ </p>
+ </usage>
+ </directivesynopsis>
+
<directivesynopsis>
<name>H2PushPriority</name>
<description>H2 Server Push Priority</description>
-1, /* connection timeout */
-1, /* keepalive timeout */
0, /* stream timeout */
+ 128, /* push diary size */
+
};
void h2_config_init(apr_pool_t *pool)
conf->h2_timeout = DEF_VAL;
conf->h2_keepalive = DEF_VAL;
conf->h2_stream_timeout = DEF_VAL;
+ conf->push_diary_size = DEF_VAL;
return conf;
}
n->h2_timeout = H2_CONFIG_GET(add, base, h2_timeout);
n->h2_keepalive = H2_CONFIG_GET(add, base, h2_keepalive);
n->h2_stream_timeout = H2_CONFIG_GET(add, base, h2_stream_timeout);
+ n->push_diary_size = H2_CONFIG_GET(add, base, push_diary_size);
return n;
}
return H2_CONFIG_GET(conf, &defconf, h2_keepalive);
case H2_CONF_STREAM_TIMEOUT_SECS:
return H2_CONFIG_GET(conf, &defconf, h2_stream_timeout);
+ case H2_CONF_PUSH_DIARY_SIZE:
+ return H2_CONFIG_GET(conf, &defconf, push_diary_size);
default:
return DEF_VAL;
}
return NULL;
}
+static const char *h2_conf_set_push_diary_size(cmd_parms *parms,
+ void *arg, 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) {
+ return "value must be >= 0";
+ }
+ return NULL;
+}
#define AP_END_CMD AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
RSRC_CONF, "timeout (seconds) for idle HTTP/2 connections, no streams open"),
AP_INIT_TAKE1("H2StreamTimeout", h2_conf_set_stream_timeout, NULL,
RSRC_CONF, "read/write timeout (seconds) for HTTP/2 streams"),
+ AP_INIT_TAKE1("H2PushDiarySize", h2_conf_set_push_diary_size, NULL,
+ RSRC_CONF, "size of push diary"),
AP_END_CMD
};
H2_CONF_TIMEOUT_SECS,
H2_CONF_KEEPALIVE_SECS,
H2_CONF_STREAM_TIMEOUT_SECS,
+ H2_CONF_PUSH_DIARY_SIZE,
} h2_config_var_t;
struct apr_hash_t;
int h2_timeout; /* timeout for http/2 connections */
int h2_keepalive; /* timeout for idle connections, no streams */
int h2_stream_timeout; /* timeout for http/2 streams, slave connections */
+ int push_diary_size; /* # of entries in push diary */
} h2_config;
#include <assert.h>
#include <stdio.h>
-#include <apr_strings.h>
#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_hash.h>
+#include <apr_time.h>
#include <httpd.h>
#include <http_core.h>
#include "h2_push.h"
#include "h2_request.h"
#include "h2_response.h"
+#include "h2_session.h"
+#include "h2_stream.h"
static const char *policy_str(h2_push_policy policy)
{
}
req->push_policy = policy;
}
+
+static unsigned int val_apr_hash(const char *str)
+{
+ apr_ssize_t len = strlen(str);
+ return apr_hashfunc_default(str, &len);
+}
+
+static void calc_apr_hash(h2_push_digest *d, h2_push *push)
+{
+ unsigned int val;
+
+ val = val_apr_hash(push->req->scheme);
+ val ^= val_apr_hash(push->req->authority);
+ val ^= val_apr_hash(push->req->path);
+
+ d->val.apr_hash = val;
+}
+
+static int cmp_apr_hash(h2_push_digest *d1, h2_push_digest *d2)
+{
+ return d1->val.apr_hash - d2->val.apr_hash;
+}
+
+h2_push_diary *h2_push_diary_create(apr_pool_t *p, apr_size_t max_entries)
+{
+ h2_push_diary *diary = NULL;
+
+ if (max_entries > 0) {
+ diary = apr_pcalloc(p, sizeof(*diary));
+
+ diary->entries = apr_array_make(p, 10, sizeof(h2_push_diary_entry*));
+ diary->max_entries = max_entries;
+ diary->dtype = H2_PUSH_DIGEST_APR_HASH;
+ diary->dcalc = calc_apr_hash;
+ diary->dcmp = cmp_apr_hash;
+ }
+
+ return diary;
+}
+
+static h2_push_diary_entry *h2_push_diary_find(h2_push_diary *diary, h2_push_digest *d)
+{
+ if (diary) {
+ h2_push_diary_entry *e;
+ int i;
+
+ for (i = 0; i < diary->entries->nelts; ++i) {
+ e = APR_ARRAY_IDX(diary->entries, i, h2_push_diary_entry*);
+ if (!diary->dcmp(&e->digest, d)) {
+ return e;
+ }
+ }
+ }
+ return NULL;
+}
+
+static h2_push_diary_entry *h2_push_diary_insert(h2_push_diary *diary, h2_push_digest *d)
+{
+ h2_push_diary_entry *e;
+ int i;
+
+ if (diary->entries->nelts < diary->max_entries) {
+ e = apr_pcalloc(diary->entries->pool, sizeof(*e));
+ APR_ARRAY_PUSH(diary->entries, h2_push_diary_entry*) = e;
+ }
+ else {
+ h2_push_diary_entry *oldest = NULL;
+ for (i = 0; i < diary->entries->nelts; ++i) {
+ e = APR_ARRAY_IDX(diary->entries, i, h2_push_diary_entry*);
+ if (!oldest || oldest->last_accessed > e->last_accessed) {
+ oldest = e;
+ }
+ }
+ e = oldest;
+ }
+
+ memcpy(&e->digest, d, sizeof(*d));
+ e->last_accessed = apr_time_now();
+ return e;
+}
+
+apr_array_header_t *h2_push_diary_update(h2_session *session, apr_array_header_t *pushes)
+{
+ apr_array_header_t *npushes = pushes;
+ h2_push_digest d;
+ h2_push_diary_entry *e;
+ int i;
+
+ if (session->push_diary && pushes) {
+ npushes = NULL;
+
+ for (i = 0; i < pushes->nelts; ++i) {
+ h2_push *push;
+
+ push = APR_ARRAY_IDX(pushes, i, h2_push*);
+ session->push_diary->dcalc(&d, push);
+ e = h2_push_diary_find(session->push_diary, &d);
+ if (e) {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c,
+ "push_diary_update: already there PUSH %s", push->req->path);
+ e->last_accessed = apr_time_now();
+ }
+ else {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, session->c,
+ "push_diary_update: adding PUSH %s", push->req->path);
+ if (!npushes) {
+ npushes = apr_array_make(pushes->pool, 5, sizeof(h2_push_diary_entry*));
+ }
+ APR_ARRAY_PUSH(npushes, h2_push*) = push;
+ h2_push_diary_insert(session->push_diary, &d);
+ }
+ }
+ }
+ return npushes;
+}
+
+apr_array_header_t *h2_push_collect_update(h2_stream *stream,
+ const struct h2_request *req,
+ const struct h2_response *res)
+{
+ apr_array_header_t *pushes = h2_push_collect(stream->pool, req, res);
+ return h2_push_diary_update(stream->session, pushes);
+}
+
struct h2_request;
struct h2_response;
struct h2_ngheader;
+struct h2_session;
+struct h2_stream;
typedef enum {
H2_PUSH_NONE,
const struct h2_request *req;
} h2_push;
+typedef enum {
+ H2_PUSH_DIGEST_APR_HASH,
+ H2_PUSH_DIGEST_SHA256
+} h2_push_digest_t;
+
+typedef struct h2_push_digest {
+ union {
+ unsigned int apr_hash;
+ unsigned char sha256[32];
+ } val;
+} h2_push_digest;
+
+typedef void h2_push_digest_calc(h2_push_digest *d, h2_push *push);
+typedef int h2_push_digest_cmp(h2_push_digest *d1, h2_push_digest *d2);
+
+typedef struct h2_push_diary_entry {
+ h2_push_digest digest;
+ apr_time_t last_accessed;
+} h2_push_diary_entry;
+
+typedef struct h2_push_diary {
+ apr_array_header_t *entries;
+ apr_size_t max_entries;
+ h2_push_digest_t dtype;
+ h2_push_digest_calc *dcalc;
+ h2_push_digest_cmp *dcmp;
+} h2_push_diary;
+
+/**
+ * Determine the list of h2_push'es to send to the client on behalf of
+ * the given request/response pair.
+ *
+ * @param p the pool to use
+ * @param req the requst from the client
+ * @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,
const struct h2_response *res);
+/**
+ * Set the push policy for the given request. Takes request headers into
+ * account, see draft https://tools.ietf.org/html/draft-ruellan-http-accept-push-policy-00
+ * for details.
+ *
+ * @param req the request to determine the policy for
+ * @param p the pool to use
+ * @param push_enabled if HTTP/2 server push is generally enabled for this request
+ */
void h2_push_policy_determine(struct h2_request *req, apr_pool_t *p, int push_enabled);
+/**
+ * Create a new push diary for the given maximum number of entries.
+ *
+ * @oaram p the pool to use
+ * @param max_entries the maximum number of entries the diary should hold
+ * @return the created diary, might be NULL of max_entries is 0
+ */
+h2_push_diary *h2_push_diary_create(apr_pool_t *p, apr_size_t max_entries);
+
+/**
+ * Filters the given pushes against the diary and returns only those pushes
+ * that were newly entered in the diary.
+ */
+apr_array_header_t *h2_push_diary_update(struct h2_session *session, apr_array_header_t *pushes);
+
+/**
+ * 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,
+ const struct h2_response *res);
+
#endif /* defined(__mod_h2__h2_push__) */
{
nghttp2_session_callbacks *callbacks = NULL;
nghttp2_option *options = NULL;
+ apr_size_t diary_size;
apr_pool_t *pool = NULL;
apr_status_t status = apr_pool_create(&pool, c->pool);
h2_session_destroy(session);
return NULL;
}
-
+
+ diary_size = h2_config_geti(session->config, H2_CONF_PUSH_DIARY_SIZE);
+ session->push_diary = h2_push_diary_create(session->pool, diary_size);
+
if (APLOGcdebug(c)) {
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c,
"session(%ld) created, timeout=%d, keepalive_timeout=%d, "
- "max_streams=%d, stream_mem=%d",
+ "max_streams=%d, stream_mem=%d, push_diary_size=%d",
session->id, session->timeout_secs, session->keepalive_secs,
- (int)session->max_stream_count, (int)session->max_stream_mem);
+ (int)session->max_stream_count, (int)session->max_stream_mem,
+ (int)diary_size);
}
}
return session;
++slen;
}
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, session->c,
+ "h2_session(%ld): start, INITIAL_WINDOW_SIZE=%ld, "
+ "MAX_CONCURRENT_STREAMS=%d",
+ session->id, (long)win_size, (int)session->max_stream_count);
*rv = nghttp2_submit_settings(session->ngh2, NGHTTP2_FLAG_NONE,
settings, slen);
if (*rv != 0) {
struct h2_mplx;
struct h2_priority;
struct h2_push;
+struct h2_push_diary;
struct h2_response;
struct h2_session;
struct h2_stream;
struct nghttp2_session *ngh2; /* the nghttp2 session (internal use) */
struct h2_workers *workers; /* for executing stream tasks */
+
+ struct h2_push_diary *push_diary; /* remember pushes, avoid duplicates */
} h2_session;
apr_array_header_t *pushes;
int i;
- pushes = h2_push_collect(stream->pool, stream->request, stream->response);
+ pushes = h2_push_collect_update(stream, 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",