7 ex__http2__server_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

examples/ex_http2_server.c

Structures

Name
struct SEEN
struct PEER

Functions

Name
void test_send_window_stall(void )
void test_send_flow_control(void )
void test_rst_stream(void )
void test_post_body(void )
void test_ping_goaway(void )
void test_multiplex(void )
void test_handshake_errors(void )
void test_handshake(void )
void test_guards(void )
void test_get(void )
void test_dribbled_reads(void )
void test_continuation(void )
void test_callback_stop(void )
void process_args(int argc, char ** argv)
ssize_t peer_write(void * io, const void * buf, size_t len)
void peer_window_update(PEER * p, uint32_t sid, uint32_t inc)
ssize_t peer_read(void * io, void * buf, size_t len)
void peer_put(PEER * p, const void * buf, size_t len)
void peer_prologue(PEER * p)
void peer_headers(PEER * p, uint32_t sid, const N_H2_HEADER * h, size_t n, int end_stream)
void peer_frame(PEER * p, int type, int flags, uint32_t sid, const unsigned char * payload, size_t len)
int out_find(const PEER * p, int type, size_t nth, N_H2_FRAME * out)
size_t out_count(const PEER * p, int type, size_t * total_payload)
int on_request(void * ud, N_H2_SERVER * srv, const N_H2_REQUEST * req)
int main(int argc, char ** argv)

Attributes

Name
int failures

Defines

Name
RUNT(f)

Functions Documentation

function test_send_window_stall

static void test_send_window_stall(
    void 
)

< DATA

function test_send_flow_control

static void test_send_flow_control(
    void 
)

< DATA

< DATA

function test_rst_stream

static void test_rst_stream(
    void 
)

< stream cancelled

< RST_STREAM

< DATA

< DATA/HEADERS: last frame of the stream

function test_post_body

static void test_post_body(
    void 
)

< DATA

< DATA

< DATA/HEADERS: last frame of the stream

< WINDOW_UPDATE

< WINDOW_UPDATE

function test_ping_goaway

static void test_ping_goaway(
    void 
)

< PING

< GOAWAY

< PING

< SETTINGS/PING: acknowledgement

function test_multiplex

static void test_multiplex(
    void 
)

< HEADERS

< HEADERS

< HEADERS

< HEADERS

function test_handshake_errors

static void test_handshake_errors(
    void 
)

< PING

function test_handshake

static void test_handshake(
    void 
)

< SETTINGS

< WINDOW_UPDATE

< SETTINGS

< SETTINGS

< SETTINGS/PING: acknowledgement

< SETTINGS

function test_guards

static void test_guards(
    void 
)

function test_get

static void test_get(
    void 
)

< HEADERS

< HEADERS/PUSH_PROMISE/CONTINUATION: header block complete

< DATA/HEADERS: last frame of the stream

< DATA

< DATA/HEADERS: last frame of the stream

< GOAWAY

function test_dribbled_reads

static void test_dribbled_reads(
    void 
)

function test_continuation

static void test_continuation(
    void 
)

< HEADERS

< DATA/HEADERS: last frame of the stream

< CONTINUATION

< HEADERS/PUSH_PROMISE/CONTINUATION: header block complete

< HEADERS

< DATA/HEADERS: last frame of the stream

< PING

< CONTINUATION

< HEADERS/PUSH_PROMISE/CONTINUATION: header block complete

< HEADERS

< DATA/HEADERS: last frame of the stream

< CONTINUATION

< HEADERS/PUSH_PROMISE/CONTINUATION: header block complete

function test_callback_stop

static void test_callback_stop(
    void 
)

< GOAWAY

function process_args

void process_args(
    int argc,
    char ** argv
)

function peer_write

static ssize_t peer_write(
    void * io,
    const void * buf,
    size_t len
)

function peer_window_update

static void peer_window_update(
    PEER * p,
    uint32_t sid,
    uint32_t inc
)

< WINDOW_UPDATE

function peer_read

static ssize_t peer_read(
    void * io,
    void * buf,
    size_t len
)

function peer_put

