2 ex__http__server_8c
gullradriel edited this page 2026-11-01 16:37:14 +01:00

examples/ex_http_server.c

Structures

Name
struct runner
server thread state
struct reply
a parsed response as the client saw it
struct client
a test client connection, plain or TLS

Functions

Name
int write_pem(const N_STR * pem, char * path)
write a PEM string to a temp file
N_HTTP_RESP * text(int status, const char * body)
text response
void test_tls(void )
the TLS server
void test_plain(void )
the plain HTTP server
void test_capacity(void )
the connection cap, on a server whose deadlines are long enough for the held connections to stay open however slow the machine is
N_HTTP_SERVER * start(N_HTTP_SERVER_CONFIG * cfg, char * port, int base)
start a server on the first free port from base
int slow_pieces(void * user, N_HTTP_STREAM * out)
producer of /stream: five pieces 300 ms apart, longer in all than the write deadline, which counts per piece
void * run_server(void * p)
run the server
int read_reply(client * c, int head_only, reply * r)
read one response
void process_args(int argc, char ** argv)
read -V LOG_LEVEL
int main(int argc, char ** argv)
run the checks
N_HTTP_RESP * handler(N_HTTP_SERVER_REQUEST * req, void * user)
the application under test
void expect_true(const char * what, int ok)
count a failed check
int closed_by_server(client * c, int ms)
has the server closed the connection (within ms)?
int cl_tls(client * c, SSL_CTX * ctx)
upgrade a client connection to TLS
void cl_sends(client * c, const char * s)
send a NUL-terminated string
void cl_send(client * c, const char * s, size_t n)
send all bytes
long cl_recv(client * c, char * b, size_t n)
receive some bytes (3 s socket timeout)
int cl_connect(const char * port, client * c)
connect to the server
void cl_close(client * c)
close a client connection

Attributes

Name
char[200000] pending
leftover bytes between responses on one connection
size_t npending
number of leftover bytes
int failures
failed checks

Functions Documentation

function write_pem

static int write_pem(
    const N_STR * pem,
    char * path
)

write a PEM string to a temp file

Parameters:

  • pem the PEM
  • path template, replaced by the file name

Return: 1 on success

function text

static N_HTTP_RESP * text(
    int status,
    const char * body
)

text response

Parameters:

  • status status code
  • text body

Return: the response

function test_tls

static void test_tls(
    void 
)

the TLS server

function test_plain

static void test_plain(
    void 
)

the plain HTTP server

function test_capacity

static void test_capacity(
    void 
)

the connection cap, on a server whose deadlines are long enough for the held connections to stay open however slow the machine is

function start

static N_HTTP_SERVER * start(
    N_HTTP_SERVER_CONFIG * cfg,
    char * port,
    int base
)

start a server on the first free port from base

Parameters:

  • cfg settings (port is filled)
  • port buffer for the port
  • base first port to try

Return: the server or NULL

function slow_pieces

static int slow_pieces(
    void * user,
    N_HTTP_STREAM * out
)

producer of /stream: five pieces 300 ms apart, longer in all than the write deadline, which counts per piece

Parameters:

  • user unused
  • out the stream

Return: TRUE when every piece was written

function run_server

static void * run_server(
    void * p
)

run the server

Parameters:

  • p the runner

Return: NULL

function read_reply

static int read_reply(
    client * c,
    int head_only,
    reply * r
)

read one response

Parameters:

  • c the connection
  • head_only response to a HEAD request (no body)
  • r receives it

Return: the status, 0 when none arrived

function process_args

static void process_args(
    int argc,
    char ** argv
)

read -V LOG_LEVEL

Parameters:

  • argc argument count
  • argv arguments

function main

int main(
    int argc,
    char ** argv
)

run the checks

Parameters:

  • argc argument count
  • argv arguments

Return: 0 when every check passes

function handler

static N_HTTP_RESP * handler(
    N_HTTP_SERVER_REQUEST * req,
    void * user
)

the application under test

Parameters:

  • req the request
  • user unused

Return: the response, NULL for /null

function expect_true

static void expect_true(
    const char * what,
    int ok
)

count a failed check

Parameters:

  • what the check
  • ok its result

function closed_by_server

