Merge pull request #10128 from thalesmg/ocsp-v50-mkII

feat: add ocsp stapling support to mqtt ssl listener (5.0)
This commit is contained in:
Thales Macedo Garitezi 2023-03-16 13:10:48 -03:00 committed by GitHub
commit 91a57faa95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2266 additions and 112 deletions

View File

@ -1753,6 +1753,63 @@ server_ssl_opts_schema_gc_after_handshake {
} }
} }
server_ssl_opts_schema_enable_ocsp_stapling {
desc {
en: "Whether to enable Online Certificate Status Protocol (OCSP) stapling for the listener."
" If set to true, requires defining the OCSP responder URL and issuer PEM path."
zh: "是否为监听器启用 OCSP Stapling 功能。 如果设置为 true"
"需要定义 OCSP Responder 的 URL 和证书签发者的 PEM 文件路径。"
}
label: {
en: "Enable OCSP Stapling"
zh: "启用 OCSP Stapling"
}
}
server_ssl_opts_schema_ocsp_responder_url {
desc {
en: "URL for the OCSP responder to check the server certificate against."
zh: "用于检查服务器证书的 OCSP Responder 的 URL。"
}
label: {
en: "OCSP Responder URL"
zh: "OCSP Responder 的 URL"
}
}
server_ssl_opts_schema_ocsp_issuer_pem {
desc {
en: "PEM-encoded certificate of the OCSP issuer for the server certificate."
zh: "服务器证书的 OCSP 签发者的 PEM 编码证书。"
}
label: {
en: "OCSP Issuer Certificate"
zh: "OCSP 签发者证书"
}
}
server_ssl_opts_schema_ocsp_refresh_interval {
desc {
en: "The period to refresh the OCSP response for the server."
zh: "为服务器刷新OCSP响应的周期。"
}
label: {
en: "OCSP Refresh Interval"
zh: "OCSP 刷新间隔"
}
}
server_ssl_opts_schema_ocsp_refresh_http_timeout {
desc {
en: "The timeout for the HTTP request when checking OCSP responses."
zh: "检查 OCSP 响应时HTTP 请求的超时。"
}
label: {
en: "OCSP Refresh HTTP Timeout"
zh: "OCSP 刷新 HTTP 超时"
}
}
fields_listeners_tcp { fields_listeners_tcp {
desc { desc {
en: """TCP listeners.""" en: """TCP listeners."""

View File

@ -30,6 +30,7 @@
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.5"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.5"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.0"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.37.0"}}},
{emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}},
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}}
@ -43,7 +44,7 @@
{meck, "0.9.2"}, {meck, "0.9.2"},
{proper, "1.4.0"}, {proper, "1.4.0"},
{bbmustache, "1.10.0"}, {bbmustache, "1.10.0"},
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.2"}}} {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.5"}}}
]}, ]},
{extra_src_dirs, [{"test", [recursive]}]} {extra_src_dirs, [{"test", [recursive]}]}
]} ]}

View File

@ -87,6 +87,10 @@
remove_handlers/0 remove_handlers/0
]). ]).
-ifdef(TEST).
-export([erase_schema_mod_and_names/0]).
-endif.
-include("logger.hrl"). -include("logger.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
@ -501,6 +505,11 @@ save_schema_mod_and_names(SchemaMod) ->
names => lists:usort(OldNames ++ RootNames) names => lists:usort(OldNames ++ RootNames)
}). }).
-ifdef(TEST).
erase_schema_mod_and_names() ->
persistent_term:erase(?PERSIS_SCHEMA_MODS).
-endif.
-spec get_schema_mod() -> #{binary() => atom()}. -spec get_schema_mod() -> #{binary() => atom()}.
get_schema_mod() -> get_schema_mod() ->
maps:get(mods, persistent_term:get(?PERSIS_SCHEMA_MODS, #{mods => #{}})). maps:get(mods, persistent_term:get(?PERSIS_SCHEMA_MODS, #{mods => #{}})).

View File

@ -0,0 +1,24 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-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.
%%
%% @doc Never update this module, create a v2 instead.
%%--------------------------------------------------------------------
-module(emqx_const_v1).
-export([make_sni_fun/1]).
make_sni_fun(ListenerID) ->
fun(SN) -> emqx_ocsp_cache:sni_fun(SN, ListenerID) end.

View File

@ -35,7 +35,8 @@ init([]) ->
child_spec(emqx_hooks, worker), child_spec(emqx_hooks, worker),
child_spec(emqx_stats, worker), child_spec(emqx_stats, worker),
child_spec(emqx_metrics, worker), child_spec(emqx_metrics, worker),
child_spec(emqx_authn_authz_metrics_sup, supervisor) child_spec(emqx_authn_authz_metrics_sup, supervisor),
child_spec(emqx_ocsp_cache, worker)
] ]
}}. }}.

View File

@ -484,8 +484,12 @@ esockd_opts(ListenerId, Type, Opts0) ->
}, },
maps:to_list( maps:to_list(
case Type of case Type of
tcp -> Opts3#{tcp_options => tcp_opts(Opts0)}; tcp ->
ssl -> Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} Opts3#{tcp_options => tcp_opts(Opts0)};
ssl ->
OptsWithSNI = inject_sni_fun(ListenerId, Opts0),
SSLOpts = ssl_opts(OptsWithSNI),
Opts3#{ssl_options => SSLOpts, tcp_options => tcp_opts(Opts0)}
end end
). ).
@ -785,3 +789,8 @@ quic_listener_optional_settings() ->
max_binding_stateless_operations, max_binding_stateless_operations,
stateless_operation_expiration_ms stateless_operation_expiration_ms
]. ].
inject_sni_fun(ListenerId, Conf = #{ssl_options := #{ocsp := #{enable_ocsp_stapling := true}}}) ->
emqx_ocsp_cache:inject_sni_fun(ListenerId, Conf);
inject_sni_fun(_ListenerId, Conf) ->
Conf.

View File

@ -0,0 +1,532 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-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.
%%
%% @doc EMQX OCSP cache.
%%--------------------------------------------------------------------
-module(emqx_ocsp_cache).
-include("logger.hrl").
-include_lib("public_key/include/public_key.hrl").
-include_lib("ssl/src/ssl_handshake.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(gen_server).
-export([
start_link/0,
sni_fun/2,
fetch_response/1,
register_listener/2,
inject_sni_fun/2
]).
%% gen_server API
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3
]).
%% internal export; only for mocking in tests
-export([http_get/2]).
-define(CACHE_TAB, ?MODULE).
-define(CALL_TIMEOUT, 20_000).
-define(RETRY_TIMEOUT, 5_000).
-define(REFRESH_TIMER(LID), {refresh_timer, LID}).
-ifdef(TEST).
-define(MIN_REFRESH_INTERVAL, timer:seconds(5)).
-else.
-define(MIN_REFRESH_INTERVAL, timer:minutes(1)).
-endif.
%% Allow usage of OTP certificate record fields (camelCase).
-elvis([
{elvis_style, atom_naming_convention, #{
regex => "^([a-z][a-z0-9]*_?)([a-zA-Z0-9]*_?)*$",
enclosed_atoms => ".*"
}}
]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
sni_fun(_ServerName, ListenerID) ->
Res =
try
fetch_response(ListenerID)
catch
_:_ -> error
end,
case Res of
{ok, Response} ->
[
{certificate_status, #certificate_status{
status_type = ?CERTIFICATE_STATUS_TYPE_OCSP,
response = Response
}}
];
error ->
[]
end.
fetch_response(ListenerID) ->
case do_lookup(ListenerID) of
{ok, DERResponse} ->
{ok, DERResponse};
{error, invalid_listener_id} ->
error;
{error, not_cached} ->
?tp(ocsp_cache_miss, #{listener_id => ListenerID}),
?SLOG(debug, #{
msg => "fetching_new_ocsp_response",
listener_id => ListenerID
}),
http_fetch(ListenerID)
end.
register_listener(ListenerID, Opts) ->
gen_server:call(?MODULE, {register_listener, ListenerID, Opts}, ?CALL_TIMEOUT).
-spec inject_sni_fun(emqx_listeners:listener_id(), map()) -> map().
inject_sni_fun(ListenerID, Conf0) ->
SNIFun = emqx_const_v1:make_sni_fun(ListenerID),
Conf = emqx_map_lib:deep_merge(Conf0, #{ssl_options => #{sni_fun => SNIFun}}),
ok = ?MODULE:register_listener(ListenerID, Conf),
Conf.
%%--------------------------------------------------------------------
%% gen_server behaviour
%%--------------------------------------------------------------------
init(_Args) ->
logger:set_process_metadata(#{domain => [emqx, ocsp, cache]}),
emqx_tables:new(?CACHE_TAB, [
named_table,
public,
{heir, whereis(emqx_kernel_sup), none},
{read_concurrency, true}
]),
?tp(ocsp_cache_init, #{}),
{ok, #{}}.
handle_call({http_fetch, ListenerID}, _From, State) ->
case do_lookup(ListenerID) of
{ok, DERResponse} ->
{reply, {ok, DERResponse}, State};
{error, invalid_listener_id} ->
{reply, error, State};
{error, not_cached} ->
Conf = undefined,
with_refresh_params(ListenerID, Conf, {reply, error, State}, fun(Params) ->
case do_http_fetch_and_cache(ListenerID, Params) of
error -> {reply, error, ensure_timer(ListenerID, State, ?RETRY_TIMEOUT)};
{ok, Response} -> {reply, {ok, Response}, ensure_timer(ListenerID, State)}
end
end)
end;
handle_call({register_listener, ListenerID, Conf}, _From, State0) ->
?SLOG(debug, #{
msg => "registering_ocsp_cache",
listener_id => ListenerID
}),
RefreshInterval0 = emqx_map_lib:deep_get([ssl_options, ocsp, refresh_interval], Conf),
RefreshInterval = max(RefreshInterval0, ?MIN_REFRESH_INTERVAL),
State = State0#{{refresh_interval, ListenerID} => RefreshInterval},
%% we need to pass the config along because this might be called
%% during the listener's `post_config_update', hence the config is
%% not yet "commited" and accessible when we need it.
Message = {refresh, ListenerID, Conf},
{reply, ok, ensure_timer(ListenerID, Message, State, 0)};
handle_call(Call, _From, State) ->
{reply, {error, {unknown_call, Call}}, State}.
handle_cast(_Cast, State) ->
{noreply, State}.
handle_info({timeout, TRef, {refresh, ListenerID}}, State0) ->
case maps:get(?REFRESH_TIMER(ListenerID), State0, undefined) of
TRef ->
?tp(ocsp_refresh_timer, #{listener_id => ListenerID}),
?SLOG(debug, #{
msg => "refreshing_ocsp_response",
listener_id => ListenerID
}),
Conf = undefined,
handle_refresh(ListenerID, Conf, State0);
_ ->
{noreply, State0}
end;
handle_info({timeout, TRef, {refresh, ListenerID, Conf}}, State0) ->
case maps:get(?REFRESH_TIMER(ListenerID), State0, undefined) of
TRef ->
?tp(ocsp_refresh_timer, #{listener_id => ListenerID}),
?SLOG(debug, #{
msg => "refreshing_ocsp_response",
listener_id => ListenerID
}),
handle_refresh(ListenerID, Conf, State0);
_ ->
{noreply, State0}
end;
handle_info(_Info, State) ->
{noreply, State}.
code_change(_Vsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% internal functions
%%--------------------------------------------------------------------
http_fetch(ListenerID) ->
%% TODO: configurable call timeout?
gen_server:call(?MODULE, {http_fetch, ListenerID}, ?CALL_TIMEOUT).
with_listener_config(ListenerID, ConfPath, ErrorResp, Fn) ->
case emqx_listeners:parse_listener_id(ListenerID) of
{ok, #{type := Type, name := Name}} ->
case emqx_config:get_listener_conf(Type, Name, ConfPath, not_found) of
not_found ->
?SLOG(error, #{
msg => "listener_config_missing",
listener_id => ListenerID
}),
ErrorResp;
Config ->
Fn(Config)
end;
_Err ->
?SLOG(error, #{
msg => "listener_id_not_found",
listener_id => ListenerID
}),
ErrorResp
end.
cache_key(ListenerID) ->
with_listener_config(ListenerID, [ssl_options], error, fun
(#{certfile := ServerCertPemPath}) ->
#'Certificate'{
tbsCertificate =
#'TBSCertificate'{
signature = Signature
}
} = read_server_cert(ServerCertPemPath),
{ok, {ocsp_response, Signature}};
(OtherConfig) ->
?SLOG(error, #{
msg => "listener_config_inconsistent",
listener_id => ListenerID,
config => OtherConfig
}),
error
end).
do_lookup(ListenerID) ->
CacheKey = cache_key(ListenerID),
case CacheKey of
error ->
{error, invalid_listener_id};
{ok, Key} ->
%% Respond immediately if a concurrent call already fetched it.
case ets:lookup(?CACHE_TAB, Key) of
[{_, DERResponse}] ->
?tp(ocsp_cache_hit, #{listener_id => ListenerID}),
{ok, DERResponse};
[] ->
{error, not_cached}
end
end.
read_server_cert(ServerCertPemPath0) ->
ServerCertPemPath = to_bin(ServerCertPemPath0),
case ets:lookup(ssl_pem_cache, ServerCertPemPath) of
[{_, [{'Certificate', ServerCertDer, _} | _]}] ->
public_key:der_decode('Certificate', ServerCertDer);
[] ->
case file:read_file(ServerCertPemPath) of
{ok, ServerCertPem} ->
[{'Certificate', ServerCertDer, _} | _] =
public_key:pem_decode(ServerCertPem),
public_key:der_decode('Certificate', ServerCertDer);
{error, Error1} ->
error({bad_server_cert_file, Error1})
end
end.
handle_refresh(ListenerID, Conf, State0) ->
%% no point in retrying if the config is inconsistent or non
%% existent.
State1 = maps:without([{refresh_interval, ListenerID}, ?REFRESH_TIMER(ListenerID)], State0),
with_refresh_params(ListenerID, Conf, {noreply, State1}, fun(Params) ->
case do_http_fetch_and_cache(ListenerID, Params) of
error ->
?SLOG(debug, #{
msg => "failed_to_fetch_ocsp_response",
listener_id => ListenerID
}),
{noreply, ensure_timer(ListenerID, State0, ?RETRY_TIMEOUT)};
{ok, _Response} ->
?SLOG(debug, #{
msg => "fetched_ocsp_response",
listener_id => ListenerID
}),
{noreply, ensure_timer(ListenerID, State0)}
end
end).
with_refresh_params(ListenerID, Conf, ErrorRet, Fn) ->
case get_refresh_params(ListenerID, Conf) of
error ->
ErrorRet;
{ok, Params} ->
try
Fn(Params)
catch
Kind:Error ->
?SLOG(error, #{
msg => "error_fetching_ocsp_response",
listener_id => ListenerID,
error => {Kind, Error}
}),
ErrorRet
end
end.
get_refresh_params(ListenerID, undefined = _Conf) ->
%% during normal periodic refreshes, we read from the emqx config.
with_listener_config(ListenerID, [ssl_options], error, fun
(
#{
ocsp := #{
issuer_pem := IssuerPemPath,
responder_url := ResponderURL,
refresh_http_timeout := HTTPTimeout
},
certfile := ServerCertPemPath
}
) ->
{ok, #{
issuer_pem => IssuerPemPath,
responder_url => ResponderURL,
refresh_http_timeout => HTTPTimeout,
server_certfile => ServerCertPemPath
}};
(OtherConfig) ->
?SLOG(error, #{
msg => "listener_config_inconsistent",
listener_id => ListenerID,
config => OtherConfig
}),
error
end);
get_refresh_params(_ListenerID, #{
ssl_options := #{
ocsp := #{
issuer_pem := IssuerPemPath,
responder_url := ResponderURL,
refresh_http_timeout := HTTPTimeout
},
certfile := ServerCertPemPath
}
}) ->
{ok, #{
issuer_pem => IssuerPemPath,
responder_url => ResponderURL,
refresh_http_timeout => HTTPTimeout,
server_certfile => ServerCertPemPath
}};
get_refresh_params(_ListenerID, _Conf) ->
error.
do_http_fetch_and_cache(ListenerID, Params) ->
#{
issuer_pem := IssuerPemPath,
responder_url := ResponderURL,
refresh_http_timeout := HTTPTimeout,
server_certfile := ServerCertPemPath
} = Params,
IssuerPem =
case file:read_file(IssuerPemPath) of
{ok, IssuerPem0} -> IssuerPem0;
{error, Error0} -> error({bad_issuer_pem_file, Error0})
end,
ServerCert = read_server_cert(ServerCertPemPath),
Request = build_ocsp_request(IssuerPem, ServerCert),
?tp(ocsp_http_fetch, #{
listener_id => ListenerID,
responder_url => ResponderURL,
timeout => HTTPTimeout
}),
RequestURI = iolist_to_binary([ResponderURL, Request]),
Resp = ?MODULE:http_get(RequestURI, HTTPTimeout),
case Resp of
{ok, {{_, 200, _}, _, Body}} ->
?SLOG(debug, #{
msg => "caching_ocsp_response",
listener_id => ListenerID
}),
%% if we got this far, the certfile is correct.
{ok, CacheKey} = cache_key(ListenerID),
true = ets:insert(?CACHE_TAB, {CacheKey, Body}),
?tp(ocsp_http_fetch_and_cache, #{
listener_id => ListenerID,
headers => true
}),
{ok, Body};
{ok, {200, Body}} ->
?SLOG(debug, #{
msg => "caching_ocsp_response",
listener_id => ListenerID
}),
%% if we got this far, the certfile is correct.
{ok, CacheKey} = cache_key(ListenerID),
true = ets:insert(?CACHE_TAB, {CacheKey, Body}),
?tp(ocsp_http_fetch_and_cache, #{
listener_id => ListenerID,
headers => false
}),
{ok, Body};
{ok, {{_, Code, _}, _, Body}} ->
?tp(
error,
ocsp_http_fetch_bad_code,
#{
listener_id => ListenerID,
body => Body,
code => Code,
headers => true
}
),
?SLOG(error, #{
msg => "error_fetching_ocsp_response",
listener_id => ListenerID,
code => Code,
body => Body
}),
error;
{ok, {Code, Body}} ->
?tp(
error,
ocsp_http_fetch_bad_code,
#{
listener_id => ListenerID,
body => Body,
code => Code,
headers => false
}
),
?SLOG(error, #{
msg => "error_fetching_ocsp_response",
listener_id => ListenerID,
code => Code,
body => Body
}),
error;
{error, Error} ->
?tp(
error,
ocsp_http_fetch_error,
#{
listener_id => ListenerID,
error => Error
}
),
?SLOG(error, #{
msg => "error_fetching_ocsp_response",
listener_id => ListenerID,
error => Error
}),
error
end.
http_get(URL, HTTPTimeout) ->
httpc:request(
get,
{URL, [{"connection", "close"}]},
[{timeout, HTTPTimeout}],
[{body_format, binary}]
).
ensure_timer(ListenerID, State) ->
Timeout = maps:get({refresh_interval, ListenerID}, State, timer:minutes(5)),
ensure_timer(ListenerID, State, Timeout).
ensure_timer(ListenerID, State, Timeout) ->
ensure_timer(ListenerID, {refresh, ListenerID}, State, Timeout).
ensure_timer(ListenerID, Message, State, Timeout) ->
emqx_misc:cancel_timer(maps:get(?REFRESH_TIMER(ListenerID), State, undefined)),
State#{
?REFRESH_TIMER(ListenerID) => emqx_misc:start_timer(
Timeout,
Message
)
}.
build_ocsp_request(IssuerPem, ServerCert) ->
[{'Certificate', IssuerDer, _} | _] = public_key:pem_decode(IssuerPem),
#'Certificate'{
tbsCertificate =
#'TBSCertificate'{
serialNumber = SerialNumber,
issuer = Issuer
}
} = ServerCert,
#'Certificate'{
tbsCertificate =
#'TBSCertificate'{
subjectPublicKeyInfo =
#'SubjectPublicKeyInfo'{subjectPublicKey = IssuerPublicKeyDer}
}
} = public_key:der_decode('Certificate', IssuerDer),
IssuerDNHash = crypto:hash(sha, public_key:der_encode('Name', Issuer)),
IssuerPKHash = crypto:hash(sha, IssuerPublicKeyDer),
Req = #'OCSPRequest'{
tbsRequest =
#'TBSRequest'{
version = 0,
requestList =
[
#'Request'{
reqCert =
#'CertID'{
hashAlgorithm =
#'AlgorithmIdentifier'{
algorithm = ?'id-sha1',
%% ???
parameters = <<5, 0>>
},
issuerNameHash = IssuerDNHash,
issuerKeyHash = IssuerPKHash,
serialNumber = SerialNumber
}
}
]
}
},
ReqDer = public_key:der_encode('OCSPRequest', Req),
base64:encode_to_string(ReqDer).
to_bin(Str) when is_list(Str) -> list_to_binary(Str);
to_bin(Bin) when is_binary(Bin) -> Bin.