static void peer_put(
    PEER * p,
    const void * buf,
    size_t len
)

function peer_prologue

static void peer_prologue(
    PEER * p
)

< SETTINGS

function peer_headers

static void peer_headers(
    PEER * p,
    uint32_t sid,
    const N_H2_HEADER * h,
    size_t n,
    int end_stream
)

< HEADERS/PUSH_PROMISE/CONTINUATION: header block complete

< DATA/HEADERS: last frame of the stream

< HEADERS

function peer_frame

static void peer_frame(
    PEER * p,
    int type,
    int flags,
    uint32_t sid,
    const unsigned char * payload,
    size_t len
)

function out_find

static int out_find(
    const PEER * p,
    int type,
    size_t nth,
    N_H2_FRAME * out
)

function out_count

static size_t out_count(
    const PEER * p,
    int type,
    size_t * total_payload
)

function on_request

static int on_request(
    void * ud,
    N_H2_SERVER * srv,
    const N_H2_REQUEST * req
)

function main

int main(
    int argc,
    char ** argv
)

Attributes Documentation

variable failures

static int failures = 0;

Macros Documentation

define RUNT

#define RUNT(
    f
)
do {                                                                \
    int before = failures;                                          \
    f();                                                            \
    if (failures != before)                                         \
        n_log(LOG_ERR, "%s: %d failure(s)", #f, failures - before); \
} while (0)

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#include "nilorea/n_http2_server.h"
#include "nilorea/n_log.h"

#include <getopt.h>
#include <stdlib.h>
#include <string.h>

static int failures = 0;

/* ------------------------------------------------------------------ */
/* the scripted peer                                                  */
/* ------------------------------------------------------------------ */

/* A byte script the server reads from, plus the bytes it wrote back. */
typedef struct PEER {
    unsigned char in[65536];   /* what the client sends */
    size_t in_len;             /* its length */
    size_t in_off;             /* how much the server has consumed */
    unsigned char out[262144]; /* what the server sent */
    size_t out_len;            /* its length */
    size_t chunk;              /* max bytes served per read (0 = no limit) */
    int read_fail;             /* return -1 from the read callback */
} PEER;

static ssize_t peer_read(void* io, void* buf, size_t len) {
    PEER* p = (PEER*)io;
    size_t n = p->in_len - p->in_off;
    if (p->read_fail)
        return -1;
    if (n == 0)
        return 0; /* end of the script: a clean peer close */
    if (n > len)
        n = len;
    if (p->chunk && n > p->chunk)
        n = p->chunk;
    memcpy(buf, p->in + p->in_off, n);
    p->in_off += n;
    return (ssize_t)n;
}

static ssize_t peer_write(void* io, const void* buf, size_t len) {
    PEER* p = (PEER*)io;
    if (p->out_len + len > sizeof(p->out))
        return -1;
    memcpy(p->out + p->out_len, buf, len);
    p->out_len += len;
    return (ssize_t)len;
}

/* Append raw bytes to the client script. */
static void peer_put(PEER* p, const void* buf, size_t len) {
    if (p->in_len + len > sizeof(p->in)) {
        failures++;
        return;
    }
    memcpy(p->in + p->in_len, buf, len);
    p->in_len += len;
}

/* Append one frame (header then payload) to the client script. */
static void peer_frame(PEER* p, int type, int flags, uint32_t sid, const unsigned char* payload, size_t len) {
    unsigned char hdr[N_H2_FRAME_HEADER_LEN];
    n_http2_frame_build_header(hdr, sizeof(hdr), (uint32_t)len, type, flags, sid);
    peer_put(p, hdr, sizeof(hdr));
    if (len)
        peer_put(p, payload, len);
}

/* Append the client preface and an empty SETTINGS. */
static void peer_prologue(PEER* p) {
    peer_put(p, N_H2_PREFACE, strlen(N_H2_PREFACE));
    peer_frame(p, N_H2_SETTINGS, 0, 0, NULL, 0);
}

/* Append a HEADERS frame carrying an HPACK-encoded header list. */
static void peer_headers(PEER* p, uint32_t sid, const N_H2_HEADER* h, size_t n, int end_stream) {
    unsigned char block[4096];
    size_t w = n_http2_hpack_encode(h, n, block, sizeof(block));
    int flags = N_H2_FLAG_END_HEADERS | (end_stream ? N_H2_FLAG_END_STREAM : 0);
    if (w == 0) {
        failures++;
        return;
    }
    peer_frame(p, N_H2_HEADERS, flags, sid, block, w);
}

/* Append a WINDOW_UPDATE frame. */
static void peer_window_update(PEER* p, uint32_t sid, uint32_t inc) {
    unsigned char b[4];
    b[0] = (unsigned char)((inc >> 24) & 0x7F);
    b[1] = (unsigned char)((inc >> 16) & 0xFF);
    b[2] = (unsigned char)((inc >> 8) & 0xFF);
    b[3] = (unsigned char)(inc & 0xFF);
    peer_frame(p, N_H2_WINDOW_UPDATE, 0, sid, b, sizeof(b));
}

/* Count the frames of one type the server sent, and sum their payload lengths. */
static size_t out_count(const PEER* p, int type, size_t* total_payload) {
    size_t off = 0, count = 0;
    if (total_payload)
        *total_payload = 0;
    while (off < p->out_len) {
        N_H2_FRAME f;
        size_t consumed = 0;
        if (n_http2_frame_parse(p->out + off, p->out_len - off, &f, &consumed) != 1)
            break;
        if (f.type == type) {
            count++;
            if (total_payload)
                *total_payload += f.length;
        }
        off += consumed;
    }
    return count;
}

/* Find the nth frame of a type the server sent. Returns 1 when found. */
static int out_find(const PEER* p, int type, size_t nth, N_H2_FRAME* out) {
    size_t off = 0, seen = 0;
    while (off < p->out_len) {
        N_H2_FRAME f;
        size_t consumed = 0;
        if (n_http2_frame_parse(p->out + off, p->out_len - off, &f, &consumed) != 1)
            break;
        if (f.type == type && seen++ == nth) {
            *out = f;
            return 1;
        }
        off += consumed;
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/* the handler                                                        */
/* ------------------------------------------------------------------ */

/* What the request callback recorded, so the checks can look at it. */
typedef struct SEEN {
    int calls;
    char method[32];
    char path[256];
    char authority[128];
    char scheme[16];
    char body[4096];
    size_t body_len;
    size_t reply_len;     /* bytes of body to answer with */
    int reply_status;     /* :status to answer with */
    int stop_after;       /* return -1 from the callback after this many calls */
    int expect_send_fail; /* the response is expected NOT to go out in full */
    unsigned char* reply; /* the reply body (owned by the test) */
} SEEN;

static int on_request(void* ud, N_H2_SERVER* srv, const N_H2_REQUEST* req) {
    SEEN* s = (SEEN*)ud;
    N_H2_HEADER h[2];
    char status[8];
    s->calls++;
    snprintf(s->method, sizeof(s->method), "%s", req->method ? req->method : "");
    snprintf(s->path, sizeof(s->path), "%s", req->path ? req->path : "");
    snprintf(s->authority, sizeof(s->authority), "%s", req->authority ? req->authority : "");
    snprintf(s->scheme, sizeof(s->scheme), "%s", req->scheme ? req->scheme : "");
    s->body_len = req->body_len < sizeof(s->body) ? req->body_len : sizeof(s->body) - 1;
    if (req->body && s->body_len)
        memcpy(s->body, req->body, s->body_len);
    s->body[s->body_len] = '\0';

    snprintf(status, sizeof(status), "%d", s->reply_status ? s->reply_status : 200);
    h[0].name = (char*)":status";
    h[0].value = status;
    h[1].name = (char*)"content-type";
    h[1].value = (char*)"text/plain";
    if ((n_http2_server_respond(srv, req->stream_id, h, 2, s->reply, s->reply_len) != 0) != (s->expect_send_fail != 0))
        failures++;
    return (s->stop_after && s->calls >= s->stop_after) ? -1 : 0;
}

/* ------------------------------------------------------------------ */
/* checks                                                             */
/* ------------------------------------------------------------------ */

/* The handshake reads the preface, answers with SETTINGS plus a connection
   WINDOW_UPDATE, and acknowledges the client's SETTINGS. */
static void test_handshake(void) {
    PEER p;
    N_H2_SERVER* srv;
    N_H2_FRAME f;

    memset(&p, 0, sizeof(p));
    peer_prologue(&p);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (!srv) {
        failures++;
        return;
    }
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (out_count(&p, N_H2_SETTINGS, NULL) != 2) failures++;      /* ours plus the ACK */
    if (out_count(&p, N_H2_WINDOW_UPDATE, NULL) != 1) failures++; /* the connection window */
    if (!out_find(&p, N_H2_SETTINGS, 0, &f) || f.flags != 0) failures++;
    if (!out_find(&p, N_H2_SETTINGS, 1, &f) || !(f.flags & N_H2_FLAG_ACK)) failures++;
    /* our SETTINGS must announce a stream receive window, or a body over 64KB stalls */
    if (out_find(&p, N_H2_SETTINGS, 0, &f)) {
        N_H2_SETTING set[8];
        size_t n = 0, i;
        int seen_win = 0;
        if (n_http2_settings_parse(f.payload, f.length, set, 8, &n) != 0) failures++;
        for (i = 0; i < n; i++)
            if (set[i].id == N_H2_SETTINGS_INITIAL_WINDOW_SIZE && set[i].value > 65535)
                seen_win = 1;
        if (!seen_win) failures++;
    }
    n_http2_server_free(&srv);
    if (srv) failures++;
}

/* A bad preface, and a first frame that is not SETTINGS, are both refused. */
static void test_handshake_errors(void) {
    PEER p;
    N_H2_SERVER* srv;

    memset(&p, 0, sizeof(p));
    peer_put(&p, "GET / HTTP/1.1\r\nHost: x\r\n\r\n\r\n\r\n\r\n", 33);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != -1) failures++;
    if (!n_http2_server_error(srv)) failures++;
    n_http2_server_free(&srv);

    memset(&p, 0, sizeof(p));
    peer_put(&p, N_H2_PREFACE, strlen(N_H2_PREFACE));
    peer_frame(&p, N_H2_PING, 0, 0, (const unsigned char*)"12345678", 8);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != -1) failures++;
    n_http2_server_free(&srv);

    /* a truncated preface is an error, not a hang */
    memset(&p, 0, sizeof(p));
    peer_put(&p, N_H2_PREFACE, 10);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != -1) failures++;
    n_http2_server_free(&srv);
}

