Table of Contents
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":
- 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;
- a client offering only "http/1.1" gets "http/1.1";
- 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_
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