View File

@ -43,6 +43,7 @@
-type cipher() :: map(). -type cipher() :: map().
-type port_number() :: 1..65536. -type port_number() :: 1..65536.
-type server_parse_option() :: #{default_port => port_number(), no_port => boolean()}. -type server_parse_option() :: #{default_port => port_number(), no_port => boolean()}.
-type url() :: binary().
-typerefl_from_string({duration/0, emqx_schema, to_duration}). -typerefl_from_string({duration/0, emqx_schema, to_duration}).
-typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}). -typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}).
@ -56,6 +57,7 @@
-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). -typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}).
-typerefl_from_string({cipher/0, emqx_schema, to_erl_cipher_suite}). -typerefl_from_string({cipher/0, emqx_schema, to_erl_cipher_suite}).
-typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). -typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}).
-typerefl_from_string({url/0, emqx_schema, to_url}).
-export([ -export([
validate_heap_size/1, validate_heap_size/1,
@ -81,7 +83,8 @@
to_bar_separated_list/1, to_bar_separated_list/1,
to_ip_port/1, to_ip_port/1,
to_erl_cipher_suite/1, to_erl_cipher_suite/1,
to_comma_separated_atoms/1 to_comma_separated_atoms/1,
to_url/1
]). ]).
-export([ -export([
@ -108,7 +111,8 @@
bar_separated_list/0, bar_separated_list/0,
ip_port/0, ip_port/0,
cipher/0, cipher/0,
comma_separated_atoms/0 comma_separated_atoms/0,
url/0
]). ]).
-export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]). -export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]).
@ -810,7 +814,7 @@ fields("mqtt_ssl_listener") ->
{"ssl_options", {"ssl_options",
sc( sc(
ref("listener_ssl_opts"), ref("listener_ssl_opts"),
#{} #{validator => fun mqtt_ssl_listener_ssl_options_validator/1}
)} )}
]; ];
fields("mqtt_ws_listener") -> fields("mqtt_ws_listener") ->
@ -1294,6 +1298,49 @@ fields("listener_quic_ssl_opts") ->
); );
fields("ssl_client_opts") -> fields("ssl_client_opts") ->
client_ssl_opts_schema(#{}); client_ssl_opts_schema(#{});
fields("ocsp") ->
[
{"enable_ocsp_stapling",
sc(
boolean(),
#{
default => false,
desc => ?DESC("server_ssl_opts_schema_enable_ocsp_stapling")
}
)},
{"responder_url",
sc(
url(),
#{
required => false,
desc => ?DESC("server_ssl_opts_schema_ocsp_responder_url")
}
)},
{"issuer_pem",
sc(
binary(),
#{
required => false,
desc => ?DESC("server_ssl_opts_schema_ocsp_issuer_pem")
}
)},
{"refresh_interval",
sc(
duration(),
#{
default => <<"5m">>,
desc => ?DESC("server_ssl_opts_schema_ocsp_refresh_interval")
}
)},
{"refresh_http_timeout",
sc(
duration(),
#{
default => <<"15s">>,
desc => ?DESC("server_ssl_opts_schema_ocsp_refresh_http_timeout")
}
)}
];
fields("deflate_opts") -> fields("deflate_opts") ->
[ [
{"level", {"level",
@ -2017,6 +2064,8 @@ desc("trace") ->
"Real-time filtering logs for the ClientID or Topic or IP for debugging."; "Real-time filtering logs for the ClientID or Topic or IP for debugging.";
desc("shared_subscription_group") -> desc("shared_subscription_group") ->
"Per group dispatch strategy for shared subscription"; "Per group dispatch strategy for shared subscription";
desc("ocsp") ->
"Per listener OCSP Stapling configuration.";
desc(_) -> desc(_) ->
undefined. undefined.
@ -2199,14 +2248,62 @@ server_ssl_opts_schema(Defaults, IsRanchListener) ->
)} )}
] ++ ] ++
[ [
Field
|| not IsRanchListener,
Field <- [
{"gc_after_handshake", {"gc_after_handshake",
sc(boolean(), #{ sc(boolean(), #{
default => false, default => false,
desc => ?DESC(server_ssl_opts_schema_gc_after_handshake) desc => ?DESC(server_ssl_opts_schema_gc_after_handshake)
})} })},
|| not IsRanchListener {"ocsp",
sc(
ref("ocsp"),
#{
required => false,
validator => fun ocsp_inner_validator/1
}
)}
]
]. ].
mqtt_ssl_listener_ssl_options_validator(Conf) ->
Checks = [
fun ocsp_outer_validator/1
],
case emqx_misc:pipeline(Checks, Conf, not_used) of
{ok, _, _} ->
ok;
{error, Reason, _NotUsed} ->
{error, Reason}
end.
ocsp_outer_validator(#{<<"ocsp">> := #{<<"enable_ocsp_stapling">> := true}} = Conf) ->
%% outer mqtt listener ssl server config
ServerCertPemPath = maps:get(<<"certfile">>, Conf, undefined),
case ServerCertPemPath of
undefined ->
{error, "Server certificate must be defined when using OCSP stapling"};
_ ->
%% check if issuer pem is readable and/or valid?
ok
end;
ocsp_outer_validator(_Conf) ->
ok.
ocsp_inner_validator(#{enable_ocsp_stapling := _} = Conf) ->
ocsp_inner_validator(emqx_map_lib:binary_key_map(Conf));
ocsp_inner_validator(#{<<"enable_ocsp_stapling">> := false} = _Conf) ->
ok;
ocsp_inner_validator(#{<<"enable_ocsp_stapling">> := true} = Conf) ->
assert_required_field(
Conf, <<"responder_url">>, "The responder URL is required for OCSP stapling"
),
assert_required_field(
Conf, <<"issuer_pem">>, "The issuer PEM path is required for OCSP stapling"
),
ok.
%% @doc Make schema for SSL client. %% @doc Make schema for SSL client.
-spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). -spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema().
client_ssl_opts_schema(Defaults) -> client_ssl_opts_schema(Defaults) ->
@ -2408,6 +2505,15 @@ to_comma_separated_binary(Str) ->
to_comma_separated_atoms(Str) -> to_comma_separated_atoms(Str) ->
{ok, lists:map(fun to_atom/1, string:tokens(Str, ", "))}. {ok, lists:map(fun to_atom/1, string:tokens(Str, ", "))}.
to_url(Str) ->
case emqx_http_lib:uri_parse(Str) of
{ok, URIMap} ->
URIString = emqx_http_lib:normalize(URIMap),
{ok, iolist_to_binary(URIString)};
Error ->
Error
end.
to_bar_separated_list(Str) -> to_bar_separated_list(Str) ->
{ok, string:tokens(Str, "| ")}. {ok, string:tokens(Str, "| ")}.
@ -2865,3 +2971,11 @@ is_quic_ssl_opts(Name) ->
%% , "handshake_timeout" %% , "handshake_timeout"
%% , "gc_after_handshake" %% , "gc_after_handshake"
]). ]).
assert_required_field(Conf, Key, ErrorMessage) ->
case maps:get(Key, Conf, undefined) of
undefined ->
throw(ErrorMessage);
_ ->
ok
end.

View File