/* One GET: the pseudo-headers reach the callback and the response comes back as
   HEADERS with END_STREAM plus one DATA frame. */
static void test_get(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_FRAME f;
    N_H2_HEADER req[4];
    unsigned char body[] = "hello h2";

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    seen.reply = body;
    seen.reply_len = sizeof(body) - 1;
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/a?b=c";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 1);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 1) failures++;
    if (strcmp(seen.method, "GET") != 0) failures++;
    if (strcmp(seen.path, "/a?b=c") != 0) failures++;
    if (strcmp(seen.authority, "example.test") != 0) failures++;
    if (strcmp(seen.scheme, "https") != 0) failures++;
    if (seen.body_len != 0) failures++;
    if (n_http2_server_request_count(srv) != 1) failures++;

    /* the response: HEADERS on stream 1, then DATA with END_STREAM */
    if (!out_find(&p, N_H2_HEADERS, 0, &f) || f.stream_id != 1) failures++;
    if (!(f.flags & N_H2_FLAG_END_HEADERS)) failures++;
    if (f.flags & N_H2_FLAG_END_STREAM) failures++; /* a body follows */
    if (!out_find(&p, N_H2_DATA, 0, &f) || f.stream_id != 1) failures++;
    if (!(f.flags & N_H2_FLAG_END_STREAM)) failures++;
    if (f.length != sizeof(body) - 1 || memcmp(f.payload, body, f.length) != 0) failures++;
    if (out_count(&p, N_H2_GOAWAY, NULL) != 1) failures++;
    n_http2_server_free(&srv);
}

