Merge pull request #10761 from zhongwencool/dashboard-https-listener

fix: bad cert file path in dashboard https listener
This commit is contained in:
zhongwencool 2023-05-22 14:17:01 +08:00 committed by GitHub
commit 2ad8c41791
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 247 additions and 20 deletions

View File

@ -28,6 +28,7 @@
-include("emqx_access_control.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("logger.hrl").
-type duration() :: integer().
-type duration_s() :: integer().
@ -3290,6 +3291,11 @@ naive_env_interpolation("$" ++ Maybe = Original) ->
{ok, Path} ->
filename:join([Path, Tail]);
error ->
?SLOG(warning, #{
msg => "failed_to_resolve_env_variable",
env => Env,
original => Original
}),
Original
end;
naive_env_interpolation(Other) ->

View File

@ -620,9 +620,11 @@ ensure_bin(A) when is_atom(A) -> atom_to_binary(A, utf8).
ensure_ssl_file_key(_SSL, []) ->
ok;
ensure_ssl_file_key(SSL, RequiredKeyPaths) ->
NotFoundRef = make_ref(),
Filter = fun(KeyPath) ->
NotFoundRef =:= emqx_utils_maps:deep_get(KeyPath, SSL, NotFoundRef)
case emqx_utils_maps:deep_find(KeyPath, SSL) of
{not_found, _, _} -> true;
_ -> false
end
end,
case lists:filter(Filter, RequiredKeyPaths) of
[] -> ok;

View File

@ -95,7 +95,7 @@ start_listeners(Listeners) ->
end
end,
{[], []},
listeners(Listeners)
listeners(ensure_ssl_cert(Listeners))
),
case ErrListeners of
[] ->
@ -140,18 +140,18 @@ apps() ->
listeners(Listeners) ->
lists:filtermap(
fun({Protocol, Conf}) ->
maps:get(enable, Conf) andalso
begin
{Conf1, Bind} = ip_port(Conf),
{true, {
listener_name(Protocol),
Protocol,
Bind,
ranch_opts(Conf1),
proto_opts(Conf1)
}}
end
fun
({Protocol, Conf = #{enable := true}}) ->
{Conf1, Bind} = ip_port(Conf),
{true, {
listener_name(Protocol),
Protocol,
Bind,
ranch_opts(Conf1),
proto_opts(Conf1)
}};
({_Protocol, #{enable := false}}) ->
false
end,
maps:to_list(Listeners)
).
@ -191,8 +191,8 @@ ranch_opts(Options) ->
end,
RanchOpts#{socket_opts => InetOpts ++ SocketOpts}.
proto_opts(Options) ->
maps:with([proxy_header], Options).
proto_opts(#{proxy_header := ProxyHeader}) ->
#{proxy_header => ProxyHeader}.
filter_false(_K, false, S) -> S;
filter_false(K, V, S) -> [{K, V} | S].
@ -224,7 +224,7 @@ return_unauthorized(Code, Message) ->
{401,
#{
<<"WWW-Authenticate">> =>
<<"Basic Realm=\"minirest-server\"">>
<<"Basic Realm=\"emqx-dashboard\"">>
},
#{code => Code, message => Message}}.
@ -247,3 +247,9 @@ api_key_authorize(Req, Key, Secret) ->
<<"Check api_key/api_secret">>
)
end.
ensure_ssl_cert(Listeners = #{https := Https0}) ->
Https1 = emqx_tls_lib:to_server_opts(tls, Https0),
Listeners#{https => maps:from_list(Https1)};
ensure_ssl_cert(Listeners) ->
Listeners.

View File

@ -36,8 +36,6 @@
-define(HOST, "http://127.0.0.1:18083").
%% -define(API_VERSION, "v5").
-define(BASE_PATH, "/api/v5").
-define(APP_DASHBOARD, emqx_dashboard).

View File

@ -0,0 +1,214 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2023 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("emqx_dashboard.hrl").
-define(NAME, 'https:dashboard').
-define(HOST, "https://127.0.0.1:18084").
-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_default_ssl_cert(_Config) ->
Conf = #{dashboard => #{listeners => #{https => #{bind => 18084, enable => true}}}},
validate_https(Conf, 512, default_ssl_cert(), verify_none),
ok.
t_normal_ssl_cert(_Config) ->
MaxConnection = 1000,
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
enable => true,
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_verify_cacertfile(_Config) ->
MaxConnection = 1024,
DefaultSSLCert = default_ssl_cert(),
SSLCert = DefaultSSLCert#{cacertfile => <<"">>},
%% default #{verify => verify_none}
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
enable => true,
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">>)
),
validate_https(VerifyPeerConf2, MaxConnection, DefaultSSLCert, verify_peer),
ok.
t_bad_certfile(_Config) ->
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
enable => true,
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),
assert_ranch_options(MaxConnection, SSLCert, Verify),
assert_https_request(),
emqx_mgmt_api_test_util:end_suite([emqx_management]).
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 = api_path([Path]),
?assertMatch(
{ok, _},
emqx_dashboard_SUITE:request_dashboard(get, ApiPath, Headers)
)
end,
?OVERVIEWS
).
api_path(Parts) ->
?HOST ++ 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">>
}.

View File

@ -0,0 +1 @@
Fixing the issue where the default value of SSL certificate for Dashboard Listener was not correctly interpolated, which caused HTTPS to be inaccessible when verify_peer and cacertfile were using the default configuration.