@ -47,8 +47,18 @@
-define(IS_TRUE(Val), ((Val =:= true) orelse (Val =:= <<"true">>))). -define(IS_TRUE(Val), ((Val =:= true) orelse (Val =:= <<"true">>))).
-define(IS_FALSE(Val), ((Val =:= false) orelse (Val =:= <<"false">>))). -define(IS_FALSE(Val), ((Val =:= false) orelse (Val =:= <<"false">>))).
-define(SSL_FILE_OPT_NAMES, [<<"keyfile">>, <<"certfile">>, <<"cacertfile">>]). -define(SSL_FILE_OPT_PATHS, [
-define(SSL_FILE_OPT_NAMES_A, [keyfile, certfile, cacertfile]). [<<"keyfile">>],
[<<"certfile">>],
[<<"cacertfile">>],
[<<"ocsp">>, <<"issuer_pem">>]
]).
-define(SSL_FILE_OPT_PATHS_A, [
[keyfile],
[certfile],
[cacertfile],
[ocsp, issuer_pem]
]).
%% non-empty string %% non-empty string
-define(IS_STRING(L), (is_list(L) andalso L =/= [] andalso is_integer(hd(L)))). -define(IS_STRING(L), (is_list(L) andalso L =/= [] andalso is_integer(hd(L)))).
@ -298,20 +308,20 @@ ensure_ssl_files(Dir, SSL, Opts) ->
RequiredKeys = maps:get(required_keys, Opts, []), RequiredKeys = maps:get(required_keys, Opts, []),
case ensure_ssl_file_key(SSL, RequiredKeys) of case ensure_ssl_file_key(SSL, RequiredKeys) of
ok -> ok ->
Keys = ?SSL_FILE_OPT_NAMES ++ ?SSL_FILE_OPT_NAMES_A, KeyPaths = ?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A,
ensure_ssl_files(Dir, SSL, Keys, Opts); ensure_ssl_files(Dir, SSL, KeyPaths, Opts);
{error, _} = Error -> {error, _} = Error ->
Error Error
end. end.
ensure_ssl_files(_Dir, SSL, [], _Opts) -> ensure_ssl_files(_Dir, SSL, [], _Opts) ->
{ok, SSL}; {ok, SSL};
ensure_ssl_files(Dir, SSL, [Key | Keys], Opts) -> ensure_ssl_files(Dir, SSL, [KeyPath | KeyPaths], Opts) ->
case ensure_ssl_file(Dir, Key, SSL, maps:get(Key, SSL, undefined), Opts) of case ensure_ssl_file(Dir, KeyPath, SSL, emqx_map_lib:deep_get(KeyPath, SSL, undefined), Opts) of
{ok, NewSSL} -> {ok, NewSSL} ->
ensure_ssl_files(Dir, NewSSL, Keys, Opts); ensure_ssl_files(Dir, NewSSL, KeyPaths, Opts);
{error, Reason} -> {error, Reason} ->
{error, Reason#{which_options => [Key]}} {error, Reason#{which_options => [KeyPath]}}
end. end.
%% @doc Compare old and new config, delete the ones in old but not in new. %% @doc Compare old and new config, delete the ones in old but not in new.
@ -321,12 +331,12 @@ delete_ssl_files(Dir, NewOpts0, OldOpts0) ->
{ok, NewOpts} = ensure_ssl_files(Dir, NewOpts0, #{dry_run => DryRun}), {ok, NewOpts} = ensure_ssl_files(Dir, NewOpts0, #{dry_run => DryRun}),
{ok, OldOpts} = ensure_ssl_files(Dir, OldOpts0, #{dry_run => DryRun}), {ok, OldOpts} = ensure_ssl_files(Dir, OldOpts0, #{dry_run => DryRun}),
Get = fun Get = fun
(_K, undefined) -> undefined; (_KP, undefined) -> undefined;
(K, Opts) -> maps:get(K, Opts, undefined) (KP, Opts) -> emqx_map_lib:deep_get(KP, Opts, undefined)
end, end,
lists:foreach( lists:foreach(
fun(Key) -> delete_old_file(Get(Key, NewOpts), Get(Key, OldOpts)) end, fun(KeyPath) -> delete_old_file(Get(KeyPath, NewOpts), Get(KeyPath, OldOpts)) end,
?SSL_FILE_OPT_NAMES ++ ?SSL_FILE_OPT_NAMES_A ?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A
), ),
%% try to delete the dir if it is empty %% try to delete the dir if it is empty
_ = file:del_dir(pem_dir(Dir)), _ = file:del_dir(pem_dir(Dir)),
@ -346,29 +356,33 @@ delete_old_file(_New, Old) ->
?SLOG(error, #{msg => "failed_to_delete_ssl_file", file_path => Old, reason => Reason}) ?SLOG(error, #{msg => "failed_to_delete_ssl_file", file_path => Old, reason => Reason})
end. end.
ensure_ssl_file(_Dir, _Key, SSL, undefined, _Opts) -> ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) ->
{ok, SSL}; {ok, SSL};
ensure_ssl_file(Dir, Key, SSL, MaybePem, Opts) -> ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, Opts) ->
case is_valid_string(MaybePem) of case is_valid_string(MaybePem) of
true -> true ->
DryRun = maps:get(dry_run, Opts, false), DryRun = maps:get(dry_run, Opts, false),
do_ensure_ssl_file(Dir, Key, SSL, MaybePem, DryRun); do_ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, DryRun);
false -> false ->
{error, #{reason => invalid_file_path_or_pem_string}} {error, #{reason => invalid_file_path_or_pem_string}}
end. end.
do_ensure_ssl_file(Dir, Key, SSL, MaybePem, DryRun) -> do_ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, DryRun) ->
case is_pem(MaybePem) of case is_pem(MaybePem) of
true -> true ->
case save_pem_file(Dir, Key, MaybePem, DryRun) of case save_pem_file(Dir, KeyPath, MaybePem, DryRun) of
{ok, Path} -> {ok, SSL#{Key => Path}}; {ok, Path} ->
{error, Reason} -> {error, Reason} NewSSL = emqx_map_lib:deep_put(KeyPath, SSL, Path),
{ok, NewSSL};
{error, Reason} ->
{error, Reason}
end; end;
false -> false ->
case is_valid_pem_file(MaybePem) of case is_valid_pem_file(MaybePem) of
true -> true ->
{ok, SSL}; {ok, SSL};
{error, enoent} when DryRun -> {ok, SSL}; {error, enoent} when DryRun ->
{ok, SSL};
{error, Reason} -> {error, Reason} ->
{error, #{ {error, #{
pem_check => invalid_pem, pem_check => invalid_pem,
@ -398,8 +412,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, Key, Pem, DryRun) -> save_pem_file(Dir, KeyPath, Pem, DryRun) ->
Path = pem_file_name(Dir, Key, 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 ->
{ok, Path}; {ok, Path};
@ -422,11 +436,14 @@ is_generated_file(Filename) ->
_ -> false _ -> false
end. end.
pem_file_name(Dir, Key, Pem) -> pem_file_name(Dir, KeyPath, Pem) ->
<<CK:8/binary, _/binary>> = crypto:hash(md5, Pem), <<CK:8/binary, _/binary>> = crypto:hash(md5, Pem),
Suffix = hex_str(CK), Suffix = hex_str(CK),
FileName = binary:replace(ensure_bin(Key), <<"file">>, <<"-", Suffix/binary>>), Segments = lists:map(fun ensure_bin/1, KeyPath),
filename:join([pem_dir(Dir), FileName]). Filename0 = iolist_to_binary(lists:join(<<"_">>, Segments)),
Filename1 = binary:replace(Filename0, <<"file">>, <<>>),
Filename = <<Filename1/binary, "-", Suffix/binary>>,
filename:join([pem_dir(Dir), Filename]).
pem_dir(Dir) -> pem_dir(Dir) ->
filename:join([emqx:mutable_certs_dir(), Dir]). filename:join([emqx:mutable_certs_dir(), Dir]).
@ -465,24 +482,26 @@ is_valid_pem_file(Path) ->
%% so they are forced to upload a cert file, or use an existing file path. %% so they are forced to upload a cert file, or use an existing file path.
-spec drop_invalid_certs(map()) -> map(). -spec drop_invalid_certs(map()) -> map().
drop_invalid_certs(#{enable := False} = SSL) when ?IS_FALSE(False) -> drop_invalid_certs(#{enable := False} = SSL) when ?IS_FALSE(False) ->
maps:without(?SSL_FILE_OPT_NAMES_A, SSL); lists:foldl(fun emqx_map_lib:deep_remove/2, SSL, ?SSL_FILE_OPT_PATHS_A);
drop_invalid_certs(#{<<"enable">> := False} = SSL) when ?IS_FALSE(False) -> drop_invalid_certs(#{<<"enable">> := False} = SSL) when ?IS_FALSE(False) ->
maps:without(?SSL_FILE_OPT_NAMES, SSL); lists:foldl(fun emqx_map_lib:deep_remove/2, SSL, ?SSL_FILE_OPT_PATHS);
drop_invalid_certs(#{enable := True} = SSL) when ?IS_TRUE(True) -> drop_invalid_certs(#{enable := True} = SSL) when ?IS_TRUE(True) ->
do_drop_invalid_certs(?SSL_FILE_OPT_NAMES_A, SSL); do_drop_invalid_certs(?SSL_FILE_OPT_PATHS_A, SSL);
drop_invalid_certs(#{<<"enable">> := True} = SSL) when ?IS_TRUE(True) -> drop_invalid_certs(#{<<"enable">> := True} = SSL) when ?IS_TRUE(True) ->
do_drop_invalid_certs(?SSL_FILE_OPT_NAMES, SSL). do_drop_invalid_certs(?SSL_FILE_OPT_PATHS, SSL).
do_drop_invalid_certs([], SSL) -> do_drop_invalid_certs([], SSL) ->
SSL; SSL;
do_drop_invalid_certs([Key | Keys], SSL) -> do_drop_invalid_certs([KeyPath | KeyPaths], SSL) ->
case maps:get(Key, SSL, undefined) of case emqx_map_lib:deep_get(KeyPath, SSL, undefined) of
undefined -> undefined ->
do_drop_invalid_certs(Keys, SSL); do_drop_invalid_certs(KeyPaths, SSL);
PemOrPath -> PemOrPath ->
case is_pem(PemOrPath) orelse is_valid_pem_file(PemOrPath) of case is_pem(PemOrPath) orelse is_valid_pem_file(PemOrPath) of
true -> do_drop_invalid_certs(Keys, SSL); true ->
{error, _} -> do_drop_invalid_certs(Keys, maps:without([Key], SSL)) do_drop_invalid_certs(KeyPaths, SSL);
{error, _} ->
do_drop_invalid_certs(KeyPaths, emqx_map_lib:deep_remove(KeyPath, SSL))
end end
end. end.
@ -565,9 +584,10 @@ ensure_bin(A) when is_atom(A) -> atom_to_binary(A, utf8).
ensure_ssl_file_key(_SSL, []) -> ensure_ssl_file_key(_SSL, []) ->
ok; ok;
ensure_ssl_file_key(SSL, RequiredKeys) -> ensure_ssl_file_key(SSL, RequiredKeyPaths) ->
Filter = fun(Key) -> not maps:is_key(Key, SSL) end, NotFoundRef = make_ref(),
case lists:filter(Filter, RequiredKeys) of Filter = fun(KeyPath) -> NotFoundRef =:= emqx_map_lib:deep_get(KeyPath, SSL, NotFoundRef) end,
case lists:filter(Filter, RequiredKeyPaths) of
[] -> ok; [] -> ok;
Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}} Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}}
end. end.

View File

@ -26,6 +26,7 @@
all() -> emqx_common_test_helpers:all(?MODULE). all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_common_test_helpers:boot_modules(all),
emqx_common_test_helpers:start_apps([]), emqx_common_test_helpers:start_apps([]),
Config. Config.

View File

@ -16,6 +16,8 @@
-module(emqx_common_test_helpers). -module(emqx_common_test_helpers).
-include("emqx_authentication.hrl").
-type special_config_handler() :: fun(). -type special_config_handler() :: fun().
-type apps() :: list(atom()). -type apps() :: list(atom()).
@ -27,6 +29,7 @@
boot_modules/1, boot_modules/1,
start_apps/1, start_apps/1,
start_apps/2, start_apps/2,
start_apps/3,
stop_apps/1, stop_apps/1,
reload/2, reload/2,
app_path/2, app_path/2,
@ -34,7 +37,8 @@
deps_path/2, deps_path/2,
flush/0, flush/0,
flush/1, flush/1,
render_and_load_app_config/1 render_and_load_app_config/1,
render_and_load_app_config/2
]). ]).
-export([ -export([
@ -183,17 +187,21 @@ start_apps(Apps) ->
application:set_env(system_monitor, db_hostname, ""), application:set_env(system_monitor, db_hostname, ""),
ok ok
end, end,
start_apps(Apps, DefaultHandler). start_apps(Apps, DefaultHandler, #{}).
-spec start_apps(Apps :: apps(), Handler :: special_config_handler()) -> ok. -spec start_apps(Apps :: apps(), Handler :: special_config_handler()) -> ok.
start_apps(Apps, SpecAppConfig) when is_function(SpecAppConfig) -> start_apps(Apps, SpecAppConfig) when is_function(SpecAppConfig) ->
start_apps(Apps, SpecAppConfig, #{}).
-spec start_apps(Apps :: apps(), Handler :: special_config_handler(), map()) -> ok.
start_apps(Apps, SpecAppConfig, Opts) when is_function(SpecAppConfig) ->
%% Load all application code to beam vm first %% Load all application code to beam vm first
%% Because, minirest, ekka etc.. application will scan these modules %% Because, minirest, ekka etc.. application will scan these modules
lists:foreach(fun load/1, [emqx | Apps]), lists:foreach(fun load/1, [emqx | Apps]),
ok = start_ekka(), ok = start_ekka(),
mnesia:clear_table(emqx_admin), mnesia:clear_table(emqx_admin),
ok = emqx_ratelimiter_SUITE:load_conf(), ok = emqx_ratelimiter_SUITE:load_conf(),
lists:foreach(fun(App) -> start_app(App, SpecAppConfig) end, [emqx | Apps]). lists:foreach(fun(App) -> start_app(App, SpecAppConfig, Opts) end, [emqx | Apps]).
load(App) -> load(App) ->
case application:load(App) of case application:load(App) of
@ -203,27 +211,31 @@ load(App) ->
end. end.
render_and_load_app_config(App) -> render_and_load_app_config(App) ->
render_and_load_app_config(App, #{}).
render_and_load_app_config(App, Opts) ->
load(App), load(App),
Schema = app_schema(App), Schema = app_schema(App),
Conf = app_path(App, filename:join(["etc", app_conf_file(App)])), ConfFilePath = maps:get(conf_file_path, Opts, filename:join(["etc", app_conf_file(App)])),
Conf = app_path(App, ConfFilePath),
try try
do_render_app_config(App, Schema, Conf) do_render_app_config(App, Schema, Conf, Opts)
catch catch
throw:E:St -> throw:E:St ->
%% turn throw into error %% turn throw into error
error({Conf, E, St}) error({Conf, E, St})
end. end.
do_render_app_config(App, Schema, ConfigFile) -> do_render_app_config(App, Schema, ConfigFile, Opts) ->
Vars = mustache_vars(App), Vars = mustache_vars(App, Opts),
RenderedConfigFile = render_config_file(ConfigFile, Vars), RenderedConfigFile = render_config_file(ConfigFile, Vars),
read_schema_configs(Schema, RenderedConfigFile), read_schema_configs(Schema, RenderedConfigFile),
force_set_config_file_paths(App, [RenderedConfigFile]), force_set_config_file_paths(App, [RenderedConfigFile]),
copy_certs(App, RenderedConfigFile), copy_certs(App, RenderedConfigFile),
ok. ok.
start_app(App, SpecAppConfig) -> start_app(App, SpecAppConfig, Opts) ->
render_and_load_app_config(App), render_and_load_app_config(App, Opts),
SpecAppConfig(App), SpecAppConfig(App),
case application:ensure_all_started(App) of case application:ensure_all_started(App) of
{ok, _} -> {ok, _} ->
@ -246,12 +258,13 @@ app_schema(App) ->
no_schema no_schema
end. end.
mustache_vars(App) -> mustache_vars(App, Opts) ->
ExtraMustacheVars = maps:get(extra_mustache_vars, Opts, []),
[ [
{platform_data_dir, app_path(App, "data")}, {platform_data_dir, app_path(App, "data")},
{platform_etc_dir, app_path(App, "etc")}, {platform_etc_dir, app_path(App, "etc")},
{platform_log_dir, app_path(App, "log")} {platform_log_dir, app_path(App, "log")}
]. ] ++ ExtraMustacheVars.
render_config_file(ConfigFile, Vars0) -> render_config_file(ConfigFile, Vars0) ->
Temp = Temp =
@ -283,6 +296,14 @@ generate_config(SchemaModule, ConfigFile) when is_atom(SchemaModule) ->
-spec stop_apps(list()) -> ok. -spec stop_apps(list()) -> ok.
stop_apps(Apps) -> stop_apps(Apps) ->
[application:stop(App) || App <- Apps ++ [emqx, ekka, mria, mnesia]], [application:stop(App) || App <- Apps ++ [emqx, ekka, mria, mnesia]],
%% to avoid inter-suite flakiness
application:unset_env(emqx, init_config_load_done),
persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY),
emqx_config:erase_schema_mod_and_names(),
ok = emqx_config:delete_override_conf_files(),
application:unset_env(emqx, local_override_conf_file),
application:unset_env(emqx, cluster_override_conf_file),
application:unset_env(gen_rpc, port_discovery),
ok. ok.
proj_root() -> proj_root() ->
@ -327,7 +348,7 @@ safe_relative_path_2(Path) ->
-spec reload(App :: atom(), SpecAppConfig :: special_config_handler()) -> ok. -spec reload(App :: atom(), SpecAppConfig :: special_config_handler()) -> ok.
reload(App, SpecAppConfigHandler) -> reload(App, SpecAppConfigHandler) ->
application:stop(App), application:stop(App),
start_app(App, SpecAppConfigHandler), start_app(App, SpecAppConfigHandler, #{}),
application:start(App). application:start(App).
ensure_mnesia_stopped() -> ensure_mnesia_stopped() ->
@ -469,7 +490,7 @@ is_all_tcp_servers_available(Servers) ->
{_, []} -> {_, []} ->
true; true;
{_, Unavail} -> {_, Unavail} ->
ct:print("Unavailable servers: ~p", [Unavail]), ct:pal("Unavailable servers: ~p", [Unavail]),
false false
end. end.

View File

@ -0,0 +1,944 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ocsp_cache_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-include_lib("ssl/src/ssl_handshake.hrl").
-define(CACHE_TAB, emqx_ocsp_cache).
all() ->
[{group, openssl}] ++ tests().
tests() ->
emqx_common_test_helpers:all(?MODULE) -- openssl_tests().
openssl_tests() ->
[t_openssl_client].
groups() ->
OpensslTests = openssl_tests(),
[
{openssl, [
{group, tls12},
{group, tls13}
]},
{tls12, [
{group, with_status_request},
{group, without_status_request}
]},
{tls13, [
{group, with_status_request},
{group, without_status_request}
]},
{with_status_request, [], OpensslTests},
{without_status_request, [], OpensslTests}
].
init_per_suite(Config) ->
application:load(emqx),
emqx_config:save_schema_mod_and_names(emqx_schema),
emqx_common_test_helpers:boot_modules(all),
Config.
end_per_suite(_Config) ->
ok.
init_per_group(tls12, Config) ->
[{tls_vsn, "-tls1_2"} | Config];
init_per_group(tls13, Config) ->
[{tls_vsn, "-tls1_3"} | Config];
init_per_group(with_status_request, Config) ->
[{status_request, true} | Config];
init_per_group(without_status_request, Config) ->
[{status_request, false} | Config];
init_per_group(_Group, Config) ->
Config.
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(t_openssl_client, Config) ->
ct:timetrap({seconds, 30}),
DataDir = ?config(data_dir, Config),
Handler = fun(_) -> ok end,
{OCSPResponderPort, OCSPOSPid} = setup_openssl_ocsp(Config),
ConfFilePath = filename:join([DataDir, "openssl_listeners.conf"]),
emqx_common_test_helpers:start_apps(
[],
Handler,
#{
extra_mustache_vars => [{test_data_dir, DataDir}],
conf_file_path => ConfFilePath
}
),
ct:sleep(1_000),
[
{ocsp_responder_port, OCSPResponderPort},
{ocsp_responder_os_pid, OCSPOSPid}
| Config
];
init_per_testcase(TestCase, Config) when
TestCase =:= t_update_listener;
TestCase =:= t_validations
->
%% when running emqx standalone tests, we can't use those
%% features.
case does_module_exist(emqx_mgmt_api_test_util) of
true ->
ct:timetrap({seconds, 30}),
%% start the listener with the default (non-ocsp) config
TestPid = self(),
ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]),
meck:expect(
emqx_ocsp_cache,
http_get,
fun(URL, _HTTPTimeout) ->
ct:pal("ocsp http request ~p", [URL]),
TestPid ! {http_get, URL},
{ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"ocsp response">>}}
end
),
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
snabbkaffe:start_trace(),
Config;
false ->
[{skip_does_not_apply, true} | Config]
end;
init_per_testcase(t_ocsp_responder_error_responses, Config) ->
ct:timetrap({seconds, 30}),
TestPid = self(),
ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]),
meck:expect(
emqx_ocsp_cache,
http_get,
fun(URL, _HTTPTimeout) ->
ct:pal("ocsp http request ~p", [URL]),
TestPid ! {http_get, URL},
persistent_term:get({?MODULE, http_response})
end
),
DataDir = ?config(data_dir, Config),
Type = ssl,
Name = test_ocsp,
ListenerOpts = #{
ssl_options =>
#{
certfile => filename:join(DataDir, "server.pem"),
ocsp => #{
enable_ocsp_stapling => true,
responder_url => <<"http://localhost:9877/">>,
issuer_pem => filename:join(DataDir, "ocsp-issuer.pem"),
refresh_http_timeout => 15_000,
refresh_interval => 1_000
}
}
},
Conf = #{listeners => #{Type => #{Name => ListenerOpts}}},
ConfBin = emqx_map_lib:binary_key_map(Conf),
hocon_tconf:check_plain(emqx_schema, ConfBin, #{required => false, atom_keys => false}),
emqx_config:put_listener_conf(Type, Name, [], ListenerOpts),
snabbkaffe:start_trace(),
_Heir = spawn_dummy_heir(),
{ok, CachePid} = emqx_ocsp_cache:start_link(),
[
{cache_pid, CachePid}
| Config
];
init_per_testcase(_TestCase, Config) ->
ct:timetrap({seconds, 10}),
TestPid = self(),
ok = meck:new(emqx_ocsp_cache, [non_strict, passthrough, no_history, no_link]),
meck:expect(
emqx_ocsp_cache,
http_get,
fun(URL, _HTTPTimeout) ->
TestPid ! {http_get, URL},
{ok, {{"HTTP/1.0", 200, 'OK'}, [], <<"ocsp response">>}}
end
),
_Heir = spawn_dummy_heir(),
{ok, CachePid} = emqx_ocsp_cache:start_link(),
DataDir = ?config(data_dir, Config),
Type = ssl,
Name = test_ocsp,
ListenerOpts = #{
ssl_options =>
#{
certfile => filename:join(DataDir, "server.pem"),
ocsp => #{
enable_ocsp_stapling => true,
responder_url => <<"http://localhost:9877/">>,
issuer_pem => filename:join(DataDir, "ocsp-issuer.pem"),
refresh_http_timeout => 15_000,
refresh_interval => 1_000
}
}
},
Conf = #{listeners => #{Type => #{Name => ListenerOpts}}},
ConfBin = emqx_map_lib:binary_key_map(Conf),
hocon_tconf:check_plain(emqx_schema, ConfBin, #{required => false, atom_keys => false}),
emqx_config:put_listener_conf(Type, Name, [], ListenerOpts),
snabbkaffe:start_trace(),
[
{cache_pid, CachePid}
| Config
].
end_per_testcase(t_openssl_client, Config) ->
OCSPResponderOSPid = ?config(ocsp_responder_os_pid, Config),
catch kill_pid(OCSPResponderOSPid),
emqx_common_test_helpers:stop_apps([]),
ok;
end_per_testcase(TestCase, Config) when
TestCase =:= t_update_listener;
TestCase =:= t_validations
->
Skip = proplists:get_bool(skip_does_not_apply, Config),
case Skip of
true ->
ok;
false ->
emqx_mgmt_api_test_util:end_suite([emqx_conf]),
meck:unload([emqx_ocsp_cache]),
ok
end;
end_per_testcase(t_ocsp_responder_error_responses, Config) ->
CachePid = ?config(cache_pid, Config),
catch gen_server:stop(CachePid),
meck:unload([emqx_ocsp_cache]),
persistent_term:erase({?MODULE, http_response}),
ok;
end_per_testcase(_TestCase, Config) ->
CachePid = ?config(cache_pid, Config),
catch gen_server:stop(CachePid),
meck:unload([emqx_ocsp_cache]),
ok.
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
%% The real cache makes `emqx_kernel_sup' the heir to its ETS table.
%% In some tests, we don't start the full supervision tree, so we need
%% this dummy process.
spawn_dummy_heir() ->
spawn_link(fun() ->
true = register(emqx_kernel_sup, self()),
receive
stop -> ok
end
end).
does_module_exist(Mod) ->
case erlang:module_loaded(Mod) of
true ->
true;
false ->
case code:ensure_loaded(Mod) of
ok ->
true;
{module, Mod} ->
true;
_ ->
false
end
end.
assert_no_http_get() ->
receive
{http_get, _URL} ->
error(should_be_cached)
after 0 ->
ok
end.
assert_http_get(N) ->
assert_http_get(N, 0).
assert_http_get(0, _Timeout) ->
ok;
assert_http_get(N, Timeout) when N > 0 ->
receive
{http_get, URL} ->
?assertMatch(<<"http://localhost:9877/", _Request64/binary>>, URL),
ok
after Timeout ->
error({no_http_get, #{mailbox => process_info(self(), messages)}})
end,
assert_http_get(N - 1, Timeout).
openssl_client_command(TLSVsn, RequestStatus, Config) ->
DataDir = ?config(data_dir, Config),
ClientCert = filename:join([DataDir, "client.pem"]),
ClientKey = filename:join([DataDir, "client.key"]),
Cacert = filename:join([DataDir, "ca.pem"]),
Openssl = os:find_executable("openssl"),
StatusOpt =
case RequestStatus of
true -> ["-status"];
false -> []
end,
[
Openssl,
"s_client",
"-connect",
"localhost:8883",
%% needed to trigger `sni_fun'
"-servername",
"localhost",
TLSVsn,
"-CAfile",
Cacert,
"-cert",
ClientCert,
"-key",
ClientKey
] ++ StatusOpt.
run_openssl_client(TLSVsn, RequestStatus, Config) ->
Command0 = openssl_client_command(TLSVsn, RequestStatus, Config),
Command = lists:flatten(lists:join(" ", Command0)),
os:cmd(Command).
%% fixme: for some reason, the port program doesn't return any output
%% when running in OTP 25 using `open_port`, but the `os:cmd` version
%% works fine.
%% the `open_port' version works fine in OTP 24 for some reason.
spawn_openssl_client(TLSVsn, RequestStatus, Config) ->
[Openssl | Args] = openssl_client_command(TLSVsn, RequestStatus, Config),
open_port(
{spawn_executable, Openssl},
[
{args, Args},
binary,
stderr_to_stdout
]
).
spawn_openssl_ocsp_responder(Config) ->
DataDir = ?config(data_dir, Config),
IssuerCert = filename:join([DataDir, "ocsp-issuer.pem"]),
IssuerKey = filename:join([DataDir, "ocsp-issuer.key"]),
Cacert = filename:join([DataDir, "ca.pem"]),
Index = filename:join([DataDir, "index.txt"]),
Openssl = os:find_executable("openssl"),
open_port(
{spawn_executable, Openssl},
[
{args, [
"ocsp",
"-ignore_err",
"-port",
"9877",
"-CA",
Cacert,
"-rkey",
IssuerKey,
"-rsigner",
IssuerCert,
"-index",
Index
]},
binary,
stderr_to_stdout
]
).
kill_pid(OSPid) ->
os:cmd("kill -9 " ++ integer_to_list(OSPid)).
test_ocsp_connection(TLSVsn, WithRequestStatus = true, Config) ->
OCSPOutput = run_openssl_client(TLSVsn, WithRequestStatus, Config),
?assertMatch(
{match, _},
re:run(OCSPOutput, "OCSP Response Status: successful"),
#{mailbox => process_info(self(), messages)}
),
?assertMatch(
{match, _},
re:run(OCSPOutput, "Cert Status: good"),
#{mailbox => process_info(self(), messages)}
),
ok;
test_ocsp_connection(TLSVsn, WithRequestStatus = false, Config) ->
OCSPOutput = run_openssl_client(TLSVsn, WithRequestStatus, Config),
?assertMatch(
nomatch,
re:run(OCSPOutput, "Cert Status: good", [{capture, none}]),
#{mailbox => process_info(self(), messages)}
),
ok.
ensure_port_open(Port) ->
do_ensure_port_open(Port, 10).
do_ensure_port_open(Port, 0) ->
error({port_not_open, Port});
do_ensure_port_open(Port, N) when N > 0 ->
Timeout = 1_000,
case gen_tcp:connect("localhost", Port, [], Timeout) of
{ok, Sock} ->
gen_tcp:close(Sock),
ok;
{error, _} ->
ct:sleep(500),
do_ensure_port_open(Port, N - 1)
end.
get_sni_fun(ListenerID) ->
#{opts := Opts} = emqx_listeners:find_by_id(ListenerID),
SSLOpts = proplists:get_value(ssl_options, Opts),
proplists:get_value(sni_fun, SSLOpts).
openssl_version() ->
Res0 = string:trim(os:cmd("openssl version"), trailing),
[_, Res] = string:split(Res0, " "),
{match, [Version]} = re:run(Res, "^([^ ]+)", [{capture, first, list}]),
Version.
setup_openssl_ocsp(Config) ->
OCSPResponderPort = spawn_openssl_ocsp_responder(Config),
{os_pid, OCSPOSPid} = erlang:port_info(OCSPResponderPort, os_pid),
%%%%%%%% Warning!!!
%% Apparently, openssl 3.0.7 introduced a bug in the responder
%% that makes it hang forever if one probes the port with
%% `gen_tcp:open' / `gen_tcp:close'... Comment this out if
%% openssl gets updated in CI or in your local machine.
OpenSSLVersion = openssl_version(),
ct:pal("openssl version: ~p", [OpenSSLVersion]),
case OpenSSLVersion of
"3." ++ _ ->
%% hope that the responder has started...
ok;
_ ->
ensure_port_open(9877)
end,
ct:sleep(1_000),
{OCSPResponderPort, OCSPOSPid}.
request(Method, Url, QueryParams, Body) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
Opts = #{return_all => true},
case emqx_mgmt_api_test_util:request_api(Method, Url, QueryParams, AuthHeader, Body, Opts) of
{ok, {Reason, Headers, BodyR}} ->
{ok, {Reason, Headers, emqx_json:decode(BodyR, [return_maps])}};
Error ->
Error
end.
get_listener_via_api(ListenerId) ->
Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]),
request(get, Path, [], []).
update_listener_via_api(ListenerId, NewConfig) ->
Path = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]),
request(put, Path, [], NewConfig).
put_http_response(Response) ->
persistent_term:put({?MODULE, http_response}, Response).
%%--------------------------------------------------------------------
%% Test cases
%%--------------------------------------------------------------------
t_request_ocsp_response(_Config) ->
?check_trace(
begin
ListenerID = <<"ssl:test_ocsp">>,
%% not yet cached.
?assertEqual([], ets:tab2list(?CACHE_TAB)),
?assertEqual(
{ok, <<"ocsp response">>},
emqx_ocsp_cache:fetch_response(ListenerID)
),
assert_http_get(1),
?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
%% already cached; should not perform request again.
?assertEqual(
{ok, <<"ocsp response">>},
emqx_ocsp_cache:fetch_response(ListenerID)
),
assert_no_http_get(),
ok
end,
fun(Trace) ->
?assert(
?strict_causality(
#{?snk_kind := ocsp_cache_miss, listener_id := _ListenerID},
#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := _ListenerID},
Trace
)
),
?assertMatch(
[_],
?of_kind(ocsp_cache_miss, Trace)
),
?assertMatch(
[_],
?of_kind(ocsp_http_fetch_and_cache, Trace)
),
?assertMatch(
[_],
?of_kind(ocsp_cache_hit, Trace)
),
ok
end
).
t_request_ocsp_response_restart_cache(Config) ->
process_flag(trap_exit, true),
CachePid = ?config(cache_pid, Config),
ListenerID = <<"ssl:test_ocsp">>,
?check_trace(
begin
[] = ets:tab2list(?CACHE_TAB),
{ok, _} = emqx_ocsp_cache:fetch_response(ListenerID),
?wait_async_action(
begin
Ref = monitor(process, CachePid),
exit(CachePid, kill),
receive
{'DOWN', Ref, process, CachePid, killed} ->
ok
after 1_000 ->
error(cache_not_killed)
end,
{ok, _} = emqx_ocsp_cache:start_link(),
ok
end,
#{?snk_kind := ocsp_cache_init}
),
{ok, _} = emqx_ocsp_cache:fetch_response(ListenerID),
ok
end,
fun(Trace) ->
%% Only one fetch because the cache table was preserved by
%% its heir ("emqx_kernel_sup").
?assertMatch(
[_],
?of_kind(ocsp_http_fetch_and_cache, Trace)
),
assert_http_get(1),
ok
end
).
t_request_ocsp_response_bad_http_status(_Config) ->
TestPid = self(),
meck:expect(
emqx_ocsp_cache,
http_get,
fun(URL, _HTTPTimeout) ->
TestPid ! {http_get, URL},
{ok, {{"HTTP/1.0", 404, 'Not Found'}, [], <<"not found">>}}
end
),
ListenerID = <<"ssl:test_ocsp">>,
%% not yet cached.
?assertEqual([], ets:tab2list(?CACHE_TAB)),
?assertEqual(
error,
emqx_ocsp_cache:fetch_response(ListenerID)
),
assert_http_get(1),
?assertEqual([], ets:tab2list(?CACHE_TAB)),
ok.
t_request_ocsp_response_timeout(_Config) ->
TestPid = self(),
meck:expect(
emqx_ocsp_cache,
http_get,
fun(URL, _HTTPTimeout) ->
TestPid ! {http_get, URL},
{error, timeout}
end
),
ListenerID = <<"ssl:test_ocsp">>,
%% not yet cached.
?assertEqual([], ets:tab2list(?CACHE_TAB)),
?assertEqual(
error,
emqx_ocsp_cache:fetch_response(ListenerID)
),
assert_http_get(1),
?assertEqual([], ets:tab2list(?CACHE_TAB)),
ok.
t_register_listener(_Config) ->
ListenerID = <<"ssl:test_ocsp">>,
Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
%% should fetch and cache immediately
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerID, Conf),
#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}
),
assert_http_get(1),
?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
ok.
t_register_twice(_Config) ->
ListenerID = <<"ssl:test_ocsp">>,
Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerID, Conf),
#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}
),
assert_http_get(1),
?assertMatch([{_, <<"ocsp response">>}], ets:tab2list(?CACHE_TAB)),
%% should have no problem in registering the same listener again.
%% this prompts an immediate refresh.
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerID, Conf),
#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID}
),
ok.
t_refresh_periodically(_Config) ->
ListenerID = <<"ssl:test_ocsp">>,
Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
%% should refresh periodically
{ok, SubRef} =
snabbkaffe:subscribe(
fun
(#{?snk_kind := ocsp_http_fetch_and_cache, listener_id := ListenerID0}) ->
ListenerID0 =:= ListenerID;
(_) ->
false
end,
_NEvents = 2,
_Timeout = 10_000
),
ok = emqx_ocsp_cache:register_listener(ListenerID, Conf),
?assertMatch({ok, [_, _]}, snabbkaffe:receive_events(SubRef)),
assert_http_get(2),
ok.
t_sni_fun_success(_Config) ->
ListenerID = <<"ssl:test_ocsp">>,
ServerName = "localhost",
?assertEqual(
[
{certificate_status, #certificate_status{
status_type = ?CERTIFICATE_STATUS_TYPE_OCSP,
response = <<"ocsp response">>
}}
],
emqx_ocsp_cache:sni_fun(ServerName, ListenerID)
),
ok.
t_sni_fun_http_error(_Config) ->
meck:expect(
emqx_ocsp_cache,
http_get,
fun(_URL, _HTTPTimeout) ->
{error, timeout}
end
),
ListenerID = <<"ssl:test_ocsp">>,
ServerName = "localhost",
?assertEqual(
[],
emqx_ocsp_cache:sni_fun(ServerName, ListenerID)
),
ok.
%% check that we can start with a non-ocsp stapling listener and
%% restart it with the new ocsp config.
t_update_listener(Config) ->
case proplists:get_bool(skip_does_not_apply, Config) of
true ->
ok;
false ->
do_t_update_listener(Config)
end.
do_t_update_listener(Config) ->
DataDir = ?config(data_dir, Config),
Keyfile = filename:join([DataDir, "server.key"]),
Certfile = filename:join([DataDir, "server.pem"]),
Cacertfile = filename:join([DataDir, "ca.pem"]),
IssuerPemPath = filename:join([DataDir, "ocsp-issuer.pem"]),
{ok, IssuerPem} = file:read_file(IssuerPemPath),
%% no ocsp at first
ListenerId = "ssl:default",
{ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId),
?assertMatch(
#{
<<"ssl_options">> :=
#{
<<"ocsp">> :=
#{<<"enable_ocsp_stapling">> := false}
}
},
ListenerData0
),
assert_no_http_get(),
%% configure ocsp
OCSPConfig =
#{
<<"ssl_options">> =>
#{
<<"keyfile">> => Keyfile,
<<"certfile">> => Certfile,
<<"cacertfile">> => Cacertfile,
<<"ocsp">> =>
#{
<<"enable_ocsp_stapling">> => true,
%% we use the file contents to check that
%% the API converts that to an internally
%% managed file
<<"issuer_pem">> => IssuerPem,
<<"responder_url">> => <<"http://localhost:9877">>
}
}
},
ListenerData1 = emqx_map_lib:deep_merge(ListenerData0, OCSPConfig),
{ok, {_, _, ListenerData2}} = update_listener_via_api(ListenerId, ListenerData1),
?assertMatch(
#{
<<"ssl_options">> :=
#{
<<"ocsp">> :=
#{
<<"enable_ocsp_stapling">> := true,
<<"issuer_pem">> := _,
<<"responder_url">> := _
}
}
},
ListenerData2
),
%% issuer pem should have been uploaded and saved to a new
%% location
?assertNotEqual(
IssuerPemPath,
emqx_map_lib:deep_get(
[<<"ssl_options">>, <<"ocsp">>, <<"issuer_pem">>],
ListenerData2
)
),
?assertNotEqual(
IssuerPem,
emqx_map_lib:deep_get(
[<<"ssl_options">>, <<"ocsp">>, <<"issuer_pem">>],
ListenerData2
)
),
assert_http_get(1, 5_000),
ok.
t_ocsp_responder_error_responses(_Config) ->
ListenerId = <<"ssl:test_ocsp">>,
Conf = emqx_config:get_listener_conf(ssl, test_ocsp, []),
?check_trace(
begin
%% successful response without headers
put_http_response({ok, {200, <<"ocsp_response">>}}),
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerId, Conf),
#{?snk_kind := ocsp_http_fetch_and_cache, headers := false},
1_000
),
%% error response with headers
put_http_response({ok, {{"HTTP/1.0", 500, "Internal Server Error"}, [], <<"error">>}}),
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerId, Conf),
#{?snk_kind := ocsp_http_fetch_bad_code, code := 500, headers := true},
1_000
),
%% error response without headers
put_http_response({ok, {500, <<"error">>}}),
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerId, Conf),
#{?snk_kind := ocsp_http_fetch_bad_code, code := 500, headers := false},
1_000
),
%% econnrefused
put_http_response(
{error,
{failed_connect, [
{to_address, {"localhost", 9877}},
{inet, [inet], econnrefused}
]}}
),
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerId, Conf),
#{?snk_kind := ocsp_http_fetch_error, error := {failed_connect, _}},
1_000
),
%% timeout
put_http_response({error, timeout}),
{ok, {ok, _}} =
?wait_async_action(
emqx_ocsp_cache:register_listener(ListenerId, Conf),
#{?snk_kind := ocsp_http_fetch_error, error := timeout},
1_000
),
ok
end,
[]
),
ok.
t_unknown_requests(_Config) ->
emqx_ocsp_cache ! unknown,
?assertEqual(ok, gen_server:cast(emqx_ocsp_cache, unknown)),
?assertEqual({error, {unknown_call, unknown}}, gen_server:call(emqx_ocsp_cache, unknown)),
ok.
t_validations(Config) ->
case proplists:get_bool(skip_does_not_apply, Config) of
true ->
ok;
false ->
do_t_validations(Config)
end.
do_t_validations(_Config) ->
ListenerId = <<"ssl:default">>,
{ok, {{_, 200, _}, _, ListenerData0}} = get_listener_via_api(ListenerId),
ListenerData1 =
emqx_map_lib:deep_merge(
ListenerData0,
#{
<<"ssl_options">> =>
#{<<"ocsp">> => #{<<"enable_ocsp_stapling">> => true}}
}
),
{error, {_, _, ResRaw1}} = update_listener_via_api(ListenerId, ListenerData1),
#{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw1} =
emqx_json:decode(ResRaw1, [return_maps]),
?assertMatch(
#{
<<"mismatches">> :=
#{
<<"listeners:ssl_not_required_bind">> :=
#{
<<"reason">> :=
<<"The responder URL is required for OCSP stapling">>
}
}
},
emqx_json:decode(MsgRaw1, [return_maps])
),
ListenerData2 =
emqx_map_lib:deep_merge(
ListenerData0,
#{
<<"ssl_options">> =>
#{
<<"ocsp">> => #{
<<"enable_ocsp_stapling">> => true,
<<"responder_url">> => <<"http://localhost:9877">>
}
}
}
),
{error, {_, _, ResRaw2}} = update_listener_via_api(ListenerId, ListenerData2),
#{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw2} =
emqx_json:decode(ResRaw2, [return_maps]),
?assertMatch(
#{
<<"mismatches">> :=
#{
<<"listeners:ssl_not_required_bind">> :=
#{
<<"reason">> :=
<<"The issuer PEM path is required for OCSP stapling">>
}
}
},
emqx_json:decode(MsgRaw2, [return_maps])
),
ListenerData3a =
emqx_map_lib:deep_merge(
ListenerData0,
#{
<<"ssl_options">> =>
#{
<<"ocsp">> => #{
<<"enable_ocsp_stapling">> => true,
<<"responder_url">> => <<"http://localhost:9877">>,
<<"issuer_pem">> => <<"some_file">>
}
}
}
),
ListenerData3 = emqx_map_lib:deep_remove([<<"ssl_options">>, <<"certfile">>], ListenerData3a),
{error, {_, _, ResRaw3}} = update_listener_via_api(ListenerId, ListenerData3),
#{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw3} =
emqx_json:decode(ResRaw3, [return_maps]),
?assertMatch(
#{
<<"mismatches">> :=
#{
<<"listeners:ssl_not_required_bind">> :=
#{
<<"reason">> :=
<<"Server certificate must be defined when using OCSP stapling">>
}
}
},
emqx_json:decode(MsgRaw3, [return_maps])
),
ok.
t_unknown_error_fetching_ocsp_response(_Config) ->
ListenerID = <<"ssl:test_ocsp">>,
TestPid = self(),
ok = meck:expect(
emqx_ocsp_cache,
http_get,
fun(_RequestURI, _HTTPTimeout) ->
TestPid ! error_raised,
meck:exception(error, something_went_wrong)
end
),
?assertEqual(error, emqx_ocsp_cache:fetch_response(ListenerID)),
receive
error_raised -> ok
after 200 -> ct:fail("should have tried to fetch ocsp response")
end,
ok.
t_openssl_client(Config) ->
TLSVsn = ?config(tls_vsn, Config),
WithStatusRequest = ?config(status_request, Config),
%% ensure ocsp response is already cached.
ListenerID = <<"ssl:default">>,
?assertMatch(
{ok, _},
emqx_ocsp_cache:fetch_response(ListenerID),
#{msgs => process_info(self(), messages)}
),
timer:sleep(500),
test_ocsp_connection(TLSVsn, WithStatusRequest, Config).

