Table of Contents
- examples/ex_http2_server.c
- Structures
- Functions
- Attributes
- Defines
- Functions Documentation
- function test_send_window_stall
- function test_send_flow_control
- function test_rst_stream
- function test_post_body
- function test_ping_goaway
- function test_multiplex
- function test_handshake_errors
- function test_handshake
- function test_guards
- function test_get
- function test_dribbled_reads
- function test_continuation
- function test_callback_stop
- function process_args
- function peer_write
- function peer_window_update
- function peer_read
- function peer_put
- function peer_prologue
- function peer_headers
- function peer_frame
- function out_find
- function out_count
- function on_request
- function main
- Attributes Documentation
- Macros Documentation
- Source code
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;
}
Modules
- AABB3D
- ACCEPT_POOL
- ALLEGRO4
- ALLEGRO5
- ANIM_LIB
- ASTAR
- ATLAS_LIB
- AVRO
- CLOCK_SYNC
- COMMONS
- CONFIGFILE
- CYPHER_BASE64
- CYPHER_VIGENERE
- DEAD_RECKONING
- ENUMS
- EXCEPTIONS
- GAME_ENV
- GIT
- GUI
- HASH_TABLE
- ISOMETRIC_ENGINE
- LIST
- LOG
- LOGNODUP
- LZ4
- NETWORKING
- NETWORK_MSG
- N_COOKIES
- N_DIFF
- N_DIGEST
- N_DISK_CACHE
- N_DNS
- N_ECC
- N_ENTROPY
- N_FILES
- N_FLUIDS
- N_GUI_COLOR
- N_GUI_COLORWHEEL
- N_GUI_CURVE
- N_GUI_GRID
- N_GUI_POPUP
- N_HEX
- N_HTTP3
- N_HTTP_COOKIE
- N_HTTP_FORM
- N_HTTP_MIME
- N_HTTP_REQ
- N_HTTP_RESP
- N_HTTP_ROUTER
- N_HTTP_SERVER
- N_ICC
- N_JSON
- N_KAFKA
- N_LINALG
- N_MODULE
- N_OPENSSL_COMPAT
- N_PRETTY
- N_PTH
- N_QUERY
- N_RANDOM
- N_SHA256
- N_STR
- N_TIFF
- N_TIME
- N_USER
- N_X509
- N_ZIP
- PARTICLES
- PCRE
- PHYSICS
- SIGNALS
- STACK
- THREADS
- TRAJECTORY
- TREE
- ZLIB