/* A POST body split over several DATA frames is reassembled, and every byte is
   given back to the connection window (including the END_STREAM frame's). */
static void test_post_body(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[5];
    size_t given = 0;

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    req[0].name = (char*)":method";
    req[0].value = (char*)"POST";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/submit";
    req[4].name = (char*)"content-type";
    req[4].value = (char*)"application/x-www-form-urlencoded";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 5, 0);
    peer_frame(&p, N_H2_DATA, 0, 1, (const unsigned char*)"user=admin", 10);
    peer_frame(&p, N_H2_DATA, N_H2_FLAG_END_STREAM, 1, (const unsigned char*)"&pass=x", 7);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 1) failures++;
    if (strcmp(seen.method, "POST") != 0) failures++;
    if (seen.body_len != 17 || strcmp(seen.body, "user=admin&pass=x") != 0) failures++;

    /* the connection window must be replenished by all 17 body bytes: dropping
       the last frame's 10+7 would starve a long-lived connection one request at
       a time, which only shows up after many requests */
    out_count(&p, N_H2_WINDOW_UPDATE, &given);
    {
        size_t off = 0, conn_inc = 0;
        while (off < p.out_len) {
            N_H2_FRAME f;
            size_t consumed = 0;
            if (n_http2_frame_parse(p.out + off, p.out_len - off, &f, &consumed) != 1)
                break;
            if (f.type == N_H2_WINDOW_UPDATE && f.stream_id == 0 && f.length == 4) {
                uint32_t inc = ((uint32_t)(f.payload[0] & 0x7F) << 24) | ((uint32_t)f.payload[1] << 16) | ((uint32_t)f.payload[2] << 8) | (uint32_t)f.payload[3];
                if (inc < 1000) /* skip the big handshake update */
                    conn_inc += inc;
            }
            off += consumed;
        }
        if (conn_inc != 17) failures++;
    }
    n_http2_server_free(&srv);
}