View File

@ -0,0 +1,68 @@
-----BEGIN CERTIFICATE-----
MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK
DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD
QTAeFw0yMzAxMTIxMzA4MTZaFw0zMzAxMDkxMzA4MTZaMGsxCzAJBgNVBAYTAlNF
MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE
CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQG7dMeU/y9HDNHzhydR0bm
wN9UGplqJOJPwqJRaZZcrn9umgJ9SU2il2ceEVxMDwzBWCRKJO5/H9A9k13SqsXM
2c2c9xXfIF1kb820lCm1Uow5hZ/auDjxliNk9kNJDigCRi3QoIs/dVeWzFsgEC2l
gxRqauN2eNFb6/yXY788YALHBsCRV2NFOFXxtPsvLXpD9Q/8EqYsSMuLARRdHVNU
ryaEF5lhShpcuz0TlIuTy2TiuXJUtJ+p7a4Z7friZ6JsrmQWsVQBj44F8TJRHWzW
C7vm9c+dzEX9eqbr5iPL+L4ctMW9Lz6ePcYfIXne6CElusRUf8G+xM1uwovF9bpV
+9IqY7tAu9G1iY9iNtJgNNDKOCcOGKcZCx6Cg1XYOEKReNnUMazvYeqRrrjV5WQ0
vOcD5zcBRNTXCddCLa7U0guXP9mQrfuk4NTH1Bt77JieTJ8cfDXHwtaKf6aGbmZP
wl1Xi/GuXNUP/xeog78RKyFwBmjt2JKwvWzMpfmH4mEkG9moh2alva+aEz6LIJuP
16g6s0Q6c793/OvUtpNcewHw4Vjn39LD9o6VLp854G4n8dVpUWSbWS+sXD1ZE69H
g/sMNMyq+09ufkbewY8xoCm/rQ1pqDZAVMWsstJEaYu7b/eb7R+RGOj1YECCV/Yp
EZPdDotbSNRkIi2d/a1NAgMBAAGjgaQwgaEwHQYDVR0OBBYEFExwhjsVUom6tQ+S
qq6xMUETvnPzMB8GA1UdIwQYMBaAFD90kfU5pc5l48THu0Ayj9SNpHuhMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG
Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAgEAK6NgdWQYtPNKQNBGjsgtgqTRh+k30iqSO6Y3yE1KGABO
EuQdVqkC2qUIbCB0M0qoV0ab50KNLfU6cbshggW4LDpcMpoQpI05fukNh1jm3ZuZ
0xsB7vlmlsv00tpqmfIl/zykPDynHKOmFh/hJP/KetMy4+wDv4/+xP31UdEj5XvG
HvMtuqOS23A+H6WPU7ol7KzKBnU2zz/xekvPbUD3JqV+ynP5bgbIZHAndd0o9T8e
NFX23Us4cTenU2/ZlOq694bRzGaK+n3Ksz995Nbtzv5fbUgqmf7Mcq4iHGRVtV11
MRyBrsXZp2vbF63c4hrf2Zd6SWRoaDKRhP2DMhajpH9zZASSTlfejg/ZRO2s+Clh
YrSTkeMAdnRt6i/q4QRcOTCfsX75RFM5v67njvTXsSaSTnAwaPi78tRtf+WSh0EP
VVPzy++BszBVlJ1VAf7soWZHCjZxZ8ZPqVTy5okoHwWQ09WmYe8GfulDh1oj0wbK
3FjN7bODWHJN+bFf5aQfK+tumYKoPG8RXL6QxpEzjFWjxhIMJHHMKfDWnAV1o1+7
/1/aDzq7MzEYBbrgQR7oE5ZHtyqhCf9LUgw0Kr7/8QWuNAdeDCJzjXRROU0hJczp
dOyfRlLbHmLLmGOnROlx6LsGNQ17zuz6SPi7ei8/ylhykawDOAGkM1+xFakmQhM=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUYjc7hD7/UJ0/VPADfNfp/WpOwRowDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ
U3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENB
MREwDwYDVQQDDAhNeVJvb3RDQTAeFw0yMzAxMTIxMzA4MTRaFw00MzAxMDcxMzA4
MTRaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcM
CVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RD
QTERMA8GA1UEAwwITXlSb290Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCnBwSOYVJw47IoMHMXTVDtOYvUt3rqsurEhFcB4O8xmf2mmwr6m7s8A5Ft
AvAehg1GvnXT3t/KiyU7BK+acTwcErGyZwS2wvdB0lpHWSpOn/u5y+4ZETvQefcj
ZTdDOM9VN5nutpitgNb+1yL8sqSexfVbY7DnYYvFjOVBYoP/SGvM9jVjCad+0WL3
FhuD+L8QAxzCieX3n9UMymlFwINQuEc+TDjuNcEqt+0J5EgS1fwzxb2RCVL0TNv4
9a71hFGCNRj20AeZm99hbdufm7+0AFO7ocV5q43rLrWFUoBzqKPYIjga/cv/UdWZ
c5RLRXw3JDSrCqkf/mOlaEhNPlmWRF9MSus5Da3wuwgGCaVzmrf30rWR5aHHcscG
e+AOgJ4HayvBUQeb6ZlRXc0YlACiLToMKxuyxDyUcDfVEXpUIsDILF8dkiVQxEU3
j9g6qjXiqPVdNiwpqXfBKObj8vNCzORnoHYs8cCgib3RgDVWeqkDmlSwlZE7CvQh
U4Loj4l7813xxzYEKkVaT1JdXPWu42CG/b4Y/+f4V+3rkJkYzUwndX6kZNksIBai
phmtvKt+CTdP1eAbT+C9AWWF3PT31+BIhuT0u9tR8BVSkXdQB8dG4M/AAJcTo640
0mdYYOXT153gEKHJuUBm750ZTy+r6NjNvpw8VrMAakJwHqnIdQIDAQABo2MwYTAd
BgNVHQ4EFgQUP3SR9TmlzmXjxMe7QDKP1I2ke6EwHwYDVR0jBBgwFoAUP3SR9Tml
zmXjxMe7QDKP1I2ke6EwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw
DQYJKoZIhvcNAQELBQADggIBAFMFv4C+I0+xOAb9v6G/IOpfPBZ1ez31EXKJJBra
lulP4nRHQMeb310JS8BIeQ3dl+7+PkSxPABZSwc3jkxdSMvhc+Z4MQtTgos+Qsjs
gH7sTqwWeeQ0lHYxWmkXijrh5OPRZwTKzYQlkcn85BCUXl2KDuNEdiqPbDTao+lc
lA0/UAvC6NCyFKq/jqf4CmW5Kx6yG1v1LaE+IXn7cbIXj+DaehocVXi0wsXqj03Q
DDUHuLHZP+LBsg4e91/0Jy2ekNRTYJifSqr+9ufHl0ZX1pFDZyf396IgZ5CQZ0PJ
nRxZHlCfsxWxmxxdy3FQSE6YwXhdTjjoAa1ApZcKkkt1beJa6/oRLze/ux5x+5q+
4QczufHd6rjoKBi6BM3FgFQ8As5iNohHXlMHd/xITo1Go3CWw2j9TGH5vzksOElK
B0mcwwt2zwNEjvfytc+tI5jcfGN3tiT5fVHS8hw9dWKevypLL+55Ua9G8ZgDHasT
XFRJHgmnbyFcaAe26D2dSKmhC9u2mHBH+MaI8dj3e7wNBfpxNgp41aFIk+QTmiFW
VXFED6DHQ/Mxq93ACalHdYg18PlIYClbT6Pf2xXBnn33YPhn5xzoTZ+cDH/RpaQp
s0UUTSJT1UTXgtXPnZWQfvKlMjJEIiVFiLEC0sgZRlWuZDRAY0CdZJJxvQp59lqu
cbTm
-----END CERTIFICATE-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
rkE8Bz5a/iwO+QwdO5Q2cp5+0nm41A==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIIGmjCCBIKgAwIBAgICEAYwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDMwNjE5NTA0N1oXDTMzMDYxMTE5NTA0N1owezELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExFTATBgNVBAMMDG9j
c3AuY2xpZW50MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKZ9mYEA
5knzGM9iSnJDFMZnjS8jA67KXBKjDXaJ1Y3qNxb2DloiKn4Hagqt/NHs9izFsOh1
yP8o6lAbAIUC6NJiBi+KI6yYBNDWZ42hyedaMCzJvD51Jcr9mN+tWAhokXtEdouI
O2rlMjhvgqNQXKHk0gshz7N9mqg1TqSFQXRtw4EAbYEVoHNF/5r+GBlcQ/ISeioU
rknmT4MNKUpcNs5MQeFcbhAVPWFlq1rGm0PWdMQVFi7N+aH+DhEO9a1rCb1hTeP9
XRSfu39NqPR8Bc2LfRPMo5VpVwOuRW0MUd7aFfeU4l3ncjk2lLtqxqr5pM3K3P4j
cF/9ri+SESCsqmMFJdeb/5XDk3Oxc3Ap11JQbLCYhtXZgFicr7QPCE5cuxM+Hem/
r3fU1XGmJ9bSczhSvpcIMc49Vf+MhlnzX1vMCVlncGAxPPumpP33TqWY1FAGBKCv
5lrD2CNWEuDVUN/AXzl5qeFvOTkhOd99H6zZyidmvaD4sQOyrZ0m/+wRti4cE0GQ
O2Gfjf0yA2rWevc9XSGfyk1yBLlkVTD583zjTdFj1NguywH8UVhEQMJrHSPvmovc
Tsttkrwl+w/y6HV7c91HrhOC2hihMq7C0ip6WaH6vIBYWNpcs+g/8CpiTVVxEJ68
5dQrDouKyR26SmWT1zLZw8tyb4Q/7YmzcHbxAgMBAAGjggE2MIIBMjAJBgNVHRME
AjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBH
ZW5lcmF0ZWQgQ2xpZW50IENlcnRpZmljYXRlMB0GA1UdDgQWBBSJ/yia067wCafe
kDCgk+e8PJTCUDAfBgNVHSMEGDAWgBRMcIY7FVKJurUPkqqusTFBE75z8zAOBgNV
HQ8BAf8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMDsGA1Ud
HwQ0MDIwMKAuoCyGKmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUu
Y3JsLnBlbTAxBggrBgEFBQcBAQQlMCMwIQYIKwYBBQUHMAGGFWh0dHA6Ly9sb2Nh
bGhvc3Q6OTg3NzANBgkqhkiG9w0BAQsFAAOCAgEAN2XfYgbrjxC6OWh9UoMLQaDD
59JPxAUBxlRtWzTWqxY2jfT+OwJfDP4e+ef2G1YEG+qyt57ddlm/EwX9IvAvG0D4
wd4tfItG88IJWKDM3wpT5KYrUsu+PlQTFmGmaWlORK/mRKlmfjbP5CIAcUedvCS9
j9PkCrbbkklAmp0ULLSLUkYajmfFOkQ+VdGhQ6nAamTeyh2Z2S4dVjsKc8yBViMo
/V6HP56rOvUqiVTcvhZtH7QDptMSTzuJ+AsmreYjwIiTGzYS/i8QVAFuPfXJKEOB
jD5WhUaP/8Snbuft4MxssPAph8okcmxLfb55nw+soNc2oS1wWwKMe7igRelq8vtg
bu00QSEGiY1eq/vFgZh0+Wohy/YeYzhO4Jq40FFpKiVbkLzexpNH/Afj2QrHuZ7y
259uGGfv5tGA+TW6PsckCQknEb5V4V35ZZlbWVRKpuADeNPoDuoYPtc5eOomIkmw
rFz/gPZWSA+4pYEgXgqcaM8+KP0i53eTbWqwy5DVgXiuaTYWU4m1FTsIZ+/nGIqW
Dsgqd/D6jivf9Yvm+VFYTZsxIfq5sMdjxSuMBo0nZrzFDpqc6m6fVVoHv5R9Yliw
MbxgmFQ84CKLy7iNKGSGVN2SIr1obMQ0e/t3NiCHib3WKzmZFoNoFCtVzAgsxGmF
Q6rY83JdIPPW4LqZNcE=
-----END CERTIFICATE-----

