Explorar o código

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 %!s(int64=2) %!d(string=hai) anos
pai
achega
b0f3a654ee
Modificáronse 3 ficheiros con 129 adicións e 58 borrados
  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")),
                 #{
                     desc => ?DESC(fields_listeners_tcp),
+                    default => default_listener(tcp),
                     required => {false, recursively}
                 }
             )},
@@ -787,6 +788,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_ssl_listener")),
                 #{
                     desc => ?DESC(fields_listeners_ssl),
+                    default => default_listener(ssl),
                     required => {false, recursively}
                 }
             )},
@@ -795,6 +797,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_ws_listener")),
                 #{
                     desc => ?DESC(fields_listeners_ws),
+                    default => default_listener(ws),
                     required => {false, recursively}
                 }
             )},
@@ -803,6 +806,7 @@ fields("listeners") ->
                 map(name, ref("mqtt_wss_listener")),
                 #{
                     desc => ?DESC(fields_listeners_wss),
+                    default => default_listener(wss),
                     required => {false, recursively}
                 }
             )},
@@ -3083,3 +3087,52 @@ assert_required_field(Conf, Key, ErrorMessage) ->
         _ ->
             ok
     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
         ok ->
             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
     end.
 
-ensure_ssl_files(_Dir, SSL, [], _Opts) ->
+ensure_ssl_files_per_key(_Dir, SSL, [], _Opts) ->
     {ok, SSL};
-ensure_ssl_files(Dir, SSL, [KeyPath | KeyPaths], Opts) ->
+ensure_ssl_files_per_key(Dir, SSL, [KeyPath | KeyPaths], Opts) ->
     case
         ensure_ssl_file(Dir, KeyPath, SSL, emqx_utils_maps:deep_get(KeyPath, SSL, undefined), Opts)
     of
         {ok, NewSSL} ->
-            ensure_ssl_files(Dir, NewSSL, KeyPaths, Opts);
+            ensure_ssl_files_per_key(Dir, NewSSL, KeyPaths, Opts);
         {error, Reason} ->
             {error, Reason#{which_options => [KeyPath]}}
     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 = undefined) ->
     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
         ok ->
             ok;
@@ -355,7 +356,7 @@ delete_old_file(_New, Old) ->
         false ->
             ok;
         {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.
 
 ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) ->
@@ -414,7 +415,8 @@ is_pem(MaybePem) ->
 %% To make it simple, the file is always overwritten.
 %% Also a potentially half-written PEM file (e.g. due to power outage)
 %% 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),
     case filelib:ensure_dir(Path) of
         ok when DryRun ->
@@ -472,7 +474,8 @@ hex_str(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}.
-is_valid_pem_file(Path) ->
+is_valid_pem_file(Path0) ->
+    Path = resolve_cert_path(Path0),
     case file:read_file(Path) of
         {ok, Pem} -> is_pem(Pem) orelse {error, not_pem};
         {error, Reason} -> {error, Reason}
@@ -513,10 +516,15 @@ do_drop_invalid_certs([KeyPath | KeyPaths], SSL) ->
 to_server_opts(Type, Opts) ->
     Versions = integral_versions(Type, maps:get(versions, 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
 %% proplist accepted by ssl library.
@@ -532,9 +540,9 @@ to_client_opts(Type, Opts) ->
     Get = fun(Key) -> GetD(Key, undefined) end,
     case GetD(enable, false) of
         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),
             SNI = ensure_sni(Get(server_name_indication)),
             Versions = integral_versions(Type, Get(versions)),
@@ -556,6 +564,59 @@ to_client_opts(Type, Opts) ->
             []
     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([{_, undefined} | T]) -> filter(T);
 filter([{_, ""} | T]) -> filter(T);