Skip to content

Commit

Permalink
Handle error from inet:sockname/1 in mongoose_tcp_listener
Browse files Browse the repository at this point in the history
  • Loading branch information
arcusfelis committed Jun 28, 2022
1 parent c213d44 commit a022594
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 20 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
{suites, "tests", dynamic_domains_SUITE}.
{suites, "tests", auth_methods_for_c2s_SUITE}.
{suites, "tests", local_iq_SUITE}.
{suites, "tests", tcp_listener_SUITE}.

{config, ["test.config"]}.
{logdir, "ct_report"}.
Expand Down
1 change: 1 addition & 0 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
{suites, "tests", domain_removal_SUITE}.
{suites, "tests", auth_methods_for_c2s_SUITE}.
{suites, "tests", local_iq_SUITE}.
{suites, "tests", tcp_listener_SUITE}.

{config, ["dynamic_domains.config", "test.config"]}.

Expand Down
99 changes: 99 additions & 0 deletions big_tests/tests/tcp_listener_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
%%==============================================================================
%% Copyright 2012 Erlang Solutions Ltd.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%==============================================================================

-module(tcp_listener_SUITE).
-compile([export_all, nowarn_export_all]).

-include_lib("escalus/include/escalus.hrl").
-include_lib("common_test/include/ct.hrl").

-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).

%%--------------------------------------------------------------------
%% Suite configuration
%%--------------------------------------------------------------------

all() ->
[{group, main}].

groups() ->
G = [{main, [sequence], test_cases()}],
ct_helper:repeat_all_until_all_ok(G).

test_cases() ->
[inet_sockname_returns_error].

suite() ->
require_rpc_nodes([mim]) ++ escalus:suite().

%%--------------------------------------------------------------------
%% Init & teardown
%%--------------------------------------------------------------------

init_per_suite(Config) ->
escalus:init_per_suite(Config).

end_per_suite(Config) ->
escalus:end_per_suite(Config).

init_per_group(_GroupName, Config) ->
setup_meck(),
Config.

end_per_group(_GroupName, Config) ->
teardown_meck(),
Config.

init_per_testcase(CaseName, Config) ->
escalus:init_per_testcase(CaseName, Config).

end_per_testcase(CaseName, Config) ->
escalus:end_per_testcase(CaseName, Config).

setup_meck() ->
{Mod, Code} = rpc(mim(), dynamic_compile, from_string, [tcp_listener_helper_code()]),
rpc(mim(), code, load_binary, [Mod, "tcp_listener_helper.erl", Code]),
ok = rpc(mim(), meck, new, [mongoose_tcp_listener, [passthrough, no_link]]),
ok = rpc(mim(), tcp_listener_helper, setup_meck, []).

teardown_meck() ->
rpc(mim(), meck, unload, []).

tcp_listener_helper_code() ->
"-module(tcp_listener_helper).\n"
"-compile([export_all, nowarn_export_all]).\n"
"setup_meck() ->\n"
" meck:expect(mongoose_tcp_listener, read_connection_details, fun read_connection_details/2).\n\n"
"read_connection_details(Socket, Opts) ->\n"
" persistent_term:put(tcp_listener_helper_info, {Socket, self()}),\n"
" gen_tcp:close(Socket),\n"
" Result = meck:passthrough([Socket, Opts]),\n"
" persistent_term:put(tcp_listener_helper_result, Result),\n"
" Result.\n".

%%--------------------------------------------------------------------
%% Test cases
%%--------------------------------------------------------------------

%% Checks that the listener does not crash if inet:sockname/1 call returns an error
inet_sockname_returns_error(Config) ->
UserSpec = escalus_users:get_userspec(Config, alice),
escalus_connection:start(UserSpec),
{_Socket, Pid} = rpc(mim(), persistent_term, get, [tcp_listener_helper_info]),
%% The listener process is still alive
[_|_] = rpc:pinfo(Pid),
{error, einval} = rpc(mim(), persistent_term, get, [tcp_listener_helper_result]),
ok.
60 changes: 40 additions & 20 deletions src/mongoose_tcp_listener.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
-export([start_listener/1, start_link/1, init/1]).

