Loïc Hoguin 2 лет назад
Родитель
Сommit
624cb955e5
5 измененных файлов с 218 добавлено и 5 удалено
  1. 4 1
      Makefile
  2. 3 3
      ebin/cowboy.app
  3. 1 1
      rebar.config
  4. 39 0
      src/cowboy.erl
  5. 171 0
      src/cowboy_http3.erl

+ 4 - 1
Makefile

@@ -15,9 +15,12 @@ CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl
 LOCAL_DEPS = crypto
 
 DEPS = cowlib ranch
-dep_cowlib = git https://github.com/ninenines/cowlib 2.12.1
+dep_cowlib = git https://github.com/ninenines/cowlib qpack
 dep_ranch = git https://github.com/ninenines/ranch 1.8.0
 
+DEPS += quicer
+dep_quicer = git https://github.com/emqx/quic main
+
 DOC_DEPS = asciideck
 
 TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper gun

+ 3 - 3
ebin/cowboy.app

@@ -1,9 +1,9 @@
 {application, 'cowboy', [
 	{description, "Small, fast, modern HTTP server."},
 	{vsn, "2.10.0"},
-	{modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_handler','cowboy_http','cowboy_http2','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket']},
+	{modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_handler','cowboy_http','cowboy_http2','cowboy_http3','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket']},
 	{registered, [cowboy_sup,cowboy_clock]},
-	{applications, [kernel,stdlib,crypto,cowlib,ranch]},
+	{applications, [kernel,stdlib,crypto,cowlib,ranch,quicer]},
 	{mod, {cowboy_app, []}},
 	{env, []}
-]}.
+]}.

+ 1 - 1
rebar.config

@@ -1,4 +1,4 @@
 {deps, [
-{cowlib,".*",{git,"https://github.com/ninenines/cowlib","2.12.1"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}}
+{cowlib,".*",{git,"https://github.com/ninenines/cowlib","qpack"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}},{quicer,".*",{git,"https://github.com/emqx/quic","main"}}
 ]}.
 {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}.

+ 39 - 0
src/cowboy.erl

@@ -16,6 +16,8 @@
 
 -export([start_clear/3]).
 -export([start_tls/3]).
+-export([start_quic/1]).
+-export([start_quic_test/0]).
 -export([stop_listener/1]).
 -export([set_env/3]).
 
@@ -61,6 +63,43 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
 	ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
 	ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).
 
+%% @todo Experimental function to start a barebone QUIC listener.
+%%       This will need to be reworked to be closer to Ranch
+%%       listeners and provide equivalent features.
+-spec start_quic(_) -> ok.
+start_quic(TransOpts) ->
+	{ok, _} = application:ensure_all_started(quicer),
+	Parent = self(),
+	Port = 4567,
+	SocketOpts0 = maps:get(socket_opts, TransOpts, []),
+	SocketOpts = [
+		{alpn, ["h3"]},
+		{peer_unidi_stream_count, 100}, %% @todo Good default?
+		{peer_bidi_stream_count, 100}
+	|SocketOpts0],
+	{ok, Listen} = quicer:listen(Port, SocketOpts),
+	spawn(fun AcceptLoop() ->
+		{ok, Conn} = quicer:accept(Listen, []),
+		{ok, Conn} = quicer:handshake(Conn),
+		Pid = spawn(fun() ->
+			receive go -> ok end,
+			cowboy_http3:init(Parent, Conn)
+		end),
+		ok = quicer:controlling_process(Conn, Pid),
+		Pid ! go,
+		AcceptLoop()
+	end),
+	ok.
+
+-spec start_quic_test() -> ok.
+start_quic_test() ->
+	start_quic(#{
+		socket_opts => [
+			{cert, "deps/quicer/test/quicer_SUITE_data/cert.pem"},
+			{key, "deps/quicer/test/quicer_SUITE_data/key.pem"}
+		]
+	}).
+
 ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) ->
 	{TransOpts, ConnectionType};
 ensure_connection_type(TransOpts) ->

+ 171 - 0
src/cowboy_http3.erl

