cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
http3_client.c
Go to the documentation of this file.
1
6#if defined(__APPLE__)
7#ifndef _DARWIN_C_SOURCE
8#define _DARWIN_C_SOURCE
9#endif
10#elif !defined(__FreeBSD__) && !defined(__NetBSD__) && !defined(__OpenBSD__) && \
11 !defined(__DragonFly__)
12#define _POSIX_C_SOURCE 200809L
13#endif
17#include "h3_hdr_out.h"
18
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22#include <unistd.h>
23#include <errno.h>
24#include <fcntl.h>
25#include <arpa/inet.h>
26#include <netdb.h>
27#include <sys/socket.h>
28#include <sys/uio.h>
29#include <poll.h>
30#include <pthread.h>
31#include <ctype.h>
32#include <strings.h>
33
34#include <openssl/ssl.h>
35#include <openssl/x509.h>
36#include <openssl/evp.h>
37
38#include <lsquic.h>
39/* The WebTransport client is experimental and lives on dev. It needs
40 * lsquic_wt.h, which main's lsquic does not have, so the Makefile refuses
41 * CWIST_WEBTRANSPORT=client here; main builds CWIST_WEBTRANSPORT=server. */
42#ifdef CWIST_WEBTRANSPORT_CLIENT
43#error "the WebTransport client is experimental and not built on main"
44#include <lsquic_wt.h>
45#endif
46#include <lsxpack_header.h>
47
48/* BSD sockets do not universally provide MSG_DONTWAIT. This client creates
49 * its UDP socket in non-blocking mode before lsquic can emit packets. */
50#ifndef MSG_DONTWAIT
51#define MSG_DONTWAIT 0
52#endif
53
54/* ------------------------------------------------------------------ */
55/* Globals */
56/* ------------------------------------------------------------------ */
57
58static pthread_mutex_t g_h3c_global_mtx = PTHREAD_MUTEX_INITIALIZER;
59static int g_h3c_global_ref = 0;
60
66static void h3c_global_init(void) {
67 pthread_mutex_lock(&g_h3c_global_mtx);
68 if (g_h3c_global_ref == 0) {
70 }
71 g_h3c_global_ref++;
72 pthread_mutex_unlock(&g_h3c_global_mtx);
73}
74
80static void h3c_global_cleanup(void) {
81 pthread_mutex_lock(&g_h3c_global_mtx);
82 if (g_h3c_global_ref > 0) {
83 g_h3c_global_ref--;
84 if (g_h3c_global_ref == 0) {
86 }
87 }
88 pthread_mutex_unlock(&g_h3c_global_mtx);
89}
90
91/* ------------------------------------------------------------------ */
92/* Client handle */
93/* ------------------------------------------------------------------ */
94
95typedef struct h3c_stream_ctx {
96 lsquic_stream_t *stream;
98 char *req_path;
99 cwist_http_method_t req_method;
100 cwist_http_header_node *req_headers;
101 char *req_body;
102 size_t req_body_len;
103 size_t req_body_sent;
104 char *resp_body;
105 size_t resp_body_len;
106 size_t resp_body_cap;
107 int headers_done;
108 int response_ready;
109 int write_done;
110#ifdef CWIST_WEBTRANSPORT_CLIENT
112#endif
114
115#ifdef CWIST_WEBTRANSPORT_CLIENT
118 struct cwist_http3_client *client;
119 int open;
120 int rejected;
121};
122#endif
123
124struct cwist_http3_client {
125 SSL_CTX *ssl_ctx;
126 lsquic_engine_t *engine;
127 lsquic_conn_t *conn;
128 int udp_fd;
129 struct sockaddr_storage peer_addr;
130 socklen_t peer_addr_len;
131 struct sockaddr_storage local_addr;
132 socklen_t local_addr_len;
133 char *host;
134 uint16_t port;
135 int timeout_ms;
136 int datagram_enabled;
137 int max_retries;
138 int retry_delay_ms;
139 int conn_timeout_ms;
140
141 pthread_mutex_t mtx;
142 pthread_cond_t cond;
143 h3c_stream_ctx_t *active_stream;
144
145 pthread_mutex_t dgram_mtx;
146 struct {
147 void *data;
148 size_t len;
149 int pending;
150 } out_dgram;
151 struct {
152 void *data;
153 size_t len;
154 int ready;
155 } in_dgram;
156#ifdef CWIST_WEBTRANSPORT_CLIENT
158#endif
159};
160
161/* ------------------------------------------------------------------ */
162/* Packet-out callback */
163/* ------------------------------------------------------------------ */
164
177static int h3c_packets_out(void *ctx, const struct lsquic_out_spec *specs, unsigned n_specs) {
178 cwist_http3_client *client = ctx;
179 unsigned i;
180 for (i = 0; i < n_specs; ++i) {
181 const struct lsquic_out_spec *spec = &specs[i];
182 struct msghdr msg = {0};
183 /* The socket is connected once the peer is known (see
184 * cwist_http3_client_request); specifying a destination on a
185 * connected UDP socket fails with EISCONN. */
186 if (client->local_addr_len == 0) {
187 msg.msg_name = (void *)spec->dest_sa;
188 msg.msg_namelen = (spec->dest_sa && spec->dest_sa->sa_family == AF_INET)
189 ? sizeof(struct sockaddr_in)
190 : sizeof(struct sockaddr_in6);
191 }
192 msg.msg_iov = (struct iovec *)spec->iov;
193 msg.msg_iovlen = spec->iovlen;
194 ssize_t nw = sendmsg(client->udp_fd, &msg, MSG_DONTWAIT);
195 if (nw < 0) {
196 if (errno == EAGAIN || errno == EWOULDBLOCK) break;
197 return -1;
198 }
199 }
200 return (int)i;
201}
202
203/* ------------------------------------------------------------------ */
204/* Header-set interface (server sends us headers) */
205/* ------------------------------------------------------------------ */
206
207#define H3C_MAX_HEADERS 64
208#define H3C_DECODE_BUF_SIZE 65536
209
210typedef struct h3c_hset {
211 struct lsxpack_header headers[H3C_MAX_HEADERS];
212 size_t count;
213 char decode_buf[H3C_DECODE_BUF_SIZE];
214 size_t decode_off;
216
222static void *h3c_hsi_create(void *hsi_ctx, lsquic_stream_t *stream, int is_push_promise) {
223 (void)hsi_ctx;
224 (void)stream;
226 h3c_hset_t *hset = calloc(1, sizeof(*hset));
227 return hset;
228}
229
242static struct lsxpack_header *h3c_hsi_prepare(void *hset_p, struct lsxpack_header *xhdr,
243 size_t req_space) {
245 if (!hset) return NULL;
246
247 /* lsquic calls us again with the same header when its initial output
248 * buffer was too small. Preserve the decoded name and grow only the
249 * value capacity; reinitializing the slot here loses the header. */
250 if (xhdr) {
251 if (req_space > LSXPACK_MAX_STRLEN || xhdr->name_offset < 0 ||
252 (size_t)xhdr->name_offset >= sizeof(hset->decode_buf) ||
253 req_space > sizeof(hset->decode_buf) - (size_t)xhdr->name_offset) {
254 return NULL;
255 }
256 xhdr->val_len = (lsxpack_strlen_t)req_space;
257 return xhdr;
258 }
259
260 if (hset->count >= H3C_MAX_HEADERS) return NULL;
261 if (req_space > sizeof(hset->decode_buf) - hset->decode_off) return NULL;
262 lsxpack_header_prepare_decode(&hset->headers[hset->count], hset->decode_buf, hset->decode_off,
263 sizeof(hset->decode_buf) - hset->decode_off);
264 return &hset->headers[hset->count];
265}
266
273static int h3c_hsi_process_header(void *hset_p, struct lsxpack_header *xhdr) {
275 /* A NULL header marks the end of a header block. */
276 if (!hset || !xhdr) return 0;
277
278 /* The QPACK decoder exposes the exact storage used by this completed
279 * header. */
280 size_t total = lsxpack_header_get_dec_size(xhdr);
281 if (total > sizeof(hset->decode_buf) - hset->decode_off) return -1;
282 hset->decode_off += total;
283 hset->count++;
284 return 0;
285}
286
292static void h3c_hsi_discard(void *hset_p) {
293 free(hset_p);
294}
295
296static const struct lsquic_hset_if h3c_hset_if = {
297 .hsi_create_header_set = h3c_hsi_create,
298 .hsi_prepare_decode = h3c_hsi_prepare,
299 .hsi_process_header = h3c_hsi_process_header,
300 .hsi_discard_header_set = h3c_hsi_discard,
301};
302
303/* ------------------------------------------------------------------ */
304/* Stream callbacks */
305/* ------------------------------------------------------------------ */
306
312static lsquic_conn_ctx_t *h3c_on_new_conn(void *stream_if_ctx, lsquic_conn_t *conn) {
314 (void)conn;
315 return (lsquic_conn_ctx_t *)client;
316}
317
325static void h3c_on_conn_closed(lsquic_conn_t *conn) {
327 if (client && client->conn == conn) {
328 client->conn = NULL;
329 }
330}
331
342static lsquic_stream_ctx_t *h3c_on_new_stream(void *stream_if_ctx, lsquic_stream_t *stream) {
344 h3c_stream_ctx_t *st = calloc(1, sizeof(*st));
345 if (!st) return NULL;
346 st->stream = stream;
347 st->req_method = CWIST_HTTP_GET;
348 /* Do not wantwrite() here: the engine would dispatch h3c_on_write
349 * before cwist_http3_client_request() has populated req_path/method/
350 * headers/body, sending a default "GET /" instead of the real request.
351 * The request function arms the write side once the fields are set. */
352 pthread_mutex_lock(&client->mtx);
353 client->active_stream = st;
354 pthread_mutex_unlock(&client->mtx);
355 return (lsquic_stream_ctx_t *)st;
356}
357
358#ifdef CWIST_WEBTRANSPORT_CLIENT
367 const struct lsquic_wt_connect_info *info) {
368 (void)info;
369 cwist_http3_client *client = ctx;
370 if (!client || !client->wt_connecting) return NULL;
371 pthread_mutex_lock(&client->mtx);
372 client->wt_connecting->native = native;
373 client->wt_connecting->open = 1;
374 if (client->active_stream) client->active_stream->response_ready = 1;
375 pthread_cond_broadcast(&client->cond);
376 pthread_mutex_unlock(&client->mtx);
377 return (lsquic_wt_session_ctx_t *)client->wt_connecting;
378}
379
386static void h3c_wt_on_session_rejected(void *ctx, const struct lsquic_wt_connect_info *info,
387 unsigned status, const char *reason, size_t reason_len) {
388 (void)info;
389 (void)status;
390 (void)reason;
392 cwist_http3_client *client = ctx;
393 if (!client || !client->wt_connecting) return;
394 pthread_mutex_lock(&client->mtx);
395 client->wt_connecting->rejected = 1;
396 if (client->active_stream) client->active_stream->response_ready = 1;
397 pthread_cond_broadcast(&client->cond);
398 pthread_mutex_unlock(&client->mtx);
399}
400
402 uint64_t code, const char *reason, size_t reason_len) {
403 (void)native;
404 (void)code;
405 (void)reason;
408 if (session) {
409 session->open = 0;
410 session->native = NULL;
411 }
412}
413
414static const struct lsquic_webtransport_if h3c_wt_if = {
415 .wti_on_session_open = h3c_wt_on_session_open,
416 .wti_on_session_rejected = h3c_wt_on_session_rejected,
417 .wti_on_session_close = h3c_wt_on_session_close,
418};
419#endif
420
436static void h3c_on_read(lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) {
438 if (!st) return;
439
440 unsigned char buf[8192];
442
443 if (!st->headers_done) {
444 void *hset = lsquic_stream_get_hset(stream);
445 if (hset) {
446 h3c_hset_t *hs = hset;
448 size_t i;
449 for (i = 0; i < hs->count && st->res; ++i) {
450 const char *raw_name = lsxpack_header_get_name(&hs->headers[i]);
451 const char *raw_value = lsxpack_header_get_value(&hs->headers[i]);
452 size_t name_len = hs->headers[i].name_len;
453 size_t value_len = hs->headers[i].val_len;
454 if (!raw_name || !raw_value || name_len == 0 || name_len > 1024 ||
456 continue;
457 /* lsxpack exposes counted slices, not C strings: copy before
458 * using strcmp/atoi, which read past the slice end. */
459 char *name = malloc(name_len + 1);
460 char *value = malloc(value_len + 1);
461 if (!name || !value) {
462 free(name);
463 free(value);
464 break;
465 }
466 memcpy(name, raw_name, name_len);
467 name[name_len] = '\0';
468 memcpy(value, raw_value, value_len);
469 value[value_len] = '\0';
470 if (strcmp(name, ":status") == 0) {
471 st->res->status_code = (cwist_http_status_t)atoi(value);
472 } else {
473 cwist_http_header_add(&st->res->headers, name, value);
474 }
475 free(value);
476 free(name);
477 }
478 st->headers_done = 1;
479#ifdef CWIST_WEBTRANSPORT_CLIENT
480 if (st->is_webtransport_connect) {
481 cwist_http3_client *client =
483 if (!client || !st->res || st->res->status_code < 200 ||
484 st->res->status_code >= 300) {
485 if (client && client->wt_connecting) client->wt_connecting->rejected = 1;
486 st->response_ready = 1;
487 return;
488 }
490 .wtci_authority = client->host,
491 .wtci_path = st->req_path,
492 .wtci_draft = lsquic_wt_peer_draft(lsquic_stream_conn(stream)),
493 };
495 .wtap_wt_if = &h3c_wt_if,
496 .wtap_wt_if_ctx = client,
497 .wtap_connect_info = &info,
498 };
499 if (lsquic_wt_accept(stream, &params) != 0) {
500 if (client->wt_connecting) client->wt_connecting->rejected = 1;
501 st->response_ready = 1;
502 }
503 return;
504 }
505#endif
506 }
507 }
508
509 while ((nread = lsquic_stream_read(stream, buf, sizeof(buf))) > 0) {
510 size_t need = st->resp_body_len + (size_t)nread;
511 if (need > st->resp_body_cap) {
512 size_t new_cap = st->resp_body_cap ? st->resp_body_cap * 2 : 4096;
513 while (new_cap < need) new_cap *= 2;
514 char *tmp = realloc(st->resp_body, new_cap);
515 if (!tmp) {
516 lsquic_stream_close(stream);
517 return;
518 }
519 st->resp_body = tmp;
520 st->resp_body_cap = new_cap;
521 }
522 memcpy(st->resp_body + st->resp_body_len, buf, (size_t)nread);
523 st->resp_body_len += (size_t)nread;
524 }
525
526 if (nread == 0) {
527 if (st->resp_body_len > 0 && st->res && st->res->body) {
528 cwist_sstring_assign_len(st->res->body, st->resp_body, st->resp_body_len);
529 }
530 st->response_ready = 1;
531 lsquic_stream_wantread(stream, 0);
532 }
533}
534
547static void h3c_on_write(lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) {
549 if (!st || st->write_done) return;
550
551 /* Send request headers */
552 struct lsxpack_header headers_arr[64];
553 char hbuf[8192];
554 h3_hdr_out hdrs = {.arr = headers_arr, .max = 64, .buf = hbuf, .cap = sizeof(hbuf)};
555
556 const char *method_str = cwist_http_method_to_string(st->req_method);
557 h3_hdr_out_add_str(&hdrs, ":method", method_str && *method_str ? method_str : "GET");
558 h3_hdr_out_add_str(&hdrs, ":path", st->req_path ? st->req_path : "/");
559 cwist_http3_client *client =
561 h3_hdr_out_add_str(&hdrs, ":authority", client && client->host ? client->host : "localhost");
562 h3_hdr_out_add_str(&hdrs, ":scheme", "https");
563
564 cwist_http_header_node *node = st->req_headers;
565 while (node && hdrs.count < hdrs.max) {
566 if (!node->key || !node->key->data || !node->value || !node->value->data) {
567 node = node->next;
568 continue;
569 }
570 const char *name = node->key->data;
571 const char *value = node->value->data;
572 size_t nlen = node->key->size;
573 size_t vlen = node->value->size;
574 if ((nlen == 7 && strncasecmp(name, ":method", nlen) == 0) ||
575 (nlen == 5 && strncasecmp(name, ":path", nlen) == 0) ||
576 (nlen == 10 && strncasecmp(name, ":authority", nlen) == 0) ||
577 (nlen == 7 && strncasecmp(name, ":scheme", nlen) == 0)) {
578 node = node->next;
579 continue;
580 }
581 h3_hdr_out_add(&hdrs, name, nlen, value, vlen);
582 node = node->next;
583 }
584
585 lsquic_http_headers_t headers = {
586 .count = (unsigned)hdrs.count,
587 .headers = headers_arr,
588 };
589
590 int has_body = (st->req_body && st->req_body_len > 0);
591 if (lsquic_stream_send_headers(stream, &headers, !has_body) != 0) {
592 lsquic_stream_close(stream);
593 return;
594 }
595
596 if (has_body) {
597 ssize_t n = lsquic_stream_write(stream, st->req_body + st->req_body_sent,
598 st->req_body_len - st->req_body_sent);
599 if (n < 0) {
600 lsquic_stream_close(stream);
601 return;
602 }
603 st->req_body_sent += (size_t)n;
604 if (st->req_body_sent >= st->req_body_len) {
605 lsquic_stream_shutdown(stream, 1);
606 st->write_done = 1;
607 lsquic_stream_wantwrite(stream, 0);
608 lsquic_stream_wantread(stream, 1);
609 }
610 } else {
611 st->write_done = 1;
612 /* lsquic ignores the eos argument of lsquic_stream_send_headers()
613 * for IETF QUIC; shut down the write side explicitly so the server
614 * sees FIN on a bodyless request. */
615 lsquic_stream_shutdown(stream, 1);
616 lsquic_stream_wantwrite(stream, 0);
617 lsquic_stream_wantread(stream, 1);
618 }
619}
620
632static void h3c_on_close(lsquic_stream_t *stream, lsquic_stream_ctx_t *st_h) {
634 if (!st) return;
635 cwist_http3_client *client =
637 pthread_mutex_lock(&client->mtx);
638 st->response_ready = 1;
639 if (client->active_stream == st) {
640 client->active_stream = NULL;
641 }
642 pthread_cond_broadcast(&client->cond);
643 pthread_mutex_unlock(&client->mtx);
644 free(st->req_path);
645 free(st->req_body);
646 free(st->resp_body);
647 free(st);
648}
649
661static ssize_t h3c_on_dg_write(lsquic_conn_t *conn, void *buf, size_t len) {
663 if (!client) return 0;
664 pthread_mutex_lock(&client->dgram_mtx);
665 if (client->out_dgram.pending && client->out_dgram.data && client->out_dgram.len > 0) {
666 size_t to_copy = client->out_dgram.len < len ? client->out_dgram.len : len;
667 memcpy(buf, client->out_dgram.data, to_copy);
668 free(client->out_dgram.data);
669 client->out_dgram.data = NULL;
670 client->out_dgram.len = 0;
671 client->out_dgram.pending = 0;
672 pthread_mutex_unlock(&client->dgram_mtx);
673 return (ssize_t)to_copy;
674 }
675 pthread_mutex_unlock(&client->dgram_mtx);
676 return 0;
677}
678
691static void h3c_on_datagram(lsquic_conn_t *conn, const void *buf, size_t len) {
693 if (!client || !buf || len == 0) return;
694 pthread_mutex_lock(&client->dgram_mtx);
695 if (client->in_dgram.data) free(client->in_dgram.data);
696 client->in_dgram.data = malloc(len);
697 if (client->in_dgram.data) {
698 memcpy(client->in_dgram.data, buf, len);
699 client->in_dgram.len = len;
700 client->in_dgram.ready = 1;
701 }
702 pthread_mutex_unlock(&client->dgram_mtx);
703}
704
705static const struct lsquic_stream_if h3c_stream_if = {
706 .on_new_conn = h3c_on_new_conn,
707 .on_conn_closed = h3c_on_conn_closed,
708 .on_new_stream = h3c_on_new_stream,
709 .on_read = h3c_on_read,
710 .on_write = h3c_on_write,
711 .on_close = h3c_on_close,
712 .on_dg_write = h3c_on_dg_write,
713 .on_datagram = h3c_on_datagram,
714};
715
716/* ------------------------------------------------------------------ */
717/* I/O loop helper */
718/* ------------------------------------------------------------------ */
719
732static void h3c_process_io(cwist_http3_client *client, int timeout_ms) {
733 lsquic_engine_t *engine = client->engine;
734 int diff = timeout_ms * 1000; /* microseconds */
735 if (diff <= 0) diff = 1000;
736
738 if (diff <= 0)
739 diff = 0;
740 else if (diff > 1000000)
741 diff = 1000000;
742 }
743
744 struct pollfd pfd = {.fd = client->udp_fd, .events = POLLIN};
745 int pret = poll(&pfd, 1, diff / 1000);
746
747 if (pret > 0 && (pfd.revents & POLLIN)) {
748 unsigned char buf[65535];
749 struct sockaddr_storage peer_addr;
750 socklen_t peer_addr_len = sizeof(peer_addr);
751 ssize_t nr = recvfrom(client->udp_fd, buf, sizeof(buf), 0, (struct sockaddr *)&peer_addr,
752 &peer_addr_len);
753 if (nr > 0) {
754 lsquic_engine_packet_in(engine, buf, (size_t)nr,
755 client->local_addr_len ? (struct sockaddr *)&client->local_addr
756 : NULL,
757 (struct sockaddr *)&peer_addr, NULL, 0);
758 }
759 }
760
762}
763
774static SSL_CTX *h3c_get_ssl_ctx(void *peer_ctx, const struct sockaddr *local) {
775 (void)local;
777 return client ? client->ssl_ctx : NULL;
778}
779
780/* ------------------------------------------------------------------ */
781/* Client API */
782/* ------------------------------------------------------------------ */
783
785 h3c_global_init();
786
787 cwist_http3_client *client = calloc(1, sizeof(*client));
788 if (!client) {
789 h3c_global_cleanup();
790 return NULL;
791 }
792
793 client->udp_fd = -1;
794 client->timeout_ms = 30000;
795 client->max_retries = 0;
796 client->retry_delay_ms = 1000;
797 client->conn_timeout_ms = 5000;
798 pthread_mutex_init(&client->mtx, NULL);
799 pthread_cond_init(&client->cond, NULL);
800 pthread_mutex_init(&client->dgram_mtx, NULL);
801
802 const SSL_METHOD *method = TLS_method();
803 client->ssl_ctx = SSL_CTX_new(method);
804 if (!client->ssl_ctx) {
805 free(client);
806 h3c_global_cleanup();
807 return NULL;
808 }
809
812 /* Secure default: verify the peer certificate chain and hostname
813 * (SNI set by lsquic also feeds BoringSSL's X509_VERIFY_PARAM host
814 * check). cwist_http3_client_set_insecure() opts out explicitly. */
815 SSL_CTX_set_verify(client->ssl_ctx, SSL_VERIFY_PEER, NULL);
816 SSL_CTX_set_default_verify_paths(client->ssl_ctx);
817
820 settings.es_versions = (1 << LSQVER_I001) | (1 << LSQVER_I002);
821 settings.es_ping_period = 15;
822 settings.es_noprogress_timeout = 30;
823 settings.es_ecn = 1;
824 settings.es_pace_packets = 1;
825 settings.es_optimistic_nat = 1;
826 settings.es_datagrams = client->datagram_enabled ? 1 : 0;
827#ifdef CWIST_WEBTRANSPORT_CLIENT
828 settings.es_webtransport = 1;
829 settings.es_max_webtransport_sessions = 4;
830 settings.es_http_datagrams = 1;
831 settings.es_reset_stream_at = 1;
832#endif
833
834 char err_buf[256];
836 fprintf(stderr, "[HTTP/3-CLIENT] Invalid engine settings: %s\n", err_buf);
837 SSL_CTX_free(client->ssl_ctx);
838 pthread_mutex_destroy(&client->mtx);
839 pthread_cond_destroy(&client->cond);
840 free(client);
841 h3c_global_cleanup();
842 return NULL;
843 }
844
845 struct lsquic_engine_api api = {
846 .ea_stream_if = &h3c_stream_if,
847 .ea_stream_if_ctx = client,
848 .ea_packets_out = h3c_packets_out,
849 .ea_packets_out_ctx = client,
850 .ea_get_ssl_ctx = h3c_get_ssl_ctx,
851 .ea_hsi_if = &h3c_hset_if,
852 .ea_hsi_ctx = NULL,
853 .ea_settings = &settings,
854 .ea_alpn = "h3",
855 };
856
857 client->engine = lsquic_engine_new(LSENG_HTTP, &api);
858 if (!client->engine) {
859 SSL_CTX_free(client->ssl_ctx);
860 pthread_mutex_destroy(&client->mtx);
861 pthread_cond_destroy(&client->cond);
862 free(client);
863 h3c_global_cleanup();
864 return NULL;
865 }
866
867 return client;
868}
869
870void cwist_http3_client_destroy(cwist_http3_client *client) {
871 if (!client) return;
872 if (client->engine) {
873 lsquic_engine_destroy(client->engine);
874 }
875 if (client->ssl_ctx) {
876 SSL_CTX_free(client->ssl_ctx);
877 }
878 if (client->udp_fd >= 0) {
879 close(client->udp_fd);
880 }
881 free(client->host);
882 free(client->out_dgram.data);
883 free(client->in_dgram.data);
884#ifdef CWIST_WEBTRANSPORT_CLIENT
885 free(client->wt_connecting);
886#endif
887 pthread_mutex_destroy(&client->dgram_mtx);
888 pthread_mutex_destroy(&client->mtx);
889 pthread_cond_destroy(&client->cond);
890 free(client);
891 h3c_global_cleanup();
892}
893
894int cwist_http3_client_set_server(cwist_http3_client *client, const char *host, uint16_t port) {
895 if (!client || !host) return -1;
896 char *copy = strdup(host);
897 if (!copy) return -1; /* keep the previous server */
898 free(client->host);
899 client->host = copy;
900 client->port = port;
901 return 0;
902}
903
904int cwist_http3_client_set_ca_bundle(cwist_http3_client *client, const char *ca_path) {
905 if (!client || !client->ssl_ctx) return -1;
906 /* Verification stays on either way; this only selects the trust store. */
907 if (ca_path) {
908 if (SSL_CTX_load_verify_locations(client->ssl_ctx, ca_path, NULL) != 1) return -1;
909 } else {
910 SSL_CTX_set_default_verify_paths(client->ssl_ctx);
911 }
912 return 0;
913}
914
915void cwist_http3_client_set_insecure(cwist_http3_client *client, int enabled) {
916 if (!client || !client->ssl_ctx) return;
917 SSL_CTX_set_verify(client->ssl_ctx, enabled ? SSL_VERIFY_NONE : SSL_VERIFY_PEER, NULL);
918}
919
920void cwist_http3_client_set_timeout_ms(cwist_http3_client *client, int timeout_ms) {
921 if (client) client->timeout_ms = timeout_ms;
922}
923
924void cwist_http3_client_enable_0rtt(cwist_http3_client *client, int enabled) {
925 if (!client || !client->ssl_ctx) return;
926 SSL_CTX_set_early_data_enabled(client->ssl_ctx, enabled ? 1 : 0);
927}
928
929void cwist_http3_client_enable_datagrams(cwist_http3_client *client, int enabled) {
930 if (client) client->datagram_enabled = enabled;
931}
932
933/* ------------------------------------------------------------------ */
934/* Synchronous request */
935/* ------------------------------------------------------------------ */
936
937cwist_error_t cwist_http3_client_request(cwist_http3_client *client, const char *path,
938 cwist_http_method_t method,
939 cwist_http_header_node *headers, const char *body,
940 size_t body_len, cwist_http_response **out_response) {
942 if (!client || !client->engine || !client->host || !path || !out_response) {
943 err.error.err_i16 = -1;
944 return err;
945 }
946
948
949 /* Create UDP socket if needed */
950 if (client->udp_fd < 0) {
951 client->udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
952 if (client->udp_fd < 0) {
953 err.error.err_i16 = -1;
954 return err;
955 }
956 int flags = fcntl(client->udp_fd, F_GETFL, 0);
957 if (flags >= 0) fcntl(client->udp_fd, F_SETFL, flags | O_NONBLOCK);
958 }
959
960 /* Resolve host if not already done */
961 if (client->peer_addr_len == 0) {
962 struct addrinfo hints = {0}, *res = NULL;
963 hints.ai_family = AF_INET;
964 hints.ai_socktype = SOCK_DGRAM;
965 char port_str[8];
966 snprintf(port_str, sizeof(port_str), "%u", client->port);
967 if (getaddrinfo(client->host, port_str, &hints, &res) != 0 || !res) {
968 err.error.err_i16 = -1;
969 return err;
970 }
971 memcpy(&client->peer_addr, res->ai_addr, res->ai_addrlen);
972 client->peer_addr_len = res->ai_addrlen;
973 freeaddrinfo(res);
974 }
975
976 /* lsquic dereferences the local address in client mode
977 * (ietf_full_conn_ci_record_addrs), so a NULL local_sa segfaults.
978 * Connect the UDP socket to the peer and learn the local endpoint. */
979 if (client->local_addr_len == 0) {
980 if (connect(client->udp_fd, (struct sockaddr *)&client->peer_addr, client->peer_addr_len) !=
981 0) {
982 err.error.err_i16 = -1;
983 return err;
984 }
985 client->local_addr_len = sizeof(client->local_addr);
986 if (getsockname(client->udp_fd, (struct sockaddr *)&client->local_addr,
987 &client->local_addr_len) != 0) {
988 client->local_addr_len = 0;
989 err.error.err_i16 = -1;
990 return err;
991 }
992 }
993
994 /* ---------------------------------------------------------------- */
995 /* Retry loop with exponential backoff */
996 /* ---------------------------------------------------------------- */
997 int attempt = 0;
998 int max_attempts = 1 + client->max_retries;
999 while (attempt < max_attempts) {
1000 /* Ensure connection is healthy */
1001 if (client->conn) {
1002 enum LSQUIC_CONN_STATUS st_status = lsquic_conn_status(client->conn, NULL, 0);
1006 client->conn = NULL; /* lsquic will clean it up via engine */
1007 }
1008 }
1009
1010 if (!client->conn) {
1011 client->conn = lsquic_engine_connect(client->engine, N_LSQVER,
1012 (struct sockaddr *)&client->local_addr,
1013 (struct sockaddr *)&client->peer_addr, client,
1014 NULL, client->host, 0, NULL, 0, NULL, 0);
1015 if (!client->conn) {
1016 err.error.err_i16 = -1;
1017 goto retry_backoff;
1018 }
1019 }
1020
1021 /* Request a new stream */
1022 pthread_mutex_lock(&client->mtx);
1023 client->active_stream = NULL;
1024 pthread_mutex_unlock(&client->mtx);
1025 lsquic_conn_make_stream(client->conn);
1026
1027 /* I/O loop until stream is created */
1030 stream_deadline.tv_sec += client->conn_timeout_ms / 1000;
1031 stream_deadline.tv_nsec += (client->conn_timeout_ms % 1000) * 1000000;
1032 if (stream_deadline.tv_nsec >= 1000000000) {
1033 stream_deadline.tv_sec++;
1034 stream_deadline.tv_nsec -= 1000000000;
1035 }
1037 while (!st) {
1038 h3c_process_io(client, 100);
1039 pthread_mutex_lock(&client->mtx);
1040 st = client->active_stream;
1041 pthread_mutex_unlock(&client->mtx);
1042 if (st) break;
1043 /* Fail fast when the handshake is dead (e.g. certificate
1044 * verification failure) instead of waiting out the timeout.
1045 * h3c_on_conn_closed() NULLs client->conn once the engine
1046 * tears the connection down. */
1047 if (!client->conn) break;
1048 enum LSQUIC_CONN_STATUS cst = lsquic_conn_status(client->conn, NULL, 0);
1051 /* The engine owns and may already have destroyed the failed
1052 * connection; drop our pointer before the next attempt. */
1053 client->conn = NULL;
1054 break;
1055 }
1056 struct timespec now;
1058 if (now.tv_sec > stream_deadline.tv_sec ||
1059 (now.tv_sec == stream_deadline.tv_sec && now.tv_nsec >= stream_deadline.tv_nsec)) {
1060 break;
1061 }
1062 }
1063 if (!st) {
1064 err.error.err_i16 = -1;
1065 goto retry_backoff;
1066 }
1067
1068 st->req_path = strdup(path);
1069 if (!st->req_path) {
1070 err.error.err_i16 = -1;
1071 goto retry_backoff;
1072 }
1073 st->req_method = method;
1074 st->req_headers = headers;
1075#ifdef CWIST_WEBTRANSPORT_CLIENT
1076 for (cwist_http_header_node *header = headers; header; header = header->next) {
1077 if (header->key && header->value && header->key->data && header->value->data &&
1078 strcasecmp(header->key->data, ":protocol") == 0 &&
1079 strcasecmp(header->value->data, "webtransport") == 0) {
1080 st->is_webtransport_connect = 1;
1081 break;
1082 }
1083 }
1084#endif
1085
1086 /* Store request body */
1087 if (body && body_len > 0) {
1088 st->req_body = malloc(body_len);
1089 if (!st->req_body) {
1090 err.error.err_i16 = -1;
1091 goto retry_backoff;
1092 }
1093 memcpy(st->req_body, body, body_len);
1094 st->req_body_len = body_len;
1095 }
1096
1097 /* All request fields are in place: arm the write side now. */
1098 lsquic_stream_wantwrite(st->stream, 1);
1099
1100 /* I/O loop until response is ready or timeout */
1101 struct timespec deadline;
1102 clock_gettime(CLOCK_REALTIME, &deadline);
1103 deadline.tv_sec += client->timeout_ms / 1000;
1104 deadline.tv_nsec += (client->timeout_ms % 1000) * 1000000;
1105 if (deadline.tv_nsec >= 1000000000) {
1106 deadline.tv_sec++;
1107 deadline.tv_nsec -= 1000000000;
1108 }
1109
1110 pthread_mutex_lock(&client->mtx);
1111 while (!st->response_ready) {
1112 pthread_mutex_unlock(&client->mtx);
1113 h3c_process_io(client, 100);
1114 pthread_mutex_lock(&client->mtx);
1115 struct timespec now;
1117 if (now.tv_sec > deadline.tv_sec ||
1118 (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) {
1119 break;
1120 }
1121 }
1122 pthread_mutex_unlock(&client->mtx);
1123
1124 if (st->response_ready) {
1125 *out_response = st->res;
1126 st->res = NULL; /* Transfer ownership */
1127 err.error.err_i16 = 0;
1128 return err;
1129 }
1130
1131 /* Timeout on this attempt: close the stream so lsquic releases the
1132 * context via h3c_on_close. Freeing it here would leave lsquic
1133 * holding a dangling stream ctx (use-after-free on the next event). */
1134 err.error.err_i16 = -1;
1135 pthread_mutex_lock(&client->mtx);
1136 if (client->active_stream == st) {
1137 client->active_stream = NULL;
1138 }
1139 pthread_mutex_unlock(&client->mtx);
1140 if (st->res) {
1142 st->res = NULL;
1143 }
1144 lsquic_stream_close(st->stream);
1145
1147 attempt++;
1148 if (attempt < max_attempts && client->retry_delay_ms > 0) {
1149 int backoff = client->retry_delay_ms * (1 << (attempt - 1));
1150 if (backoff > 30000) backoff = 30000; /* cap at 30s */
1151 usleep((useconds_t)backoff * 1000);
1152 }
1153 }
1154
1155 return err;
1156}
1157
1158/* ------------------------------------------------------------------ */
1159/* Datagram API (RFC 9221) */
1160/* ------------------------------------------------------------------ */
1161
1162int cwist_http3_client_send_datagram(cwist_http3_client *client, const void *data, size_t len) {
1163 if (!client || !client->conn || !data || len == 0) return -1;
1164 if (!client->datagram_enabled) return -1;
1165
1166 pthread_mutex_lock(&client->dgram_mtx);
1167 if (client->out_dgram.pending) {
1168 pthread_mutex_unlock(&client->dgram_mtx);
1169 return -1;
1170 }
1171 client->out_dgram.data = malloc(len);
1172 if (!client->out_dgram.data) {
1173 pthread_mutex_unlock(&client->dgram_mtx);
1174 return -1;
1175 }
1176 memcpy(client->out_dgram.data, data, len);
1177 client->out_dgram.len = len;
1178 client->out_dgram.pending = 1;
1179 pthread_mutex_unlock(&client->dgram_mtx);
1180
1181 lsquic_conn_want_datagram_write(client->conn, 1);
1182 return 0;
1183}
1184
1185ssize_t cwist_http3_client_recv_datagram(cwist_http3_client *client, void *buf, size_t len) {
1186 if (!client || !buf || len == 0) return -1;
1187
1188 pthread_mutex_lock(&client->dgram_mtx);
1189 if (!client->in_dgram.ready || !client->in_dgram.data) {
1190 pthread_mutex_unlock(&client->dgram_mtx);
1191 return -1;
1192 }
1193 size_t to_copy = client->in_dgram.len < len ? client->in_dgram.len : len;
1194 memcpy(buf, client->in_dgram.data, to_copy);
1195 free(client->in_dgram.data);
1196 client->in_dgram.data = NULL;
1197 client->in_dgram.len = 0;
1198 client->in_dgram.ready = 0;
1199 pthread_mutex_unlock(&client->dgram_mtx);
1200 return (ssize_t)to_copy;
1201}
1202/* ------------------------------------------------------------------ */
1203/* Resilience knobs */
1204/* ------------------------------------------------------------------ */
1205
1206void cwist_http3_client_set_max_retries(cwist_http3_client *client, int max_retries) {
1207 if (client) client->max_retries = max_retries > 0 ? max_retries : 0;
1208}
1209
1210void cwist_http3_client_set_retry_delay_ms(cwist_http3_client *client, int delay_ms) {
1211 if (client) client->retry_delay_ms = delay_ms > 0 ? delay_ms : 0;
1212}
1213
1214void cwist_http3_client_set_conn_timeout_ms(cwist_http3_client *client, int timeout_ms) {
1215 if (client) client->conn_timeout_ms = timeout_ms > 0 ? timeout_ms : 5000;
1216}
Memory allocation wrappers backed by libttak.
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
mbedTLS TLS backend: EVP subset (HMAC digest tags, AES-256-CBC).
Outgoing HTTP/3 header set builder for lsquic (private).
ssize_t cwist_http3_client_recv_datagram(cwist_http3_client *client, void *buf, size_t len)
Receive an unreliable QUIC datagram.
void cwist_http3_client_set_conn_timeout_ms(cwist_http3_client *client, int timeout_ms)
Set the connection establishment / stream creation timeout.
int cwist_http3_client_send_datagram(cwist_http3_client *client, const void *data, size_t len)
Send an unreliable QUIC datagram.
cwist_error_t cwist_http3_client_request(cwist_http3_client *client, const char *path, cwist_http_method_t method, cwist_http_header_node *headers, const char *body, size_t body_len, cwist_http_response **out_response)
Perform a synchronous HTTP/3 request.
#define H3C_MAX_HEADERS
int cwist_http3_client_set_ca_bundle(cwist_http3_client *client, const char *ca_path)
Load CA bundle for TLS certificate verification.
struct h3c_stream_ctx h3c_stream_ctx_t
void cwist_http3_client_enable_datagrams(cwist_http3_client *client, int enabled)
Enable or disable QUIC datagram support (RFC 9221).
void cwist_http3_client_set_insecure(cwist_http3_client *client, int enabled)
Disable or re-enable TLS certificate verification.
int cwist_http3_client_set_server(cwist_http3_client *client, const char *host, uint16_t port)
Set server address for subsequent requests.
void cwist_http3_client_set_timeout_ms(cwist_http3_client *client, int timeout_ms)
Set request timeout in milliseconds.
void cwist_http3_client_set_max_retries(cwist_http3_client *client, int max_retries)
Set the maximum number of request retries on transient failure.
void cwist_http3_client_enable_0rtt(cwist_http3_client *client, int enabled)
Enable or disable 0-RTT session resumption.
#define H3C_DECODE_BUF_SIZE
struct h3c_hset h3c_hset_t
#define MSG_DONTWAIT
void cwist_http3_client_set_retry_delay_ms(cwist_http3_client *client, int delay_ms)
Set the base delay between retries in milliseconds.
cwist_http3_client * cwist_http3_client_create(void)
void cwist_http3_client_destroy(cwist_http3_client *client)
HTTP/3 client using lsquic/BoringSSL.
struct cwist_http3_client cwist_http3_client
Opaque HTTP/3 client handle.
cwist_http_status_t
Definition http.h:27
cwist_http_method_t
Definition http.h:16
@ CWIST_HTTP_GET
Definition http.h:17
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
void cwist_http_response_destroy(cwist_http_response *res)
Destroy an HTTP response object.
Definition http.c:2315
cwist_http_response * cwist_http_response_create(void)
Create a new HTTP response object.
Definition http.c:2291
const char * cwist_http_method_to_string(cwist_http_method_t method)
Convert method enum to string.
Definition http.c:1554
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
mbedTLS TLS backend: OpenSSL-shaped seam over the CWIST TLS subset.
#define SSL_VERIFY_NONE
Definition ssl.h:80
#define TLS1_3_VERSION
Definition ssl.h:49
#define SSL_CTX_new
Definition ssl.h:98
#define SSL_VERIFY_PEER
Definition ssl.h:81
#define SSL_CTX_free
Definition ssl.h:99
struct cwist_mtls_method SSL_METHOD
Definition ssl.h:45
#define SSL_CTX_set_default_verify_paths
Definition ssl.h:125
#define SSL_CTX_set_verify
Definition ssl.h:124
struct cwist_mtls_ctx SSL_CTX
Definition ssl.h:43
#define SSL_CTX_set_min_proto_version
Definition ssl.h:106
Integrated error structure used throughout the system.
Definition cwist_err.h:72
Linked list for headers to handle multiple headers easily.
Definition http.h:59
cwist_sstring * value
Definition http.h:61
cwist_sstring * key
Definition http.h:60
struct cwist_http_header_node * next
Definition http.h:62
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
size_t size
Definition sstring.h:20
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
struct lsxpack_header * arr
Definition h3_hdr_out.h:19
Unified error code and type definitions for the CWIST system layer.