static int closed_by_server(
    client * c,
    int ms
)

has the server closed the connection (within ms)?

Parameters:

  • c the connection
  • ms how long to wait

Return: 1 when closed

function cl_tls

static int cl_tls(
    client * c,
    SSL_CTX * ctx
)

upgrade a client connection to TLS

Parameters:

  • c the connection
  • ctx client context

Return: 1 on a completed handshake

function cl_sends

static void cl_sends(
    client * c,
    const char * s
)

send a NUL-terminated string

Parameters:

  • c the connection
  • s the string

function cl_send

static void cl_send(
    client * c,
    const char * s,
    size_t n
)

send all bytes

Parameters:

  • c the connection
  • s bytes
  • n how many

function cl_recv

static long cl_recv(
    client * c,
    char * b,
    size_t n
)

receive some bytes (3 s socket timeout)

Parameters:

  • c the connection
  • b buffer
  • n its size

Return: bytes, 0 on close, -1 on timeout/error

function cl_connect

static int cl_connect(
    const char * port,
    client * c
)

connect to the server

Parameters:

  • port the port
  • c receives the connection

Return: 1 on success

function cl_close

static void cl_close(
    client * c
)

close a client connection

Parameters:

  • c the connection

Attributes Documentation

variable pending

static char[200000] pending;

leftover bytes between responses on one connection

variable npending

static size_t npending = 0;

number of leftover bytes

variable failures

static int failures = 0;

failed checks

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_common.h"
#include "nilorea/n_http_response.h"
#include "nilorea/n_http_server.h"
#include "nilorea/n_log.h"
#include "nilorea/n_network.h"
#include "nilorea/n_str.h"
#ifdef HAVE_OPENSSL
#include "nilorea/n_x509.h"
#include <openssl/err.h>
#include <openssl/ssl.h>
#endif

#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

static int failures = 0;

static void expect_true(const char* what, int ok) {
    if (!ok) {
        n_log(LOG_ERR, "FAIL: %s", what);
        failures++;
    } else {
        n_log(LOG_INFO, "ok: %s", what);
    }
}

static void process_args(int argc, char** argv) {
    int opt = 0;
    while ((opt = getopt(argc, argv, "V:")) != EOF) {
        if (opt == '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);
        }
    }
}

static N_HTTP_RESP* text(int status, const char* body) {
    N_HTTP_RESP* r = n_http_response_new(status);
    n_http_response_set_header(r, "Content-Type", "text/plain");
    n_http_response_append(r, body, strlen(body));
    return r;
}

static int slow_pieces(void* user, N_HTTP_STREAM* out) {
    (void)user;
    for (int i = 0; i < 5; i++) {
        char piece[32];
        struct timespec ts = {0, 300 * 1000000L};
        nanosleep(&ts, NULL);
        snprintf(piece, sizeof(piece), "data: %d\n\n", i);
        if (!n_http_stream_write(out, piece, strlen(piece)))
            return FALSE;
    }
    return TRUE;
}

static N_HTTP_RESP* handler(N_HTTP_SERVER_REQUEST* req, void* user) {
    (void)user;
    if (strcmp(req->path, "/hello") == 0)
        return text(200, "hello");
    if (strcmp(req->path, "/null") == 0)
        return NULL;
    if (strcmp(req->path, "/info") == 0) {
        char b[256];
        snprintf(b, sizeof(b), "path=%s query=%s tls=%d peer=%s", req->path, req->query, req->tls, req->peer_ip);
        return text(200, b);
    }
    if (strcmp(req->path, "/echo") == 0) {
        N_STR* body = NULL;
        N_HTTP_RESP* r;
        if (!n_http_server_read_body(req, 1024, &body)) {
            free_nstr(&body);
            return text(req->body_status, "body refused");
        }
        r = n_http_response_new(200);
        n_http_response_append(r, body->data, body->written);
        free_nstr(&body);
        return r;
    }
    if (strcmp(req->path, "/file") == 0) {
        char path[] = "ex_http_server_body_XXXXXX";
        int fd = mkstemp(path);
        char b[64];
        int ok = fd >= 0 && n_http_server_read_body_to_fd(req, 65536, fd);
        long long size = fd >= 0 ? (long long)lseek(fd, 0, SEEK_END) : -1;
        if (fd >= 0) {
            close(fd);
            unlink(path);
        }
        if (!ok)
            return text(req->body_status ? req->body_status : 500, "file refused");
        snprintf(b, sizeof(b), "stored %lld", size);
        return text(200, b);
    }
    if (strcmp(req->path, "/nobody") == 0)
        return text(200, "ignored");
    if (strcmp(req->path, "/stream") == 0) {
        N_HTTP_RESP* r = n_http_response_new(200);
        n_http_response_set_header(r, "Content-Type", "text/event-stream");
        n_http_response_stream(r, slow_pieces, NULL, NULL);
        return r;
    }
    return text(404, "not found");
}

