diff src/http/v2/ngx_http_v2_filter_module.c @ 6246:257b51c37c5a

The HTTP/2 implementation (RFC 7240, 7241). The SPDY support is removed, as it's incompatible with the new module.
author Valentin Bartenev <vbart@nginx.com>
date Fri, 11 Sep 2015 20:13:06 +0300
parents
children cbb8c32f78b5
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/src/http/v2/ngx_http_v2_filter_module.c
@@ -0,0 +1,1290 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ * Copyright (C) Valentin V. Bartenev
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+#include <nginx.h>
+#include <ngx_http_v2_module.h>
+
+
+#define ngx_http_v2_integer_octets(v)  (((v) + 127) / 128)
+
+#define ngx_http_v2_literal_size(h)                                           \
+    (ngx_http_v2_integer_octets(sizeof(h) - 1) + sizeof(h) - 1)
+
+#define ngx_http_v2_indexed(i)      (128 + (i))
+#define ngx_http_v2_inc_indexed(i)  (64 + (i))
+
+
+#define NGX_HTTP_V2_STATUS_INDEX          8
+#define NGX_HTTP_V2_STATUS_200_INDEX      8
+#define NGX_HTTP_V2_STATUS_204_INDEX      9
+#define NGX_HTTP_V2_STATUS_206_INDEX      10
+#define NGX_HTTP_V2_STATUS_304_INDEX      11
+#define NGX_HTTP_V2_STATUS_400_INDEX      12
+#define NGX_HTTP_V2_STATUS_404_INDEX      13
+#define NGX_HTTP_V2_STATUS_500_INDEX      14
+
+#define NGX_HTTP_V2_CONTENT_LENGTH_INDEX  28
+#define NGX_HTTP_V2_CONTENT_TYPE_INDEX    31
+#define NGX_HTTP_V2_DATE_INDEX            33
+#define NGX_HTTP_V2_LAST_MODIFIED_INDEX   44
+#define NGX_HTTP_V2_LOCATION_INDEX        46
+#define NGX_HTTP_V2_SERVER_INDEX          54
+#define NGX_HTTP_V2_VARY_INDEX            59
+
+
+static u_char *ngx_http_v2_write_int(u_char *pos, ngx_uint_t prefix,
+    ngx_uint_t value);
+static void ngx_http_v2_write_headers_head(u_char *pos, size_t length,
+    ngx_uint_t sid, ngx_uint_t end_headers, ngx_uint_t end_stream);
+static void ngx_http_v2_write_continuation_head(u_char *pos, size_t length,
+    ngx_uint_t sid, ngx_uint_t end_headers);
+
+static ngx_chain_t *ngx_http_v2_send_chain(ngx_connection_t *fc,
+    ngx_chain_t *in, off_t limit);
+
+static ngx_chain_t *ngx_http_v2_filter_get_shadow(
+    ngx_http_v2_stream_t *stream, ngx_buf_t *buf, off_t offset, off_t size);
+static ngx_http_v2_out_frame_t *ngx_http_v2_filter_get_data_frame(
+    ngx_http_v2_stream_t *stream, size_t len, ngx_chain_t *first,
+    ngx_chain_t *last);
+
+static ngx_inline ngx_int_t ngx_http_v2_flow_control(
+    ngx_http_v2_connection_t *h2c, ngx_http_v2_stream_t *stream);
+static void ngx_http_v2_waiting_queue(ngx_http_v2_connection_t *h2c,
+    ngx_http_v2_stream_t *stream);
+
+static ngx_inline ngx_int_t ngx_http_v2_filter_send(
+    ngx_connection_t *fc, ngx_http_v2_stream_t *stream);
+
+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_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(
+    ngx_http_v2_stream_t *stream, ngx_http_v2_out_frame_t *frame);
+static ngx_inline void ngx_http_v2_handle_stream(
+    ngx_http_v2_connection_t *h2c, ngx_http_v2_stream_t *stream);
+
+static void ngx_http_v2_filter_cleanup(void *data);
+
+static ngx_int_t ngx_http_v2_filter_init(ngx_conf_t *cf);
+
+
+static ngx_http_module_t  ngx_http_v2_filter_module_ctx = {
+    NULL,                                  /* preconfiguration */
+    ngx_http_v2_filter_init,               /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    NULL,                                  /* create server configuration */
+    NULL,                                  /* merge server configuration */
+
+    NULL,                                  /* create location configuration */
+    NULL                                   /* merge location configuration */
+};
+
+
+ngx_module_t  ngx_http_v2_filter_module = {
+    NGX_MODULE_V1,
+    &ngx_http_v2_filter_module_ctx,        /* module context */
+    NULL,                                  /* module directives */
+    NGX_HTTP_MODULE,                       /* module type */
+    NULL,                                  /* init master */
+    NULL,                                  /* init module */
+    NULL,                                  /* init process */
+    NULL,                                  /* init thread */
+    NULL,                                  /* exit thread */
+    NULL,                                  /* exit process */
+    NULL,                                  /* exit master */
+    NGX_MODULE_V1_PADDING
+};
+
+
+static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
+
+
+static ngx_int_t
+ngx_http_v2_header_filter(ngx_http_request_t *r)
+{
+    u_char                     status, *p, *head;
+    size_t                     len, rest, frame_size;
+    ngx_buf_t                 *b;
+    ngx_str_t                  host, location;
+    ngx_uint_t                 i, port, continuation;
+    ngx_chain_t               *cl;
+    ngx_list_part_t           *part;
+    ngx_table_elt_t           *header;
+    ngx_connection_t          *fc;
+    ngx_http_cleanup_t        *cln;
+    ngx_http_v2_stream_t      *stream;
+    ngx_http_v2_out_frame_t   *frame;
+    ngx_http_core_loc_conf_t  *clcf;
+    ngx_http_core_srv_conf_t  *cscf;
+    struct sockaddr_in        *sin;
+#if (NGX_HAVE_INET6)
+    struct sockaddr_in6       *sin6;
+#endif
+    u_char                     addr[NGX_SOCKADDR_STRLEN];
+
+
+    if (!r->stream) {
+        return ngx_http_next_header_filter(r);
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http2 header filter");
+
+    if (r->header_sent) {
+        return NGX_OK;
+    }
+
+    r->header_sent = 1;
+
+    if (r != r->main) {
+        return NGX_OK;
+    }
+
+    if (r->method == NGX_HTTP_HEAD) {
+        r->header_only = 1;
+    }
+
+    switch (r->headers_out.status) {
+
+    case NGX_HTTP_OK:
+        status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_200_INDEX);
+        break;
+
+    case NGX_HTTP_NO_CONTENT:
+        r->header_only = 1;
+
+        ngx_str_null(&r->headers_out.content_type);
+
+        r->headers_out.content_length = NULL;
+        r->headers_out.content_length_n = -1;
+
+        r->headers_out.last_modified_time = -1;
+        r->headers_out.last_modified = NULL;
+
+        status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_204_INDEX);
+        break;
+
+    case NGX_HTTP_PARTIAL_CONTENT:
+        status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_206_INDEX);
+        break;
+
+    case NGX_HTTP_NOT_MODIFIED:
+        r->header_only = 1;
+        status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_304_INDEX);
+        break;
+
+    default:
+        r->headers_out.last_modified_time = -1;
+        r->headers_out.last_modified = NULL;
+
+        switch (r->headers_out.status) {
+
+        case NGX_HTTP_BAD_REQUEST:
+            status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_400_INDEX);
+            break;
+
+        case NGX_HTTP_NOT_FOUND:
+            status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_404_INDEX);
+            break;
+
+        case NGX_HTTP_INTERNAL_SERVER_ERROR:
+            status = ngx_http_v2_indexed(NGX_HTTP_V2_STATUS_500_INDEX);
+            break;
+
+        default:
+            status = 0;
+        }
+    }
+
+    len = status ? 1 : 1 + ngx_http_v2_literal_size("418");
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    if (r->headers_out.server == NULL) {
+        len += 1 + clcf->server_tokens ? ngx_http_v2_literal_size(NGINX_VER)
+                                       : ngx_http_v2_literal_size("nginx");
+    }
+
+    if (r->headers_out.date == NULL) {
+        len += 1 + ngx_http_v2_literal_size("Wed, 31 Dec 1986 18:00:00 GMT");
+    }
+
+    if (r->headers_out.content_type.len) {
+        len += NGX_HTTP_V2_INT_OCTETS + r->headers_out.content_type.len;
+
+        if (r->headers_out.content_type_len == r->headers_out.content_type.len
+            && r->headers_out.charset.len)
+        {
+            len += sizeof("; charset=") - 1 + r->headers_out.charset.len;
+        }
+    }
+
+    if (r->headers_out.content_length == NULL
+        && r->headers_out.content_length_n >= 0)
+    {
+        len += 1 + ngx_http_v2_integer_octets(NGX_OFF_T_LEN) + NGX_OFF_T_LEN;
+    }
+
+    if (r->headers_out.last_modified == NULL
+        && r->headers_out.last_modified_time != -1)
+    {
+        len += 1 + ngx_http_v2_literal_size("Wed, 31 Dec 1986 18:00:00 GMT");
+    }
+
+    fc = r->connection;
+
+    if (r->headers_out.location && r->headers_out.location->value.len) {
+
+        if (r->headers_out.location->value.data[0] == '/') {
+            if (clcf->server_name_in_redirect) {
+                cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+                host = cscf->server_name;
+
+            } else if (r->headers_in.server.len) {
+                host = r->headers_in.server;
+
+            } else {
+                host.len = NGX_SOCKADDR_STRLEN;
+                host.data = addr;
+
+                if (ngx_connection_local_sockaddr(fc, &host, 0) != NGX_OK) {
+                    return NGX_ERROR;
+                }
+            }
+
+            switch (fc->local_sockaddr->sa_family) {
+
+#if (NGX_HAVE_INET6)
+            case AF_INET6:
+                sin6 = (struct sockaddr_in6 *) fc->local_sockaddr;
+                port = ntohs(sin6->sin6_port);
+                break;
+#endif
+#if (NGX_HAVE_UNIX_DOMAIN)
+            case AF_UNIX:
+                port = 0;
+                break;
+#endif
+            default: /* AF_INET */
+                sin = (struct sockaddr_in *) fc->local_sockaddr;
+                port = ntohs(sin->sin_port);
+                break;
+            }
+
+            location.len = sizeof("https://") - 1 + host.len
+                           + r->headers_out.location->value.len;
+
+            if (clcf->port_in_redirect) {
+
+#if (NGX_HTTP_SSL)
+                if (fc->ssl)
+                    port = (port == 443) ? 0 : port;
+                else
+#endif
+                    port = (port == 80) ? 0 : port;
+
+            } else {
+                port = 0;
+            }
+
+            if (port) {
+                location.len += sizeof(":65535") - 1;
+            }
+
+            location.data = ngx_pnalloc(r->pool, location.len);
+            if (location.data == NULL) {
+                return NGX_ERROR;
+            }
+
+            p = ngx_cpymem(location.data, "http", sizeof("http") - 1);
+
+#if (NGX_HTTP_SSL)
+            if (fc->ssl) {
+                *p++ = 's';
+            }
+#endif
+
+            *p++ = ':'; *p++ = '/'; *p++ = '/';
+            p = ngx_cpymem(p, host.data, host.len);
+
+            if (port) {
+                p = ngx_sprintf(p, ":%ui", port);
+            }
+
+            p = ngx_cpymem(p, r->headers_out.location->value.data,
+                              r->headers_out.location->value.len);
+
+            /* update r->headers_out.location->value for possible logging */
+
+            r->headers_out.location->value.len = p - location.data;
+            r->headers_out.location->value.data = location.data;
+            ngx_str_set(&r->headers_out.location->key, "Location");
+        }
+
+        r->headers_out.location->hash = 0;
+
+        len += 1 + NGX_HTTP_V2_INT_OCTETS + r->headers_out.location->value.len;
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        if (clcf->gzip_vary) {
+            len += 1 + ngx_http_v2_literal_size("Accept-Encoding");
+
+        } else {
+            r->gzip_vary = 0;
+        }
+    }
+#endif
+
+    part = &r->headers_out.headers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        if (header[i].key.len > NGX_HTTP_V2_MAX_FIELD) {
+            ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0,
+                          "too long response header name: \"%V\"",
+                          &header[i].key);
+            return NGX_ERROR;
+        }
+
+        if (header[i].value.len > NGX_HTTP_V2_MAX_FIELD) {
+            ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0,
+                          "too long response header value: \"%V: %V\"",
+                          &header[i].key, &header[i].value);
+            return NGX_ERROR;
+        }
+
+        len += 1 + NGX_HTTP_V2_INT_OCTETS + header[i].key.len
+                 + NGX_HTTP_V2_INT_OCTETS + header[i].value.len;
+    }
+
+    stream = r->stream;
+    frame_size = stream->connection->frame_size;
+
+    len += NGX_HTTP_V2_FRAME_HEADER_SIZE
+           * ((len + frame_size - 1) / frame_size);
+
+    b = ngx_create_temp_buf(r->pool, len);
+    if (b == NULL) {
+        return NGX_ERROR;
+    }
+
+    b->last_buf = r->header_only;
+
+    b->last += NGX_HTTP_V2_FRAME_HEADER_SIZE;
+
+    if (status) {
+        *b->last++ = status;
+
+    } else {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_STATUS_INDEX);
+        *b->last++ = 3;
+        b->last = ngx_sprintf(b->last, "%03ui", r->headers_out.status);
+    }
+
+    if (r->headers_out.server == NULL) {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_SERVER_INDEX);
+
+        if (clcf->server_tokens) {
+            *b->last++ = sizeof(NGINX_VER) - 1;
+            b->last = ngx_cpymem(b->last, NGINX_VER, sizeof(NGINX_VER) - 1);
+
+        } else {
+            *b->last++ = sizeof("nginx") - 1;
+            b->last = ngx_cpymem(b->last, "nginx", sizeof("nginx") - 1);
+        }
+    }
+
+    if (r->headers_out.date == NULL) {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_DATE_INDEX);
+        *b->last++ = (u_char) ngx_cached_http_time.len;
+
+        b->last = ngx_cpymem(b->last, ngx_cached_http_time.data,
+                             ngx_cached_http_time.len);
+    }
+
+    if (r->headers_out.content_type.len) {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_CONTENT_TYPE_INDEX);
+
+        if (r->headers_out.content_type_len == r->headers_out.content_type.len
+            && r->headers_out.charset.len)
+        {
+            *b->last = 0;
+            b->last = ngx_http_v2_write_int(b->last, ngx_http_v2_prefix(7),
+                                            r->headers_out.content_type.len
+                                            + sizeof("; charset=") - 1
+                                            + r->headers_out.charset.len);
+
+            p = b->last;
+
+            b->last = ngx_cpymem(p, r->headers_out.content_type.data,
+                                 r->headers_out.content_type.len);
+
+            b->last = ngx_cpymem(b->last, "; charset=",
+                                 sizeof("; charset=") - 1);
+
+            b->last = ngx_cpymem(b->last, r->headers_out.charset.data,
+                                 r->headers_out.charset.len);
+
+            /* update r->headers_out.content_type for possible logging */
+
+            r->headers_out.content_type.len = b->last - p;
+            r->headers_out.content_type.data = p;
+
+        } else {
+            *b->last = 0;
+            b->last = ngx_http_v2_write_int(b->last, ngx_http_v2_prefix(7),
+                                            r->headers_out.content_type.len);
+            b->last = ngx_cpymem(b->last, r->headers_out.content_type.data,
+                                 r->headers_out.content_type.len);
+        }
+    }
+
+    if (r->headers_out.content_length == NULL
+        && r->headers_out.content_length_n >= 0)
+    {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_CONTENT_LENGTH_INDEX);
+
+        p = b->last;
+        b->last = ngx_sprintf(b->last + 1, "%O",
+                              r->headers_out.content_length_n);
+        *p = (u_char) (b->last - p - 1);
+    }
+
+    if (r->headers_out.last_modified == NULL
+        && r->headers_out.last_modified_time != -1)
+    {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_LAST_MODIFIED_INDEX);
+
+        p = b->last;
+        b->last = ngx_http_time(b->last + 1, r->headers_out.last_modified_time);
+        *p = (u_char) (b->last - p - 1);
+    }
+
+    if (r->headers_out.location && r->headers_out.location->value.len) {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_LOCATION_INDEX);
+
+        *b->last = 0;
+        b->last = ngx_http_v2_write_int(b->last, ngx_http_v2_prefix(7),
+                                        r->headers_out.location->value.len);
+        b->last = ngx_cpymem(b->last, r->headers_out.location->value.data,
+                                      r->headers_out.location->value.len);
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        *b->last++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_VARY_INDEX);
+        *b->last++ = sizeof("Accept-Encoding") - 1;
+        b->last = ngx_cpymem(b->last, "Accept-Encoding",
+                             sizeof("Accept-Encoding") - 1);
+    }
+#endif
+
+    continuation = 0;
+    head = b->pos;
+
+    len = b->last - head - NGX_HTTP_V2_FRAME_HEADER_SIZE;
+    rest = frame_size - len;
+
+    part = &r->headers_out.headers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        len = 1 + NGX_HTTP_V2_INT_OCTETS * 2
+              + header[i].key.len
+              + header[i].value.len;
+
+        if (len > rest) {
+            len = b->last - head - NGX_HTTP_V2_FRAME_HEADER_SIZE;
+
+            if (continuation) {
+                ngx_http_v2_write_continuation_head(head, len,
+                                                    stream->node->id, 0);
+            } else {
+                continuation = 1;
+                ngx_http_v2_write_headers_head(head, len, stream->node->id, 0,
+                                               r->header_only);
+            }
+
+            rest = frame_size;
+            head = b->last;
+
+            b->last += NGX_HTTP_V2_FRAME_HEADER_SIZE;
+        }
+
+        p = b->last;
+
+        *p++ = 0;
+
+        *p = 0;
+        p = ngx_http_v2_write_int(p, ngx_http_v2_prefix(7), header[i].key.len);
+        ngx_strlow(p, header[i].key.data, header[i].key.len);
+        p += header[i].key.len;
+
+        *p = 0;
+        p = ngx_http_v2_write_int(p, ngx_http_v2_prefix(7),
+                                  header[i].value.len);
+        p = ngx_cpymem(p, header[i].value.data, header[i].value.len);
+
+        rest -= p - b->last;
+        b->last = p;
+    }
+
+    len = b->last - head - NGX_HTTP_V2_FRAME_HEADER_SIZE;
+
+    if (continuation) {
+        ngx_http_v2_write_continuation_head(head, len, stream->node->id, 1);
+
+    } else {
+        ngx_http_v2_write_headers_head(head, len, stream->node->id, 1,
+                                       r->header_only);
+    }
+
+    cl = ngx_alloc_chain_link(r->pool);
+    if (cl == NULL) {
+        return NGX_ERROR;
+    }
+
+    cl->buf = b;
+    cl->next = NULL;
+
+    frame = ngx_palloc(r->pool, sizeof(ngx_http_v2_out_frame_t));
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->first = cl;
+    frame->last = cl;
+    frame->handler = ngx_http_v2_headers_frame_handler;
+    frame->stream = stream;
+    frame->length = b->last - b->pos - NGX_HTTP_V2_FRAME_HEADER_SIZE;
+    frame->blocked = 1;
+    frame->fin = r->header_only;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, stream->request->connection->log, 0,
+                   "http2:%ui create HEADERS frame %p: len:%uz",
+                   stream->node->id, frame, frame->length);
+
+    ngx_http_v2_queue_blocked_frame(stream->connection, frame);
+
+    cln = ngx_http_cleanup_add(r, 0);
+    if (cln == NULL) {
+        return NGX_ERROR;
+    }
+
+    cln->handler = ngx_http_v2_filter_cleanup;
+    cln->data = stream;
+
+    stream->queued = 1;
+
+    fc->send_chain = ngx_http_v2_send_chain;
+    fc->need_last_buf = 1;
+
+    return ngx_http_v2_filter_send(fc, stream);
+}
+
+
+static u_char *
+ngx_http_v2_write_int(u_char *pos, ngx_uint_t prefix, ngx_uint_t value)
+{
+    if (value < prefix) {
+        *pos++ |= value;
+        return pos;
+    }
+
+    *pos++ |= prefix;
+    value -= prefix;
+
+    while (value >= 128) {
+        *pos++ = value % 128 + 128;
+        value /= 128;
+    }
+
+    *pos++ = (u_char) value;
+
+    return pos;
+}
+
+
+static void
+ngx_http_v2_write_headers_head(u_char *pos, size_t length, ngx_uint_t sid,
+    ngx_uint_t end_headers, ngx_uint_t end_stream)
+{
+    u_char  flags;
+
+    pos = ngx_http_v2_write_len_and_type(pos, length,
+                                         NGX_HTTP_V2_HEADERS_FRAME);
+
+    flags = NGX_HTTP_V2_NO_FLAG;
+
+    if (end_headers) {
+        flags |= NGX_HTTP_V2_END_HEADERS_FLAG;
+    }
+
+    if (end_stream) {
+        flags |= NGX_HTTP_V2_END_STREAM_FLAG;
+    }
+
+    *pos++ = flags;
+
+    (void) ngx_http_v2_write_sid(pos, sid);
+}
+
+
+static void
+ngx_http_v2_write_continuation_head(u_char *pos, size_t length, ngx_uint_t sid,
+    ngx_uint_t end_headers)
+{
+    pos = ngx_http_v2_write_len_and_type(pos, length,
+                                         NGX_HTTP_V2_CONTINUATION_FRAME);
+
+    *pos++ = end_headers ? NGX_HTTP_V2_END_HEADERS_FLAG : NGX_HTTP_V2_NO_FLAG;
+
+    (void) ngx_http_v2_write_sid(pos, sid);
+}
+
+
+static ngx_chain_t *
+ngx_http_v2_send_chain(ngx_connection_t *fc, ngx_chain_t *in, off_t limit)
+{
+    off_t                      size, offset;
+    size_t                     rest, frame_size;
+    ngx_chain_t               *cl, *out, **ln;
+    ngx_http_request_t        *r;
+    ngx_http_v2_stream_t      *stream;
+    ngx_http_v2_loc_conf_t    *h2lcf;
+    ngx_http_v2_out_frame_t   *frame;
+    ngx_http_v2_connection_t  *h2c;
+
+    r = fc->data;
+    stream = r->stream;
+
+#if (NGX_SUPPRESS_WARN)
+    size = 0;
+#endif
+
+    while (in) {
+        size = ngx_buf_size(in->buf);
+
+        if (size || in->buf->last_buf) {
+            break;
+        }
+
+        in = in->next;
+    }
+
+    if (in == NULL) {
+
+        if (stream->queued) {
+            fc->write->delayed = 1;
+        } else {
+            fc->buffered &= ~NGX_HTTP_V2_BUFFERED;
+        }
+
+        return NULL;
+    }
+
+    h2c = stream->connection;
+
+    if (size && ngx_http_v2_flow_control(h2c, stream) == NGX_DECLINED) {
+        fc->write->delayed = 1;
+        return in;
+    }
+
+    if (in->buf->tag == (ngx_buf_tag_t) &ngx_http_v2_filter_get_shadow) {
+        cl = ngx_alloc_chain_link(r->pool);
+        if (cl == NULL) {
+            return NGX_CHAIN_ERROR;
+        }
+
+        cl->buf = in->buf;
+        in->buf = cl->buf->shadow;
+
+        offset = ngx_buf_in_memory(in->buf)
+                 ? (cl->buf->pos - in->buf->pos)
+                 : (cl->buf->file_pos - in->buf->file_pos);
+
+        cl->next = stream->free_bufs;
+        stream->free_bufs = cl;
+
+    } else {
+        offset = 0;
+    }
+
+    if (limit == 0 || limit > (off_t) h2c->send_window) {
+        limit = h2c->send_window;
+    }
+
+    if (limit > stream->send_window) {
+        limit = (stream->send_window > 0) ? stream->send_window : 0;
+    }
+
+    h2lcf = ngx_http_get_module_loc_conf(r, ngx_http_v2_module);
+
+    frame_size = (h2lcf->chunk_size < h2c->frame_size)
+                 ? h2lcf->chunk_size : h2c->frame_size;
+
+#if (NGX_SUPPRESS_WARN)
+    cl = NULL;
+#endif
+
+    for ( ;; ) {
+        if ((off_t) frame_size > limit) {
+            frame_size = (size_t) limit;
+        }
+
+        ln = &out;
+        rest = frame_size;
+
+        while ((off_t) rest >= size) {
+
+            if (offset) {
+                cl = ngx_http_v2_filter_get_shadow(stream, in->buf,
+                                                   offset, size);
+                if (cl == NULL) {
+                    return NGX_CHAIN_ERROR;
+                }
+
+                offset = 0;
+
+            } else {
+                cl = ngx_alloc_chain_link(r->pool);
+                if (cl == NULL) {
+                    return NGX_CHAIN_ERROR;
+                }
+
+                cl->buf = in->buf;
+            }
+
+            *ln = cl;
+            ln = &cl->next;
+
+            rest -= (size_t) size;
+            in = in->next;
+
+            if (in == NULL) {
+                frame_size -= rest;
+                rest = 0;
+                break;
+            }
+
+            size = ngx_buf_size(in->buf);
+        }
+
+        if (rest) {
+            cl = ngx_http_v2_filter_get_shadow(stream, in->buf, offset, rest);
+            if (cl == NULL) {
+                return NGX_CHAIN_ERROR;
+            }
+
+            cl->buf->flush = 0;
+            cl->buf->last_buf = 0;
+
+            *ln = cl;
+
+            offset += rest;
+            size -= rest;
+        }
+
+        frame = ngx_http_v2_filter_get_data_frame(stream, frame_size, out, cl);
+        if (frame == NULL) {
+            return NGX_CHAIN_ERROR;
+        }
+
+        ngx_http_v2_queue_frame(h2c, frame);
+
+        h2c->send_window -= frame_size;
+
+        stream->send_window -= frame_size;
+        stream->queued++;
+
+        if (in == NULL) {
+            break;
+        }
+
+        limit -= frame_size;
+
+        if (limit == 0) {
+            break;
+        }
+    }
+
+    if (offset) {
+        cl = ngx_http_v2_filter_get_shadow(stream, in->buf, offset, size);
+        if (cl == NULL) {
+            return NGX_CHAIN_ERROR;
+        }
+
+        in->buf = cl->buf;
+        ngx_free_chain(r->pool, cl);
+    }
+
+    if (ngx_http_v2_filter_send(fc, stream) == NGX_ERROR) {
+        return NGX_CHAIN_ERROR;
+    }
+
+    if (in && ngx_http_v2_flow_control(h2c, stream) == NGX_DECLINED) {
+        fc->write->delayed = 1;
+    }
+
+    return in;
+}
+
+
+static ngx_chain_t *
+ngx_http_v2_filter_get_shadow(ngx_http_v2_stream_t *stream, ngx_buf_t *buf,
+    off_t offset, off_t size)
+{
+    ngx_buf_t    *chunk;
+    ngx_chain_t  *cl;
+
+    cl = ngx_chain_get_free_buf(stream->request->pool, &stream->free_bufs);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    chunk = cl->buf;
+
+    ngx_memcpy(chunk, buf, sizeof(ngx_buf_t));
+
+    chunk->tag = (ngx_buf_tag_t) &ngx_http_v2_filter_get_shadow;
+    chunk->shadow = buf;
+
+    if (ngx_buf_in_memory(chunk)) {
+        chunk->pos += offset;
+        chunk->last = chunk->pos + size;
+    }
+
+    if (chunk->in_file) {
+        chunk->file_pos += offset;
+        chunk->file_last = chunk->file_pos + size;
+    }
+
+    return cl;
+}
+
+
+static ngx_http_v2_out_frame_t *
+ngx_http_v2_filter_get_data_frame(ngx_http_v2_stream_t *stream,
+    size_t len, ngx_chain_t *first, ngx_chain_t *last)
+{
+    u_char                      flags;
+    ngx_buf_t                  *buf;
+    ngx_chain_t                *cl;
+    ngx_http_v2_out_frame_t  *frame;
+
+
+    frame = stream->free_frames;
+
+    if (frame) {
+        stream->free_frames = frame->next;
+
+    } else {
+        frame = ngx_palloc(stream->request->pool,
+                           sizeof(ngx_http_v2_out_frame_t));
+        if (frame == NULL) {
+            return NULL;
+        }
+    }
+
+    flags = last->buf->last_buf ? NGX_HTTP_V2_END_STREAM_FLAG : 0;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_HTTP, stream->request->connection->log, 0,
+                   "http2:%ui create DATA frame %p: len:%uz flags:%ui",
+                   stream->node->id, frame, len, (ngx_uint_t) flags);
+
+    cl = ngx_chain_get_free_buf(stream->request->pool,
+                                &stream->free_data_headers);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    buf = cl->buf;
+
+    if (!buf->start) {
+        buf->start = ngx_palloc(stream->request->pool, NGX_HTTP_V2_FRAME_HEADER_SIZE);
+        if (buf->start == NULL) {
+            return NULL;
+        }
+
+        buf->end = buf->start + NGX_HTTP_V2_FRAME_HEADER_SIZE;
+        buf->last = buf->end;
+
+        buf->tag = (ngx_buf_tag_t) &ngx_http_v2_filter_get_data_frame;
+        buf->memory = 1;
+    }
+
+    buf->pos = buf->start;
+    buf->last = buf->pos;
+
+    buf->last = ngx_http_v2_write_len_and_type(buf->last, len,
+                                               NGX_HTTP_V2_DATA_FRAME);
+    *buf->last++ = flags;
+
+    buf->last = ngx_http_v2_write_sid(buf->last, stream->node->id);
+
+    cl->next = first;
+    first = cl;
+
+    last->buf->flush = 1;
+
+    frame->first = first;
+    frame->last = last;
+    frame->handler = ngx_http_v2_data_frame_handler;
+    frame->stream = stream;
+    frame->length = len;
+    frame->blocked = 0;
+    frame->fin = last->buf->last_buf;
+
+    return frame;
+}
+
+
+static ngx_inline ngx_int_t
+ngx_http_v2_filter_send(ngx_connection_t *fc, ngx_http_v2_stream_t *stream)
+{
+    stream->blocked = 1;
+
+    if (ngx_http_v2_send_output_queue(stream->connection) == NGX_ERROR) {
+        fc->error = 1;
+        return NGX_ERROR;
+    }
+
+    stream->blocked = 0;
+
+    if (stream->queued) {
+        fc->buffered |= NGX_HTTP_V2_BUFFERED;
+        fc->write->delayed = 1;
+        return NGX_AGAIN;
+    }
+
+    fc->buffered &= ~NGX_HTTP_V2_BUFFERED;
+
+    return NGX_OK;
+}
+
+
+static ngx_inline ngx_int_t
+ngx_http_v2_flow_control(ngx_http_v2_connection_t *h2c,
+    ngx_http_v2_stream_t *stream)
+{
+    if (stream->send_window <= 0) {
+        stream->exhausted = 1;
+        return NGX_DECLINED;
+    }
+
+    if (h2c->send_window == 0) {
+        ngx_http_v2_waiting_queue(h2c, stream);
+        return NGX_DECLINED;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_http_v2_waiting_queue(ngx_http_v2_connection_t *h2c,
+    ngx_http_v2_stream_t *stream)
+{
+    ngx_queue_t           *q;
+    ngx_http_v2_stream_t  *s;
+
+    if (stream->handled) {
+        return;
+    }
+
+    stream->handled = 1;
+
+    for (q = ngx_queue_last(&h2c->waiting);
+         q != ngx_queue_sentinel(&h2c->waiting);
+         q = ngx_queue_prev(q))
+    {
+        s = ngx_queue_data(q, ngx_http_v2_stream_t, queue);
+
+        if (s->node->rank < stream->node->rank
+            || (s->node->rank == stream->node->rank
+                && s->node->rel_weight >= stream->node->rel_weight))
+        {
+            break;
+        }
+    }
+
+    ngx_queue_insert_after(q, &stream->queue);
+}
+
+
+
+static ngx_int_t
+ngx_http_v2_headers_frame_handler(ngx_http_v2_connection_t *h2c,
+    ngx_http_v2_out_frame_t *frame)
+{
+    ngx_buf_t             *buf;
+    ngx_http_v2_stream_t  *stream;
+
+    buf = frame->first->buf;
+
+    if (buf->pos != buf->last) {
+        return NGX_AGAIN;
+    }
+
+    stream = frame->stream;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
+                   "http2:%ui HEADERS frame %p was sent",
+                   stream->node->id, frame);
+
+    ngx_free_chain(stream->request->pool, frame->first);
+
+    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)
+{
+    ngx_buf_t             *buf;
+    ngx_chain_t           *cl, *ln;
+    ngx_http_v2_stream_t  *stream;
+
+    stream = frame->stream;
+
+    cl = frame->first;
+
+    if (cl->buf->tag == (ngx_buf_tag_t) &ngx_http_v2_filter_get_data_frame) {
+
+        if (cl->buf->pos != cl->buf->last) {
+            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
+                           "http2:%ui DATA frame %p was sent partially",
+                           stream->node->id, frame);
+
+            return NGX_AGAIN;
+        }
+
+        ln = cl->next;
+
+        cl->next = stream->free_data_headers;
+        stream->free_data_headers = cl;
+
+        if (cl == frame->last) {
+            goto done;
+        }
+
+        cl = ln;
+    }
+
+    for ( ;; ) {
+        if (cl->buf->tag == (ngx_buf_tag_t) &ngx_http_v2_filter_get_shadow) {
+            buf = cl->buf->shadow;
+
+            if (ngx_buf_in_memory(buf)) {
+                buf->pos = cl->buf->pos;
+            }
+
+            if (buf->in_file) {
+                buf->file_pos = cl->buf->file_pos;
+            }
+        }
+
+        if (ngx_buf_size(cl->buf) != 0) {
+
+            if (cl != frame->first) {
+                frame->first = cl;
+                ngx_http_v2_handle_stream(h2c, stream);
+            }
+
+            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
+                           "http2:%ui DATA 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_filter_get_shadow) {
+            cl->next = stream->free_bufs;
+            stream->free_bufs = cl;
+
+        } else {
+            ngx_free_chain(stream->request->pool, cl);
+        }
+
+        if (cl == frame->last) {
+            goto done;
+        }
+
+        cl = ln;
+    }
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
+                   "http2:%ui DATA frame %p was sent",
+                   stream->node->id, frame);
+
+    stream->request->header_size += NGX_HTTP_V2_FRAME_HEADER_SIZE;
+
+    ngx_http_v2_handle_frame(stream, frame);
+
+    ngx_http_v2_handle_stream(h2c, stream);
+
+    return NGX_OK;
+}
+
+
+static ngx_inline void
+ngx_http_v2_handle_frame(ngx_http_v2_stream_t *stream,
+    ngx_http_v2_out_frame_t *frame)
+{
+    ngx_http_request_t  *r;
+
+    r = stream->request;
+
+    r->connection->sent += NGX_HTTP_V2_FRAME_HEADER_SIZE + frame->length;
+
+    if (frame->fin) {
+        stream->out_closed = 1;
+    }
+
+    frame->next = stream->free_frames;
+    stream->free_frames = frame;
+
+    stream->queued--;
+}
+
+
+static ngx_inline void
+ngx_http_v2_handle_stream(ngx_http_v2_connection_t *h2c,
+    ngx_http_v2_stream_t *stream)
+{
+    ngx_event_t  *wev;
+
+    if (stream->handled || stream->blocked || stream->exhausted) {
+        return;
+    }
+
+    wev = stream->request->connection->write;
+
+    /*
+     * This timer can only be set if the stream was delayed because of rate
+     * limit.  In that case the event should be triggered by the timer.
+     */
+
+    if (!wev->timer_set) {
+        wev->delayed = 0;
+
+        stream->handled = 1;
+        ngx_queue_insert_tail(&h2c->posted, &stream->queue);
+    }
+}
+
+
+static void
+ngx_http_v2_filter_cleanup(void *data)
+{
+    ngx_http_v2_stream_t *stream = data;
+
+    size_t                     window;
+    ngx_http_v2_out_frame_t   *frame, **fn;
+    ngx_http_v2_connection_t  *h2c;
+
+    if (stream->handled) {
+        stream->handled = 0;
+        ngx_queue_remove(&stream->queue);
+    }
+
+    if (stream->queued == 0) {
+        return;
+    }
+
+    window = 0;
+    h2c = stream->connection;
+    fn = &h2c->last_out;
+
+    for ( ;; ) {
+        frame = *fn;
+
+        if (frame == NULL) {
+            break;
+        }
+
+        if (frame->stream == stream && !frame->blocked) {
+            *fn = frame->next;
+
+            window += frame->length;
+
+            if (--stream->queued == 0) {
+                break;
+            }
+
+            continue;
+        }
+
+        fn = &frame->next;
+    }
+
+    if (h2c->send_window == 0 && window && !ngx_queue_empty(&h2c->waiting)) {
+        ngx_queue_add(&h2c->posted, &h2c->waiting);
+        ngx_queue_init(&h2c->waiting);
+    }
+
+    h2c->send_window += window;
+}
+
+
+static ngx_int_t
+ngx_http_v2_filter_init(ngx_conf_t *cf)
+{
+    ngx_http_next_header_filter = ngx_http_top_header_filter;
+    ngx_http_top_header_filter = ngx_http_v2_header_filter;
+
+    return NGX_OK;
+}