From 46c53cd4fbdd51cd74fac9a611ea124fa946ee55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 13 May 2025 14:20:11 +0200 Subject: [PATCH] WIP fixes for Chromium --- src/cowboy.erl | 8 +++- src/cowboy_http3.erl | 5 ++- src/cowboy_webtransport.erl | 7 ++-- test/cowboy_test.erl | 1 + test/draft_h3_webtransport_SUITE.erl | 59 ++++++++++++++++++++++++++++ test/handlers/wt_echo_h.erl | 21 ++++++++++ 6 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 test/draft_h3_webtransport_SUITE.erl create mode 100644 test/handlers/wt_echo_h.erl diff --git a/src/cowboy.erl b/src/cowboy.erl index d46691f9..29b9788c 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -95,8 +95,12 @@ start_quic(Ref, TransOpts, ProtoOpts) -> end, SocketOpts = [ {alpn, ["h3"]}, %% @todo Why not binary? - {peer_unidi_stream_count, 3}, %% We only need control and QPACK enc/dec. - {peer_bidi_stream_count, 100} + {peer_unidi_stream_count, 100}, %% We only need control and QPACK enc/dec. + {peer_bidi_stream_count, 100}, + %% For WebTransport. @todo Also increase default unidi stream count. + %% @todo We probably don't want it enabled if WT isn't used. + {datagram_send_enabled, 1}, + {datagram_receive_enabled, 1} |SocketOpts2], _ListenerPid = spawn(fun() -> {ok, Listener} = quicer:listen(Port, SocketOpts), diff --git a/src/cowboy_http3.erl b/src/cowboy_http3.erl index d76683b1..7ad65c88 100644 --- a/src/cowboy_http3.erl +++ b/src/cowboy_http3.erl @@ -716,9 +716,10 @@ commands(State, Stream, [Error = {internal_error, _, _}|_Tail]) -> reset_stream(State, Stream, Error); %% Use a different protocol within the stream (CONNECT :protocol). %% @todo Make sure we error out when the feature is disabled. -commands(State0=#state{http3_machine=HTTP3Machine0}, Stream0=#stream{id=StreamID}, +commands(State0, Stream0=#stream{id=StreamID}, [{switch_protocol, Headers, cowboy_webtransport, WTState=#{}}|Tail]) -> State = info(stream_store(State0, Stream0), StreamID, {headers, 200, Headers}), + #state{http3_machine=HTTP3Machine0} = State, Stream1 = #stream{state=StreamState} = stream_get(State, StreamID), %% The stream becomes a WT session at that point. It is the %% parent stream of all streams in this WT session. The @@ -864,7 +865,7 @@ webtransport_event(State, SessionID, Event) -> ok. webtransport_commands(State, SessionID, Commands) -> - Session = #stream{status=webtransport_session} = stream_get(SessionID, State), + Session = #stream{status=webtransport_session} = stream_get(State, SessionID), wt_commands(State, Session, Commands). wt_commands(State, _, []) -> diff --git a/src/cowboy_webtransport.erl b/src/cowboy_webtransport.erl index 3ef4d9a5..6beaba4a 100644 --- a/src/cowboy_webtransport.erl +++ b/src/cowboy_webtransport.erl @@ -34,6 +34,7 @@ -export_type([opts/0]). -record(state, { + id :: cow_http3:stream_id(), parent :: pid(), opts = #{} :: opts(), handler :: module(), @@ -72,7 +73,7 @@ upgrade(Req=#{version := 'HTTP/3', pid := Pid, streamid := StreamID}, Env, Handl FilterFun -> FilterFun(Req) end, %% @todo add parent, ref, streamid here directly - State = #state{parent=Pid, opts=Opts, handler=Handler, req=FilteredReq}, + State = #state{id=StreamID, parent=Pid, opts=Opts, handler=Handler, req=FilteredReq}, %% @todo Must check is_upgrade_request (rename, not an upgrade) %% and also ensure that all the relevant settings are enabled (quic and h3) @@ -179,8 +180,8 @@ handler_call_result(State0, HandlerState, Commands) -> commands([], State, []) -> {ok, State}; -commands([], State=#state{parent=Pid}, Commands) -> - Pid ! {'$webtransport_commands', lists:reverse(Commands)}, +commands([], State=#state{id=SessionID, parent=Pid}, Commands) -> + Pid ! {'$webtransport_commands', SessionID, lists:reverse(Commands)}, {ok, State}; %% {open_stream, OpenStreamRef, StreamType, InitialData}. commands([Command={open_stream, _, _, _}|Tail], State, Acc) -> diff --git a/test/cowboy_test.erl b/test/cowboy_test.erl index 541e8f90..a17eddb7 100644 --- a/test/cowboy_test.erl +++ b/test/cowboy_test.erl @@ -53,6 +53,7 @@ init_http3(Ref, ProtoOpts, Config) -> }, {ok, Listener} = cowboy:start_quic(Ref, TransOpts, ProtoOpts), {ok, {_, Port}} = quicer:sockname(Listener), + ct:pal("port ~p", [Port]), %% @todo Keep listener information around in a better place. persistent_term:put({cowboy_test_quic, Ref}, Listener), [{ref, Ref}, {type, quic}, {protocol, http3}, {port, Port}, {opts, TransOpts}|Config]. diff --git a/test/draft_h3_webtransport_SUITE.erl b/test/draft_h3_webtransport_SUITE.erl new file mode 100644 index 00000000..442ec15b --- /dev/null +++ b/test/draft_h3_webtransport_SUITE.erl @@ -0,0 +1,59 @@ +%% Copyright (c) Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(draft_h3_webtransport_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +all() -> + [{group, enabled}]. + +groups() -> + Tests = ct_helper:all(?MODULE), + [{enabled, [], Tests}]. %% @todo Enable parallel when all is better. + +init_per_group(Name = enabled, Config) -> + cowboy_test:init_http3(Name, #{ + enable_connect_protocol => true, + h3_datagram => true, + enable_webtransport => true, %% For compatibility with draft-02. + webtransport_max_sessions => 10, + env => #{dispatch => cowboy_router:compile(init_routes(Config))} + }, Config). + +end_per_group(Name, _) -> + cowboy_test:stop_group(Name). + +init_routes(_) -> [ + {"localhost", [ + {"/wt", wt_echo_h, []} + ]} +]. + +%% Temporary. + +%% To start Chromium the command line is roughly: +%% chromium --ignore-certificate-errors-spki-list=LeLykt63i2FRAm+XO91yBoSjKfrXnAFygqe5xt0zgDA= --ignore-certificate-errors --user-data-dir=/tmp/chromium-wt --allow-insecure-localhost --webtransport-developer-mode --enable-quic https://googlechrome.github.io/samples/webtransport/client.html +%% +%% To find the SPKI the command is roughly: +%% openssl x509 -in ~/ninenines/cowboy/test/rfc9114_SUITE_data/server.pem -pubkey -noout | \ +%% openssl pkey -pubin -outform der | \ +%% openssl dgst -sha256 -binary | \ +%% openssl enc -base64 + +run(_Config) -> + timer:sleep(infinity). diff --git a/test/handlers/wt_echo_h.erl b/test/handlers/wt_echo_h.erl new file mode 100644 index 00000000..97056cf6 --- /dev/null +++ b/test/handlers/wt_echo_h.erl @@ -0,0 +1,21 @@ +%% This module echoes client events back, +%% including creating new streams. + +-module(wt_echo_h). +%% @todo -behavior(cowboy_webtransport). + +-export([init/2]). +-export([webtransport_handle/2]). + +init(Req, _) -> + {cowboy_webtransport, Req, undefined}. + +%% @todo WT handle {stream_open,4,bidi} +%% @todo WT handle {stream_data,4,nofin,<<>>} %% skip? + +webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, HandlerState) -> + ct:pal("WT handle ~p~n", [Event]), + {[{send, StreamID, Data}], HandlerState}; +webtransport_handle(Event, HandlerState) -> + ct:pal("WT handle ~p~n", [Event]), + {[], HandlerState}.