Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http2_server.c

n_http2_server regression: preface, SETTINGS, requests, flow control.

n_http2_server regression: preface, SETTINGS, requests, flow control.

Author
Castagnier Mickael
Version
1.0

The server is driven against a scripted peer held in memory: the client bytes are pre-built into a buffer the read callback serves, and everything the server writes is collected by the write callback and parsed back as frames. No socket is involved, so every path (including the flow-control stalls, which need an exact byte budget) is deterministic.

/*
* 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_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)) {
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_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) {
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) {
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) {
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;
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))
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;
memset(&p, 0, sizeof(p));
if (!srv) {
return;
}
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++;
}
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;
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);
memset(&p, 0, sizeof(p));
peer_frame(&p, N_H2_PING, 0, 0, (const unsigned char*)"12345678", 8);
/* a truncated preface is an error, not a hang */
memset(&p, 0, sizeof(p));
}
/* 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_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_headers(&p, 1, req, 4, 1);
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++;
/* 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_STREAM) failures++; /* a body follows */
if (!out_find(&p, N_H2_DATA, 0, &f) || f.stream_id != 1) 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++;
}
/* 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_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_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);
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 */
{
size_t off = 0, conn_inc = 0;
while (off < p.out_len) {
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++;
}
}
/* 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_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) {
return;
}
half = w / 2;
memset(&p, 0, sizeof(p));
memset(&seen, 0, sizeof(seen));
peer_frame(&p, N_H2_CONTINUATION, N_H2_FLAG_END_HEADERS, 1, block + half, w - half);
if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
if (seen.calls != 1 || strcmp(seen.path, "/split") != 0) failures++;
/* 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_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);
if (n_http2_server_run(srv, on_request, &seen) == 0) failures++; /* a connection error */
if (seen.calls != 0) failures++;
/* a CONTINUATION for another stream is the same class of error */
memset(&p, 0, sizeof(p));
memset(&seen, 0, sizeof(seen));
peer_frame(&p, N_H2_CONTINUATION, N_H2_FLAG_END_HEADERS, 3, block + half, w - half);
if (n_http2_server_run(srv, on_request, &seen) == 0) failures++;
}
/* Several streams on one connection are served in the order they complete. */
static void test_multiplex(void) {
PEER p;
SEEN seen;
N_H2_HEADER req[4];
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_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);
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++;
}
/* 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;
unsigned char goaway[8];
memset(&p, 0, sizeof(p));
memset(&seen, 0, sizeof(seen));
memset(goaway, 0, sizeof(goaway));
peer_frame(&p, N_H2_PING, 0, 0, (const unsigned char*)"abcdefgh", 8);
peer_frame(&p, N_H2_GOAWAY, 0, 0, goaway, sizeof(goaway));
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++;
}
/* 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_HEADER req[4];
size_t data_bytes = 0;
const size_t reply_len = 100000;
unsigned char* reply = malloc(reply_len);
if (!reply) {
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_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);
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++;
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_HEADER req[4];
const size_t reply_len = 100000;
unsigned char* reply = malloc(reply_len);
if (!reply) {
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_headers(&p, 1, req, 4, 1);
if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
{
size_t sent = 0;
out_count(&p, N_H2_DATA, &sent);
if (sent != 65535) failures++; /* exactly the initial window, no more */
}
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_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_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);
if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
if (seen.calls != 0) failures++;
}
/* 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_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_headers(&p, 1, req, 4, 1);
peer_headers(&p, 3, req, 4, 1);
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++;
}
/* 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_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_headers(&p, 1, req, 4, 1);
if (n_http2_server_run(srv, on_request, &seen) != 0) failures++;
if (seen.calls != 1 || strcmp(seen.path, "/dribble") != 0) failures++;
}
/* Guards: NULL arguments are refused, and a read error is reported. */
static void test_guards(void) {
PEER p;
SEEN seen;
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++;
n_http2_server_free(NULL); /* must not crash */
memset(&p, 0, sizeof(p));
memset(&seen, 0, sizeof(seen));
p.read_fail = 1;
if (n_http2_server_run(srv, NULL, &seen) != -1) failures++;
}
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))
else if (!strncmp("LOG_NOTICE", optarg, 10))
else if (!strncmp("LOG_INFO", optarg, 8))
else if (!strncmp("LOG_ERR", optarg, 7))
else if (!strncmp("LOG_DEBUG", optarg, 9))
break;
case 'v':
fprintf(stderr, "ex_http2_server\n");
exit(1);
default:
break;
}
}
}
int main(int argc, char** argv) {
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)
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;
}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void type(N_GUI_CTX *gui, const char *s)
Type a whole string into whatever has focus.
static void peer_frame(PEER *p, int type, int flags, uint32_t sid, const unsigned char *payload, size_t len)
size_t in_len
static void test_send_window_stall(void)
static int on_request(void *ud, N_H2_SERVER *srv, const N_H2_REQUEST *req)
static void peer_put(PEER *p, const void *buf, size_t len)
unsigned char in[65536]
char method[32]
static void peer_window_update(PEER *p, uint32_t sid, uint32_t inc)
static void test_dribbled_reads(void)
static void peer_headers(PEER *p, uint32_t sid, const N_H2_HEADER *h, size_t n, int end_stream)
static void test_send_flow_control(void)
unsigned char out[262144]
size_t out_len
static void test_callback_stop(void)
static void test_handshake_errors(void)
size_t in_off
static void test_post_body(void)
static ssize_t peer_write(void *io, const void *buf, size_t len)
size_t body_len
char body[4096]
static void test_guards(void)
static ssize_t peer_read(void *io, void *buf, size_t len)
int stop_after
size_t reply_len
int expect_send_fail
static void test_continuation(void)
static void test_rst_stream(void)
static void peer_prologue(PEER *p)
static void test_handshake(void)
size_t chunk
char scheme[16]
static void test_get(void)
char authority[128]
int reply_status
char path[256]
unsigned char * reply
int read_fail
static void test_ping_goaway(void)
static int out_find(const PEER *p, int type, size_t nth, N_H2_FRAME *out)
static void test_multiplex(void)
static size_t out_count(const PEER *p, int type, size_t *total_payload)
#define RUNT(f)
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_NULL
no log output
Definition n_log.h:70
#define LOG_INFO
informational
Definition n_log.h:106
size_t n_http2_hpack_encode(const N_H2_HEADER *headers, size_t n, unsigned char *out, size_t out_cap)
encode a header list into a block using literal representations without indexing and without Huffman ...
Definition n_http2.c:554
int n_http2_settings_parse(const unsigned char *payload, size_t len, N_H2_SETTING *out, size_t max, size_t *count)
parse a SETTINGS frame payload (6-byte id/value entries) into out.
Definition n_http2.c:70
int n_http2_frame_parse(const unsigned char *buf, size_t len, N_H2_FRAME *frame, size_t *consumed)
parse one HTTP/2 frame from a byte buffer (stateless).
Definition n_http2.c:32
size_t n_http2_frame_build_header(unsigned char *out, size_t out_cap, uint32_t length, int type, int flags, uint32_t stream_id)
write a 9-byte HTTP/2 frame header into out.
Definition n_http2.c:52
char * value
header field value (owned after decode)
Definition n_http2.h:125
#define N_H2_PREFACE
the client connection preface sent before any HTTP/2 frame (RFC 7540 3.5)
Definition n_http2.h:51
#define N_H2_FLAG_END_HEADERS
HEADERS/PUSH_PROMISE/CONTINUATION: header block complete.
Definition n_http2.h:75
#define N_H2_RST_STREAM
RST_STREAM.
Definition n_http2.h:62
#define N_H2_FRAME_HEADER_LEN
length of the fixed frame header in bytes
Definition n_http2.h:53
#define N_H2_FLAG_ACK
SETTINGS/PING: acknowledgement.
Definition n_http2.h:74
char * name
header field name (owned after decode)
Definition n_http2.h:124
#define N_H2_SETTINGS_INITIAL_WINDOW_SIZE
Definition n_http2.h:85
#define N_H2_WINDOW_UPDATE
WINDOW_UPDATE.
Definition n_http2.h:67
#define N_H2_DATA
DATA.
Definition n_http2.h:59
uint32_t length
payload length (24-bit)
Definition n_http2.h:92
#define N_H2_CONTINUATION
CONTINUATION.
Definition n_http2.h:68
const unsigned char * payload
pointer into the input buffer (may be NULL when length is 0)
Definition n_http2.h:96
int type
frame type (N_H2_*)
Definition n_http2.h:93
int flags
frame flags
Definition n_http2.h:94
#define N_H2_SETTINGS
SETTINGS.
Definition n_http2.h:63
uint32_t stream_id
stream identifier (31-bit; the reserved bit is cleared)
Definition n_http2.h:95
#define N_H2_HEADERS
HEADERS.
Definition n_http2.h:60
#define N_H2_FLAG_END_STREAM
DATA/HEADERS: last frame of the stream.
Definition n_http2.h:73
#define N_H2_GOAWAY
GOAWAY.
Definition n_http2.h:66
#define N_H2_PING
PING.
Definition n_http2.h:65
a parsed HTTP/2 frame; payload aliases the input buffer
Definition n_http2.h:91
one HPACK header field; after a decode both strings are owned (heap)
Definition n_http2.h:123
one SETTINGS parameter (identifier and value)
Definition n_http2.h:100
int n_http2_server_respond(N_H2_SERVER *s, uint32_t stream_id, const N_H2_HEADER *headers, size_t nheaders, const unsigned char *body, size_t body_len)
write a complete response on a stream (HEADERS then DATA, END_STREAM)
int n_http2_server_run(N_H2_SERVER *s, n_h2_srv_request_cb cb, void *user_data)
serve the connection until it ends
N_H2_SERVER * n_http2_server_new(void *io, n_h2_srv_read_fn rd, n_h2_srv_write_fn wr)
create a server connection over the given I/O callbacks
void n_http2_server_free(N_H2_SERVER **srv)
free a server connection and NULL the caller's pointer
const char * n_http2_server_error(const N_H2_SERVER *s)
the reason the connection failed, or NULL when none was recorded
size_t n_http2_server_request_count(const N_H2_SERVER *s)
the number of requests delivered to the callback so far
int n_http2_server_handshake(N_H2_SERVER *s)
read and validate the client connection preface, then send ours
server connection state
HTTP/2 (RFC 7540) server-side connection and stream state machine.
const char * authority
the :authority pseudo-header, or NULL when absent
uint32_t stream_id
the stream the request arrived on
const char * method
the :method pseudo-header, or NULL when absent
size_t body_len
the body length in bytes
#define N_H2_ERR_CANCEL
stream cancelled
const char * scheme
the :scheme pseudo-header, or NULL when absent
const char * path
the :path pseudo-header, or NULL when absent
const unsigned char * body
the request body, or NULL when there is none
one complete request received on a stream; every pointer is owned by the server and valid only for th...
Generic log system.