typedef struct {
    NETWORK* netw; 
#ifdef HAVE_OPENSSL
    SSL* ssl; 
#endif
} client;

static int cl_connect(const char* port, client* c) {
    memset(c, 0, sizeof(*c));
    if (netw_connect(&c->netw, "127.0.0.1", (char*)port, NETWORK_IPALL) != TRUE || !c->netw)
        return 0;
    netw_setsockopt(c->netw, SO_RCVTIMEO, 3000);
    return 1;
}

#ifdef HAVE_OPENSSL
static int cl_tls(client* c, SSL_CTX* ctx) {
    c->ssl = SSL_new(ctx);
    SSL_set_fd(c->ssl, (int)c->netw->link.sock);
    SSL_set_tlsext_host_name(c->ssl, "localhost");
    return SSL_connect(c->ssl) == 1;
}
#endif

static void cl_send(client* c, const char* s, size_t n) {
#ifdef HAVE_OPENSSL
    if (c->ssl) {
        SSL_write(c->ssl, s, (int)n);
        return;
    }
#endif
    while (n > 0) {
        ssize_t w = send(c->netw->link.sock, s, n, MSG_NOSIGNAL);
        if (w <= 0)
            return;
        s += w;
        n -= (size_t)w;
    }
}

static void cl_sends(client* c, const char* s) {
    cl_send(c, s, strlen(s));
}

static long cl_recv(client* c, char* b, size_t n) {
#ifdef HAVE_OPENSSL
    if (c->ssl) {
        int r = SSL_read(c->ssl, b, (int)n);
        if (r > 0)
            return r;
        r = SSL_get_error(c->ssl, r);
        ERR_clear_error();
        return r == SSL_ERROR_ZERO_RETURN || r == SSL_ERROR_SYSCALL ? 0 : -1;
    }
#endif
    return (long)recv(c->netw->link.sock, b, n, 0);
}

static void cl_close(client* c) {
    if (!c->netw)
        return;
#ifdef HAVE_OPENSSL
    if (c->ssl) {
        SSL_shutdown(c->ssl);
        SSL_free(c->ssl);
        c->ssl = NULL;
        ERR_clear_error();
    }
#endif
    netw_close(&c->netw);
}

typedef struct {
    int status;       
    char head[4096];  
    char body[70000]; 
    long body_len;    
} reply;

static char pending[200000];
static size_t npending = 0;

static int read_reply(client* c, int head_only, reply* r) {
    const char* end = NULL;
    memset(r, 0, sizeof(*r));
    while (!(end = strstr(pending, "\r\n\r\n"))) {
        long n;
        if (npending >= sizeof(pending) - 1)
            return 0;
        n = cl_recv(c, pending + npending, sizeof(pending) - 1 - npending);
        if (n <= 0)
            return 0;
        npending += (size_t)n;
        pending[npending] = '\0';
    }
    {
        size_t hl = (size_t)(end - pending) + 4;
        long clen = 0;
        const char* cl;
        memcpy(r->head, pending, hl < sizeof(r->head) - 1 ? hl : sizeof(r->head) - 1);
        sscanf(pending, "HTTP/1.1 %d", &r->status);
        cl = strstr(r->head, "Content-Length: ");
        if (cl)
            clen = atol(cl + 16);
        if (head_only || r->status == 100 || r->status == 204 || r->status == 304)
            clen = 0;
        while ((long)(npending - hl) < clen) {
            long n = cl_recv(c, pending + npending, sizeof(pending) - 1 - npending);
            if (n <= 0)
                return 0;
            npending += (size_t)n;
            pending[npending] = '\0';
        }
        memcpy(r->body, pending + hl, (size_t)clen);
        r->body[clen] = '\0';
        r->body_len = clen;
        memmove(pending, pending + hl + clen, npending - hl - (size_t)clen + 1);
        npending -= hl + (size_t)clen;
    }
    return r->status;
}

