6 ex__network__alpn_8c
gullradriel edited this page 2026-10-09 00:55:18 +02:00

examples/ex_network_alpn.c

Structures

Name
struct SRV
server worker context
struct CA_CTX
CA used by the server to mint per-host leaves.

Functions

Name
ssize_t tls_write(void * io, const void * buf, size_t len)
ssize_t tls_read(void * io, void * buf, size_t len)
void * server_fn(void * p)
void process_args(int argc, char ** argv)
int pick_cb(const char * sni, N_STR ** cert_pem, N_STR ** key_pem, void * user_data)
int main(int argc, char ** argv)
int h2_on_request(void * ud, N_H2_SERVER * srv, const N_H2_REQUEST * req)
int h2_client_get(NETWORK * c, const char * path, int * status, char * body, size_t body_cap)
NETWORK * client_connect(const char * port, const char *const * protos, size_t count, char * got, size_t got_len)
NETWORK * bind_free(char * port_out, size_t port_out_len)

Attributes

Name
int failures

Defines

Name
ROUNDS
how many connections the server accepts before it stops
H2_REPLY
the body the HTTP/2 handler answers with
ALPN_HOST
the host name the client requests via SNI

Functions Documentation

function tls_write

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

function tls_read

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

function server_fn

static void * server_fn(
    void * p
)

function process_args

void process_args(
    int argc,
    char ** argv
)

function pick_cb

static int pick_cb(
    const char * sni,
    N_STR ** cert_pem,
    N_STR ** key_pem,
    void * user_data
)

function main

int main(
    int argc,
    char ** argv
)

function h2_on_request

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

function h2_client_get

static int h2_client_get(
    NETWORK * c,
    const char * path,
    int * status,
    char * body,
    size_t body_cap
)

< SETTINGS

< HEADERS

< HEADERS/PUSH_PROMISE/CONTINUATION: header block complete

< DATA/HEADERS: last frame of the stream

< HEADERS

< DATA

< DATA/HEADERS: last frame of the stream

function client_connect

static NETWORK * client_connect(
    const char * port,
    const char *const * protos,
    size_t count,
    char * got,
    size_t got_len
)

function bind_free

static NETWORK * bind_free(
    char * port_out,
    size_t port_out_len
)

Attributes Documentation

variable failures

static int failures = 0;

Macros Documentation

define ROUNDS

#define ROUNDS 3

how many connections the server accepts before it stops

define H2_REPLY

#define H2_REPLY "served-over-h2"

the body the HTTP/2 handler answers with

define ALPN_HOST

#define ALPN_HOST "alpn.host.example"

the host name the client requests via SNI

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_http2_server.h"
#include "nilorea/n_log.h"
#include "nilorea/n_network.h"
#include "nilorea/n_str.h"
#include "nilorea/n_x509.h"

#include <getopt.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/ssl.h>

#define ALPN_HOST "alpn.host.example"
#define ROUNDS 3
#define H2_REPLY "served-over-h2"

static int failures = 0;

typedef struct CA_CTX {
    N_STR* cert; 
    N_STR* key;  
} CA_CTX;

typedef struct SRV {
    NETWORK* listen;           
    CA_CTX* ca;                
    char selected[ROUNDS][32]; 
    int rounds_done;           
    int h2_requests;           
    char h2_path[128];         
} SRV;

/* Parse the -V LOG_LEVEL verbosity argument. */
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_network_alpn\n");
                exit(1);
            case 'h':
            default:
                fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
                break;
        }
    }
}

/* SNI pick callback: mint a leaf for the requested host signed by the CA. */
static int pick_cb(const char* sni, N_STR** cert_pem, N_STR** key_pem, void* user_data) {
    CA_CTX* ca = (CA_CTX*)user_data;
    const char* host = (sni && sni[0]) ? sni : "localhost";
    return n_x509_mint_host_cert(host, ca->cert, ca->key, 825, cert_pem, key_pem);
}

/* ------------------------------------------------------------------ */
/* byte I/O over a TLS NETWORK, for the HTTP/2 state machine           */
/* ------------------------------------------------------------------ */