@@ -0,0 +1,171 @@
+%% Copyright (c) 2023, Loïc Hoguin <essen@ninenines.eu>
+%%
+%% 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(cowboy_http3).
+
+-export([init/2]).
+
+-include_lib("quicer/include/quicer.hrl").
+
+-record(stream, {
+	id :: non_neg_integer(), %% @todo specs
+	dir :: unidi_local | unidi_remote | bidi,
+	ref :: any(), %% @todo specs
+	role :: undefined | req | control | push | encoder | decoder
+}).
+
+-record(state, {
+	parent :: pid(),
+	conn :: any(), %% @todo specs
+
+	%% Quick pointers for commonly used streams.
+	local_encoder_stream :: any(), %% @todo specs
+
+	%% Bidirectional streams are used for requests and responses.
+	streams = #{} :: map() %% @todo specs
+}).
+
+-spec init(_, _) -> no_return().
+init(Parent, Conn) ->
+	{ok, Conn} = quicer:async_accept_stream(Conn, []),
+	%% Immediately open a control, encoder and decoder stream.
+	{ok, ControlRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	quicer:send(ControlRef, <<0>>), %% @todo Also send settings frame.
+	{ok, ControlID} = quicer:get_stream_id(ControlRef),
+	{ok, EncoderRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	quicer:send(EncoderRef, <<2>>),
+	{ok, EncoderID} = quicer:get_stream_id(EncoderRef),
+	{ok, DecoderRef} = quicer:start_stream(Conn,
+		#{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+	quicer:send(DecoderRef, <<3>>),
+	{ok, DecoderID} = quicer:get_stream_id(DecoderRef),
+	%% Quick! Let's go!
+	loop(#state{parent=Parent, conn=Conn, local_encoder_stream=EncoderRef, streams=#{
+		ControlRef => #stream{id=ControlID, dir=unidi_local, ref=ControlRef, role=control},
+		EncoderRef => #stream{id=EncoderID, dir=unidi_local, ref=EncoderRef, role=encoder},
+		DecoderRef => #stream{id=DecoderID, dir=unidi_local, ref=DecoderRef, role=decoder}
+	}}).
+
+loop(State0=#state{conn=Conn}) ->
+	receive
+		%% Stream data.
+		{quic, Data, StreamRef, Props} when is_binary(Data) ->
+			State = stream_data(Data, State0, StreamRef, Props),
+			loop(State);
+		%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED
+		{quic, new_stream, StreamRef, Flags} ->
+			%% Conn does not change.
+			{ok, Conn} = quicer:async_accept_stream(Conn, []),
+			State = stream_new_remote(State0, StreamRef, Flags),
+			loop(State);
+		%% QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE
+		{quic, stream_closed, StreamRef, Flags} ->
+			State = stream_closed(State0, StreamRef, Flags),
+			loop(State);
+		%% QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE
+		%%
+		%% Connection closed.
+		{quic, closed, Conn, _Flags} ->
+			quicer:close_connection(Conn),
+			%% @todo terminate here?
+			ok;
+		%%
+		%% The following events are currently ignored either because
+		%% I do not know what they do or because we do not need to
+		%% take action.
+		%%
+		%% QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT
+		{quic, transport_shutdown, Conn, _Flags} ->
+			%% @todo Why isn't it BY_PEER when using curl?
+			loop(State0);
+		%% QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN
+		{quic, peer_send_shutdown, _StreamRef, undefined} ->
+			loop(State0);
+		%% QUIC_STREAM_EVENT_SEND_SHUTDOWN_COMPLETE
+		{quic, send_shutdown_complete, _StreamRef, _IsGraceful} ->
+			loop(State0);
+		Msg ->
+			logger:error("msg ~p", [Msg]),
+			loop(State0)
+	end.
+
+stream_new_remote(State=#state{streams=Streams}, StreamRef, Flags) ->
+	{ok, StreamID} = quicer:get_stream_id(StreamRef),
+	{StreamDir, Role} = case quicer:is_unidirectional(Flags) of
+		true -> {unidi_remote, undefined};
+		false -> {bidi, req}
+	end,
+	Stream = #stream{id=StreamID, dir=StreamDir, ref=StreamRef, role=Role},
+	logger:debug("new stream ~p", [Stream]),
+	State#state{streams=Streams#{StreamRef => Stream}}.
+
+stream_data(Data, State=#state{streams=Streams}, StreamRef, _Props) ->
+	#{StreamRef := Stream} = Streams,
+	stream_data2(Data, State, Stream).
+
+stream_data2(Data, State, Stream=#stream{role=req}) ->
+	stream_data_req(State, Data, Stream);
+stream_data2(_Data, State, _Stream=#stream{role=control}) ->
+	State; %stream_data_control(...);
+stream_data2(_Data, State, _Stream=#stream{role=encoder}) ->
+	State; %stream_data_encoder(...);
+stream_data2(_Data, State, _Stream=#stream{role=decoder}) ->
+	State; %stream_data_decoder(...);
+stream_data2(Data, State, Stream=#stream{role=undefined, dir=unidi_remote}) ->
+	stream_data_undefined(State, Data, Stream).
+
+%% @todo Frame type and length are using https://www.rfc-editor.org/rfc/rfc9000.html#name-variable-length-integer-enc
+%% @todo Check stream state and update it afterwards.
+stream_data_req(State=#state{local_encoder_stream=EncoderRef},
+		Req = <<1, _Len, FieldsBin/binary>>, #stream{ref=StreamRef}) ->
+	logger:debug("data ~p~nfields ~p", [Req, cow_qpack:decode_field_section(FieldsBin, 0, cow_qpack:init())]),
+	StreamID = quicer:get_stream_id(StreamRef),
+	{ok, Data, EncData, _} = cow_qpack:encode_field_section([
+		{<<":status">>, <<"200">>},
+		{<<"content-length">>, <<"12">>},
+		{<<"content-type">>, <<"text/plain">>}
+	], StreamID, cow_qpack:init()),
+	%% Send the encoder data.
+	quicer:send(EncoderRef, EncData),
+	%% Then the response data.
+	DataLen = iolist_size(Data),
+	quicer:send(StreamRef, [<<1, DataLen>>, Data]),
+	quicer:send(StreamRef, <<0,12,"Hello world!">>, ?QUIC_SEND_FLAG_FIN),
+%	quicer:shutdown_stream(StreamRef),
+	logger:debug("sent response ~p~nenc data ~p", [iolist_to_binary([<<1, DataLen>>, Data]), EncData]),
+	State.
+
+%% @todo stream_control
+%% @todo stream_encoder
+%% @todo stream_decoder
+
+%% @todo We should probably reject, not crash, unknown/bad types.
+stream_data_undefined(State, <<TypeBin, Rest/bits>>, Stream0) ->
+	Role = case TypeBin of
+		0 -> control;
+		2 -> encoder;
+		3 -> decoder
+	end,
+	Stream = Stream0#stream{role=Role},
+	stream_data2(Rest, stream_update(State, Stream), Stream).
+
+stream_closed(State=#state{streams=Streams0}, StreamRef, _Flags) ->
+	{_Stream, Streams} = maps:take(StreamRef, Streams0),
+	%% @todo terminate stream
+	State#state{streams=Streams}.
+
+stream_update(State=#state{streams=Streams}, Stream=#stream{ref=StreamRef}) ->
+	State#state{streams=Streams#{StreamRef => Stream}}.