Packages
hackney
2.0.1
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
c_src/quic_conn.c
/**
* quic_conn.c - QUIC connection resource management using lsquic
*
* This file is part of hackney released under the Apache 2 license.
* See the NOTICE for more information.
*
* Copyright (c) 2024-2026 Benoit Chesneau
*/
#include "quic_conn.h"
#include "atoms.h"
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <lsxpack_header.h>
/* Maximum UDP packet size */
#define MAX_UDP_PAYLOAD 1500
/* Buffer for incoming packets */
#define RECV_BUF_SIZE 65536
/* Global resource type for QUIC connections */
ErlNifResourceType *QUIC_CONN_RESOURCE = NULL;
/* Global initialization flag - use atomic to prevent race conditions */
static volatile int g_lsquic_initialized = 0;
static ErlNifMutex *g_init_mutex = NULL;
/* Forward declarations for lsquic callbacks */
static lsquic_conn_ctx_t *on_new_conn(void *stream_if_ctx, lsquic_conn_t *conn);
static void on_conn_closed(lsquic_conn_t *conn);
static lsquic_stream_ctx_t *on_new_stream(void *stream_if_ctx, lsquic_stream_t *s);
static void on_read(lsquic_stream_t *s, lsquic_stream_ctx_t *h);
static void on_write(lsquic_stream_t *s, lsquic_stream_ctx_t *h);
static void on_close(lsquic_stream_t *s, lsquic_stream_ctx_t *h);
static void on_hsk_done(lsquic_conn_t *conn, enum lsquic_hsk_status status);
static void on_goaway_received(lsquic_conn_t *conn);
/* Packet output callback */
static int packets_out(void *ctx, const struct lsquic_out_spec *specs, unsigned n_specs);
/* SSL context callback */
static SSL_CTX *get_ssl_ctx(void *peer_ctx, const struct sockaddr *local);
/* Header set interface callbacks */
static void *hsi_create_header_set(void *ctx, lsquic_stream_t *s, int is_push);
static struct lsxpack_header *hsi_prepare_decode(void *hdr_set,
struct lsxpack_header *hdr,
size_t space);
static int hsi_process_header(void *hdr_set, struct lsxpack_header *hdr);
static void hsi_discard_header_set(void *hdr_set);
/* Stream interface for lsquic */
static const struct lsquic_stream_if stream_if = {
.on_new_conn = on_new_conn,
.on_conn_closed = on_conn_closed,
.on_new_stream = on_new_stream,
.on_read = on_read,
.on_write = on_write,
.on_close = on_close,
.on_hsk_done = on_hsk_done,
.on_goaway_received = on_goaway_received,
};
/* Header set interface */
static const struct lsquic_hset_if hset_if = {
.hsi_create_header_set = hsi_create_header_set,
.hsi_prepare_decode = hsi_prepare_decode,
.hsi_process_header = hsi_process_header,
.hsi_discard_header_set = hsi_discard_header_set,
.hsi_flags = 0,
};
/*===========================================================================
* Utility functions
*===========================================================================*/
/* Get current time in microseconds */
static uint64_t get_time_us(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;
}
/* Set socket to non-blocking mode */
static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/* Create UDP socket and bind */
static int create_udp_socket(int family, struct sockaddr_storage *local_addr,
socklen_t *local_addrlen) {
int fd = socket(family, SOCK_DGRAM, 0);
if (fd < 0) return -1;
if (set_nonblocking(fd) < 0) {
close(fd);
return -1;
}
/* Enable address reuse */
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
/* Bind to any available port */
if (family == AF_INET) {
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
} else {
struct sockaddr_in6 addr = {0};
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = 0;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
}
/* Get the bound address */
*local_addrlen = sizeof(*local_addr);
if (getsockname(fd, (struct sockaddr *)local_addr, local_addrlen) < 0) {
close(fd);
return -1;
}
return fd;
}
/* Resolve hostname to sockaddr */
static int resolve_hostname(const char *hostname, uint16_t port,
struct sockaddr_storage *addr, socklen_t *addrlen) {
struct addrinfo hints = {0};
struct addrinfo *res;
char port_str[6];
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
snprintf(port_str, sizeof(port_str), "%u", port);
int ret = getaddrinfo(hostname, port_str, &hints, &res);
if (ret != 0) {
return -1;
}
memcpy(addr, res->ai_addr, res->ai_addrlen);
*addrlen = res->ai_addrlen;
freeaddrinfo(res);
return 0;
}
/*===========================================================================
* Global initialization
*===========================================================================*/
int quic_global_init(void) {
/* Fast path - already initialized */
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_ACQUIRE)) return 0;
/* Create mutex on first call - this is safe because NIF load is single-threaded */
if (!g_init_mutex) {
g_init_mutex = enif_mutex_create("quic_global_init_mutex");
if (!g_init_mutex) return -1;
}
/* Double-checked locking */
enif_mutex_lock(g_init_mutex);
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_RELAXED)) {
enif_mutex_unlock(g_init_mutex);
return 0;
}
if (lsquic_global_init(LSQUIC_GLOBAL_CLIENT) != 0) {
enif_mutex_unlock(g_init_mutex);
return -1;
}
__atomic_store_n(&g_lsquic_initialized, 1, __ATOMIC_RELEASE);
enif_mutex_unlock(g_init_mutex);
return 0;
}
void quic_global_cleanup(void) {
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_ACQUIRE)) {
if (g_init_mutex) {
enif_mutex_lock(g_init_mutex);
}
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_RELAXED)) {
lsquic_global_cleanup();
__atomic_store_n(&g_lsquic_initialized, 0, __ATOMIC_RELEASE);
}
if (g_init_mutex) {
enif_mutex_unlock(g_init_mutex);
enif_mutex_destroy(g_init_mutex);
g_init_mutex = NULL;
}
}
}
/*===========================================================================
* SSL context
*===========================================================================*/
static SSL_CTX *create_ssl_ctx(void) {
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) return NULL;
/* Set minimum TLS version to 1.3 (required for QUIC) */
SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION);
SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);
/* Set default verify paths */
SSL_CTX_set_default_verify_paths(ctx);
/* For now, skip verification (TODO: make configurable) */
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
return ctx;
}
static SSL_CTX *get_ssl_ctx(void *peer_ctx, const struct sockaddr *local) {
UNUSED(local);
QuicConn *conn = (QuicConn *)peer_ctx;
return conn->ssl_ctx;
}
/*===========================================================================
* lsquic stream interface callbacks
*===========================================================================*/
static lsquic_conn_ctx_t *on_new_conn(void *stream_if_ctx, lsquic_conn_t *c) {
QuicConn *conn = (QuicConn *)stream_if_ctx;
conn->conn = c;
return (lsquic_conn_ctx_t *)conn;
}
static void on_conn_closed(lsquic_conn_t *c) {
lsquic_conn_ctx_t *ctx = lsquic_conn_get_ctx(c);
if (!ctx) return;
QuicConn *conn = (QuicConn *)ctx;
/* Note: Don't use mutex here - called from I/O thread which may hold it */
conn->state = QUIC_CONN_CLOSED;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (!env) return;
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_CLOSED, enif_make_atom(env, "peer_closed")));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
static void on_hsk_done(lsquic_conn_t *c, enum lsquic_hsk_status status) {
lsquic_conn_ctx_t *ctx = lsquic_conn_get_ctx(c);
if (!ctx) return;
QuicConn *conn = (QuicConn *)ctx;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (!env) return;
if (status == LSQ_HSK_OK || status == LSQ_HSK_RESUMED_OK) {
/* Note: Don't use mutex here - we're called from I/O thread which holds it */
conn->state = QUIC_CONN_CONNECTED;
/* Notify owner of successful connection */
ERL_NIF_TERM info = enif_make_new_map(env);
enif_make_map_put(env, info,
enif_make_atom(env, "resumed"),
status == LSQ_HSK_RESUMED_OK ? ATOM_TRUE : ATOM_FALSE,
&info);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_CONNECTED, info));
enif_send(NULL, &conn->owner_pid, env, msg);
} else {
/* Handshake failed */
/* Note: Don't use mutex here - we're called from I/O thread which holds it */
conn->state = QUIC_CONN_CLOSED;
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_CLOSED,
enif_make_atom(env, "handshake_failed")));
enif_send(NULL, &conn->owner_pid, env, msg);
}
enif_free_env(env);
}
static void on_goaway_received(lsquic_conn_t *c) {
lsquic_conn_ctx_t *ctx = lsquic_conn_get_ctx(c);
if (!ctx) return;
QuicConn *conn = (QuicConn *)ctx;
/* Note: Don't use mutex here - called from I/O thread which may hold it */
conn->state = QUIC_CONN_DRAINING;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (!env) return;
ErlNifBinary empty_bin;
enif_alloc_binary(0, &empty_bin);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_GOAWAY,
enif_make_int64(env, 0), /* LastStreamId - not available in callback */
enif_make_int64(env, 0), /* ErrorCode */
enif_make_binary(env, &empty_bin)));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
static lsquic_stream_ctx_t *on_new_stream(void *stream_if_ctx, lsquic_stream_t *s) {
QuicConn *conn = (QuicConn *)stream_if_ctx;
if (!s) return NULL;
/* Create stream context */
QuicStream *stream = enif_alloc(sizeof(QuicStream));
if (!stream) return NULL;
memset(stream, 0, sizeof(QuicStream));
stream->stream_id = lsquic_stream_id(s);
stream->stream = s; /* Store lsquic stream handle */
stream->conn = conn;
/* Add to connection's stream list (no mutex - called from I/O thread) */
stream->next = conn->streams;
conn->streams = stream;
/* Want to read headers/data */
lsquic_stream_wantread(s, 1);
/* Notify Erlang of the new stream */
ErlNifEnv *env = enif_alloc_env();
if (env) {
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_STREAM_OPENED,
enif_make_int64(env, stream->stream_id)));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
return (lsquic_stream_ctx_t *)stream;
}
static void on_read(lsquic_stream_t *s, lsquic_stream_ctx_t *h) {
if (!h) return;
QuicStream *stream = (QuicStream *)h;
QuicConn *conn = stream->conn;
/* Check if we need to read headers first */
if (!stream->headers_received) {
void *hset = lsquic_stream_get_hset(s);
if (hset) {
QuicHeaderSet *header_set = (QuicHeaderSet *)hset;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (env) {
/* Copy headers list from header_set env to new env */
ERL_NIF_TERM headers = enif_make_copy(env, header_set->headers_list);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_STREAM_HEADERS,
enif_make_int64(env, stream->stream_id),
headers,
ATOM_FALSE)); /* fin flag */
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
/* Clean up header set */
enif_free_env(header_set->env);
enif_free(header_set);
stream->headers_received = true;
}
}
/* Read data */
unsigned char buf[16384];
ssize_t nread;
while ((nread = lsquic_stream_read(s, buf, sizeof(buf))) > 0) {
ErlNifEnv *env = enif_alloc_env();
if (!env) continue;
ErlNifBinary bin;
enif_alloc_binary(nread, &bin);
memcpy(bin.data, buf, nread);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_STREAM_DATA,
enif_make_int64(env, stream->stream_id),
enif_make_binary(env, &bin),
ATOM_FALSE));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
if (nread == 0) {
/* EOF - FIN received */
stream->fin_received = true;
ErlNifEnv *env = enif_alloc_env();
if (env) {
ErlNifBinary empty_bin;
enif_alloc_binary(0, &empty_bin);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_STREAM_DATA,
enif_make_int64(env, stream->stream_id),
enif_make_binary(env, &empty_bin),
ATOM_TRUE));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
lsquic_stream_wantread(s, 0);
} else if (nread < 0 && errno != EWOULDBLOCK) {
/* Error */
lsquic_stream_wantread(s, 0);
}
}
static void on_write(lsquic_stream_t *s, lsquic_stream_ctx_t *h) {
if (!h) return;
QuicStream *stream = (QuicStream *)h;
QuicConn *conn = stream->conn;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (env) {
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_SEND_READY,
enif_make_int64(env, stream->stream_id)));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
/* Don't want write events until explicitly requested */
lsquic_stream_wantwrite(s, 0);
}
static void on_close(lsquic_stream_t *s, lsquic_stream_ctx_t *h) {
if (!h) return;
QuicStream *stream = (QuicStream *)h;
QuicConn *conn = stream->conn;
/* Remove from connection's stream list (no mutex - called from I/O thread) */
QuicStream **pp = &conn->streams;
while (*pp) {
if (*pp == stream) {
*pp = stream->next;
break;
}
pp = &(*pp)->next;
}
enif_free(stream);
}
/*===========================================================================
* Header set interface callbacks
*===========================================================================*/
static void *hsi_create_header_set(void *ctx, lsquic_stream_t *s, int is_push) {
UNUSED(is_push);
QuicConn *conn = (QuicConn *)ctx;
QuicHeaderSet *hset = enif_alloc(sizeof(QuicHeaderSet));
if (!hset) return NULL;
hset->env = enif_alloc_env();
if (!hset->env) {
enif_free(hset);
return NULL;
}
hset->headers_list = enif_make_list(hset->env, 0);
hset->conn = conn;
hset->stream_id = s ? (int64_t)lsquic_stream_id(s) : (int64_t)-1;
return hset;
}
static struct lsxpack_header *hsi_prepare_decode(void *hdr_set,
struct lsxpack_header *hdr,
size_t space) {
UNUSED(hdr_set);
if (hdr) {
/* Resize existing header - use enif_alloc since we can't realloc */
if (space > LSXPACK_MAX_STRLEN) return NULL;
char *new_buf = enif_alloc(space);
if (!new_buf) return NULL;
/* Copy existing data if any */
if (hdr->buf && hdr->val_len > 0) {
size_t copy_len = hdr->val_len < space ? hdr->val_len : space;
memcpy(new_buf, hdr->buf, copy_len);
enif_free(hdr->buf);
}
hdr->buf = new_buf;
hdr->val_len = space;
return hdr;
}
/* Create new header */
struct lsxpack_header *new_hdr = enif_alloc(sizeof(struct lsxpack_header));
if (!new_hdr) return NULL;
memset(new_hdr, 0, sizeof(*new_hdr));
new_hdr->buf = enif_alloc(space);
if (!new_hdr->buf) {
enif_free(new_hdr);
return NULL;
}
lsxpack_header_prepare_decode(new_hdr, new_hdr->buf, 0, space);
return new_hdr;
}
static int hsi_process_header(void *hdr_set, struct lsxpack_header *hdr) {
if (!hdr) {
return 0; /* End of headers */
}
QuicHeaderSet *hs = (QuicHeaderSet *)hdr_set;
/* Extract name and value */
const char *name = lsxpack_header_get_name(hdr);
size_t name_len = hdr->name_len;
const char *value = lsxpack_header_get_value(hdr);
size_t value_len = hdr->val_len;
/* Create Erlang binaries */
ErlNifBinary name_bin, value_bin;
enif_alloc_binary(name_len, &name_bin);
enif_alloc_binary(value_len, &value_bin);
memcpy(name_bin.data, name, name_len);
memcpy(value_bin.data, value, value_len);
/* Create {Name, Value} tuple and prepend to list */
ERL_NIF_TERM tuple = enif_make_tuple2(hs->env,
enif_make_binary(hs->env, &name_bin),
enif_make_binary(hs->env, &value_bin));
hs->headers_list = enif_make_list_cell(hs->env, tuple, hs->headers_list);
/* Free the header buffer */
enif_free(hdr->buf);
enif_free(hdr);
return 0;
}
static void hsi_discard_header_set(void *hdr_set) {
if (!hdr_set) return;
QuicHeaderSet *hs = (QuicHeaderSet *)hdr_set;
if (hs->env) enif_free_env(hs->env);
enif_free(hs);
}
/*===========================================================================
* Packet output
*===========================================================================*/
static int packets_out(void *ctx, const struct lsquic_out_spec *specs,
unsigned n_specs) {
QuicConn *conn = (QuicConn *)ctx;
unsigned n;
for (n = 0; n < n_specs; n++) {
struct msghdr msg = {0};
msg.msg_name = (void *)specs[n].dest_sa;
msg.msg_namelen = specs[n].dest_sa->sa_family == AF_INET ?
sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
msg.msg_iov = specs[n].iov;
msg.msg_iovlen = specs[n].iovlen;
ssize_t sent = sendmsg(conn->sockfd, &msg, 0);
if (sent < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return n;
}
return -1;
}
}
return n;
}
/*===========================================================================
* Connection processing (called from dirty scheduler)
*===========================================================================*/
int quic_conn_process(QuicConn *conn, ErlNifEnv *env) {
UNUSED(env);
if (!conn || __atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
return -1;
}
/* Mark select as not armed (since enif_select is one-shot) */
conn->select_armed = false;
enif_mutex_lock(conn->mutex);
if (!conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Receive any pending packets */
unsigned char buf[RECV_BUF_SIZE];
struct sockaddr_storage peer_addr;
socklen_t peer_addrlen;
ssize_t nread;
while ((nread = recvfrom(conn->sockfd, buf, sizeof(buf), MSG_DONTWAIT,
(struct sockaddr *)&peer_addr, &peer_addrlen)) > 0) {
peer_addrlen = sizeof(peer_addr); /* Reset for next iteration */
lsquic_engine_packet_in(conn->engine, buf, nread,
(struct sockaddr *)&conn->local_addr,
(struct sockaddr *)&peer_addr,
conn, 0);
}
/* Process connections - triggers callbacks */
lsquic_engine_process_conns(conn->engine);
/* Get next timeout */
int diff = 0;
int has_tick = lsquic_engine_earliest_adv_tick(conn->engine, &diff);
if (has_tick) {
conn->next_timeout_us = get_time_us() + (uint64_t)diff;
} else {
conn->next_timeout_us = 0;
}
/* Send any pending packets */
if (lsquic_engine_has_unsent_packets(conn->engine)) {
lsquic_engine_send_unsent_packets(conn->engine);
}
enif_mutex_unlock(conn->mutex);
/* Return timeout in ms, or -1 for infinity */
return has_tick ? (diff / 1000) : -1;
}
/*===========================================================================
* Resource management
*===========================================================================*/
void quic_conn_resource_dtor(ErlNifEnv *env, void *obj) {
UNUSED(env);
QuicConn *conn = (QuicConn *)obj;
quic_conn_destroy(conn);
}
int quic_conn_resource_init(ErlNifEnv *env) {
ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
QUIC_CONN_RESOURCE = enif_open_resource_type(
env,
NULL,
"quic_conn",
quic_conn_resource_dtor,
flags,
NULL
);
if (!QUIC_CONN_RESOURCE) return 0;
/* Initialize lsquic globally */
return quic_global_init() == 0;
}
QuicConn *quic_conn_create(ErlNifEnv *env, ErlNifPid owner_pid) {
UNUSED(env);
QuicConn *conn = enif_alloc_resource(QUIC_CONN_RESOURCE, sizeof(QuicConn));
if (!conn) return NULL;
memset(conn, 0, sizeof(QuicConn));
conn->sockfd = -1;
conn->owner_pid = owner_pid;
conn->state = QUIC_CONN_IDLE;
conn->ref_count = 1;
conn->mutex = enif_mutex_create("quic_conn_mutex");
if (!conn->mutex) {
enif_release_resource(conn);
return NULL;
}
return conn;
}
int quic_conn_connect(QuicConn *conn, const char *hostname, uint16_t port,
int sockfd, const struct sockaddr *local_addr, socklen_t local_addrlen) {
if (!conn || !hostname) return -1;
/* Store hostname */
conn->hostname = strdup(hostname);
if (!conn->hostname) return -1;
conn->port = port;
/* Resolve hostname */
if (resolve_hostname(hostname, port, &conn->remote_addr, &conn->remote_addrlen) < 0) {
return -1;
}
/* Use provided socket or create new one */
if (sockfd >= 0) {
/* Use external socket - ensure non-blocking */
set_nonblocking(sockfd);
conn->sockfd = sockfd;
/* Use provided local address or get from socket */
if (local_addr && local_addrlen > 0) {
memcpy(&conn->local_addr, local_addr, local_addrlen);
conn->local_addrlen = local_addrlen;
} else {
conn->local_addrlen = sizeof(conn->local_addr);
if (getsockname(sockfd, (struct sockaddr *)&conn->local_addr,
&conn->local_addrlen) < 0) {
return -1;
}
}
} else {
/* Create new UDP socket */
int family = conn->remote_addr.ss_family;
conn->sockfd = create_udp_socket(family, &conn->local_addr, &conn->local_addrlen);
if (conn->sockfd < 0) {
return -1;
}
}
/* Create SSL context */
conn->ssl_ctx = create_ssl_ctx();
if (!conn->ssl_ctx) {
close(conn->sockfd);
conn->sockfd = -1;
return -1;
}
/* Create lsquic engine */
struct lsquic_engine_settings settings;
lsquic_engine_init_settings(&settings, LSENG_HTTP);
/* Use only IETF QUIC v1 */
settings.es_versions = (1 << LSQVER_I001);
struct lsquic_engine_api api = {0};
api.ea_settings = &settings;
api.ea_stream_if = &stream_if;
api.ea_stream_if_ctx = conn;
api.ea_packets_out = packets_out;
api.ea_packets_out_ctx = conn;
api.ea_get_ssl_ctx = get_ssl_ctx;
api.ea_hsi_if = &hset_if;
api.ea_hsi_ctx = conn;
conn->engine = lsquic_engine_new(LSENG_HTTP, &api);
if (!conn->engine) {
SSL_CTX_free(conn->ssl_ctx);
conn->ssl_ctx = NULL;
close(conn->sockfd);
conn->sockfd = -1;
return -1;
}
/* Initiate connection */
conn->state = QUIC_CONN_HANDSHAKING;
lsquic_conn_t *lconn = lsquic_engine_connect(
conn->engine,
LSQVER_I001, /* QUIC v1 (RFC 9000) */
(struct sockaddr *)&conn->local_addr,
(struct sockaddr *)&conn->remote_addr,
conn, /* peer_ctx */
(lsquic_conn_ctx_t *)conn, /* conn_ctx */
hostname, /* SNI */
0, /* base_plpmtu - let engine decide */
NULL, 0, /* session resumption */
NULL, 0 /* token */
);
if (!lconn) {
lsquic_engine_destroy(conn->engine);
conn->engine = NULL;
SSL_CTX_free(conn->ssl_ctx);
conn->ssl_ctx = NULL;
close(conn->sockfd);
conn->sockfd = -1;
conn->state = QUIC_CONN_IDLE;
return -1;
}
conn->conn = lconn;
/* Process to send initial packets */
lsquic_engine_process_conns(conn->engine);
/* Connection is ready - caller will use enif_select() for I/O notifications */
/* and call quic_conn_process() from a dirty scheduler */
return 0;
}
void quic_conn_keep(QuicConn *conn) {
if (!conn) return;
/* Don't keep a destroyed connection */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
return;
}
enif_mutex_lock(conn->mutex);
conn->ref_count++;
enif_mutex_unlock(conn->mutex);
enif_keep_resource(conn);
}
void quic_conn_release(QuicConn *conn) {
if (!conn) return;
/* Check if already destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
enif_release_resource(conn);
return;
}
enif_mutex_lock(conn->mutex);
int ref = --conn->ref_count;
enif_mutex_unlock(conn->mutex);
if (ref <= 0) {
/* Don't call destroy here - let the Erlang GC call the destructor */
/* The destructor will call quic_conn_destroy */
}
enif_release_resource(conn);
}
void quic_conn_destroy(QuicConn *conn) {
if (!conn) return;
/* Atomically check and set destroyed flag to prevent double-free */
int expected = 0;
if (!__atomic_compare_exchange_n(&conn->destroyed, &expected, 1,
0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
/* Already destroyed or being destroyed */
return;
}
/* Stop select notifications if armed */
if (conn->select_armed && conn->sockfd >= 0) {
/* Note: enif_select with STOP flag cancels notifications */
/* This is called from destructor, so env may not be valid */
conn->select_armed = false;
}
/* Close lsquic connection */
if (conn->conn && conn->engine) {
lsquic_conn_close(conn->conn);
lsquic_engine_process_conns(conn->engine);
conn->conn = NULL;
}
/* Destroy lsquic engine */
if (conn->engine) {
lsquic_engine_destroy(conn->engine);
conn->engine = NULL;
}
/* Free SSL context */
if (conn->ssl_ctx) {
SSL_CTX_free(conn->ssl_ctx);
conn->ssl_ctx = NULL;
}
/* Close socket */
if (conn->sockfd >= 0) {
close(conn->sockfd);
conn->sockfd = -1;
}
/* Free hostname */
if (conn->hostname) {
free(conn->hostname);
conn->hostname = NULL;
}
/* Free session ticket */
if (conn->session_ticket) {
free(conn->session_ticket);
conn->session_ticket = NULL;
}
/* Free streams */
QuicStream *stream = conn->streams;
while (stream) {
QuicStream *next = stream->next;
enif_free(stream);
stream = next;
}
conn->streams = NULL;
/* Free mutex */
if (conn->mutex) {
enif_mutex_destroy(conn->mutex);
conn->mutex = NULL;
}
}
/*===========================================================================
* Stream operations
*===========================================================================*/
/* Find a stream by its ID */
static QuicStream *find_stream(QuicConn *conn, int64_t stream_id) {
QuicStream *stream = conn->streams;
while (stream) {
if (stream->stream_id == stream_id) {
return stream;
}
stream = stream->next;
}
return NULL;
}
int64_t quic_conn_open_stream(QuicConn *conn) {
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Remember the current head of streams list */
QuicStream *prev_head = conn->streams;
/* Request a new stream - callback will be triggered synchronously */
lsquic_conn_make_stream(conn->conn);
lsquic_engine_process_conns(conn->engine);
/* on_new_stream adds new stream at head of list */
int64_t stream_id = -1;
if (conn->streams && conn->streams != prev_head) {
stream_id = conn->streams->stream_id;
}
enif_mutex_unlock(conn->mutex);
return stream_id;
}
int quic_conn_close(QuicConn *conn) {
if (!conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->conn && conn->engine && conn->state != QUIC_CONN_CLOSED) {
conn->state = QUIC_CONN_DRAINING;
lsquic_conn_close(conn->conn);
lsquic_engine_process_conns(conn->engine);
}
enif_mutex_unlock(conn->mutex);
return 0;
}
/* Maximum headers we support in a single send */
#define MAX_HEADERS 64
int quic_conn_send_headers(QuicConn *conn, int64_t stream_id,
ErlNifEnv *env, ERL_NIF_TERM headers_list, bool fin) {
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
QuicStream *stream = find_stream(conn, stream_id);
if (!stream || !stream->stream) {
enif_mutex_unlock(conn->mutex);
return -1;
}
if (stream->headers_sent) {
enif_mutex_unlock(conn->mutex);
return -1; /* Headers already sent */
}
/* Count headers and calculate buffer size */
unsigned count = 0;
size_t total_buf_size = 0;
ERL_NIF_TERM list = headers_list;
ERL_NIF_TERM head;
while (enif_get_list_cell(env, list, &head, &list)) {
int arity;
const ERL_NIF_TERM *tuple;
if (!enif_get_tuple(env, head, &arity, &tuple) || arity != 2) {
enif_mutex_unlock(conn->mutex);
return -1;
}
ErlNifBinary name_bin, value_bin;
if (!enif_inspect_binary(env, tuple[0], &name_bin) &&
!enif_inspect_iolist_as_binary(env, tuple[0], &name_bin)) {
enif_mutex_unlock(conn->mutex);
return -1;
}
if (!enif_inspect_binary(env, tuple[1], &value_bin) &&
!enif_inspect_iolist_as_binary(env, tuple[1], &value_bin)) {
enif_mutex_unlock(conn->mutex);
return -1;
}
total_buf_size += name_bin.size + value_bin.size;
count++;
if (count >= MAX_HEADERS) break;
}
if (count == 0) {
enif_mutex_unlock(conn->mutex);
return -1; /* Empty headers */
}
/* Allocate buffer and headers array */
char *buf = enif_alloc(total_buf_size);
struct lsxpack_header *headers = enif_alloc(count * sizeof(struct lsxpack_header));
if (!buf || !headers) {
if (buf) enif_free(buf);
if (headers) enif_free(headers);
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Fill in headers */
list = headers_list;
size_t offset = 0;
unsigned i = 0;
while (enif_get_list_cell(env, list, &head, &list) && i < count) {
int arity;
const ERL_NIF_TERM *tuple;
enif_get_tuple(env, head, &arity, &tuple);
ErlNifBinary name_bin, value_bin;
enif_inspect_iolist_as_binary(env, tuple[0], &name_bin);
enif_inspect_iolist_as_binary(env, tuple[1], &value_bin);
/* Copy name and value to buffer */
size_t name_offset = offset;
memcpy(buf + offset, name_bin.data, name_bin.size);
offset += name_bin.size;
size_t val_offset = offset;
memcpy(buf + offset, value_bin.data, value_bin.size);
offset += value_bin.size;
/* Set up the header */
lsxpack_header_set_offset2(&headers[i], buf,
name_offset, name_bin.size,
val_offset, value_bin.size);
i++;
}
/* Create HTTP headers struct */
lsquic_http_headers_t http_headers = {
.count = count,
.headers = headers
};
/* Send headers */
int ret = lsquic_stream_send_headers(stream->stream, &http_headers, fin);
enif_free(buf);
enif_free(headers);
if (ret == 0) {
stream->headers_sent = true;
if (fin) {
stream->fin_sent = true;
/* Shutdown write side to send FIN */
lsquic_stream_shutdown(stream->stream, 1); /* 1 = write side */
}
/* Flush the stream to ensure headers are sent */
lsquic_stream_flush(stream->stream);
/* Re-enable read after sending request headers */
lsquic_stream_wantread(stream->stream, 1);
}
/* Process to send the headers packet */
lsquic_engine_process_conns(conn->engine);
/* Send any packets immediately */
if (lsquic_engine_has_unsent_packets(conn->engine)) {
lsquic_engine_send_unsent_packets(conn->engine);
}
enif_mutex_unlock(conn->mutex);
return ret;
}
int quic_conn_send_data(QuicConn *conn, int64_t stream_id,
const uint8_t *data, size_t len, bool fin) {
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
QuicStream *stream = find_stream(conn, stream_id);
if (!stream || !stream->stream) {
enif_mutex_unlock(conn->mutex);
return -1;
}
ssize_t written = lsquic_stream_write(stream->stream, data, len);
if (written < 0) {
enif_mutex_unlock(conn->mutex);
return -1;
}
if (fin) {
lsquic_stream_shutdown(stream->stream, 1); /* Shutdown write side */
stream->fin_sent = true;
}
lsquic_engine_process_conns(conn->engine);
enif_mutex_unlock(conn->mutex);
return (int)written;
}
int quic_conn_reset_stream(QuicConn *conn, int64_t stream_id, uint64_t error_code) {
UNUSED(error_code); /* lsquic doesn't have a reset with error code API */
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
QuicStream *stream = find_stream(conn, stream_id);
if (!stream || !stream->stream) {
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Close the stream (lsquic doesn't have a reset with error code) */
lsquic_stream_close(stream->stream);
lsquic_engine_process_conns(conn->engine);
enif_mutex_unlock(conn->mutex);
return 0;
}
int64_t quic_conn_handle_timeout(QuicConn *conn) {
if (!conn || !conn->engine) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
/* Double-check engine after acquiring mutex */
if (!conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
lsquic_engine_process_conns(conn->engine);
int diff;
int has_tick = lsquic_engine_earliest_adv_tick(conn->engine, &diff);
int64_t result = has_tick ? diff / 1000 : -1; /* Convert us to ms */
enif_mutex_unlock(conn->mutex);
return result;
}
int quic_conn_peername(QuicConn *conn, struct sockaddr_storage *addr, socklen_t *addrlen) {
if (!conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
memcpy(addr, &conn->remote_addr, conn->remote_addrlen);
*addrlen = conn->remote_addrlen;
enif_mutex_unlock(conn->mutex);
return 0;
}
int quic_conn_sockname(QuicConn *conn, struct sockaddr_storage *addr, socklen_t *addrlen) {
if (!conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
memcpy(addr, &conn->local_addr, conn->local_addrlen);
*addrlen = conn->local_addrlen;
enif_mutex_unlock(conn->mutex);
return 0;
}