View File

@ -0,0 +1,6 @@
V 330419130816Z 1000 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=localhost
V 330419130816Z 1001 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=MyClient
R 330419130816Z 230112130816Z 1002 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=client-revoked
V 330419130816Z 1003 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=ocsp.server
V 330419130816Z 1004 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=ocsp.client
V 330425123656Z 1005 unknown /C=SE/ST=Stockholm/L=Stockholm/O=MyOrgName/OU=MyIntermediateCA/CN=client-no-dist-points

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
xBO4CmF79pAcAXvDoRB619Flx5/G
-----END PRIVATE KEY-----

View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF+zCCA+OgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQK
DAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJvb3RD
QTAeFw0yMzAxMTIxMzA4MTZaFw0zMzAxMDkxMzA4MTZaMGsxCzAJBgNVBAYTAlNF
MRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcGA1UE
CwwQTXlJbnRlcm1lZGlhdGVDQTEZMBcGA1UEAwwQTXlJbnRlcm1lZGlhdGVDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQG7dMeU/y9HDNHzhydR0bm
wN9UGplqJOJPwqJRaZZcrn9umgJ9SU2il2ceEVxMDwzBWCRKJO5/H9A9k13SqsXM
2c2c9xXfIF1kb820lCm1Uow5hZ/auDjxliNk9kNJDigCRi3QoIs/dVeWzFsgEC2l
gxRqauN2eNFb6/yXY788YALHBsCRV2NFOFXxtPsvLXpD9Q/8EqYsSMuLARRdHVNU
ryaEF5lhShpcuz0TlIuTy2TiuXJUtJ+p7a4Z7friZ6JsrmQWsVQBj44F8TJRHWzW
C7vm9c+dzEX9eqbr5iPL+L4ctMW9Lz6ePcYfIXne6CElusRUf8G+xM1uwovF9bpV
+9IqY7tAu9G1iY9iNtJgNNDKOCcOGKcZCx6Cg1XYOEKReNnUMazvYeqRrrjV5WQ0
vOcD5zcBRNTXCddCLa7U0guXP9mQrfuk4NTH1Bt77JieTJ8cfDXHwtaKf6aGbmZP
wl1Xi/GuXNUP/xeog78RKyFwBmjt2JKwvWzMpfmH4mEkG9moh2alva+aEz6LIJuP
16g6s0Q6c793/OvUtpNcewHw4Vjn39LD9o6VLp854G4n8dVpUWSbWS+sXD1ZE69H
g/sMNMyq+09ufkbewY8xoCm/rQ1pqDZAVMWsstJEaYu7b/eb7R+RGOj1YECCV/Yp
EZPdDotbSNRkIi2d/a1NAgMBAAGjgaQwgaEwHQYDVR0OBBYEFExwhjsVUom6tQ+S
qq6xMUETvnPzMB8GA1UdIwQYMBaAFD90kfU5pc5l48THu0Ayj9SNpHuhMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDsGA1UdHwQ0MDIwMKAuoCyG
Kmh0dHA6Ly9sb2NhbGhvc3Q6OTg3OC9pbnRlcm1lZGlhdGUuY3JsLnBlbTANBgkq
hkiG9w0BAQsFAAOCAgEAK6NgdWQYtPNKQNBGjsgtgqTRh+k30iqSO6Y3yE1KGABO
EuQdVqkC2qUIbCB0M0qoV0ab50KNLfU6cbshggW4LDpcMpoQpI05fukNh1jm3ZuZ
0xsB7vlmlsv00tpqmfIl/zykPDynHKOmFh/hJP/KetMy4+wDv4/+xP31UdEj5XvG
HvMtuqOS23A+H6WPU7ol7KzKBnU2zz/xekvPbUD3JqV+ynP5bgbIZHAndd0o9T8e
NFX23Us4cTenU2/ZlOq694bRzGaK+n3Ksz995Nbtzv5fbUgqmf7Mcq4iHGRVtV11
MRyBrsXZp2vbF63c4hrf2Zd6SWRoaDKRhP2DMhajpH9zZASSTlfejg/ZRO2s+Clh
YrSTkeMAdnRt6i/q4QRcOTCfsX75RFM5v67njvTXsSaSTnAwaPi78tRtf+WSh0EP
VVPzy++BszBVlJ1VAf7soWZHCjZxZ8ZPqVTy5okoHwWQ09WmYe8GfulDh1oj0wbK
3FjN7bODWHJN+bFf5aQfK+tumYKoPG8RXL6QxpEzjFWjxhIMJHHMKfDWnAV1o1+7
/1/aDzq7MzEYBbrgQR7oE5ZHtyqhCf9LUgw0Kr7/8QWuNAdeDCJzjXRROU0hJczp
dOyfRlLbHmLLmGOnROlx6LsGNQ17zuz6SPi7ei8/ylhykawDOAGkM1+xFakmQhM=
-----END CERTIFICATE-----

