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.
This commit is contained in:
Zaiming (Stone) Shi 2023-04-27 12:22:09 +02:00
parent c58ffce75f
commit b0f3a654ee
3 changed files with 129 additions and 58 deletions

View File

@ -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"
# }
# }

View File

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

View File

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