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/hackney_quic_nif.c
/**
* hackney_quic_nif.c - NIF entry point for hackney QUIC support
*
* 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 "hackney_quic_nif.h"
#include "atoms.h"
#include "quic_conn.h"
#include <arpa/inet.h>
/* Atom definitions - declared extern in atoms.h */
ERL_NIF_TERM ATOM_OK;
ERL_NIF_TERM ATOM_ERROR;
ERL_NIF_TERM ATOM_TRUE;
ERL_NIF_TERM ATOM_FALSE;
ERL_NIF_TERM ATOM_UNDEFINED;
ERL_NIF_TERM ATOM_QUIC;
ERL_NIF_TERM ATOM_CONNECTED;
ERL_NIF_TERM ATOM_CLOSED;
ERL_NIF_TERM ATOM_TRANSPORT_ERROR;
ERL_NIF_TERM ATOM_STREAM_HEADERS;
ERL_NIF_TERM ATOM_STREAM_DATA;
ERL_NIF_TERM ATOM_STREAM_OPENED;
ERL_NIF_TERM ATOM_STREAM_RESET;
ERL_NIF_TERM ATOM_STOP_SENDING;
ERL_NIF_TERM ATOM_GOAWAY;
ERL_NIF_TERM ATOM_SESSION_TICKET;
ERL_NIF_TERM ATOM_SEND_READY;
ERL_NIF_TERM ATOM_TIMER;
ERL_NIF_TERM ATOM_BLOCKED;
ERL_NIF_TERM ATOM_NOT_CONNECTED;
ERL_NIF_TERM ATOM_BADARG;
ERL_NIF_TERM ATOM_ENOMEM;
ERL_NIF_TERM ATOM_TIMEOUT;
ERL_NIF_TERM ATOM_CLOSED_BY_PEER;
ERL_NIF_TERM ATOM_CONNECTION_REFUSED;
ERL_NIF_TERM ATOM_TLS_ERROR;
ERL_NIF_TERM ATOM_PROTOCOL_ERROR;
ERL_NIF_TERM ATOM_IDLE;
ERL_NIF_TERM ATOM_HANDSHAKING;
ERL_NIF_TERM ATOM_DRAINING;
/* Helper to convert sockaddr to Erlang {IP, Port} tuple */
static ERL_NIF_TERM
make_address(ErlNifEnv *env, const struct sockaddr_storage *addr, socklen_t addrlen)
{
UNUSED(addrlen);
if (addr->ss_family == AF_INET) {
const struct sockaddr_in *sin = (const struct sockaddr_in *)addr;
unsigned char *ip = (unsigned char *)&sin->sin_addr.s_addr;
ERL_NIF_TERM ip_tuple = enif_make_tuple4(env,
enif_make_int(env, ip[0]),
enif_make_int(env, ip[1]),
enif_make_int(env, ip[2]),
enif_make_int(env, ip[3]));
return enif_make_tuple2(env, ip_tuple, enif_make_int(env, ntohs(sin->sin_port)));
} else if (addr->ss_family == AF_INET6) {
const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)addr;
const uint16_t *ip = (const uint16_t *)&sin6->sin6_addr;
ERL_NIF_TERM ip_tuple = enif_make_tuple8(env,
enif_make_int(env, ntohs(ip[0])),
enif_make_int(env, ntohs(ip[1])),
enif_make_int(env, ntohs(ip[2])),
enif_make_int(env, ntohs(ip[3])),
enif_make_int(env, ntohs(ip[4])),
enif_make_int(env, ntohs(ip[5])),
enif_make_int(env, ntohs(ip[6])),
enif_make_int(env, ntohs(ip[7])));
return enif_make_tuple2(env, ip_tuple, enif_make_int(env, ntohs(sin6->sin6_port)));
}
return enif_make_badarg(env);
}
/*
* NIF: connect(Host, Port, Opts, OwnerPid) -> {ok, ConnRef} | {error, Reason}
*
* Initiates a QUIC connection to the specified host:port.
* This is a dirty NIF that performs the connection asynchronously.
*
* Opts is a map that may contain:
* - socket_fd: integer() - Existing UDP socket file descriptor to use
* - verify: boolean() - Whether to verify server certificate (default: false)
*/
static ERL_NIF_TERM
nif_connect(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
/* Extract host */
ErlNifBinary host_bin;
if (!enif_inspect_binary(env, argv[0], &host_bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &host_bin)) {
return enif_make_badarg(env);
}
/* Null-terminate the hostname */
char *hostname = enif_alloc(host_bin.size + 1);
if (!hostname) {
return make_error(env, ATOM_ENOMEM);
}
memcpy(hostname, host_bin.data, host_bin.size);
hostname[host_bin.size] = '\0';
/* Extract port */
unsigned int port;
if (!enif_get_uint(env, argv[1], &port) || port > 65535) {
enif_free(hostname);
return enif_make_badarg(env);
}
/* Parse options map */
int socket_fd = -1; /* -1 means create new socket */
if (enif_is_map(env, argv[2])) {
ERL_NIF_TERM key, value;
/* Check for socket_fd option */
key = enif_make_atom(env, "socket_fd");
if (enif_get_map_value(env, argv[2], key, &value)) {
if (!enif_get_int(env, value, &socket_fd)) {
enif_free(hostname);
return enif_make_badarg(env);
}
}
}
/* Extract owner pid */
ErlNifPid owner_pid;
if (!enif_get_local_pid(env, argv[3], &owner_pid)) {
enif_free(hostname);
return enif_make_badarg(env);
}
/* Create connection resource */
QuicConn *conn = quic_conn_create(env, owner_pid);
if (!conn) {
enif_free(hostname);
return make_error(env, ATOM_ENOMEM);
}
/* Initiate connection */
int ret = quic_conn_connect(conn, hostname, (uint16_t)port, socket_fd, NULL, 0);
enif_free(hostname);
if (ret < 0) {
enif_release_resource(conn);
return make_error_str(env, "connect_failed");
}
/* Create a reference for select messages */
conn->select_ref = enif_make_ref(env);
/* Arm enif_select for incoming packets */
if (conn->sockfd >= 0) {
int sel_ret = enif_select(env, (ErlNifEvent)conn->sockfd,
ERL_NIF_SELECT_READ,
conn, /* resource object */
NULL, /* pid - use calling process */
conn->select_ref);
if (sel_ret >= 0) {
conn->select_armed = true;
}
}
ERL_NIF_TERM conn_ref = enif_make_resource(env, conn);
enif_release_resource(conn); /* Erlang now owns it */
return make_ok(env, conn_ref);
}
/*
* NIF: close(ConnRef, Reason) -> ok
*
* Closes a QUIC connection gracefully.
*/
static ERL_NIF_TERM
nif_close(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
quic_conn_close(conn);
return ATOM_OK;
}
/*
* NIF: open_stream(ConnRef) -> {ok, StreamId} | {error, Reason}
*
* Opens a new bidirectional stream on the connection.
*/
static ERL_NIF_TERM
nif_open_stream(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
int64_t stream_id = quic_conn_open_stream(conn);
if (stream_id < 0) {
return make_error(env, ATOM_NOT_CONNECTED);
}
/* Stream ID 0 means the request was queued; actual ID will come via callback */
return make_ok(env, enif_make_int64(env, stream_id));
}
/*
* NIF: send_headers(ConnRef, StreamId, Headers, Fin) -> ok | {error, Reason}
*
* Sends HTTP/3 headers on a stream.
*/
static ERL_NIF_TERM
nif_send_headers(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
/* Extract stream ID */
ErlNifSInt64 stream_id;
if (!enif_get_int64(env, argv[1], &stream_id)) {
return enif_make_badarg(env);
}
/* Extract fin flag */
char fin_str[6];
int fin = 0;
if (enif_get_atom(env, argv[3], fin_str, sizeof(fin_str), ERL_NIF_LATIN1)) {
fin = (strcmp(fin_str, "true") == 0);
} else {
return enif_make_badarg(env);
}
/* Send headers */
int ret = quic_conn_send_headers(conn, stream_id, env, argv[2], fin);
if (ret < 0) {
return make_error(env, ATOM_NOT_CONNECTED);
}
return ATOM_OK;
}
/*
* NIF: send_data(ConnRef, StreamId, Data, Fin) -> ok | {error, Reason}
*
* Sends data on a stream.
*/
static ERL_NIF_TERM
nif_send_data(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
/* Extract stream ID */
ErlNifSInt64 stream_id;
if (!enif_get_int64(env, argv[1], &stream_id)) {
return enif_make_badarg(env);
}
/* Extract data */
ErlNifBinary data_bin;
if (!enif_inspect_iolist_as_binary(env, argv[2], &data_bin)) {
return enif_make_badarg(env);
}
/* Extract fin flag */
char fin_str[6];
int fin = 0;
if (enif_get_atom(env, argv[3], fin_str, sizeof(fin_str), ERL_NIF_LATIN1)) {
fin = (strcmp(fin_str, "true") == 0);
} else {
return enif_make_badarg(env);
}
/* Send data */
int ret = quic_conn_send_data(conn, stream_id, data_bin.data, data_bin.size, fin);
if (ret < 0) {
return make_error(env, ATOM_NOT_CONNECTED);
}
return ATOM_OK;
}
/*
* NIF: reset_stream(ConnRef, StreamId, ErrorCode) -> ok | {error, Reason}
*
* Resets a stream with the given error code.
*/
static ERL_NIF_TERM
nif_reset_stream(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
/* Extract stream ID */
ErlNifSInt64 stream_id;
if (!enif_get_int64(env, argv[1], &stream_id)) {
return enif_make_badarg(env);
}
/* Extract error code */
unsigned long error_code;
if (!enif_get_ulong(env, argv[2], &error_code)) {
return enif_make_badarg(env);
}
/* Reset stream */
int ret = quic_conn_reset_stream(conn, stream_id, error_code);
if (ret < 0) {
return make_error(env, ATOM_NOT_CONNECTED);
}
return ATOM_OK;
}
/*
* NIF: handle_timeout(ConnRef, NowMs) -> NextTimeoutMs | infinity
*
* Handles connection timeouts for retransmission.
*/
static ERL_NIF_TERM
nif_handle_timeout(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
int64_t next_timeout = quic_conn_handle_timeout(conn);
if (next_timeout < 0) {
return enif_make_atom(env, "infinity");
}
return enif_make_int64(env, next_timeout);
}
/*
* NIF: peername(ConnRef) -> {ok, {IP, Port}} | {error, Reason}
*
* Returns the remote address of the connection.
*/
static ERL_NIF_TERM
nif_peername(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
struct sockaddr_storage addr;
socklen_t addrlen;
if (quic_conn_peername(conn, &addr, &addrlen) < 0) {
return make_error_str(env, "not_connected");
}
return make_ok(env, make_address(env, &addr, addrlen));
}
/*
* NIF: sockname(ConnRef) -> {ok, {IP, Port}} | {error, Reason}
*
* Returns the local address of the connection.
*/
static ERL_NIF_TERM
nif_sockname(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
struct sockaddr_storage addr;
socklen_t addrlen;
if (quic_conn_sockname(conn, &addr, &addrlen) < 0) {
return make_error_str(env, "not_connected");
}
return make_ok(env, make_address(env, &addr, addrlen));
}
/*
* NIF: setopts(ConnRef, Opts) -> ok | {error, Reason}
*
* Sets connection options.
*/
static ERL_NIF_TERM
nif_setopts(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
/* TODO: Implement option setting */
UNUSED(argv);
return ATOM_OK;
}
/*
* NIF: process(ConnRef) -> NextTimeoutMs | infinity
*
* Processes pending QUIC events (reads packets, handles timeouts).
* This is a dirty I/O NIF that should be called when:
* 1. The socket has data ready (after receiving {select, _, _, ready_input})
* 2. A timer has expired
*
* Returns the next timeout in milliseconds, or 'infinity' if no timeout needed.
* The caller should re-arm enif_select after this returns.
*/
static ERL_NIF_TERM
nif_process(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
UNUSED(argc);
QuicConn *conn;
if (!enif_get_resource(env, argv[0], QUIC_CONN_RESOURCE, (void **)&conn)) {
return enif_make_badarg(env);
}
/* Process pending I/O */
int timeout_ms = quic_conn_process(conn, env);
/* Re-arm enif_select for next packet (if socket still valid) */
if (conn->sockfd >= 0 &&
!__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
int sel_ret = enif_select(env, (ErlNifEvent)conn->sockfd,
ERL_NIF_SELECT_READ,
conn,
NULL,
conn->select_ref);
if (sel_ret >= 0) {
conn->select_armed = true;
}
}
if (timeout_ms < 0) {
return enif_make_atom(env, "infinity");
}
return enif_make_int(env, timeout_ms);
}
/* NIF function table - names must match Erlang _nif function names */
static ErlNifFunc nif_funcs[] = {
/* {name, arity, function, flags} */
{"connect_nif", 4, nif_connect, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"close_nif", 2, nif_close, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"open_stream_nif", 1, nif_open_stream, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"send_headers_nif", 4, nif_send_headers, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"send_data_nif", 4, nif_send_data, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"reset_stream_nif", 3, nif_reset_stream, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"handle_timeout_nif", 2, nif_handle_timeout, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"process_nif", 1, nif_process, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"peername_nif", 1, nif_peername, 0},
{"sockname_nif", 1, nif_sockname, 0},
{"setopts_nif", 2, nif_setopts, 0}
};
/* NIF load callback */
static int
nif_load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info)
{
UNUSED(priv_data);
UNUSED(load_info);
/* Initialize atoms */
init_atoms(env);
/* Initialize resource types */
if (!quic_conn_resource_init(env)) {
return -1;
}
return 0;
}
/* NIF upgrade callback */
static int
nif_upgrade(ErlNifEnv *env, void **priv_data, void **old_priv_data, ERL_NIF_TERM load_info)
{
UNUSED(old_priv_data);
return nif_load(env, priv_data, load_info);
}
/* NIF unload callback */
static void
nif_unload(ErlNifEnv *env, void *priv_data)
{
UNUSED(env);
UNUSED(priv_data);
/* Cleanup lsquic global state */
quic_global_cleanup();
}
/* NIF initialization */
ERL_NIF_INIT(hackney_quic, nif_funcs, nif_load, NULL, nif_upgrade, nif_unload)