Browse Source

refactor: delete default listeners from default config

The new config overriding rule is very much confusing for
people who wants to persist listener config changes made from
dashboard

This commit moves the default values from default config file
to schema source code.
In order to support build-time cert path at runtime, there
is also a naive environment variable interplation feature added.
Zaiming (Stone) Shi 2 years ago
parent
commit
b0f3a654ee
3 changed files with 129 additions and 58 deletions
  1. 0 43
      apps/emqx/etc/emqx.conf
  2. 53 0
      apps/emqx/src/emqx_schema.erl
  3. 76 15
      apps/emqx/src/emqx_tls_lib.erl

+ 0 - 43
apps/emqx/etc/emqx.conf

@@ -1,43 +0,0 @@
-listeners.tcp.default {
-  bind = "0.0.0.0:1883"
-  max_connections = 1024000
-}
-
-listeners.ssl.default {
-  bind = "0.0.0.0:8883"
-  max_connections = 512000
-  ssl_options {
-    keyfile = "{{ platform_etc_dir }}/certs/key.pem"
-    certfile = "{{ platform_etc_dir }}/certs/cert.pem"
-    cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
-  }
-}
-
-listeners.ws.default {
-  bind = "0.0.0.0:8083"
-  max_connections = 1024000
-  websocket.mqtt_path = "/mqtt"
-}
-
-listeners.wss.default {
-  bind = "0.0.0.0:8084"
-  max_connections = 512000
-  websocket.mqtt_path = "/mqtt"
-  ssl_options {
-    keyfile = "{{ platform_etc_dir }}/certs/key.pem"
-    certfile = "{{ platform_etc_dir }}/certs/cert.pem"
-    cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
-  }
-}
-
-# listeners.quic.default {
-#  enabled = true
-#  bind = "0.0.0.0:14567"
-#  max_connections = 1024000
-#  ssl_options {
-#   verify = verify_none
-#   keyfile = "{{ platform_etc_dir }}/certs/key.pem"
-#   certfile = "{{ platform_etc_dir }}/certs/cert.pem"
-#   cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
-#  }
-# }

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

@@ -779,6 +779,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_tcp_listener")),
                 map(name, ref("mqtt_tcp_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_tcp),
                     desc => ?DESC(fields_listeners_tcp),
+                    default => default_listener(tcp),
                     required => {false, recursively}
                     required => {false, recursively}
                 }
                 }
             )},
             )},
@@ -787,6 +788,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_ssl_listener")),
                 map(name, ref("mqtt_ssl_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_ssl),
                     desc => ?DESC(fields_listeners_ssl),
+                    default => default_listener(ssl),
                     required => {false, recursively}
                     required => {false, recursively}
                 }
                 }
             )},
             )},
@@ -795,6 +797,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_ws_listener")),
                 map(name, ref("mqtt_ws_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_ws),
                     desc => ?DESC(fields_listeners_ws),
+                    default => default_listener(ws),
                     required => {false, recursively}
                     required => {false, recursively}
                 }
                 }
             )},
             )},
@@ -803,6 +806,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_wss_listener")),
                 map(name, ref("mqtt_wss_listener")),
                 #{
                 #{
                     desc => ?DESC(fields_listeners_wss),
                     desc => ?DESC(fields_listeners_wss),
+                    default => default_listener(wss),
                     required => {false, recursively}
                     required => {false, recursively}
                 }
                 }
             )},
             )},
@@ -3083,3 +3087,52 @@ assert_required_field(Conf, Key, ErrorMessage) ->
         _ ->
         _ ->
             ok
             ok
     end.
     end.
+
+default_listener(tcp) ->
+    #{
+        <<"default">> =>
+            #{
+                <<"bind">> => <<"0.0.0.0:1883">>,
+                <<"max_connections">> => 1024000
+            }
+    };
+default_listener(ws) ->
+    #{
+        <<"default">> =>
+            #{
+                <<"bind">> => <<"0.0.0.0:8083">>,
+                <<"max_connections">> => 1024000,
+                <<"websocket">> => #{<<"mqtt_path">> => <<"/mqtt">>}
+            }
+    };
+default_listener(SSLListener) ->
+    %% The env variable is resolved in emqx_tls_lib
+    CertFile = fun(Name) ->
+        iolist_to_binary("${EMQX_ETC_DIR}/" ++ filename:join(["certs", Name]))
+    end,
+    SslOptions = #{
+        <<"cacertfile">> => CertFile(<<"cacert.pem">>),
+        <<"certfile">> => CertFile(<<"cert.pem">>),
+        <<"keyfile">> => CertFile(<<"key.pem">>)
+    },
+    case SSLListener of
+        ssl ->
+            #{
+                <<"default">> =>
+                    #{
+                        <<"bind">> => <<"0.0.0.0:8883">>,
+                        <<"max_connections">> => 512000,
+                        <<"ssl_options">> => SslOptions
+                    }
+            };
+        wss ->
+            #{
+                <<"default">> =>
+                    #{
+                        <<"bind">> => <<"0.0.0.0:8084">>,
+                        <<"max_connections">> => 512000,
+                        <<"ssl_options">> => SslOptions,
+                        <<"websocket">> => #{<<"mqtt_path">> => <<"/mqtt">>}
+                    }
+            }
+    end.