/* A header block split into CONTINUATION frames is reassembled; a frame
   interleaved inside the block is a connection error. */
static void test_continuation(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];
    unsigned char block[4096];
    size_t w, half;

    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/split";
    w = n_http2_hpack_encode(req, 4, block, sizeof(block));
    if (w < 4) {
        failures++;
        return;
    }
    half = w / 2;

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    peer_prologue(&p);
    peer_frame(&p, N_H2_HEADERS, N_H2_FLAG_END_STREAM, 1, block, half);
    peer_frame(&p, N_H2_CONTINUATION, N_H2_FLAG_END_HEADERS, 1, block + half, w - half);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 1 || strcmp(seen.path, "/split") != 0) failures++;
    n_http2_server_free(&srv);

    /* a PING between the HEADERS and its CONTINUATION leaves HPACK undefined:
       the connection must fail rather than decode a guess */
    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    peer_prologue(&p);
    peer_frame(&p, N_H2_HEADERS, N_H2_FLAG_END_STREAM, 1, block, half);
    peer_frame(&p, N_H2_PING, 0, 0, (const unsigned char*)"12345678", 8);
    peer_frame(&p, N_H2_CONTINUATION, N_H2_FLAG_END_HEADERS, 1, block + half, w - half);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) == 0) failures++; /* a connection error */
    if (seen.calls != 0) failures++;
    if (!n_http2_server_error(srv)) failures++;
    n_http2_server_free(&srv);

    /* a CONTINUATION for another stream is the same class of error */
    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    peer_prologue(&p);
    peer_frame(&p, N_H2_HEADERS, N_H2_FLAG_END_STREAM, 1, block, half);
    peer_frame(&p, N_H2_CONTINUATION, N_H2_FLAG_END_HEADERS, 3, block + half, w - half);
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) == 0) failures++;
    n_http2_server_free(&srv);
}