View File

@ -0,0 +1,14 @@
listeners.ssl.default {
bind = "0.0.0.0:8883"
max_connections = 512000
ssl_options {
keyfile = "{{ test_data_dir }}/server.key"
certfile = "{{ test_data_dir }}/server.pem"
cacertfile = "{{ test_data_dir }}/ca.pem"
ocsp {
enable_ocsp_stapling = true
issuer_pem = "{{ test_data_dir }}/ocsp-issuer.pem"
responder_url = "http://127.0.0.1:9877"
}
}
}

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCnVPRWgP59GU15
HddFwPZflFfcSkeuWU8tgKQhZcNoBli4lIfemuoV/hkGRVFexAiAw3/u5wvOaMaN
V8n9KxxgAUNLh5YaknpnNdhfQDyM0S5UJIbVeLzAQWxkBXpI3uBfW4WPSULRnVyR
psLEfl1qOklGOyuZfRbkkkkVwtJEmGEH0kz0fy6xenn3R3/mTeIbj+5TNqiBXWn1
/qgTiNf2Ni7SE6Nk2lP4V8iofcBIrsp6KtEWdipGEJZeXCg/X0g/qVt15tF1l00M
uEWRHt1qGBELJJTcNzQvdqHAPz0AfQRjTtXyocw5+pFth8Q8a7gyjrjv5nhnpAKQ
msrt3vyNAgMBAAECggEABnWvIQ/Fw0qQxRYz00uJt1LguW5cqgxklBsdOvTUwFVO
Y4HIZP2R/9tZV/ahF4l10pK5g52DxSoiUB6Ne6qIY+RolqfbUZdKBmX7vmGadM02
fqUSV3dbwghEiO/1Mo74FnZQB6IKZFEw26aWakN+k7VAUufB3SEJGzXSgHaO63ru
dFGSiYI8U+q+YnhUJjCnmI12fycNfy451TdUQtGZb6pNmm5HRUF6hpAV8Le9LojP
Ql9eacPpsrzU15X5ElCQZ/f9iNh1bplcISuhrULgKUKOvAVrBlEK67uRVy6g98xA
c/rgNLkbL/jZEsAc3/vHAyFgd3lABfwpBGLHej3QgQKBgQDFNYmfBNQr89HC5Zc+
M6jXcAT/R+0GNczBTfC4iyNemwqsumSSRelNZ748UefKuS3F6Mvb2CBqE2LbB61G
hrnCffG2pARjZ491SefRwghhWWVGLP1p8KliLgOGBehA1REgJb+XULncjuHZuh4O
LVn3HVnWGxeBGg+yKa6Z4YQi3QKBgQDZN0O8ZcZY74lRJ0UjscD9mJ1yHlsssZag
njkX/f0GR/iVpfaIxQNC3gvWUy2LsU0He9sidcB0cfej0j/qZObQyFsCB0+utOgy
+hX7gokV2pes27WICbNWE2lJL4QZRJgvf82OaEy57kfDrm+eK1XaSZTZ10P82C9u
gAmMnontcQKBgGu29lhY9tqa7jOZ26Yp6Uri8JfO3XPK5u+edqEVvlfqL0Zw+IW8
kdWpmIqx4f0kcA/tO4v03J+TvycLZmVjKQtGZ0PvCkaRRhY2K9yyMomZnmtaH4BB
5wKtR1do2pauyg/ZDnDDswD5OfsGYWw08TK8YVlEqu3lIjWZ9rguKVIxAoGAZYUk
zVqr10ks3pcCA2rCjkPT4lA5wKvHgI4ylPoKVfMxRY/pp4acvZXV5ne9o7pcDBFh
G7v5FPNnEFPlt4EtN4tMragJH9hBZgHoYEJkG6islweg0lHmVWaBIMlqbfzXO+v5
gINSyNuLAvP2CvCqEXmubhnkFrpbgMOqsuQuBqECgYB3ss2PDhBF+5qoWgqymFof
1ovRPuQ9sPjWBn5IrCdoYITDnbBzBZERx7GLs6A/PUlWgST7jkb1PY/TxYSUfXzJ
SNd47q0mCQ+IUdqUbHgpK9b1ncwLMsnexpYZdHJWRLgnUhOx7OMjJc/4iLCAFCoN
3KJ7/V1keo7GBHOwnsFcCA==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCTCCA/GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux
EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL
DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X
DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNloweDELMAkGA1UEBhMCU0UxEjAQ
BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN
eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExEjAQBgNVBAMMCWxv
Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdU9FaA/n0Z
TXkd10XA9l+UV9xKR65ZTy2ApCFlw2gGWLiUh96a6hX+GQZFUV7ECIDDf+7nC85o
xo1Xyf0rHGABQ0uHlhqSemc12F9APIzRLlQkhtV4vMBBbGQFekje4F9bhY9JQtGd
XJGmwsR+XWo6SUY7K5l9FuSSSRXC0kSYYQfSTPR/LrF6efdHf+ZN4huP7lM2qIFd
afX+qBOI1/Y2LtITo2TaU/hXyKh9wEiuynoq0RZ2KkYQll5cKD9fSD+pW3Xm0XWX
TQy4RZEe3WoYEQsklNw3NC92ocA/PQB9BGNO1fKhzDn6kW2HxDxruDKOuO/meGek
ApCayu3e/I0CAwEAAaOCAagwggGkMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQD
AgZAMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NMIEdlbmVyYXRlZCBTZXJ2ZXIgQ2Vy
dGlmaWNhdGUwHQYDVR0OBBYEFGy5LQPzIelruJl7mL0mtUXM57XhMIGaBgNVHSME
gZIwgY+AFExwhjsVUom6tQ+Sqq6xMUETvnPzoXOkcTBvMQswCQYDVQQGEwJTRTES
MBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xEjAQBgNVBAoM
CU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAPBgNVBAMMCE15Um9vdENB
ggIQADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwOwYDVR0f
BDQwMjAwoC6gLIYqaHR0cDovL2xvY2FsaG9zdDo5ODc4L2ludGVybWVkaWF0ZS5j
cmwucGVtMDEGCCsGAQUFBwEBBCUwIzAhBggrBgEFBQcwAYYVaHR0cDovL2xvY2Fs
aG9zdDo5ODc3MA0GCSqGSIb3DQEBCwUAA4ICAQCX3EQgiCVqLhnCNd0pmptxXPxo
l1KyZkpdrFa/NgSqRhkuZSAkszwBDDS/gzkHFKEUhmqs6/UZwN4+Rr3LzrHonBiN
aQ6GeNNXZ/3xAQfUCwjjGmz9Sgw6kaX19Gnk2CjI6xP7T+O5UmsMI9hHUepC9nWa
XX2a0hsO/KOVu5ZZckI16Ek/jxs2/HEN0epYdvjKFAaVmzZZ5PATNjrPQXvPmq2r
x++La+3bXZsrH8P2FhPpM5t/IxKKW/Tlpgz92c2jVSIHF5khSA/MFDC+dk80OFmm
v4ZTPIMuZ//Q+wo0f9P48rsL9D27qS7CA+8pn9wu+cfnBDSt7JD5Yipa1gHz71fy
YTa9qRxIAPpzW2v7TFZE8eSKFUY9ipCeM2BbdmCQGmq4+v36b5TZoyjH4k0UVWGo
Gclos2cic5Vxi8E6hb7b7yZpjEfn/5lbCiGMfAnI6aoOyrWg6keaRA33kaLUEZiK
OgFNbPkjiTV0ZQyLXf7uK9YFhpVzJ0dv0CFNse8rZb7A7PLn8VrV/ZFnJ9rPoawn
t7ZGxC0d5BRSEyEeEgsQdxuY4m8OkE18zwhCkt2Qs3uosOWlIrYmqSEa0i/sPSQP
jiwB4nEdBrf8ZygzuYjT5T9YRSwhVox4spS/Av8Ells5JnkuKAhCVv9gHxYwbj0c
CzyLJgE1z9Tq63m+gQ==
-----END CERTIFICATE-----

