Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_network_alpn.c

TLS ALPN negotiation, and a real HTTP/2 exchange over the "h2" it selects.

TLS ALPN negotiation, and a real HTTP/2 exchange over the "h2" it selects

Author
Castagnier Mickael
Version
1.0

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.
/*
* 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_log.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 {
typedef struct SRV {
char selected[ROUNDS][32];
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))
else if (!strncmp("LOG_NOTICE", optarg, 10))
else if (!strncmp("LOG_INFO", optarg, 8))
else if (!strncmp("LOG_ERR", optarg, 7))
else if (!strncmp("LOG_DEBUG", optarg, 9))
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;
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)
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++) {
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);
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) {
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)));
}
} 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);
}
}
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];
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) {
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");
} 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:
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;
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)
}
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] = "";
process_args(argc, argv);
memset(&srv, 0, sizeof(srv));
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.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");
} else {
int status = 0;
char body[128] = "";
if (strcmp(got, "h2") != 0) {
n_log(LOG_ERR, "round 1 negotiated '%s', expected h2", got);
}
if (h2_client_get(cli, "/alpn", &status, body, sizeof(body)) != 0) {
n_log(LOG_ERR, "round 1 HTTP/2 exchange failed");
}
if (status != 200 || strcmp(body, H2_REPLY) != 0) {
n_log(LOG_ERR, "round 1 got status %d body '%s'", status, body);
}
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");
} else {
char buf[8] = "";
if (strcmp(got, "http/1.1") != 0) {
n_log(LOG_ERR, "round 2 negotiated '%s', expected http/1.1", got);
}
tls_write(cli, "ping", 4);
if (SSL_read(cli->ssl, buf, 4) <= 0) {
n_log(LOG_ERR, "round 2 exchange failed");
}
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");
} else {
char buf[8] = "";
if (got[0] != '\0') {
n_log(LOG_ERR, "round 3 negotiated '%s', expected nothing", got);
}
tls_write(cli, "ping", 4);
if (SSL_read(cli->ssl, buf, 4) <= 0) {
n_log(LOG_ERR, "round 3 exchange failed");
}
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);
}
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);
}
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]);
}
/* 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.key);
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;
}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static int h2_client_get(NETWORK *c, const char *path, int *status, char *body, size_t body_cap)
static void * server_fn(void *p)
#define ROUNDS
how many connections the server accepts before it stops
#define ALPN_HOST
the host name the client requests via SNI
static int h2_on_request(void *ud, N_H2_SERVER *srv, const N_H2_REQUEST *req)
static NETWORK * bind_free(char *port_out, size_t port_out_len)
#define H2_REPLY
the body the HTTP/2 handler answers with
static int pick_cb(const char *sni, N_STR **cert_pem, N_STR **key_pem, void *user_data)
static NETWORK * client_connect(const char *port, const char *const *protos, size_t count, char *got, size_t got_len)
static ssize_t tls_read(void *io, void *buf, size_t len)
static ssize_t tls_write(void *io, const void *buf, size_t len)
N_STR * key
the CA private key PEM
N_STR * cert
the CA certificate PEM
CA used by the server to mint per-host leaves.
volatile sig_atomic_t done
char * cert
char * port
NETWORK * listen
the listening socket
char h2_path[128]
the :path of the last HTTP/2 request
int rounds_done
how many connections it served
int h2_requests
requests served over HTTP/2
char selected[3][32]
what ALPN the server answered per round
CA_CTX * ca
the CA to mint leaves with
server worker context
volatile char key[227]
TRUE while the key is held.
#define _str(__PTR)
define true
Definition n_common.h:193
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_NULL
no log output
Definition n_log.h:70
#define LOG_INFO
informational
Definition n_log.h:106
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
Definition n_str.h:203
A box including a string and his lenght.
Definition n_str.h:61
int n_x509_generate_ca(const char *cn, int days, N_STR **ca_cert_pem, N_STR **ca_key_pem)
Generate a self-signed certificate authority (CA) keypair and cert.
Definition n_x509.c:226
int n_x509_mint_host_cert(const char *host, const N_STR *ca_cert_pem, const N_STR *ca_key_pem, int days, N_STR **leaf_cert_pem, N_STR **leaf_key_pem)
Mint a per-host leaf certificate signed by the given CA.
Definition n_x509.c:285
SSL * ssl
SSL handle.
Definition n_network.h:446
int netw_ssl_set_verify(NETWORK *netw, int enable)
enable or disable SSL peer certificate verification
Definition n_network.c:2053
int netw_ssl_server_handshake_ex(NETWORK *netw, netw_sni_pick_cb pick, void *user_data, const char *const *alpn, size_t alpn_count)
upgrade an already-accepted plaintext connection to a TLS server, selecting the certificate per clien...
Definition n_network.c:1947
int netw_init_openssl(void)
Do not directly use, internal api.
Definition n_network.c:1335
int netw_make_listening(NETWORK **netw, char *addr, char *port, int nbpending, int ip_version)
Make a NETWORK be a Listening network.
Definition n_network.c:3493
int netw_ssl_do_handshake(NETWORK *netw, const char *sni_hostname)
Complete the SSL handshake on an already-connected NETWORK.
Definition n_network.c:2948
int netw_unload_openssl(void)
Do not directly use, internal api.
Definition n_network.c:1368
#define NETWORK_IPALL
Flag for auto detection by OS of ip version to use.
Definition n_network.h:48
int netw_ssl_connect_client_to(NETWORK **netw, char *host, char *port, int ip_version, int connect_timeout_ms)
Connect as an SSL client without providing a client certificate.
Definition n_network.c:2839
int netw_ssl_set_alpn(NETWORK *netw, const char *const *protos, size_t count)
offer an ALPN protocol list on a TLS client connection, before its handshake
Definition n_network.c:1834
NETWORK * netw_accept_from(NETWORK *from)
make a normal blocking 'accept' .
Definition n_network.c:4178
int netw_close(NETWORK **netw)
Closing a specified Network, destroy queues, free the structure.
Definition n_network.c:3258
int netw_ssl_get_alpn(NETWORK *netw, char *out, size_t out_len)
get the ALPN protocol the completed handshake selected
Definition n_network.c:1864
Structure of a NETWORK.
Definition n_network.h:375
Common headers and low-level functions & define.
int n_http2_hpack_decode(N_H2_HPACK *ctx, const unsigned char *block, size_t len, N_H2_HEADER *out, size_t max_out, size_t *count)
decode an HPACK header block into out[] (owned name/value strings).
Definition n_http2.c:472
size_t n_http2_hpack_encode(const N_H2_HEADER *headers, size_t n, unsigned char *out, size_t out_cap)
encode a header list into a block using literal representations without indexing and without Huffman ...
Definition n_http2.c:554
N_H2_HPACK * n_http2_hpack_new(size_t max_table_size)
create an HPACK codec context with the given dynamic table size limit (0 uses N_H2_HPACK_DEFAULT_TABL...
Definition n_http2.c:353
void n_http2_hpack_free(N_H2_HPACK **pctx)
free an HPACK codec context and NULL the caller's pointer
Definition n_http2.c:375
void n_http2_hpack_headers_free(N_H2_HEADER *headers, size_t n)
free the owned name/value strings of a decoded header array (does not free the array itself)
Definition n_http2.c:460
int n_http2_frame_parse(const unsigned char *buf, size_t len, N_H2_FRAME *frame, size_t *consumed)
parse one HTTP/2 frame from a byte buffer (stateless).
Definition n_http2.c:32
size_t n_http2_frame_build_header(unsigned char *out, size_t out_cap, uint32_t length, int type, int flags, uint32_t stream_id)
write a 9-byte HTTP/2 frame header into out.
Definition n_http2.c:52
HPACK codec context: the dynamic table (index 0 = most recently added).
Definition n_http2.c:182
char * value
header field value (owned after decode)
Definition n_http2.h:125
#define N_H2_PREFACE
the client connection preface sent before any HTTP/2 frame (RFC 7540 3.5)
Definition n_http2.h:51
#define N_H2_FLAG_END_HEADERS
HEADERS/PUSH_PROMISE/CONTINUATION: header block complete.
Definition n_http2.h:75
#define N_H2_FRAME_HEADER_LEN
length of the fixed frame header in bytes
Definition n_http2.h:53
char * name
header field name (owned after decode)
Definition n_http2.h:124
#define N_H2_DATA
DATA.
Definition n_http2.h:59
uint32_t length
payload length (24-bit)
Definition n_http2.h:92
const unsigned char * payload
pointer into the input buffer (may be NULL when length is 0)
Definition n_http2.h:96
int type
frame type (N_H2_*)
Definition n_http2.h:93
int flags
frame flags
Definition n_http2.h:94
#define N_H2_SETTINGS
SETTINGS.
Definition n_http2.h:63
uint32_t stream_id
stream identifier (31-bit; the reserved bit is cleared)
Definition n_http2.h:95
#define N_H2_HEADERS
HEADERS.
Definition n_http2.h:60
#define N_H2_FLAG_END_STREAM
DATA/HEADERS: last frame of the stream.
Definition n_http2.h:73
a parsed HTTP/2 frame; payload aliases the input buffer
Definition n_http2.h:91
one HPACK header field; after a decode both strings are owned (heap)
Definition n_http2.h:123
int n_http2_server_respond(N_H2_SERVER *s, uint32_t stream_id, const N_H2_HEADER *headers, size_t nheaders, const unsigned char *body, size_t body_len)
write a complete response on a stream (HEADERS then DATA, END_STREAM)
int n_http2_server_run(N_H2_SERVER *s, n_h2_srv_request_cb cb, void *user_data)
serve the connection until it ends
N_H2_SERVER * n_http2_server_new(void *io, n_h2_srv_read_fn rd, n_h2_srv_write_fn wr)
create a server connection over the given I/O callbacks
void n_http2_server_free(N_H2_SERVER **srv)
free a server connection and NULL the caller's pointer
const char * n_http2_server_error(const N_H2_SERVER *s)
the reason the connection failed, or NULL when none was recorded
int n_http2_server_handshake(N_H2_SERVER *s)
read and validate the client connection preface, then send ours
server connection state
HTTP/2 (RFC 7540) server-side connection and stream state machine.
uint32_t stream_id
the stream the request arrived on
const char * path
the :path pseudo-header, or NULL when absent
one complete request received on a stream; every pointer is owned by the server and valid only for th...
Generic log system.
Network Engine.
N_STR and string function declaration.
X.509 helpers: self-signed CA generation and per-host leaf minting.