Packages
macula
0.40.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
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.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/macula_quic.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% Main API module for Macula QUIC transport.
%%% Provides a simplified wrapper around the quicer library.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_quic).
-include_lib("kernel/include/logger.hrl").
-export([
listen/2,
connect/4,
accept/2,
accept_stream/2,
open_stream/1,
send/2,
async_send/2,
recv/2,
close/1,
peername/1
]).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Start a QUIC listener on a specific address and port.
%%
%% The first argument can be:
%% - `Port' (integer) — binds to 0.0.0.0:Port (all IPv4, Docker-safe)
%% - `{Address, Port}' — binds to Address:Port (IPv4 or IPv6)
%% Address is a string: "0.0.0.0", "192.168.1.1", "2600:3c0e:e001:ec::100"
%%
%% Options:
%% {cert, CertFile} - Path to PEM certificate file
%% {key, KeyFile} - Path to PEM private key file
%% {alpn, [Protocol]} - List of ALPN protocols (e.g., ["macula"])
%% {peer_unidi_stream_count, N} - Max unidirectional streams
%% {peer_bidi_stream_count, N} - Max bidirectional streams
%% {idle_timeout_ms, N} - Connection idle timeout in milliseconds
%% {keep_alive_interval_ms, N} - Keep-alive PING interval in milliseconds
%% @end
-spec listen(inet:port_number() | {string(), inet:port_number()}, list()) ->
{ok, reference()} | {error, term()}.
listen({Address, Port}, Opts) ->
listen_on(format_listen_on(Address, Port), Opts);
listen(Port, Opts) when is_integer(Port) ->
listen_on("0.0.0.0:" ++ integer_to_list(Port), Opts).
%% @private Format address:port string for quicer.
%% IPv6 addresses are wrapped in brackets per RFC 2732.
format_listen_on(Address, Port) ->
PortStr = integer_to_list(Port),
case string:find(Address, ":") of
nomatch -> Address ++ ":" ++ PortStr; %% IPv4: "1.2.3.4:4433"
_ -> "[" ++ Address ++ "]:" ++ PortStr %% IPv6: "[::1]:4433"
end.
%% @private Start listener on a formatted listen_on string.
listen_on(ListenOn, Opts) ->
CertFile = proplists:get_value(cert, Opts),
KeyFile = proplists:get_value(key, Opts),
AlpnProtocols = proplists:get_value(alpn, Opts, ["macula"]),
PeerUnidiStreamCount = proplists:get_value(peer_unidi_stream_count, Opts, 3),
PeerBidiStreamCount = proplists:get_value(peer_bidi_stream_count, Opts, 100),
IdleTimeoutMs = proplists:get_value(idle_timeout_ms, Opts, 60000),
KeepAliveIntervalMs = proplists:get_value(keep_alive_interval_ms, Opts, 20000),
HandshakeIdleTimeoutMs = proplists:get_value(handshake_idle_timeout_ms, Opts, 30000),
ListenerOpts = [
{certfile, CertFile},
{keyfile, KeyFile},
{alpn, AlpnProtocols},
{peer_unidi_stream_count, PeerUnidiStreamCount},
{peer_bidi_stream_count, PeerBidiStreamCount},
{idle_timeout_ms, IdleTimeoutMs},
{keep_alive_interval_ms, KeepAliveIntervalMs},
{handshake_idle_timeout_ms, HandshakeIdleTimeoutMs}
],
?LOG_INFO("Starting listener on ~s with idle_timeout=~pms, keep_alive=~pms",
[ListenOn, IdleTimeoutMs, KeepAliveIntervalMs]),
quicer:listen(ListenOn, ListenerOpts).
%% @doc Connect to a QUIC server.
%% Options:
%% {alpn, [Protocol]} - List of ALPN protocols
%% {verify, none | verify_peer} - Certificate verification mode
%% {cacertfile, Path} - CA certificate bundle for verification (v0.16.3+)
%% {depth, N} - Max certificate chain depth (v0.16.3+)
%% {server_name_indication, Host} - SNI hostname (v0.16.3+)
%% {idle_timeout_ms, N} - Connection idle timeout in milliseconds
%% {keep_alive_interval_ms, N} - Keep-alive PING interval in milliseconds
%% @end
-spec connect(string() | inet:ip_address(), inet:port_number(), list(), timeout()) ->
{ok, reference()} | {error, term()}.
connect(Host, Port, Opts, Timeout) ->
%% Extract QUIC-specific options with defaults
AlpnProtocols = proplists:get_value(alpn, Opts, ["macula"]),
%% Timeout and keep-alive configuration for mesh stability
%% CRITICAL: Both endpoints negotiate idle timeout - the SMALLER value wins
%% So we must configure our client with proper values too
IdleTimeoutMs = proplists:get_value(idle_timeout_ms, Opts, 60000),
KeepAliveIntervalMs = proplists:get_value(keep_alive_interval_ms, Opts, 20000),
HandshakeIdleTimeoutMs = proplists:get_value(handshake_idle_timeout_ms, Opts, 30000),
%% Build base quicer options
BaseOpts = [
{alpn, AlpnProtocols},
{idle_timeout_ms, IdleTimeoutMs},
{keep_alive_interval_ms, KeepAliveIntervalMs},
{handshake_idle_timeout_ms, HandshakeIdleTimeoutMs}
],
%% Pass through ALL TLS options from macula_tls (v0.16.3+)
%% This includes: verify, cacertfile, depth, server_name_indication, verify_fun
TlsOptKeys = [verify, cacertfile, depth, server_name_indication, verify_fun, certfile, keyfile],
TlsOpts = [{K, V} || K <- TlsOptKeys, {ok, V} <- [safe_get_value(K, Opts)]],
QuicerOpts = BaseOpts ++ TlsOpts,
%% Log connection attempt with TLS mode info
VerifyMode = proplists:get_value(verify, Opts, none),
CACertFile = proplists:get_value(cacertfile, Opts, undefined),
?LOG_DEBUG("Connecting to ~s:~p with idle_timeout=~pms, keep_alive=~pms, verify=~p, cacertfile=~p",
[Host, Port, IdleTimeoutMs, KeepAliveIntervalMs, VerifyMode, CACertFile]),
?LOG_DEBUG("Full QuicerOpts: ~p", [QuicerOpts]),
quicer:connect(Host, Port, QuicerOpts, Timeout).
%% @doc Safely get a value from proplist, returning {ok, Value} or error.
%% @private
-spec safe_get_value(atom(), list()) -> {ok, term()} | error.
safe_get_value(Key, Opts) ->
case proplists:get_value(Key, Opts) of
undefined -> error;
Value -> {ok, Value}
end.
%% @doc Accept an incoming connection on a listener.
%% After accepting, the connection needs handshake to complete.
-spec accept(reference(), timeout()) -> {ok, reference()} | {error, term()}.
accept(ListenerPid, Timeout) ->
case quicer:accept(ListenerPid, [], Timeout) of
{ok, Conn} ->
%% Complete TLS handshake
case quicer:handshake(Conn) of
{ok, Conn} -> {ok, Conn};
Error -> Error
end;
Error ->
Error
end.
%% @doc Accept an incoming stream on a connection.
-spec accept_stream(reference(), timeout()) -> {ok, reference()} | {error, term()}.
accept_stream(ConnPid, _Timeout) ->
%% accept_stream/2 doesn't take timeout, it returns immediately
quicer:accept_stream(ConnPid, []).
%% @doc Open a new bidirectional stream on a connection.
-spec open_stream(reference()) -> {ok, reference()} | {error, term()}.
open_stream(ConnPid) ->
quicer:start_stream(ConnPid, []).
%% @doc Send data on a stream (blocking).
-spec send(reference(), iodata()) -> ok | {error, term()}.
send(StreamPid, Data) ->
case quicer:send(StreamPid, Data) of
{ok, _BytesSent} -> ok;
Error -> Error
end.
%% @doc Send data on a stream asynchronously (non-blocking).
%% This returns immediately without waiting for QUIC flow control.
-spec async_send(reference(), iodata()) -> ok | {error, term()}.
async_send(StreamPid, Data) ->
case quicer:async_send(StreamPid, Data) of
{ok, _BytesSent} -> ok;
Error -> Error
end.
%% @doc Receive data from a stream (blocking).
-spec recv(reference(), timeout()) -> {ok, binary()} | {error, term()}.
recv(StreamPid, Timeout) ->
%% quicer sends data as messages, so we need to receive from mailbox
receive
{quic, Data, StreamPid, _Props} ->
{ok, Data}
after Timeout ->
{error, timeout}
end.
%% @doc Close a listener, connection, or stream.
%% Tries stream, then connection, then listener close in sequence.
-spec close(reference()) -> ok.
close(Pid) ->
%% quicer uses different close functions based on resource type
close_as_stream(catch quicer:close_stream(Pid), Pid).
%% @private Stream close succeeded
close_as_stream(ok, _Pid) ->
ok;
%% @private Stream close failed - try as connection
close_as_stream(_, Pid) ->
close_as_connection(catch quicer:close_connection(Pid), Pid).
%% @private Connection close succeeded
close_as_connection(ok, _Pid) ->
ok;
%% @private Connection close failed - try as listener
close_as_connection(_, Pid) ->
_ = catch quicer:close_listener(Pid),
ok.
%% @doc Get the peer's address from a stream or connection handle.
%% Returns {ok, {IP, Port}} on success or {error, Reason} on failure.
%% Works with both stream and connection handles.
-spec peername(term()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}.
peername(Handle) ->
%% quicer:peername/1 works on both stream and connection handles
quicer:peername(Handle).