View File

@ -473,3 +473,43 @@ password_converter_test() ->
?assertEqual(<<"123">>, emqx_schema:password_converter(<<"123">>, #{})), ?assertEqual(<<"123">>, emqx_schema:password_converter(<<"123">>, #{})),
?assertThrow("must_quote", emqx_schema:password_converter(foobar, #{})), ?assertThrow("must_quote", emqx_schema:password_converter(foobar, #{})),
ok. ok.
url_type_test_() ->
[
?_assertEqual(
{ok, <<"http://some.server/">>},
typerefl:from_string(emqx_schema:url(), <<"http://some.server/">>)
),
?_assertEqual(
{ok, <<"http://192.168.0.1/">>},
typerefl:from_string(emqx_schema:url(), <<"http://192.168.0.1">>)
),
?_assertEqual(
{ok, <<"http://some.server/">>},
typerefl:from_string(emqx_schema:url(), "http://some.server/")
),
?_assertEqual(
{ok, <<"http://some.server/">>},
typerefl:from_string(emqx_schema:url(), <<"http://some.server">>)
),
?_assertEqual(
{ok, <<"http://some.server:9090/">>},
typerefl:from_string(emqx_schema:url(), <<"http://some.server:9090">>)
),
?_assertEqual(
{ok, <<"https://some.server:9090/">>},
typerefl:from_string(emqx_schema:url(), <<"https://some.server:9090">>)
),
?_assertEqual(
{ok, <<"https://some.server:9090/path?q=uery">>},
typerefl:from_string(emqx_schema:url(), <<"https://some.server:9090/path?q=uery">>)
),
?_assertEqual(
{error, {unsupported_scheme, <<"postgres">>}},
typerefl:from_string(emqx_schema:url(), <<"postgres://some.server:9090">>)
),
?_assertEqual(
{error, empty_host_not_allowed},
typerefl:from_string(emqx_schema:url(), <<"">>)
)
].

View File

@ -117,7 +117,7 @@ ssl_files_failure_test_() ->
%% empty string %% empty string
?assertMatch( ?assertMatch(
{error, #{ {error, #{
reason := invalid_file_path_or_pem_string, which_options := [<<"keyfile">>] reason := invalid_file_path_or_pem_string, which_options := [[<<"keyfile">>]]
}}, }},
emqx_tls_lib:ensure_ssl_files("/tmp", #{ emqx_tls_lib:ensure_ssl_files("/tmp", #{
<<"keyfile">> => <<>>, <<"keyfile">> => <<>>,
@ -128,7 +128,7 @@ ssl_files_failure_test_() ->
%% not valid unicode %% not valid unicode
?assertMatch( ?assertMatch(
{error, #{ {error, #{
reason := invalid_file_path_or_pem_string, which_options := [<<"keyfile">>] reason := invalid_file_path_or_pem_string, which_options := [[<<"keyfile">>]]
}}, }},
emqx_tls_lib:ensure_ssl_files("/tmp", #{ emqx_tls_lib:ensure_ssl_files("/tmp", #{
<<"keyfile">> => <<255, 255>>, <<"keyfile">> => <<255, 255>>,
@ -136,6 +136,18 @@ ssl_files_failure_test_() ->
<<"cacertfile">> => bin(test_key()) <<"cacertfile">> => bin(test_key())
}) })
), ),
?assertMatch(
{error, #{
reason := invalid_file_path_or_pem_string,
which_options := [[<<"ocsp">>, <<"issuer_pem">>]]
}},
emqx_tls_lib:ensure_ssl_files("/tmp", #{
<<"keyfile">> => bin(test_key()),
<<"certfile">> => bin(test_key()),
<<"cacertfile">> => bin(test_key()),
<<"ocsp">> => #{<<"issuer_pem">> => <<255, 255>>}
})
),
%% not printable %% not printable
?assertMatch( ?assertMatch(
{error, #{reason := invalid_file_path_or_pem_string}}, {error, #{reason := invalid_file_path_or_pem_string}},
@ -155,7 +167,8 @@ ssl_files_failure_test_() ->
#{ #{
<<"cacertfile">> => bin(TmpFile), <<"cacertfile">> => bin(TmpFile),
<<"keyfile">> => bin(TmpFile), <<"keyfile">> => bin(TmpFile),
<<"certfile">> => bin(TmpFile) <<"certfile">> => bin(TmpFile),
<<"ocsp">> => #{<<"issuer_pem">> => bin(TmpFile)}
} }
) )
) )
@ -170,22 +183,29 @@ ssl_files_save_delete_test() ->
SSL0 = #{ SSL0 = #{
<<"keyfile">> => Key, <<"keyfile">> => Key,
<<"certfile">> => Key, <<"certfile">> => Key,
<<"cacertfile">> => Key <<"cacertfile">> => Key,
<<"ocsp">> => #{<<"issuer_pem">> => Key}
}, },
Dir = filename:join(["/tmp", "ssl-test-dir"]), Dir = filename:join(["/tmp", "ssl-test-dir"]),
{ok, SSL} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), {ok, SSL} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0),
File = maps:get(<<"keyfile">>, SSL), FileKey = maps:get(<<"keyfile">>, SSL),
?assertMatch(<<"/tmp/ssl-test-dir/key-", _:16/binary>>, File), ?assertMatch(<<"/tmp/ssl-test-dir/key-", _:16/binary>>, FileKey),
?assertEqual({ok, bin(test_key())}, file:read_file(File)), ?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)),
FileIssuerPem = emqx_map_lib:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL),
?assertMatch(<<"/tmp/ssl-test-dir/ocsp_issuer_pem-", _:16/binary>>, FileIssuerPem),
?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)),
%% no old file to delete %% no old file to delete
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, undefined), ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, undefined),
?assertEqual({ok, bin(test_key())}, file:read_file(File)), ?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)),
?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)),
%% old and new identical, no delete %% old and new identical, no delete
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, SSL), ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, SSL),
?assertEqual({ok, bin(test_key())}, file:read_file(File)), ?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)),
?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)),
%% new is gone, delete old %% new is gone, delete old
ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL), ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL),
?assertEqual({error, enoent}, file:read_file(File)), ?assertEqual({error, enoent}, file:read_file(FileKey)),
?assertEqual({error, enoent}, file:read_file(FileIssuerPem)),
%% test idempotence %% test idempotence
ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL), ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL),
ok. ok.
@ -198,7 +218,8 @@ ssl_files_handle_non_generated_file_test() ->
SSL0 = #{ SSL0 = #{
<<"keyfile">> => TmpKeyFile, <<"keyfile">> => TmpKeyFile,
<<"certfile">> => TmpKeyFile, <<"certfile">> => TmpKeyFile,
<<"cacertfile">> => TmpKeyFile <<"cacertfile">> => TmpKeyFile,
<<"ocsp">> => #{<<"issuer_pem">> => TmpKeyFile}
}, },
Dir = filename:join(["/tmp", "ssl-test-dir-00"]), Dir = filename:join(["/tmp", "ssl-test-dir-00"]),
{ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), {ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0),
@ -216,24 +237,32 @@ ssl_file_replace_test() ->
SSL0 = #{ SSL0 = #{
<<"keyfile">> => Key1, <<"keyfile">> => Key1,
<<"certfile">> => Key1, <<"certfile">> => Key1,
<<"cacertfile">> => Key1 <<"cacertfile">> => Key1,
<<"ocsp">> => #{<<"issuer_pem">> => Key1}
}, },
SSL1 = #{ SSL1 = #{
<<"keyfile">> => Key2, <<"keyfile">> => Key2,
<<"certfile">> => Key2, <<"certfile">> => Key2,
<<"cacertfile">> => Key2 <<"cacertfile">> => Key2,
<<"ocsp">> => #{<<"issuer_pem">> => Key2}
}, },
Dir = filename:join(["/tmp", "ssl-test-dir2"]), Dir = filename:join(["/tmp", "ssl-test-dir2"]),
{ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), {ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0),
{ok, SSL3} = emqx_tls_lib:ensure_ssl_files(Dir, SSL1), {ok, SSL3} = emqx_tls_lib:ensure_ssl_files(Dir, SSL1),
File1 = maps:get(<<"keyfile">>, SSL2), File1 = maps:get(<<"keyfile">>, SSL2),
File2 = maps:get(<<"keyfile">>, SSL3), File2 = maps:get(<<"keyfile">>, SSL3),
IssuerPem1 = emqx_map_lib:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL2),
IssuerPem2 = emqx_map_lib:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL3),
?assert(filelib:is_regular(File1)), ?assert(filelib:is_regular(File1)),
?assert(filelib:is_regular(File2)), ?assert(filelib:is_regular(File2)),
?assert(filelib:is_regular(IssuerPem1)),
?assert(filelib:is_regular(IssuerPem2)),
%% delete old file (File1, in SSL2) %% delete old file (File1, in SSL2)
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL3, SSL2), ok = emqx_tls_lib:delete_ssl_files(Dir, SSL3, SSL2),
?assertNot(filelib:is_regular(File1)), ?assertNot(filelib:is_regular(File1)),
?assert(filelib:is_regular(File2)), ?assert(filelib:is_regular(File2)),
?assertNot(filelib:is_regular(IssuerPem1)),
?assert(filelib:is_regular(IssuerPem2)),
ok. ok.
bin(X) -> iolist_to_binary(X). bin(X) -> iolist_to_binary(X).

