Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,33 @@
{quic, sockname, 1},
{quic, close, 2},
{quic_connection, lookup, 1},
%% quic v0.11.0 new APIs
{quic, get_stats, 1},
{quic, send_ping, 1},
{quic, migrate, 1},
{quic, set_stream_deadline, 3},
%% hackney_quic wrapper functions called by hackney_conn
{hackney_quic, connect, 4},
{hackney_quic, close, 2},
{hackney_quic, process, 1},
{hackney_quic, peername, 1},
{hackney_quic, sockname, 1},
{hackney_quic, setopts, 2},
%% hackney_quic v0.11.0 new wrapper functions
{hackney_quic, get_stats, 1},
{hackney_quic, send_ping, 1},
{hackney_quic, migrate, 1},
{hackney_quic, set_stream_deadline, 3},
{hackney_quic, set_congestion_control, 2},
%% hackney_h3 functions called by hackney_conn
{hackney_h3, send_request, 6},
{hackney_h3, send_request_headers, 5},
{hackney_h3, send_body_chunk, 4},
{hackney_h3, finish_send_body, 3},
{hackney_h3, parse_response_headers, 1}
{hackney_h3, parse_response_headers, 1},
%% hackney_h3 v0.11.0 migration functions
{hackney_h3, migrate, 1},
{hackney_h3, get_connection_info, 1}
]}.

{cover_enabled, true}.
Expand All @@ -57,7 +71,7 @@