+ 76 - 15
apps/emqx/src/emqx_tls_lib.erl

@@ -309,19 +309,19 @@ ensure_ssl_files(Dir, SSL, Opts) ->
     case ensure_ssl_file_key(SSL, RequiredKeys) of
     case ensure_ssl_file_key(SSL, RequiredKeys) of
         ok ->
         ok ->
             KeyPaths = ?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A,
             KeyPaths = ?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A,
-            ensure_ssl_files(Dir, SSL, KeyPaths, Opts);
+            ensure_ssl_files_per_key(Dir, SSL, KeyPaths, Opts);
         {error, _} = Error ->
         {error, _} = Error ->
             Error
             Error
     end.
     end.
 
 
-ensure_ssl_files(_Dir, SSL, [], _Opts) ->
+ensure_ssl_files_per_key(_Dir, SSL, [], _Opts) ->
     {ok, SSL};
     {ok, SSL};
-ensure_ssl_files(Dir, SSL, [KeyPath | KeyPaths], Opts) ->
+ensure_ssl_files_per_key(Dir, SSL, [KeyPath | KeyPaths], Opts) ->
     case
     case
         ensure_ssl_file(Dir, KeyPath, SSL, emqx_utils_maps:deep_get(KeyPath, SSL, undefined), Opts)
         ensure_ssl_file(Dir, KeyPath, SSL, emqx_utils_maps:deep_get(KeyPath, SSL, undefined), Opts)
     of
     of
         {ok, NewSSL} ->
         {ok, NewSSL} ->
-            ensure_ssl_files(Dir, NewSSL, KeyPaths, Opts);
+            ensure_ssl_files_per_key(Dir, NewSSL, KeyPaths, Opts);
         {error, Reason} ->
         {error, Reason} ->
             {error, Reason#{which_options => [KeyPath]}}
             {error, Reason#{which_options => [KeyPath]}}
     end.
     end.
@@ -347,7 +347,8 @@ delete_ssl_files(Dir, NewOpts0, OldOpts0) ->
 delete_old_file(New, Old) when New =:= Old -> ok;
 delete_old_file(New, Old) when New =:= Old -> ok;
 delete_old_file(_New, _Old = undefined) ->
 delete_old_file(_New, _Old = undefined) ->
     ok;
     ok;
-delete_old_file(_New, Old) ->
+delete_old_file(_New, Old0) ->
+    Old = resolve_cert_path(Old0),
     case is_generated_file(Old) andalso filelib:is_regular(Old) andalso file:delete(Old) of
     case is_generated_file(Old) andalso filelib:is_regular(Old) andalso file:delete(Old) of
         ok ->
         ok ->
             ok;
             ok;
@@ -355,7 +356,7 @@ delete_old_file(_New, Old) ->
         false ->
         false ->
             ok;
             ok;
         {error, Reason} ->
         {error, Reason} ->
-            ?SLOG(error, #{msg => "failed_to_delete_ssl_file", file_path => Old, reason => Reason})
+            ?SLOG(error, #{msg => "failed_to_delete_ssl_file", file_path => Old0, reason => Reason})
     end.
     end.
 
 
 ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) ->
 ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) ->
@@ -414,7 +415,8 @@ is_pem(MaybePem) ->
 %% To make it simple, the file is always overwritten.
 %% To make it simple, the file is always overwritten.
 %% Also a potentially half-written PEM file (e.g. due to power outage)
 %% Also a potentially half-written PEM file (e.g. due to power outage)
 %% can be corrected with an overwrite.
 %% can be corrected with an overwrite.
-save_pem_file(Dir, KeyPath, Pem, DryRun) ->
+save_pem_file(Dir0, KeyPath, Pem, DryRun) ->
+    Dir = resolve_cert_path(Dir0),
     Path = pem_file_name(Dir, KeyPath, Pem),
     Path = pem_file_name(Dir, KeyPath, Pem),
     case filelib:ensure_dir(Path) of
     case filelib:ensure_dir(Path) of
         ok when DryRun ->
         ok when DryRun ->
@@ -472,7 +474,8 @@ hex_str(Bin) ->
     iolist_to_binary([io_lib:format("~2.16.0b", [X]) || <<X:8>> <= Bin]).
     iolist_to_binary([io_lib:format("~2.16.0b", [X]) || <<X:8>> <= Bin]).
 
 
 %% @doc Returns 'true' when the file is a valid pem, otherwise {error, Reason}.
 %% @doc Returns 'true' when the file is a valid pem, otherwise {error, Reason}.
-is_valid_pem_file(Path) ->
+is_valid_pem_file(Path0) ->
+    Path = resolve_cert_path(Path0),
     case file:read_file(Path) of
     case file:read_file(Path) of
         {ok, Pem} -> is_pem(Pem) orelse {error, not_pem};
         {ok, Pem} -> is_pem(Pem) orelse {error, not_pem};
         {error, Reason} -> {error, Reason}
         {error, Reason} -> {error, Reason}
@@ -513,10 +516,15 @@ do_drop_invalid_certs([KeyPath | KeyPaths], SSL) ->
 to_server_opts(Type, Opts) ->
 to_server_opts(Type, Opts) ->
     Versions = integral_versions(Type, maps:get(versions, Opts, undefined)),
     Versions = integral_versions(Type, maps:get(versions, Opts, undefined)),
     Ciphers = integral_ciphers(Versions, maps:get(ciphers, Opts, undefined)),
     Ciphers = integral_ciphers(Versions, maps:get(ciphers, Opts, undefined)),
-    maps:to_list(Opts#{
-        ciphers => Ciphers,
-        versions => Versions
-    }).
+    filter(
+        maps:to_list(Opts#{
+            keyfile => resolve_cert_path_strict(maps:get(keyfile, Opts, undefined)),
+            certfile => resolve_cert_path_strict(maps:get(certfile, Opts, undefined)),
+            cacertfile => resolve_cert_path_strict(maps:get(cacertfile, Opts, undefined)),
+            ciphers => Ciphers,
+            versions => Versions
+        })
+    ).
 
 
 %% @doc Convert hocon-checked tls client options (map()) to
 %% @doc Convert hocon-checked tls client options (map()) to
 %% proplist accepted by ssl library.
 %% proplist accepted by ssl library.