View File

@ -55,7 +55,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),

View File

@ -52,7 +52,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = stop_apps([emqx_resource, cowboy]), ok = stop_apps([emqx_resource, cowboy]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers(); ok = emqx_authz_test_lib:reset_authorizers();

View File

@ -36,7 +36,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),

View File

@ -50,7 +50,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers(); ok = emqx_authz_test_lib:reset_authorizers();

View File

@ -57,7 +57,7 @@ end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?MYSQL_RESOURCE), ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),

View File

@ -57,7 +57,7 @@ end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?PGSQL_RESOURCE), ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),

View File

@ -58,7 +58,7 @@ end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?REDIS_RESOURCE), ok = emqx_resource:remove_local(?REDIS_RESOURCE),
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),

View File

@ -42,6 +42,8 @@ init_per_suite(_Config) ->
[]. [].
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_config:put([bridges], #{}),
ok = emqx_config:put_raw([bridges], #{}),
ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_bridge]), ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_bridge]),
ok = emqx_connector_test_helpers:stop_apps([emqx_resource]), ok = emqx_connector_test_helpers:stop_apps([emqx_resource]),
_ = application:stop(emqx_connector), _ = application:stop(emqx_connector),

View File

@ -163,7 +163,7 @@ diff_listeners(Type, Stop, Start) -> {#{Type => Stop}, #{Type => Start}}.
ensure_ssl_cert(#{<<"listeners">> := #{<<"https">> := #{<<"enable">> := true}}} = Conf) -> ensure_ssl_cert(#{<<"listeners">> := #{<<"https">> := #{<<"enable">> := true}}} = Conf) ->
Https = emqx_map_lib:deep_get([<<"listeners">>, <<"https">>], Conf, undefined), Https = emqx_map_lib:deep_get([<<"listeners">>, <<"https">>], Conf, undefined),
Opts = #{required_keys => [<<"keyfile">>, <<"certfile">>, <<"cacertfile">>]}, Opts = #{required_keys => [[<<"keyfile">>], [<<"certfile">>], [<<"cacertfile">>]]},
case emqx_tls_lib:ensure_ssl_files(?DIR, Https, Opts) of case emqx_tls_lib:ensure_ssl_files(?DIR, Https, Opts) of
{ok, undefined} -> {ok, undefined} ->
{error, <<"ssl_cert_not_found">>}; {error, <<"ssl_cert_not_found">>};

View File

@ -24,13 +24,13 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx_hooks.hrl").
-include_lib("emqx_conf/include/emqx_conf.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(DEFAULT_CLUSTER_NAME_ATOM, emqxcl). -define(DEFAULT_CLUSTER_NAME_ATOM, emqxcl).
-define(OTHER_CLUSTER_NAME_ATOM, test_emqx_cluster). -define(OTHER_CLUSTER_NAME_ATOM, test_emqx_cluster).
-define(OTHER_CLUSTER_NAME_STRING, "test_emqx_cluster"). -define(OTHER_CLUSTER_NAME_STRING, "test_emqx_cluster").
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
-define(CONF_DEFAULT, << -define(CONF_DEFAULT, <<
"\n" "\n"
@ -54,6 +54,8 @@
"}\n" "}\n"
>>). >>).
-import(emqx_common_test_helpers, [on_exit/1]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Setups %% Setups
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -89,7 +91,7 @@ init_per_testcase(_, Config) ->
timer:sleep(200), timer:sleep(200),
Config. Config.
end_per_testcase(_, Config) -> end_per_testcase(_, _Config) ->
case erlang:whereis(node()) of case erlang:whereis(node()) of
undefined -> undefined ->
ok; ok;
@ -97,7 +99,8 @@ end_per_testcase(_, Config) ->
erlang:unlink(P), erlang:unlink(P),
erlang:exit(P, kill) erlang:exit(P, kill)
end, end,
Config. emqx_common_test_helpers:call_janitor(),
ok.
load_cfg(Cfg) -> load_cfg(Cfg) ->
ok = emqx_common_test_helpers:load_config(emqx_exhook_schema, Cfg). ok = emqx_common_test_helpers:load_config(emqx_exhook_schema, Cfg).
@ -300,6 +303,12 @@ t_cluster_name(_) ->
emqx_common_test_helpers:stop_apps([emqx, emqx_exhook]), emqx_common_test_helpers:stop_apps([emqx, emqx_exhook]),
emqx_common_test_helpers:start_apps([emqx, emqx_exhook], SetEnvFun), emqx_common_test_helpers:start_apps([emqx, emqx_exhook], SetEnvFun),
on_exit(fun() ->
emqx_common_test_helpers:stop_apps([emqx, emqx_exhook]),
load_cfg(?CONF_DEFAULT),
emqx_common_test_helpers:start_apps([emqx_exhook]),
mria:wait_for_tables([?CLUSTER_MFA, ?CLUSTER_COMMIT])
end),
?assertEqual(?OTHER_CLUSTER_NAME_STRING, emqx_sys:cluster_name()), ?assertEqual(?OTHER_CLUSTER_NAME_STRING, emqx_sys:cluster_name()),

View File

@ -77,7 +77,7 @@ init_per_suite(Config) ->
end_per_suite(Config) -> end_per_suite(Config) ->
emqx_gateway_auth_ct:stop(), emqx_gateway_auth_ct:stop(),
emqx_config:erase(gateway), emqx_config:erase(gateway),
emqx_mgmt_api_test_util:end_suite([cowboy, emqx_authn, emqx_gateway]), emqx_mgmt_api_test_util:end_suite([cowboy, emqx_conf, emqx_authn, emqx_gateway]),
Config. Config.
init_per_testcase(_Case, Config) -> init_per_testcase(_Case, Config) ->

View File

@ -24,12 +24,15 @@ init_suite() ->
init_suite([]). init_suite([]).
init_suite(Apps) -> init_suite(Apps) ->
init_suite(Apps, fun set_special_configs/1). init_suite(Apps, fun set_special_configs/1, #{}).
init_suite(Apps, SetConfigs) -> init_suite(Apps, SetConfigs) when is_function(SetConfigs) ->
init_suite(Apps, SetConfigs, #{}).
init_suite(Apps, SetConfigs, Opts) ->
mria:start(), mria:start(),
application:load(emqx_management), application:load(emqx_management),
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], SetConfigs), emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], SetConfigs, Opts),
emqx_common_test_http:create_default_app(). emqx_common_test_http:create_default_app().
end_suite() -> end_suite() ->

View File

@ -295,8 +295,15 @@ t_batch_query_counter(_) ->
ok ok
end, end,
fun(Trace) -> fun(Trace) ->
QueryTrace = ?of_kind(call_batch_query, Trace), QueryTrace = [
?assertMatch([#{batch := BatchReq} | _] when length(BatchReq) > 1, QueryTrace) Event
|| Event = #{
?snk_kind := call_batch_query,
batch := BatchReq
} <- Trace,
length(BatchReq) > 1
],
?assertMatch([_ | _], QueryTrace)
end end
), ),
{ok, NMsgs} = emqx_resource:query(?ID, get_counter), {ok, NMsgs} = emqx_resource:query(?ID, get_counter),
@ -648,19 +655,18 @@ t_query_counter_async_inflight_batch(_) ->
5_000 5_000
), ),
fun(Trace) -> fun(Trace) ->
QueryTrace = ?of_kind(call_batch_query_async, Trace), QueryTrace = [
?assertMatch( Event
[ || Event = #{
#{ ?snk_kind := call_batch_query_async,
batch := [ batch := [
{query, _, {inc_counter, 1}, _, _}, {query, _, {inc_counter, 1}, _, _},
{query, _, {inc_counter, 1}, _, _} {query, _, {inc_counter, 1}, _, _}
] ]
} } <-
| _ Trace
], ],
QueryTrace ?assertMatch([_ | _], QueryTrace)
)
end end
), ),
tap_metrics(?LINE), tap_metrics(?LINE),
@ -1275,10 +1281,11 @@ t_retry_batch(_Config) ->
%% each time should be the original batch (no duplicate %% each time should be the original batch (no duplicate
%% elements or reordering). %% elements or reordering).
ExpectedSeenPayloads = lists:flatten(lists:duplicate(4, Payloads)), ExpectedSeenPayloads = lists:flatten(lists:duplicate(4, Payloads)),
?assertEqual( Trace1 = lists:sublist(
ExpectedSeenPayloads, ?projection(n, ?of_kind(connector_demo_batch_inc_individual, Trace)),
?projection(n, ?of_kind(connector_demo_batch_inc_individual, Trace)) length(ExpectedSeenPayloads)
), ),
?assertEqual(ExpectedSeenPayloads, Trace1),
?assertMatch( ?assertMatch(
[#{n := ExpectedCount}], [#{n := ExpectedCount}],
?of_kind(connector_demo_inc_counter, Trace) ?of_kind(connector_demo_inc_counter, Trace)

View File

@ -27,7 +27,7 @@
{profiles, [ {profiles, [
{test, [ {test, [
{deps, [ {deps, [
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}} {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.5"}}}
]} ]}
]} ]}
]}. ]}.

View File

@ -0,0 +1 @@
Add support for OCSP stapling and CRL check for SSL MQTT listeners.

View File

@ -0,0 +1 @@
为 SSL MQTT 监听器增加对 OCSP Stapling 的支持。

View File

@ -61,7 +61,7 @@ defmodule EMQXUmbrella.MixProject do
{:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true},
{:replayq, github: "emqx/replayq", tag: "0.3.7", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.7", override: true},
{:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
{:emqtt, github: "emqx/emqtt", tag: "1.8.2", override: true}, {:emqtt, github: "emqx/emqtt", tag: "1.8.5", override: true},
{:rulesql, github: "emqx/rulesql", tag: "0.1.4"}, {:rulesql, github: "emqx/rulesql", tag: "0.1.4"},
{:observer_cli, "1.7.1"}, {:observer_cli, "1.7.1"},
{:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.3"}, {:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.3"},

View File

@ -63,7 +63,7 @@
, {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}}
, {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}}
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
, {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.2"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.8.5"}}}
, {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.4"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.4"}}}
, {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}}

View File

@ -12,6 +12,7 @@ CMD
CN CN
CONNACK CONNACK
CoAP CoAP
CRLs
Cygwin Cygwin
DES DES
DN DN
@ -41,6 +42,7 @@ Makefile
MitM MitM
Multicast Multicast
NIF NIF
OCSP
OTP OTP
PEM PEM
PINGREQ PINGREQ