Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_http3.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
32#include "nilorea/n_http3.h"
33#include "nilorea/n_log.h"
34
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38
39/* ---------------------------------------------------------------- */
40/* pure helpers (available in every build) */
41/* ---------------------------------------------------------------- */
42
44 if (!r) {
45 return;
46 }
47 free(r->headers);
48 free(r->body);
49 free(r->error);
50 r->headers = NULL;
51 r->body = NULL;
52 r->error = NULL;
53 r->body_len = 0;
54 r->status = 0;
55}
56
57int n_http3_parse_url(const char* url, char* host, size_t hostsz, char* port, size_t portsz, char* path, size_t pathsz) {
58 const char* p;
59 const char* host_start;
60 const char* slash;
61 const char* hostend;
62 const char* colon;
63 size_t hlen;
64 size_t plen;
65 if (!url || strncmp(url, "https://", 8) != 0) {
66 return -1;
67 }
68 p = url + 8;
69 host_start = p;
70 slash = strchr(p, '/');
71 hostend = slash ? slash : (p + strlen(p));
72
73 /* IPv6 literal in brackets: [::1]:443 */
74 if (*host_start == '[') {
75 const char* rb = memchr(host_start, ']', (size_t)(hostend - host_start));
76 if (!rb) {
77 return -1;
78 }
79 hlen = (size_t)(rb - host_start - 1);
80 host_start++;
81 colon = (rb + 1 < hostend && rb[1] == ':') ? (rb + 1) : NULL;
82 } else {
83 colon = memchr(host_start, ':', (size_t)(hostend - host_start));
84 hlen = colon ? (size_t)(colon - host_start) : (size_t)(hostend - host_start);
85 }
86 if (hlen == 0) {
87 return -1;
88 }
89 if (host) {
90 if (hlen + 1 > hostsz) {
91 return -1;
92 }
93 memcpy(host, host_start, hlen);
94 host[hlen] = '\0';
95 }
96 if (port) {
97 if (colon) {
98 plen = (size_t)(hostend - colon - 1);
99 if (plen == 0 || plen + 1 > portsz) {
100 return -1;
101 }
102 memcpy(port, colon + 1, plen);
103 port[plen] = '\0';
104 } else {
105 if (portsz < 4) {
106 return -1;
107 }
108 memcpy(port, "443", 4);
109 }
110 }
111 if (path) {
112 if (slash) {
113 plen = strlen(slash);
114 if (plen + 1 > pathsz) {
115 return -1;
116 }
117 memcpy(path, slash, plen + 1);
118 } else {
119 if (pathsz < 2) {
120 return -1;
121 }
122 memcpy(path, "/", 2);
123 }
124 }
125 return 0;
126}
127
128int n_http3_get(const char* url, int timeout_ms, N_HTTP3_RESPONSE* out) {
129 return n_http3_request("GET", url, NULL, NULL, 0, timeout_ms, out);
130}
131
132/* ================================================================ */
133#ifdef HAVE_HTTP3
134/* ================================================================ */
135
136#include "nilorea/n_network.h" /* portable SOCKET / closesocket / INVALID_SOCKET + socket includes */
137
138#include <ngtcp2/ngtcp2.h>
139#include <ngtcp2/ngtcp2_crypto.h>
140#include <ngtcp2/ngtcp2_crypto_ossl.h>
141#include <nghttp3/nghttp3.h>
142
143#include <openssl/err.h>
144#include <openssl/rand.h>
145#include <openssl/ssl.h>
146
147#include <stdint.h>
148#include <time.h>
149
150#ifdef _WIN32
151#include <winsock2.h> /* WSAPoll, already pulled by n_network.h on Windows */
152#else
153#include <poll.h>
154#endif
155
157#define H3_MAX_HEADERS 256
159#define H3_SEND_BUF 1500
161#define H3_RECV_BUF 65536
162
164typedef struct h3_body {
165 const uint8_t* data;
166 size_t len;
167} h3_body;
168
170typedef struct h3_hdr {
171 char* name;
172 char* value;
173} h3_hdr;
174
176typedef struct h3_client {
177 SOCKET fd;
178 ngtcp2_conn* conn;
179 ngtcp2_crypto_conn_ref conn_ref;
180 SSL_CTX* ssl_ctx;
181 SSL* ssl;
182 ngtcp2_crypto_ossl_ctx* ossl_ctx;
183 nghttp3_conn* h3;
184
185 struct sockaddr_storage local_addr;
186 socklen_t local_addrlen;
187 struct sockaddr_storage remote_addr;
188 socklen_t remote_addrlen;
189
190 int64_t stream_id;
191 int handshake_done;
192 int stream_done;
193
194 const char* host;
195 const char* authority;
196 const char* method;
197 const char* path;
198 const char* extra_headers;
199 int allow_illegal;
200 h3_body req_body;
201 int has_body;
202
203 char status[8];
204 h3_hdr headers[H3_MAX_HEADERS];
205 int nheaders;
206 uint8_t* body;
207 size_t body_len;
208 size_t body_cap;
209 int oom;
210} h3_client;
211
212/* ---------------------------------------------------------------- */
213/* small platform + string helpers */
214/* ---------------------------------------------------------------- */
215
216static uint64_t h3_now_ns(void) {
217 struct timespec ts;
218 clock_gettime(CLOCK_MONOTONIC, &ts);
219 return (uint64_t)ts.tv_sec * NGTCP2_SECONDS + (uint64_t)ts.tv_nsec;
220}
221
222static int h3_set_nonblocking(SOCKET fd) {
223#ifdef _WIN32
224 u_long on = 1;
225 return ioctlsocket(fd, (long)FIONBIO, &on) == 0 ? 0 : -1;
226#else
227 int fl = fcntl(fd, F_GETFL, 0);
228 if (fl < 0) {
229 return -1;
230 }
231 return fcntl(fd, F_SETFL, fl | O_NONBLOCK) == 0 ? 0 : -1;
232#endif
233}
234
235static int h3_would_block(void) {
236#ifdef _WIN32
237 int e = WSAGetLastError();
238 return e == WSAEWOULDBLOCK || e == WSAEINTR;
239#else
240 return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR;
241#endif
242}
243
244/* poll the socket for readability. returns >0 readable, 0 timeout, -1 error. */
245static int h3_poll_read(SOCKET fd, int timeout_ms) {
246#ifdef _WIN32
247 WSAPOLLFD pfd;
248 pfd.fd = fd;
249 pfd.events = POLLRDNORM;
250 pfd.revents = 0;
251 return WSAPoll(&pfd, 1, timeout_ms);
252#else
253 struct pollfd pfd;
254 pfd.fd = fd;
255 pfd.events = POLLIN;
256 pfd.revents = 0;
257 return poll(&pfd, 1, timeout_ms);
258#endif
259}
260
261static char* h3_dupn(const uint8_t* s, size_t n) {
262 char* p = malloc(n + 1);
263 if (!p) {
264 return NULL;
265 }
266 memcpy(p, s, n);
267 p[n] = '\0';
268 return p;
269}
270
271/* ---------------------------------------------------------------- */
272/* ngtcp2 <-> tls glue */
273/* ---------------------------------------------------------------- */
274
275/* signature must match ngtcp2_crypto_conn_ref.get_conn, so ref stays non-const */
276/* cppcheck-suppress constParameterCallback */
277static ngtcp2_conn* h3_get_conn(ngtcp2_crypto_conn_ref* ref) {
278 const h3_client* c = (const h3_client*)ref->user_data;
279 return c->conn;
280}
281
282static void h3_rand_cb(uint8_t* dest, size_t destlen, const ngtcp2_rand_ctx* rctx) {
283 (void)rctx;
284 if (RAND_bytes(dest, (int)destlen) != 1) {
285 size_t i;
286 for (i = 0; i < destlen; i++) {
287 dest[i] = (uint8_t)rand();
288 }
289 }
290}
291
292static int h3_get_new_cid_cb(ngtcp2_conn* conn, ngtcp2_cid* cid, uint8_t* token, size_t cidlen, void* user_data) {
293 (void)conn;
294 (void)user_data;
295 if (RAND_bytes(cid->data, (int)cidlen) != 1) {
296 return NGTCP2_ERR_CALLBACK_FAILURE;
297 }
298 cid->datalen = cidlen;
299 if (RAND_bytes(token, NGTCP2_STATELESS_RESET_TOKENLEN) != 1) {
300 return NGTCP2_ERR_CALLBACK_FAILURE;
301 }
302 return 0;
303}
304
305static int h3_handshake_completed_cb(ngtcp2_conn* conn, void* user_data) {
306 (void)conn;
307 ((h3_client*)user_data)->handshake_done = 1;
308 return 0;
309}
310
311static int h3_recv_stream_data_cb(ngtcp2_conn* conn, uint32_t flags, int64_t stream_id, uint64_t offset, const uint8_t* data, size_t datalen, void* user_data, void* stream_user_data) {
312 h3_client* c = (h3_client*)user_data;
313 int fin = (flags & NGTCP2_STREAM_DATA_FLAG_FIN) != 0;
314 nghttp3_ssize nconsumed;
315 (void)offset;
316 (void)stream_user_data;
317 if (!c->h3) {
318 return 0;
319 }
320 nconsumed = nghttp3_conn_read_stream(c->h3, stream_id, data, datalen, fin);
321 if (nconsumed < 0) {
322 n_log(LOG_ERR, "nghttp3_conn_read_stream: %s", nghttp3_strerror((int)nconsumed));
323 return NGTCP2_ERR_CALLBACK_FAILURE;
324 }
325 ngtcp2_conn_extend_max_stream_offset(conn, stream_id, (uint64_t)nconsumed);
326 ngtcp2_conn_extend_max_offset(conn, (uint64_t)nconsumed);
327 return 0;
328}
329
330static int h3_acked_stream_data_offset_cb(ngtcp2_conn* conn, int64_t stream_id, uint64_t offset, uint64_t datalen, void* user_data, void* stream_user_data) {
331 h3_client* c = (h3_client*)user_data;
332 (void)conn;
333 (void)offset;
334 (void)stream_user_data;
335 if (c->h3) {
336 nghttp3_conn_add_ack_offset(c->h3, stream_id, datalen);
337 }
338 return 0;
339}
340
341static int h3_stream_close_cb(ngtcp2_conn* conn, uint32_t flags, int64_t stream_id, uint64_t app_error_code, void* user_data, void* stream_user_data) {
342 h3_client* c = (h3_client*)user_data;
343 (void)conn;
344 (void)stream_user_data;
345 if (!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) {
346 app_error_code = NGHTTP3_H3_NO_ERROR;
347 }
348 if (c->h3) {
349 int rv = nghttp3_conn_close_stream(c->h3, stream_id, app_error_code);
350 if (rv != 0 && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
351 n_log(LOG_ERR, "nghttp3_conn_close_stream: %s", nghttp3_strerror(rv));
352 return NGTCP2_ERR_CALLBACK_FAILURE;
353 }
354 }
355 if (stream_id == c->stream_id) {
356 c->stream_done = 1;
357 }
358 return 0;
359}
360
361static int h3_stream_reset_cb(ngtcp2_conn* conn, int64_t stream_id, uint64_t final_size, uint64_t app_error_code, void* user_data, void* stream_user_data) {
362 h3_client* c = (h3_client*)user_data;
363 (void)conn;
364 (void)final_size;
365 (void)app_error_code;
366 (void)stream_user_data;
367 if (c->h3) {
368 nghttp3_conn_shutdown_stream_read(c->h3, stream_id);
369 }
370 return 0;
371}
372
373/* ---------------------------------------------------------------- */
374/* nghttp3 callbacks */
375/* ---------------------------------------------------------------- */
376
377static int h3_recv_header_cb(nghttp3_conn* conn, int64_t stream_id, int32_t token, nghttp3_rcbuf* name, nghttp3_rcbuf* value, uint8_t flags, void* conn_user_data, void* stream_user_data) {
378 h3_client* c = (h3_client*)conn_user_data;
379 nghttp3_vec n = nghttp3_rcbuf_get_buf(name);
380 nghttp3_vec v = nghttp3_rcbuf_get_buf(value);
381 (void)conn;
382 (void)stream_id;
383 (void)flags;
384 (void)stream_user_data;
385 if (token == NGHTTP3_QPACK_TOKEN__STATUS || (n.len == 7 && memcmp(n.base, ":status", 7) == 0)) {
386 snprintf(c->status, sizeof(c->status), "%.*s", (int)v.len, (const char*)v.base);
387 return 0;
388 }
389 if (c->nheaders < H3_MAX_HEADERS) {
390 char* hn = h3_dupn(n.base, n.len);
391 char* hv = h3_dupn(v.base, v.len);
392 if (hn && hv) {
393 c->headers[c->nheaders].name = hn;
394 c->headers[c->nheaders].value = hv;
395 c->nheaders++;
396 } else {
397 free(hn);
398 free(hv);
399 c->oom = 1;
400 }
401 }
402 return 0;
403}
404
405static int h3_recv_data_cb(nghttp3_conn* conn, int64_t stream_id, const uint8_t* data, size_t datalen, void* conn_user_data, void* stream_user_data) {
406 h3_client* c = (h3_client*)conn_user_data;
407 (void)conn;
408 (void)stream_id;
409 (void)stream_user_data;
410 if (c->body_len + datalen > c->body_cap) {
411 size_t newcap = c->body_cap ? c->body_cap : 65536;
412 uint8_t* nb;
413 while (newcap < c->body_len + datalen) {
414 newcap *= 2;
415 }
416 if (newcap > N_HTTP3_BODY_CAP) {
417 newcap = N_HTTP3_BODY_CAP;
418 }
419 if (c->body_len + datalen > newcap) {
420 datalen = newcap - c->body_len; /* body exceeds the cap: keep what fits */
421 }
422 if (datalen == 0) {
423 return 0;
424 }
425 nb = realloc(c->body, newcap);
426 if (!nb) {
427 c->oom = 1;
428 return NGHTTP3_ERR_CALLBACK_FAILURE;
429 }
430 c->body = nb;
431 c->body_cap = newcap;
432 }
433 if (datalen) {
434 memcpy(c->body + c->body_len, data, datalen);
435 c->body_len += datalen;
436 }
437 return 0;
438}
439
440static int h3_deferred_consume_cb(nghttp3_conn* conn, int64_t stream_id, size_t consumed, void* conn_user_data, void* stream_user_data) {
441 h3_client* c = (h3_client*)conn_user_data;
442 (void)conn;
443 (void)stream_user_data;
444 ngtcp2_conn_extend_max_stream_offset(c->conn, stream_id, consumed);
445 ngtcp2_conn_extend_max_offset(c->conn, consumed);
446 return 0;
447}
448
449static int h3_end_stream_cb(nghttp3_conn* conn, int64_t stream_id, void* conn_user_data, void* stream_user_data) {
450 h3_client* c = (h3_client*)conn_user_data;
451 (void)conn;
452 (void)stream_user_data;
453 if (stream_id == c->stream_id) {
454 c->stream_done = 1;
455 }
456 return 0;
457}
458
459static int h3_stream_close_h3_cb(nghttp3_conn* conn, int64_t stream_id, uint64_t app_error_code, void* conn_user_data, void* stream_user_data) {
460 h3_client* c = (h3_client*)conn_user_data;
461 (void)conn;
462 (void)app_error_code;
463 (void)stream_user_data;
464 if (stream_id == c->stream_id) {
465 c->stream_done = 1;
466 }
467 return 0;
468}
469
470/* request-body data reader: hand nghttp3 the whole buffer once, then EOF */
471static nghttp3_ssize h3_body_read_cb(nghttp3_conn* conn, int64_t stream_id, nghttp3_vec* vec, size_t veccnt, uint32_t* pflags, void* conn_user_data, void* stream_user_data) {
472 h3_client* c = (h3_client*)conn_user_data;
473 (void)conn;
474 (void)stream_id;
475 (void)veccnt;
476 (void)stream_user_data;
477 *pflags = NGHTTP3_DATA_FLAG_EOF;
478 if (c->req_body.len == 0) {
479 return 0;
480 }
481 vec[0].base = (uint8_t*)c->req_body.data;
482 vec[0].len = c->req_body.len;
483 return 1;
484}
485
486/* ---------------------------------------------------------------- */
487/* socket + connection setup */
488/* ---------------------------------------------------------------- */
489
490static SOCKET h3_udp_connect(const char* host, const char* port, struct sockaddr_storage* remote, socklen_t* remotelen) {
491 struct addrinfo hints;
492 struct addrinfo* res = NULL;
493 struct addrinfo* rp;
494 SOCKET fd = INVALID_SOCKET;
495 int rv;
496 memset(&hints, 0, sizeof(hints));
497 hints.ai_family = AF_UNSPEC;
498 hints.ai_socktype = SOCK_DGRAM;
499 hints.ai_protocol = IPPROTO_UDP;
500 rv = getaddrinfo(host, port, &hints, &res);
501 if (rv != 0) {
502 n_log(LOG_ERR, "getaddrinfo(%s:%s) failed", host, port);
503 return INVALID_SOCKET;
504 }
505 for (rp = res; rp; rp = rp->ai_next) {
506 fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
507 if (fd == INVALID_SOCKET) {
508 continue;
509 }
510 if (connect(fd, rp->ai_addr, (socklen_t)rp->ai_addrlen) == 0) {
511 memcpy(remote, rp->ai_addr, rp->ai_addrlen);
512 *remotelen = (socklen_t)rp->ai_addrlen;
513 break;
514 }
515 closesocket(fd);
516 fd = INVALID_SOCKET;
517 }
518 freeaddrinfo(res);
519 if (fd == INVALID_SOCKET) {
520 n_log(LOG_ERR, "could not open a UDP socket to %s:%s", host, port);
521 return INVALID_SOCKET;
522 }
523 if (h3_set_nonblocking(fd) != 0) {
524 closesocket(fd);
525 return INVALID_SOCKET;
526 }
527 return fd;
528}
529
530static int h3_setup_tls(h3_client* c) {
531 static const unsigned char alpn[] = "\x02h3";
532 c->ssl_ctx = SSL_CTX_new(TLS_client_method());
533 if (!c->ssl_ctx) {
534 return -1;
535 }
536 SSL_CTX_set_min_proto_version(c->ssl_ctx, TLS1_3_VERSION);
537 SSL_CTX_set_max_proto_version(c->ssl_ctx, TLS1_3_VERSION);
538 c->ssl = SSL_new(c->ssl_ctx);
539 if (!c->ssl) {
540 return -1;
541 }
542 c->conn_ref.get_conn = h3_get_conn;
543 c->conn_ref.user_data = c;
544 SSL_set_app_data(c->ssl, &c->conn_ref);
545 if (ngtcp2_crypto_ossl_configure_client_session(c->ssl) != 0) {
546 n_log(LOG_ERR, "ngtcp2_crypto_ossl_configure_client_session failed");
547 return -1;
548 }
549 SSL_set_connect_state(c->ssl);
550 if (SSL_set_alpn_protos(c->ssl, alpn, sizeof(alpn) - 1) != 0) {
551 return -1;
552 }
553 SSL_set_tlsext_host_name(c->ssl, c->host);
554 SSL_set1_host(c->ssl, c->host);
555 if (ngtcp2_crypto_ossl_ctx_new(&c->ossl_ctx, NULL) != 0) {
556 return -1;
557 }
558 ngtcp2_crypto_ossl_ctx_set_ssl(c->ossl_ctx, c->ssl);
559 return 0;
560}
561
562static int h3_setup_conn(h3_client* c) {
563 ngtcp2_settings settings;
564 ngtcp2_transport_params params;
565 ngtcp2_cid dcid;
566 ngtcp2_cid scid;
567 ngtcp2_path path;
568 ngtcp2_callbacks cb;
569 int rv;
570
571 ngtcp2_settings_default(&settings);
572 settings.initial_ts = h3_now_ns();
573
574 ngtcp2_transport_params_default(&params);
575 params.initial_max_streams_bidi = 0;
576 params.initial_max_streams_uni = 3;
577 params.initial_max_data = 1024 * 1024;
578 params.initial_max_stream_data_bidi_local = 256 * 1024;
579 params.initial_max_stream_data_bidi_remote = 256 * 1024;
580 params.initial_max_stream_data_uni = 256 * 1024;
581
582 dcid.datalen = 16;
583 scid.datalen = 16;
584 if (RAND_bytes(dcid.data, 16) != 1 || RAND_bytes(scid.data, 16) != 1) {
585 return -1;
586 }
587
588 path.local.addr = (ngtcp2_sockaddr*)&c->local_addr;
589 path.local.addrlen = c->local_addrlen;
590 path.remote.addr = (ngtcp2_sockaddr*)&c->remote_addr;
591 path.remote.addrlen = c->remote_addrlen;
592 path.user_data = NULL;
593
594 memset(&cb, 0, sizeof(cb));
595 cb.client_initial = ngtcp2_crypto_client_initial_cb;
596 cb.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb;
597 cb.encrypt = ngtcp2_crypto_encrypt_cb;
598 cb.decrypt = ngtcp2_crypto_decrypt_cb;
599 cb.hp_mask = ngtcp2_crypto_hp_mask_cb;
600 cb.recv_retry = ngtcp2_crypto_recv_retry_cb;
601 cb.update_key = ngtcp2_crypto_update_key_cb;
602 cb.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb;
603 cb.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
604 cb.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb;
605 cb.version_negotiation = ngtcp2_crypto_version_negotiation_cb;
606 cb.rand = h3_rand_cb;
607 cb.get_new_connection_id = h3_get_new_cid_cb;
608 cb.handshake_completed = h3_handshake_completed_cb;
609 cb.recv_stream_data = h3_recv_stream_data_cb;
610 cb.acked_stream_data_offset = h3_acked_stream_data_offset_cb;
611 cb.stream_close = h3_stream_close_cb;
612 cb.stream_reset = h3_stream_reset_cb;
613
614 rv = ngtcp2_conn_client_new(&c->conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, &cb, &settings, &params, NULL, c);
615 if (rv != 0) {
616 n_log(LOG_ERR, "ngtcp2_conn_client_new: %s", ngtcp2_strerror(rv));
617 return -1;
618 }
619 ngtcp2_conn_set_tls_native_handle(c->conn, c->ossl_ctx);
620 return 0;
621}
622
623/* lowercase a header name in place (ASCII) */
624static void h3_lower(char* s, size_t n) {
625 size_t i;
626 for (i = 0; i < n; i++) {
627 if (s[i] >= 'A' && s[i] <= 'Z') {
628 s[i] = (char)(s[i] - 'A' + 'a');
629 }
630 }
631}
632
633/* a header name HTTP/3 forbids (connection-specific) or that we set ourselves */
634static int h3_header_banned(const char* name, size_t n) {
635 static const char* banned[] = {"host", "connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection", "content-length", NULL};
636 int i;
637 for (i = 0; banned[i]; i++) {
638 if (strlen(banned[i]) == n && memcmp(name, banned[i], n) == 0) {
639 return 1;
640 }
641 }
642 return 0;
643}
644
645static int h3_setup_h3(h3_client* c) {
646 nghttp3_settings h3settings;
647 nghttp3_callbacks h3cb;
648 nghttp3_nv nva[4 + H3_MAX_HEADERS];
649 size_t nvlen = 0;
650 /* a pointer to clen is kept in nva until submit_request, so it must live to the end of the function */
651 /* cppcheck-suppress variableScope */
652 char clen[32];
653 char* hdr_copy = NULL;
654 int64_t ctrl_id = -1;
655 int64_t enc_id = -1;
656 int64_t dec_id = -1;
657 int64_t stream_id = -1;
658 nghttp3_data_reader dr;
659 const nghttp3_data_reader* drp = NULL;
660 int rv;
661
662 nghttp3_settings_default(&h3settings);
663 memset(&h3cb, 0, sizeof(h3cb));
664 h3cb.recv_header = h3_recv_header_cb;
665 h3cb.recv_data = h3_recv_data_cb;
666 h3cb.deferred_consume = h3_deferred_consume_cb;
667 h3cb.end_stream = h3_end_stream_cb;
668 h3cb.stream_close = h3_stream_close_h3_cb;
669
670 rv = nghttp3_conn_client_new(&c->h3, &h3cb, &h3settings, NULL, c);
671 if (rv != 0) {
672 n_log(LOG_ERR, "nghttp3_conn_client_new: %s", nghttp3_strerror(rv));
673 return -1;
674 }
675
676 if (ngtcp2_conn_open_uni_stream(c->conn, &ctrl_id, NULL) != 0 || nghttp3_conn_bind_control_stream(c->h3, ctrl_id) != 0) {
677 return -1;
678 }
679 if (ngtcp2_conn_open_uni_stream(c->conn, &enc_id, NULL) != 0 || ngtcp2_conn_open_uni_stream(c->conn, &dec_id, NULL) != 0 || nghttp3_conn_bind_qpack_streams(c->h3, enc_id, dec_id) != 0) {
680 return -1;
681 }
682 if (ngtcp2_conn_open_bidi_stream(c->conn, &stream_id, NULL) != 0) {
683 return -1;
684 }
685 c->stream_id = stream_id;
686
687 nva[nvlen++] = (nghttp3_nv){(uint8_t*)":method", (uint8_t*)c->method, 7, strlen(c->method), NGHTTP3_NV_FLAG_NONE};
688 nva[nvlen++] = (nghttp3_nv){(uint8_t*)":scheme", (uint8_t*)"https", 7, 5, NGHTTP3_NV_FLAG_NONE};
689 nva[nvlen++] = (nghttp3_nv){(uint8_t*)":authority", (uint8_t*)c->authority, 10, strlen(c->authority), NGHTTP3_NV_FLAG_NONE};
690 nva[nvlen++] = (nghttp3_nv){(uint8_t*)":path", (uint8_t*)c->path, 5, strlen(c->path), NGHTTP3_NV_FLAG_NONE};
691
692 if (c->has_body) {
693 int wl = snprintf(clen, sizeof(clen), "%zu", c->req_body.len);
694 if (wl > 0) {
695 nva[nvlen++] = (nghttp3_nv){(uint8_t*)"content-length", (uint8_t*)clen, 14, (size_t)wl, NGHTTP3_NV_FLAG_NONE};
696 }
697 }
698
699 /* caller-supplied extra headers ("Name: Value\r\n" lines) */
700 if (c->extra_headers && c->extra_headers[0]) {
701 hdr_copy = strdup(c->extra_headers);
702 if (hdr_copy) {
703 char* save = NULL;
704 char* line;
705 for (line = strtok_r(hdr_copy, "\r\n", &save); line && nvlen < (sizeof(nva) / sizeof(nva[0])); line = strtok_r(NULL, "\r\n", &save)) {
706 char* colon = strchr(line, ':');
707 char* val;
708 size_t nlen;
709 if (!colon) {
710 continue;
711 }
712 *colon = '\0';
713 nlen = strlen(line);
714 h3_lower(line, nlen);
715 /* a pseudo-header (':...') is never caller-injectable; the connection-specific /
716 framing filter is bypassed when allow_illegal is set (security testing) */
717 if (nlen == 0 || line[0] == ':' || (!c->allow_illegal && h3_header_banned(line, nlen))) {
718 continue;
719 }
720 val = colon + 1;
721 while (*val == ' ' || *val == '\t') {
722 val++;
723 }
724 nva[nvlen++] = (nghttp3_nv){(uint8_t*)line, (uint8_t*)val, nlen, strlen(val), NGHTTP3_NV_FLAG_NONE};
725 }
726 }
727 }
728
729 if (c->has_body) {
730 dr.read_data = h3_body_read_cb;
731 drp = &dr;
732 }
733 rv = nghttp3_conn_submit_request(c->h3, stream_id, nva, nvlen, drp, c);
734 free(hdr_copy);
735 if (rv != 0) {
736 n_log(LOG_ERR, "nghttp3_conn_submit_request: %s", nghttp3_strerror(rv));
737 return -1;
738 }
739 return 0;
740}
741
742/* ---------------------------------------------------------------- */
743/* read / write pumps + event loop */
744/* ---------------------------------------------------------------- */
745
746static int h3_write(h3_client* c) {
747 uint8_t buf[H3_SEND_BUF];
748 ngtcp2_path_storage ps;
749 ngtcp2_pkt_info pi;
750 uint64_t ts = h3_now_ns();
751 nghttp3_vec vec[16];
752 size_t max_udp;
753
754 ngtcp2_path_storage_zero(&ps);
755 memset(&pi, 0, sizeof(pi));
756 max_udp = ngtcp2_conn_get_max_tx_udp_payload_size(c->conn);
757 if (max_udp > sizeof(buf)) {
758 max_udp = sizeof(buf);
759 }
760
761 for (;;) {
762 int64_t stream_id = -1;
763 int fin = 0;
764 nghttp3_ssize sveccnt = 0;
765 ngtcp2_ssize ndatalen = 0;
766 uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE;
767 ngtcp2_ssize nwrite;
768
769 if (c->h3 && ngtcp2_conn_get_max_data_left(c->conn)) {
770 sveccnt = nghttp3_conn_writev_stream(c->h3, &stream_id, &fin, vec, 16);
771 if (sveccnt < 0) {
772 n_log(LOG_ERR, "nghttp3_conn_writev_stream: %s", nghttp3_strerror((int)sveccnt));
773 return -1;
774 }
775 }
776 if (fin) {
777 flags |= NGTCP2_WRITE_STREAM_FLAG_FIN;
778 }
779 nwrite = ngtcp2_conn_writev_stream(c->conn, &ps.path, &pi, buf, max_udp, &ndatalen, flags, stream_id, (const ngtcp2_vec*)vec, (size_t)sveccnt, ts);
780 if (nwrite < 0) {
781 switch (nwrite) {
782 case NGTCP2_ERR_STREAM_DATA_BLOCKED:
783 if (c->h3 && stream_id >= 0) {
784 nghttp3_conn_block_stream(c->h3, stream_id);
785 }
786 continue;
787 case NGTCP2_ERR_STREAM_SHUT_WR:
788 if (c->h3 && stream_id >= 0) {
789 nghttp3_conn_shutdown_stream_write(c->h3, stream_id);
790 }
791 continue;
792 case NGTCP2_ERR_WRITE_MORE:
793 if (c->h3 && stream_id >= 0 && ndatalen >= 0) {
794 if (nghttp3_conn_add_write_offset(c->h3, stream_id, (size_t)ndatalen) != 0) {
795 return -1;
796 }
797 }
798 continue;
799 default:
800 n_log(LOG_ERR, "ngtcp2_conn_writev_stream: %s", ngtcp2_strerror((int)nwrite));
801 return -1;
802 }
803 }
804 if (c->h3 && stream_id >= 0 && ndatalen >= 0) {
805 if (nghttp3_conn_add_write_offset(c->h3, stream_id, (size_t)ndatalen) != 0) {
806 return -1;
807 }
808 }
809 if (nwrite == 0) {
810 return 0; /* nothing more to send now */
811 }
812 for (;;) {
813 ssize_t s = send(c->fd, (const char*)buf, (size_t)nwrite, 0);
814 if (s < 0) {
815 if (h3_would_block()) {
816 break; /* buffer full / interrupted: QUIC will retransmit */
817 }
818 n_log(LOG_ERR, "send failed on the QUIC socket");
819 return -1;
820 }
821 break;
822 }
823 }
824}
825
826static int h3_read(h3_client* c) {
827 uint8_t buf[H3_RECV_BUF];
828 for (;;) {
829 struct sockaddr_storage ss;
830 socklen_t sslen = sizeof(ss);
831 ngtcp2_path path;
832 ngtcp2_pkt_info pi;
833 int rv;
834 ssize_t n = recvfrom(c->fd, (char*)buf, sizeof(buf), 0, (struct sockaddr*)&ss, &sslen);
835 if (n < 0) {
836 if (h3_would_block()) {
837 return 0;
838 }
839 n_log(LOG_ERR, "recvfrom failed on the QUIC socket");
840 return -1;
841 }
842 path.local.addr = (ngtcp2_sockaddr*)&c->local_addr;
843 path.local.addrlen = c->local_addrlen;
844 path.remote.addr = (ngtcp2_sockaddr*)&ss;
845 path.remote.addrlen = sslen;
846 path.user_data = NULL;
847 memset(&pi, 0, sizeof(pi));
848 rv = ngtcp2_conn_read_pkt(c->conn, &path, &pi, buf, (size_t)n, h3_now_ns());
849 if (rv != 0) {
850 if (rv == NGTCP2_ERR_DRAINING || rv == NGTCP2_ERR_CLOSING) {
851 c->stream_done = 1;
852 return 0;
853 }
854 n_log(LOG_ERR, "ngtcp2_conn_read_pkt: %s", ngtcp2_strerror(rv));
855 return -1;
856 }
857 }
858}
859
860static int h3_run(h3_client* c, int timeout_ms) {
861 uint64_t start = h3_now_ns();
862 uint64_t budget_ns = (uint64_t)timeout_ms * NGTCP2_MILLISECONDS;
863 for (;;) {
864 ngtcp2_tstamp expiry;
865 uint64_t now;
866 int timeout;
867 int pr;
868
869 if (h3_write(c) != 0) {
870 return -1;
871 }
872 if (c->handshake_done && !c->h3) {
873 if (h3_setup_h3(c) != 0) {
874 return -1;
875 }
876 continue; /* flush the request immediately */
877 }
878 if (c->stream_done) {
879 return 0;
880 }
881 if (h3_now_ns() - start > budget_ns) {
882 n_log(LOG_ERR, "n_http3: timed out after %d ms waiting for the response", timeout_ms);
883 return -1;
884 }
885 expiry = ngtcp2_conn_get_expiry(c->conn);
886 now = h3_now_ns();
887 if (expiry == UINT64_MAX) {
888 timeout = 1000;
889 } else if (expiry <= now) {
890 timeout = 0;
891 } else {
892 uint64_t d = (expiry - now) / NGTCP2_MILLISECONDS;
893 timeout = d > 1000 ? 1000 : (int)d;
894 }
895 pr = h3_poll_read(c->fd, timeout);
896 if (pr < 0) {
897 if (h3_would_block()) {
898 continue;
899 }
900 n_log(LOG_ERR, "poll failed on the QUIC socket");
901 return -1;
902 }
903 if (pr == 0) {
904 int rv = ngtcp2_conn_handle_expiry(c->conn, h3_now_ns());
905 if (rv != 0) {
906 n_log(LOG_ERR, "ngtcp2_conn_handle_expiry: %s", ngtcp2_strerror(rv));
907 return -1;
908 }
909 continue;
910 }
911 if (h3_read(c) != 0) {
912 return -1;
913 }
914 }
915}
916
917static void h3_close_conn(h3_client* c) {
918 uint8_t buf[H3_SEND_BUF];
919 ngtcp2_path_storage ps;
920 ngtcp2_pkt_info pi;
921 ngtcp2_ccerr ccerr;
922 ngtcp2_ssize n;
923 if (!c->conn) {
924 return;
925 }
926 ngtcp2_path_storage_zero(&ps);
927 memset(&pi, 0, sizeof(pi));
928 ngtcp2_ccerr_default(&ccerr);
929 n = ngtcp2_conn_write_connection_close(c->conn, &ps.path, &pi, buf, sizeof(buf), &ccerr, h3_now_ns());
930 if (n > 0) {
931 ssize_t s = send(c->fd, (const char*)buf, (size_t)n, 0);
932 (void)s;
933 }
934}
935
936static void h3_client_cleanup(h3_client* c) {
937 int i;
938 for (i = 0; i < c->nheaders; i++) {
939 free(c->headers[i].name);
940 free(c->headers[i].value);
941 }
942 free(c->body);
943 if (c->h3) {
944 nghttp3_conn_del(c->h3);
945 }
946 if (c->conn) {
947 ngtcp2_conn_del(c->conn);
948 }
949 if (c->ossl_ctx) {
950 ngtcp2_crypto_ossl_ctx_del(c->ossl_ctx);
951 }
952 if (c->ssl) {
953 SSL_free(c->ssl);
954 }
955 if (c->ssl_ctx) {
956 SSL_CTX_free(c->ssl_ctx);
957 }
958 if (c->fd != INVALID_SOCKET) {
959 closesocket(c->fd);
960 }
961}
962
963/* build out->headers by joining the collected header lines */
964static char* h3_join_headers(const h3_client* c) {
965 size_t total = 1;
966 char* out;
967 char* p;
968 int i;
969 for (i = 0; i < c->nheaders; i++) {
970 total += strlen(c->headers[i].name) + 2 + strlen(c->headers[i].value) + 2;
971 }
972 out = malloc(total);
973 if (!out) {
974 return NULL;
975 }
976 p = out;
977 for (i = 0; i < c->nheaders; i++) {
978 int wl = snprintf(p, total - (size_t)(p - out), "%s: %s\r\n", c->headers[i].name, c->headers[i].value);
979 if (wl > 0) {
980 p += wl;
981 }
982 }
983 *p = '\0';
984 return out;
985}
986
987static int h3_fail(N_HTTP3_RESPONSE* out, const char* msg) {
988 if (out && !out->error) {
989 out->error = strdup(msg);
990 }
991 return -1;
992}
993
994int n_http3_available(void) {
995 return 1;
996}
997
998int n_http3_request(const char* method, const char* url, const char* headers, const unsigned char* body, size_t body_len, int timeout_ms, N_HTTP3_RESPONSE* out) {
999 return n_http3_request_ex(method, url, headers, body, body_len, timeout_ms, 0u, out);
1000}
1001
1002int n_http3_request_ex(const char* method, const char* url, const char* headers, const unsigned char* body, size_t body_len, int timeout_ms, unsigned flags, N_HTTP3_RESPONSE* out) {
1003 static int ossl_ready = 0;
1004 h3_client c;
1005 char host[256];
1006 char port[16];
1007 char path[2048];
1008 char authority[300];
1009 int ret;
1010
1011 if (!out) {
1012 return -1;
1013 }
1014 memset(out, 0, sizeof(*out));
1015 if (!url || !url[0]) {
1016 return h3_fail(out, "empty url");
1017 }
1018 if (n_http3_parse_url(url, host, sizeof(host), port, sizeof(port), path, sizeof(path)) != 0) {
1019 return h3_fail(out, "could not parse the https url");
1020 }
1021 if (timeout_ms <= 0) {
1022 timeout_ms = N_HTTP3_DEFAULT_TIMEOUT_MS;
1023 }
1024 if (strcmp(port, "443") == 0) {
1025 snprintf(authority, sizeof(authority), "%s", host);
1026 } else {
1027 snprintf(authority, sizeof(authority), "%s:%s", host, port);
1028 }
1029
1030 if (!ossl_ready) {
1031 if (ngtcp2_crypto_ossl_init() != 0) {
1032 return h3_fail(out, "ngtcp2_crypto_ossl_init failed");
1033 }
1034#ifdef _WIN32
1035 {
1036 WSADATA wsa;
1037 WSAStartup(MAKEWORD(2, 2), &wsa); /* ensure winsock is up (idempotent per process) */
1038 }
1039#endif
1040 ossl_ready = 1;
1041 }
1042
1043 memset(&c, 0, sizeof(c));
1044 c.stream_id = -1;
1045 c.host = host;
1046 c.authority = authority;
1047 c.method = (method && method[0]) ? method : "GET";
1048 c.path = path;
1049 c.extra_headers = headers;
1050 c.allow_illegal = (flags & N_HTTP3_REQ_ALLOW_ILLEGAL_HEADERS) ? 1 : 0;
1051 if (body && body_len > 0) {
1052 c.req_body.data = body;
1053 c.req_body.len = body_len;
1054 c.has_body = 1;
1055 }
1056
1057 c.fd = h3_udp_connect(host, port, &c.remote_addr, &c.remote_addrlen);
1058 if (c.fd == INVALID_SOCKET) {
1059 h3_client_cleanup(&c);
1060 return h3_fail(out, "could not open a QUIC (UDP) socket to the host");
1061 }
1062 c.local_addrlen = sizeof(c.local_addr);
1063 if (getsockname(c.fd, (struct sockaddr*)&c.local_addr, &c.local_addrlen) != 0) {
1064 h3_client_cleanup(&c);
1065 return h3_fail(out, "getsockname failed");
1066 }
1067 if (h3_setup_tls(&c) != 0) {
1068 h3_client_cleanup(&c);
1069 return h3_fail(out, "TLS setup for QUIC failed");
1070 }
1071 if (h3_setup_conn(&c) != 0) {
1072 h3_client_cleanup(&c);
1073 return h3_fail(out, "QUIC connection setup failed");
1074 }
1075
1076 if (h3_run(&c, timeout_ms) != 0) {
1077 h3_close_conn(&c);
1078 h3_client_cleanup(&c);
1079 return h3_fail(out, "the HTTP/3 exchange failed (see the log)");
1080 }
1081 h3_close_conn(&c);
1082
1083 out->status = c.status[0] ? atoi(c.status) : 0;
1084 out->headers = h3_join_headers(&c);
1085 if (c.body && c.body_len) {
1086 out->body = malloc(c.body_len);
1087 if (out->body) {
1088 memcpy(out->body, c.body, c.body_len);
1089 out->body_len = c.body_len;
1090 }
1091 }
1092 ret = out->status ? 0 : h3_fail(out, "no HTTP status was received");
1093 if (c.oom && !out->error) {
1094 out->error = strdup("some response data was dropped (out of memory or body cap)");
1095 }
1096 h3_client_cleanup(&c);
1097 return ret;
1098}
1099
1100/* ================================================================ */
1101#else /* !HAVE_HTTP3 */
1102/* ================================================================ */
1103
1105 return 0;
1106}
1107
1108int n_http3_request(const char* method, const char* url, const char* headers, const unsigned char* body, size_t body_len, int timeout_ms, N_HTTP3_RESPONSE* out) {
1109 (void)method;
1110 (void)url;
1111 (void)headers;
1112 (void)body;
1113 (void)body_len;
1114 (void)timeout_ms;
1115 if (!out) {
1116 return -1;
1117 }
1118 memset(out, 0, sizeof(*out));
1119 out->error = strdup("HTTP/3 support was not compiled in (build the library with HAVE_HTTP3)");
1120 return -1;
1121}
1122
1123int n_http3_request_ex(const char* method, const char* url, const char* headers, const unsigned char* body, size_t body_len, int timeout_ms, unsigned flags, N_HTTP3_RESPONSE* out) {
1124 (void)flags;
1125 return n_http3_request(method, url, headers, body, body_len, timeout_ms, out);
1126}
1127
1128#endif /* HAVE_HTTP3 */
char * port
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_ERR
error conditions
Definition n_log.h:76
char * headers
Response header block as joined "name: value\r\n" lines (lowercased names), or NULL.
Definition n_http3.h:71
int status
HTTP status code, or 0 if none was received.
Definition n_http3.h:70
size_t body_len
Number of valid bytes in body.
Definition n_http3.h:73
unsigned char * body
Response body bytes (NOT NUL-terminated), or NULL.
Definition n_http3.h:72
char * error
Failure reason (heap), or NULL on a completed exchange.
Definition n_http3.h:74
int n_http3_parse_url(const char *url, char *host, size_t hostsz, char *port, size_t portsz, char *path, size_t pathsz)
Split an https URL into host, port and path (pure; available in every build).
Definition n_http3.c:57
int n_http3_request_ex(const char *method, const char *url, const char *headers, const unsigned char *body, size_t body_len, int timeout_ms, unsigned flags, N_HTTP3_RESPONSE *out)
Perform one blocking HTTP/3 request, with request flags.
Definition n_http3.c:1123
#define N_HTTP3_DEFAULT_TIMEOUT_MS
Default per-request budget (milliseconds) when the caller passes <= 0.
Definition n_http3.h:56
#define N_HTTP3_BODY_CAP
Hard cap on the response body kept in memory (bytes).
Definition n_http3.h:58
int n_http3_get(const char *url, int timeout_ms, N_HTTP3_RESPONSE *out)
Convenience wrapper: HTTP/3 GET of url.
Definition n_http3.c:128
int n_http3_request(const char *method, const char *url, const char *headers, const unsigned char *body, size_t body_len, int timeout_ms, N_HTTP3_RESPONSE *out)
Perform one blocking HTTP/3 request over a fresh QUIC connection.
Definition n_http3.c:1108
int n_http3_available(void)
Report whether HTTP/3 support was compiled into the library.
Definition n_http3.c:1104
void n_http3_response_free(N_HTTP3_RESPONSE *r)
Free the heap buffers held by r and zero the struct (safe on NULL / zeroed).
Definition n_http3.c:43
#define N_HTTP3_REQ_ALLOW_ILLEGAL_HEADERS
n_http3_request_ex flag: send the caller's extra headers WITHOUT the built-in connection-specific / f...
Definition n_http3.h:66
Result of an HTTP/3 exchange.
Definition n_http3.h:69
int SOCKET
default socket declaration
Definition n_network.h:102
Minimal blocking HTTP/3 (over QUIC) client.
Generic log system.
Network Engine.