static int closed_by_server(client* c, int ms) {
    char b[64];
    struct pollfd p;
    p.fd = (int)c->netw->link.sock;
    p.events = POLLIN;
    if (npending > 0)
        return 0;
#ifdef HAVE_OPENSSL
    if (c->ssl && SSL_pending(c->ssl) > 0)
        return 0;
#endif
    if (poll(&p, 1, ms) <= 0)
        return 0;
    return cl_recv(c, b, sizeof(b)) == 0;
}

typedef struct {
    N_HTTP_SERVER* srv; 
    int ok;             
} runner;

static void* run_server(void* p) {
    runner* r = (runner*)p;
    r->ok = n_http_server_run(r->srv);
    return NULL;
}

static N_HTTP_SERVER* start(N_HTTP_SERVER_CONFIG* cfg, char* port, int base) {
    int p;
    for (p = base; p < base + 100; p++) {
        N_HTTP_SERVER* s;
        snprintf(port, 16, "%d", p);
        cfg->port = port;
        s = n_http_server_new(cfg);
        if (s)
            return s;
    }
    return NULL;
}

static void test_plain(void) {
    N_HTTP_SERVER_CONFIG cfg;
    N_HTTP_SERVER* srv;
    N_HTTP_SERVER_STATS st;
    runner run;
    pthread_t th;
    char port[16];
    client c;
    static reply r;
    long long t0;

    n_http_server_config_defaults(&cfg);
    cfg.handler = handler;
    cfg.workers = 2;
    cfg.header_timeout_ms = 300;
    cfg.idle_timeout_ms = 300;
    cfg.body_timeout_ms = 300;
    cfg.write_timeout_ms = 1000;
    cfg.max_body = 100000;
    srv = start(&cfg, port, 18600);
    expect_true("server bound", srv != NULL);
    if (!srv)
        return;
    run.srv = srv;
    run.ok = 0;
    pthread_create(&th, NULL, run_server, &run);

    /* keep-alive, pipelining, HEAD */
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("GET 200", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "hello") == 0);
        expect_true("keep-alive: no Connection: close", strstr(r.head, "Connection: close") == NULL);
        expect_true("Date sent", strstr(r.head, "\r\nDate: ") != NULL);
        cl_sends(&c, "GET /info?a=1&b=2 HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("second request on the same connection", read_reply(&c, 0, &r) == 200 && strstr(r.body, "path=/info query=a=1&b=2 tls=0 peer=127.0.0.1"));
        cl_sends(&c, "HEAD /hello HTTP/1.1\r\nHost: t\r\n\r\nGET /hello HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("pipelined HEAD: length, no body", read_reply(&c, 1, &r) == 200 && strstr(r.head, "Content-Length: 5\r\n") && r.body_len == 0);
        expect_true("pipelined GET after it", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "hello") == 0);
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 3\r\n\r\nabc");
        expect_true("POST body echoed", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "abc") == 0);
        cl_sends(&c, "GET /null HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("NULL response: 500", read_reply(&c, 0, &r) == 500);
        cl_sends(&c, "GET /hello HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n");
        expect_true("Connection: close honoured", read_reply(&c, 0, &r) == 200 && strstr(r.head, "Connection: close") && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    /* a streamed body longer than the write deadline, then the connection is reused */
    if (cl_connect(port, &c)) {
        const char* end = NULL;
        npending = 0;
        pending[0] = '\0';
        cl_sends(&c, "GET /stream HTTP/1.1\r\nHost: t\r\n\r\n");
        while (!(end = strstr(pending, "\r\n0\r\n\r\n"))) {
            long n = npending < sizeof(pending) - 1 ? cl_recv(&c, pending + npending, sizeof(pending) - 1 - npending) : 0;
            if (n <= 0)
                break;
            npending += (size_t)n;
            pending[npending] = '\0';
        }
        expect_true("streamed: chunked head", strstr(pending, "Transfer-Encoding: chunked\r\n") && !strstr(pending, "Content-Length"));
        expect_true("streamed: every piece in its chunk, then the last chunk",
                    end && strstr(pending, "9\r\ndata: 0\n\n\r\n") && strstr(pending, "9\r\ndata: 4\n\n\r\n0\r\n\r\n"));
        if (end) {
            size_t used = (size_t)(end - pending) + 7;
            memmove(pending, pending + used, npending - used + 1);
            npending -= used;
        }
        cl_sends(&c, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("streamed: the connection is kept", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "hello") == 0);
        cl_close(&c);
    }
    /* idle timeout after a keep-alive response */
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n");
        read_reply(&c, 0, &r);
        t0 = (long long)time(NULL);
        expect_true("idle connection closed after the idle timeout", closed_by_server(&c, 2000) && (long long)time(NULL) - t0 <= 2);
        cl_close(&c);
    }
    /* HTTP/1.0 */
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "GET /hello HTTP/1.0\r\n\r\n");
        expect_true("HTTP/1.0 without keep-alive closes", read_reply(&c, 0, &r) == 200 && strstr(r.head, "Connection: close") && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    /* bodies */
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 2000\r\n\r\n");
        expect_true("body over the handler's cap: 413 and close", read_reply(&c, 0, &r) == 413 && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 200000\r\n\r\n");
        expect_true("body over max_body: 413 before the handler", read_reply(&c, 0, &r) == 413 && strstr(r.body, "413") && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        static char big[60000 + 128];
        size_t i, hl;
        npending = 0;
        hl = (size_t)snprintf(big, sizeof(big), "POST /file HTTP/1.1\r\nHost: t\r\nContent-Length: 60000\r\n\r\n");
        for (i = 0; i < 60000; i++)
            big[hl + i] = (char)('a' + i % 26);
        cl_send(&c, big, hl + 60000);
        expect_true("60000-byte body stored in a file", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "stored 60000") == 0);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /nobody HTTP/1.1\r\nHost: t\r\nContent-Length: 4\r\n\r\nabcd");
        expect_true("unread body: answered, then closed", read_reply(&c, 0, &r) == 200 && strstr(r.head, "Connection: close") && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 10\r\n\r\nabc");
        expect_true("body cut short: 408 after the body deadline", read_reply(&c, 0, &r) == 408);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n");
        expect_true("100 Continue sent when the body is read", read_reply(&c, 0, &r) == 100);
        cl_sends(&c, "hello");
        expect_true("then the final response", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "hello") == 0);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 5\r\nExpect: something-else\r\n\r\n");
        expect_true("unknown Expect: 417", read_reply(&c, 0, &r) == 417);
        cl_close(&c);
    }
    /* bad requests */
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "BREW /pot HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("unknown method: 501 and close", read_reply(&c, 0, &r) == 501 && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "TRACE / HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("TRACE: 501", read_reply(&c, 0, &r) == 501);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 1073742300\r\n\r\n");
        expect_true("Content-Length above every cap: 413", read_reply(&c, 0, &r) == 413);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\ncontent-length:  99999999999999999999999 \r\n\r\n");
        expect_true("Content-Length of 23 digits: 413", read_reply(&c, 0, &r) == 413);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 1073742300\r\nContent-Length: 1073742300\r\n\r\n");
        expect_true("two huge Content-Length headers: 400", read_reply(&c, 0, &r) == 400);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 12x\r\n\r\n");
        expect_true("malformed Content-Length: 400", read_reply(&c, 0, &r) == 400);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "get / HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("lowercase method: 400", read_reply(&c, 0, &r) == 400);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "GET\t/ HTTP/1.1\r\nHost: t\r\n\r\n");
        expect_true("malformed request line: 400", read_reply(&c, 0, &r) == 400);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "GET /hello HTTP/1.1\r\n\r\n");
        expect_true("HTTP/1.1 without Host: 400", read_reply(&c, 0, &r) == 400);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        static char huge[20000];
        npending = 0;
        memset(huge, 'a', sizeof(huge));
        memcpy(huge, "GET /hello HTTP/1.1\r\nHost: t\r\nX-Big: ", 37);
        cl_send(&c, huge, sizeof(huge));
        expect_true("head over 16 KiB: 431", read_reply(&c, 0, &r) == 431);
        cl_close(&c);
    }
    /* slow peers */
    if (cl_connect(port, &c)) {
        npending = 0;
        cl_sends(&c, "GET /hello HTTP/1.1\r\nHo");
        expect_true("head not finished in time: 408", read_reply(&c, 0, &r) == 408 && closed_by_server(&c, 1000));
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        int i;
        struct timespec ts0, ts1;
        long elapsed;
        struct pollfd p;
        npending = 0;
        p.fd = (int)c.netw->link.sock;
        p.events = POLLIN;
        clock_gettime(CLOCK_MONOTONIC, &ts0);
        /* one byte every 100 ms for 600 ms: every single read is quick, so
         * only a TOTAL head deadline (300 ms) stops it; stop sending as soon
         * as the answer is readable, without reading it */
        for (i = 0; i < 6; i++) {
            if (poll(&p, 1, 0) > 0)
                break;
            cl_send(&c, "G", 1);
            usleep(100000);
        }
        expect_true("byte-at-a-time head: 408", read_reply(&c, 0, &r) == 408);
        clock_gettime(CLOCK_MONOTONIC, &ts1);
        elapsed = (ts1.tv_sec - ts0.tv_sec) * 1000 + (ts1.tv_nsec - ts0.tv_nsec) / 1000000;
        expect_true("byte-at-a-time head: stopped by the total deadline, not per read", elapsed < 550);
        cl_close(&c);
    }
    if (cl_connect(port, &c)) {
        npending = 0;
        expect_true("silent connection closed after the header timeout", closed_by_server(&c, 2000));
        cl_close(&c);
    }
    n_http_server_stop(srv);
    t0 = (long long)time(NULL);
    pthread_join(th, NULL);
    expect_true("run() returns after stop", run.ok == TRUE && (long long)time(NULL) - t0 <= 2);
    n_http_server_stats(srv, &st);
    n_log(LOG_INFO, "stats: accepted %llu refused %llu active %llu requests %llu timeouts %llu bad %llu", st.accepted, st.refused, st.active, st.requests, st.timeouts, st.bad_requests);
    expect_true("stats", st.requests >= 10 && st.timeouts >= 3 && st.bad_requests >= 4 && st.active == 0);
    n_http_server_free(&srv);
    expect_true("free sets NULL", srv == NULL);
}

