Skip to content

Commit cf8a3c3

Browse files
committed
feat(http3): expose peername/sockname/peercert
Wires the three calls through to the underlying quic connection via quic_h3:get_quic_conn/1 (available since quic 1.0.0). setopts still returns {error, not_supported} since QUIC has no {active, once}-style socket model.
1 parent b26d1f9 commit cf8a3c3

5 files changed

Lines changed: 74 additions & 10 deletions

File tree

NEWS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ UNRELEASED
1919
`close/2`, `process/1`).
2020
- `hackney_qpack.erl` removed (~622 LOC); the QPACK codec lives in
2121
`quic_qpack` in the `quic` dependency.
22-
- H3 peername/sockname/setopts/peercert now return `{error, not_supported}`:
23-
the underlying `quic_h3` connection does not expose them.
22+
- H3 `peername`/`sockname`/`peercert` are wired through the underlying
23+
`quic` connection and work the same as for HTTP/1.1 and HTTP/2.
24+
`setopts` still returns `{error, not_supported}` since QUIC has no
25+
`{active, once}`-style socket model.
2426
- `rebar.config`: `quic` dependency pinned to hex `1.0.0`.
2527

2628
3.2.1 - 2026-03-01

rebar.config

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
{quic_h3, send_data, 4},
3232
{quic_h3, cancel, 3},
3333
{quic_h3, close, 1},
34+
{quic_h3, get_quic_conn, 1},
35+
{quic, peername, 1},
36+
{quic, sockname, 1},
37+
{quic, peercert, 1},
3438
%% hackney_h3 functions called by hackney_conn
3539
{hackney_h3, connect, 4},
3640
{hackney_h3, close, 2},
@@ -41,7 +45,10 @@
4145
{hackney_h3, send_data, 4},
4246
{hackney_h3, send_body_chunk, 4},
4347
{hackney_h3, finish_send_body, 3},
44-
{hackney_h3, parse_response_headers, 1}
48+
{hackney_h3, parse_response_headers, 1},
49+
{hackney_h3, peername, 1},
50+
{hackney_h3, sockname, 1},
51+
{hackney_h3, peercert, 1}
4552
]}.
4653

4754
{cover_enabled, true}.

src/hackney_conn.erl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,19 +1509,19 @@ handle_common({call, From}, sockname, _State, #conn_data{transport = Transport,
15091509
Result = Transport:sockname(Socket),
15101510
{keep_state_and_data, [{reply, From, Result}]};
15111511

1512-
%% HTTP/3 socket operations — peername/sockname/peercert/setopts not exposed
1513-
%% by the underlying quic_h3 connection.
1512+
%% HTTP/3 socket operations — peername/sockname/peercert delegated to the
1513+
%% underlying quic connection; setopts has no analog over QUIC.
15141514
handle_common({call, From}, peername, _State,
15151515
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1516-
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
1516+
{keep_state_and_data, [{reply, From, hackney_h3:peername(H3Conn)}]};
15171517

15181518
handle_common({call, From}, sockname, _State,
15191519
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1520-
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
1520+
{keep_state_and_data, [{reply, From, hackney_h3:sockname(H3Conn)}]};
15211521

15221522
handle_common({call, From}, peercert, _State,
15231523
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1524-
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
1524+
{keep_state_and_data, [{reply, From, hackney_h3:peercert(H3Conn)}]};
15251525

15261526
handle_common({call, From}, {setopts, _Opts}, _State,
15271527
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->

src/hackney_h3.erl

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
reset_stream/3,
5656
handle_timeout/2,
5757
process/1,
58-
get_fd/1
58+
get_fd/1,
59+
peername/1,
60+
sockname/1,
61+
peercert/1
5962
]).
6063

6164
%% gen_server callbacks
@@ -602,6 +605,25 @@ handle_timeout(_ConnRef, _NowMs) ->
602605
process(_ConnRef) ->
603606
infinity.
604607

608+
-spec peername(reference()) ->
609+
{ok, {inet:ip_address(), inet:port_number()}} | {error, term()}.
610+
peername(ConnRef) ->
611+
quic_call(ConnRef, peername).
612+
613+
-spec sockname(reference()) ->
614+
{ok, {inet:ip_address(), inet:port_number()}} | {error, term()}.
615+
sockname(ConnRef) ->
616+
quic_call(ConnRef, sockname).
617+
618+
-spec peercert(reference()) -> {ok, binary()} | {error, term()}.
619+
peercert(ConnRef) ->
620+
quic_call(ConnRef, peercert).
621+
622+
quic_call(ConnRef, Op) ->
623+
with_pid(ConnRef,
624+
fun(Pid) -> gen_server:call(Pid, {quic_op, Op}) end,
625+
{error, not_connected}).
626+
605627
%%====================================================================
606628
%% gen_server callbacks
607629
%%====================================================================
@@ -640,6 +662,15 @@ handle_call({send_data, StreamId, Data, Fin}, _From, #state{h3_conn = Conn} = St
640662
handle_call({reset_stream, StreamId, ErrorCode}, _From, #state{h3_conn = Conn} = State) ->
641663
{reply, quic_h3:cancel(Conn, StreamId, ErrorCode), State};
642664

665+
handle_call({quic_op, Op}, _From, #state{h3_conn = Conn} = State)
666+
when Op =:= peername; Op =:= sockname; Op =:= peercert ->
667+
Reply = try
668+
quic:Op(quic_h3:get_quic_conn(Conn))
669+
catch
670+
_:Reason -> {error, Reason}
671+
end,
672+
{reply, Reply, State};
673+
643674
handle_call(_Request, _From, State) ->
644675
{reply, {error, unknown_request}, State}.
645676

test/hackney_conn_http3_tests.erl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ http3_conn_test_() ->
3838
fun setup/0, fun cleanup/1,
3939
[
4040
{"HTTP/3 connection and request", fun test_h3_connection_request/0},
41-
{"HTTP/3 get_protocol returns http3", fun test_h3_get_protocol/0}
41+
{"HTTP/3 get_protocol returns http3", fun test_h3_get_protocol/0},
42+
{"HTTP/3 peername/sockname/peercert", fun test_h3_peer_info/0}
4243
]
4344
}
4445
}.
@@ -115,6 +116,29 @@ test_h3_get_protocol() ->
115116
hackney_conn:stop(Pid)
116117
end.
117118

119+
test_h3_peer_info() ->
120+
{ok, Pid} = hackney_conn:start_link(#{
121+
host => "cloudflare.com",
122+
port => 443,
123+
transport => hackney_ssl,
124+
connect_options => [{protocols, [http3]}],
125+
connect_timeout => 15000
126+
}),
127+
case hackney_conn:connect(Pid, 15000) of
128+
ok ->
129+
?assertEqual(http3, hackney_conn:get_protocol(Pid)),
130+
?assertMatch({ok, {_, _}}, hackney_conn:peername(Pid)),
131+
?assertMatch({ok, {_, _}}, hackney_conn:sockname(Pid)),
132+
case hackney_conn:peercert(Pid) of
133+
{ok, Cert} -> ?assert(is_binary(Cert));
134+
{error, no_peercert} -> ok
135+
end,
136+
hackney_conn:stop(Pid);
137+
{error, Reason} ->
138+
hackney_conn:stop(Pid),
139+
?debugFmt("H3 peer info test skipped: ~p", [Reason])
140+
end.
141+
118142
%%====================================================================
119143
%% hackney_h3 Module Tests
120144
%%====================================================================

0 commit comments

Comments
 (0)