# HG changeset patch # User Ruslan Ermilov # Date 1518072903 -10800 # Node ID 641306096f5b8c994a99b281a9c91b955659ed59 # Parent cadb43014c7cbc1ff630dba59bbca8f7cc27854b HTTP/2: server push. Resources to be pushed are configured with the "http2_push" directive. Also, preload links from the Link response headers, as described in https://www.w3.org/TR/preload/#server-push-http-2, can be pushed, if enabled with the "http2_push_preload" directive. Only relative URIs with absolute paths can be pushed. The number of concurrent pushes is normally limited by a client, but cannot exceed a hard limit set by the "http2_max_concurrent_pushes" directive. diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -35,12 +35,11 @@ #define NGX_HTTP_V2_GOAWAY_SIZE 8 #define NGX_HTTP_V2_WINDOW_UPDATE_SIZE 4 -#define NGX_HTTP_V2_STREAM_ID_SIZE 4 - #define NGX_HTTP_V2_SETTINGS_PARAM_SIZE 6 /* settings fields */ #define NGX_HTTP_V2_HEADER_TABLE_SIZE_SETTING 0x1 +#define NGX_HTTP_V2_ENABLE_PUSH_SETTING 0x2 #define NGX_HTTP_V2_MAX_STREAMS_SETTING 0x3 #define NGX_HTTP_V2_INIT_WINDOW_SIZE_SETTING 0x4 #define NGX_HTTP_V2_MAX_FRAME_SIZE_SETTING 0x5 @@ -121,7 +120,7 @@ static ngx_int_t ngx_http_v2_parse_int(n u_char **pos, u_char *end, ngx_uint_t prefix); static ngx_http_v2_stream_t *ngx_http_v2_create_stream( - ngx_http_v2_connection_t *h2c); + ngx_http_v2_connection_t *h2c, ngx_uint_t push); static ngx_http_v2_node_t *ngx_http_v2_get_node_by_id( ngx_http_v2_connection_t *h2c, ngx_uint_t sid, ngx_uint_t alloc); static ngx_http_v2_node_t *ngx_http_v2_get_closed_node( @@ -162,6 +161,7 @@ static ngx_int_t ngx_http_v2_cookie(ngx_ ngx_http_v2_header_t *header); static ngx_int_t ngx_http_v2_construct_cookie_header(ngx_http_request_t *r); static void ngx_http_v2_run_request(ngx_http_request_t *r); +static void ngx_http_v2_run_request_handler(ngx_event_t *ev); static ngx_int_t ngx_http_v2_process_request_body(ngx_http_request_t *r, u_char *pos, size_t size, ngx_uint_t last); static ngx_int_t ngx_http_v2_filter_request_body(ngx_http_request_t *r); @@ -249,6 +249,8 @@ ngx_http_v2_init(ngx_event_t *rev) h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module); + h2c->concurrent_pushes = h2scf->concurrent_pushes; + h2c->pool = ngx_create_pool(h2scf->pool_size, h2c->connection->log); if (h2c->pool == NULL) { ngx_http_close_connection(c); @@ -366,7 +368,9 @@ ngx_http_v2_read_handler(ngx_event_t *re break; } - if (n == 0 && (h2c->state.incomplete || h2c->processing)) { + if (n == 0 + && (h2c->state.incomplete || h2c->processing || h2c->pushing)) + { ngx_log_error(NGX_LOG_INFO, c->log, 0, "client prematurely closed connection"); } @@ -405,7 +409,7 @@ ngx_http_v2_read_handler(ngx_event_t *re h2c->blocked = 0; - if (h2c->processing) { + if (h2c->processing || h2c->pushing) { if (rev->timer_set) { ngx_del_timer(rev); } @@ -589,7 +593,7 @@ ngx_http_v2_handle_connection(ngx_http_v ngx_connection_t *c; ngx_http_v2_srv_conf_t *h2scf; - if (h2c->last_out || h2c->processing) { + if (h2c->last_out || h2c->processing || h2c->pushing) { return; } @@ -1123,7 +1127,7 @@ ngx_http_v2_state_headers(ngx_http_v2_co h2c->closed_nodes--; } - stream = ngx_http_v2_create_stream(h2c); + stream = ngx_http_v2_create_stream(h2c, 0); if (stream == NULL) { return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR); } @@ -1909,6 +1913,11 @@ ngx_http_v2_state_rst_stream(ngx_http_v2 "client canceled stream %ui", h2c->state.sid); break; + case NGX_HTTP_V2_REFUSED_STREAM: + ngx_log_error(NGX_LOG_INFO, fc->log, 0, + "client refused stream %ui", h2c->state.sid); + break; + case NGX_HTTP_V2_INTERNAL_ERROR: ngx_log_error(NGX_LOG_INFO, fc->log, 0, "client terminated stream %ui due to internal error", @@ -1966,6 +1975,7 @@ ngx_http_v2_state_settings_params(ngx_ht { ssize_t window_delta; ngx_uint_t id, value; + ngx_http_v2_srv_conf_t *h2scf; ngx_http_v2_out_frame_t *frame; window_delta = 0; @@ -2016,6 +2026,27 @@ ngx_http_v2_state_settings_params(ngx_ht h2c->frame_size = value; break; + case NGX_HTTP_V2_ENABLE_PUSH_SETTING: + + if (value > 1) { + ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0, + "client sent SETTINGS frame with incorrect " + "ENABLE_PUSH value %ui", value); + + return ngx_http_v2_connection_error(h2c, + NGX_HTTP_V2_PROTOCOL_ERROR); + } + + h2c->push_disabled = !value; + break; + + case NGX_HTTP_V2_MAX_STREAMS_SETTING: + h2scf = ngx_http_get_module_srv_conf(h2c->http_connection->conf_ctx, + ngx_http_v2_module); + + h2c->concurrent_pushes = ngx_min(value, h2scf->concurrent_pushes); + break; + default: break; } @@ -2483,6 +2514,119 @@ ngx_http_v2_parse_int(ngx_http_v2_connec } +ngx_int_t +ngx_http_v2_push_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t depend, + size_t request_length, ngx_str_t *path, ngx_str_t *authority) +{ + ngx_int_t rc; + ngx_str_t value; + ngx_connection_t *fc; + ngx_http_request_t *r; + ngx_http_v2_node_t *node; + ngx_http_v2_stream_t *stream; + + node = ngx_http_v2_get_node_by_id(h2c, h2c->last_push, 1); + + if (node == NULL) { + return NGX_ERROR; + } + + if (node->parent) { + ngx_queue_remove(&node->reuse); + h2c->closed_nodes--; + } + + stream = ngx_http_v2_create_stream(h2c, 1); + if (stream == NULL) { + return NGX_ERROR; + } + + stream->pool = ngx_create_pool(1024, h2c->connection->log); + if (stream->pool == NULL) { + return NGX_ERROR; + } + + r = stream->request; + fc = r->connection; + + r->request_length = request_length; + + stream->in_closed = 1; + stream->node = node; + + node->stream = stream; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0, + "http2 push stream sid:%ui " + "depends on %ui excl:0 weight:16", + h2c->last_push, depend); + + node->weight = NGX_HTTP_V2_DEFAULT_WEIGHT; + ngx_http_v2_set_dependency(h2c, node, depend, 0); + + r->method_name = ngx_http_core_get_method; + r->method = NGX_HTTP_GET; + + r->schema_start = (u_char *) "https"; + +#if (NGX_HTTP_SSL) + if (fc->ssl) { + r->schema_end = r->schema_start + 5; + + } else +#endif + { + r->schema_end = r->schema_start + 4; + } + + value.len = authority->len; + + value.data = ngx_pstrdup(stream->pool, authority); + if (value.data == NULL) { + return NGX_ERROR; + } + + rc = ngx_http_v2_parse_authority(r, &value); + + if (rc != NGX_OK) { + goto error; + } + + value.len = path->len; + + value.data = ngx_pstrdup(stream->pool, path); + if (value.data == NULL) { + return NGX_ERROR; + } + + rc = ngx_http_v2_parse_path(r, &value); + + if (rc != NGX_OK) { + goto error; + } + + fc->write->handler = ngx_http_v2_run_request_handler; + ngx_post_event(fc->write, &ngx_posted_events); + + return NGX_OK; + +error: + + if (rc == NGX_ABORT) { + return NGX_ERROR; + } + + if (rc == NGX_DECLINED) { + ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); + return NGX_ERROR; + } + + (void) ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR); + + return NGX_ERROR; +} + + static ngx_int_t ngx_http_v2_send_settings(ngx_http_v2_connection_t *h2c) { @@ -2743,7 +2887,7 @@ ngx_http_v2_frame_handler(ngx_http_v2_co static ngx_http_v2_stream_t * -ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c) +ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t push) { ngx_log_t *log; ngx_event_t *rev, *wev; @@ -2798,7 +2942,13 @@ ngx_http_v2_create_stream(ngx_http_v2_co ngx_memcpy(log, h2c->connection->log, sizeof(ngx_log_t)); log->data = ctx; - log->action = "reading client request headers"; + + if (push) { + log->action = "processing pushed request headers"; + + } else { + log->action = "reading client request headers"; + } ngx_memzero(rev, sizeof(ngx_event_t)); @@ -2870,7 +3020,12 @@ ngx_http_v2_create_stream(ngx_http_v2_co stream->send_window = h2c->init_window; stream->recv_window = h2scf->preread_size; - h2c->processing++; + if (push) { + h2c->pushing++; + + } else { + h2c->processing++; + } return stream; } @@ -3532,6 +3687,22 @@ ngx_http_v2_run_request(ngx_http_request } +static void +ngx_http_v2_run_request_handler(ngx_event_t *ev) +{ + ngx_connection_t *fc; + ngx_http_request_t *r; + + fc = ev->data; + r = fc->data; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 run request handler"); + + ngx_http_v2_run_request(r); +} + + ngx_int_t ngx_http_v2_read_request_body(ngx_http_request_t *r) { @@ -4003,6 +4174,7 @@ void ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc) { ngx_pool_t *pool; + ngx_uint_t push; ngx_event_t *ev; ngx_connection_t *fc; ngx_http_v2_node_t *node; @@ -4011,9 +4183,10 @@ ngx_http_v2_close_stream(ngx_http_v2_str h2c = stream->connection; node = stream->node; - ngx_log_debug3(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0, - "http2 close stream %ui, queued %ui, processing %ui", - node->id, stream->queued, h2c->processing); + ngx_log_debug4(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0, + "http2 close stream %ui, queued %ui, " + "processing %ui, pushing %ui", + node->id, stream->queued, h2c->processing, h2c->pushing); fc = stream->request->connection; @@ -4069,6 +4242,8 @@ ngx_http_v2_close_stream(ngx_http_v2_str h2c->state.stream = NULL; } + push = stream->node->id % 2 == 0; + node->stream = NULL; ngx_queue_insert_tail(&h2c->closed, &node->reuse); @@ -4116,9 +4291,14 @@ ngx_http_v2_close_stream(ngx_http_v2_str fc->data = h2c->free_fake_connections; h2c->free_fake_connections = fc; - h2c->processing--; - - if (h2c->processing || h2c->blocked) { + if (push) { + h2c->pushing--; + + } else { + h2c->processing--; + } + + if (h2c->processing || h2c->pushing || h2c->blocked) { return; } @@ -4267,7 +4447,7 @@ ngx_http_v2_finalize_connection(ngx_http c->error = 1; - if (!h2c->processing) { + if (!h2c->processing && !h2c->pushing) { ngx_http_close_connection(c); return; } @@ -4316,7 +4496,7 @@ ngx_http_v2_finalize_connection(ngx_http h2c->blocked = 0; - if (h2c->processing) { + if (h2c->processing || h2c->pushing) { return; } diff --git a/src/http/v2/ngx_http_v2.h b/src/http/v2/ngx_http_v2.h --- a/src/http/v2/ngx_http_v2.h +++ b/src/http/v2/ngx_http_v2.h @@ -24,6 +24,8 @@ #define NGX_HTTP_V2_MAX_FIELD \ (127 + (1 << (NGX_HTTP_V2_INT_OCTETS - 1) * 7) - 1) +#define NGX_HTTP_V2_STREAM_ID_SIZE 4 + #define NGX_HTTP_V2_FRAME_HEADER_SIZE 9 /* frame types */ @@ -118,6 +120,9 @@ struct ngx_http_v2_connection_s { ngx_uint_t processing; + ngx_uint_t pushing; + ngx_uint_t concurrent_pushes; + size_t send_window; size_t recv_window; size_t init_window; @@ -143,12 +148,14 @@ struct ngx_http_v2_connection_s { ngx_queue_t closed; ngx_uint_t last_sid; + ngx_uint_t last_push; unsigned closed_nodes:8; unsigned settings_ack:1; unsigned table_update:1; unsigned blocked:1; unsigned goaway:1; + unsigned push_disabled:1; }; @@ -276,6 +283,10 @@ void ngx_http_v2_init(ngx_event_t *rev); ngx_int_t ngx_http_v2_read_request_body(ngx_http_request_t *r); ngx_int_t ngx_http_v2_read_unbuffered_request_body(ngx_http_request_t *r); +ngx_int_t ngx_http_v2_push_stream(ngx_http_v2_connection_t *h2c, + ngx_uint_t depend, size_t request_length, ngx_str_t *path, + ngx_str_t *authority); + void ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc); ngx_int_t ngx_http_v2_send_output_queue(ngx_http_v2_connection_t *h2c); diff --git a/src/http/v2/ngx_http_v2_filter_module.c b/src/http/v2/ngx_http_v2_filter_module.c --- a/src/http/v2/ngx_http_v2_filter_module.c +++ b/src/http/v2/ngx_http_v2_filter_module.c @@ -2,6 +2,7 @@ /* * Copyright (C) Nginx, Inc. * Copyright (C) Valentin V. Bartenev + * Copyright (C) Ruslan Ermilov */ @@ -33,6 +34,13 @@ #define NGX_HTTP_V2_ENCODE_RAW 0 #define NGX_HTTP_V2_ENCODE_HUFF 0x80 +#define NGX_HTTP_V2_AUTHORITY_INDEX 1 +#define NGX_HTTP_V2_METHOD_GET_INDEX 2 +#define NGX_HTTP_V2_PATH_INDEX 4 + +#define NGX_HTTP_V2_SCHEME_HTTP_INDEX 6 +#define NGX_HTTP_V2_SCHEME_HTTPS_INDEX 7 + #define NGX_HTTP_V2_STATUS_INDEX 8 #define NGX_HTTP_V2_STATUS_200_INDEX 8 #define NGX_HTTP_V2_STATUS_204_INDEX 9 @@ -53,12 +61,18 @@ #define NGX_HTTP_V2_NO_TRAILERS (ngx_http_v2_out_frame_t *) -1 +static ngx_int_t ngx_http_v2_push_resources(ngx_http_request_t *r); +static ngx_int_t ngx_http_v2_push_resource(ngx_http_request_t *r, + ngx_str_t *path, ngx_str_t *authority); + static u_char *ngx_http_v2_string_encode(u_char *dst, u_char *src, size_t len, u_char *tmp, ngx_uint_t lower); static u_char *ngx_http_v2_write_int(u_char *pos, ngx_uint_t prefix, ngx_uint_t value); static ngx_http_v2_out_frame_t *ngx_http_v2_create_headers_frame( ngx_http_request_t *r, u_char *pos, u_char *end, ngx_uint_t fin); +static ngx_http_v2_out_frame_t *ngx_http_v2_create_push_frame( + ngx_http_request_t *r, u_char *pos, u_char *end); static ngx_http_v2_out_frame_t *ngx_http_v2_create_trailers_frame( ngx_http_request_t *r); @@ -81,6 +95,8 @@ static ngx_inline ngx_int_t ngx_http_v2_ static ngx_int_t ngx_http_v2_headers_frame_handler( ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame); +static ngx_int_t ngx_http_v2_push_frame_handler( + ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame); static ngx_int_t ngx_http_v2_data_frame_handler( ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame); static ngx_inline void ngx_http_v2_handle_frame( @@ -241,6 +257,15 @@ ngx_http_v2_header_filter(ngx_http_reque h2c = stream->connection; + if (!h2c->push_disabled && !h2c->goaway + && stream->node->id % 2 == 1 + && r->method != NGX_HTTP_HEAD) + { + if (ngx_http_v2_push_resources(r) != NGX_OK) { + return NGX_ERROR; + } + } + len = h2c->table_update ? 1 : 0; len += status ? 1 : 1 + ngx_http_v2_literal_size("418"); @@ -638,7 +663,7 @@ ngx_http_v2_header_filter(ngx_http_reque ngx_http_v2_queue_blocked_frame(h2c, frame); - stream->queued = 1; + stream->queued++; cln = ngx_http_cleanup_add(r, 0); if (cln == NULL) { @@ -655,6 +680,365 @@ ngx_http_v2_header_filter(ngx_http_reque } +static ngx_int_t +ngx_http_v2_push_resources(ngx_http_request_t *r) +{ + u_char *start, *end, *last; + ngx_int_t rc; + ngx_str_t path, authority; + ngx_uint_t i, push; + ngx_table_elt_t **h; + ngx_connection_t *fc; + ngx_http_v2_stream_t *stream; + ngx_http_v2_loc_conf_t *h2lcf; + ngx_http_v2_connection_t *h2c; + ngx_http_complex_value_t *pushes; + + fc = r->connection; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, "http2 push resources"); + + stream = r->stream; + h2c = stream->connection; + + ngx_str_null(&authority); + + h2lcf = ngx_http_get_module_loc_conf(r, ngx_http_v2_module); + + if (h2lcf->pushes) { + pushes = h2lcf->pushes->elts; + + for (i = 0; i < h2lcf->pushes->nelts; i++) { + + if (ngx_http_complex_value(r, &pushes[i], &path) != NGX_OK) { + return NGX_ERROR; + } + + if (path.len == 0) { + continue; + } + + if (path.len == 3 && ngx_strncmp(path.data, "off", 3) == 0) { + continue; + } + + rc = ngx_http_v2_push_resource(r, &path, &authority); + + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + if (rc == NGX_ABORT) { + return NGX_OK; + } + + /* NGX_OK, NGX_DECLINED */ + } + } + + if (!h2lcf->push_preload) { + return NGX_OK; + } + + h = r->headers_out.link.elts; + + for (i = 0; i < r->headers_out.link.nelts; i++) { + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 parse link: \"%V\"", &h[i]->value); + + start = h[i]->value.data; + end = h[i]->value.data + h[i]->value.len; + + next_link: + + while (start < end && *start == ' ') { start++; } + + if (start == end || *start++ != '<') { + continue; + } + + while (start < end && *start == ' ') { start++; } + + for (last = start; last < end && *last != '>'; last++) { + /* void */ + } + + if (last == start || last == end) { + continue; + } + + path.len = last - start; + path.data = start; + + start = last + 1; + + while (start < end && *start == ' ') { start++; } + + if (start == end) { + continue; + } + + if (*start == ',') { + start++; + goto next_link; + } + + if (*start++ != ';') { + continue; + } + + last = ngx_strlchr(start, end, ','); + + if (last == NULL) { + last = end; + } + + push = 0; + + for ( ;; ) { + + while (start < last && *start == ' ') { start++; } + + if (last - start >= 6 + && ngx_strncasecmp(start, (u_char *) "nopush", 6) == 0) + { + start += 6; + + if (start == last || *start == ' ' || *start == ';') { + push = 0; + break; + } + + goto next_param; + } + + if (last - start >= 11 + && ngx_strncasecmp(start, (u_char *) "rel=preload", 11) == 0) + { + start += 11; + + if (start == last || *start == ' ' || *start == ';') { + push = 1; + } + + goto next_param; + } + + if (last - start >= 4 + && ngx_strncasecmp(start, (u_char *) "rel=", 4) == 0) + { + start += 4; + + while (start < last && *start == ' ') { start++; } + + if (start == last || *start++ != '"') { + goto next_param; + } + + for ( ;; ) { + + while (start < last && *start == ' ') { start++; } + + if (last - start >= 7 + && ngx_strncasecmp(start, (u_char *) "preload", 7) == 0) + { + start += 7; + + if (start < last && (*start == ' ' || *start == '"')) { + push = 1; + break; + } + } + + while (start < last && *start != ' ' && *start != '"') { + start++; + } + + if (start == last) { + break; + } + + if (*start == '"') { + break; + } + + start++; + } + } + + next_param: + + start = ngx_strlchr(start, last, ';'); + + if (start == NULL) { + break; + } + + start++; + } + + if (push) { + while (path.len && path.data[path.len - 1] == ' ') { + path.len--; + } + } + + if (push && path.len + && !(path.len > 1 && path.data[0] == '/' && path.data[1] == '/')) + { + rc = ngx_http_v2_push_resource(r, &path, &authority); + + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + if (rc == NGX_ABORT) { + return NGX_OK; + } + + /* NGX_OK, NGX_DECLINED */ + } + + if (last < end) { + start = last + 1; + goto next_link; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_v2_push_resource(ngx_http_request_t *r, ngx_str_t *path, + ngx_str_t *authority) +{ + u_char *start, *pos, *tmp; + size_t len; + ngx_table_elt_t *host; + ngx_connection_t *fc; + ngx_http_v2_stream_t *stream; + ngx_http_v2_out_frame_t *frame; + ngx_http_v2_connection_t *h2c; + + fc = r->connection; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, "http2 push resource"); + + stream = r->stream; + h2c = stream->connection; + + if (!ngx_path_separator(path->data[0])) { + ngx_log_error(NGX_LOG_WARN, fc->log, 0, + "non-absolute path \"%V\" not pushed", path); + return NGX_DECLINED; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0, + "http2 pushing:%ui limit:%ui", + h2c->pushing, h2c->concurrent_pushes); + + if (h2c->pushing >= h2c->concurrent_pushes) { + return NGX_ABORT; + } + + if (h2c->last_push == 0x7ffffffe) { + return NGX_ABORT; + } + + if (path->len > NGX_HTTP_V2_MAX_FIELD) { + return NGX_DECLINED; + } + + host = r->headers_in.host; + + if (authority->len == 0 && host) { + + len = 1 + NGX_HTTP_V2_INT_OCTETS + host->value.len; + + tmp = ngx_palloc(r->pool, len); + pos = ngx_pnalloc(r->pool, len); + + if (pos == NULL || tmp == NULL) { + return NGX_ERROR; + } + + authority->data = pos; + + *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_AUTHORITY_INDEX); + pos = ngx_http_v2_write_value(pos, host->value.data, host->value.len, + tmp); + + authority->len = pos - authority->data; + } + + len = (h2c->table_update ? 1 : 0) + + 1 + + 1 + NGX_HTTP_V2_INT_OCTETS + path->len + + authority->len + + 1; + + tmp = ngx_palloc(r->pool, len); + pos = ngx_pnalloc(r->pool, len); + + if (pos == NULL || tmp == NULL) { + return NGX_ERROR; + } + + start = pos; + + if (h2c->table_update) { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 table size update: 0"); + *pos++ = (1 << 5) | 0; + h2c->table_update = 0; + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 push header: \":method: GET\""); + + *pos++ = ngx_http_v2_indexed(NGX_HTTP_V2_METHOD_GET_INDEX); + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 push header: \":path: %V\"", path); + + *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_PATH_INDEX); + pos = ngx_http_v2_write_value(pos, path->data, path->len, tmp); + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 push header: \":authority: %V\"", &host->value); + + pos = ngx_cpymem(pos, authority->data, authority->len); + +#if (NGX_HTTP_SSL) + if (fc->ssl) { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 push header: \":scheme: https\""); + *pos++ = ngx_http_v2_indexed(NGX_HTTP_V2_SCHEME_HTTPS_INDEX); + + } else +#endif + { + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, + "http2 push header: \":scheme: http\""); + *pos++ = ngx_http_v2_indexed(NGX_HTTP_V2_SCHEME_HTTP_INDEX); + } + + frame = ngx_http_v2_create_push_frame(r, start, pos); + if (frame == NULL) { + return NGX_ERROR; + } + + ngx_http_v2_queue_blocked_frame(h2c, frame); + + stream->queued++; + + return ngx_http_v2_push_stream(h2c, stream->node->id, pos - start, + path, &host->value); +} + + static u_char * ngx_http_v2_string_encode(u_char *dst, u_char *src, size_t len, u_char *tmp, ngx_uint_t lower) @@ -809,6 +1193,125 @@ ngx_http_v2_create_headers_frame(ngx_htt static ngx_http_v2_out_frame_t * +ngx_http_v2_create_push_frame(ngx_http_request_t *r, u_char *pos, u_char *end) +{ + u_char type, flags; + size_t rest, frame_size, len; + ngx_buf_t *b; + ngx_chain_t *cl, **ll; + ngx_http_v2_stream_t *stream; + ngx_http_v2_out_frame_t *frame; + ngx_http_v2_connection_t *h2c; + + stream = r->stream; + h2c = stream->connection; + rest = NGX_HTTP_V2_STREAM_ID_SIZE + (end - pos); + + frame = ngx_palloc(r->pool, sizeof(ngx_http_v2_out_frame_t)); + if (frame == NULL) { + return NULL; + } + + frame->handler = ngx_http_v2_push_frame_handler; + frame->stream = stream; + frame->length = rest; + frame->blocked = 1; + frame->fin = 0; + + ll = &frame->first; + + type = NGX_HTTP_V2_PUSH_PROMISE_FRAME; + flags = NGX_HTTP_V2_NO_FLAG; + frame_size = h2c->frame_size; + + for ( ;; ) { + if (rest <= frame_size) { + frame_size = rest; + flags |= NGX_HTTP_V2_END_HEADERS_FLAG; + } + + b = ngx_create_temp_buf(r->pool, + NGX_HTTP_V2_FRAME_HEADER_SIZE + + ((type == NGX_HTTP_V2_PUSH_PROMISE_FRAME) + ? NGX_HTTP_V2_STREAM_ID_SIZE : 0)); + if (b == NULL) { + return NULL; + } + + b->last = ngx_http_v2_write_len_and_type(b->last, frame_size, type); + *b->last++ = flags; + b->last = ngx_http_v2_write_sid(b->last, stream->node->id); + + b->tag = (ngx_buf_tag_t) &ngx_http_v2_module; + + if (type == NGX_HTTP_V2_PUSH_PROMISE_FRAME) { + h2c->last_push += 2; + + b->last = ngx_http_v2_write_sid(b->last, h2c->last_push); + len = frame_size - NGX_HTTP_V2_STREAM_ID_SIZE; + + } else { + len = frame_size; + } + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NULL; + } + + cl->buf = b; + + *ll = cl; + ll = &cl->next; + + b = ngx_calloc_buf(r->pool); + if (b == NULL) { + return NULL; + } + + b->pos = pos; + + pos += len; + + b->last = pos; + b->start = b->pos; + b->end = b->last; + b->temporary = 1; + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return NULL; + } + + cl->buf = b; + + *ll = cl; + ll = &cl->next; + + rest -= frame_size; + + if (rest) { + frame->length += NGX_HTTP_V2_FRAME_HEADER_SIZE; + + type = NGX_HTTP_V2_CONTINUATION_FRAME; + continue; + } + + cl->next = NULL; + frame->last = cl; + + ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http2:%ui create PUSH_PROMISE frame %p: " + "sid:%ui len:%uz", + stream->node->id, frame, h2c->last_push, + frame->length); + + return frame; + } +} + + +static ngx_http_v2_out_frame_t * ngx_http_v2_create_trailers_frame(ngx_http_request_t *r) { u_char *pos, *start, *tmp; @@ -1377,6 +1880,60 @@ ngx_http_v2_headers_frame_handler(ngx_ht static ngx_int_t +ngx_http_v2_push_frame_handler(ngx_http_v2_connection_t *h2c, + ngx_http_v2_out_frame_t *frame) +{ + ngx_chain_t *cl, *ln; + ngx_http_v2_stream_t *stream; + + stream = frame->stream; + cl = frame->first; + + for ( ;; ) { + if (cl->buf->pos != cl->buf->last) { + frame->first = cl; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0, + "http2:%ui PUSH_PROMISE frame %p was sent partially", + stream->node->id, frame); + + return NGX_AGAIN; + } + + ln = cl->next; + + if (cl->buf->tag == (ngx_buf_tag_t) &ngx_http_v2_module) { + cl->next = stream->free_frame_headers; + stream->free_frame_headers = cl; + + } else { + cl->next = stream->free_bufs; + stream->free_bufs = cl; + } + + if (cl == frame->last) { + break; + } + + cl = ln; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0, + "http2:%ui PUSH_PROMISE frame %p was sent", + stream->node->id, frame); + + stream->request->header_size += NGX_HTTP_V2_FRAME_HEADER_SIZE + + frame->length; + + ngx_http_v2_handle_frame(stream, frame); + + ngx_http_v2_handle_stream(h2c, stream); + + return NGX_OK; +} + + +static ngx_int_t ngx_http_v2_data_frame_handler(ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame) { diff --git a/src/http/v2/ngx_http_v2_module.c b/src/http/v2/ngx_http_v2_module.c --- a/src/http/v2/ngx_http_v2_module.c +++ b/src/http/v2/ngx_http_v2_module.c @@ -27,6 +27,8 @@ static void *ngx_http_v2_create_loc_conf static char *ngx_http_v2_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); +static char *ngx_http_v2_push(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); + static char *ngx_http_v2_recv_buffer_size(ngx_conf_t *cf, void *post, void *data); static char *ngx_http_v2_pool_size(ngx_conf_t *cf, void *post, void *data); @@ -73,6 +75,13 @@ static ngx_command_t ngx_http_v2_comman offsetof(ngx_http_v2_srv_conf_t, concurrent_streams), NULL }, + { ngx_string("http2_max_concurrent_pushes"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_v2_srv_conf_t, concurrent_pushes), + NULL }, + { ngx_string("http2_max_requests"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, ngx_conf_set_num_slot, @@ -129,6 +138,20 @@ static ngx_command_t ngx_http_v2_comman offsetof(ngx_http_v2_loc_conf_t, chunk_size), &ngx_http_v2_chunk_size_post }, + { ngx_string("http2_push_preload"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_v2_loc_conf_t, push_preload), + NULL }, + + { ngx_string("http2_push"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_http_v2_push, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + { ngx_string("spdy_recv_buffer_size"), NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, ngx_http_v2_spdy_deprecated, @@ -329,6 +352,7 @@ ngx_http_v2_create_srv_conf(ngx_conf_t * h2scf->pool_size = NGX_CONF_UNSET_SIZE; h2scf->concurrent_streams = NGX_CONF_UNSET_UINT; + h2scf->concurrent_pushes = NGX_CONF_UNSET_UINT; h2scf->max_requests = NGX_CONF_UNSET_UINT; h2scf->max_field_size = NGX_CONF_UNSET_SIZE; @@ -355,6 +379,8 @@ ngx_http_v2_merge_srv_conf(ngx_conf_t *c ngx_conf_merge_uint_value(conf->concurrent_streams, prev->concurrent_streams, 128); + ngx_conf_merge_uint_value(conf->concurrent_pushes, + prev->concurrent_pushes, 10); ngx_conf_merge_uint_value(conf->max_requests, prev->max_requests, 1000); ngx_conf_merge_size_value(conf->max_field_size, prev->max_field_size, @@ -386,8 +412,17 @@ ngx_http_v2_create_loc_conf(ngx_conf_t * return NULL; } + /* + * set by ngx_pcalloc(): + * + * h2lcf->pushes = NULL; + */ + h2lcf->chunk_size = NGX_CONF_UNSET_SIZE; + h2lcf->push_preload = NGX_CONF_UNSET; + h2lcf->push = NGX_CONF_UNSET; + return h2lcf; } @@ -400,6 +435,72 @@ ngx_http_v2_merge_loc_conf(ngx_conf_t *c ngx_conf_merge_size_value(conf->chunk_size, prev->chunk_size, 8 * 1024); + ngx_conf_merge_value(conf->push, prev->push, 1); + + if (conf->push && conf->pushes == NULL) { + conf->pushes = prev->pushes; + } + + ngx_conf_merge_value(conf->push_preload, prev->push_preload, 0); + + return NGX_CONF_OK; +} + + +static char * +ngx_http_v2_push(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_v2_loc_conf_t *h2lcf = conf; + + ngx_str_t *value; + ngx_http_complex_value_t *cv; + ngx_http_compile_complex_value_t ccv; + + value = cf->args->elts; + + if (ngx_strcmp(value[1].data, "off") == 0) { + + if (h2lcf->pushes) { + return "\"off\" parameter cannot be used with URI"; + } + + if (h2lcf->push == 0) { + return "is duplicate"; + } + + h2lcf->push = 0; + return NGX_CONF_OK; + } + + if (h2lcf->push == 0) { + return "URI cannot be used with \"off\" parameter"; + } + + h2lcf->push = 1; + + if (h2lcf->pushes == NULL) { + h2lcf->pushes = ngx_array_create(cf->pool, 1, + sizeof(ngx_http_complex_value_t)); + if (h2lcf->pushes == NULL) { + return NGX_CONF_ERROR; + } + } + + cv = ngx_array_push(h2lcf->pushes); + if (cv == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); + + ccv.cf = cf; + ccv.value = &value[1]; + ccv.complex_value = cv; + + if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { + return NGX_CONF_ERROR; + } + return NGX_CONF_OK; } diff --git a/src/http/v2/ngx_http_v2_module.h b/src/http/v2/ngx_http_v2_module.h --- a/src/http/v2/ngx_http_v2_module.h +++ b/src/http/v2/ngx_http_v2_module.h @@ -23,6 +23,7 @@ typedef struct { typedef struct { size_t pool_size; ngx_uint_t concurrent_streams; + ngx_uint_t concurrent_pushes; ngx_uint_t max_requests; size_t max_field_size; size_t max_header_size; @@ -35,6 +36,11 @@ typedef struct { typedef struct { size_t chunk_size; + + ngx_flag_t push_preload; + + ngx_flag_t push; + ngx_array_t *pushes; } ngx_http_v2_loc_conf_t;