diff src/http/v3/ngx_http_v3_streams.c @ 8215:38c0898b6df7 quic

HTTP/3.
author Roman Arutyunyan <arut@nginx.com>
date Fri, 13 Mar 2020 19:36:33 +0300
parents
children 268f4389130d
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_streams.c
@@ -0,0 +1,1097 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+#define NGX_HTTP_V3_CONTROL_STREAM  0x00
+#define NGX_HTTP_V3_PUSH_STREAM     0x01
+#define NGX_HTTP_V3_ENCODER_STREAM  0x02
+#define NGX_HTTP_V3_DECODER_STREAM  0x03
+
+
+typedef struct {
+    uint32_t    signature; /* QSTR */
+    u_char      buf[4];
+
+    ngx_uint_t  len;
+    ngx_uint_t  type;
+    ngx_uint_t  state;
+    ngx_uint_t  index;
+    ngx_uint_t  offset;
+
+    ngx_str_t   name;
+    ngx_str_t   value;
+
+    unsigned    client:1;
+    unsigned    dynamic:1;
+    unsigned    huffman:1;
+} ngx_http_v3_uni_stream_t;
+
+
+static void ngx_http_v3_close_uni_stream(ngx_connection_t *c);
+static void ngx_http_v3_uni_stream_cleanup(void *data);
+static void ngx_http_v3_read_uni_stream_type(ngx_event_t *rev);
+static void ngx_http_v3_dummy_stream_handler(ngx_event_t *rev);
+static void ngx_http_v3_client_encoder_handler(ngx_event_t *rev);
+static void ngx_http_v3_client_decoder_handler(ngx_event_t *rev);
+
+static ngx_connection_t *ngx_http_v3_create_uni_stream(ngx_connection_t *c,
+    ngx_uint_t type);
+static ngx_connection_t *ngx_http_v3_get_server_encoder(ngx_connection_t *c);
+static ngx_connection_t *ngx_http_v3_get_server_decoder(ngx_connection_t *c);
+
+
+void
+ngx_http_v3_handle_client_uni_stream(ngx_connection_t *c)
+{
+    ngx_pool_cleanup_t        *cln;
+    ngx_http_v3_uni_stream_t  *us;
+
+    c->log->connection = c->number;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 new uni stream id:0x%uXL", c->qs->id);
+
+    us = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_uni_stream_t));
+    if (us == NULL) {
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    us->signature = NGX_HTTP_V3_STREAM;
+    us->client = 1;
+    us->type = (ngx_uint_t) -1;
+
+    c->data = us;
+
+    cln = ngx_pool_cleanup_add(c->pool, 0);
+    if (cln == NULL) {
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    cln->handler = ngx_http_v3_uni_stream_cleanup;
+    cln->data = c;
+
+    c->read->handler = ngx_http_v3_read_uni_stream_type;
+    c->read->handler(c->read);
+}
+
+
+static void
+ngx_http_v3_close_uni_stream(ngx_connection_t *c)
+{
+    ngx_pool_t  *pool;
+
+    c->destroyed = 1;
+
+    pool = c->pool;
+
+    ngx_close_connection(c);
+
+    ngx_destroy_pool(pool);
+}
+
+
+static void
+ngx_http_v3_uni_stream_cleanup(void *data)
+{
+    ngx_connection_t  *c = data;
+
+    ngx_http_v3_connection_t  *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    us = c->data;
+    h3c = c->qs->parent->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 close stream");
+
+    switch (us->type) {
+
+    case NGX_HTTP_V3_ENCODER_STREAM:
+
+        if (us->client) {
+            h3c->client_encoder = NULL;
+        } else {
+            h3c->server_encoder = NULL;
+        }
+
+        break;
+
+    case NGX_HTTP_V3_DECODER_STREAM:
+
+        if (us->client) {
+            h3c->client_decoder = NULL;
+        } else {
+            h3c->server_decoder = NULL;
+        }
+
+        break;
+    }
+}
+
+
+static void
+ngx_http_v3_read_uni_stream_type(ngx_event_t *rev)
+{
+    u_char                    *p;
+    ssize_t                    n, len;
+    ngx_connection_t          *c;
+    ngx_http_v3_connection_t  *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    c = rev->data;
+    us = c->data;
+    h3c = c->qs->parent->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 read stream type");
+
+    while (rev->ready) {
+
+        p = &us->buf[us->len];
+
+        if (us->len == 0) {
+            len = 1;
+        } else {
+            len = (us->buf[0] >> 6) + 1 - us->len;
+        }
+
+        n = c->recv(c, p, len);
+
+        if (n == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (n == NGX_AGAIN) {
+            break;
+        }
+
+        us->len += n;
+
+        if (n != len) {
+            break;
+        }
+
+        if ((us->buf[0] >> 6) + 1 == us->len) {
+            us->type = ngx_http_v3_decode_varlen_int(us->buf);
+
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 stream type:%ui", us->type);
+
+            switch (us->type) {
+
+            case NGX_HTTP_V3_ENCODER_STREAM:
+                if (h3c->client_encoder) {
+                    goto failed;
+                }
+
+                h3c->client_encoder = c;
+                rev->handler = ngx_http_v3_client_encoder_handler;
+                break;
+
+            case NGX_HTTP_V3_DECODER_STREAM:
+                if (h3c->client_decoder) {
+                    goto failed;
+                }
+
+                h3c->client_decoder = c;
+                rev->handler = ngx_http_v3_client_decoder_handler;
+                break;
+
+            case NGX_HTTP_V3_CONTROL_STREAM:
+            case NGX_HTTP_V3_PUSH_STREAM:
+
+                /* ignore these */
+
+            default:
+                rev->handler = ngx_http_v3_dummy_stream_handler;
+            }
+
+            rev->handler(rev);
+            return;
+        }
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(c);
+}
+
+
+static void
+ngx_http_v3_dummy_stream_handler(ngx_event_t *rev)
+{
+    u_char             buf[128];
+    ngx_connection_t  *c;
+
+    /* read out and ignore */
+
+    c = rev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 dummy stream reader");
+
+    while (rev->ready) {
+        if (c->recv(c, buf, sizeof(buf)) == NGX_ERROR) {
+            goto failed;
+        }
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(c);
+}
+
+
+static void
+ngx_http_v3_client_encoder_handler(ngx_event_t *rev)
+{
+    u_char                     v;
+    ssize_t                    n;
+    ngx_str_t                  name, value;
+    ngx_uint_t                 dynamic, huffman, index, offset;
+    ngx_connection_t          *c, *pc;
+    ngx_http_v3_uni_stream_t  *st;
+    enum {
+        sw_start = 0,
+        sw_inr_name_index,
+        sw_inr_value_length,
+        sw_inr_read_value_length,
+        sw_inr_value,
+        sw_iwnr_name_length,
+        sw_iwnr_name,
+        sw_iwnr_value_length,
+        sw_iwnr_read_value_length,
+        sw_iwnr_value,
+        sw_capacity,
+        sw_duplicate
+    } state;
+
+    c = rev->data;
+    st = c->data;
+    pc = c->qs->parent;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 client encoder");
+
+    state = st->state;
+    dynamic = st->dynamic;
+    huffman = st->huffman;
+    index = st->index;
+    offset = st->offset;
+    name = st->name;
+    value = st->value;
+
+    while (rev->ready) {
+
+        /* XXX limit checks */
+        /* XXX buffer input */
+
+        n = c->recv(c, &v, 1);
+
+        if (n == NGX_ERROR || n == 0) {
+            goto failed;
+        }
+
+        if (n != 1) {
+            break;
+        }
+
+        /* XXX v -> ch */
+
+        switch (state) {
+
+        case sw_start:
+
+            if (v & 0x80) {
+                /* Insert With Name Reference */
+
+                dynamic = (v & 0x40) ? 0 : 1;
+                index = v & 0x3f;
+
+                if (index != 0x3f) {
+                    state = sw_inr_value_length;
+                    break;
+                }
+
+                index = 0;
+                state = sw_inr_name_index;
+                break;
+            }
+
+            if (v & 0x40) {
+                /*  Insert Without Name Reference */
+
+                huffman = (v & 0x20) ? 1 : 0;
+                name.len = v & 0x1f;
+
+                if (name.len != 0x1f) {
+                    offset = 0;
+                    state = sw_iwnr_name;
+                    break;
+                }
+
+                name.len = 0;
+                state = sw_iwnr_name_length;
+                break;
+            }
+
+            if (v & 0x20) {
+                /*  Set Dynamic Table Capacity */
+
+                index = v & 0x1f;
+
+                if (index != 0x1f) {
+                    if (ngx_http_v3_set_capacity(c, index) != NGX_OK) {
+                        goto failed;
+                    }
+
+                    break;
+                }
+
+                index = 0;
+                state = sw_capacity;
+                break;
+            }
+
+            /* Duplicate */
+
+            index = v & 0x1f;
+
+            if (index != 0x1f) {
+                if (ngx_http_v3_duplicate(c, index) != NGX_OK) {
+                    goto failed;
+                }
+
+                break;
+            }
+
+            index = 0;
+            state = sw_duplicate;
+            break;
+
+        case sw_inr_name_index:
+
+            index = (index << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            index += 0x3f;
+            state = sw_inr_value_length;
+            break;
+
+        case sw_inr_value_length:
+
+            huffman = (v & 0x80) ? 1 : 0;
+            value.len = v & 0x7f;
+
+            if (value.len == 0) {
+                value.data = NULL;
+
+                if (ngx_http_v3_ref_insert(c, dynamic, index, &value) != NGX_OK)
+                {
+                    goto failed;
+                }
+
+                state = sw_start;
+                break;
+            }
+
+            if (value.len != 0x7f) {
+                value.data = ngx_pnalloc(pc->pool, value.len);
+                if (value.data == NULL) {
+                    goto failed;
+                }
+
+                state = sw_inr_value;
+                offset = 0;
+                break;
+            }
+
+            value.len = 0;
+            state = sw_inr_read_value_length;
+            break;
+
+        case sw_inr_read_value_length:
+
+            value.len = (value.len << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            value.len += 0x7f;
+
+            value.data = ngx_pnalloc(pc->pool, value.len);
+            if (value.data == NULL) {
+                goto failed;
+            }
+
+            state = sw_inr_value;
+            offset = 0;
+            break;
+
+        case sw_inr_value:
+
+            value.data[offset++] = v;
+            if (offset != value.len) {
+                break;
+            }
+
+            if (huffman) {
+                if (ngx_http_v3_decode_huffman(pc, &value) != NGX_OK) {
+                    goto failed;
+                }
+            }
+
+            if (ngx_http_v3_ref_insert(c, dynamic, index, &value) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+
+        case sw_iwnr_name_length:
+
+            name.len = (name.len << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            name.len += 0x1f;
+
+            name.data = ngx_pnalloc(pc->pool, name.len);
+            if (name.data == NULL) {
+                goto failed;
+            }
+
+            offset = 0;
+            state = sw_iwnr_name;
+            break;
+
+        case sw_iwnr_name:
+
+            name.data[offset++] = v;
+            if (offset != name.len) {
+                break;
+            }
+
+            if (huffman) {
+                if (ngx_http_v3_decode_huffman(pc, &name) != NGX_OK) {
+                    goto failed;
+                }
+            }
+
+            state = sw_iwnr_value_length;
+            break;
+
+        case sw_iwnr_value_length:
+
+            huffman = (v & 0x80) ? 1 : 0;
+            value.len = v & 0x7f;
+
+            if (value.len == 0) {
+                value.data = NULL;
+
+                if (ngx_http_v3_insert(c, &name, &value) != NGX_OK) {
+                    goto failed;
+                }
+
+                state = sw_start;
+                break;
+            }
+
+            if (value.len != 0x7f) {
+                value.data = ngx_pnalloc(pc->pool, value.len);
+                if (value.data == NULL) {
+                    goto failed;
+                }
+
+                offset = 0;
+                state = sw_iwnr_value;
+                break;
+            }
+
+            state = sw_iwnr_read_value_length;
+            break;
+
+        case sw_iwnr_read_value_length:
+
+            value.len = (value.len << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            value.data = ngx_pnalloc(pc->pool, value.len);
+            if (value.data == NULL) {
+                goto failed;
+            }
+
+            offset = 0;
+            state = sw_iwnr_value;
+            break;
+
+        case sw_iwnr_value:
+
+            value.data[offset++] = v;
+            if (offset != value.len) {
+                break;
+            }
+
+            if (huffman) {
+                if (ngx_http_v3_decode_huffman(pc, &value) != NGX_OK) {
+                    goto failed;
+                }
+            }
+
+            if (ngx_http_v3_insert(c, &name, &value) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+
+
+        case sw_capacity:
+
+            index = (index << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            index += 0x1f;
+
+            if (ngx_http_v3_set_capacity(c, index) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+
+        case sw_duplicate:
+
+            index = (index << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            index += 0x1f;
+
+            if (ngx_http_v3_duplicate(c, index) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+        }
+    }
+
+    st->state = state;
+    st->dynamic = dynamic;
+    st->huffman = huffman;
+    st->index = index;
+    st->offset = offset;
+    st->name = name;
+    st->value = value;
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(c);
+}
+
+
+static void
+ngx_http_v3_client_decoder_handler(ngx_event_t *rev)
+{
+    u_char                     v;
+    ssize_t                    n;
+    ngx_uint_t                 index;
+    ngx_connection_t          *c;
+    ngx_http_v3_uni_stream_t  *st;
+    enum {
+        sw_start = 0,
+        sw_ack_header,
+        sw_cancel_stream,
+        sw_inc_insert_count
+    } state;
+
+    c = rev->data;
+    st = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 client decoder");
+
+    state = st->state;
+    index = st->index;
+
+    while (rev->ready) {
+
+        /* XXX limit checks */
+        /* XXX buffer input */
+
+        n = c->recv(c, &v, 1);
+
+        if (n == NGX_ERROR || n == 0) {
+            goto failed;
+        }
+
+        if (n != 1) {
+            break;
+        }
+
+        switch (state) {
+
+        case sw_start:
+
+            if (v & 0x80) {
+                /* Header Acknowledgement */
+
+                index = v & 0x7f;
+
+                if (index != 0x7f) {
+                    if (ngx_http_v3_ack_header(c, index) != NGX_OK) {
+                        goto failed;
+                    }
+
+                    break;
+                }
+
+                index = 0;
+                state = sw_ack_header;
+                break;
+            }
+
+            if (v & 0x40) {
+                /*  Stream Cancellation */
+
+                index = v & 0x3f;
+
+                if (index != 0x3f) {
+                    if (ngx_http_v3_cancel_stream(c, index) != NGX_OK) {
+                        goto failed;
+                    }
+
+                    break;
+                }
+
+                index = 0;
+                state = sw_cancel_stream;
+                break;
+            }
+
+            /*  Insert Count Increment */
+
+            index = v & 0x3f;
+
+            if (index != 0x3f) {
+                if (ngx_http_v3_inc_insert_count(c, index) != NGX_OK) {
+                    goto failed;
+                }
+
+                break;
+            }
+
+            index = 0;
+            state = sw_inc_insert_count;
+            break;
+
+        case sw_ack_header:
+
+            index = (index << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            index += 0x7f;
+
+            if (ngx_http_v3_ack_header(c, index) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+
+        case sw_cancel_stream:
+
+            index = (index << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            index += 0x3f;
+
+            if (ngx_http_v3_cancel_stream(c, index) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+
+        case sw_inc_insert_count:
+
+            index = (index << 7) + (v & 0x7f);
+            if (v & 0x80) {
+                break;
+            }
+
+            index += 0x3f;
+
+            if (ngx_http_v3_inc_insert_count(c, index) != NGX_OK) {
+                goto failed;
+            }
+
+            state = sw_start;
+            break;
+        }
+    }
+
+    st->state = state;
+    st->index = index;
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(c);
+}
+
+
+/* XXX async & buffered stream writes */
+
+static ngx_connection_t *
+ngx_http_v3_create_uni_stream(ngx_connection_t *c, ngx_uint_t type)
+{
+    u_char                     buf[NGX_HTTP_V3_VARLEN_INT_LEN];
+    size_t                     n;
+    ngx_connection_t          *sc;
+    ngx_pool_cleanup_t        *cln;
+    ngx_http_v3_uni_stream_t  *us;
+
+    sc = ngx_quic_create_uni_stream(c->qs->parent);
+    if (sc == NULL) {
+        return NULL;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 create uni stream, type:%ui", type);
+
+    us = ngx_pcalloc(sc->pool, sizeof(ngx_http_v3_uni_stream_t));
+    if (us == NULL) {
+        goto failed;
+    }
+
+    us->signature = NGX_HTTP_V3_STREAM;
+    us->type = type;
+    sc->data = us;
+
+    cln = ngx_pool_cleanup_add(sc->pool, 0);
+    if (cln == NULL) {
+        goto failed;
+    }
+
+    cln->handler = ngx_http_v3_uni_stream_cleanup;
+    cln->data = sc;
+
+    n = (u_char *) ngx_http_v3_encode_varlen_int(buf, type) - buf;
+
+    if (sc->send(sc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return sc;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(sc);
+
+    return NULL;
+}
+
+
+static ngx_connection_t *
+ngx_http_v3_get_server_encoder(ngx_connection_t *c)
+{
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->qs->parent->data;
+
+    if (h3c->server_encoder == NULL) {
+        h3c->server_encoder = ngx_http_v3_create_uni_stream(c,
+                                                   NGX_HTTP_V3_ENCODER_STREAM);
+    }
+
+    return h3c->server_encoder;
+}
+
+
+static ngx_connection_t *
+ngx_http_v3_get_server_decoder(ngx_connection_t *c)
+{
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->qs->parent->data;
+
+    if (h3c->server_decoder == NULL) {
+        h3c->server_decoder = ngx_http_v3_create_uni_stream(c,
+                                                   NGX_HTTP_V3_DECODER_STREAM);
+    }
+
+    return h3c->server_decoder;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_ref_insert(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index, ngx_str_t *value)
+{
+    u_char            *p, buf[NGX_HTTP_V3_PREFIX_INT_LEN * 2];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client ref insert, %s[%ui] \"%V\"",
+                   dynamic ? "dynamic" : "static", index, value);
+
+    ec = ngx_http_v3_get_server_encoder(c);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    p = buf;
+
+    *p = (dynamic ? 0x80 : 0xc0);
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, index, 6);
+
+    /* XXX option for huffman? */
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, value->len, 7);
+
+    n = p - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    if (ec->send(ec, value->data, value->len) != (ssize_t) value->len) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(ec);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_insert(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client insert \"%V\":\"%V\"", name, value);
+
+    ec = ngx_http_v3_get_server_encoder(c);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    /* XXX option for huffman? */
+    buf[0] = 0x40;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, name->len, 5) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    if (ec->send(ec, name->data, name->len) != (ssize_t) name->len) {
+        goto failed;
+    }
+
+    /* XXX option for huffman? */
+    buf[0] = 0;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, value->len, 7) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    if (ec->send(ec, value->data, value->len) != (ssize_t) value->len) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(ec);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_set_capacity(ngx_connection_t *c, ngx_uint_t capacity)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client set capacity %ui", capacity);
+
+    ec = ngx_http_v3_get_server_encoder(c);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0x20;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, capacity, 5) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(ec);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_duplicate(ngx_connection_t *c, ngx_uint_t index)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client duplicate %ui", index);
+
+    ec = ngx_http_v3_get_server_encoder(c);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, index, 5) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(ec);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_ack_header(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *dc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client ack header %ui", stream_id);
+
+    dc = ngx_http_v3_get_server_decoder(c);
+    if (dc == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0x80;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, stream_id, 7) - buf;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(dc);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_cancel_stream(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *dc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client cancel stream %ui", stream_id);
+
+    dc = ngx_http_v3_get_server_decoder(c);
+    if (dc == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0x40;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, stream_id, 6) - buf;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(dc);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_inc_insert_count(ngx_connection_t *c, ngx_uint_t inc)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *dc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client increment insert count %ui", inc);
+
+    dc = ngx_http_v3_get_server_decoder(c);
+    if (dc == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, inc, 6) - buf;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(dc);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}