@@ -532,9 +540,9 @@ to_client_opts(Type, Opts) ->
     Get = fun(Key) -> GetD(Key, undefined) end,
     Get = fun(Key) -> GetD(Key, undefined) end,
     case GetD(enable, false) of
     case GetD(enable, false) of
         true ->
         true ->
-            KeyFile = ensure_str(Get(keyfile)),
-            CertFile = ensure_str(Get(certfile)),
-            CAFile = ensure_str(Get(cacertfile)),
+            KeyFile = resolve_cert_path_strict(Get(keyfile)),
+            CertFile = resolve_cert_path_strict(Get(certfile)),
+            CAFile = resolve_cert_path_strict(Get(cacertfile)),
             Verify = GetD(verify, verify_none),
             Verify = GetD(verify, verify_none),
             SNI = ensure_sni(Get(server_name_indication)),
             SNI = ensure_sni(Get(server_name_indication)),
             Versions = integral_versions(Type, Get(versions)),
             Versions = integral_versions(Type, Get(versions)),
@@ -556,6 +564,59 @@ to_client_opts(Type, Opts) ->
             []
             []
     end.
     end.
 
 
+resolve_cert_path_strict(Path) ->
+    case resolve_cert_path(Path) of
+        undefined ->
+            undefined;
+        ResolvedPath ->
+            case filelib:is_regular(ResolvedPath) of
+                true ->
+                    ResolvedPath;
+                false ->
+                    PathToLog = ensure_str(Path),
+                    LogData =
+                        case PathToLog =:= ResolvedPath of
+                            true ->
+                                #{path => PathToLog};
+                            false ->
+                                #{path => PathToLog, resolved_path => ResolvedPath}
+                        end,
+                    ?SLOG(error, LogData#{msg => "cert_file_not_found"}),
+                    undefined
+            end
+    end.
+
+resolve_cert_path(undefined) ->
+    undefined;
+resolve_cert_path(Path) ->
+    case ensure_str(Path) of
+        "$" ++ Maybe ->
+            naive_env_resolver(Maybe);
+        Other ->
+            Other
+    end.
+
+%% resolves a file path like "ENV_VARIABLE/sub/path" or "{ENV_VARIABLE}/sub/path"
+%% in windows, it could be "ENV_VARIABLE/sub\path" or "{ENV_VARIABLE}/sub\path"
+naive_env_resolver(Maybe) ->
+    case string:split(Maybe, "/") of
+        [_] ->
+            Maybe;
+        [Env, SubPath] ->
+            case os:getenv(trim_env_name(Env)) of
+                false ->
+                    SubPath;
+                "" ->
+                    SubPath;
+                EnvValue ->
+                    filename:join(EnvValue, SubPath)
+            end
+    end.
+
+%% delete the first and last curly braces
+trim_env_name(Env) ->
+    string:trim(Env, both, "{}").
+
 filter([]) -> [];
 filter([]) -> [];
 filter([{_, undefined} | T]) -> filter(T);
 filter([{_, undefined} | T]) -> filter(T);
 filter([{_, ""} | T]) -> filter(T);
 filter([{_, ""} | T]) -> filter(T);