{deps, [
%% Pure Erlang QUIC for HTTP/3 support
{quic, "~>0.10.2"},
{quic, "~>0.11.0"},
{idna, "~>7.1.0"},
{mimerl, "~>1.4"},
{certifi, "~>2.16.0"},
Expand Down
42 changes: 40 additions & 2 deletions src/hackney_conn.erl
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@
enable_push = false :: false | pid(),

%% HTTP/3 support (QUIC)
%% QUIC connection reference from hackney_quic
h3_conn :: reference() | undefined,
%% QUIC connection pid from hackney_quic
h3_conn :: pid() | undefined,
%% Map of active HTTP/3 streams: StreamId => {From, StreamState}
h3_streams = #{} :: #{non_neg_integer() => {gen_statem:from() | pid(), atom() | tuple()}},
%% Current HTTP/3 stream ID for streaming body mode
Expand Down Expand Up @@ -958,6 +958,27 @@ connected(info, {quic, ConnRef, {transport_error, Code, Msg}},
%% QUIC transport error
handle_h3_error({transport_error, Code, Msg}, Data);

%% v0.11.0: QUIC connection migration events
connected(info, {quic, ConnRef, {path_validated, _PathInfo}},
#conn_data{h3_conn = ConnRef}) ->
%% Path validation completed - connection can now use the new path
keep_state_and_data;

connected(info, {quic, ConnRef, {migration_completed, _NewAddr}},
#conn_data{h3_conn = ConnRef}) ->
%% Connection migration completed successfully
keep_state_and_data;

connected(info, {quic, ConnRef, {path_challenge, _Token}},
#conn_data{h3_conn = ConnRef}) ->
%% Path challenge received - handled internally by QUIC layer
keep_state_and_data;

connected(info, {quic, ConnRef, {path_response, _Token}},
#conn_data{h3_conn = ConnRef}) ->
%% Path response received - path validation in progress
keep_state_and_data;

%% QUIC socket ready - drive event loop
connected(info, {select, _Resource, _Ref, ready_input},
#conn_data{h3_conn = ConnRef}) when ConnRef =/= undefined ->
Expand Down Expand Up @@ -1160,6 +1181,23 @@ streaming_body(info, {quic, ConnRef, {transport_error, Code, Msg}},
#conn_data{h3_conn = ConnRef} = Data) ->
handle_h3_error({transport_error, Code, Msg}, Data);

%% v0.11.0: QUIC connection migration events
streaming_body(info, {quic, ConnRef, {path_validated, _PathInfo}},
#conn_data{h3_conn = ConnRef}) ->
keep_state_and_data;

streaming_body(info, {quic, ConnRef, {migration_completed, _NewAddr}},
#conn_data{h3_conn = ConnRef}) ->
keep_state_and_data;

streaming_body(info, {quic, ConnRef, {path_challenge, _Token}},
#conn_data{h3_conn = ConnRef}) ->
keep_state_and_data;

streaming_body(info, {quic, ConnRef, {path_response, _Token}},
#conn_data{h3_conn = ConnRef}) ->
keep_state_and_data;

%% QUIC socket ready - drive event loop
streaming_body(info, {select, _Resource, _Ref, ready_input},
#conn_data{h3_conn = ConnRef}) when ConnRef =/= undefined ->
Expand Down
87 changes: 85 additions & 2 deletions src/hackney_h3.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@

-include("hackney_lib.hrl").

%% Suppress dialyzer warnings for redirect handling code (not yet fully integrated)
-dialyzer({nowarn_function, [
handle_redirect/11,
get_redirect_location/1,
resolve_redirect_url/2,
redirect_method/2,
do_request/7,
do_request_with_redirect/8
]}).

-export([
%% High-level API
is_available/0,
Expand All @@ -47,10 +57,13 @@
parse_response_headers/1,
%% Connection close
close/1,
close/2
close/2,
%% v0.11.0: Connection migration
migrate/1,
get_connection_info/1
]).

-type h3_conn() :: reference().
-type h3_conn() :: pid().
-type stream_id() :: non_neg_integer().
-type method() :: get | post | put | delete | head | options | patch | atom() | binary().
-type url() :: binary() | string().
Expand Down Expand Up @@ -377,6 +390,76 @@ close(ConnRef) ->
close(ConnRef, Reason) ->
hackney_quic:close(ConnRef, Reason).

%%====================================================================
%% v0.11.0: Connection Migration
%%====================================================================

%% @doc Trigger connection migration to a new network path.
%% This initiates path validation and CID rotation for unlinkability.
%% Useful when the network changes (e.g., WiFi to cellular).
-spec migrate(h3_conn()) -> ok | {error, term()}.
migrate(ConnRef) ->
hackney_quic:migrate(ConnRef).

%% @doc Get connection information including migration status.
%% Returns a map with:
%% - migration_state: idle | migrating | migrated
%% - last_migration_addr: the last address migrated to (if any)
%% - peername: current peer address
%% - sockname: current local address
-spec get_connection_info(h3_conn()) -> {ok, map()} | {error, term()}.
get_connection_info(ConnRef) ->
case hackney_quic:peername(ConnRef) of
{ok, PeerAddr} ->
case hackney_quic:sockname(ConnRef) of
{ok, LocalAddr} ->
%% Get migration state from hackney_quic
case get_conn_pid(ConnRef) of
{ok, Pid} ->
MigInfo = gen_server:call(Pid, get_migration_state),
case MigInfo of
{ok, #{migration_state := MigState, last_migration_addr := LastAddr}} ->
{ok, #{
peername => PeerAddr,
sockname => LocalAddr,
migration_state => MigState,
last_migration_addr => LastAddr
}};
_ ->
{ok, #{
peername => PeerAddr,
sockname => LocalAddr,
migration_state => idle,
last_migration_addr => undefined
}}
end;
error ->
{ok, #{
peername => PeerAddr,
sockname => LocalAddr,
migration_state => idle,
last_migration_addr => undefined
}}
end;
{error, _} = Error ->
Error
end;
{error, _} = Error ->
Error
end.

%% @private Get the gen_server pid for a connection reference.
get_conn_pid(ConnRef) ->
case ets:whereis(hackney_quic_conns) of
undefined ->
error;
_ ->
case ets:lookup(hackney_quic_conns, ConnRef) of
[{ConnRef, Pid}] -> {ok, Pid};
[] -> error
end
end.

%%====================================================================
%% Internal functions
%%====================================================================
Expand Down
Loading
Loading