diff --git a/Makefile b/Makefile index 80421040..3c9b577a 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,8 @@ clean: distclean fmt: - rebar3 fmt + @clang-format-12 -i c_src/* + @rebar3 fmt .PHONY: distclean diff --git a/c_src/quicer_config.c b/c_src/quicer_config.c index 50954918..837ce0ba 100644 --- a/c_src/quicer_config.c +++ b/c_src/quicer_config.c @@ -229,9 +229,9 @@ ServerLoadConfiguration(ErlNifEnv *env, } unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN] = { 0 }; + QUIC_BUFFER *alpn_buffers = NULL; - if (!load_alpn(env, option, &alpn_buffer_length, alpn_buffers)) + if (!load_alpn(env, option, &alpn_buffer_length, &alpn_buffers)) { return ATOM_ALPN; } @@ -240,14 +240,15 @@ ServerLoadConfiguration(ErlNifEnv *env, // Allocate/initialize the configuration object, with the configured ALPN // and settings. // - QUIC_STATUS Status = QUIC_STATUS_SUCCESS; - if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, - alpn_buffers, - alpn_buffer_length, - &Settings, - sizeof(Settings), - CredConfig, // Context - Configuration))) + QUIC_STATUS Status = MsQuic->ConfigurationOpen(Registration, + alpn_buffers, + alpn_buffer_length, + &Settings, + sizeof(Settings), + CredConfig, // Context + Configuration); + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + if (QUIC_FAILED(Status)) { return ATOM_STATUS(Status); } @@ -313,9 +314,9 @@ ClientLoadConfiguration(ErlNifEnv *env, } unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN]; + QUIC_BUFFER *alpn_buffers = NULL; - if (!load_alpn(env, options, &alpn_buffer_length, alpn_buffers)) + if (!load_alpn(env, options, &alpn_buffer_length, &alpn_buffers)) { ret = ATOM_ALPN; goto done; @@ -325,14 +326,15 @@ ClientLoadConfiguration(ErlNifEnv *env, // Allocate/initialize the configuration object, with the configured ALPN // and settings. // - QUIC_STATUS Status = QUIC_STATUS_SUCCESS; - if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, - alpn_buffers, - alpn_buffer_length, - &Settings, - sizeof(Settings), - NULL, - Configuration))) + QUIC_STATUS Status = MsQuic->ConfigurationOpen(Registration, + alpn_buffers, + alpn_buffer_length, + &Settings, + sizeof(Settings), + NULL, + Configuration); + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + if (QUIC_FAILED(Status)) { ret = ATOM_STATUS(Status); goto done; @@ -357,50 +359,83 @@ ClientLoadConfiguration(ErlNifEnv *env, return ret; } +/* +** load alpn from eterm options to the alpn_buffers +** @NOTE 1:caller must call free_alpn_buffers after use +*/ bool load_alpn(ErlNifEnv *env, const ERL_NIF_TERM *options, unsigned *alpn_buffer_length, - QUIC_BUFFER alpn_buffers[]) + QUIC_BUFFER **alpn_buffers) { ERL_NIF_TERM alpn_list; + assert(*alpn_buffers == NULL); if (!enif_get_map_value(env, *options, ATOM_ALPN, &alpn_list)) { return false; } - if (!enif_get_list_length(env, alpn_list, alpn_buffer_length)) + if (!enif_get_list_length(env, alpn_list, alpn_buffer_length) + || alpn_buffer_length == 0) { return false; } - ERL_NIF_TERM head, tail; + *alpn_buffers = malloc((*alpn_buffer_length) * sizeof(QUIC_BUFFER)); - if (!enif_get_list_cell(env, alpn_list, &head, &tail)) + if (!*alpn_buffers) { return false; } - for (int i = 0; i < (int)(*alpn_buffer_length); i++) + CxPlatZeroMemory(*alpn_buffers, (*alpn_buffer_length) * sizeof(QUIC_BUFFER)); + + ERL_NIF_TERM list, head, tail; + unsigned i = 0; + list = alpn_list; + while (enif_get_list_cell(env, list, &head, &tail)) { - // @todo check if PATH_MAX is the correct length - char str[PATH_MAX]; - if (enif_get_string(env, head, str, PATH_MAX, ERL_NIF_LATIN1) <= 0) + unsigned len = 0; +#if ERL_NIF_MINOR_VERSION > 16 + if (!enif_get_string_length(env, head, &len, ERL_NIF_LATIN1)) +#else + if (!enif_get_list_length(env, head, &len)) +#endif { - return false; + goto exit; } + len++; // for '\0' + char *str = malloc(len * sizeof(char)); - alpn_buffers[i].Buffer = (uint8_t *)str; - alpn_buffers[i].Length = strlen(str); - - if (!enif_get_list_cell(env, tail, &head, &tail) - && i + 1 < (int)(*alpn_buffer_length)) + if (enif_get_string(env, head, str, len, ERL_NIF_LATIN1) <= 0) { - return false; + free(str); + str = NULL; + goto exit; } - } + (*alpn_buffers)[i].Buffer = (uint8_t *)str; + (*alpn_buffers)[i].Length = len - 1; // msquic doesn't need '\0' + i++; + list = tail; + } return true; + +exit: + free_alpn_buffers(*alpn_buffers, i); + return false; +} + +void +free_alpn_buffers(QUIC_BUFFER *alpn_buffers, unsigned len) +{ + for (unsigned i = 0; i < len; i++) + { + free(alpn_buffers[i].Buffer); + } + free(alpn_buffers); + alpn_buffers = NULL; } bool diff --git a/c_src/quicer_config.h b/c_src/quicer_config.h index 6ef12a77..8972ce12 100644 --- a/c_src/quicer_config.h +++ b/c_src/quicer_config.h @@ -77,7 +77,10 @@ ERL_NIF_TERM ClientLoadConfiguration(ErlNifEnv *env, bool load_alpn(ErlNifEnv *env, const ERL_NIF_TERM *option, unsigned *alpn_buffer_length, - QUIC_BUFFER alpn_buffers[]); + QUIC_BUFFER **alpn_buffers); + +void free_alpn_buffers(QUIC_BUFFER *alpn_buffers, unsigned alpn_buffer_length); + bool load_verify(ErlNifEnv *env, const ERL_NIF_TERM *option, const bool default_verify); diff --git a/c_src/quicer_connection.c b/c_src/quicer_connection.c index 093093c1..1fb4e806 100644 --- a/c_src/quicer_connection.c +++ b/c_src/quicer_connection.c @@ -1054,7 +1054,7 @@ addr2eterm(ErlNifEnv *env, QUIC_ADDR *addr) enif_make_int(env, ntohs(ip[5])), enif_make_int(env, ntohs(ip[6])), enif_make_int(env, ntohs(ip[7]))), - enif_make_int(env, addr->Ipv6.sin6_port)); + enif_make_int(env, ntohs(addr->Ipv6.sin6_port))); } else { diff --git a/c_src/quicer_listener.c b/c_src/quicer_listener.c index 00126f6c..fa1c858d 100644 --- a/c_src/quicer_listener.c +++ b/c_src/quicer_listener.c @@ -410,7 +410,7 @@ listen2(ErlNifEnv *env, __unused_parm__ int argc, const ERL_NIF_TERM argv[]) // Now try to start listener unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN]; + QUIC_BUFFER *alpn_buffers = NULL; // Allow insecure, default is false ERL_NIF_TERM eisInsecure; @@ -420,16 +420,18 @@ listen2(ErlNifEnv *env, __unused_parm__ int argc, const ERL_NIF_TERM argv[]) l_ctx->allow_insecure = TRUE; } - if (!load_alpn(env, &options, &alpn_buffer_length, alpn_buffers)) + if (!load_alpn(env, &options, &alpn_buffer_length, &alpn_buffers)) { ret = ERROR_TUPLE_2(ATOM_ALPN); goto exit; } // Start Listener - if (QUIC_FAILED( - Status = MsQuic->ListenerStart( - l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address))) + Status = MsQuic->ListenerStart( + l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address); + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + + if (QUIC_FAILED(Status)) { TP_NIF_3(start_fail, (uintptr_t)(l_ctx->Listener), Status); HQUIC Listener = l_ctx->Listener; @@ -515,10 +517,9 @@ stop_listener1(ErlNifEnv *env, return ret; } +// For simplicity, we do not support to switch the Registration ERL_NIF_TERM -start_listener3(ErlNifEnv *env, - __unused_parm__ int argc, - const ERL_NIF_TERM argv[]) +start_listener3(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { ERL_NIF_TERM listener_handle = argv[0]; ERL_NIF_TERM elisten_on = argv[1]; @@ -526,7 +527,7 @@ start_listener3(ErlNifEnv *env, QuicerListenerCTX *l_ctx; unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN]; + QUIC_BUFFER *alpn_buffers = NULL; QUIC_ADDR Address = {}; int UdpPort = 0; @@ -534,6 +535,8 @@ start_listener3(ErlNifEnv *env, ERL_NIF_TERM ret = ATOM_OK; QUIC_STATUS Status = QUIC_STATUS_SUCCESS; + CXPLAT_FRE_ASSERT(argc == 3); + if (!enif_get_resource( env, listener_handle, ctx_listener_t, (void **)&l_ctx)) { @@ -561,31 +564,89 @@ start_listener3(ErlNifEnv *env, return ERROR_TUPLE_2(ATOM_BADARG); } - if (!load_alpn(env, &options, &alpn_buffer_length, alpn_buffers)) + QuicerConfigCTX *new_config_ctx = init_config_ctx(); + if (!new_config_ctx) + { + return ERROR_TUPLE_2(ATOM_ERROR_NOT_ENOUGH_MEMORY); + } + + QUIC_CREDENTIAL_CONFIG CredConfig = { 0 }; +#if defined(QUICER_USE_TRUSTED_STORE) + X509_STORE *trusted_store = NULL; + ret = eoptions_to_cred_config(env, options, &CredConfig, &trusted_store); +#else + ret = eoptions_to_cred_config(env, options, &CredConfig, NULL); +#endif // QUICER_USE_TRUSTED_STORE + + if (!IS_SAME_TERM(ret, ATOM_OK)) { - return ERROR_TUPLE_2(ATOM_ALPN); + return ERROR_TUPLE_2(ret); } + // =================================================== + // Safe to access l_ctx now + // =================================================== enif_mutex_lock(l_ctx->lock); + if (!l_ctx->Listener) { ret = ERROR_TUPLE_2(ATOM_CLOSED); goto exit; } - if (QUIC_FAILED( - Status = MsQuic->ListenerStart( - l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address))) + QuicerRegistrationCTX *target_r_ctx = NULL; + + // This is a read, do not need to bump the ref count + target_r_ctx = l_ctx->r_ctx ? l_ctx->r_ctx : G_r_ctx; + + ret = ServerLoadConfiguration(env, + &options, + target_r_ctx->Registration, + &new_config_ctx->Configuration, + &CredConfig); + free_certificate(&CredConfig); + + if (!IS_SAME_TERM(ret, ATOM_OK)) + { + enif_release_resource(new_config_ctx); + ret = ERROR_TUPLE_2(ret); + goto exit; + } + + QuicerConfigCTX *old_config_ctx = l_ctx->config_resource; + l_ctx->config_resource = new_config_ctx; + +#if defined(QUICER_USE_TRUSTED_STORE) + X509_STORE_free(l_ctx->trusted_store); + l_ctx->trusted_store = trusted_store; +#endif // QUICER_USE_TRUSTED_STORE + // Now we swap the config + + if (!load_alpn(env, &options, &alpn_buffer_length, &alpn_buffers)) + { + enif_release_resource(new_config_ctx); + ret = ERROR_TUPLE_2(ATOM_ALPN); + goto exit; + } + Status = MsQuic->ListenerStart( + l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address); + + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + + if (QUIC_FAILED(Status)) { TP_NIF_3(start_fail, (uintptr_t)(l_ctx->Listener), Status); ret = ERROR_TUPLE_3(ATOM_LISTENER_START_ERROR, ATOM_STATUS(Status)); + enif_release_resource(new_config_ctx); goto exit; } l_ctx->is_stopped = FALSE; + // the ongoing handshake will be completed with the old config + enif_release_resource(old_config_ctx); + exit: enif_mutex_unlock(l_ctx->lock); - return ret; } diff --git a/src/quicer.erl b/src/quicer.erl index af0c52eb..bf268460 100644 --- a/src/quicer.erl +++ b/src/quicer.erl @@ -265,7 +265,7 @@ reg_open(Profile) -> reg_close() -> quicer_nif:reg_close(). -%% @doc Start a stopped listener with listener handle. +%% @doc Start a stopped listener with listener handle with new Options. -spec start_listener(listener_handle(), listen_on(), listen_opts()) -> {ok, pid()} | {error, any()}. start_listener(Listener, Port, Options) when is_list(Options) -> @@ -274,7 +274,7 @@ start_listener(Listener, Port, Options) -> quicer_nif:start_listener(Listener, Port, Options). %% @doc Stop a started listener which could be closed or restarted later. --spec stop_listener(listener_handle()) -> ok. +-spec stop_listener(listener_handle()) -> ok | {error, any()}. stop_listener(Handle) -> case quicer_nif:stop_listener(Handle) of ok -> diff --git a/src/quicer_connection.erl b/src/quicer_connection.erl index 336ac8d6..f705177e 100644 --- a/src/quicer_connection.erl +++ b/src/quicer_connection.erl @@ -215,7 +215,7 @@ start_link(undefined, Listener, {_LOpts, COpts, _SOpts} = Opts, Sup) when is_map start_link(CallbackModule, Listener, Opts, Sup) -> gen_server:start_link(?MODULE, [CallbackModule, Listener, Opts, Sup], []). --spec get_cb_state(ConnPid :: pid()) -> {ok, cb_state()} | {error, any()}. +-spec get_cb_state(ConnPid :: pid()) -> cb_state() | {error, any()}. get_cb_state(ConnPid) -> gen_server:call(ConnPid, get_cb_state, infinity). diff --git a/src/quicer_listener.erl b/src/quicer_listener.erl index 048c3d98..d68d3412 100644 --- a/src/quicer_listener.erl +++ b/src/quicer_listener.erl @@ -21,7 +21,11 @@ -export([ start_link/3, start_listener/3, - stop_listener/1 + stop_listener/1, + lock/2, + unlock/2, + reload/2, + get_handle/2 ]). %% gen_server callbacks @@ -35,9 +39,11 @@ -record(state, { name :: atom(), + listen_on :: quicer:listen_on(), listener :: quicer:listener_handle(), conn_sup :: pid(), - alpn :: [string()] + alpn :: [string()], + opts :: quicer:listener_opts() }). -export_type([listener_name/0]). @@ -72,6 +78,26 @@ start_listener(Name, ListenOn, Options) -> stop_listener(Name) -> quicer_listener_sup:stop_listener(Name). +-spec lock(pid(), timeout()) -> ok | {error, _}. +lock(Pid, Timeout) -> + gen_server:call(Pid, lock, Timeout). + +-spec unlock(pid(), timeout()) -> ok | {error, _}. +unlock(Pid, Timeout) -> + gen_server:call(Pid, unlock, Timeout). + +%% @doc Reload the listener with new *listener* opts. +%% @NOTE: the acceptor opts and stream opts are not reloaded. +%%% if you want to reload them, you should restart the listener (terminate and spawn). +%% @end +-spec reload(pid(), NewConf :: map()) -> ok | {error, _}. +reload(Pid, NewConf) -> + gen_server:call(Pid, {reload, NewConf}, infinity). + +-spec get_handle(pid(), timeout()) -> quicer:listener_handle(). +get_handle(Pid, Timeout) -> + gen_server:call(Pid, get_handle, Timeout). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== @@ -98,8 +124,10 @@ init([Name, ListenOn, {#{conn_acceptors := N, alpn := Alpn} = LOpts, _COpts, _SO _ = [{ok, _} = supervisor:start_child(ConnSup, [ConnSup]) || _ <- lists:seq(1, N)], {ok, #state{ name = Name, + listen_on = ListenOn, listener = L, conn_sup = ConnSup, + opts = LOpts, alpn = Alpn }}. @@ -118,8 +146,28 @@ init([Name, ListenOn, {#{conn_acceptors := N, alpn := Alpn} = LOpts, _COpts, _SO | {noreply, NewState :: term(), hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: term()} | {stop, Reason :: term(), NewState :: term()}. -handle_call(_Request, _From, State) -> - Reply = ok, +handle_call(get_handle, _From, State) -> + {reply, {ok, State#state.listener}, State}; +handle_call(lock, _From, State) -> + Res = quicer:stop_listener(State#state.listener), + {reply, Res, State}; +handle_call(unlock, _From, State) -> + Res = quicer:start_listener( + State#state.listener, + State#state.listen_on, + State#state.opts + ), + {reply, Res, State}; +handle_call({reload, NewConf}, _From, State) -> + _ = quicer:stop_listener(State#state.listener), + Res = quicer:start_listener( + State#state.listener, + State#state.listen_on, + NewConf + ), + {reply, Res, State}; +handle_call(Request, _From, State) -> + Reply = {error, {unimpl, Request}}, {reply, Reply, State}. %%-------------------------------------------------------------------- diff --git a/test/example_client_connection.erl b/test/example_client_connection.erl index 2dee73a2..9dfc77bc 100644 --- a/test/example_client_connection.erl +++ b/test/example_client_connection.erl @@ -134,10 +134,12 @@ local_address_changed(_C, _NewAddr, S) -> streams_available(_C, {_BidirCnt, _UnidirCnt}, S) -> {hibernate, S}. -peer_needs_streams(C, #{unidi_streams := Current}, S) -> +peer_needs_streams(C, unidi_streams, S) -> + {ok, Current} = quicer:getopt(C, param_conn_local_unidi_stream_count), ok = quicer:setopt(C, param_conn_settings, #{peer_unidi_stream_count => Current + 1}), {ok, S}; -peer_needs_streams(C, #{bidi_streams := Current}, S) -> +peer_needs_streams(C, bidi_streams, S) -> + {ok, Current} = quicer:getopt(C, param_conn_local_bidi_stream_count), ok = quicer:setopt(C, param_conn_settings, #{peer_bidi_stream_count => Current + 1}), {ok, S}. diff --git a/test/example_client_stream.erl b/test/example_client_stream.erl index 54b615fc..32a4a407 100644 --- a/test/example_client_stream.erl +++ b/test/example_client_stream.erl @@ -30,7 +30,8 @@ stream_closed/3, peer_accepted/3, passive/3, - handle_call/3 + handle_call/3, + handle_info/2 ]). -export([handle_stream_data/4]). @@ -115,9 +116,6 @@ passive(Stream, undefined, S) -> ct:fail("Steam ~p go into passive mode", [Stream]), {ok, S}. -handle_call(_Request, _From, S) -> - {reply, {error, not_impl}, S}. - stream_closed( _Stream, #{ @@ -138,3 +136,9 @@ stream_closed( is_integer(Code) -> {stop, normal, S}. + +handle_call(_Request, _From, S) -> + {reply, {error, not_impl}, S}. + +handle_info(_, S) -> + {ok, S}. diff --git a/test/quicer_listener_SUITE.erl b/test/quicer_listener_SUITE.erl index 120d1b39..9543b30d 100644 --- a/test/quicer_listener_SUITE.erl +++ b/test/quicer_listener_SUITE.erl @@ -20,6 +20,8 @@ -include_lib("stdlib/include/assert.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-include_lib("quicer/include/quicer.hrl"). + -compile(export_all). -compile(nowarn_export_all). @@ -382,6 +384,176 @@ tc_stop_start_listener_with_new_port(Config) -> gen_udp:close(Sock1), ok = quicer:close_listener(L). +tc_listener_lock(Config) -> + process_flag(trap_exit, true), + ServerConnCallback = example_server_connection, + ServerStreamCallback = example_server_stream, + Port = select_port(), + application:ensure_all_started(quicer), + ListenerOpts = [ + {conn_acceptors, 32}, + {peer_bidi_stream_count, 0}, + {peer_unidi_stream_count, 2} + | default_listen_opts(Config) + ], + ConnectionOpts = [ + {conn_callback, ServerConnCallback}, + {stream_acceptors, 2} + | default_conn_opts() + ], + StreamOpts = [ + {stream_callback, ServerStreamCallback} + | default_stream_opts() + ], + Options = {ListenerOpts, ConnectionOpts, StreamOpts}, + + %% GIVEN: A QUIC connection between example client and example server + {ok, QuicApp} = quicer:spawn_listener(sample, Port, Options), + ClientConnOpts = default_conn_opts_verify(Config, ca), + {ok, ClientConnPid} = example_client_connection:start_link( + "localhost", + Port, + {ClientConnOpts, default_stream_opts()} + ), + + %% ensure conn successfully established before lock + #{is_resumed := false} = snabbkaffe:retry( + 50, + 20, + fun() -> + #{is_resumed := false} = quicer_connection:get_cb_state(ClientConnPid) + end + ), + + %% WHEN: the listener is locked + ok = quicer_listener:lock(QuicApp, infinity), + + %% THEN: 1) new connection should be rejected + ?assertMatch( + {error, transport_down, #{error := 1, status := Status}} when + connection_idle == Status orelse unreachable == Status, + quicer:connect( + "localhost", + Port, + default_conn_opts_verify(Config, 'ca'), + 2000 + ) + ), + + %% THEN: 2) existing client connection should be kept, and traffic still works + Handle = quicer_connection:get_handle(ClientConnPid), + ?assertMatch({ok, {_, Port}}, quicer:peername(Handle)), + + {ok, LocalStream} = quicer:async_csend( + Handle, + <<"hello_after_lock_listener">>, + [{active, true}], + %?QUIC_SEND_FLAG_NONE + ?QUIC_SEND_FLAG_FIN + ), + receive + {quic, <<"hello_after_lock_listener">>, LocalStream, _} -> + ct:pal("Client received hello_after_lock_listener from ~p", [LocalStream]), + ok + end, + + gen_server:stop(ClientConnPid), + quicer_listener:stop_listener(QuicApp), + ok. + +tc_listener_conf_reload(Config) -> + process_flag(trap_exit, true), + DataDir = ?config(data_dir, Config), + ServerConnCallback = example_server_connection, + ServerStreamCallback = example_server_stream, + Port = select_port(), + application:ensure_all_started(quicer), + ListenerOpts = [ + {conn_acceptors, 32}, + {peer_bidi_stream_count, 0}, + {peer_unidi_stream_count, 1} + | default_listen_opts(Config) + ], + ConnectionOpts = [ + {conn_callback, ServerConnCallback}, + {stream_acceptors, 2} + | default_conn_opts() + ], + StreamOpts = [ + {stream_callback, ServerStreamCallback} + | default_stream_opts() + ], + Options = {ListenerOpts, ConnectionOpts, StreamOpts}, + + %% Given a QUIC connection between example client and example server + {ok, QuicApp} = quicer:spawn_listener(sample, Port, Options), + ClientConnOpts = default_conn_opts_verify(Config, ca), + {ok, ClientConnPid} = example_client_connection:start_link( + "localhost", + Port, + {ClientConnOpts, default_stream_opts()} + ), + + ct:pal("C1 status : ~p", [sys:get_status(ClientConnPid)]), + {ok, LHandle} = quicer_listener:get_handle(QuicApp, 5000), + + %% WHEN: the listener is reloaded with new listener opts (New cert, key and cacert). + ok = quicer_listener:lock(QuicApp, infinity), + ok = quicer_listener:unlock(QuicApp, infinity), + NewListenerOpts = + ListenerOpts ++ + [ + {certfile, filename:join(DataDir, "other-server.pem")}, + {keyfile, filename:join(DataDir, "other-server.key")}, + {cacertfile, filename:join(DataDir, "other-ca.pem")} + ], + ok = quicer_listener:reload(QuicApp, NewListenerOpts), + %% THEN: the listener handle is unchanged + ?assertEqual({ok, LHandle}, quicer_listener:get_handle(QuicApp, 5000)), + + %% THEN: start new connection with old cacert must fail + ?assertMatch( + {error, transport_down, #{error := _, status := Status}} when + Status =:= bad_certificate; + Status =:= cert_untrusted_root; + Status =:= handshake_failure, + quicer:connect( + "localhost", + Port, + default_conn_opts_verify(Config, 'ca'), + 5000 + ) + ), + %% WHEN: start new connection with new cacert + {ok, Conn2} = quicer:connect( + "localhost", + Port, + default_conn_opts_verify(Config, 'other-ca'), + 5000 + ), + + %% THEN: the new connection shall be established and traffic can be sent and received + {ok, Stream2} = quicer:start_stream( + Conn2, + #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL} + ), + {ok, _} = quicer:send(Stream2, <<"ping_from_conn_2">>), + + receive + {quic, new_stream, Stream2Remote, #{is_orphan := true}} -> + quicer:setopt(Stream2Remote, active, true), + ok + end, + + receive + {quic, <<"ping_from_conn_2">>, Stream2Remote, _} -> ok + after 2000 -> + ct:fail("nothing from conn 2"), + quicer_test_lib:report_unhandled_messages() + end, + gen_server:stop(ClientConnPid), + quicer_listener:stop_listener(QuicApp). + tc_stop_close_listener(Config) -> Port = select_port(), {ok, L} = quicer:listen(Port, default_listen_opts(Config)), @@ -629,8 +801,13 @@ default_listener_opts(Config, Verify) -> | tl(default_listen_opts(Config)) ]. -%%%_* Emacs ==================================================================== -%%% Local Variables: -%%% allout-layout: t -%%% erlang-indent-level: 2 -%%% End: +default_conn_opts_verify(Config, Ca) -> + DataDir = ?config(data_dir, Config), + CACertFile = filename:join(DataDir, Ca) ++ ".pem", + [ + {verify, verify_peer}, + {cacertfile, CACertFile}, + {alpn, ["sample"]}, + %% {sslkeylogfile, "/tmp/SSLKEYLOGFILE"}, + {idle_timeout_ms, 5000} + ]. diff --git a/test/quicer_test_lib.erl b/test/quicer_test_lib.erl index 871ca88c..141db8db 100644 --- a/test/quicer_test_lib.erl +++ b/test/quicer_test_lib.erl @@ -289,6 +289,7 @@ generate_tls_certs(Config) -> gen_host_cert("client", "ca", DataDir), gen_ca(DataDir, "other-ca"), gen_host_cert("other-client", "other-ca", DataDir), + gen_host_cert("other-server", "other-ca", DataDir), gen_host_cert("server-password", "ca", DataDir, #{password => ?SERVER_KEY_PASSWORD}), %% New certs for TLS chain tests