static void test_capacity(void) {
    N_HTTP_SERVER_CONFIG cfg;
    N_HTTP_SERVER* srv;
    runner run;
    pthread_t th;
    char port[16];
    client c, c2, c3, c4, c5;
    static reply r;

    n_http_server_config_defaults(&cfg);
    cfg.handler = handler;
    cfg.workers = 2;
    cfg.header_timeout_ms = 5000;
    srv = start(&cfg, port, 18800);
    expect_true("capacity server bound", srv != NULL);
    if (!srv)
        return;
    run.srv = srv;
    run.ok = 0;
    pthread_create(&th, NULL, run_server, &run);
    /* 2 workers hold c and c2, c3 and c4 fill the queue of 2, c5 is refused */
    memset(&c3, 0, sizeof(c3));
    memset(&c4, 0, sizeof(c4));
    /* let the workers take c and c2 before the queue is filled */
    if (cl_connect(port, &c) && cl_connect(port, &c2)) {
        usleep(300000);
        if (cl_connect(port, &c3) && cl_connect(port, &c4)) {
            usleep(300000);
            npending = 0;
            if (cl_connect(port, &c5)) {
                expect_true("over capacity: 503", read_reply(&c5, 0, &r) == 503);
                cl_close(&c5);
            }
        }
    }
    cl_close(&c4);
    cl_close(&c3);
    cl_close(&c2);
    cl_close(&c);
    n_http_server_stop(srv);
    pthread_join(th, NULL);
    expect_true("capacity server stopped while connections were held", run.ok == TRUE);
    n_http_server_free(&srv);
}