/* Several streams on one connection are served in the order they complete. */
static void test_multiplex(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];
    N_H2_FRAME f;

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/one";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 1);
    req[3].value = (char*)"/two";
    peer_headers(&p, 3, req, 4, 1);
    req[3].value = (char*)"/three";
    peer_headers(&p, 5, req, 4, 1);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 3) failures++;
    if (strcmp(seen.path, "/three") != 0) failures++; /* the last one served */
    if (out_count(&p, N_H2_HEADERS, NULL) != 3) failures++;
    if (!out_find(&p, N_H2_HEADERS, 0, &f) || f.stream_id != 1) failures++;
    if (!out_find(&p, N_H2_HEADERS, 1, &f) || f.stream_id != 3) failures++;
    if (!out_find(&p, N_H2_HEADERS, 2, &f) || f.stream_id != 5) failures++;
    n_http2_server_free(&srv);
}

/* PING is answered with a PING ACK carrying the same payload; a client GOAWAY
   ends the run. */
static void test_ping_goaway(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_FRAME f;
    unsigned char goaway[8];

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    memset(goaway, 0, sizeof(goaway));
    peer_prologue(&p);
    peer_frame(&p, N_H2_PING, 0, 0, (const unsigned char*)"abcdefgh", 8);
    peer_frame(&p, N_H2_GOAWAY, 0, 0, goaway, sizeof(goaway));
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (!out_find(&p, N_H2_PING, 0, &f)) failures++;
    if (!(f.flags & N_H2_FLAG_ACK) || f.length != 8 || memcmp(f.payload, "abcdefgh", 8) != 0) failures++;
    if (seen.calls != 0) failures++;
    n_http2_server_free(&srv);
}

/* A response larger than the peer's initial 65535-byte window is written only as
   far as the window allows, then continues once a WINDOW_UPDATE arrives. This is
   the check a server that ignores send-side flow control fails by hanging. */
static void test_send_flow_control(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];
    size_t data_bytes = 0;
    const size_t reply_len = 100000;
    unsigned char* reply = malloc(reply_len);

    if (!reply) {
        failures++;
        return;
    }
    memset(reply, 'A', reply_len);
    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    seen.reply = reply;
    seen.reply_len = reply_len;
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/big";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 1);
    /* the client reopens both windows, but only after the request: the server
       must park mid-body and resume, not deadlock and not overshoot */
    peer_window_update(&p, 0, 100000);
    peer_window_update(&p, 1, 100000);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    out_count(&p, N_H2_DATA, &data_bytes);
    if (data_bytes != reply_len) failures++;
    /* 100000 bytes at the default 16384 max frame size is 7 DATA frames */
    if (out_count(&p, N_H2_DATA, NULL) != 7) failures++;
    n_http2_server_free(&srv);
    free(reply);
}

/* When the peer never reopens the window the write fails instead of hanging. */
static void test_send_window_stall(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];
    const size_t reply_len = 100000;
    unsigned char* reply = malloc(reply_len);

    if (!reply) {
        failures++;
        return;
    }
    memset(reply, 'B', reply_len);
    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    seen.reply = reply;
    seen.reply_len = reply_len;
    seen.expect_send_fail = 1;
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/big";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 1);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (!n_http2_server_error(srv)) failures++;
    {
        size_t sent = 0;
        out_count(&p, N_H2_DATA, &sent);
        if (sent != 65535) failures++; /* exactly the initial window, no more */
    }
    n_http2_server_free(&srv);
    free(reply);
}

/* A reset stream is dropped, and its request never reaches the callback. */
static void test_rst_stream(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];
    unsigned char code[4] = {0, 0, 0, N_H2_ERR_CANCEL};

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/gone";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 0);
    peer_frame(&p, N_H2_RST_STREAM, 0, 1, code, sizeof(code));
    peer_frame(&p, N_H2_DATA, N_H2_FLAG_END_STREAM, 1, (const unsigned char*)"late", 4);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 0) failures++;
    n_http2_server_free(&srv);
}

/* The callback can stop the connection by returning -1, and the server still
   says goodbye with a GOAWAY. */
