emqx/apps/emqx_dashboard/test/emqx_dashboard_https_SUITE.erl

335 lines
11 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% 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(emqx_dashboard_https_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(NAME, 'https:dashboard').
-define(HOST_HTTPS, "https://127.0.0.1:18084").
-define(HOST_HTTP, "http://127.0.0.1:18083").
-define(BASE_PATH, "/api/v5").
-define(OVERVIEWS, [
"alarms",
"banned",
"stats",
"metrics",
"listeners",
"clients",
"subscriptions"
]).
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> Config.
end_per_suite(_Config) -> emqx_mgmt_api_test_util:end_suite([emqx_management]).
init_per_testcase(_TestCase, Config) -> Config.
end_per_testcase(_TestCase, _Config) -> emqx_mgmt_api_test_util:end_suite([emqx_management]).
t_update_conf(_Config) ->
Conf = #{
dashboard => #{
listeners => #{
https => #{bind => 18084, ssl_options => #{depth => 5}},
http => #{bind => 18083}
}
}
},
emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
emqx_mgmt_api_test_util:init_suite([emqx_management], fun(X) -> X end),
Headers = emqx_dashboard_SUITE:auth_header_(),
{ok, Client1} = emqx_dashboard_SUITE:request_dashboard(
get, https_api_path(["clients"]), Headers
),
{ok, Client2} = emqx_dashboard_SUITE:request_dashboard(
get, http_api_path(["clients"]), Headers
),
Raw = emqx:get_raw_config([<<"dashboard">>]),
?assertEqual(
5,
emqx_utils_maps:deep_get(
[<<"listeners">>, <<"https">>, <<"ssl_options">>, <<"depth">>], Raw
)
),
?assertEqual(Client1, Client2),
?check_trace(
begin
Raw1 = emqx_utils_maps:deep_put(
[<<"listeners">>, <<"https">>, <<"bind">>], Raw, 0
),
?assertMatch({ok, _}, emqx:update_config([<<"dashboard">>], Raw1)),
?assertEqual(Raw1, emqx:get_raw_config([<<"dashboard">>])),
{ok, _} = ?block_until(#{?snk_kind := regenerate_minirest_dispatch}, 10000),
ok
end,
fun(ok, Trace) ->
%% Don't start new listener, so is empty
?assertMatch([#{listeners := []}], ?of_kind(regenerate_minirest_dispatch, Trace))
end
),
{ok, Client3} = emqx_dashboard_SUITE:request_dashboard(
get, http_api_path(["clients"]), Headers
),
?assertEqual(Client1, Client3),
?assertMatch(
{error,
{failed_connect, [
_,
{inet, [inet], econnrefused}
]}},
emqx_dashboard_SUITE:request_dashboard(get, https_api_path(["clients"]), Headers)
),
%% reset
?check_trace(
begin
?assertMatch({ok, _}, emqx:update_config([<<"dashboard">>], Raw)),
?assertEqual(Raw, emqx:get_raw_config([<<"dashboard">>])),
{ok, _} = ?block_until(#{?snk_kind := regenerate_minirest_dispatch}, 10000),
ok
end,
fun(ok, Trace) ->
%% start new listener('https:dashboard')
?assertMatch(
[#{listeners := ['https:dashboard']}], ?of_kind(regenerate_minirest_dispatch, Trace)
)
end
),
{ok, Client1} = emqx_dashboard_SUITE:request_dashboard(
get, https_api_path(["clients"]), Headers
),
{ok, Client2} = emqx_dashboard_SUITE:request_dashboard(
get, http_api_path(["clients"]), Headers
),
emqx_mgmt_api_test_util:end_suite([emqx_management]).
t_default_ssl_cert(_Config) ->
Conf = #{dashboard => #{listeners => #{https => #{bind => 18084}}}},
validate_https(Conf, 512, default_ssl_cert(), verify_none),
ok.
t_compatibility_ssl_cert(_Config) ->
MaxConnection = 1000,
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
cacertfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cacert.pem">>),
certfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cert.pem">>),
keyfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/key.pem">>),
max_connections => MaxConnection
}
}
}
},
validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none),
ok.
t_normal_ssl_cert(_Config) ->
MaxConnection = 1024,
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
ssl_options => #{
cacertfile => naive_env_interpolation(
<<"${EMQX_ETC_DIR}/certs/cacert.pem">>
),
certfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cert.pem">>),
keyfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/key.pem">>),
depth => 5
},
max_connections => MaxConnection
}
}
}
},
validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none),
ok.
t_verify_cacertfile(_Config) ->
MaxConnection = 1024,
DefaultSSLCert = default_ssl_cert(),
SSLCert = DefaultSSLCert#{cacertfile => <<"">>},
%% default #{verify => verify_none}
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
cacertfile => <<"">>,
max_connections => MaxConnection
}
}
}
},
validate_https(Conf, MaxConnection, SSLCert, verify_none),
%% verify_peer but cacertfile is empty
VerifyPeerConf1 = emqx_utils_maps:deep_put(
[dashboard, listeners, https, verify],
Conf,
verify_peer
),
emqx_common_test_helpers:load_config(emqx_dashboard_schema, VerifyPeerConf1),
?assertMatch({error, [?NAME]}, emqx_dashboard:start_listeners()),
%% verify_peer and cacertfile is ok.
VerifyPeerConf2 = emqx_utils_maps:deep_put(
[dashboard, listeners, https, cacertfile],
VerifyPeerConf1,
naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cacert.pem">>)
),
%% we always test client with verify_none and no client cert is sent
%% since the server is configured with verify_peer
%% hence the expected observation on the client side is an error
ErrorReason =
try
validate_https(VerifyPeerConf2, MaxConnection, DefaultSSLCert, verify_peer)
catch
error:{https_client_error, Reason} ->
Reason
end,
%% There seems to be a race-condition causing the return value to vary a bit
case ErrorReason of
socket_closed_remotely ->
ok;
{ssl_error, _SslSock, {tls_alert, {certificate_required, _}}} ->
ok;
Other ->
throw({unexpected, Other})
end.
t_bad_certfile(_Config) ->
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
certfile => <<"${EMQX_ETC_DIR}/certs/not_found_cert.pem">>
}
}
}
},
emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
?assertMatch({error, [?NAME]}, emqx_dashboard:start_listeners()),
ok.
validate_https(Conf, MaxConnection, SSLCert, Verify) ->
emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
emqx_mgmt_api_test_util:init_suite([emqx_management], fun(X) -> X end),
try
assert_ranch_options(MaxConnection, SSLCert, Verify),
assert_https_request()
after
emqx_mgmt_api_test_util:end_suite([emqx_management])
end.
assert_ranch_options(MaxConnections0, SSLCert, Verify) ->
Middlewares = [emqx_dashboard_middleware, cowboy_router, cowboy_handler],
[
?NAME,
ranch_ssl,
#{
max_connections := MaxConnections,
num_acceptors := _,
socket_opts := SocketOpts
},
cowboy_tls,
#{
env := #{
dispatch := {persistent_term, ?NAME},
options := #{
name := ?NAME,
protocol := https,
protocol_options := #{proxy_header := false},
security := [#{basicAuth := []}, #{bearerAuth := []}],
swagger_global_spec := _
}
},
middlewares := Middlewares,
proxy_header := false
}
] = ranch_server:get_listener_start_args(?NAME),
?assertEqual(MaxConnections0, MaxConnections),
?assert(lists:member(inet, SocketOpts), SocketOpts),
#{
backlog := 1024,
ciphers := Ciphers,
port := 18084,
send_timeout := 10000,
verify := Verify,
versions := Versions
} = SocketMaps = maps:from_list(SocketOpts -- [inet]),
%% without tlsv1.1 tlsv1
?assertMatch(['tlsv1.3', 'tlsv1.2'], Versions),
?assert(Ciphers =/= []),
maps:foreach(
fun(K, ConfVal) ->
case maps:find(K, SocketMaps) of
{ok, File} -> ?assertEqual(naive_env_interpolation(ConfVal), File);
error -> ?assertEqual(<<"">>, ConfVal)
end
end,
SSLCert
),
?assertMatch(
#{
env := #{dispatch := {persistent_term, ?NAME}},
middlewares := Middlewares,
proxy_header := false
},
ranch:get_protocol_options(?NAME)
),
ok.
assert_https_request() ->
Headers = emqx_dashboard_SUITE:auth_header_(),
lists:foreach(
fun(Path) ->
ApiPath = https_api_path([Path]),
case emqx_dashboard_SUITE:request_dashboard(get, ApiPath, Headers) of
{ok, _} -> ok;
{error, Reason} -> error({https_client_error, Reason})
end
end,
?OVERVIEWS
).
https_api_path(Parts) ->
?HOST_HTTPS ++ filename:join([?BASE_PATH | Parts]).
http_api_path(Parts) ->
?HOST_HTTP ++ filename:join([?BASE_PATH | Parts]).
naive_env_interpolation(Str0) ->
Str1 = emqx_schema:naive_env_interpolation(Str0),
%% assert all envs are replaced
?assertNot(lists:member($$, Str1)),
Str1.
default_ssl_cert() ->
#{
cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>
}.