#ifdef HAVE_OPENSSL
static int write_pem(const N_STR* pem, char* path) {
    int fd = mkstemp(path);
    int ok = fd >= 0 && write(fd, pem->data, pem->written) == (ssize_t)pem->written;
    if (fd >= 0)
        close(fd);
    return ok;
}

static void test_tls(void) {
    N_STR *ca_cert = NULL, *ca_key = NULL, *cert = NULL, *key = NULL;
    char cert_path[] = "ex_http_server_cert_XXXXXX", key_path[] = "ex_http_server_key_XXXXXX";
    N_HTTP_SERVER_CONFIG cfg;
    N_HTTP_SERVER* srv;
    N_HTTP_SERVER_STATS st;
    runner run;
    pthread_t th;
    char port[16];
    client c;
    static reply r;

    if (n_x509_generate_ca("ex_http_server CA", 2, &ca_cert, &ca_key) != 0 ||
        n_x509_mint_host_cert("localhost", ca_cert, ca_key, 2, &cert, &key) != 0 || !write_pem(cert, cert_path) || !write_pem(key, key_path)) {
        expect_true("test certificate", 0);
        return;
    }
    n_http_server_config_defaults(&cfg);
    cfg.handler = handler;
    cfg.workers = 4;
    cfg.handshake_timeout_ms = 300;
    cfg.tls_key_file = key_path;
    cfg.tls_cert_file = cert_path;
    srv = start(&cfg, port, 18700);
    expect_true("TLS server bound", srv != NULL);
    cfg.tls_cert_file = NULL;
    expect_true("key without certificate refused", n_http_server_new(&cfg) == NULL);
    if (srv) {
        run.srv = srv;
        run.ok = 0;
        pthread_create(&th, NULL, run_server, &run);
        SSL_CTX* cctx = SSL_CTX_new(TLS_client_method());
        SSL_CTX_set_verify(cctx, SSL_VERIFY_NONE, NULL);
        if (cl_connect(port, &c) && cl_tls(&c, cctx)) {
            npending = 0;
            expect_true("TLS 1.3 negotiated", strcmp(SSL_get_version(c.ssl), "TLSv1.3") == 0);
            cl_sends(&c, "GET /info HTTP/1.1\r\nHost: t\r\n\r\n");
            expect_true("GET over TLS", read_reply(&c, 0, &r) == 200 && strstr(r.body, "tls=1"));
            cl_sends(&c, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n");
            expect_true("keep-alive over TLS", read_reply(&c, 0, &r) == 200 && strcmp(r.body, "hello") == 0);
        } else {
            expect_true("TLS client handshake", 0);
        }
        cl_close(&c);
        if (cl_connect(port, &c)) {
            npending = 0;
            expect_true("no handshake: closed after the handshake timeout", closed_by_server(&c, 2000));
            cl_close(&c);
        }
        if (cl_connect(port, &c)) {
            npending = 0;
            cl_sends(&c, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n");
            expect_true("plain HTTP on the TLS port: closed", read_reply(&c, 0, &r) == 0);
            cl_close(&c);
        }
        SSL_CTX_free(cctx);
        n_http_server_stop(srv);
        pthread_join(th, NULL);
        n_http_server_stats(srv, &st);
        expect_true("TLS failures counted", st.tls_failures >= 2);
        n_http_server_free(&srv);
    }
    unlink(cert_path);
    unlink(key_path);
    free_nstr(&ca_cert);
    free_nstr(&ca_key);
    free_nstr(&cert);
    free_nstr(&key);
}
#endif

int main(int argc, char** argv) {
    N_HTTP_SERVER_CONFIG bad;
    set_log_level(LOG_NOTICE);
    process_args(argc, argv);
#ifdef HAVE_OPENSSL
    netw_init_openssl();
#endif

    n_http_server_config_defaults(&bad);
    expect_true("config without handler refused", n_http_server_new(&bad) == NULL);
    bad.handler = handler;
    bad.workers = 0;
    expect_true("config without workers refused", n_http_server_new(&bad) == NULL);
    n_http_server_stop(NULL);
    n_http_server_free(NULL);

    test_plain();
    test_capacity();
#ifdef HAVE_OPENSSL
    test_tls();
    netw_unload_openssl();
#endif

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