5 ex_network_alpn_8c example
gullradriel edited this page 2026-10-09 00:51:57 +02:00

ex_network_alpn.c

TLS ALPN negotiation, and a real HTTP/2 exchange over the "h2" it selects Three connections against one SNI-minting TLS server that offers "h2" then "http/1.1":

  1. a client offering both gets "h2", and the connection then carries a real HTTP/2 request answered by n_http2_server over the TLS socket;
  2. a client offering only "http/1.1" gets "http/1.1";
  3. a client offering nothing negotiates no protocol at all, and the handshake still succeeds. ```c

/*

  • 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;

}


_Filename: ex_network_alpn.c_