static void test_callback_stop(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    seen.stop_after = 1;
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/one";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 1);
    peer_headers(&p, 3, req, 4, 1);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 1) failures++;
    if (out_count(&p, N_H2_GOAWAY, NULL) != 1) failures++;
    n_http2_server_free(&srv);
}

/* The frame reader must not depend on how the bytes are cut up by the transport:
   the same script served one byte at a time behaves identically. */
static void test_dribbled_reads(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;
    N_H2_HEADER req[4];

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    p.chunk = 1;
    req[0].name = (char*)":method";
    req[0].value = (char*)"GET";
    req[1].name = (char*)":scheme";
    req[1].value = (char*)"https";
    req[2].name = (char*)":authority";
    req[2].value = (char*)"example.test";
    req[3].name = (char*)":path";
    req[3].value = (char*)"/dribble";
    peer_prologue(&p);
    peer_headers(&p, 1, req, 4, 1);

    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != 0) failures++;
    if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
    if (seen.calls != 1 || strcmp(seen.path, "/dribble") != 0) failures++;
    n_http2_server_free(&srv);
}

/* Guards: NULL arguments are refused, and a read error is reported. */
static void test_guards(void) {
    PEER p;
    SEEN seen;
    N_H2_SERVER* srv;

    if (n_http2_server_new(NULL, NULL, peer_write) != NULL) failures++;
    if (n_http2_server_new(NULL, peer_read, NULL) != NULL) failures++;
    if (n_http2_server_handshake(NULL) != -1) failures++;
    if (n_http2_server_error(NULL) != NULL) failures++;
    if (n_http2_server_request_count(NULL) != 0) failures++;
    n_http2_server_free(NULL); /* must not crash */

    memset(&p, 0, sizeof(p));
    memset(&seen, 0, sizeof(seen));
    p.read_fail = 1;
    srv = n_http2_server_new(&p, peer_read, peer_write);
    if (n_http2_server_handshake(srv) != -1) failures++;
    if (n_http2_server_run(srv, NULL, &seen) != -1) failures++;
    n_http2_server_free(&srv);
}

void process_args(int argc, char** argv) {
    int opt = 0;
    while ((opt = getopt(argc, argv, "hvV:")) != EOF) {
        switch (opt) {
            case 'V':
                if (!strncmp("LOG_NULL", optarg, 8))
                    set_log_level(LOG_NULL);
                else if (!strncmp("LOG_NOTICE", optarg, 10))
                    set_log_level(LOG_NOTICE);
                else if (!strncmp("LOG_INFO", optarg, 8))
                    set_log_level(LOG_INFO);
                else if (!strncmp("LOG_ERR", optarg, 7))
                    set_log_level(LOG_ERR);
                else if (!strncmp("LOG_DEBUG", optarg, 9))
                    set_log_level(LOG_DEBUG);
                break;
            case 'v':
                fprintf(stderr, "ex_http2_server\n");
                exit(1);
            default:
                break;
        }
    }
}

int main(int argc, char** argv) {
    set_log_level(LOG_ERR);
    process_args(argc, argv);

/* run one check and name it when it fails, so a regression points at its case
   instead of at a single total */
#define RUNT(f)                                                         \
    do {                                                                \
        int before = failures;                                          \
        f();                                                            \
        if (failures != before)                                         \
            n_log(LOG_ERR, "%s: %d failure(s)", #f, failures - before); \
    } while (0)

    RUNT(test_handshake);
    RUNT(test_handshake_errors);
    RUNT(test_get);
    RUNT(test_post_body);
    RUNT(test_continuation);
    RUNT(test_multiplex);
    RUNT(test_ping_goaway);
    RUNT(test_send_flow_control);
    RUNT(test_send_window_stall);
    RUNT(test_rst_stream);
    RUNT(test_callback_stop);
    RUNT(test_dribbled_reads);
    RUNT(test_guards);

    if (failures) {
        n_log(LOG_ERR, "ex_http2_server: %d failure(s)", failures);
        return 1;
    }
    n_log(LOG_NOTICE, "ex_http2_server: all checks passed");
    return 0;
}