%% Internal
-export([start_accept_loop/3, accept_loop/3]).
-export([start_accept_loop/3, accept_loop/3, read_connection_details/2]).

-ignore_xref([start_link/1, start_accept_loop/3]).
-ignore_xref([start_link/1, start_accept_loop/3, read_connection_details/2]).

-type options() :: #{module := module(),
port := inet:port_number(),
Expand Down Expand Up @@ -99,8 +99,8 @@ start_accept_loop(ListenSock, Module, Opts) ->
-spec accept_loop(Socket :: port(),
Module :: module(),
Opts :: options()) -> no_return().
accept_loop(ListenSocket, Module, Opts = #{proxy_protocol := ProxyProtocol}) ->
case do_accept(ListenSocket, ProxyProtocol) of
accept_loop(ListenSocket, Module, Opts) ->
case do_accept(ListenSocket, Opts) of
{ok, Socket, ConnectionDetails} ->
?LOG_INFO(#{what => tcp_accepted,
socket => Socket, handler_module => Module,
Expand All @@ -115,34 +115,54 @@ accept_loop(ListenSocket, Module, Opts = #{proxy_protocol := ProxyProtocol}) ->
?MODULE:accept_loop(ListenSocket, Module, Opts)
end.

-spec do_accept(gen_tcp:socket(), boolean()) ->
-spec do_accept(gen_tcp:socket(), options()) ->
{ok, gen_tcp:socket(), connection_details()} | {error, term()}.
do_accept(ListenSocket, ProxyProtocol) ->
do_accept(ListenSocket, Opts) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} when ProxyProtocol ->
read_proxy_header(Socket);
{ok, Socket} ->
{ok, {DestAddr, DestPort}} = inet:sockname(Socket),
{ok, {SrcAddr, SrcPort}} = inet:peername(Socket),
?MODULE:read_connection_details(Socket, Opts);
Other ->
Other
end.

-spec read_connection_details(gen_tcp:socket(), options()) ->
{ok, gen_tcp:socket(), connection_details()} | {error, term()}.
read_connection_details(Socket, #{proxy_protocol := true}) ->
read_proxy_header(Socket);
read_connection_details(Socket, _Opts) ->
case {inet:sockname(Socket), inet:peername(Socket)} of
{{ok, {DestAddr, DestPort}}, {ok, {SrcAddr, SrcPort}}} ->
{ok, Socket, #{proxy => false,
src_address => SrcAddr,
src_port => SrcPort,
dest_address => DestAddr,
dest_port => DestPort}};
Other ->
Other
gen_tcp:close(Socket),
{error, simple_reason(Other)}
end.

-spec read_proxy_header(gen_tcp:socket()) -> {ok, gen_tcp:socket(), connection_details()}.
simple_reason({{error, Reason}, _}) ->
Reason;
simple_reason({_, {error, Reason}}) ->
Reason.

-spec read_proxy_header(gen_tcp:socket()) ->
{ok, gen_tcp:socket(), connection_details()} | {error, term()}.
read_proxy_header(Socket) ->
{ok, ProxyInfo} = ranch_tcp:recv_proxy_header(Socket, 1000),
{ok, Socket, #{proxy => true,
src_address => maps:get(src_address, ProxyInfo),
src_port => maps:get(src_port, ProxyInfo),
dest_address => maps:get(dest_address, ProxyInfo),
dest_port => maps:get(dest_port, ProxyInfo),
version => maps:get(version, ProxyInfo)
}}.
case ranch_tcp:recv_proxy_header(Socket, 1000) of
{ok, ProxyInfo} ->
{ok, Socket, #{proxy => true,
src_address => maps:get(src_address, ProxyInfo),
src_port => maps:get(src_port, ProxyInfo),
dest_address => maps:get(dest_address, ProxyInfo),
dest_port => maps:get(dest_port, ProxyInfo),
version => maps:get(version, ProxyInfo)
}};
{error, Reason} ->
gen_tcp:close(Socket),
{error, Reason}
end.

-spec make_childspec(Id :: term(), ListenSock :: port(),
Module :: module(), Opts :: options()) ->
Expand Down

0 comments on commit a022594

Please sign in to comment.