static ssize_t tls_read(void* io, void* buf, size_t len) {
    NETWORK* n = (NETWORK*)io;
    int r = SSL_read(n->ssl, buf, (int)len);
    return (r < 0) ? -1 : (ssize_t)r;
}

static ssize_t tls_write(void* io, const void* buf, size_t len) {
    NETWORK* n = (NETWORK*)io;
    size_t off = 0;
    while (off < len) {
        int w = SSL_write(n->ssl, (const char*)buf + off, (int)(len - off));
        if (w <= 0)
            return -1;
        off += (size_t)w;
    }
    return (ssize_t)len;
}

/* ------------------------------------------------------------------ */
/* the server                                                         */
/* ------------------------------------------------------------------ */

/* Answer every HTTP/2 request with a small text body. */
static int h2_on_request(void* ud, N_H2_SERVER* srv, const N_H2_REQUEST* req) {
    SRV* s = (SRV*)ud;
    N_H2_HEADER h[2];
    s->h2_requests++;
    snprintf(s->h2_path, sizeof(s->h2_path), "%s", req->path ? req->path : "");
    h[0].name = (char*)":status";
    h[0].value = (char*)"200";
    h[1].name = (char*)"content-type";
    h[1].value = (char*)"text/plain";
    if (n_http2_server_respond(srv, req->stream_id, h, 2, (const unsigned char*)H2_REPLY, strlen(H2_REPLY)) != 0)
        failures++;
    return 0;
}

/* Accept ROUNDS connections, negotiating ALPN on each. */
static void* server_fn(void* p) {
    static const char* const protos[] = {"h2", "http/1.1"};
    SRV* s = (SRV*)p;
    int round;
    for (round = 0; round < ROUNDS; round++) {
        NETWORK* c = netw_accept_from(s->listen);
        if (!c)
            break;
        if (netw_ssl_server_handshake_ex(c, pick_cb, s->ca, protos, 2) != TRUE) {
            n_log(LOG_ERR, "server TLS handshake failed on round %d", round);
            failures++;
            netw_close(&c);
            continue;
        }
        if (netw_ssl_get_alpn(c, s->selected[round], sizeof(s->selected[round])) != TRUE)
            s->selected[round][0] = '\0';
        if (strcmp(s->selected[round], "h2") == 0) {
            N_H2_SERVER* h2 = n_http2_server_new(c, tls_read, tls_write);
            if (!h2 || n_http2_server_handshake(h2) != 0 || n_http2_server_run(h2, h2_on_request, s) != 0) {
                n_log(LOG_ERR, "HTTP/2 server failed: %s", _str(n_http2_server_error(h2)));
                failures++;
            }
            n_http2_server_free(&h2);
        } else {
            /* the non-h2 rounds only prove the handshake completed */
            char buf[16] = "";
            if (SSL_read(c->ssl, buf, 4) > 0)
                tls_write(c, "pong", 4);
        }
        s->rounds_done++;
        netw_close(&c);
    }
    return NULL;
}

/* ------------------------------------------------------------------ */
/* a minimal HTTP/2 client, enough to check the server answers         */
/* ------------------------------------------------------------------ */

/* Send one GET on stream 1 and read the response status and body. Returns 0 on
   success, -1 on a protocol or I/O error. */
