Просмотр исходного кода

fix: bad cert file path in dashboard https listener

某文 2 лет назад
Родитель
Сommit
7c2aac64bc

+ 6 - 0
apps/emqx/src/emqx_schema.erl

@@ -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) ->

+ 4 - 2
apps/emqx/src/emqx_tls_lib.erl

@@ -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;

+ 22 - 16
apps/emqx_dashboard/src/emqx_dashboard.erl

@@ -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.

+ 0 - 2
apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl

@@ -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).

+ 214 - 0
apps/emqx_dashboard/test/emqx_dashboard_https_SUITE.erl

@@ -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">>
+    }.