static int h2_client_get(NETWORK* c, const char* path, int* status, char* body, size_t body_cap) {
    unsigned char block[512], frame[1024], rb[8192];
    N_H2_HEADER req[4];
    N_H2_HPACK* dec = n_http2_hpack_new(0);
    size_t w, rlen = 0, blen = 0;
    int done = 0, rc = -1;

    *status = 0;
    body[0] = '\0';
    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*)ALPN_HOST;
    req[3].name = (char*)":path";
    req[3].value = (char*)path;
    w = n_http2_hpack_encode(req, 4, block, sizeof(block));
    if (w == 0 || !dec)
        goto out;

    if (tls_write(c, N_H2_PREFACE, strlen(N_H2_PREFACE)) < 0)
        goto out;
    if (n_http2_frame_build_header(frame, sizeof(frame), 0, N_H2_SETTINGS, 0, 0) == 0)
        goto out;
    if (tls_write(c, frame, N_H2_FRAME_HEADER_LEN) < 0)
        goto out;
    if (n_http2_frame_build_header(frame, sizeof(frame), (uint32_t)w, N_H2_HEADERS, N_H2_FLAG_END_HEADERS | N_H2_FLAG_END_STREAM, 1) == 0)
        goto out;
    memcpy(frame + N_H2_FRAME_HEADER_LEN, block, w);
    if (tls_write(c, frame, N_H2_FRAME_HEADER_LEN + w) < 0)
        goto out;

    while (!done) {
        N_H2_FRAME f;
        size_t consumed = 0;
        int pr = (rlen > 0) ? n_http2_frame_parse(rb, rlen, &f, &consumed) : 0;
        if (pr < 0)
            goto out;
        if (pr == 0) {
            ssize_t n;
            if (rlen == sizeof(rb))
                goto out;
            n = tls_read(c, rb + rlen, sizeof(rb) - rlen);
            if (n <= 0)
                goto out;
            rlen += (size_t)n;
            continue;
        }
        if (f.type == N_H2_HEADERS && f.stream_id == 1) {
            N_H2_HEADER hdrs[32];
            size_t count = 0, i;
            if (n_http2_hpack_decode(dec, f.payload, f.length, hdrs, 32, &count) != 0)
                goto out;
            for (i = 0; i < count; i++)
                if (hdrs[i].name && strcmp(hdrs[i].name, ":status") == 0)
                    *status = atoi(hdrs[i].value ? hdrs[i].value : "0");
            n_http2_hpack_headers_free(hdrs, count);
        } else if (f.type == N_H2_DATA && f.stream_id == 1) {
            if (blen + f.length < body_cap) {
                memcpy(body + blen, f.payload, f.length);
                blen += f.length;
                body[blen] = '\0';
            }
        }
        if (f.stream_id == 1 && (f.flags & N_H2_FLAG_END_STREAM))
            done = 1;
        memmove(rb, rb + consumed, rlen - consumed);
        rlen -= consumed;
    }
    rc = 0;
out:
    n_http2_hpack_free(&dec);
    return rc;
}

/* ------------------------------------------------------------------ */
/* the client rounds                                                  */
/* ------------------------------------------------------------------ */

/* Connect, offer the given ALPN list (none when count is 0), and report what was
   negotiated in got[]. Returns the connected NETWORK, or NULL. */
static NETWORK* client_connect(const char* port, const char* const* protos, size_t count, char* got, size_t got_len) {
    NETWORK* cli = NULL;
    got[0] = '\0';
    if (netw_ssl_connect_client_to(&cli, "127.0.0.1", (char*)port, NETWORK_IPALL, 5000) != TRUE || !cli)
        return NULL;
    netw_ssl_set_verify(cli, 0);
    if (count > 0 && netw_ssl_set_alpn(cli, protos, count) != TRUE) {
        netw_close(&cli);
        return NULL;
    }
    if (netw_ssl_do_handshake(cli, (char*)ALPN_HOST) != TRUE) {
        netw_close(&cli);
        return NULL;
    }
    netw_ssl_get_alpn(cli, got, got_len);
    return cli;
}

/* Bind a loopback listening socket on the first free port in a range. */
static NETWORK* bind_free(char* port_out, size_t port_out_len) {
    int p;
    for (p = 18400; p < 18500; p++) {
        NETWORK* l = NULL;
        char ps[16];
        snprintf(ps, sizeof(ps), "%d", p);
        if (netw_make_listening(&l, "127.0.0.1", ps, 5, NETWORK_IPALL) == TRUE && l) {
            snprintf(port_out, port_out_len, "%s", ps);
            return l;
        }
        if (l)
            netw_close(&l);
    }
    return NULL;
}

int main(int argc, char** argv) {
    static const char* const both[] = {"h2", "http/1.1"};
    static const char* const only11[] = {"http/1.1"};
    CA_CTX ca = {NULL, NULL};
    NETWORK* listen = NULL;
    NETWORK* cli = NULL;
    SRV srv;
    pthread_t th;
    char port[16] = "";
    char got[32] = "";

    set_log_level(LOG_NOTICE);
    process_args(argc, argv);
    memset(&srv, 0, sizeof(srv));

    netw_init_openssl();
    if (n_x509_generate_ca("Nilorea ALPN Test CA", 3650, &ca.cert, &ca.key) != 0 || !ca.cert || !ca.key) {
        n_log(LOG_ERR, "CA generation failed");
        return 1;
    }
    listen = bind_free(port, sizeof(port));
    if (!listen) {
        n_log(LOG_ERR, "could not bind a loopback port");
        free_nstr(&ca.cert);
        free_nstr(&ca.key);
        return 1;
    }
    srv.listen = listen;
    srv.ca = &ca;
    pthread_create(&th, NULL, server_fn, &srv);

    /* round 1: both offered, h2 selected, and a real HTTP/2 exchange over it */
    cli = client_connect(port, both, 2, got, sizeof(got));
    if (!cli) {
        n_log(LOG_ERR, "round 1 connect failed");
        failures++;
    } else {
        int status = 0;
        char body[128] = "";
        if (strcmp(got, "h2") != 0) {
            n_log(LOG_ERR, "round 1 negotiated '%s', expected h2", got);
            failures++;
        }
        if (h2_client_get(cli, "/alpn", &status, body, sizeof(body)) != 0) {
            n_log(LOG_ERR, "round 1 HTTP/2 exchange failed");
            failures++;
        }
        if (status != 200 || strcmp(body, H2_REPLY) != 0) {
            n_log(LOG_ERR, "round 1 got status %d body '%s'", status, body);
            failures++;
        }
        netw_close(&cli);
    }

    /* round 2: the client speaks only HTTP/1.1, so that is what is selected */
    cli = client_connect(port, only11, 1, got, sizeof(got));
    if (!cli) {
        n_log(LOG_ERR, "round 2 connect failed");
        failures++;
    } else {
        char buf[8] = "";
        if (strcmp(got, "http/1.1") != 0) {
            n_log(LOG_ERR, "round 2 negotiated '%s', expected http/1.1", got);
            failures++;
        }
        tls_write(cli, "ping", 4);
        if (SSL_read(cli->ssl, buf, 4) <= 0) {
            n_log(LOG_ERR, "round 2 exchange failed");
            failures++;
        }
        netw_close(&cli);
    }

    /* round 3: no ALPN offered at all: nothing is negotiated, and the handshake
       still succeeds. A server that answered with a protocol here would break
       every client that does not speak ALPN. */
    cli = client_connect(port, NULL, 0, got, sizeof(got));
    if (!cli) {
        n_log(LOG_ERR, "round 3 connect failed");
        failures++;
    } else {
        char buf[8] = "";
        if (got[0] != '\0') {
            n_log(LOG_ERR, "round 3 negotiated '%s', expected nothing", got);
            failures++;
        }
        tls_write(cli, "ping", 4);
        if (SSL_read(cli->ssl, buf, 4) <= 0) {
            n_log(LOG_ERR, "round 3 exchange failed");
            failures++;
        }
        netw_close(&cli);
    }

    pthread_join(th, NULL);
    if (srv.rounds_done != ROUNDS) {
        n_log(LOG_ERR, "the server served %d of %d rounds", srv.rounds_done, ROUNDS);
        failures++;
    }
    if (srv.h2_requests != 1 || strcmp(srv.h2_path, "/alpn") != 0) {
        n_log(LOG_ERR, "the HTTP/2 handler saw %d request(s), path '%s'", srv.h2_requests, srv.h2_path);
        failures++;
    }
    if (strcmp(srv.selected[0], "h2") != 0 || strcmp(srv.selected[1], "http/1.1") != 0 || srv.selected[2][0] != '\0') {
        n_log(LOG_ERR, "the server side saw '%s', '%s', '%s'", srv.selected[0], srv.selected[1], srv.selected[2]);
        failures++;
    }

    /* guards */
    if (netw_ssl_set_alpn(NULL, both, 2) != FALSE) failures++;
    if (netw_ssl_get_alpn(NULL, got, sizeof(got)) != FALSE) failures++;
    if (netw_ssl_server_handshake_ex(NULL, pick_cb, &ca, both, 2) != FALSE) failures++;

    netw_close(&listen);
    free_nstr(&ca.cert);
    free_nstr(&ca.key);
    netw_unload_openssl();

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