From 899ba579fc55a04e95b780ffec5d20d0942dbda2 Mon Sep 17 00:00:00 2001 From: William Yang Date: Sat, 27 Mar 2021 14:03:10 +0100 Subject: [PATCH 001/379] feat(quic): compile and start quicer listener. --- apps/emqx/etc/emqx.conf | 306 +++++++++++++++++++++++++++++++ apps/emqx/src/emqx_listeners.erl | 17 +- rebar.config | 1 + rebar.config.erl | 1 + 4 files changed, 324 insertions(+), 1 deletion(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index f6627bf1c..6be808264 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -2155,6 +2155,312 @@ listener.wss.external.allow_origin_absence = true ## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" + +##-------------------------------------------------------------------- +## External QUIC listener for MQTT Protocol + +## listener.quic.$name is the IP address and port that the MQTT/QUIC +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8084, 127.0.0.1:8084, ::1:8084 +listener.quic.external = 4567 + +## The path of WebSocket MQTT endpoint +## +## Value: URL Path +listener.quic.external.mqtt_path = /mqtt + +## The acceptor pool for external MQTT/QUIC listener. +## +## Value: Number +listener.quic.external.acceptors = 4 + +## Maximum number of concurrent MQTT/Webwocket/SSL connections. +## +## Value: Number +listener.quic.external.max_connections = 16 + +## Maximum MQTT/QUIC connections per second. +## +## See: listener.tcp.$name.max_conn_rate +## +## Value: Number +listener.quic.external.max_conn_rate = 1000 + +## Simulate the {active, N} option for the MQTT/QUIC connections. +## +## Value: Number +listener.quic.external.active_n = 100 + +## Zone of the external MQTT/QUIC listener belonged to. +## +## Value: String +listener.quic.external.zone = external + +## The access control rules for the MQTT/QUIC listener. +## +## See: listener.tcp.$name.access. +## +## Value: ACL Rule +listener.quic.external.access.1 = allow all + +## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. +## Set to false for WeChat MiniApp. +## +## Value: true | false +## listener.quic.external.fail_if_no_subprotocol = true + +## Supported subprotocols +## +## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 +## listener.quic.external.supported_subprotocols = mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 + +## Enable the Proxy Protocol V1/2 support. +## +## See: listener.tcp.$name.proxy_protocol +## +## Value: on | off +## listener.quic.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.tcp.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.quic.external.proxy_protocol_timeout = 3s + +## TLS versions only to protect from POODLE attack. +## +## See: listener.ssl.$name.tls_versions +## +## Value: String, seperated by ',' +## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier +## listener.quic.external.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 + +## Path to the file containing the user's private PEM-encoded key. +## +## See: listener.ssl.$name.keyfile +## +## Value: File +listener.quic.external.keyfile = {{ platform_etc_dir }}/certs/key.pem + +## Path to a file containing the user certificate. +## +## See: listener.ssl.$name.certfile +## +## Value: File +listener.quic.external.certfile = {{ platform_etc_dir }}/certs/cert.pem + +## Path to the file containing PEM-encoded CA certificates. +## +## See: listener.ssl.$name.cacert +## +## Value: File +## listener.quic.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem + +## Maximum number of non-self-issued intermediate certificates that +## can follow the peer certificate in a valid certification path. +## +## See: listener.ssl.external.depth +## +## Value: Number +## listener.quic.external.depth = 10 + +## String containing the user's password. Only used if the private keyfile +## is password-protected. +## +## See: listener.ssl.$name.key_password +## +## Value: String +## listener.quic.external.key_password = yourpass + +## See: listener.ssl.$name.dhfile +## +## Value: File +## listener.ssl.external.dhfile = {{ platform_etc_dir }}/certs/dh-params.pem + +## See: listener.ssl.$name.verify +## +## Value: verify_peer | verify_none +## listener.quic.external.verify = verify_peer + +## See: listener.ssl.$name.fail_if_no_peer_cert +## +## Value: false | true +## listener.quic.external.fail_if_no_peer_cert = true + +## See: listener.ssl.$name.ciphers +## +## Value: Ciphers +listener.quic.external.ciphers = TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA + +## Ciphers for TLS PSK. +## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot +## be configured at the same time. +## See 'https://tools.ietf.org/html/rfc4279#section-2'. +## listener.quic.external.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA + +## See: listener.ssl.$name.secure_renegotiate +## +## Value: on | off +## listener.quic.external.secure_renegotiate = off + +## See: listener.ssl.$name.reuse_sessions +## +## Value: on | off +## listener.quic.external.reuse_sessions = on + +## See: listener.ssl.$name.honor_cipher_order +## +## Value: on | off +## listener.quic.external.honor_cipher_order = on + +## See: listener.ssl.$name.peer_cert_as_username +## +## Value: cn | dn | crt | pem | md5 +## listener.quic.external.peer_cert_as_username = cn + +## See: listener.ssl.$name.peer_cert_as_clientid +## +## Value: cn | dn | crt | pem | md5 +## listener.quic.external.peer_cert_as_clientid = cn + +## TCP backlog for the QUIC connection. +## +## See: listener.tcp.$name.backlog +## +## Value: Number >= 0 +listener.quic.external.backlog = 1024 + +## The TCP send timeout for the QUIC connection. +## +## See: listener.tcp.$name.send_timeout +## +## Value: Duration +listener.quic.external.send_timeout = 15s + +## Close the QUIC connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +listener.quic.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for the QUIC connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +## listener.quic.external.recbuf = 4KB + +## The TCP send buffer(os kernel) for the QUIC connections. +## +## See: listener.tcp.$name.sndbuf +## +## Value: Bytes +## listener.quic.external.sndbuf = 4KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.quic.external.buffer = 4KB + +## The TCP_NODELAY flag for QUIC connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +## listener.quic.external.nodelay = true + +## The compress flag for external QUIC connections. +## +## If this Value is set true,the websocket message would be compressed +## +## Value: true | false +## listener.quic.external.compress = true + +## The level of deflate options for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.level +## +## Value: none | default | best_compression | best_speed +## listener.quic.external.deflate_opts.level = default + +## The mem_level of deflate options for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.mem_level +## +## Valid range is 1-9 +## listener.quic.external.deflate_opts.mem_level = 8 + +## The strategy of deflate options for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.strategy +## +## Value: default | filtered | huffman_only | rle +## listener.quic.external.deflate_opts.strategy = default + +## The deflate option for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.server_context_takeover +## +## Value: takeover | no_takeover +## listener.quic.external.deflate_opts.server_context_takeover = takeover + +## The deflate option for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.client_context_takeover +## +## Value: takeover | no_takeover +## listener.quic.external.deflate_opts.client_context_takeover = takeover + +## The deflate options for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.server_max_window_bits +## +## Valid range is 8-15 +## listener.quic.external.deflate_opts.server_max_window_bits = 15 + +## The deflate options for external QUIC connections. +## +## See: listener.quic.$name.deflate_opts.client_max_window_bits +## +## Valid range is 8-15 +## listener.quic.external.deflate_opts.client_max_window_bits = 15 + +## The idle timeout for external QUIC connections. +## +## See: listener.quic.$name.idle_timeout +## +## Value: Duration +## listener.quic.external.idle_timeout = 60s + +## The max frame size for external QUIC connections. +## +## Value: Number +## listener.quic.external.max_frame_size = 0 + +## Whether a WebSocket message is allowed to contain multiple MQTT packets +## +## Value: single | multiple +listener.quic.external.mqtt_piggyback = multiple +## Enable origin check in header for secure websocket connection +## +## Value: true | false (default false) +listener.quic.external.check_origin_enable = false +## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.quic.external.allow_origin_absence = true +## Comma separated list of allowed origin in header for secure websocket connection +## +## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 +listener.quic.external.check_origins = https://localhost:8084, https://127.0.0.1:8084 + ## CONFIG_SECTION_END=listeners ================================================ ## CONFIG_SECTION_BGN=modules ================================================== diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 1f3d1776b..d97fe32ed 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -139,7 +139,22 @@ start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, - ranch_opts(Options), ws_opts(Options)). + ranch_opts(Options), ws_opts(Options)); + +%% MQTT over QUIC +start_listener(quic, ListenOn, Options) -> + SSLOpts = proplists:get_value(ssl_options, Options), + ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} + , {key, proplists:get_value(keyfile, SSLOpts)} + , {alpn, ["mqtt"]} + , {conn_acceptors, 32} + ], + ConnectionOpts = [ {conn_callback, quicer_server_conn_callback} + , {idle_timeout_ms, 5000} + , {peer_unidi_stream_count, 1} + , {peer_bidi_stream_count, 10}], + StreamOpts = [{stream_callback, quicer_echo_server_stream_callback}], + quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. diff --git a/rebar.config b/rebar.config index 109b680c5..eaa0ea6cf 100644 --- a/rebar.config +++ b/rebar.config @@ -54,6 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} + , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "quicer_application"}}} ]}. {xref_ignores, diff --git a/rebar.config.erl b/rebar.config.erl index 0248e6dab..a05b09686 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -241,6 +241,7 @@ relx_apps(ReleaseType) -> , compiler , runtime_tools , cuttlefish + , quicer , emqx , {mnesia, load} , {ekka, load} From 70f22d2c1b980e885f96354fea35dae57fbd75ee Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Mar 2021 23:39:09 +0200 Subject: [PATCH 002/379] feat(quic): reuse emqx_connection module for quic. --- apps/emqx/src/emqx_connection.erl | 7 +++ apps/emqx/src/emqx_listeners.erl | 4 +- apps/emqx/src/emqx_quic_connection.erl | 26 ++++++++ apps/emqx/src/emqx_quic_stream.erl | 83 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 apps/emqx/src/emqx_quic_connection.erl create mode 100644 apps/emqx/src/emqx_quic_stream.erl diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index ab91c02b4..6900e4f1e 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -416,6 +416,13 @@ handle_msg({Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl -> ok = emqx_metrics:inc('bytes.received', Oct), parse_incoming(Data, State); +handle_msg({quic, Data, _Sock, _, _, _}, State) -> + ?LOG(debug, "RECV ~0p", [Data]), + Oct = iolist_size(Data), + inc_counter(incoming_bytes, Oct), + ok = emqx_metrics:inc('bytes.received', Oct), + parse_incoming(Data, State); + handle_msg({incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State = #state{idle_timer = IdleTimer}) -> ok = emqx_misc:cancel_timer(IdleTimer), diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index d97fe32ed..b3d6bf319 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -149,11 +149,11 @@ start_listener(quic, ListenOn, Options) -> , {alpn, ["mqtt"]} , {conn_acceptors, 32} ], - ConnectionOpts = [ {conn_callback, quicer_server_conn_callback} + ConnectionOpts = [ {conn_callback, emqx_quic_connection} , {idle_timeout_ms, 5000} , {peer_unidi_stream_count, 1} , {peer_bidi_stream_count, 10}], - StreamOpts = [{stream_callback, quicer_echo_server_stream_callback}], + StreamOpts = [], quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl new file mode 100644 index 000000000..b83522c6e --- /dev/null +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -0,0 +1,26 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_quic_connection). + +%% Callbacks +-export([ new_conn/2 + ]). + +new_conn(Conn, {_L, COpts, _S}) when is_map(COpts) -> + new_conn(Conn, maps:to_list(COpts)); +new_conn(Conn, COpts) -> + emqx_connection:start_link(emqx_quic_stream, Conn, COpts). diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl new file mode 100644 index 000000000..e12d95f30 --- /dev/null +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -0,0 +1,83 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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. +%%-------------------------------------------------------------------- + +%% MQTT/QUIC Stream +-module(emqx_quic_stream). + +%% emqx transport Callbacks +-export([ type/1 + , wait/1 + , getstat/2 + , fast_close/1 + , ensure_ok_or_exit/2 + , async_send/3 + , setopts/2 + , getopts/2 + , peername/1 + , sockname/1 + , peercert/1 + ]). + +wait(Conn) -> + quicer:accept_stream(Conn, []). + +type(_) -> + quic. + +peername(S) -> + quicer:peername(S). + +sockname(S) -> + quicer:sockname(S). + +peercert(_S) -> + nossl. + +getstat(Socket, Stats) -> + Res = quicer:getstats(Socket, Stats), + {ok, lists:keyreplace(send_pend, 1, Res, {send_pend, 0})}. + +setopts(_Socket, _Opts) -> + ok. + +getopts(_Socket, _Opts) -> + %% todo + { ok, [{high_watermark, 0}, + {high_msgq_watermark, 0}, + {sndbuf, 0}, + {recbuf, 0}, + {buffer,80000}]}. + +fast_close(Stream) -> + quicer:close_stream(Stream). + +-spec(ensure_ok_or_exit(atom(), list(term())) -> term()). +ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) -> + case erlang:apply(?MODULE, Fun, Args) of + {error, Reason} when Reason =:= enotconn; Reason =:= closed -> + fast_close(Sock), + exit(normal); + {error, Reason} -> + fast_close(Sock), + exit({shutdown, Reason}); + Result -> Result + end. + +async_send(Stream, Data, Options) when is_list(Data) -> + async_send(Stream, iolist_to_binary(Data), Options); +async_send(Stream, Data, _Options) when is_binary(Data) -> + {ok, _Len} = quicer:send(Stream, Data), + ok. From 087aa1dd53bb87f45b997e6b07e9caecb73f252c Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 31 Mar 2021 13:45:40 +0200 Subject: [PATCH 003/379] feat(quic): handle stream close. --- apps/emqx/etc/emqx.conf | 4 ++-- apps/emqx/src/emqx_connection.erl | 3 +++ apps/emqx/src/emqx_quic_stream.erl | 4 +++- rebar.config | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 6be808264..19100c41e 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -2164,8 +2164,8 @@ listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1 ## ## Value: IP:Port | Port ## -## Examples: 8084, 127.0.0.1:8084, ::1:8084 -listener.quic.external = 4567 +## Examples: 14567, 127.0.0.1:14567, ::1:14567 +listener.quic.external = 14567 ## The path of WebSocket MQTT endpoint ## diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 6900e4f1e..8e3ee400b 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -738,6 +738,9 @@ handle_info({sock_error, Reason}, State) -> end, handle_info({sock_closed, Reason}, close_socket(State)); +handle_info({quic, closed, _Channel, ReasonFlag}, State) -> + handle_info({sock_closed, ReasonFlag}, State); + handle_info(Info, State) -> with_channel(handle_info, [Info], State). diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index e12d95f30..a80af643a 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -62,7 +62,9 @@ getopts(_Socket, _Opts) -> {buffer,80000}]}. fast_close(Stream) -> - quicer:close_stream(Stream). + quicer:close_stream(Stream), + %% Stream might be closed already. + ok. -spec(ensure_ok_or_exit(atom(), list(term())) -> term()). ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) -> diff --git a/rebar.config b/rebar.config index eaa0ea6cf..ac0604728 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "quicer_application"}}} + , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "fix/getopt3-free-bin"}}} ]}. {xref_ignores, From 570e096b5690ece5aba7ad01304b589b8909f639 Mon Sep 17 00:00:00 2001 From: William Yang Date: Sat, 3 Apr 2021 11:22:47 +0200 Subject: [PATCH 004/379] fix(quic): return empty list for dead 'Socket' --- apps/emqx/src/emqx_quic_stream.erl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index a80af643a..1c99ad7da 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -47,8 +47,10 @@ peercert(_S) -> nossl. getstat(Socket, Stats) -> - Res = quicer:getstats(Socket, Stats), - {ok, lists:keyreplace(send_pend, 1, Res, {send_pend, 0})}. + case quicer:getstats(Socket, Stats) of + {error, _} -> []; + Res -> {ok, Res} + end. setopts(_Socket, _Opts) -> ok. From 9570d01792fbdd91301dddf511037a9d8e1dcc83 Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 5 Apr 2021 14:28:00 +0200 Subject: [PATCH 005/379] fix(quic): error handling for getstats. - return {error, closed} instead - quicer demo/3 branch. --- apps/emqx/src/emqx_quic_stream.erl | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 1c99ad7da..056c5dd24 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -48,7 +48,7 @@ peercert(_S) -> getstat(Socket, Stats) -> case quicer:getstats(Socket, Stats) of - {error, _} -> []; + {error, _} -> {error, closed}; Res -> {ok, Res} end. diff --git a/rebar.config b/rebar.config index ac0604728..67efca3f6 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "fix/getopt3-free-bin"}}} + , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "demo/3"}}} ]}. {xref_ignores, From 6dc4088f7e169744056cb63c8683c3f054055100 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 5 May 2021 12:41:12 +0200 Subject: [PATCH 006/379] chore(ci): disable centos7 build --- .github/workflows/build_slim_packages.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 6c9bbf04a..53bbba702 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -18,7 +18,7 @@ jobs: - erl23.2.7.2-emqx-2 os: - ubuntu20.04 - - centos7 + #- centos7 container: emqx/build-env:${{ matrix.erl_otp }}-${{ matrix.os }} From 06f9674ce3ce57d1fc0496729eacee744a8b46b7 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 5 May 2021 13:30:50 +0200 Subject: [PATCH 007/379] feat(quic): add quicer to application deps list. --- apps/emqx/src/emqx.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index e909702ae..a6984370e 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -4,7 +4,7 @@ {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, - {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon]}, + {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,quicer]}, {mod, {emqx_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, From 14057c033d5d5d1bc6746b5e60f70cda17fe1169 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 7 May 2021 13:43:48 +0200 Subject: [PATCH 008/379] feat(quic): support stop/start quic listeners. --- apps/emqx/src/emqx.erl | 4 ++-- apps/emqx/src/emqx_app.erl | 3 +++ apps/emqx/src/emqx_listeners.erl | 4 +++- rebar.config | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 2d2e4eb52..449116738 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -239,10 +239,10 @@ reboot() -> -ifdef(EMQX_ENTERPRISE). default_started_applications() -> - [gproc, esockd, ranch, cowboy, ekka, emqx]. + [gproc, esockd, ranch, cowboy, ekka, quicer, emqx]. -else. default_started_applications() -> - [gproc, esockd, ranch, cowboy, ekka, emqx, emqx_modules]. + [gproc, esockd, ranch, cowboy, ekka, quicer, emqx, emqx_modules]. -endif. %%-------------------------------------------------------------------- diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 26b81c8e7..d786a42b9 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -49,6 +49,9 @@ start(_Type, _Args) -> ok = emqx_plugins:init(), _ = emqx_plugins:load(), _ = start_ce_modules(), + %% @fixme unsure why we need this. + quicer_nif:open_lib(), + quicer_nif:reg_open(), emqx_boot:is_enabled(listeners) andalso (ok = emqx_listeners:start()), register(emqx, self()), ok = emqx_alarm_handler:load(), diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index b3d6bf319..2d7a77e65 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -141,7 +141,7 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), ws_opts(Options)); -%% MQTT over QUIC +%% Start MQTT/QUIC listener start_listener(quic, ListenOn, Options) -> SSLOpts = proplists:get_value(ssl_options, Options), ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} @@ -253,6 +253,8 @@ stop_listener(Proto, ListenOn, _Opts) when Proto == http; Proto == ws -> cowboy:stop_listener(ws_name('mqtt:ws', ListenOn)); stop_listener(Proto, ListenOn, _Opts) when Proto == https; Proto == wss -> cowboy:stop_listener(ws_name('mqtt:wss', ListenOn)); +stop_listener(quic, _ListenOn, _Opts) -> + quicer:stop_listener('mqtt:quic'); stop_listener(Proto, ListenOn, _Opts) -> esockd:close(Proto, ListenOn). diff --git a/rebar.config b/rebar.config index 67efca3f6..b602005f6 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "demo/3"}}} + , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "dev/william/main-prepare-emqx"}}} ]}. {xref_ignores, From e062be2b0e1e4835358b6aa21d14b9d77cf372bf Mon Sep 17 00:00:00 2001 From: William Yang Date: Sat, 8 May 2021 23:31:29 +0200 Subject: [PATCH 009/379] feat(quic): reload quicer lib before start listener --- apps/emqx/src/emqx_app.erl | 3 --- apps/emqx/src/emqx_listeners.erl | 3 +++ apps/emqx/test/emqx_listeners_SUITE.erl | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index d786a42b9..26b81c8e7 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -49,9 +49,6 @@ start(_Type, _Args) -> ok = emqx_plugins:init(), _ = emqx_plugins:load(), _ = start_ce_modules(), - %% @fixme unsure why we need this. - quicer_nif:open_lib(), - quicer_nif:reg_open(), emqx_boot:is_enabled(listeners) andalso (ok = emqx_listeners:start()), register(emqx, self()), ok = emqx_alarm_handler:load(), diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 2d7a77e65..d66606895 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -143,6 +143,9 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> %% Start MQTT/QUIC listener start_listener(quic, ListenOn, Options) -> + %% @fixme unsure why we need reopen lib and reopen config. + quicer_nif:open_lib(), + quicer_nif:reg_open(), SSLOpts = proplists:get_value(ssl_options, Options), ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} , {key, proplists:get_value(keyfile, SSLOpts)} diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index 53f388dfa..41b9126b0 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -28,6 +28,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> NewConfig = generate_config(), application:ensure_all_started(esockd), + application:ensure_all_started(quicer), application:ensure_all_started(cowboy), lists:foreach(fun set_app_env/1, NewConfig), Config. From f9a113477e059831655ba9c1a57a683a667a434f Mon Sep 17 00:00:00 2001 From: William Yang Date: Sun, 9 May 2021 00:00:40 +0200 Subject: [PATCH 010/379] feat(quic): use quicer:getstat instead. --- apps/emqx/src/emqx_quic_stream.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 056c5dd24..22d194632 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -47,7 +47,7 @@ peercert(_S) -> nossl. getstat(Socket, Stats) -> - case quicer:getstats(Socket, Stats) of + case quicer:getstat(Socket, Stats) of {error, _} -> {error, closed}; Res -> {ok, Res} end. From 1ffd2cf2459479ed2e8c1465a9184999e4f6c904 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 11 May 2021 08:28:11 +0200 Subject: [PATCH 011/379] chore(config): adapt to new config format --- apps/emqx/etc/emqx.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 19100c41e..bb4c76534 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -2170,7 +2170,7 @@ listener.quic.external = 14567 ## The path of WebSocket MQTT endpoint ## ## Value: URL Path -listener.quic.external.mqtt_path = /mqtt +listener.quic.external.mqtt_path = "/mqtt" ## The acceptor pool for external MQTT/QUIC listener. ## @@ -2244,14 +2244,14 @@ listener.quic.external.access.1 = allow all ## See: listener.ssl.$name.keyfile ## ## Value: File -listener.quic.external.keyfile = {{ platform_etc_dir }}/certs/key.pem +listener.quic.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. ## ## See: listener.ssl.$name.certfile ## ## Value: File -listener.quic.external.certfile = {{ platform_etc_dir }}/certs/cert.pem +listener.quic.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" ## Path to the file containing PEM-encoded CA certificates. ## @@ -2294,7 +2294,7 @@ listener.quic.external.certfile = {{ platform_etc_dir }}/certs/cert.pem ## See: listener.ssl.$name.ciphers ## ## Value: Ciphers -listener.quic.external.ciphers = TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA +listener.quic.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" ## Ciphers for TLS PSK. ## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot @@ -2459,7 +2459,7 @@ listener.quic.external.allow_origin_absence = true ## Comma separated list of allowed origin in header for secure websocket connection ## ## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 -listener.quic.external.check_origins = https://localhost:8084, https://127.0.0.1:8084 +listener.quic.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" ## CONFIG_SECTION_END=listeners ================================================ From 5356668eaccb33a6e689455cf6dd9ca680df65b2 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 10 Jun 2021 16:18:16 +0200 Subject: [PATCH 012/379] feat(quic): adapt to hocon schema --- apps/emqx/etc/emqx.conf | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index bb4c76534..fd6de554e 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1841,7 +1841,7 @@ listener.ws.external.check_origins = "http://localhost:18083, http://127.0.0.1:1 ##-------------------------------------------------------------------- ## External WebSocket/SSL listener for MQTT Protocol -## listener.wss.$name is the IP address and port that the MQTT/WebSocket/SSL +## listener.wss.$name.endpoint is the IP address and port that the MQTT/WebSocket/SSL ## listener will bind. ## ## Value: IP:Port | Port @@ -2159,18 +2159,13 @@ listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1 ##-------------------------------------------------------------------- ## External QUIC listener for MQTT Protocol -## listener.quic.$name is the IP address and port that the MQTT/QUIC +## listener.quic.$name.endpoint is the IP address and port that the MQTT/QUIC ## listener will bind. ## ## Value: IP:Port | Port ## ## Examples: 14567, 127.0.0.1:14567, ::1:14567 -listener.quic.external = 14567 - -## The path of WebSocket MQTT endpoint -## -## Value: URL Path -listener.quic.external.mqtt_path = "/mqtt" +listener.quic.external.endpoint = 14567 ## The acceptor pool for external MQTT/QUIC listener. ## @@ -2204,25 +2199,7 @@ listener.quic.external.zone = external ## See: listener.tcp.$name.access. ## ## Value: ACL Rule -listener.quic.external.access.1 = allow all - -## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. -## Set to false for WeChat MiniApp. -## -## Value: true | false -## listener.quic.external.fail_if_no_subprotocol = true - -## Supported subprotocols -## -## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 -## listener.quic.external.supported_subprotocols = mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 - -## Enable the Proxy Protocol V1/2 support. -## -## See: listener.tcp.$name.proxy_protocol -## -## Value: on | off -## listener.quic.external.proxy_protocol = on +listener.quic.external.access.1 = "allow all" ## Sets the timeout for proxy protocol. ## From 14614fbe33ee27a26e705945261994f8534374b9 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 11 May 2021 22:52:25 +0200 Subject: [PATCH 013/379] feat(quic): adapt to new quicer API. --- apps/emqx/src/emqx_listeners.erl | 2 ++ apps/emqx/src/emqx_quic_stream.erl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index d66606895..6ee07e17b 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -150,6 +150,8 @@ start_listener(quic, ListenOn, Options) -> ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} , {key, proplists:get_value(keyfile, SSLOpts)} , {alpn, ["mqtt"]} + , {peer_unidi_stream_count, 1} + , {peer_bidi_stream_count, 10} , {conn_acceptors, 32} ], ConnectionOpts = [ {conn_callback, emqx_quic_connection} diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 22d194632..4fbe2ed65 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -49,7 +49,7 @@ peercert(_S) -> getstat(Socket, Stats) -> case quicer:getstat(Socket, Stats) of {error, _} -> {error, closed}; - Res -> {ok, Res} + Res -> Res end. setopts(_Socket, _Opts) -> From e63f86e5f0882266a0d35ff1e0737609195c157f Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 12 May 2021 12:17:04 +0200 Subject: [PATCH 014/379] Revert "chore(ci): disable centos7 build" This reverts commit 22e8da1b37492af7def46329ba97dd4cc7741bdf. --- .github/workflows/build_slim_packages.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 53bbba702..6c9bbf04a 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -18,7 +18,7 @@ jobs: - erl23.2.7.2-emqx-2 os: - ubuntu20.04 - #- centos7 + - centos7 container: emqx/build-env:${{ matrix.erl_otp }}-${{ matrix.os }} From 3200bbb301d816ed7112c676e99b56f3cea0d25b Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 1 Jun 2021 09:53:48 +0200 Subject: [PATCH 015/379] fix(build): test with centos branch --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index b602005f6..394296e23 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "dev/william/main-prepare-emqx"}}} + , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "dev/william/main-prepare-emqx-centos7"}}} ]}. {xref_ignores, From bb6459ba3ada82dc1f74be8c1db78ee6a9928a01 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 11 Jun 2021 08:20:23 +0200 Subject: [PATCH 016/379] build: add quic dep in app/emqx/rebar.config --- apps/emqx/rebar.config | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 7e649ae80..d30946a02 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -20,6 +20,7 @@ , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} + , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "dev/william/main-prepare-emqx-centos7"}}} ]}. {plugins, [rebar3_proper]}. From 68844cefd9a119420a0dc5f73fc8d76109ebc082 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 11 Jun 2021 11:12:07 +0200 Subject: [PATCH 017/379] feat(quic): update emqx_schema for quic --- apps/emqx/etc/emqx.conf | 220 +++++++++++++++++----------------- apps/emqx/src/emqx_schema.erl | 34 +++++- 2 files changed, 143 insertions(+), 111 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index fd6de554e..b115964cf 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -2184,43 +2184,43 @@ listener.quic.external.max_connections = 16 ## Value: Number listener.quic.external.max_conn_rate = 1000 -## Simulate the {active, N} option for the MQTT/QUIC connections. -## -## Value: Number -listener.quic.external.active_n = 100 +# ## Simulate the {active, N} option for the MQTT/QUIC connections. +# ## +# ## Value: Number +# listener.quic.external.active_n = 100 ## Zone of the external MQTT/QUIC listener belonged to. ## ## Value: String listener.quic.external.zone = external -## The access control rules for the MQTT/QUIC listener. -## -## See: listener.tcp.$name.access. -## -## Value: ACL Rule -listener.quic.external.access.1 = "allow all" +# ## The access control rules for the MQTT/QUIC listener. +# ## +# ## See: listener.tcp.$name.access. +# ## +# ## Value: ACL Rule +# listener.quic.external.access.1 = "allow all" -## Sets the timeout for proxy protocol. -## -## See: listener.tcp.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.quic.external.proxy_protocol_timeout = 3s +# ## Sets the timeout for proxy protocol. +# ## +# ## See: listener.tcp.$name.proxy_protocol_timeout +# ## +# ## Value: Duration +# ## listener.quic.external.proxy_protocol_timeout = 3s -## TLS versions only to protect from POODLE attack. -## -## See: listener.ssl.$name.tls_versions -## -## Value: String, seperated by ',' -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## listener.quic.external.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 +# ## TLS versions only to protect from POODLE attack. +# ## +# ## See: listener.ssl.$name.tls_versions +# ## +# ## Value: String, seperated by ',' +# ## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier +# ## listener.quic.external.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 -## Path to the file containing the user's private PEM-encoded key. -## -## See: listener.ssl.$name.keyfile -## -## Value: File +# ## Path to the file containing the user's private PEM-encoded key. +# ## +# ## See: listener.ssl.$name.keyfile +# ## +# ## Value: File listener.quic.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. @@ -2230,100 +2230,100 @@ listener.quic.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Value: File listener.quic.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" -## Path to the file containing PEM-encoded CA certificates. -## -## See: listener.ssl.$name.cacert -## -## Value: File -## listener.quic.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem +# ## Path to the file containing PEM-encoded CA certificates. +# ## +# ## See: listener.ssl.$name.cacert +# ## +# ## Value: File +# ## listener.quic.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem -## Maximum number of non-self-issued intermediate certificates that -## can follow the peer certificate in a valid certification path. -## -## See: listener.ssl.external.depth -## -## Value: Number -## listener.quic.external.depth = 10 +# ## Maximum number of non-self-issued intermediate certificates that +# ## can follow the peer certificate in a valid certification path. +# ## +# ## See: listener.ssl.external.depth +# ## +# ## Value: Number +# ## listener.quic.external.depth = 10 -## String containing the user's password. Only used if the private keyfile -## is password-protected. -## -## See: listener.ssl.$name.key_password -## -## Value: String -## listener.quic.external.key_password = yourpass +# ## String containing the user's password. Only used if the private keyfile +# ## is password-protected. +# ## +# ## See: listener.ssl.$name.key_password +# ## +# ## Value: String +# ## listener.quic.external.key_password = yourpass -## See: listener.ssl.$name.dhfile -## -## Value: File -## listener.ssl.external.dhfile = {{ platform_etc_dir }}/certs/dh-params.pem +# ## See: listener.ssl.$name.dhfile +# ## +# ## Value: File +# ## listener.ssl.external.dhfile = {{ platform_etc_dir }}/certs/dh-params.pem -## See: listener.ssl.$name.verify -## -## Value: verify_peer | verify_none -## listener.quic.external.verify = verify_peer +# ## See: listener.ssl.$name.verify +# ## +# ## Value: verify_peer | verify_none +# ## listener.quic.external.verify = verify_peer -## See: listener.ssl.$name.fail_if_no_peer_cert -## -## Value: false | true -## listener.quic.external.fail_if_no_peer_cert = true +# ## See: listener.ssl.$name.fail_if_no_peer_cert +# ## +# ## Value: false | true +# ## listener.quic.external.fail_if_no_peer_cert = true -## See: listener.ssl.$name.ciphers -## -## Value: Ciphers +# ## See: listener.ssl.$name.ciphers +# ## +# ## Value: Ciphers listener.quic.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## Ciphers for TLS PSK. -## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -## listener.quic.external.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA +# ## Ciphers for TLS PSK. +# ## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot +# ## be configured at the same time. +# ## See 'https://tools.ietf.org/html/rfc4279#section-2'. +# ## listener.quic.external.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA -## See: listener.ssl.$name.secure_renegotiate -## -## Value: on | off -## listener.quic.external.secure_renegotiate = off +# ## See: listener.ssl.$name.secure_renegotiate +# ## +# ## Value: on | off +# ## listener.quic.external.secure_renegotiate = off -## See: listener.ssl.$name.reuse_sessions -## -## Value: on | off -## listener.quic.external.reuse_sessions = on +# ## See: listener.ssl.$name.reuse_sessions +# ## +# ## Value: on | off +# ## listener.quic.external.reuse_sessions = on -## See: listener.ssl.$name.honor_cipher_order -## -## Value: on | off -## listener.quic.external.honor_cipher_order = on +# ## See: listener.ssl.$name.honor_cipher_order +# ## +# ## Value: on | off +# ## listener.quic.external.honor_cipher_order = on -## See: listener.ssl.$name.peer_cert_as_username -## -## Value: cn | dn | crt | pem | md5 -## listener.quic.external.peer_cert_as_username = cn +# ## See: listener.ssl.$name.peer_cert_as_username +# ## +# ## Value: cn | dn | crt | pem | md5 +# ## listener.quic.external.peer_cert_as_username = cn -## See: listener.ssl.$name.peer_cert_as_clientid -## -## Value: cn | dn | crt | pem | md5 -## listener.quic.external.peer_cert_as_clientid = cn +# ## See: listener.ssl.$name.peer_cert_as_clientid +# ## +# ## Value: cn | dn | crt | pem | md5 +# ## listener.quic.external.peer_cert_as_clientid = cn -## TCP backlog for the QUIC connection. -## -## See: listener.tcp.$name.backlog -## -## Value: Number >= 0 -listener.quic.external.backlog = 1024 +# ## TCP backlog for the QUIC connection. +# ## +# ## See: listener.tcp.$name.backlog +# ## +# ## Value: Number >= 0 +# listener.quic.external.backlog = 1024 -## The TCP send timeout for the QUIC connection. -## -## See: listener.tcp.$name.send_timeout -## -## Value: Duration -listener.quic.external.send_timeout = 15s +# ## The TCP send timeout for the QUIC connection. +# ## +# ## See: listener.tcp.$name.send_timeout +# ## +# ## Value: Duration +# listener.quic.external.send_timeout = 15s -## Close the QUIC connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -listener.quic.external.send_timeout_close = on +# ## Close the QUIC connection if send timeout. +# ## +# ## See: listener.tcp.$name.send_timeout_close +# ## +# ## Value: on | off +# listener.quic.external.send_timeout_close = on ## The TCP receive buffer(os kernel) for the QUIC connections. ## @@ -2424,19 +2424,19 @@ listener.quic.external.send_timeout_close = on ## Whether a WebSocket message is allowed to contain multiple MQTT packets ## ## Value: single | multiple -listener.quic.external.mqtt_piggyback = multiple +#listener.quic.external.mqtt_piggyback = multiple ## Enable origin check in header for secure websocket connection ## ## Value: true | false (default false) -listener.quic.external.check_origin_enable = false +#listener.quic.external.check_origin_enable = false ## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true ## ## Value: true | false (default true) -listener.quic.external.allow_origin_absence = true +#listener.quic.external.allow_origin_absence = true ## Comma separated list of allowed origin in header for secure websocket connection ## ## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 -listener.quic.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" +#listener.quic.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" ## CONFIG_SECTION_END=listeners ================================================ diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 316f4b77c..d1c163e58 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -282,6 +282,7 @@ fields("listener") -> , {"ssl", ref("ssl_listener")} , {"ws", ref("ws_listener")} , {"wss", ref("wss_listener")} + , {"quic", ref("quic_listener")} ]; fields("tcp_listener") -> @@ -296,6 +297,9 @@ fields("ws_listener") -> fields("wss_listener") -> [ {"$name", ref("wss_listener_settings")}]; +fields("quic_listener") -> + [ {"$name", ref("quic_listener_settings")}]; + fields("listener_settings") -> [ {"endpoint", t(union(ip_port(), integer()))} , {"acceptors", t(integer(), undefined, 8)} @@ -356,6 +360,32 @@ fields("wss_listener_settings") -> Settings = lists:ukeymerge(1, Ssl, fields("ws_listener_settings")), lists:keydelete("high_watermark", 1, Settings); +fields("quic_listener_settings") -> + Unsupported = [ "max_connections" + , "max_conn_rate" + , "active_n" + , "access" + , "proxy_protocol" + , "proxy_protocol_timeout" + , "backlog" + , "send_timeout" + , "send_timeout_close" + , "recvbuf" + , "sndbuf" + , "buffer" + , "high_watermark" + , "tune_buffer" + , "nodelay" + , "reuseaddr" + ], + lists:foldl(fun(K, Acc) -> + lists:keydelete(K, 1, Acc) + end, + [ {"certfile", t(string(), "emqx.certfile", undefined)} + , {"keyfile", t(string(), "emqx.keyfile", undefined)} + | fields("listener_settings")], + Unsupported); + fields("access") -> [ {"$id", t(string(), undefined, undefined)}]; @@ -772,7 +802,9 @@ tr_listeners(Conf) -> lists:flatten([TcpListeners("tcp", Name) || Name <- keys("listener.tcp", Conf)] ++ [TcpListeners("ws", Name) || Name <- keys("listener.ws", Conf)] ++ [SslListeners("ssl", Name) || Name <- keys("listener.ssl", Conf)] - ++ [SslListeners("wss", Name) || Name <- keys("listener.wss", Conf)]). + ++ [SslListeners("wss", Name) || Name <- keys("listener.wss", Conf)] + ++ [SslListeners("quic", Name) || Name <- keys("listener.quic", Conf)] + ). tr_modules(Conf) -> Subscriptions = fun() -> From af2faed10740b14071b78a8ab1fc5a4f52247890 Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 14 Jun 2021 11:29:23 +0200 Subject: [PATCH 018/379] feat(quic): switch to deps on emqx quicer repo --- apps/emqx/rebar.config | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index d30946a02..5719ec0af 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -20,7 +20,7 @@ , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "dev/william/main-prepare-emqx-centos7"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.2"}}} ]}. {plugins, [rebar3_proper]}. diff --git a/rebar.config b/rebar.config index 394296e23..413158842 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/qzhuyan/quic.git", {branch, "dev/william/main-prepare-emqx-centos7"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.2"}}} ]}. {xref_ignores, From e34470f9f2e045f76ee4012788c16a6b4b130870 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 15 Jun 2021 09:23:11 +0200 Subject: [PATCH 019/379] feat(quic): remove unsupported configs. --- apps/emqx/etc/emqx.conf | 262 +++++++++------------------------------- 1 file changed, 60 insertions(+), 202 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index b115964cf..f9c1d26ff 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -2184,43 +2184,21 @@ listener.quic.external.max_connections = 16 ## Value: Number listener.quic.external.max_conn_rate = 1000 -# ## Simulate the {active, N} option for the MQTT/QUIC connections. -# ## -# ## Value: Number -# listener.quic.external.active_n = 100 +## Simulate the {active, N} option for the MQTT/QUIC connections. +## @todo +## Value: Number +## listener.quic.external.active_n = 100 ## Zone of the external MQTT/QUIC listener belonged to. ## ## Value: String listener.quic.external.zone = external -# ## The access control rules for the MQTT/QUIC listener. -# ## -# ## See: listener.tcp.$name.access. -# ## -# ## Value: ACL Rule -# listener.quic.external.access.1 = "allow all" - -# ## Sets the timeout for proxy protocol. -# ## -# ## See: listener.tcp.$name.proxy_protocol_timeout -# ## -# ## Value: Duration -# ## listener.quic.external.proxy_protocol_timeout = 3s - -# ## TLS versions only to protect from POODLE attack. -# ## -# ## See: listener.ssl.$name.tls_versions -# ## -# ## Value: String, seperated by ',' -# ## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -# ## listener.quic.external.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 - -# ## Path to the file containing the user's private PEM-encoded key. -# ## -# ## See: listener.ssl.$name.keyfile -# ## -# ## Value: File +## Path to the file containing the user's private PEM-encoded key. +## +## See: listener.ssl.$name.keyfile +## +## Value: File listener.quic.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. @@ -2230,214 +2208,94 @@ listener.quic.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ## Value: File listener.quic.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" -# ## Path to the file containing PEM-encoded CA certificates. -# ## -# ## See: listener.ssl.$name.cacert -# ## -# ## Value: File -# ## listener.quic.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem +## Path to the file containing PEM-encoded CA certificates. +## @todo +## See: listener.ssl.$name.cacert +## +## Value: File +## listener.quic.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem -# ## Maximum number of non-self-issued intermediate certificates that -# ## can follow the peer certificate in a valid certification path. -# ## -# ## See: listener.ssl.external.depth -# ## -# ## Value: Number -# ## listener.quic.external.depth = 10 +## String containing the user's password. Only used if the private keyfile +## is password-protected. +## @todo +## See: listener.ssl.$name.key_password +## +## Value: String +## listener.quic.external.key_password = yourpass -# ## String containing the user's password. Only used if the private keyfile -# ## is password-protected. -# ## -# ## See: listener.ssl.$name.key_password -# ## -# ## Value: String -# ## listener.quic.external.key_password = yourpass +## See: listener.ssl.$name.verify +## @todo +## Value: verify_peer | verify_none +## listener.quic.external.verify = verify_peer -# ## See: listener.ssl.$name.dhfile -# ## -# ## Value: File -# ## listener.ssl.external.dhfile = {{ platform_etc_dir }}/certs/dh-params.pem +## See: listener.ssl.$name.fail_if_no_peer_cert +## @todo +## Value: false | true +## listener.quic.external.fail_if_no_peer_cert = true -# ## See: listener.ssl.$name.verify -# ## -# ## Value: verify_peer | verify_none -# ## listener.quic.external.verify = verify_peer +## See: listener.ssl.$name.ciphers +## @todo +## Value: Ciphers +listener.quic.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256" -# ## See: listener.ssl.$name.fail_if_no_peer_cert -# ## -# ## Value: false | true -# ## listener.quic.external.fail_if_no_peer_cert = true +## Ciphers for TLS PSK. +## @todo +## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot +## be configured at the same time. +## See 'https://tools.ietf.org/html/rfc4279#section-2'. +## listener.quic.external.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA -# ## See: listener.ssl.$name.ciphers -# ## -# ## Value: Ciphers -listener.quic.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" +## See: listener.ssl.$name.honor_cipher_order +## @todo +## Value: on | off +## listener.quic.external.honor_cipher_order = on -# ## Ciphers for TLS PSK. -# ## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot -# ## be configured at the same time. -# ## See 'https://tools.ietf.org/html/rfc4279#section-2'. -# ## listener.quic.external.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA - -# ## See: listener.ssl.$name.secure_renegotiate -# ## -# ## Value: on | off -# ## listener.quic.external.secure_renegotiate = off - -# ## See: listener.ssl.$name.reuse_sessions -# ## -# ## Value: on | off -# ## listener.quic.external.reuse_sessions = on - -# ## See: listener.ssl.$name.honor_cipher_order -# ## -# ## Value: on | off -# ## listener.quic.external.honor_cipher_order = on - -# ## See: listener.ssl.$name.peer_cert_as_username -# ## -# ## Value: cn | dn | crt | pem | md5 -# ## listener.quic.external.peer_cert_as_username = cn - -# ## See: listener.ssl.$name.peer_cert_as_clientid -# ## -# ## Value: cn | dn | crt | pem | md5 -# ## listener.quic.external.peer_cert_as_clientid = cn - -# ## TCP backlog for the QUIC connection. -# ## -# ## See: listener.tcp.$name.backlog -# ## -# ## Value: Number >= 0 -# listener.quic.external.backlog = 1024 - -# ## The TCP send timeout for the QUIC connection. -# ## -# ## See: listener.tcp.$name.send_timeout -# ## -# ## Value: Duration +## The send timeout for the QUIC stream. +## @todo +## +## Value: Duration # listener.quic.external.send_timeout = 15s -# ## Close the QUIC connection if send timeout. -# ## -# ## See: listener.tcp.$name.send_timeout_close -# ## -# ## Value: on | off -# listener.quic.external.send_timeout_close = on - -## The TCP receive buffer(os kernel) for the QUIC connections. +## Close the QUIC connection if send timeout. +## @todo +## See: listener.tcp.$name.send_timeout_close ## +## Value: on | off +## listener.quic.external.send_timeout_close = on + +## The receive buffer for the QUIC connections. +## @todo ## See: listener.tcp.$name.recbuf ## ## Value: Bytes ## listener.quic.external.recbuf = 4KB ## The TCP send buffer(os kernel) for the QUIC connections. -## +## @todo ## See: listener.tcp.$name.sndbuf ## ## Value: Bytes ## listener.quic.external.sndbuf = 4KB ## The size of the user-level software buffer used by the driver. -## +## @todo ## See: listener.tcp.$name.buffer ## ## Value: Bytes ## listener.quic.external.buffer = 4KB -## The TCP_NODELAY flag for QUIC connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -## listener.quic.external.nodelay = true - -## The compress flag for external QUIC connections. -## -## If this Value is set true,the websocket message would be compressed -## -## Value: true | false -## listener.quic.external.compress = true - -## The level of deflate options for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.level -## -## Value: none | default | best_compression | best_speed -## listener.quic.external.deflate_opts.level = default - -## The mem_level of deflate options for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.mem_level -## -## Valid range is 1-9 -## listener.quic.external.deflate_opts.mem_level = 8 - -## The strategy of deflate options for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.strategy -## -## Value: default | filtered | huffman_only | rle -## listener.quic.external.deflate_opts.strategy = default - -## The deflate option for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.server_context_takeover -## -## Value: takeover | no_takeover -## listener.quic.external.deflate_opts.server_context_takeover = takeover - -## The deflate option for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.client_context_takeover -## -## Value: takeover | no_takeover -## listener.quic.external.deflate_opts.client_context_takeover = takeover - -## The deflate options for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.server_max_window_bits -## -## Valid range is 8-15 -## listener.quic.external.deflate_opts.server_max_window_bits = 15 - -## The deflate options for external QUIC connections. -## -## See: listener.quic.$name.deflate_opts.client_max_window_bits -## -## Valid range is 8-15 -## listener.quic.external.deflate_opts.client_max_window_bits = 15 - ## The idle timeout for external QUIC connections. -## +## @todo ## See: listener.quic.$name.idle_timeout ## ## Value: Duration ## listener.quic.external.idle_timeout = 60s ## The max frame size for external QUIC connections. -## +## @todo ## Value: Number ## listener.quic.external.max_frame_size = 0 -## Whether a WebSocket message is allowed to contain multiple MQTT packets -## -## Value: single | multiple -#listener.quic.external.mqtt_piggyback = multiple -## Enable origin check in header for secure websocket connection -## -## Value: true | false (default false) -#listener.quic.external.check_origin_enable = false -## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true -## -## Value: true | false (default true) -#listener.quic.external.allow_origin_absence = true -## Comma separated list of allowed origin in header for secure websocket connection -## -## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 -#listener.quic.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" - ## CONFIG_SECTION_END=listeners ================================================ ## CONFIG_SECTION_BGN=modules ================================================== From fd785240f51a7b32ac46556aec261a15328a6838 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 15 Jun 2021 14:07:57 +0200 Subject: [PATCH 020/379] feat(quic): bump quicer to 0.0.3 --- apps/emqx/rebar.config | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 5719ec0af..1a58efd5b 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -20,7 +20,7 @@ , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.2"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.3"}}} ]}. {plugins, [rebar3_proper]}. diff --git a/rebar.config b/rebar.config index 413158842..d8cee22b5 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.5.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.2"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.3"}}} ]}. {xref_ignores, From 4e2e2d5635237b8626b124119a0655700edea729 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 15 Jun 2021 15:35:42 +0200 Subject: [PATCH 021/379] feat(quic): update emqx_schema for quic --- apps/emqx/src/emqx_schema.erl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index d1c163e58..f8a5de183 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -361,9 +361,7 @@ fields("wss_listener_settings") -> lists:keydelete("high_watermark", 1, Settings); fields("quic_listener_settings") -> - Unsupported = [ "max_connections" - , "max_conn_rate" - , "active_n" + Unsupported = [ "active_n" , "access" , "proxy_protocol" , "proxy_protocol_timeout" @@ -381,8 +379,9 @@ fields("quic_listener_settings") -> lists:foldl(fun(K, Acc) -> lists:keydelete(K, 1, Acc) end, - [ {"certfile", t(string(), "emqx.certfile", undefined)} - , {"keyfile", t(string(), "emqx.keyfile", undefined)} + [ {"certfile", t(string(), undefined, undefined)} + , {"keyfile", t(string(), undefined, undefined)} + , {"ciphers", t(comma_separated_list(), undefined, "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256")} | fields("listener_settings")], Unsupported); From b4a9d663aee800141cbfbcaba0fe7d5935b269c3 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 16 Jun 2021 15:08:56 +0200 Subject: [PATCH 022/379] feat(quic): quic conn idle_timeout default 1min --- apps/emqx/src/emqx_listeners.erl | 10 +++++----- apps/emqx/src/emqx_schema.erl | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 6ee07e17b..c7d42e2e4 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -147,17 +147,17 @@ start_listener(quic, ListenOn, Options) -> quicer_nif:open_lib(), quicer_nif:reg_open(), SSLOpts = proplists:get_value(ssl_options, Options), + DefAcceptors = erlang:system_info(schedulers_online) * 8, ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} , {key, proplists:get_value(keyfile, SSLOpts)} , {alpn, ["mqtt"]} - , {peer_unidi_stream_count, 1} - , {peer_bidi_stream_count, 10} - , {conn_acceptors, 32} + , {conn_acceptors, proplists:get_value(acceptors, Options, DefAcceptors)} + , {idle_timeout_ms, proplists:get_value(idle_timeout, Options, 60000)} ], ConnectionOpts = [ {conn_callback, emqx_quic_connection} - , {idle_timeout_ms, 5000} , {peer_unidi_stream_count, 1} - , {peer_bidi_stream_count, 10}], + , {peer_bidi_stream_count, 10} + ], StreamOpts = [], quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index f8a5de183..2660fa668 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -382,6 +382,7 @@ fields("quic_listener_settings") -> [ {"certfile", t(string(), undefined, undefined)} , {"keyfile", t(string(), undefined, undefined)} , {"ciphers", t(comma_separated_list(), undefined, "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256")} + , {"idle_timeout", t(duration(), undefined, 60000)} | fields("listener_settings")], Unsupported); From d1978aaaf278d9524e5456ac36758e23c4c582fb Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 17 Jun 2021 09:02:34 +0200 Subject: [PATCH 023/379] chore(quic): fix format --- apps/emqx/src/emqx_quic_stream.erl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 4fbe2ed65..e5cd4c3fc 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -56,12 +56,12 @@ setopts(_Socket, _Opts) -> ok. getopts(_Socket, _Opts) -> - %% todo - { ok, [{high_watermark, 0}, - {high_msgq_watermark, 0}, - {sndbuf, 0}, - {recbuf, 0}, - {buffer,80000}]}. + %% @todo + {ok, [{high_watermark, 0}, + {high_msgq_watermark, 0}, + {sndbuf, 0}, + {recbuf, 0}, + {buffer,80000}]}. fast_close(Stream) -> quicer:close_stream(Stream), @@ -77,7 +77,7 @@ ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) -> {error, Reason} -> fast_close(Sock), exit({shutdown, Reason}); - Result -> Result + Result -> Result end. async_send(Stream, Data, Options) when is_list(Data) -> From 6063fc72f71763a2640cd89cb9a42edfb7f280f0 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Fri, 25 Jun 2021 16:27:33 +0200 Subject: [PATCH 024/379] chore(authentication): Migrate to RLOG --- apps/emqx_authentication/include/emqx_authentication.hrl | 2 ++ apps/emqx_authentication/src/emqx_authentication.erl | 9 ++++++--- apps/emqx_authentication/src/emqx_authentication_app.erl | 3 +++ .../src/emqx_authentication_mnesia.erl | 8 +++++--- .../test/emqx_authentication_SUITE.erl | 6 ++---- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl index 09d3c5fc4..814c03eb4 100644 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ b/apps/emqx_authentication/include/emqx_authentication.hrl @@ -39,3 +39,5 @@ , services :: [{service_name(), #service{}}] , created_at :: integer() }). + +-define(AUTH_SHARD, emqx_authentication_shard). diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index ab7b8537c..a3d0241c0 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -56,6 +56,9 @@ -define(CHAIN_TAB, emqx_authentication_chain). -define(SERVICE_TYPE_TAB, emqx_authentication_service_type). +-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). +-rlog_shard({?AUTH_SHARD, ?SERVICE_TYPE_TAB}). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -370,7 +373,7 @@ validate_other_service_params([#{type := Type, params := Params} = ServiceParams {error, not_found} -> {error, {not_found, {service_type, Type}}} end. - + no_duplicate_names(Names) -> no_duplicate_names(Names, #{}). @@ -423,7 +426,7 @@ extract_services([ServiceName | More], Services, Acc) -> false -> {error, {not_found, {service, ServiceName}}} end. - + move_service_to_the_front_(ServiceName, Services) -> move_service_to_the_front_(ServiceName, Services, []). @@ -513,7 +516,7 @@ trans(Fun) -> trans(Fun, []). trans(Fun, Args) -> - case mnesia:transaction(Fun, Args) of + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authentication/src/emqx_authentication_app.erl index 2d395def7..3bea3c3d6 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx_authentication/src/emqx_authentication_app.erl @@ -20,6 +20,8 @@ -emqx_plugin(?MODULE). +-include("emqx_authentication.hrl"). + %% Application callbacks -export([ start/2 , stop/1 @@ -27,6 +29,7 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_authentication_sup:start_link(), + ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), ok = emqx_authentication:register_service_types(), {ok, Sup}. diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 53dc4dd73..09307e38f 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -51,7 +51,7 @@ salt_rounds => #{ order => 3, type => number, - default => 10 + default => 10 } } }). @@ -72,6 +72,8 @@ -define(TAB, mnesia_basic_auth). +-rlog_shard({?AUTH_SHARD, ?TAB}). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -231,7 +233,7 @@ import(UserGroup, [#{<<"user_id">> := UserID, import(_UserGroup, [_ | _More]) -> {error, bad_format}. -%% Importing 5w users needs 1.7 seconds +%% Importing 5w users needs 1.7 seconds import(UserGroup, File, Seq) -> case file:read_line(File) of {ok, Line} -> @@ -330,7 +332,7 @@ trans(Fun) -> trans(Fun, []). trans(Fun, Args) -> - case mnesia:transaction(Fun, Args) of + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. diff --git a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl index d110d940a..d9f9ace8b 100644 --- a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl +++ b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl @@ -28,6 +28,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), emqx_ct_helpers:start_apps([emqx_authentication]), Config. @@ -40,7 +41,7 @@ t_chain(_) -> ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(#{id => ChainID})), ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), ok. @@ -186,6 +187,3 @@ t_multi_mnesia_service(_) -> ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), ok. - - - From 97fa19f244b3d22e74066390d7f67efa68c0b2b5 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 28 Jun 2021 14:11:34 +0800 Subject: [PATCH 025/379] build: delete priv/emqx.schema file --- priv/emqx.schema | 2470 ---------------------------------------------- 1 file changed, 2470 deletions(-) delete mode 100644 priv/emqx.schema diff --git a/priv/emqx.schema b/priv/emqx.schema deleted file mode 100644 index 142a9acf4..000000000 --- a/priv/emqx.schema +++ /dev/null @@ -1,2470 +0,0 @@ -%%-*- mode: erlang -*- -%% EMQ X R4.0 config mapping - -%%-------------------------------------------------------------------- -%% Cluster -%%-------------------------------------------------------------------- - -%% @doc Cluster name -{mapping, "cluster.name", "ekka.cluster_name", [ - {default, emqxcl}, - {datatype, atom} -]}. - -%% @doc Cluster discovery -{mapping, "cluster.discovery", "ekka.cluster_discovery", [ - {default, manual}, - {datatype, atom} -]}. - -%% @doc Clean down node from the cluster -{mapping, "cluster.autoclean", "ekka.cluster_autoclean", [ - {datatype, {duration, ms}} -]}. - -%% @doc Cluster autoheal -{mapping, "cluster.autoheal", "ekka.cluster_autoheal", [ - {datatype, flag}, - {default, off} -]}. - -%%-------------------------------------------------------------------- -%% Cluster by static node list - -{mapping, "cluster.static.seeds", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Cluster by UDP Multicast - -{mapping, "cluster.mcast.addr", "ekka.cluster_discovery", [ - {default, "239.192.0.1"}, - {datatype, string} -]}. - -{mapping, "cluster.mcast.ports", "ekka.cluster_discovery", [ - {default, "4369"}, - {datatype, string} -]}. - -{mapping, "cluster.mcast.iface", "ekka.cluster_discovery", [ - {datatype, string}, - {default, "0.0.0.0"} -]}. - -{mapping, "cluster.mcast.ttl", "ekka.cluster_discovery", [ - {datatype, integer}, - {default, 255} -]}. - -{mapping, "cluster.mcast.loop", "ekka.cluster_discovery", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "cluster.mcast.sndbuf", "ekka.cluster_discovery", [ - {datatype, bytesize}, - {default, "16KB"} -]}. - -{mapping, "cluster.mcast.recbuf", "ekka.cluster_discovery", [ - {datatype, bytesize}, - {default, "16KB"} -]}. - -{mapping, "cluster.mcast.buffer", "ekka.cluster_discovery", [ - {datatype, bytesize}, - {default, "32KB"} -]}. - -%%-------------------------------------------------------------------- -%% Cluster by DNS A Record - -{mapping, "cluster.dns.name", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%% @doc The erlang distributed protocol -{mapping, "cluster.proto_dist", "ekka.proto_dist", [ - {default, "inet_tcp"}, - {datatype, {enum, [inet_tcp, inet6_tcp, inet_tls]}}, - hidden -]}. - -{mapping, "cluster.dns.app", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Cluster using etcd - -{mapping, "cluster.etcd.server", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.prefix", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.node_ttl", "ekka.cluster_discovery", [ - {datatype, {duration, ms}}, - {default, "1m"} -]}. - -{mapping, "cluster.etcd.ssl.keyfile", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.ssl.certfile", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.etcd.ssl.cacertfile", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Cluster on K8s - -{mapping, "cluster.k8s.apiserver", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.service_name", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.address_type", "ekka.cluster_discovery", [ - {datatype, {enum, [ip, dns, hostname]}} -]}. - -{mapping, "cluster.k8s.app_name", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.namespace", "ekka.cluster_discovery", [ - {datatype, string} -]}. - -{mapping, "cluster.k8s.suffix", "ekka.cluster_discovery", [ - {datatype, string}, - {default, ""} - ]}. - -{translation, "ekka.cluster_discovery", fun(Conf) -> - Strategy = cuttlefish:conf_get("cluster.discovery", Conf), - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - IpPort = fun(S) -> - [Addr, Port] = string:tokens(S, ":"), - {ok, Ip} = inet:parse_address(Addr), - {Ip, Port} - end, - Options = fun(static) -> - [{seeds, [list_to_atom(S) || S <- string:tokens(cuttlefish:conf_get("cluster.static.seeds", Conf, ""), ",")]}]; - (mcast) -> - {ok, Addr} = inet:parse_address(cuttlefish:conf_get("cluster.mcast.addr", Conf)), - {ok, Iface} = inet:parse_address(cuttlefish:conf_get("cluster.mcast.iface", Conf)), - Ports = [list_to_integer(S) || S <- string:tokens(cuttlefish:conf_get("cluster.mcast.ports", Conf), ",")], - [{addr, Addr}, {ports, Ports}, {iface, Iface}, - {ttl, cuttlefish:conf_get("cluster.mcast.ttl", Conf, 1)}, - {loop, cuttlefish:conf_get("cluster.mcast.loop", Conf, true)}]; - (dns) -> - [{name, cuttlefish:conf_get("cluster.dns.name", Conf)}, - {app, cuttlefish:conf_get("cluster.dns.app", Conf)}]; - (etcd) -> - SslOpts = fun(Conf) -> - Options = cuttlefish_variable:filter_by_prefix("cluster.etcd.ssl", Conf), - lists:map(fun({["cluster", "etcd", "ssl", Name], Value}) -> - {list_to_atom(Name), Value} - end, Options) - end, - [{server, string:tokens(cuttlefish:conf_get("cluster.etcd.server", Conf), ",")}, - {prefix, cuttlefish:conf_get("cluster.etcd.prefix", Conf, "emqcl")}, - {node_ttl, cuttlefish:conf_get("cluster.etcd.node_ttl", Conf, 60)}, - {ssl_options, SslOpts(Conf)}]; - (k8s) -> - [{apiserver, cuttlefish:conf_get("cluster.k8s.apiserver", Conf)}, - {service_name, cuttlefish:conf_get("cluster.k8s.service_name", Conf)}, - {address_type, cuttlefish:conf_get("cluster.k8s.address_type", Conf, ip)}, - {app_name, cuttlefish:conf_get("cluster.k8s.app_name", Conf)}, - {namespace, cuttlefish:conf_get("cluster.k8s.namespace", Conf)}, - {suffix, cuttlefish:conf_get("cluster.k8s.suffix", Conf, "")}]; - (manual) -> - [ ] - end, - {Strategy, Filter(Options(Strategy))} -end}. - -%%-------------------------------------------------------------------- -%% Node -%%-------------------------------------------------------------------- - -%% @doc Node name -{mapping, "node.name", "vm_args.-name", [ - {default, "emqx@127.0.0.1"}, - {override_env, "NODE_NAME"} -]}. - -%% @doc Specify SSL Options in the file if using SSL for erlang distribution -{mapping, "node.ssl_dist_optfile", "vm_args.-ssl_dist_optfile", [ - {datatype, string}, - hidden -]}. - -%% @doc Secret cookie for distributed erlang node -{mapping, "node.cookie", "vm_args.-setcookie", [ - {default, "emqxsecretcookie"}, - {override_env, "NODE_COOKIE"} -]}. - -{mapping, "node.data_dir", "emqx.data_dir", [ - {datatype, string} -]}. - -%% @doc http://erlang.org/doc/man/heart.html -{mapping, "node.heartbeat", "vm_args.-heart", [ - {datatype, flag}, - hidden -]}. - -{translation, "vm_args.-heart", fun(Conf) -> - case cuttlefish:conf_get("node.heartbeat", Conf) of - true -> ""; - false -> cuttlefish:invalid("should be 'on' or comment the line!") - end -end}. - -%% @doc More information at: http://erlang.org/doc/man/erl.html -{mapping, "node.async_threads", "vm_args.+A", [ - {datatype, integer}, - {validators, ["range:0-1024"]} -]}. - -%% @doc Erlang Process Limit -{mapping, "node.process_limit", "vm_args.+P", [ - {datatype, integer}, - hidden -]}. - -%% @doc The maximum number of concurrent ports/sockets. -%% Valid range is 1024-134217727 -{mapping, "node.max_ports", "vm_args.+Q", [ - {datatype, integer}, - {validators, ["range4ports"]}, - {override_env, "MAX_PORTS"} -]}. - -{validator, "range4ports", "must be 1024 to 134217727", - fun(X) -> X >= 1024 andalso X =< 134217727 end}. - -%% @doc http://www.erlang.org/doc/man/erl.html#%2bzdbbl -{mapping, "node.dist_buffer_size", "vm_args.+zdbbl", [ - {datatype, bytesize}, - {commented, "32MB"}, - hidden, - {validators, ["zdbbl_range"]} -]}. - -{translation, "vm_args.+zdbbl", - fun(Conf) -> - ZDBBL = cuttlefish:conf_get("node.dist_buffer_size", Conf, undefined), - case ZDBBL of - undefined -> undefined; - X when is_integer(X) -> cuttlefish_util:ceiling(X / 1024); %% Bytes to Kilobytes; - _ -> undefined - end - end}. - -{validator, "zdbbl_range", "must be between 1KB and 2097151KB", - fun(ZDBBL) -> - %% 2097151KB = 2147482624 - ZDBBL >= 1024 andalso ZDBBL =< 2147482624 - end -}. - -%% @doc Global GC Interval -{mapping, "node.global_gc_interval", "emqx.global_gc_interval", [ - {datatype, {duration, s}} -]}. - -%% @doc http://www.erlang.org/doc/man/erlang.html#system_flag-2 -{mapping, "node.fullsweep_after", "vm_args.-env ERL_FULLSWEEP_AFTER", [ - {default, 1000}, - {datatype, integer}, - hidden, - {validators, ["positive_integer"]} -]}. - -{validator, "positive_integer", "must be a positive integer", - fun(X) -> X >= 0 end}. - -%% Note: OTP R15 and earlier uses -env ERL_MAX_ETS_TABLES, -%% R16+ uses +e -%% @doc The ETS table limit -{mapping, "node.max_ets_tables", - cuttlefish:otp("R16", "vm_args.+e", "vm_args.-env ERL_MAX_ETS_TABLES"), [ - {default, 256000}, - {datatype, integer}, - hidden -]}. - -%% @doc Set the location of crash dumps -{mapping, "node.crash_dump", "vm_args.-env ERL_CRASH_DUMP", [ - {default, "{{crash_dump}}"}, - {datatype, file}, - hidden -]}. - -%% @doc http://www.erlang.org/doc/man/kernel_app.html#net_ticktime -{mapping, "node.dist_net_ticktime", "vm_args.-kernel net_ticktime", [ - {datatype, integer}, - hidden -]}. - -%% @doc http://www.erlang.org/doc/man/kernel_app.html -{mapping, "node.dist_listen_min", "kernel.inet_dist_listen_min", [ - {commented, 6369}, - {datatype, integer}, - hidden -]}. - -%% @see node.dist_listen_min -{mapping, "node.dist_listen_max", "kernel.inet_dist_listen_max", [ - {commented, 6369}, - {datatype, integer}, - hidden -]}. - -{mapping, "node.backtrace_depth", "emqx.backtrace_depth", [ - {default, 16}, - {datatype, integer} -]}. - -%%-------------------------------------------------------------------- -%% RPC -%%-------------------------------------------------------------------- - -%% RPC Mode. -{mapping, "rpc.mode", "emqx.rpc_mode", [ - {default, async}, - {datatype, {enum, [sync, async]}} -]}. - -{mapping, "rpc.async_batch_size", "gen_rpc.max_batch_size", [ - {default, 256}, - {datatype, integer} -]}. - -{mapping, "rpc.port_discovery", "gen_rpc.port_discovery", [ - {default, stateless}, - {datatype, {enum, [manual, stateless]}} -]}. - -%% RPC server port. -{mapping, "rpc.tcp_server_port", "gen_rpc.tcp_server_port", [ - {default, 5369}, - {datatype, integer} -]}. - -%% Number of tcp connections when connecting to RPC server -{mapping, "rpc.tcp_client_num", "gen_rpc.tcp_client_num", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:gt_0_lt_256"]} -]}. - -{translation, "gen_rpc.tcp_client_num", fun(Conf) -> - case cuttlefish:conf_get("rpc.tcp_client_num", Conf) of - 0 -> 1; %% keep allowing 0 for backward compatibility - V -> V - end -end}. - -%% Client connect timeout -{mapping, "rpc.connect_timeout", "gen_rpc.connect_timeout", [ - {default, "5s"}, - {datatype, {duration, ms}} -]}. - -%% Client and Server send timeout -{mapping, "rpc.send_timeout", "gen_rpc.send_timeout", [ - {default, 5000}, - {datatype, {duration, ms}} -]}. - -%% Authentication timeout -{mapping, "rpc.authentication_timeout", "gen_rpc.authentication_timeout", [ - {default, 5000}, - {datatype, {duration, ms}} -]}. - -%% Default receive timeout for call() functions -{mapping, "rpc.call_receive_timeout", "gen_rpc.call_receive_timeout", [ - {default, 15000}, - {datatype, {duration, ms}} -]}. - -%% Socket keepalive configuration -{mapping, "rpc.socket_keepalive_idle", "gen_rpc.socket_keepalive_idle", [ - {default, 7200}, - {datatype, {duration, s}} -]}. - -%% Seconds between probes -{mapping, "rpc.socket_keepalive_interval", "gen_rpc.socket_keepalive_interval", [ - {default, 75}, - {datatype, {duration, s}} -]}. - -%% Probes lost to close the connection -{mapping, "rpc.socket_keepalive_count", "gen_rpc.socket_keepalive_count", [ - {default, 9}, - {datatype, integer} -]}. - -%% Size of TCP send buffer -{mapping, "rpc.socket_sndbuf", "gen_rpc.socket_sndbuf", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -%% Size of TCP receive buffer -{mapping, "rpc.socket_recbuf", "gen_rpc.socket_recbuf", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -%% Size of TCP receive buffer -{mapping, "rpc.socket_buffer", "gen_rpc.socket_buffer", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -{validator, "range:gt_0_lt_256", "must greater than 0 and less than 256", - fun(X) -> X >= 0 andalso X < 256 end -}. - -%% Force client to use server listening port, because we do no provide -%% per-node listening port manual mapping from configs. -%% i.e. all nodes in the cluster should agree to the same -%% listening port number. -{translation, "gen_rpc.tcp_client_port", fun(_, _, Conf) -> - cuttlefish:conf_get("rpc.tcp_server_port", Conf) -end}. - -%%-------------------------------------------------------------------- -%% Log -%%-------------------------------------------------------------------- - -{mapping, "log.to", "kernel.logger", [ - {default, file}, - {datatype, {enum, [file, console, both]}} -]}. - -{mapping, "log.level", "kernel.logger", [ - {default, warning}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} -]}. - -{mapping, "log.primary_log_level", "kernel.logger_level", [ - {default, warning}, - {datatype, {enum, [debug, info, notice, warning, error, critical, alert, emergency, all]}} -]}. - -{mapping, "log.dir", "kernel.logger", [ - {default, "log"}, - {datatype, string} -]}. - -{mapping, "log.file", "kernel.logger", [ - {default, "emqx.log"}, - {datatype, file} -]}. - -{mapping, "log.chars_limit", "kernel.logger", [ - {default, -1}, - {datatype, integer} -]}. - -{mapping, "log.supervisor_reports", "kernel.logger", [ - {default, error}, - {datatype, {enum, [error, progress]}}, - hidden -]}. - -%% @doc Maximum depth in Erlang term log formatting -%% and message queue inspection. -{mapping, "log.max_depth", "kernel.error_logger_format_depth", [ - {default, 20}, - {datatype, [{enum, [unlimited]}, integer]} -]}. - -%% @doc format logs as JSON objects -{mapping, "log.formatter", "kernel.logger", [ - {default, text}, - {datatype, {enum, [text, json]}} -]}. - -%% @doc format logs in a single line. -{mapping, "log.single_line", "kernel.logger", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "log.rotation", "kernel.logger", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "log.rotation.size", "kernel.logger", [ - {default, "10MB"}, - {datatype, bytesize} -]}. - -{mapping, "log.size", "kernel.logger", [ - {default, infinity}, - {datatype, [bytesize, atom]} -]}. - -{mapping, "log.rotation.count", "kernel.logger", [ - {default, 5}, - {datatype, integer} -]}. - -{mapping, "log.$level.file", "kernel.logger", [ - {datatype, file} -]}. - -{mapping, "log.sync_mode_qlen", "kernel.logger", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "log.drop_mode_qlen", "kernel.logger", [ - {default, 3000}, - {datatype, integer} -]}. - -{mapping, "log.flush_qlen", "kernel.logger", [ - {default, 8000}, - {datatype, integer} -]}. - -{mapping, "log.overload_kill", "kernel.logger", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "log.overload_kill_mem_size", "kernel.logger", [ - {default, "30MB"}, - {datatype, bytesize} -]}. - -{mapping, "log.overload_kill_qlen", "kernel.logger", [ - {default, 20000}, - {datatype, integer} -]}. - -{mapping, "log.overload_kill_restart_after", "kernel.logger", [ - {default, "5s"}, - {datatype, [{duration, ms}, atom]} -]}. - -{mapping, "log.burst_limit", "kernel.logger", [ - {default, "disabled"}, - {datatype, string} -]}. - -{mapping, "log.error_logger", "kernel.error_logger", [ - {default, silent}, - {datatype, {enum, [silent]}}, - hidden -]}. - -{translation, "kernel.logger_level", fun(_, _, Conf) -> - cuttlefish:conf_get("log.level", Conf) -end}. - -{translation, "kernel.logger", fun(Conf) -> - LogTo = cuttlefish:conf_get("log.to", Conf), - LogLevel = cuttlefish:conf_get("log.level", Conf), - LogType = case cuttlefish:conf_get("log.rotation", Conf) of - true -> wrap; - false -> halt - end, - CharsLimit = case cuttlefish:conf_get("log.chars_limit", Conf) of - -1 -> unlimited; - V -> V - end, - SingleLine = cuttlefish:conf_get("log.single_line", Conf), - FmtName = cuttlefish:conf_get("log.formatter", Conf), - Formatter = - case FmtName of - json -> - {emqx_logger_jsonfmt, - #{chars_limit => CharsLimit, - single_line => SingleLine - }}; - text -> - {emqx_logger_textfmt, - #{template => - [time," [",level,"] ", - {clientid, - [{peername, - [clientid,"@",peername," "], - [clientid, " "]}], - [{peername, - [peername," "], - []}]}, - msg,"\n"], - chars_limit => CharsLimit, - single_line => SingleLine - }} - end, - {BustLimitOn, {MaxBurstCount, TimeWindow}} = - case string:tokens(cuttlefish:conf_get("log.burst_limit", Conf), ", ") of - ["disabled"] -> {false, {20000, 1000}}; - [Count, Window] -> - {true, {list_to_integer(Count), - case cuttlefish_duration:parse(Window, ms) of - Secs when is_integer(Secs) -> Secs; - {error, Reason1} -> error(Reason1) - end}} - end, - FileConf = fun(Filename) -> - BasicConf = - #{type => LogType, - file => filename:join(cuttlefish:conf_get("log.dir", Conf), Filename), - max_no_files => cuttlefish:conf_get("log.rotation.count", Conf), - sync_mode_qlen => cuttlefish:conf_get("log.sync_mode_qlen", Conf), - drop_mode_qlen => cuttlefish:conf_get("log.drop_mode_qlen", Conf), - flush_qlen => cuttlefish:conf_get("log.flush_qlen", Conf), - overload_kill_enable => cuttlefish:conf_get("log.overload_kill", Conf), - overload_kill_qlen => cuttlefish:conf_get("log.overload_kill_qlen", Conf), - overload_kill_mem_size => cuttlefish:conf_get("log.overload_kill_mem_size", Conf), - overload_kill_restart_after => cuttlefish:conf_get("log.overload_kill_restart_after", Conf), - burst_limit_enable => BustLimitOn, - burst_limit_max_count => MaxBurstCount, - burst_limit_window_time => TimeWindow - }, - MaxNoBytes = case LogType of - wrap -> cuttlefish:conf_get("log.rotation.size", Conf); - halt -> cuttlefish:conf_get("log.size", Conf) - end, - BasicConf#{max_no_bytes => MaxNoBytes} - end, - - Filters = case cuttlefish:conf_get("log.supervisor_reports", Conf) of - error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; - progress -> [] - end, - - %% For the default logger that outputs to console - DefaultHandler = - if LogTo =:= console orelse LogTo =:= both -> - [{handler, console, logger_std_h, - #{level => LogLevel, - config => #{type => standard_io}, - formatter => Formatter, - filters => Filters - } - }]; - true -> - [{handler, default, undefined}] - end, - - %% For the file logger - FileHandler = - if LogTo =:= file orelse LogTo =:= both -> - [{handler, file, logger_disk_log_h, - #{level => LogLevel, - config => FileConf(cuttlefish:conf_get("log.file", Conf)), - formatter => Formatter, - filesync_repeat_interval => no_repeat, - filters => Filters - }}]; - true -> [] - end, - - %% For creating additional log files for specific log levels. - AdditionalLogFiles = - lists:foldl( - fun({[_, Level, _] = K, Filename}, Acc) when LogTo =:= file; LogTo =:= both -> - case cuttlefish_variable:is_fuzzy_match(K, ["log", "$level", "file"]) of - true -> [{Level, Filename} | Acc]; - false -> Acc - end; - ({_K, _V}, Acc) -> - Acc - end, [], Conf), - AdditionalHandlers = - [{handler, list_to_atom("file_for_"++Level), logger_disk_log_h, - #{level => list_to_atom(Level), - config => FileConf(Filename), - formatter => Formatter, - filesync_repeat_interval => no_repeat}} - || {Level, Filename} <- AdditionalLogFiles], - - DefaultHandler ++ FileHandler ++ AdditionalHandlers -end}. - -%%-------------------------------------------------------------------- -%% Authentication/ACL -%%-------------------------------------------------------------------- - -%% @doc Allow anonymous authentication. -{mapping, "allow_anonymous", "emqx.allow_anonymous", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc ACL nomatch. -{mapping, "acl_nomatch", "emqx.acl_nomatch", [ - {default, deny}, - {datatype, {enum, [allow, deny]}} -]}. - -%% @doc Default ACL file. -{mapping, "acl_file", "emqx.acl_file", [ - {datatype, string}, - hidden -]}. - -%% @doc Enable ACL cache for publish. -{mapping, "enable_acl_cache", "emqx.enable_acl_cache", [ - {default, on}, - {datatype, flag} -]}. - -%% @doc ACL cache time-to-live. -{mapping, "acl_cache_ttl", "emqx.acl_cache_ttl", [ - {default, "1m"}, - {datatype, {duration, ms}} -]}. - -%% @doc ACL cache size. -{mapping, "acl_cache_max_size", "emqx.acl_cache_max_size", [ - {default, 32}, - {datatype, integer}, - {validators, ["range:gt_0"]} -]}. - -%% @doc Action when acl check reject current operation -{mapping, "acl_deny_action", "emqx.acl_deny_action", [ - {default, ignore}, - {datatype, {enum, [ignore, disconnect]}} -]}. - -%% @doc Flapping detect policy -{mapping, "flapping_detect_policy", "emqx.flapping_detect_policy", [ - {datatype, string}, - {default, "30,1m,5m"} -]}. - -{translation, "emqx.flapping_detect_policy", fun(Conf) -> - Policy = cuttlefish:conf_get("flapping_detect_policy", Conf), - [Threshold, Duration, Interval] = string:tokens(Policy, ", "), - ParseDuration = fun(S, Dur) -> - case cuttlefish_duration:parse(S, Dur) of - I when is_integer(I) -> I; - {error, Reason} -> error(Reason) - end - end, - #{threshold => list_to_integer(Threshold), - duration => ParseDuration(Duration, ms), - banned_interval => ParseDuration(Interval, s) - } -end}. - -{validator, "range:gt_0", "must greater than 0", - fun(X) -> X > 0 end -}. - -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% @doc Max Packet Size Allowed, 1MB by default. -{mapping, "mqtt.max_packet_size", "emqx.max_packet_size", [ - {default, "1MB"}, - {datatype, bytesize}, - {override_env, "MAX_PACKET_SIZE"} -]}. - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "mqtt.max_clientid_len", "emqx.max_clientid_len", [ - {default, 65535}, - {datatype, integer} -]}. - -%% @doc Set the Maximum topic levels. -{mapping, "mqtt.max_topic_levels", "emqx.max_topic_levels", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Set the Maximum QoS allowed. -{mapping, "mqtt.max_qos_allowed", "emqx.max_qos_allowed", [ - {default, 2}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -%% @doc Set the Maximum Topic Alias. -{mapping, "mqtt.max_topic_alias", "emqx.max_topic_alias", [ - {default, 65535}, - {datatype, integer} -]}. - -%% @doc Whether the server supports MQTT retained messages. -{mapping, "mqtt.retain_available", "emqx.retain_available", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports MQTT Wildcard Subscriptions. -{mapping, "mqtt.wildcard_subscription", "emqx.wildcard_subscription", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports MQTT Shared Subscriptions. -{mapping, "mqtt.shared_subscription", "emqx.shared_subscription", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -{mapping, "mqtt.ignore_loop_deliver", "emqx.ignore_loop_deliver", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether to parse the MQTT frame in strict mode -{mapping, "mqtt.strict_mode", "emqx.strict_mode", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Specify the response information returned to the client -{mapping, "mqtt.response_information", "emqx.response_information", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Zones -%%-------------------------------------------------------------------- - -%% @doc Idle timeout of the MQTT connection. -{mapping, "zone.$name.idle_timeout", "emqx.zones", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "zone.$name.allow_anonymous", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "zone.$name.acl_nomatch", "emqx.zones", [ - {datatype, {enum, [allow, deny]}} -]}. - -%% @doc Enable ACL check. -{mapping, "zone.$name.enable_acl", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Action when acl check reject current operation -{mapping, "zone.$name.acl_deny_action", "emqx.zones", [ - {default, ignore}, - {datatype, {enum, [ignore, disconnect]}} -]}. - -%% @doc Enable Ban. -{mapping, "zone.$name.enable_ban", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Enable per connection statistics. -{mapping, "zone.$name.enable_stats", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Publish limit of the MQTT connections. -{mapping, "zone.$name.publish_limit", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Max Packet Size Allowed, 64K by default. -{mapping, "zone.$name.max_packet_size", "emqx.zones", [ - {datatype, bytesize} -]}. - -%% @doc Set the Max ClientId Length Allowed. -{mapping, "zone.$name.max_clientid_len", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum topic levels. -{mapping, "zone.$name.max_topic_levels", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Set the Maximum QoS allowed. -{mapping, "zone.$name.max_qos_allowed", "emqx.zones", [ - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -%% @doc Set the Maximum topic alias. -{mapping, "zone.$name.max_topic_alias", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Whether the server supports retained messages. -{mapping, "zone.$name.retain_available", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Wildcard Subscriptions. -{mapping, "zone.$name.wildcard_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether the Server supports Shared Subscriptions. -{mapping, "zone.$name.shared_subscription", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Server Keepalive -{mapping, "zone.$name.server_keepalive", "emqx.zones", [ - {datatype, integer} -]}. - -%% @doc Keepalive backoff -{mapping, "zone.$name.keepalive_backoff", "emqx.zones", [ - {default, 0.75}, - {datatype, float} -]}. - -%% @doc Max Number of Subscriptions Allowed. -{mapping, "zone.$name.max_subscriptions", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Upgrade QoS according to subscription? -{mapping, "zone.$name.upgrade_qos", "emqx.zones", [ - {default, off}, - {datatype, flag} -]}. - -%% @doc Max number of QoS 1 and 2 messages that can be “inflight” at one time. -%% 0 is equivalent to maximum allowed -{mapping, "zone.$name.max_inflight", "emqx.zones", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:1-65535"]} -]}. - -%% @doc Retry interval for redelivering QoS1/2 messages. -{mapping, "zone.$name.retry_interval", "emqx.zones", [ - {default, "30s"}, - {datatype, {duration, s}} -]}. - -%% @doc Max Packets that Awaiting PUBREL, 0 means no limit -{mapping, "zone.$name.max_awaiting_rel", "emqx.zones", [ - {default, 0}, - {datatype, integer} -]}. - -%% @doc Awaiting PUBREL timeout -{mapping, "zone.$name.await_rel_timeout", "emqx.zones", [ - {default, "300s"}, - {datatype, {duration, s}} -]}. - -%% @doc Ignore loop delivery of messages -{mapping, "zone.$name.ignore_loop_deliver", "emqx.zones", [ - {datatype, {enum, [true, false]}} -]}. - -%% @doc Session Expiry Interval -{mapping, "zone.$name.session_expiry_interval", "emqx.zones", [ - {default, "2h"}, - {datatype, {duration, s}} -]}. - -%% @doc Max queue length. Enqueued messages when persistent client -%% disconnected, or inflight window is full. 0 means no limit. -{mapping, "zone.$name.max_mqueue_len", "emqx.zones", [ - {default, 1000}, - {datatype, integer} -]}. - -%% @doc Topic Priorities, comma separated topic=priority pairs, -%% where priority should be integer in range 1-255 (inclusive) -%% 1 being the lowest and 255 being the highest. -%% default value `none` to indicate no priority table, hence all -%% messages are treated equal, which means either highest ('infinity'), -%% or lowest (0) depending on mqueue_default_priority config. -{mapping, "zone.$name.mqueue_priorities", "emqx.zones", [ - {default, "none"}, - {datatype, string} -]}. - -%% @doc Default priority for topics not in priority table. -{mapping, "zone.$name.mqueue_default_priority", "emqx.zones", [ - {default, lowest}, - {datatype, {enum, [highest, lowest]}} -]}. - -%% @doc Queue Qos0 messages? -{mapping, "zone.$name.mqueue_store_qos0", "emqx.zones", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "zone.$name.enable_flapping_detect", "emqx.zones", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "zone.$name.rate_limit.conn_messages_in", "emqx.zones", [ - {datatype, string} -]}. - -{mapping, "zone.$name.rate_limit.conn_bytes_in", "emqx.zones", [ - {datatype, string} -]}. - -{mapping, "zone.$name.conn_congestion.alarm", "emqx.zones", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "zone.$name.conn_congestion.min_alarm_sustain_duration", "emqx.zones", [ - {default, "1m"}, - {datatype, {duration, ms}} -]}. - -{mapping, "zone.$name.quota.conn_messages_routing", "emqx.zones", [ - {datatype, string} -]}. - -{mapping, "zone.$name.quota.overall_messages_routing", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Force connection/session process GC after this number of -%% messages | bytes passed through. -%% Numbers delimited by `|'. Zero or negative is to disable. -{mapping, "zone.$name.force_gc_policy", "emqx.zones", [ - {datatype, string} - ]}. - -%% @doc Max message queue length and total heap size to force shutdown -%% connection/session process. -%% Message queue here is the Erlang process mailbox, but not the number -%% of queued MQTT messages of QoS 1 and 2. -%% Zero or negative is to disable. -{mapping, "zone.$name.force_shutdown_policy", "emqx.zones", [ - {default, "default"}, - {datatype, string} -]}. - -{mapping, "zone.$name.mountpoint", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Use username replace client id -{mapping, "zone.$name.use_username_as_clientid", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Whether to parse the MQTT frame in strict mode -{mapping, "zone.$name.strict_mode", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Specify the response information returned to the client -{mapping, "zone.$name.response_information", "emqx.zones", [ - {datatype, string} -]}. - -%% @doc Whether to bypass the authentication step -{mapping, "zone.$name.bypass_auth_plugins", "emqx.zones", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.zones", fun(Conf) -> - Ratelimit = fun(Val) -> - [L, D] = string:tokens(Val, ", "), - Limit = case cuttlefish_bytesize:parse(L) of - Sz when is_integer(Sz) -> Sz; - {error, Reason1} -> error(Reason1) - end, - Duration = case cuttlefish_duration:parse(D, s) of - Secs when is_integer(Secs) -> Secs; - {error, Reason} -> error(Reason) - end, - {Limit, Duration} - end, - Mapping = fun(["publish_limit"], Val) -> - %% XXX: Deprecated at v4.2 - {publish_limit, Ratelimit(Val)}; - (["force_gc_policy"], Val) -> - [Count, Bytes] = string:tokens(Val, "| "), - GcPolicy = case cuttlefish_bytesize:parse(Bytes) of - {error, Reason} -> - error(Reason); - Bytes1 -> - #{bytes => Bytes1, - count => list_to_integer(Count)} - end, - {force_gc_policy, GcPolicy}; - (["force_shutdown_policy"], "default") -> - {DefaultLen, DefaultSize} = - case WordSize = erlang:system_info(wordsize) of - 8 -> % arch_64 - {10000, cuttlefish_bytesize:parse("64MB")}; - 4 -> % arch_32 - {1000, cuttlefish_bytesize:parse("32MB")} - end, - {force_shutdown_policy, #{message_queue_len => DefaultLen, - max_heap_size => DefaultSize div WordSize - }}; - (["force_shutdown_policy"], Val) -> - [Len, Siz] = string:tokens(Val, "| "), - MaxSiz = case WordSize = erlang:system_info(wordsize) of - 8 -> % arch_64 - (1 bsl 59) - 1; - 4 -> % arch_32 - (1 bsl 27) - 1 - end, - ShutdownPolicy = - case cuttlefish_bytesize:parse(Siz) of - {error, Reason} -> - error(Reason); - Siz1 when Siz1 > MaxSiz -> - cuttlefish:invalid(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); - Siz1 -> - #{message_queue_len => list_to_integer(Len), - max_heap_size => Siz1 div WordSize} - end, - {force_shutdown_policy, ShutdownPolicy}; - (["mqueue_priorities"], Val) -> - case Val of - "none" -> {mqueue_priorities, none}; % NO_PRIORITY_TABLE - _ -> - MqueuePriorities = lists:foldl(fun(T, Acc) -> - %% NOTE: space in "= " is intended - [Topic, Prio] = string:tokens(T, "= "), - P = list_to_integer(Prio), - (P < 0 orelse P > 255) andalso error({bad_priority, Topic, Prio}), - maps:put(iolist_to_binary(Topic), P, Acc) - end, #{}, string:tokens(Val, ",")), - {mqueue_priorities, MqueuePriorities} - end; - (["mountpoint"], Val) -> - {mountpoint, iolist_to_binary(Val)}; - (["response_information"], Val) -> - {response_information, iolist_to_binary(Val)}; - (["rate_limit", "conn_messages_in"], Val) -> - {ratelimit, {conn_messages_in, Ratelimit(Val)}}; - (["rate_limit", "conn_bytes_in"], Val) -> - {ratelimit, {conn_bytes_in, Ratelimit(Val)}}; - (["conn_congestion", "alarm"], Val) -> - {conn_congestion_alarm_enabled, Val}; - (["conn_congestion", "min_alarm_sustain_duration"], Val) -> - {conn_congestion_min_alarm_sustain_duration, Val}; - (["quota", "conn_messages_routing"], Val) -> - {quota, {conn_messages_routing, Ratelimit(Val)}}; - (["quota", "overall_messages_routing"], Val) -> - {quota, {overall_messages_routing, Ratelimit(Val)}}; - ([Opt], Val) -> - {list_to_atom(Opt), Val} - end, - maps:to_list( - lists:foldl( - fun({["zone", Name | Opt], Val}, Zones) -> - NVal = Mapping(Opt, Val), - maps:update_with(list_to_atom(Name), - fun(Opts) -> - case NVal of - {Key, Rl} when Key == ratelimit; - Key == quota -> - Rls = proplists:get_value(Key, Opts, []), - lists:keystore(Key, 1, Opts, {Key, [Rl|Rls]}); - _ -> - [NVal|Opts] - end - end, [NVal], Zones) - end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("zone.", Conf)))) -end}. - -%%-------------------------------------------------------------------- -%% Listeners -%%-------------------------------------------------------------------- - -%%-------------------------------------------------------------------- -%% TCP Listeners - -{mapping, "listener.tcp.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.tcp.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.tcp.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.tcp.$name.rate_limit", "emqx.listeners", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "listener.tcp.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.tcp.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.tcp.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -%% The proxy-protocol protocol can get the certificate CN through tcp -{mapping, "listener.tcp.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -%% The proxy-protocol protocol can get the certificate CN through tcp -{mapping, "listener.tcp.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -{mapping, "listener.tcp.$name.backlog", "emqx.listeners", [ - {datatype, integer}, - {default, 1024} -]}. - -{mapping, "listener.tcp.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.tcp.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.tcp.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.tcp.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.tcp.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.tcp.$name.high_watermark", "emqx.listeners", [ - {datatype, bytesize}, - {default, "1MB"} - ]}. - -{mapping, "listener.tcp.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.tcp.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.tcp.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -%%-------------------------------------------------------------------- -%% SSL Listeners - -{mapping, "listener.ssl.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.ssl.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.rate_limit", "emqx.listeners", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -{mapping, "listener.ssl.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.ssl.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.ssl.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ssl.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ssl.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ssl.$name.high_watermark", "emqx.listeners", [ - {datatype, bytesize}, - {default, "1MB"} - ]}. - -{mapping, "listener.ssl.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.ssl.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ssl.$name.reuseaddr", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ssl.$name.tls_versions", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.psk_ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.handshake_timeout", "emqx.listeners", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "listener.ssl.$name.depth", "emqx.listeners", [ - {default, 10}, - {datatype, integer} -]}. - -{mapping, "listener.ssl.$name.key_password", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.dhfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.keyfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.certfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.cacertfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ssl.$name.verify", "emqx.listeners", [ - {datatype, atom} -]}. - -{mapping, "listener.ssl.$name.fail_if_no_peer_cert", "emqx.listeners", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.ssl.$name.secure_renegotiate", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.reuse_sessions", "emqx.listeners", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.honor_cipher_order", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ssl.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -{mapping, "listener.ssl.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -%%-------------------------------------------------------------------- -%% MQTT/WebSocket Listeners - -{mapping, "listener.ws.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.ws.$name.mqtt_path", "emqx.listeners", [ - {default, "/mqtt"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.ws.$name.fail_if_no_subprotocol", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.ws.$name.supported_subprotocols", "emqx.listeners", [ - {default, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_address_header", "emqx.listeners", [ - {default, "X-Forwarded-For"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_port_header", "emqx.listeners", [ - {default, "X-Forwarded-Port"}, - {datatype, string} -]}. - -{mapping, "listener.ws.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.ws.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -{mapping, "listener.ws.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.ws.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.ws.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.ws.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ws.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ws.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.ws.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.ws.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ws.$name.compress", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.level", "emqx.listeners", [ - {datatype, {enum, [none, default, best_compression, best_speed]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.mem_level", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:1-9"]}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.strategy", "emqx.listeners", [ - {datatype, {enum, [default, filtered, huffman_only, rle]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.server_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.client_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.server_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.ws.$name.deflate_opts.client_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.ws.$name.idle_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - hidden -]}. - -{mapping, "listener.ws.$name.max_frame_size", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.ws.$name.mqtt_piggyback", "emqx.listeners", [ - {datatype, {enum, [single, multiple]}}, - {default, multiple}, - hidden -]}. - -{mapping, "listener.ws.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -{mapping, "listener.ws.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn]}} -]}. - -{mapping, "listener.ws.$name.check_origin_enable", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, false}, - hidden -]}. - -{mapping, "listener.ws.$name.allow_origin_absence", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, true}, - hidden -]}. - -{mapping, "listener.ws.$name.check_origins", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -%%-------------------------------------------------------------------- -%% MQTT/WebSocket/SSL Listeners - -{mapping, "listener.wss.$name", "emqx.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "listener.wss.$name.mqtt_path", "emqx.listeners", [ - {default, "/mqtt"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.acceptors", "emqx.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.max_connections", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.active_n", "emqx.listeners", [ - {default, 100}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.zone", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.rate_limit", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.fail_if_no_subprotocol", "emqx.listeners", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.wss.$name.supported_subprotocols", "emqx.listeners", [ - {default, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.access.$id", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_address_header", "emqx.listeners", [ - {default, "X-Forwarded-For"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_port_header", "emqx.listeners", [ - {default, "X-Forwarded-Port"}, - {datatype, string} -]}. - -{mapping, "listener.wss.$name.proxy_protocol", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.proxy_protocol_timeout", "emqx.listeners", [ - {datatype, {duration, ms}} -]}. - -%%{mapping, "listener.wss.$name.handshake_timeout", "emqx.listeners", [ -%% {default, "15s"}, -%% {datatype, {duration, ms}} -%%]}. - -{mapping, "listener.wss.$name.backlog", "emqx.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.send_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "listener.wss.$name.send_timeout_close", "emqx.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "listener.wss.$name.recbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.wss.$name.sndbuf", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.wss.$name.buffer", "emqx.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "listener.wss.$name.tune_buffer", "emqx.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "listener.wss.$name.nodelay", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.wss.$name.tls_versions", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.psk_ciphers", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.keyfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.certfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.cacertfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.dhfile", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.depth", "emqx.listeners", [ - {default, 10}, - {datatype, integer} -]}. - -{mapping, "listener.wss.$name.key_password", "emqx.listeners", [ - {datatype, string} -]}. - -{mapping, "listener.wss.$name.verify", "emqx.listeners", [ - {datatype, atom} -]}. - -{mapping, "listener.wss.$name.fail_if_no_peer_cert", "emqx.listeners", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "listener.wss.$name.secure_renegotiate", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.reuse_sessions", "emqx.listeners", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.honor_cipher_order", "emqx.listeners", [ - {datatype, flag} -]}. - -{mapping, "listener.wss.$name.peer_cert_as_username", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -{mapping, "listener.wss.$name.peer_cert_as_clientid", "emqx.listeners", [ - {datatype, {enum, [cn, dn, crt, pem, md5]}} -]}. - -{mapping, "listener.wss.$name.compress", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.level", "emqx.listeners", [ - {datatype, {enum, [none, default, best_compression, best_speed]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.mem_level", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:1-9"]}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.strategy", "emqx.listeners", [ - {datatype, {enum, [default, filtered, huffman_only, rle]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.server_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.client_context_takeover", "emqx.listeners", [ - {datatype, {enum, [takeover, no_takeover]}}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.server_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:8-15"]}, - hidden -]}. - -{mapping, "listener.wss.$name.deflate_opts.client_max_window_bits", "emqx.listeners", [ - {datatype, integer}, - {validators, ["range:8-15"]}, - hidden -]}. - -{mapping, "listener.wss.$name.idle_timeout", "emqx.listeners", [ - {datatype, {duration, ms}}, - hidden -]}. - -{mapping, "listener.wss.$name.max_frame_size", "emqx.listeners", [ - {datatype, integer}, - hidden -]}. - -{mapping, "listener.wss.$name.mqtt_piggyback", "emqx.listeners", [ - {datatype, {enum, [single, multiple]}}, - {default, multiple}, - hidden -]}. - -{mapping, "listener.wss.$name.check_origin_enable", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, false}, - hidden -]}. - -{mapping, "listener.wss.$name.allow_origin_absence", "emqx.listeners", [ - {datatype, {enum, [true, false]}}, - {default, true}, - hidden -]}. - -{mapping, "listener.wss.$name.check_origins", "emqx.listeners", [ - {datatype, string}, - hidden -]}. - -{translation, "emqx.listeners", fun(Conf) -> - - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - - Atom = fun(undefined) -> undefined; (S) -> list_to_atom(S) end, - - Access = fun(S) -> - [A, CIDR] = string:tokens(S, " "), - {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} - end, - - AccOpts = fun(Prefix) -> - case cuttlefish_variable:filter_by_prefix(Prefix ++ ".access", Conf) of - [] -> []; - Rules -> [{access_rules, [Access(Rule) || {_, Rule} <- Rules]}] - end - end, - - RateLimit = fun(undefined) -> - undefined; - (Val) -> - [L, D] = string:tokens(Val, ", "), - Limit = case cuttlefish_bytesize:parse(L) of - Sz when is_integer(Sz) -> Sz; - {error, Reason} -> error(Reason) - end, - Duration = case cuttlefish_duration:parse(D, s) of - Secs when is_integer(Secs) -> Secs; - {error, Reason1} -> error(Reason1) - end, - {Limit, Duration} - end, - - CheckOrigin = fun(S) -> - Origins = string:tokens(S, ","), - [ list_to_binary(string:trim(O)) || O <- Origins] - end, - - WsOpts = fun(Prefix) -> - case cuttlefish_variable:filter_by_prefix(Prefix ++ ".check_origins", Conf) of - [] -> undefined; - Rules -> - OriginList = [CheckOrigin(Rule) || {_, Rule} <- Rules], - lists:flatten(OriginList) - end - end, - - LisOpts = fun(Prefix) -> - Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, - {mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, - {max_connections, cuttlefish:conf_get(Prefix ++ ".max_connections", Conf)}, - {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, - {active_n, cuttlefish:conf_get(Prefix ++ ".active_n", Conf, undefined)}, - {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, - {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - {rate_limit, RateLimit(cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined))}, - {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, - {proxy_address_header, list_to_binary(string:lowercase(cuttlefish:conf_get(Prefix ++ ".proxy_address_header", Conf, "")))}, - {proxy_port_header, list_to_binary(string:lowercase(cuttlefish:conf_get(Prefix ++ ".proxy_port_header", Conf, "")))}, - {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, - {fail_if_no_subprotocol, cuttlefish:conf_get(Prefix ++ ".fail_if_no_subprotocol", Conf, undefined)}, - {supported_subprotocols, string:tokens(cuttlefish:conf_get(Prefix ++ ".supported_subprotocols", Conf, ""), ", ")}, - {peer_cert_as_username, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_username", Conf, undefined)}, - {peer_cert_as_clientid, cuttlefish:conf_get(Prefix ++ ".peer_cert_as_clientid", Conf, undefined)}, - {compress, cuttlefish:conf_get(Prefix ++ ".compress", Conf, undefined)}, - {idle_timeout, cuttlefish:conf_get(Prefix ++ ".idle_timeout", Conf, undefined)}, - {max_frame_size, cuttlefish:conf_get(Prefix ++ ".max_frame_size", Conf, undefined)}, - {mqtt_piggyback, cuttlefish:conf_get(Prefix ++ ".mqtt_piggyback", Conf, undefined)}, - {check_origin_enable, cuttlefish:conf_get(Prefix ++ ".check_origin_enable", Conf, undefined)}, - {allow_origin_absence, cuttlefish:conf_get(Prefix ++ ".allow_origin_absence", Conf, undefined)}, - {check_origins, WsOpts(Prefix)} | AccOpts(Prefix)]) - end, - DeflateOpts = fun(Prefix) -> - Filter([{level, cuttlefish:conf_get(Prefix ++ ".deflate_opts.level", Conf, undefined)}, - {mem_level, cuttlefish:conf_get(Prefix ++ ".deflate_opts.mem_level", Conf, undefined)}, - {strategy, cuttlefish:conf_get(Prefix ++ ".deflate_opts.strategy", Conf, undefined)}, - {server_context_takeover, cuttlefish:conf_get(Prefix ++ ".deflate_opts.server_context_takeover", Conf, undefined)}, - {client_context_takeover, cuttlefish:conf_get(Prefix ++ ".deflate_opts.client_context_takeover", Conf, undefined)}, - {server_max_windows_bits, cuttlefish:conf_get(Prefix ++ ".deflate_opts.server_max_window_bits", Conf, undefined)}, - {client_max_windows_bits, cuttlefish:conf_get(Prefix ++ ".deflate_opts.client_max_window_bits", Conf, undefined)}]) - end, - TcpOpts = fun(Prefix) -> - Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)}, - {send_timeout, cuttlefish:conf_get(Prefix ++ ".send_timeout", Conf, undefined)}, - {send_timeout_close, cuttlefish:conf_get(Prefix ++ ".send_timeout_close", Conf, undefined)}, - {recbuf, cuttlefish:conf_get(Prefix ++ ".recbuf", Conf, undefined)}, - {sndbuf, cuttlefish:conf_get(Prefix ++ ".sndbuf", Conf, undefined)}, - {buffer, cuttlefish:conf_get(Prefix ++ ".buffer", Conf, undefined)}, - {high_watermark, cuttlefish:conf_get(Prefix ++ ".high_watermark", Conf, undefined)}, - {nodelay, cuttlefish:conf_get(Prefix ++ ".nodelay", Conf, true)}, - {reuseaddr, cuttlefish:conf_get(Prefix ++ ".reuseaddr", Conf, undefined)}]) - end, - SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, - MapPSKCiphers = fun(PSKCiphers) -> - lists:map( - fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha}; - ("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha}; - ("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha}; - ("PSK-RC4-SHA") -> {psk, rc4_128, sha} - end, PSKCiphers) - end, - SslOpts = fun(Prefix) -> - Versions = case SplitFun(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf, undefined)) of - undefined -> undefined; - L -> [list_to_atom(V) || V <- L] - end, - TLSCiphers = cuttlefish:conf_get(Prefix++".ciphers", Conf, undefined), - PSKCiphers = cuttlefish:conf_get(Prefix++".psk_ciphers", Conf, undefined), - Ciphers = - case {TLSCiphers, PSKCiphers} of - {undefined, undefined} -> - cuttlefish:invalid(Prefix++".ciphers or "++Prefix++".psk_ciphers is absent"); - {TLSCiphers, undefined} -> - SplitFun(TLSCiphers); - {undefined, PSKCiphers} -> - MapPSKCiphers(SplitFun(PSKCiphers)); - {_TLSCiphers, _PSKCiphers} -> - cuttlefish:invalid(Prefix++".ciphers and "++Prefix++".psk_ciphers cannot be configured at the same time") - end, - UserLookupFun = - case PSKCiphers of - undefined -> undefined; - _ -> {fun emqx_psk:lookup/3, <<>>} - end, - Filter([{versions, Versions}, - {ciphers, Ciphers}, - {user_lookup_fun, UserLookupFun}, - {handshake_timeout, cuttlefish:conf_get(Prefix ++ ".handshake_timeout", Conf, undefined)}, - {depth, cuttlefish:conf_get(Prefix ++ ".depth", Conf, undefined)}, - {password, cuttlefish:conf_get(Prefix ++ ".key_password", Conf, undefined)}, - {dhfile, cuttlefish:conf_get(Prefix ++ ".dhfile", Conf, undefined)}, - {keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, - {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, - {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}, - {verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)}, - {fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)}, - {secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)}, - {reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)}, - {honor_cipher_order, cuttlefish:conf_get(Prefix ++ ".honor_cipher_order", Conf, undefined)}]) - end, - - Listen_fix = fun({Ip, Port}) -> case inet:parse_address(Ip) of - {ok, R} -> {R, Port}; - _ -> {Ip, Port} - end; - (Other) -> Other - end, - - TcpListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - ListenOnN = case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> []; - ListenOn -> Listen_fix(ListenOn) - end, - [#{ proto => Atom(Type) - , name => Name - , listen_on => ListenOnN - , opts => [ {deflate_options, DeflateOpts(Prefix)} - , {tcp_options, TcpOpts(Prefix)} - | LisOpts(Prefix) - ] - } - ] - end, - SslListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - case cuttlefish:conf_get(Prefix, Conf, undefined) of - undefined -> - []; - ListenOn -> - [#{ proto => Atom(Type) - , name => Name - , listen_on => Listen_fix(ListenOn) - , opts => [ {deflate_options, DeflateOpts(Prefix)} - , {tcp_options, TcpOpts(Prefix)} - , {ssl_options, SslOpts(Prefix)} - | LisOpts(Prefix) - ] - } - ] - end - end, - - lists:flatten([TcpListeners(Type, Name) || {["listener", Type, Name], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.tcp", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.ws", Conf)] - ++ - [SslListeners(Type, Name) || {["listener", Type, Name], ListenOn} - <- cuttlefish_variable:filter_by_prefix("listener.ssl", Conf) - ++ cuttlefish_variable:filter_by_prefix("listener.wss", Conf)]) -end}. - -%%-------------------------------------------------------------------- -%% Modules -%%-------------------------------------------------------------------- - -{mapping, "modules.loaded_file", "emqx.modules_loaded_file", [ - {datatype, string} -]}. - -{mapping, "module.presence.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.subscription.$id.topic", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.subscription.$id.qos", "emqx.modules", [ - {default, 1}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.subscription.$id.nl", "emqx.modules", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:0-1"]} -]}. - -{mapping, "module.subscription.$id.rap", "emqx.modules", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:0-1"]} -]}. - -{mapping, "module.subscription.$id.rh", "emqx.modules", [ - {default, 0}, - {datatype, integer}, - {validators, ["range:0-2"]} -]}. - -{mapping, "module.rewrite.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.rewrite.pub.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{mapping, "module.rewrite.sub.rule.$id", "emqx.modules", [ - {datatype, string} -]}. - -{translation, "emqx.modules", fun(Conf, _, Conf1) -> - Subscriptions = fun() -> - List = cuttlefish_variable:filter_by_prefix("module.subscription", Conf), - TopicList = [{N, Topic}|| {[_,"subscription",N,"topic"], Topic} <- List], - [{iolist_to_binary(T), #{ qos => cuttlefish:conf_get("module.subscription." ++ N ++ ".qos", Conf, 0), - nl => cuttlefish:conf_get("module.subscription." ++ N ++ ".nl", Conf, 0), - rap => cuttlefish:conf_get("module.subscription." ++ N ++ ".rap", Conf, 0), - rh => cuttlefish:conf_get("module.subscription." ++ N ++ ".rh", Conf, 0) - }} || {N, T} <- TopicList] - end, - Rewrites = fun() -> - Rules = cuttlefish_variable:filter_by_prefix("module.rewrite.rule", Conf), - PubRules = cuttlefish_variable:filter_by_prefix("module.rewrite.pub.rule", Conf), - SubRules = cuttlefish_variable:filter_by_prefix("module.rewrite.sub.rule", Conf), - TotalRules = lists:append( - [ {["module", "rewrite", "pub", "rule", I], Rule} || {["module", "rewrite", "rule", I], Rule} <- Rules] ++ PubRules, - [ {["module", "rewrite", "sub", "rule", I], Rule} || {["module", "rewrite", "rule", I], Rule} <- Rules] ++ SubRules - ), - lists:map(fun({[_, "rewrite", PubOrSub, "rule", I], Rule}) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, list_to_atom(PubOrSub), list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} - end, TotalRules) - end, - lists:append([ - [{emqx_mod_presence, [{qos, cuttlefish:conf_get("module.presence.qos", Conf, 1)}]}], - [{emqx_mod_subscription, Subscriptions()}], - [{emqx_mod_rewrite, Rewrites()}], - [{emqx_mod_topic_metrics, []}], - [{emqx_mod_delayed, []}], - [{emqx_mod_acl_internal, [{acl_file, cuttlefish:conf_get("acl_file", Conf1)}]}] - ]) -end}. - -%%------------------------------------------------------------------- -%% Plugins -%%------------------------------------------------------------------- - -{mapping, "plugins.etc_dir", "emqx.plugins_etc_dir", [ - {datatype, string} -]}. - -{mapping, "plugins.loaded_file", "emqx.plugins_loaded_file", [ - {datatype, string} -]}. - -{mapping, "plugins.expand_plugins_dir", "emqx.expand_plugins_dir", [ - {datatype, string} -]}. - -%%-------------------------------------------------------------------- -%% Broker -%%-------------------------------------------------------------------- - -{mapping, "broker.sys_interval", "emqx.broker_sys_interval", [ - {datatype, {duration, ms}}, - {default, "1m"} -]}. - -{mapping, "broker.sys_heartbeat", "emqx.broker_sys_heartbeat", [ - {datatype, {duration, ms}}, - {default, "30s"} -]}. - -{mapping, "broker.enable_session_registry", "emqx.enable_session_registry", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "broker.session_locking_strategy", "emqx.session_locking_strategy", [ - {default, quorum}, - {datatype, {enum, [local,leader,quorum,all]}} -]}. - -%% @doc Shared Subscription Dispatch Strategy. -{mapping, "broker.shared_subscription_strategy", "emqx.shared_subscription_strategy", [ - {default, round_robin}, - {datatype, - {enum, - [random, %% randomly pick a subscriber - round_robin, %% round robin alive subscribers one message after another - sticky, %% pick a random subscriber and stick to it - hash, %% hash client ID to a group member - hash_clientid, - hash_topic - ]}} -]}. - -%% @doc Enable or disable shared dispatch acknowledgement for QoS1 and QoS2 messages -{mapping, "broker.shared_dispatch_ack_enabled", "emqx.shared_dispatch_ack_enabled", - [ {default, false}, - {datatype, {enum, [true, false]}} - ]}. - -{mapping, "broker.route_batch_clean", "emqx.route_batch_clean", [ - {default, on}, - {datatype, flag} -]}. - -%% @doc Performance toggle for subscribe/unsubscribe wildcard topic. -%% Change this toggle only when there are many wildcard topics. -%% key: mnesia translational updates with per-key locks. recommended for single node setup. -%% tab: mnesia translational updates with table lock. recommended for multi-nodes setup. -%% global: global lock protected updates. recommended for larger cluster. -%% NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster -%% -{mapping, "broker.perf.route_lock_type", "emqx.route_lock_type", [ - {default, key}, - {datatype, {enum, [key, tab, global]}} -]}. - -%% @doc Enable trie path compaction. -%% Enabling it significantly improves wildcard topic subscribe rate, -%% if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', -%% where ID is unique per subscriber. -%% -%% Topic match performance (when publishing) may degrade if messages -%% are mostly published to topics with large number of levels. -%% -%% NOTE: This is a cluster-wide configuration. -%% It rquires all nodes to be stopped before changing it. -{mapping, "broker.perf.trie_compaction", "emqx.trie_compaction", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -%%-------------------------------------------------------------------- -%% System Monitor -%%-------------------------------------------------------------------- - -%% @doc Long GC, don't monitor in production mode for: -%% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 -{mapping, "sysmon.long_gc", "emqx.sysmon", [ - {default, 0}, - {datatype, [integer, {duration, ms}]} -]}. - -%% @doc Long Schedule(ms) -{mapping, "sysmon.long_schedule", "emqx.sysmon", [ - {default, 240}, - {datatype, [integer, {duration, ms}]} -]}. - -%% @doc Large Heap -{mapping, "sysmon.large_heap", "emqx.sysmon", [ - {default, "8MB"}, - {datatype, bytesize} -]}. - -%% @doc Monitor Busy Port -{mapping, "sysmon.busy_port", "emqx.sysmon", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -%% @doc Monitor Busy Dist Port -{mapping, "sysmon.busy_dist_port", "emqx.sysmon", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx.sysmon", fun(Conf) -> - Configs = cuttlefish_variable:filter_by_prefix("sysmon", Conf), - [{list_to_atom(Name), Value} || {[_, Name], Value} <- Configs] -end}. - -%%-------------------------------------------------------------------- -%% Operating System Monitor -%%-------------------------------------------------------------------- - -{mapping, "os_mon.cpu_check_interval", "emqx.os_mon", [ - {default, 60}, - {datatype, {duration, s}} -]}. - -{mapping, "os_mon.cpu_high_watermark", "emqx.os_mon", [ - {default, "80%"}, - {datatype, {percent, float}} -]}. - -{mapping, "os_mon.cpu_low_watermark", "emqx.os_mon", [ - {default, "60%"}, - {datatype, {percent, float}} -]}. - -{mapping, "os_mon.mem_check_interval", "emqx.os_mon", [ - {default, 60}, - {datatype, {duration, s}} -]}. - -{mapping, "os_mon.sysmem_high_watermark", "emqx.os_mon", [ - {default, "70%"}, - {datatype, {percent, float}} -]}. - -{mapping, "os_mon.procmem_high_watermark", "emqx.os_mon", [ - {default, "5%"}, - {datatype, {percent, float}} -]}. - -{translation, "emqx.os_mon", fun(Conf) -> - [{cpu_check_interval, cuttlefish:conf_get("os_mon.cpu_check_interval", Conf)}, - {cpu_high_watermark, cuttlefish:conf_get("os_mon.cpu_high_watermark", Conf) * 100}, - {cpu_low_watermark, cuttlefish:conf_get("os_mon.cpu_low_watermark", Conf) * 100}, - {mem_check_interval, cuttlefish:conf_get("os_mon.mem_check_interval", Conf)}, - {sysmem_high_watermark, cuttlefish:conf_get("os_mon.sysmem_high_watermark", Conf) * 100}, - {procmem_high_watermark, cuttlefish:conf_get("os_mon.procmem_high_watermark", Conf) * 100}] -end}. - -%%-------------------------------------------------------------------- -%% VM Monitor -%%-------------------------------------------------------------------- -{mapping, "vm_mon.check_interval", "emqx.vm_mon", [ - {default, 30}, - {datatype, {duration, s}} -]}. - -{mapping, "vm_mon.process_high_watermark", "emqx.vm_mon", [ - {default, "80%"}, - {datatype, {percent, float}} -]}. - -{mapping, "vm_mon.process_low_watermark", "emqx.vm_mon", [ - {default, "60%"}, - {datatype, {percent, float}} -]}. - -{translation, "emqx.vm_mon", fun(Conf) -> - [{check_interval, cuttlefish:conf_get("vm_mon.check_interval", Conf)}, - {process_high_watermark, cuttlefish:conf_get("vm_mon.process_high_watermark", Conf) * 100}, - {process_low_watermark, cuttlefish:conf_get("vm_mon.process_low_watermark", Conf) * 100}] -end}. - -%%-------------------------------------------------------------------- -%% Alarm -%%-------------------------------------------------------------------- -{mapping, "alarm.actions", "emqx.alarm", [ - {default, "log,publish"}, - {datatype, string} -]}. - -{mapping, "alarm.size_limit", "emqx.alarm", [ - {default, 1000}, - {datatype, integer} -]}. - -{mapping, "alarm.validity_period", "emqx.alarm", [ - {default, "24h"}, - {datatype, {duration, s}} -]}. - -{translation, "emqx.alarm", fun(Conf) -> - [{actions, [list_to_atom(Action) || Action <- string:tokens(cuttlefish:conf_get("alarm.actions", Conf), ",")]}, - {size_limit, cuttlefish:conf_get("alarm.size_limit", Conf)}, - {validity_period, cuttlefish:conf_get("alarm.validity_period", Conf)}] -end}. - -%%-------------------------------------------------------------------- -%% Telemetry -%%-------------------------------------------------------------------- -{mapping, "telemetry.enabled", "emqx.telemetry", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "telemetry.url", "emqx.telemetry", [ - {default, "https://telemetry-emqx-io.bigpar.vercel.app/api/telemetry"}, - {datatype, string} -]}. - -{mapping, "telemetry.report_interval", "emqx.telemetry", [ - {default, "7d"}, - {datatype, {duration, s}} -]}. - -{translation, "emqx.telemetry", fun(Conf) -> - [ {enabled, cuttlefish:conf_get("telemetry.enabled", Conf)} - , {url, cuttlefish:conf_get("telemetry.url", Conf)} - , {report_interval, cuttlefish:conf_get("telemetry.report_interval", Conf)} - ] -end}. From 54aeacee1444108c061e16b9d6fdb2a5d0bcf58f Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 28 Jun 2021 11:30:38 +0800 Subject: [PATCH 026/379] feat(rule-engine): update the configuration file to hocon --- .../etc/emqx_rule_engine.conf | 44 ++----------- .../priv/emqx_rule_engine.schema | 61 ------------------- .../src/emqx_rule_engine.app.src | 2 +- .../src/emqx_rule_engine.appup.src | 38 ------------ .../src/emqx_rule_engine_schema.erl | 29 +++++++++ .../emqx_rule_engine/src/emqx_rule_events.erl | 45 ++++---------- .../test/emqx_rule_engine_SUITE.erl | 19 +----- .../src/emqx_telemetry_schema.erl | 18 +++++- data/loaded_plugins.tmpl | 1 - rebar.config.erl | 5 +- 10 files changed, 66 insertions(+), 196 deletions(-) delete mode 100644 apps/emqx_rule_engine/priv/emqx_rule_engine.schema delete mode 100644 apps/emqx_rule_engine/src/emqx_rule_engine.appup.src create mode 100644 apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl diff --git a/apps/emqx_rule_engine/etc/emqx_rule_engine.conf b/apps/emqx_rule_engine/etc/emqx_rule_engine.conf index 556c59970..c1637d66d 100644 --- a/apps/emqx_rule_engine/etc/emqx_rule_engine.conf +++ b/apps/emqx_rule_engine/etc/emqx_rule_engine.conf @@ -1,42 +1,6 @@ ##==================================================================== -## Rule Engine for EMQ X R4.0 +## Rule Engine for EMQ X R5.0 ##==================================================================== - -rule_engine.ignore_sys_message = on - -## Event Messages -## -## If enabled (on), rule engine publishes the event as an MQTT message -## with topic='$events/' on the occurrence of an emqx event. -## -## If disabled, rule engine stops publishing the event messages, but -## the event message can still be processed by the rule SQL. e.g. rule SQL: -## -## SELECT * FROM "$events/client_connected" -## -## will still work even if 'rule_engine.events.client_connected' is set to 'off' -## -## EMQ Event to event message mapping: -## -## - client.connected -> $events/client_connected -## - client.disconnected -> $events/client_disconnected -## - session.subscribed -> $events/session_subscribed -## - session.unsubscribed -> $events/session_unsubscribed -## - message.delivered -> $events/message_delivered -## - message.acked -> $events/message_acked -## - message.dropped -> $events/message_dropped -## -## Config Value Format: Toggle, QoS-Level -## -## Toggle: on/off -## -## QoS-Level: qos0/qos1/qos2 - -#rule_engine.events.client_connected = "on, qos1" -rule_engine.events.client_connected = off -rule_engine.events.client_disconnected = off -rule_engine.events.session_subscribed = off -rule_engine.events.session_unsubscribed = off -rule_engine.events.message_delivered = off -rule_engine.events.message_acked = off -rule_engine.events.message_dropped = off +emqx_rule_engine:{ + ignore_sys_message: true +} diff --git a/apps/emqx_rule_engine/priv/emqx_rule_engine.schema b/apps/emqx_rule_engine/priv/emqx_rule_engine.schema deleted file mode 100644 index c5549aa36..000000000 --- a/apps/emqx_rule_engine/priv/emqx_rule_engine.schema +++ /dev/null @@ -1,61 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_rule_engine config mapping - -{mapping, "rule_engine.ignore_sys_message", "emqx_rule_engine.ignore_sys_message", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "rule_engine.events.$name", "emqx_rule_engine.events", [ - {default, "off, qos1"}, - {datatype, string} -]}. - -{translation, "emqx_rule_engine.events", fun(Conf) -> - SupportedHooks = - [ 'client.connected' - , 'client.disconnected' - , 'session.subscribed' - , 'session.unsubscribed' - , 'message.delivered' - , 'message.acked' - , 'message.dropped' - ], - - HookPoint = fun(Event) -> - case string:split(Event, "_") of - [Prefix, Name] -> - Point = list_to_atom(lists:append([Prefix, ".", Name])), - case lists:member(Point, SupportedHooks) of - true -> Point; - false -> error({unsupported_event, Event}) - end; - [_] -> - error({invalid_event, Event}) - end - end, - - QoS = fun ("qos"++Level = QoSLevel) -> - case list_to_integer(Level) of - QoSL when QoSL =:= 0; QoSL =:= 1; QoSL =:= 2 -> - QoSL; - _ -> - error({invalid_qos_level, QoSLevel}) - end; - (QoSLevel) -> - error({invalid_qos, QoSLevel}) - end, - - lists:foldl( - fun({EE=[_,"events",EvtName], Val}, Acc) -> - case string:split(string:trim(Val), ",", all) of - ["on"++_, Snd] -> - [{HookPoint(EvtName), on, QoS(string:trim(Snd))} | Acc]; - ["on"++_] -> - [{HookPoint(EvtName), on, 1} | Acc]; - [_] -> - Acc - end; - ({_, _}, Acc) -> Acc - end, [], cuttlefish_variable:filter_by_prefix("rule_engine.events", Conf)) -end}. diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src index aebb73150..ff25dcfd3 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src @@ -1,6 +1,6 @@ {application, emqx_rule_engine, [{description, "EMQ X Rule Engine"}, - {vsn, "4.3.3"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_rule_engine_sup, emqx_rule_registry]}, {applications, [kernel,stdlib,rulesql,getopt]}, diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.appup.src b/apps/emqx_rule_engine/src/emqx_rule_engine.appup.src deleted file mode 100644 index 01c07c124..000000000 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.appup.src +++ /dev/null @@ -1,38 +0,0 @@ -%% -*-: erlang -*- -{"4.3.3", - [ {"4.3.0", - [ {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []} - , {load_module, emqx_rule_engine, brutal_purge, soft_purge, []} - , {load_module, emqx_rule_registry, brutal_purge, soft_purge, []} - , {apply, {emqx_stats, cancel_update, [rule_registery_stats]}} - ]}, - {"4.3.1", - [ {load_module, emqx_rule_engine, brutal_purge, soft_purge, []} - , {load_module, emqx_rule_registry, brutal_purge, soft_purge, []} - , {apply, {emqx_stats, cancel_update, [rule_registery_stats]}} - ]}, - {"4.3.2", - [ {load_module, emqx_rule_registry, brutal_purge, soft_purge, []} - , {apply, {emqx_stats, cancel_update, [rule_registery_stats]}} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.0", - [ {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []} - , {load_module, emqx_rule_engine, brutal_purge, soft_purge, []} - , {load_module, emqx_rule_registry, brutal_purge, soft_purge, []} - , {apply, {emqx_stats, cancel_update, [rule_registery_stats]}} - ]}, - {"4.3.1", - [ {load_module, emqx_rule_engine, brutal_purge, soft_purge, []} - , {load_module, emqx_rule_registry, brutal_purge, soft_purge, []} - , {apply, {emqx_stats, cancel_update, [rule_registery_stats]}} - ]}, - {"4.3.2", - [ {load_module, emqx_rule_registry, brutal_purge, soft_purge, []} - , {apply, {emqx_stats, cancel_update, [rule_registery_stats]}} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl new file mode 100644 index 000000000..f7658c208 --- /dev/null +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_rule_engine_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1]). + +structs() -> ["emqx_rule_engine"]. + +fields("emqx_rule_engine") -> + [{ignore_sys_message, emqx_schema:t(boolean(), undefined, true)}]. diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 97e40439d..26689b022 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -63,9 +63,10 @@ -endif. load(Topic) -> + IgnoreSys = proplists:get_value(ignore_sys_message, env(), true), HookPoint = event_name(Topic), emqx_hooks:put(HookPoint, {?MODULE, hook_fun(HookPoint), - [hook_conf(HookPoint, env())]}). + [#{ignore_sys_message => IgnoreSys}]}). unload() -> lists:foreach(fun(HookPoint) -> @@ -97,26 +98,26 @@ on_message_publish(Message = #message{topic = Topic}, _Env) -> {ok, Message}. on_client_connected(ClientInfo, ConnInfo, Env) -> - may_publish_and_apply('client.connected', + apply_event('client.connected', fun() -> eventmsg_connected(ClientInfo, ConnInfo) end, Env). on_client_disconnected(ClientInfo, Reason, ConnInfo, Env) -> - may_publish_and_apply('client.disconnected', + apply_event('client.disconnected', fun() -> eventmsg_disconnected(ClientInfo, ConnInfo, Reason) end, Env). on_session_subscribed(ClientInfo, Topic, SubOpts, Env) -> - may_publish_and_apply('session.subscribed', + apply_event('session.subscribed', fun() -> eventmsg_sub_or_unsub('session.subscribed', ClientInfo, Topic, SubOpts) end, Env). on_session_unsubscribed(ClientInfo, Topic, SubOpts, Env) -> - may_publish_and_apply('session.unsubscribed', + apply_event('session.unsubscribed', fun() -> eventmsg_sub_or_unsub('session.unsubscribed', ClientInfo, Topic, SubOpts) end, Env). on_message_dropped(Message = #message{flags = #{sys := true}}, _, _, #{ignore_sys_message := true}) -> {ok, Message}; on_message_dropped(Message, _, Reason, Env) -> - may_publish_and_apply('message.dropped', + apply_event('message.dropped', fun() -> eventmsg_dropped(Message, Reason) end, Env), {ok, Message}. @@ -124,7 +125,7 @@ on_message_delivered(_ClientInfo, Message = #message{flags = #{sys := true}}, #{ignore_sys_message := true}) -> {ok, Message}; on_message_delivered(ClientInfo, Message, Env) -> - may_publish_and_apply('message.delivered', + apply_event('message.delivered', fun() -> eventmsg_delivered(ClientInfo, Message) end, Env), {ok, Message}. @@ -132,7 +133,7 @@ on_message_acked(_ClientInfo, Message = #message{flags = #{sys := true}}, #{ignore_sys_message := true}) -> {ok, Message}; on_message_acked(ClientInfo, Message, Env) -> - may_publish_and_apply('message.acked', + apply_event('message.acked', fun() -> eventmsg_acked(ClientInfo, Message) end, Env), {ok, Message}. @@ -297,31 +298,15 @@ with_basic_columns(EventName, Data) when is_map(Data) -> }. %%-------------------------------------------------------------------- -%% Events publishing and rules applying +%% rules applying %%-------------------------------------------------------------------- - -may_publish_and_apply(EventName, GenEventMsg, #{enabled := true, qos := QoS}) -> - EventTopic = event_topic(EventName), - EventMsg = GenEventMsg(), - case emqx_json:safe_encode(EventMsg) of - {ok, Payload} -> - _ = emqx_broker:safe_publish(make_msg(QoS, EventTopic, Payload)), - ok; - {error, _Reason} -> - ?LOG(error, "Failed to encode event msg for ~p, msg: ~p", [EventName, EventMsg]) - end, - emqx_rule_runtime:apply_rules(emqx_rule_registry:get_rules_for(EventTopic), EventMsg); -may_publish_and_apply(EventName, GenEventMsg, _Env) -> +apply_event(EventName, GenEventMsg, _Env) -> EventTopic = event_topic(EventName), case emqx_rule_registry:get_rules_for(EventTopic) of [] -> ok; Rules -> emqx_rule_runtime:apply_rules(Rules, GenEventMsg()) end. -make_msg(QoS, Topic, Payload) -> - emqx_message:set_flags(#{sys => true, event => true}, - emqx_message:make(emqx_events, QoS, Topic, iolist_to_binary(Payload))). - %%-------------------------------------------------------------------- %% Columns %%-------------------------------------------------------------------- @@ -559,14 +544,6 @@ columns_with_exam('session.unsubscribed') -> %% Helper functions %%-------------------------------------------------------------------- -hook_conf(HookPoint, Env) -> - Events = proplists:get_value(events, Env, []), - IgnoreSys = proplists:get_value(ignore_sys_message, Env, true), - case lists:keyfind(HookPoint, 1, Events) of - {_, on, QoS} -> #{enabled => true, qos => QoS, ignore_sys_message => IgnoreSys}; - _ -> #{enabled => false, qos => 1, ignore_sys_message => IgnoreSys} - end. - hook_fun(Event) -> case string:split(atom_to_list(Event), ".") of [Prefix, Name] -> diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index d8244c018..da3e963f0 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -149,11 +149,11 @@ groups() -> init_per_suite(Config) -> ok = ekka_mnesia:start(), ok = emqx_rule_registry:mnesia(boot), - start_apps(), + ok = emqx_ct_helpers:start_apps([emqx_rule_engine], fun set_special_configs/1), Config. end_per_suite(_Config) -> - stop_apps(), + emqx_ct_helpers:stop_apps([emqx_rule_engine]), ok. on_resource_create(_id, _) -> #{}. @@ -2545,21 +2545,6 @@ init_events_counters() -> %%------------------------------------------------------------------------------ %% Start Apps %%------------------------------------------------------------------------------ - -stop_apps() -> - stopped = mnesia:stop(), - [application:stop(App) || App <- [emqx_rule_engine, emqx]]. - -start_apps() -> - [start_apps(App, SchemaFile, ConfigFile) || - {App, SchemaFile, ConfigFile} - <- [{emqx, emqx_schema, deps_path(emqx, "etc/emqx.conf")}, - {emqx_rule_engine, local_path("priv/emqx_rule_engine.schema"), - local_path("etc/emqx_rule_engine.conf")}]]. - -start_apps(App, Schema, ConfigFile) -> - emqx_ct_helpers:start_app(App, Schema, ConfigFile, fun set_special_configs/1). - deps_path(App, RelativePath) -> Path0 = code:lib_dir(App), Path = case file:read_link(Path0) of diff --git a/apps/emqx_telemetry/src/emqx_telemetry_schema.erl b/apps/emqx_telemetry/src/emqx_telemetry_schema.erl index 4d5cab684..0addd4726 100644 --- a/apps/emqx_telemetry/src/emqx_telemetry_schema.erl +++ b/apps/emqx_telemetry/src/emqx_telemetry_schema.erl @@ -1,3 +1,19 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + -module(emqx_telemetry_schema). -include_lib("typerefl/include/types.hrl"). @@ -10,4 +26,4 @@ structs() -> ["emqx_telemetry"]. fields("emqx_telemetry") -> - [{enabled, emqx_schema:t(boolean(), undefined, false)}]. \ No newline at end of file + [{enabled, emqx_schema:t(boolean(), undefined, false)}]. diff --git a/data/loaded_plugins.tmpl b/data/loaded_plugins.tmpl index c2b6311f2..d26d56abf 100644 --- a/data/loaded_plugins.tmpl +++ b/data/loaded_plugins.tmpl @@ -2,5 +2,4 @@ {emqx_dashboard, true}. {emqx_modules, {{enable_plugin_emqx_modules}}}. {emqx_retainer, {{enable_plugin_emqx_retainer}}}. -{emqx_rule_engine, {{enable_plugin_emqx_rule_engine}}}. {emqx_bridge_mqtt, {{enable_plugin_emqx_bridge_mqtt}}}. diff --git a/rebar.config.erl b/rebar.config.erl index d0822f3b2..5f00f87b2 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -192,8 +192,7 @@ overlay_vars_rel(RelType) -> cloud -> "vm.args"; edge -> "vm.args.edge" end, - [ {enable_plugin_emqx_rule_engine, RelType =:= cloud} - , {enable_plugin_emqx_bridge_mqtt, RelType =:= edge} + [ {enable_plugin_emqx_bridge_mqtt, RelType =:= edge} , {enable_plugin_emqx_modules, false} %% modules is not a plugin in ce , {enable_plugin_emqx_retainer, true} , {vm_args_file, VmArgs} @@ -254,6 +253,7 @@ relx_apps(ReleaseType) -> , emqx_resource , emqx_connector , emqx_data_bridge + , emqx_rule_engine ] ++ [emqx_telemetry || not is_enterprise()] ++ [emqx_modules || not is_enterprise()] @@ -286,7 +286,6 @@ relx_plugin_apps(ReleaseType) -> , emqx_stomp , emqx_authentication , emqx_web_hook - , emqx_rule_engine , emqx_statsd ] ++ relx_plugin_apps_per_rel(ReleaseType) From 434beef3ad0731b682b8c3eea7921025930f4d1c Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 28 Jun 2021 14:02:37 +0800 Subject: [PATCH 027/379] feat(rule-engine): Update the configuration file to hocon --- .../emqx_rule_engine/src/emqx_rule_events.erl | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 26689b022..824cfdcb1 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -63,10 +63,8 @@ -endif. load(Topic) -> - IgnoreSys = proplists:get_value(ignore_sys_message, env(), true), HookPoint = event_name(Topic), - emqx_hooks:put(HookPoint, {?MODULE, hook_fun(HookPoint), - [#{ignore_sys_message => IgnoreSys}]}). + emqx_hooks:put(HookPoint, {?MODULE, hook_fun(HookPoint), [[]]}). unload() -> lists:foreach(fun(HookPoint) -> @@ -77,23 +75,18 @@ unload(Topic) -> HookPoint = event_name(Topic), emqx_hooks:del(HookPoint, {?MODULE, hook_fun(HookPoint)}). -env() -> - application:get_all_env(?APP). - %%-------------------------------------------------------------------- %% Callbacks %%-------------------------------------------------------------------- - -on_message_publish(Message = #message{flags = #{event := true}}, - _Env) -> - {ok, Message}; -on_message_publish(Message = #message{flags = #{sys := true}}, - #{ignore_sys_message := true}) -> - {ok, Message}; on_message_publish(Message = #message{topic = Topic}, _Env) -> - case emqx_rule_registry:get_rules_for(Topic) of - [] -> ok; - Rules -> emqx_rule_runtime:apply_rules(Rules, eventmsg_publish(Message)) + case ignore_sys_message(Message) of + true -> + ok; + false -> + case emqx_rule_registry:get_rules_for(Topic) of + [] -> ok; + Rules -> emqx_rule_runtime:apply_rules(Rules, eventmsg_publish(Message)) + end end, {ok, Message}. @@ -113,28 +106,31 @@ on_session_unsubscribed(ClientInfo, Topic, SubOpts, Env) -> apply_event('session.unsubscribed', fun() -> eventmsg_sub_or_unsub('session.unsubscribed', ClientInfo, Topic, SubOpts) end, Env). -on_message_dropped(Message = #message{flags = #{sys := true}}, - _, _, #{ignore_sys_message := true}) -> - {ok, Message}; on_message_dropped(Message, _, Reason, Env) -> - apply_event('message.dropped', - fun() -> eventmsg_dropped(Message, Reason) end, Env), + case ignore_sys_message(Message) of + true -> ok; + false -> + apply_event('message.dropped', + fun() -> eventmsg_dropped(Message, Reason) end, Env) + end, {ok, Message}. -on_message_delivered(_ClientInfo, Message = #message{flags = #{sys := true}}, - #{ignore_sys_message := true}) -> - {ok, Message}; on_message_delivered(ClientInfo, Message, Env) -> - apply_event('message.delivered', - fun() -> eventmsg_delivered(ClientInfo, Message) end, Env), + case ignore_sys_message(Message) of + true -> ok; + false -> + apply_event('message.delivered', + fun() -> eventmsg_delivered(ClientInfo, Message) end, Env) + end, {ok, Message}. -on_message_acked(_ClientInfo, Message = #message{flags = #{sys := true}}, - #{ignore_sys_message := true}) -> - {ok, Message}; on_message_acked(ClientInfo, Message, Env) -> - apply_event('message.acked', - fun() -> eventmsg_acked(ClientInfo, Message) end, Env), + case ignore_sys_message(Message) of + true -> ok; + false -> + apply_event('message.acked', + fun() -> eventmsg_acked(ClientInfo, Message) end, Env) + end, {ok, Message}. %%-------------------------------------------------------------------- @@ -597,3 +593,7 @@ printable_maps(Headers) -> }; (K, V0, AccIn) -> AccIn#{K => V0} end, #{}, Headers). + +ignore_sys_message(#message{flags = Flags}) -> + maps:get(sys, Flags, false) andalso + emqx_config:get([emqx_rule_engine, ignore_sys_message]). From 33036a1a913f753bc314cb7cb498e51f7864bdcc Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 28 Jun 2021 15:24:35 +0800 Subject: [PATCH 028/379] chore(plugins): rm emqx-pks-file plugin --- apps/emqx_psk_file/.gitignore | 16 ---- apps/emqx_psk_file/README.md | 2 - apps/emqx_psk_file/etc/emqx_psk_file.conf | 2 - apps/emqx_psk_file/etc/psk.txt | 2 - apps/emqx_psk_file/priv/emqx_psk_file.schema | 10 --- apps/emqx_psk_file/rebar.config | 16 ---- apps/emqx_psk_file/src/emqx_psk_file.app.src | 14 ---- .../emqx_psk_file/src/emqx_psk_file.appup.src | 13 --- apps/emqx_psk_file/src/emqx_psk_file.erl | 82 ------------------- apps/emqx_psk_file/src/emqx_psk_file_app.erl | 33 -------- apps/emqx_psk_file/src/emqx_psk_file_sup.erl | 32 -------- .../test/emqx_psk_file_SUITE.erl | 24 ------ rebar.config.erl | 2 - scripts/inject-deps.escript | 1 - 14 files changed, 249 deletions(-) delete mode 100644 apps/emqx_psk_file/.gitignore delete mode 100644 apps/emqx_psk_file/README.md delete mode 100644 apps/emqx_psk_file/etc/emqx_psk_file.conf delete mode 100644 apps/emqx_psk_file/etc/psk.txt delete mode 100644 apps/emqx_psk_file/priv/emqx_psk_file.schema delete mode 100644 apps/emqx_psk_file/rebar.config delete mode 100644 apps/emqx_psk_file/src/emqx_psk_file.app.src delete mode 100644 apps/emqx_psk_file/src/emqx_psk_file.appup.src delete mode 100644 apps/emqx_psk_file/src/emqx_psk_file.erl delete mode 100644 apps/emqx_psk_file/src/emqx_psk_file_app.erl delete mode 100644 apps/emqx_psk_file/src/emqx_psk_file_sup.erl delete mode 100644 apps/emqx_psk_file/test/emqx_psk_file_SUITE.erl diff --git a/apps/emqx_psk_file/.gitignore b/apps/emqx_psk_file/.gitignore deleted file mode 100644 index 0379a99df..000000000 --- a/apps/emqx_psk_file/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -data/ -emqx_actorcloud_schema_parser.d -.DS_Store -_build -rebar.lock diff --git a/apps/emqx_psk_file/README.md b/apps/emqx_psk_file/README.md deleted file mode 100644 index 3ba976b81..000000000 --- a/apps/emqx_psk_file/README.md +++ /dev/null @@ -1,2 +0,0 @@ -## EMQX TLS/DTLS PSK Plugin from file - diff --git a/apps/emqx_psk_file/etc/emqx_psk_file.conf b/apps/emqx_psk_file/etc/emqx_psk_file.conf deleted file mode 100644 index 88c5bbdb1..000000000 --- a/apps/emqx_psk_file/etc/emqx_psk_file.conf +++ /dev/null @@ -1,2 +0,0 @@ -psk.file.path = "{{ platform_etc_dir }}/psk.txt" -psk.file.delimiter = ":" diff --git a/apps/emqx_psk_file/etc/psk.txt b/apps/emqx_psk_file/etc/psk.txt deleted file mode 100644 index 3cf33d814..000000000 --- a/apps/emqx_psk_file/etc/psk.txt +++ /dev/null @@ -1,2 +0,0 @@ -client1:1234 -client2:abcd diff --git a/apps/emqx_psk_file/priv/emqx_psk_file.schema b/apps/emqx_psk_file/priv/emqx_psk_file.schema deleted file mode 100644 index 0c784d99b..000000000 --- a/apps/emqx_psk_file/priv/emqx_psk_file.schema +++ /dev/null @@ -1,10 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_psk_file config mapping - -{mapping, "psk.file.path", "emqx_psk_file.path", [ - {datatype, string} -]}. - -{mapping, "psk.file.delimiter", "emqx_psk_file.delimiter", [ - {datatype, string} -]}. \ No newline at end of file diff --git a/apps/emqx_psk_file/rebar.config b/apps/emqx_psk_file/rebar.config deleted file mode 100644 index 7ac3b98c8..000000000 --- a/apps/emqx_psk_file/rebar.config +++ /dev/null @@ -1,16 +0,0 @@ -{deps, []}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. diff --git a/apps/emqx_psk_file/src/emqx_psk_file.app.src b/apps/emqx_psk_file/src/emqx_psk_file.app.src deleted file mode 100644 index ef18c8b69..000000000 --- a/apps/emqx_psk_file/src/emqx_psk_file.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_psk_file, - [{description,"EMQX PSK Plugin from File"}, - {vsn, "4.3.1"}, % strict semver, bump manually! - {modules,[]}, - {registered,[emqx_psk_file_sup]}, - {applications,[kernel,stdlib]}, - {mod,{emqx_psk_file_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-psk-file"} - ]} - ]}. diff --git a/apps/emqx_psk_file/src/emqx_psk_file.appup.src b/apps/emqx_psk_file/src/emqx_psk_file.appup.src deleted file mode 100644 index c34a3f71a..000000000 --- a/apps/emqx_psk_file/src/emqx_psk_file.appup.src +++ /dev/null @@ -1,13 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.0", [ - {restart_application, emqx_psk_file} - ]} - ], - [ - {"4.3.0", [ - {restart_application, emqx_psk_file} - ]} - ] -}. diff --git a/apps/emqx_psk_file/src/emqx_psk_file.erl b/apps/emqx_psk_file/src/emqx_psk_file.erl deleted file mode 100644 index 3afd6dc73..000000000 --- a/apps/emqx_psk_file/src/emqx_psk_file.erl +++ /dev/null @@ -1,82 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_psk_file). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --import(proplists, [get_value/2]). - --export([load/1, unload/0]). - -%% Hooks functions --export([on_psk_lookup/2]). - --define(TAB, ?MODULE). --define(LF, 10). - --record(psk_entry, {psk_id :: binary(), - psk_str :: binary()}). - -%% Called when the plugin application start -load(Env) -> - _ = ets:new(?TAB, [set, named_table, {keypos, #psk_entry.psk_id}]), - {ok, PskFile} = file:open(get_value(path, Env), [read, raw, binary, read_ahead]), - preload_psks(PskFile, bin(get_value(delimiter, Env))), - _ = file:close(PskFile), - emqx:hook('tls_handshake.psk_lookup', {?MODULE, on_psk_lookup, []}). - -%% Called when the plugin application stop -unload() -> - emqx:unhook('tls_handshake.psk_lookup', {?MODULE, on_psk_lookup}). - -on_psk_lookup(ClientPSKID, UserState) -> - case ets:lookup(?TAB, ClientPSKID) of - [#psk_entry{psk_str = PskStr}] -> - {stop, PskStr}; - [] -> - {ok, UserState} - end. - -preload_psks(FileHandler, Delimiter) -> - case file:read_line(FileHandler) of - {ok, Line} -> - case binary:split(Line, Delimiter) of - [Key, Rem] -> - ets:insert(?TAB, #psk_entry{psk_id = Key, psk_str = trim_lf(Rem)}), - preload_psks(FileHandler, Delimiter); - [Line] -> - ?LOG(warning, "[~p] - Invalid line: ~p, delimiter: ~p", [?MODULE, Line, Delimiter]) - end; - eof -> - ?LOG(info, "[~p] - PSK file is preloaded", [?MODULE]); - {error, Reason} -> - ?LOG(error, "[~p] - Read lines from PSK file: ~p", [?MODULE, Reason]) - end. - -bin(Str) when is_list(Str) -> list_to_binary(Str); -bin(Bin) when is_binary(Bin) -> Bin. - -%% Trim the tailing LF -trim_lf(<<>>) -> <<>>; -trim_lf(Bin) -> - Size = byte_size(Bin), - case binary:at(Bin, Size-1) of - ?LF -> binary_part(Bin, 0, Size-1); - _ -> Bin - end. - diff --git a/apps/emqx_psk_file/src/emqx_psk_file_app.erl b/apps/emqx_psk_file/src/emqx_psk_file_app.erl deleted file mode 100644 index 934ffe49e..000000000 --- a/apps/emqx_psk_file/src/emqx_psk_file_app.erl +++ /dev/null @@ -1,33 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_psk_file_app). - --behaviour(application). - --emqx_plugin(?MODULE). - -%% Application callbacks --export([start/2, stop/1]). - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_psk_file_sup:start_link(), - _ = emqx_psk_file:load( - application:get_all_env(emqx_psk_file)), - {ok, Sup}. - -stop(_State) -> - emqx_psk_file:unload(). diff --git a/apps/emqx_psk_file/src/emqx_psk_file_sup.erl b/apps/emqx_psk_file/src/emqx_psk_file_sup.erl deleted file mode 100644 index 041eecdb6..000000000 --- a/apps/emqx_psk_file/src/emqx_psk_file_sup.erl +++ /dev/null @@ -1,32 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_psk_file_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - {ok, { {one_for_one, 0, 1}, []} }. - diff --git a/apps/emqx_psk_file/test/emqx_psk_file_SUITE.erl b/apps/emqx_psk_file/test/emqx_psk_file_SUITE.erl deleted file mode 100644 index d0083247d..000000000 --- a/apps/emqx_psk_file/test/emqx_psk_file_SUITE.erl +++ /dev/null @@ -1,24 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_psk_file_SUITE). --compile(nowarn_export_all). --compile(export_all). - -all() -> []. - -groups() -> - []. diff --git a/rebar.config.erl b/rebar.config.erl index 5f00f87b2..14a8f93bc 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -297,7 +297,6 @@ relx_plugin_apps_per_rel(cloud) -> , emqx_exhook , emqx_exproto , emqx_prometheus - , emqx_psk_file ]; relx_plugin_apps_per_rel(edge) -> []. @@ -355,7 +354,6 @@ etc_overlay(ReleaseType) -> extra_overlay(cloud) -> [ {copy,"{{base_dir}}/lib/emqx_lwm2m/lwm2m_xml","etc/"} - , {copy, "{{base_dir}}/lib/emqx_psk_file/etc/psk.txt", "etc/psk.txt"} ]; extra_overlay(edge) -> []. diff --git a/scripts/inject-deps.escript b/scripts/inject-deps.escript index 0b6371342..8100c3959 100755 --- a/scripts/inject-deps.escript +++ b/scripts/inject-deps.escript @@ -44,7 +44,6 @@ edge_excludes() -> , emqx_exhook , emqx_exproto , emqx_prometheus - , emqx_psk_file ]. base_deps() -> From dc1deff3f3d9aa834c208112727f8d755690df9c Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Fri, 25 Jun 2021 19:02:55 +0200 Subject: [PATCH 029/379] refactor(rlog): Fix initialization of emqx_cm_registry table --- apps/emqx/src/emqx_cm_registry.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_cm_registry.erl b/apps/emqx/src/emqx_cm_registry.erl index d8095b445..30035eca5 100644 --- a/apps/emqx/src/emqx_cm_registry.erl +++ b/apps/emqx/src/emqx_cm_registry.erl @@ -48,7 +48,9 @@ -define(TAB, emqx_channel_registry). -define(LOCK, {?MODULE, cleanup_down}). --rlog_shard({?ROUTE_SHARD, ?TAB}). +-define(CM_SHARD, emqx_cm_shard). + +-rlog_shard({?CM_SHARD, ?TAB}). -record(channel, {chid, pid}). @@ -111,6 +113,7 @@ init([]) -> {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB, ram_copies), + ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), ok = ekka:monitor(membership), {ok, #{}}. @@ -125,7 +128,7 @@ handle_cast(Msg, State) -> handle_info({membership, {mnesia, down, Node}}, State) -> global:trans({?LOCK, self()}, fun() -> - ekka_mnesia:transaction(?ROUTE_SHARD, fun cleanup_channels/1, [Node]) + ekka_mnesia:transaction(?CM_SHARD, fun cleanup_channels/1, [Node]) end), {noreply, State}; From faad90c9d410166d2c333aca22de10afbd108c84 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 28 Jun 2021 16:12:06 +0800 Subject: [PATCH 030/379] chore(plugins): rm emqx-web-hook and mv webhook action to emqx_rule_actions --- .../src/emqx_bridge_mqtt.app.src | 2 +- .../src/emqx_bridge_mqtt.appup.src | 16 - apps/emqx_rule_actions/README.md | 11 + .../rebar.config | 21 +- .../src/emqx_bridge_mqtt_actions.erl | 0 .../src/emqx_rule_actions.app.src | 11 + .../src/emqx_web_hook_actions.erl | 0 apps/emqx_web_hook/.gitignore | 31 -- apps/emqx_web_hook/README.md | 194 --------- apps/emqx_web_hook/TODO | 3 - apps/emqx_web_hook/etc/emqx_web_hook.conf | 77 ---- apps/emqx_web_hook/include/emqx_web_hook.hrl | 1 - apps/emqx_web_hook/priv/emqx_web_hook.schema | 105 ----- apps/emqx_web_hook/src/emqx_web_hook.app.src | 14 - .../emqx_web_hook/src/emqx_web_hook.appup.src | 18 - apps/emqx_web_hook/src/emqx_web_hook.erl | 390 ----------------- apps/emqx_web_hook/src/emqx_web_hook_app.erl | 102 ----- apps/emqx_web_hook/src/emqx_web_hook_sup.erl | 29 -- .../test/emqx_web_hook_SUITE.erl | 284 ------------- .../test/emqx_web_hook_SUITE_data/ca.pem | 19 - .../emqx_web_hook_SUITE_data/client-cert.pem | 19 - .../emqx_web_hook_SUITE_data/client-key.pem | 27 -- .../emqx_web_hook_SUITE_data/server-cert.pem | 19 - .../emqx_web_hook_SUITE_data/server-key.pem | 27 -- apps/emqx_web_hook/test/http_server.erl | 102 ----- .../test/props/prop_webhook_confs.erl | 146 ------- .../test/props/prop_webhook_hooks.erl | 397 ------------------ rebar.config.erl | 2 +- 28 files changed, 38 insertions(+), 2029 deletions(-) delete mode 100644 apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src create mode 100644 apps/emqx_rule_actions/README.md rename apps/{emqx_web_hook => emqx_rule_actions}/rebar.config (57%) rename apps/{emqx_bridge_mqtt => emqx_rule_actions}/src/emqx_bridge_mqtt_actions.erl (100%) create mode 100644 apps/emqx_rule_actions/src/emqx_rule_actions.app.src rename apps/{emqx_web_hook => emqx_rule_actions}/src/emqx_web_hook_actions.erl (100%) delete mode 100644 apps/emqx_web_hook/.gitignore delete mode 100644 apps/emqx_web_hook/README.md delete mode 100644 apps/emqx_web_hook/TODO delete mode 100644 apps/emqx_web_hook/etc/emqx_web_hook.conf delete mode 100644 apps/emqx_web_hook/include/emqx_web_hook.hrl delete mode 100644 apps/emqx_web_hook/priv/emqx_web_hook.schema delete mode 100644 apps/emqx_web_hook/src/emqx_web_hook.app.src delete mode 100644 apps/emqx_web_hook/src/emqx_web_hook.appup.src delete mode 100644 apps/emqx_web_hook/src/emqx_web_hook.erl delete mode 100644 apps/emqx_web_hook/src/emqx_web_hook_app.erl delete mode 100644 apps/emqx_web_hook/src/emqx_web_hook_sup.erl delete mode 100644 apps/emqx_web_hook/test/emqx_web_hook_SUITE.erl delete mode 100644 apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/ca.pem delete mode 100644 apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-cert.pem delete mode 100644 apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-key.pem delete mode 100644 apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-cert.pem delete mode 100644 apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-key.pem delete mode 100644 apps/emqx_web_hook/test/http_server.erl delete mode 100644 apps/emqx_web_hook/test/props/prop_webhook_confs.erl delete mode 100644 apps/emqx_web_hook/test/props/prop_webhook_hooks.erl diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src index 83ce7a759..385c89965 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.app.src @@ -1,6 +1,6 @@ {application, emqx_bridge_mqtt, [{description, "EMQ X Bridge to MQTT Broker"}, - {vsn, "4.3.1"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,replayq,emqtt]}, diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src deleted file mode 100644 index 03e6119ae..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src +++ /dev/null @@ -1,16 +0,0 @@ -%% -*-: erlang -*- - -{VSN, - [ - {"4.3.0", [ - {load_module, emqx_bridge_worker, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.0", [ - {load_module, emqx_bridge_worker, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_rule_actions/README.md b/apps/emqx_rule_actions/README.md new file mode 100644 index 000000000..c17e1a34a --- /dev/null +++ b/apps/emqx_rule_actions/README.md @@ -0,0 +1,11 @@ +# emqx_rule_actions + +This project contains a collection of rule actions/resources. It is mainly for + making unit test easier. Also it's easier for us to create utils that many + modules depends on it. + +## Build +----- + + $ rebar3 compile + diff --git a/apps/emqx_web_hook/rebar.config b/apps/emqx_rule_actions/rebar.config similarity index 57% rename from apps/emqx_web_hook/rebar.config rename to apps/emqx_rule_actions/rebar.config index 387972c9f..097c18a3d 100644 --- a/apps/emqx_web_hook/rebar.config +++ b/apps/emqx_rule_actions/rebar.config @@ -1,18 +1,25 @@ -{plugins, [rebar3_proper]}. - {deps, []}. -{edoc_opts, [{preprocess, true}]}. {erl_opts, [warn_unused_vars, warn_shadow_vars, warn_unused_import, warn_obsolete_guard, - debug_info, - {parse_transform}]}. + no_debug_info, + compressed, %% for edge + {parse_transform} + ]}. + +{overrides, [{add, [{erl_opts, [no_debug_info, compressed]}]}]}. + +{edoc_opts, [{preprocess, true}]}. {xref_checks, [undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. + warnings_as_errors, deprecated_functions + ]}. + {cover_enabled, true}. {cover_opts, [verbose]}. -{cover_export_enabled, true}. \ No newline at end of file +{cover_export_enabled, true}. + +{plugins, [rebar3_proper]}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_actions.erl b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl similarity index 100% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_actions.erl rename to apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl diff --git a/apps/emqx_rule_actions/src/emqx_rule_actions.app.src b/apps/emqx_rule_actions/src/emqx_rule_actions.app.src new file mode 100644 index 000000000..fd95c3572 --- /dev/null +++ b/apps/emqx_rule_actions/src/emqx_rule_actions.app.src @@ -0,0 +1,11 @@ +{application, emqx_rule_actions, + [{description, "Rule actions"}, + {vsn, "5.0.0"}, + {registered, []}, + {applications, + [kernel,stdlib]}, + {env,[]}, + {modules, []}, + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/apps/emqx_web_hook/src/emqx_web_hook_actions.erl b/apps/emqx_rule_actions/src/emqx_web_hook_actions.erl similarity index 100% rename from apps/emqx_web_hook/src/emqx_web_hook_actions.erl rename to apps/emqx_rule_actions/src/emqx_web_hook_actions.erl diff --git a/apps/emqx_web_hook/.gitignore b/apps/emqx_web_hook/.gitignore deleted file mode 100644 index e6348348a..000000000 --- a/apps/emqx_web_hook/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -.rebar3 -_* -.eunit -*.o -*.beam -*.plt -*.swp -*.swo -.erlang.cookie -ebin -log -erl_crash.dump -.rebar -logs -_build -.idea -data -.DS_Store -.erlang.mk/ -cover -ct.coverdata -deps -eunit.coverdata -test/ct.cover.spec -emqx_web_hook.d -emq_web_hook.d -rebar.lock -erlang.mk -rebar3.crashdump -etc/emqx_web_hook.conf.rendered -Mnesia.nonode@nohost diff --git a/apps/emqx_web_hook/README.md b/apps/emqx_web_hook/README.md deleted file mode 100644 index c76c2936d..000000000 --- a/apps/emqx_web_hook/README.md +++ /dev/null @@ -1,194 +0,0 @@ - -# emqx-web-hook - -EMQ X WebHook plugin. - -Please see: [EMQ X - WebHook](https://docs.emqx.io/broker/latest/en/advanced/webhook.html) - -## emqx_web_hook.conf - -```properties -## The web services URL for Hook request -## -## Value: String -web.hook.url = http://127.0.0.1:8080 - -## Encode message payload field -## -## Value: base64 | base62 -## web.hook.encode_payload = base64 - -##-------------------------------------------------------------------- -## Hook Rules - -## These configuration items represent a list of events should be forwarded -## -## Format: -## web.hook.rule.. = -web.hook.rule.client.connect.1 = {"action": "on_client_connect"} -web.hook.rule.client.connack.1 = {"action": "on_client_connack"} -web.hook.rule.client.connected.1 = {"action": "on_client_connected"} -web.hook.rule.client.disconnected.1 = {"action": "on_client_disconnected"} -web.hook.rule.client.subscribe.1 = {"action": "on_client_subscribe"} -web.hook.rule.client.unsubscribe.1 = {"action": "on_client_unsubscribe"} -web.hook.rule.session.subscribed.1 = {"action": "on_session_subscribed"} -web.hook.rule.session.unsubscribed.1 = {"action": "on_session_unsubscribed"} -web.hook.rule.session.terminated.1 = {"action": "on_session_terminated"} -web.hook.rule.message.publish.1 = {"action": "on_message_publish"} -web.hook.rule.message.delivered.1 = {"action": "on_message_delivered"} -web.hook.rule.message.acked.1 = {"action": "on_message_acked"} -``` - -## API - -The HTTP request parameter format: - -* client.connected -```json -{ - "action":"client_connected", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "keepalive": 60, - "ipaddress": "127.0.0.1", - "proto_ver": 4, - "connected_at": 1556176748, - "conn_ack":0 -} -``` - -* client.disconnected -```json -{ - "action":"client_disconnected", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "reason":"normal" -} -``` - -* client.subscribe -```json -{ - "action":"client_subscribe", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "topic":"world", - "opts":{ - "qos":0 - } -} -``` - -* client.unsubscribe -```json -{ - "action":"client_unsubscribe", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "topic":"world" -} -``` - -* session.created -```json -{ - "action":"session_created", - "clientid":"C_1492410235117", - "username":"C_1492410235117" -} -``` - -* session.subscribed -```json -{ - "action":"session_subscribed", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "topic":"world", - "opts":{ - "qos":0 - } -} -``` - -* session.unsubscribed -```json -{ - "action":"session_unsubscribed", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "topic":"world" -} -``` - -* session.terminated -```json -{ - "action":"session_terminated", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "reason":"normal" -} -``` - -* message.publish -```json -{ - "action":"message_publish", - "from_client_id":"C_1492410235117", - "from_username":"C_1492410235117", - "topic":"world", - "qos":0, - "retain":true, - "payload":"Hello world!", - "ts":1492412774 -} -``` - -* message.delivered -```json -{ - "action":"message_delivered", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "from_client_id":"C_1492410235117", - "from_username":"C_1492410235117", - "topic":"world", - "qos":0, - "retain":true, - "payload":"Hello world!", - "ts":1492412826 -} -``` - -* message.acked -```json -{ - "action":"message_acked", - "clientid":"C_1492410235117", - "username":"C_1492410235117", - "from_client_id":"C_1492410235117", - "from_username":"C_1492410235117", - "topic":"world", - "qos":1, - "retain":true, - "payload":"Hello world!", - "ts":1492412914 -} -``` - -## License - -Apache License Version 2.0 - -## Author - -* [Sakib Sami](https://github.com/s4kibs4mi) - -## Contributors - -* [Deng](https://github.com/turtleDeng) -* [vishr](https://github.com/vishr) -* [emqplus](https://github.com/emqplus) -* [huangdan](https://github.com/huangdan) diff --git a/apps/emqx_web_hook/TODO b/apps/emqx_web_hook/TODO deleted file mode 100644 index 31bf5a2ad..000000000 --- a/apps/emqx_web_hook/TODO +++ /dev/null @@ -1,3 +0,0 @@ -1. HTTPS -2. More HTTP Headers and Options -3. MQTT 5.0 diff --git a/apps/emqx_web_hook/etc/emqx_web_hook.conf b/apps/emqx_web_hook/etc/emqx_web_hook.conf deleted file mode 100644 index 6707e4673..000000000 --- a/apps/emqx_web_hook/etc/emqx_web_hook.conf +++ /dev/null @@ -1,77 +0,0 @@ -##==================================================================== -## WebHook -##==================================================================== - -## Webhook URL -## -## Value: String -web.hook.url = "http://127.0.0.1:80" - -## HTTP Headers -## -## Example: -## 1. web.hook.headers.content-type = "application/json" -## 2. web.hook.headers.accept = "*" -## -## Value: String -web.hook.headers.content-type = "application/json" - -## The encoding format of the payload field in the HTTP body -## The payload field only appears in the on_message_publish and on_message_delivered actions -## -## Value: plain | base64 | base62 -web.hook.body.encoding_of_payload_field = plain - -##-------------------------------------------------------------------- -## PEM format file of CA's -## -## Value: File -## web.hook.ssl.cacertfile = - -## Certificate file to use, PEM format assumed -## -## Value: File -## web.hook.ssl.certfile = - -## Private key file to use, PEM format assumed -## -## Value: File -## web.hook.ssl.keyfile = - -## Turn on peer certificate verification -## -## Value: true | false -## web.hook.ssl.verify = false - -## If not specified, the server's names returned in server's certificate is validated against -## what's provided `web.hook.url` config's host part. -## Setting to 'disable' will make EMQ X ignore unmatched server names. -## If set with a host name, the server's names returned in server's certificate is validated -## against this value. -## -## Value: String | disable -## web.hook.ssl.server_name_indication = disable - -## Connection process pool size -## -## Value: Number -web.hook.pool_size = 32 - -##-------------------------------------------------------------------- -## Hook Rules -## These configuration items represent a list of events should be forwarded -## -## Format: -## web.hook.rule.. = -#web.hook.rule.client.connect.1 = "{"action": "on_client_connect"}" -#web.hook.rule.client.connack.1 = "{"action": "on_client_connack"}" -#web.hook.rule.client.connected.1 = "{"action": "on_client_connected"}" -#web.hook.rule.client.disconnected.1 = "{"action": "on_client_disconnected"}" -#web.hook.rule.client.subscribe.1 = "{"action": "on_client_subscribe"}" -#web.hook.rule.client.unsubscribe.1 = "{"action": "on_client_unsubscribe"}" -#web.hook.rule.session.subscribed.1 = "{"action": "on_session_subscribed"}" -#web.hook.rule.session.unsubscribed.1 = "{"action": "on_session_unsubscribed"}" -#web.hook.rule.session.terminated.1 = "{"action": "on_session_terminated"}" -#web.hook.rule.message.publish.1 = "{"action": "on_message_publish"}" -#web.hook.rule.message.delivered.1 = "{"action": "on_message_delivered"}" -#web.hook.rule.message.acked.1 = ""{"action": "on_message_acked"}" diff --git a/apps/emqx_web_hook/include/emqx_web_hook.hrl b/apps/emqx_web_hook/include/emqx_web_hook.hrl deleted file mode 100644 index 73019ec8c..000000000 --- a/apps/emqx_web_hook/include/emqx_web_hook.hrl +++ /dev/null @@ -1 +0,0 @@ --define(APP, emqx_web_hook). diff --git a/apps/emqx_web_hook/priv/emqx_web_hook.schema b/apps/emqx_web_hook/priv/emqx_web_hook.schema deleted file mode 100644 index 8ba1cc0fd..000000000 --- a/apps/emqx_web_hook/priv/emqx_web_hook.schema +++ /dev/null @@ -1,105 +0,0 @@ -%%-*- mode: erlang -*- -%% EMQ X R3.0 config mapping - -{mapping, "web.hook.url", "emqx_web_hook.url", [ - {datatype, string} -]}. - -{mapping, "web.hook.headers.$name", "emqx_web_hook.headers", [ - {datatype, string} -]}. - -{mapping, "web.hook.body.encoding_of_payload_field", "emqx_web_hook.encoding_of_payload_field", [ - {default, plain}, - {datatype, {enum, [plain, base62, base64]}} -]}. - -{mapping, "web.hook.ssl.cacertfile", "emqx_web_hook.cacertfile", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "web.hook.ssl.certfile", "emqx_web_hook.certfile", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "web.hook.ssl.keyfile", "emqx_web_hook.keyfile", [ - {default, ""}, - {datatype, string} -]}. - -{mapping, "web.hook.ssl.verify", "emqx_web_hook.verify", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "web.hook.ssl.server_name_indication", "emqx_web_hook.server_name_indication", [ - {datatype, string} -]}. - -{mapping, "web.hook.pool_size", "emqx_web_hook.pool_size", [ - {default, 32}, - {datatype, integer} -]}. - -{mapping, "web.hook.rule.client.connect.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.client.connack.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.client.connected.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.client.disconnected.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.client.subscribe.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.client.unsubscribe.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.session.subscribed.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.session.unsubscribed.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.session.terminated.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.message.publish.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.message.acked.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{mapping, "web.hook.rule.message.delivered.$name", "emqx_web_hook.rules", [ - {datatype, string} -]}. - -{translation, "emqx_web_hook.headers", fun(Conf) -> - Headers = cuttlefish_variable:filter_by_prefix("web.hook.headers", Conf), - [{K, V} || {[_, _, _, K], V} <- Headers] -end}. - -{translation, "emqx_web_hook.rules", fun(Conf) -> - Hooks = cuttlefish_variable:filter_by_prefix("web.hook.rule", Conf), - lists:map( - fun({[_, _, _,Name1,Name2, _], Val}) -> - {lists:concat([Name1,".",Name2]), Val} - end, Hooks) -end}. diff --git a/apps/emqx_web_hook/src/emqx_web_hook.app.src b/apps/emqx_web_hook/src/emqx_web_hook.app.src deleted file mode 100644 index e1cfda173..000000000 --- a/apps/emqx_web_hook/src/emqx_web_hook.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_web_hook, - [{description, "EMQ X WebHook Plugin"}, - {vsn, "4.3.2"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_web_hook_sup]}, - {applications, [kernel,stdlib,ehttpc]}, - {mod, {emqx_web_hook_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-web-hook"} - ]} - ]}. diff --git a/apps/emqx_web_hook/src/emqx_web_hook.appup.src b/apps/emqx_web_hook/src/emqx_web_hook.appup.src deleted file mode 100644 index ae6e9e1ae..000000000 --- a/apps/emqx_web_hook/src/emqx_web_hook.appup.src +++ /dev/null @@ -1,18 +0,0 @@ -%% -*-: erlang -*- - -{VSN, - [ - {<<"4.3.[0-1]">>, [ - {restart_application, emqx_web_hook}, - {apply,{emqx_rule_engine,refresh_resource,[web_hook]}} - ]}, - {<<".*">>, []} - ], - [ - {<<"4.3.[0-1]">>, [ - {restart_application, emqx_web_hook}, - {apply,{emqx_rule_engine,refresh_resource,[web_hook]}} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_web_hook/src/emqx_web_hook.erl b/apps/emqx_web_hook/src/emqx_web_hook.erl deleted file mode 100644 index 7af83d749..000000000 --- a/apps/emqx_web_hook/src/emqx_web_hook.erl +++ /dev/null @@ -1,390 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_web_hook). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --define(APP, emqx_web_hook). - --logger_header("[WebHook]"). - --import(inet, [ntoa/1]). - -%% APIs --export([ register_metrics/0 - , load/0 - , unload/0 - ]). - -%% Hooks callback --export([ on_client_connect/3 - , on_client_connack/4 - , on_client_connected/3 - , on_client_disconnected/4 - , on_client_subscribe/4 - , on_client_unsubscribe/4 - ]). - --export([ on_session_subscribed/4 - , on_session_unsubscribed/4 - , on_session_terminated/4 - ]). --export([ on_message_publish/2 - , on_message_delivered/3 - , on_message_acked/3 - ]). - -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- - -register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, - ['webhook.client_connect', - 'webhook.client_connack', - 'webhook.client_connected', - 'webhook.client_disconnected', - 'webhook.client_subscribe', - 'webhook.client_unsubscribe', - 'webhook.session_subscribed', - 'webhook.session_unsubscribed', - 'webhook.session_terminated', - 'webhook.message_publish', - 'webhook.message_delivered', - 'webhook.message_acked']). - -load() -> - lists:foreach( - fun({Hook, Fun, Filter}) -> - emqx:hook(Hook, {?MODULE, Fun, [{Filter}]}) - end, parse_rule(application:get_env(?APP, rules, []))). - -unload() -> - lists:foreach( - fun({Hook, Fun, _Filter}) -> - emqx:unhook(Hook, {?MODULE, Fun}) - end, parse_rule(application:get_env(?APP, rules, []))). - -%%-------------------------------------------------------------------- -%% Client connect -%%-------------------------------------------------------------------- - -on_client_connect(ConnInfo = #{clientid := ClientId, username := Username, peername := {Peerhost, _}}, _ConnProp, _Env) -> - emqx_metrics:inc('webhook.client_connect'), - Params = #{ action => client_connect - , node => node() - , clientid => ClientId - , username => maybe(Username) - , ipaddress => iolist_to_binary(ntoa(Peerhost)) - , keepalive => maps:get(keepalive, ConnInfo) - , proto_ver => maps:get(proto_ver, ConnInfo) - }, - send_http_request(ClientId, Params). - -%%-------------------------------------------------------------------- -%% Client connack -%%-------------------------------------------------------------------- - -on_client_connack(ConnInfo = #{clientid := ClientId, username := Username, peername := {Peerhost, _}}, Rc, _AckProp, _Env) -> - emqx_metrics:inc('webhook.client_connack'), - Params = #{ action => client_connack - , node => node() - , clientid => ClientId - , username => maybe(Username) - , ipaddress => iolist_to_binary(ntoa(Peerhost)) - , keepalive => maps:get(keepalive, ConnInfo) - , proto_ver => maps:get(proto_ver, ConnInfo) - , conn_ack => Rc - }, - send_http_request(ClientId, Params). - -%%-------------------------------------------------------------------- -%% Client connected -%%-------------------------------------------------------------------- - -on_client_connected(#{clientid := ClientId, username := Username, peerhost := Peerhost}, ConnInfo, _Env) -> - emqx_metrics:inc('webhook.client_connected'), - Params = #{ action => client_connected - , node => node() - , clientid => ClientId - , username => maybe(Username) - , ipaddress => iolist_to_binary(ntoa(Peerhost)) - , keepalive => maps:get(keepalive, ConnInfo) - , proto_ver => maps:get(proto_ver, ConnInfo) - , connected_at => maps:get(connected_at, ConnInfo) - }, - send_http_request(ClientId, Params). - -%%-------------------------------------------------------------------- -%% Client disconnected -%%-------------------------------------------------------------------- - -on_client_disconnected(ClientInfo, {shutdown, Reason}, ConnInfo, Env) when is_atom(Reason) -> - on_client_disconnected(ClientInfo, Reason, ConnInfo, Env); -on_client_disconnected(#{clientid := ClientId, username := Username}, Reason, ConnInfo, _Env) -> - emqx_metrics:inc('webhook.client_disconnected'), - Params = #{ action => client_disconnected - , node => node() - , clientid => ClientId - , username => maybe(Username) - , reason => stringfy(maybe(Reason)) - , disconnected_at => maps:get(disconnected_at, ConnInfo, erlang:system_time(millisecond)) - }, - send_http_request(ClientId, Params). - -%%-------------------------------------------------------------------- -%% Client subscribe -%%-------------------------------------------------------------------- - -on_client_subscribe(#{clientid := ClientId, username := Username}, _Properties, TopicTable, {Filter}) -> - lists:foreach(fun({Topic, Opts}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.client_subscribe'), - Params = #{ action => client_subscribe - , node => node() - , clientid => ClientId - , username => maybe(Username) - , topic => Topic - , opts => Opts - }, - send_http_request(ClientId, Params) - end, Topic, Filter) - end, TopicTable). - -%%-------------------------------------------------------------------- -%% Client unsubscribe -%%-------------------------------------------------------------------- - -on_client_unsubscribe(#{clientid := ClientId, username := Username}, _Properties, TopicTable, {Filter}) -> - lists:foreach(fun({Topic, Opts}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.client_unsubscribe'), - Params = #{ action => client_unsubscribe - , node => node() - , clientid => ClientId - , username => maybe(Username) - , topic => Topic - , opts => Opts - }, - send_http_request(ClientId, Params) - end, Topic, Filter) - end, TopicTable). - -%%-------------------------------------------------------------------- -%% Session subscribed -%%-------------------------------------------------------------------- - -on_session_subscribed(#{clientid := ClientId, username := Username}, Topic, Opts, {Filter}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.session_subscribed'), - Params = #{ action => session_subscribed - , node => node() - , clientid => ClientId - , username => maybe(Username) - , topic => Topic - , opts => Opts - }, - send_http_request(ClientId, Params) - end, Topic, Filter). - -%%-------------------------------------------------------------------- -%% Session unsubscribed -%%-------------------------------------------------------------------- - -on_session_unsubscribed(#{clientid := ClientId, username := Username}, Topic, _Opts, {Filter}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.session_unsubscribed'), - Params = #{ action => session_unsubscribed - , node => node() - , clientid => ClientId - , username => maybe(Username) - , topic => Topic - }, - send_http_request(ClientId, Params) - end, Topic, Filter). - -%%-------------------------------------------------------------------- -%% Session terminated -%%-------------------------------------------------------------------- - -on_session_terminated(Info, {shutdown, Reason}, SessInfo, Env) when is_atom(Reason) -> - on_session_terminated(Info, Reason, SessInfo, Env); -on_session_terminated(#{clientid := ClientId, username := Username}, Reason, _SessInfo, _Env) -> - emqx_metrics:inc('webhook.session_terminated'), - Params = #{ action => session_terminated - , node => node() - , clientid => ClientId - , username => maybe(Username) - , reason => stringfy(maybe(Reason)) - }, - send_http_request(ClientId, Params). - -%%-------------------------------------------------------------------- -%% Message publish -%%-------------------------------------------------------------------- - -on_message_publish(Message = #message{topic = <<"$SYS/", _/binary>>}, _Env) -> - {ok, Message}; -on_message_publish(Message = #message{topic = Topic}, {Filter}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.message_publish'), - {FromClientId, FromUsername} = parse_from(Message), - Params = #{ action => message_publish - , node => node() - , from_client_id => FromClientId - , from_username => FromUsername - , topic => Message#message.topic - , qos => Message#message.qos - , retain => emqx_message:get_flag(retain, Message) - , payload => encode_payload(Message#message.payload) - , ts => Message#message.timestamp - }, - send_http_request(FromClientId, Params), - {ok, Message} - end, Message, Topic, Filter). - -%%-------------------------------------------------------------------- -%% Message deliver -%%-------------------------------------------------------------------- - -on_message_delivered(_ClientInfo,#message{topic = <<"$SYS/", _/binary>>}, _Env) -> - ok; -on_message_delivered(#{clientid := ClientId, username := Username}, - Message = #message{topic = Topic}, {Filter}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.message_delivered'), - {FromClientId, FromUsername} = parse_from(Message), - Params = #{ action => message_delivered - , node => node() - , clientid => ClientId - , username => maybe(Username) - , from_client_id => FromClientId - , from_username => FromUsername - , topic => Message#message.topic - , qos => Message#message.qos - , retain => emqx_message:get_flag(retain, Message) - , payload => encode_payload(Message#message.payload) - , ts => Message#message.timestamp - }, - send_http_request(ClientId, Params) - end, Topic, Filter). - -%%-------------------------------------------------------------------- -%% Message acked -%%-------------------------------------------------------------------- - -on_message_acked(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}, _Env) -> - ok; -on_message_acked(#{clientid := ClientId, username := Username}, - Message = #message{topic = Topic}, {Filter}) -> - with_filter( - fun() -> - emqx_metrics:inc('webhook.message_acked'), - {FromClientId, FromUsername} = parse_from(Message), - Params = #{ action => message_acked - , node => node() - , clientid => ClientId - , username => maybe(Username) - , from_client_id => FromClientId - , from_username => FromUsername - , topic => Message#message.topic - , qos => Message#message.qos - , retain => emqx_message:get_flag(retain, Message) - , payload => encode_payload(Message#message.payload) - , ts => Message#message.timestamp - }, - send_http_request(ClientId, Params) - end, Topic, Filter). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -send_http_request(ClientID, Params) -> - {ok, Path} = application:get_env(?APP, path), - Headers = application:get_env(?APP, headers, []), - Body = emqx_json:encode(Params), - ?LOG(debug, "Send to: ~0p, params: ~s", [Path, Body]), - case ehttpc:request(ehttpc_pool:pick_worker(?APP, ClientID), post, {Path, Headers, Body}) of - {ok, StatusCode, _} when StatusCode >= 200 andalso StatusCode < 300 -> - ok; - {ok, StatusCode, _, _} when StatusCode >= 200 andalso StatusCode < 300 -> - ok; - {ok, StatusCode, _} -> - ?LOG(warning, "HTTP request failed with status code: ~p", [StatusCode]), - ok; - {ok, StatusCode, _, _} -> - ?LOG(warning, "HTTP request failed with status code: ~p", [StatusCode]), - ok; - {error, Reason} -> - ?LOG(error, "HTTP request error: ~p", [Reason]), - ok - end. - -parse_rule(Rules) -> - parse_rule(Rules, []). -parse_rule([], Acc) -> - lists:reverse(Acc); -parse_rule([{Rule, Conf} | Rules], Acc) -> - Params = emqx_json:decode(iolist_to_binary(Conf)), - Action = proplists:get_value(<<"action">>, Params), - Filter = proplists:get_value(<<"topic">>, Params), - parse_rule(Rules, [{list_to_atom(Rule), binary_to_existing_atom(Action, utf8), Filter} | Acc]). - -with_filter(Fun, _, undefined) -> - Fun(), ok; -with_filter(Fun, Topic, Filter) -> - case emqx_topic:match(Topic, Filter) of - true -> Fun(), ok; - false -> ok - end. - -with_filter(Fun, _, _, undefined) -> - Fun(); -with_filter(Fun, Msg, Topic, Filter) -> - case emqx_topic:match(Topic, Filter) of - true -> Fun(); - false -> {ok, Msg} - end. - -parse_from(Message) -> - {emqx_message:from(Message), maybe(emqx_message:get_header(username, Message))}. - -encode_payload(Payload) -> - encode_payload(Payload, application:get_env(?APP, encoding_of_payload_field, plain)). - -encode_payload(Payload, base62) -> emqx_base62:encode(Payload); -encode_payload(Payload, base64) -> base64:encode(Payload); -encode_payload(Payload, plain) -> Payload. - -stringfy(Term) when is_binary(Term) -> - Term; -stringfy(Term) when is_atom(Term) -> - atom_to_binary(Term, utf8); -stringfy(Term) -> - unicode:characters_to_binary((io_lib:format("~0p", [Term]))). - -maybe(undefined) -> null; -maybe(Str) -> Str. - diff --git a/apps/emqx_web_hook/src/emqx_web_hook_app.erl b/apps/emqx_web_hook/src/emqx_web_hook_app.erl deleted file mode 100644 index 580742c47..000000000 --- a/apps/emqx_web_hook/src/emqx_web_hook_app.erl +++ /dev/null @@ -1,102 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_web_hook_app). - --behaviour(application). - --emqx_plugin(?MODULE). - --include("emqx_web_hook.hrl"). - --export([ start/2 - , stop/1 - ]). - -start(_StartType, _StartArgs) -> - translate_env(), - {ok, Sup} = emqx_web_hook_sup:start_link(), - {ok, PoolOpts} = application:get_env(?APP, pool_opts), - {ok, _Pid} = ehttpc_sup:start_pool(?APP, PoolOpts), - emqx_web_hook:register_metrics(), - emqx_web_hook:load(), - {ok, Sup}. - -stop(_State) -> - emqx_web_hook:unload(), - ehttpc_sup:stop_pool(?APP). - -translate_env() -> - {ok, URL} = application:get_env(?APP, url), - {ok, #{host := Host, - port := Port, - scheme := Scheme} = URIMap} = emqx_http_lib:uri_parse(URL), - Path = path(URIMap), - PoolSize = application:get_env(?APP, pool_size, 32), - MoreOpts = case Scheme of - http -> - [{transport_opts, emqx_misc:ipv6_probe([])}]; - https -> - CACertFile = application:get_env(?APP, cacertfile, undefined), - CertFile = application:get_env(?APP, certfile, undefined), - KeyFile = application:get_env(?APP, keyfile, undefined), - {ok, Verify} = application:get_env(?APP, verify), - VerifyType = case Verify of - true -> verify_peer; - false -> verify_none - end, - SNI = case application:get_env(?APP, server_name_indication, undefined) of - "disable" -> disable; - SNI0 -> SNI0 - end, - TLSOpts = lists:filter(fun({_K, V}) -> - V /= <<>> andalso V /= undefined andalso V /= "" andalso true - end, [{keyfile, KeyFile}, - {certfile, CertFile}, - {cacertfile, CACertFile}, - {verify, VerifyType}, - {server_name_indication, SNI}]), - NTLSOpts = [ {versions, emqx_tls_lib:default_versions()} - , {ciphers, emqx_tls_lib:default_ciphers()} - | TLSOpts - ], - [{transport, ssl}, {transport_opts, emqx_misc:ipv6_probe(NTLSOpts)}] - end, - PoolOpts = [{host, Host}, - {port, Port}, - {pool_size, PoolSize}, - {pool_type, hash}, - {connect_timeout, 5000}, - {retry, 5}, - {retry_timeout, 1000}] ++ MoreOpts, - application:set_env(?APP, path, Path), - application:set_env(?APP, pool_opts, PoolOpts), - Headers = application:get_env(?APP, headers, []), - NHeaders = set_content_type(Headers), - application:set_env(?APP, headers, NHeaders). - -path(#{path := "", 'query' := Query}) -> - "?" ++ Query; -path(#{path := Path, 'query' := Query}) -> - Path ++ "?" ++ Query; -path(#{path := ""}) -> - "/"; -path(#{path := Path}) -> - Path. - -set_content_type(Headers) -> - NHeaders = proplists:delete(<<"Content-Type">>, proplists:delete(<<"content-type">>, Headers)), - [{<<"content-type">>, <<"application/json">>} | NHeaders]. diff --git a/apps/emqx_web_hook/src/emqx_web_hook_sup.erl b/apps/emqx_web_hook/src/emqx_web_hook_sup.erl deleted file mode 100644 index ec46efaa0..000000000 --- a/apps/emqx_web_hook/src/emqx_web_hook_sup.erl +++ /dev/null @@ -1,29 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_web_hook_sup). - --behaviour(supervisor). - --export([start_link/0]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - {ok, {{one_for_all, 0, 1}, []}}. diff --git a/apps/emqx_web_hook/test/emqx_web_hook_SUITE.erl b/apps/emqx_web_hook/test/emqx_web_hook_SUITE.erl deleted file mode 100644 index 864e1b150..000000000 --- a/apps/emqx_web_hook/test/emqx_web_hook_SUITE.erl +++ /dev/null @@ -1,284 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_web_hook_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --define(HOOK_LOOKUP(H), emqx_hooks:lookup(list_to_atom(H))). --define(ACTION(Name), #{<<"action">> := Name}). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> - [ {group, http} - , {group, https} - , {group, ipv6http} - , {group, ipv6https} - , {group, all} - ]. - -groups() -> - Cases = [test_full_flow], - [ {http, [sequence], Cases} - , {https, [sequence], Cases} - , {ipv6http, [sequence], Cases} - , {ipv6https, [sequence], Cases} - , {all, [sequence], emqx_ct:all(?MODULE)} - ]. - -start_apps(F) -> emqx_ct_helpers:start_apps(apps(), F). - -init_per_group(Name, Config) -> - application:ensure_all_started(emqx_management), - set_special_cfgs(), - BasePort = - case Name of - all -> 8801; - http -> 8811; - https -> 8821; - ipv6http -> 8831; - ipv6https -> 8841 - end, - CF = case Name of - all -> fun set_special_configs_http/1; - http -> fun set_special_configs_http/1; - https -> fun set_special_configs_https/1; - ipv6http -> fun set_special_configs_ipv6_http/1; - ipv6https -> fun set_special_configs_ipv6_https/1 - end, - start_apps(fun(_) -> CF(BasePort) end), - Opts = case atom_to_list(Name) of - "ipv6" ++ _ -> [{ip, {0,0,0,0,0,0,0,1}}, inet6]; - _ -> [inet] - end, - [{base_port, BasePort}, {transport_opts, Opts} | Config]. - -end_per_group(_Name, Config) -> - emqx_ct_helpers:stop_apps(apps()), - Config. - -set_special_configs_http(Port) -> - application:set_env(emqx_web_hook, url, "http://127.0.0.1:" ++ integer_to_list(Port)). - -set_special_configs_https(Port) -> - set_ssl_configs(), - application:set_env(emqx_web_hook, url, "https://127.0.0.1:" ++ integer_to_list(Port+1)). - -set_special_configs_ipv6_http(Port) -> - application:set_env(emqx_web_hook, url, "http://[::1]:" ++ integer_to_list(Port)). - -set_special_configs_ipv6_https(Port) -> - set_ssl_configs(), - application:set_env(emqx_web_hook, url, "https://[::1]:" ++ integer_to_list(Port+1)). - -set_ssl_configs() -> - Path = emqx_ct_helpers:deps_path(emqx_web_hook, "test/emqx_web_hook_SUITE_data/"), - SslOpts = [{keyfile, Path ++ "/client-key.pem"}, - {certfile, Path ++ "/client-cert.pem"}, - {cacertfile, Path ++ "/ca.pem"}], - application:set_env(emqx_web_hook, ssl, true), - application:set_env(emqx_web_hook, ssloptions, SslOpts). - -set_special_cfgs() -> - AllRules = [{"message.acked", "{\"action\": \"on_message_acked\"}"}, - {"message.delivered", "{\"action\": \"on_message_delivered\"}"}, - {"message.publish", "{\"action\": \"on_message_publish\"}"}, - {"session.terminated", "{\"action\": \"on_session_terminated\"}"}, - {"session.unsubscribed", "{\"action\": \"on_session_unsubscribed\"}"}, - {"session.subscribed", "{\"action\": \"on_session_subscribed\"}"}, - {"client.unsubscribe", "{\"action\": \"on_client_unsubscribe\"}"}, - {"client.subscribe", "{\"action\": \"on_client_subscribe\"}"}, - {"client.disconnected", "{\"action\": \"on_client_disconnected\"}"}, - {"client.connected", "{\"action\": \"on_client_connected\"}"}, - {"client.connack", "{\"action\": \"on_client_connack\"}"}, - {"client.connect", "{\"action\": \"on_client_connect\"}"}], - application:set_env(emqx_web_hook, rules, AllRules). - -%%-------------------------------------------------------------------- -%% Test cases -%%-------------------------------------------------------------------- - -test_full_flow(Config) -> - [_|_] = Opts = proplists:get_value(transport_opts, Config), - BasePort = proplists:get_value(base_port, Config), - Tester = self(), - {ok, ServerPid} = http_server:start_link(Tester, BasePort, Opts), - receive {ServerPid, ready} -> ok - after 1000 -> error(timeout) - end, - application:set_env(emqx_web_hook, headers, [{"k1","K1"}, {"k2", "K2"}]), - ClientId = iolist_to_binary(["client-", integer_to_list(erlang:system_time())]), - {ok, C} = emqtt:start_link([ {clientid, ClientId} - , {proto_ver, v5} - , {keepalive, 60} - ]), - try - do_test_full_flow(C, ClientId) - after - Ref = erlang:monitor(process, ServerPid), - http_server:stop(ServerPid), - receive {'DOWN', Ref, _, _, _} -> ok - after 5000 -> error(timeout) - end - end. - -do_test_full_flow(C, ClientId) -> - {ok, _} = emqtt:connect(C), - {ok, _, _} = emqtt:subscribe(C, <<"TopicA">>, qos2), - {ok, _} = emqtt:publish(C, <<"TopicA">>, <<"Payload...">>, qos2), - {ok, _, _} = emqtt:unsubscribe(C, <<"TopicA">>), - emqtt:disconnect(C), - validate_params_and_headers(undefined, ClientId). - -validate_params_and_headers(ClientState, ClientId) -> - receive - {http_server, {Params0, _Bool}, Headers} -> - Params = emqx_json:decode(Params0, [return_maps]), - try - validate_hook_resp(ClientId, Params), - validate_hook_headers(Headers), - case maps:get(<<"action">>, Params) of - <<"session_terminated">> -> - ok; - <<"client_connect">> -> - validate_params_and_headers(connected, ClientId); - _ -> - validate_params_and_headers(ClientState, ClientId) %% continue looping - end - catch - throw : {unknown_client, Other} -> - ct:pal("ignored_event_from_other_client ~p~nexpecting:~p~n~p~n~p", - [Other, ClientId, Params, Headers]), - validate_params_and_headers(ClientState, ClientId) %% continue looping - end - after - 5000 -> - case ClientState =:= undefined of - true -> error("client_was_never_connected"); - false -> error("terminate_action_is_not_received_in_time") - end - end. - -t_check_hooked(_) -> - {ok, Rules} = application:get_env(emqx_web_hook, rules), - lists:foreach(fun({HookName, _Action}) -> - Hooks = ?HOOK_LOOKUP(HookName), - ?assertEqual(true, length(Hooks) > 0) - end, Rules). - -t_change_config(_) -> - {ok, Rules} = application:get_env(emqx_web_hook, rules), - emqx_web_hook:unload(), - HookRules = lists:keydelete("message.delivered", 1, Rules), - application:set_env(emqx_web_hook, rules, HookRules), - emqx_web_hook:load(), - ?assertEqual([], ?HOOK_LOOKUP("message.delivered")), - emqx_web_hook:unload(), - application:set_env(emqx_web_hook, rules, Rules), - emqx_web_hook:load(). - -%%-------------------------------------------------------------------- -%% Utils -%%-------------------------------------------------------------------- - -validate_hook_headers(Headers) -> - ?assertEqual(<<"K1">>, maps:get(<<"k1">>, Headers)), - ?assertEqual(<<"K2">>, maps:get(<<"k2">>, Headers)). - -validate_hook_resp(ClientId, Body = ?ACTION(<<"client_connect">>)) -> - assert_username_clientid(ClientId, Body), - ?assertEqual(5, maps:get(<<"proto_ver">>, Body)), - ?assertEqual(60, maps:get(<<"keepalive">>, Body)), - ?assertEqual(<<"127.0.0.1">>, maps:get(<<"ipaddress">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)), - ok; -validate_hook_resp(ClientId, Body = ?ACTION(<<"client_connack">>)) -> - assert_username_clientid(ClientId, Body), - ?assertEqual(5, maps:get(<<"proto_ver">>, Body)), - ?assertEqual(60, maps:get(<<"keepalive">>, Body)), - ?assertEqual(<<"success">>, maps:get(<<"conn_ack">>, Body)), - ?assertEqual(<<"127.0.0.1">>, maps:get(<<"ipaddress">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)), - ok; -validate_hook_resp(ClientId, Body = ?ACTION(<<"client_connected">>)) -> - assert_username_clientid(ClientId, Body), - _ = maps:get(<<"connected_at">>, Body), - ?assertEqual(5, maps:get(<<"proto_ver">>, Body)), - ?assertEqual(60, maps:get(<<"keepalive">>, Body)), - ?assertEqual(<<"127.0.0.1">>, maps:get(<<"ipaddress">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(ClientId, Body = ?ACTION(<<"client_disconnected">>)) -> - assert_username_clientid(ClientId, Body), - ?assertEqual(<<"normal">>, maps:get(<<"reason">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(ClientId, Body = ?ACTION(<<"client_subscribe">>)) -> - assert_username_clientid(ClientId, Body), - _ = maps:get(<<"opts">>, Body), - ?assertEqual(<<"TopicA">>, maps:get(<<"topic">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(ClientId, Body = ?ACTION(<<"client_unsubscribe">>)) -> - assert_username_clientid(ClientId, Body), - _ = maps:get(<<"opts">>, Body), - ?assertEqual(<<"TopicA">>, maps:get(<<"topic">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(ClientId, Body = ?ACTION(<<"session_subscribed">>)) -> - assert_username_clientid(ClientId, Body), - _ = maps:get(<<"opts">>, Body), - ?assertEqual(<<"TopicA">>, maps:get(<<"topic">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(ClientId, Body = ?ACTION(<<"session_unsubscribed">>)) -> - assert_username_clientid(ClientId, Body), - ?assertEqual(<<"TopicA">>, maps:get(<<"topic">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(ClientId, Body = ?ACTION(<<"session_terminated">>)) -> - assert_username_clientid(ClientId, Body), - ?assertEqual(<<"normal">>, maps:get(<<"reason">>, Body)), - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)); -validate_hook_resp(_ClientId, Body = ?ACTION(<<"message_publish">>)) -> - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)), - assert_messages_attrs(Body); -validate_hook_resp(_ClientId, Body = ?ACTION(<<"message_delivered">>)) -> - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)), - assert_messages_attrs(Body); -validate_hook_resp(_ClientId, Body = ?ACTION(<<"message_acked">>)) -> - ?assertEqual(<<"test@127.0.0.1">>, maps:get(<<"node">>, Body)), - assert_messages_attrs(Body). - -assert_username_clientid(ClientId, #{<<"clientid">> := ClientId, <<"username">> := Username}) -> - ?assertEqual(null, Username); -assert_username_clientid(_ClientId, #{<<"clientid">> := Other}) -> - throw({unknown_client, Other}). - -assert_messages_attrs(#{ <<"ts">> := _ - , <<"qos">> := _ - , <<"topic">> := _ - , <<"retain">> := _ - , <<"payload">> := _ - , <<"from_username">> := _ - , <<"from_client_id">> := _ - }) -> - ok. - -apps() -> - [emqx_web_hook, emqx_modules, emqx_management, emqx_rule_engine]. diff --git a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/ca.pem b/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/ca.pem deleted file mode 100644 index 00b31d8a4..000000000 --- a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/ca.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s -KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1 -JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE -ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK -9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT -sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA -AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp -GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay -Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef -rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N -SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg -o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65 -tNPx3CL7GA== ------END CERTIFICATE----- diff --git a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-cert.pem b/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-cert.pem deleted file mode 100644 index aad1404ca..000000000 --- a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv -EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw -sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8 -3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh -Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe -CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH -Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn -g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP -IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm -RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39 -ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r -O9EkaPcgYH8= ------END CERTIFICATE----- diff --git a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-key.pem b/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-key.pem deleted file mode 100644 index 6789d0291..000000000 --- a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI -EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF -vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96 -iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC -7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR -49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y -WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6 -GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd -Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj -CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8 -jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S -S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo -ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy -gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi -zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/ -jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj -EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB -xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi -OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP -S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4 -LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t -i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs -kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO -q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk -SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-cert.pem b/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-cert.pem deleted file mode 100644 index a2f9688df..000000000 --- a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR -TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X -DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf -U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua -NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z -G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL -JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB -4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy -TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9 -AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6 -zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI -hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F -sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD -3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR -+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC -dN/klu446fI= ------END CERTIFICATE----- diff --git a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-key.pem b/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-key.pem deleted file mode 100644 index a1dfd5f78..000000000 --- a/apps/emqx_web_hook/test/emqx_web_hook_SUITE_data/server-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj -U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho -XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT -29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX -NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv -f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn -WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP -PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV -4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS -VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk -Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb -SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq -EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx -VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH -cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0 -ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h -J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ -h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K -eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq -dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD -PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes -Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2 -/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH -PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd -JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI ------END RSA PRIVATE KEY----- diff --git a/apps/emqx_web_hook/test/http_server.erl b/apps/emqx_web_hook/test/http_server.erl deleted file mode 100644 index 791f725d1..000000000 --- a/apps/emqx_web_hook/test/http_server.erl +++ /dev/null @@ -1,102 +0,0 @@ -%%-------------------------------------------------------------------- -%% A Simple HTTP Server based cowboy -%% -%% It will deliver the http-request params to initialer process -%%-------------------------------------------------------------------- -%% -%% Author:wwhai -%% --module(http_server). --behaviour(gen_server). - --export([start_link/3]). --export([stop/1]). --export([code_change/3, handle_call/3, handle_cast/2, handle_info/2, init/1, init/2, terminate/2]). --record(state, {parent :: pid()}). -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- - -start_link(Parent, BasePort, Opts) -> - stop_http(), - stop_https(), - timer:sleep(100), - gen_server:start_link(?MODULE, {Parent, BasePort, Opts}, []). - -init({Parent, BasePort, Opts}) -> - ok = start_http(Parent, [{port, BasePort} | Opts]), - ok = start_https(Parent, [{port, BasePort + 1} | Opts]), - Parent ! {self(), ready}, - {ok, #state{parent = Parent}}. - -handle_call(_Request, _From, State) -> - {reply, ignored, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - stop_http(), - stop_https(). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -stop(Pid) -> - ok = gen_server:stop(Pid). - -%%-------------------------------------------------------------------- -%% Callbacks -%%-------------------------------------------------------------------- - -start_http(Parent, Opts) -> - {ok, _Pid1} = cowboy:start_clear(http, Opts, #{ - env => #{dispatch => compile_router(Parent)} - }), - Port = proplists:get_value(port, Opts), - io:format(standard_error, "[TEST LOG] Start http server on ~p successfully!~n", [Port]). - -start_https(Parent, Opts) -> - Path = emqx_ct_helpers:deps_path(emqx_web_hook, "test/emqx_web_hook_SUITE_data/"), - SslOpts = [{keyfile, Path ++ "/server-key.pem"}, - {cacertfile, Path ++ "/ca.pem"}, - {certfile, Path ++ "/server-cert.pem"}], - - {ok, _Pid2} = cowboy:start_tls(https, Opts ++ SslOpts, - #{env => #{dispatch => compile_router(Parent)}}), - Port = proplists:get_value(port, Opts), - io:format(standard_error, "[TEST LOG] Start https server on ~p successfully!~n", [Port]). - -stop_http() -> - cowboy:stop_listener(http), - io:format("[TEST LOG] Stopped http server"). - -stop_https() -> - cowboy:stop_listener(https), - io:format("[TEST LOG] Stopped https server"). - -compile_router(Parent) -> - {ok, _} = application:ensure_all_started(cowboy), - cowboy_router:compile([ - {'_', [{"/", ?MODULE, #{parent => Parent}}]} - ]). - -init(Req, #{parent := Parent} = State) -> - Method = cowboy_req:method(Req), - Headers = cowboy_req:headers(Req), - [Params] = case Method of - <<"GET">> -> cowboy_req:parse_qs(Req); - <<"POST">> -> - {ok, PostVals, _} = cowboy_req:read_urlencoded_body(Req), - PostVals - end, - Parent ! {?MODULE, Params, Headers}, - {ok, reply(Req, ok), State}. - -reply(Req, ok) -> - cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, <<"ok">>, Req); -reply(Req, error) -> - cowboy_req:reply(404, #{<<"content-type">> => <<"text/plain">>}, <<"deny">>, Req). diff --git a/apps/emqx_web_hook/test/props/prop_webhook_confs.erl b/apps/emqx_web_hook/test/props/prop_webhook_confs.erl deleted file mode 100644 index 8946ce1d2..000000000 --- a/apps/emqx_web_hook/test/props/prop_webhook_confs.erl +++ /dev/null @@ -1,146 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(prop_webhook_confs). --include_lib("proper/include/proper.hrl"). - --import(emqx_ct_proper_types, - [ url/0 - , nof/1 - ]). - --define(ALL(Vars, Types, Exprs), - ?SETUP(fun() -> - State = do_setup(), - fun() -> do_teardown(State) end - end, ?FORALL(Vars, Types, Exprs))). - -%%-------------------------------------------------------------------- -%% Properties -%%-------------------------------------------------------------------- - -prop_confs() -> - Schema = cuttlefish_schema:files(filelib:wildcard(code:priv_dir(emqx_web_hook) ++ "/*.schema")), - ?ALL({Url, Confs0}, {url(), confs()}, - begin - Confs = [{"web.hook.url", Url}|Confs0], - Envs = cuttlefish_generator:map(Schema, cuttlefish_conf_file(Confs)), - - assert_confs(Confs, Envs), - - set_application_envs(Envs), - {ok, _} = application:ensure_all_started(emqx_web_hook), - application:stop(emqx_web_hook), - unset_application_envs(Envs), - true - end). - -%%-------------------------------------------------------------------- -%% Helpers -%%-------------------------------------------------------------------- - -do_setup() -> - logger:set_primary_config(#{level => warning}), - emqx_ct_helpers:start_apps([], fun set_special_cfgs/1), - ok. - -do_teardown(_) -> - emqx_ct_helpers:stop_apps([]), - logger:set_primary_config(#{level => info}), - ok. - -set_special_cfgs(_) -> - application:set_env(emqx, plugins_loaded_file, undefined), - application:set_env(emqx, modules_loaded_file, undefined), - ok. - -assert_confs([{"web.hook.url", Url}|More], Envs) -> - %% Assert! - Url = deep_get_env("emqx_web_hook.url", Envs), - assert_confs(More, Envs); - -assert_confs([{"web.hook.rule." ++ HookName0, Spec}|More], Envs) -> - HookName = re:replace(HookName0, "\\.[0-9]", "", [{return, list}]), - Rules = deep_get_env("emqx_web_hook.rules", Envs), - - %% Assert! - Spec = proplists:get_value(HookName, Rules), - - assert_confs(More, Envs); - -assert_confs([_|More], Envs) -> - assert_confs(More, Envs); - -assert_confs([], _) -> - true. - -deep_get_env(Path, Envs) -> - lists:foldl( - fun(_K, undefiend) -> undefiend; - (K, Acc) -> proplists:get_value(binary_to_atom(K, utf8), Acc) - end, Envs, re:split(Path, "\\.")). - -set_application_envs(Envs) -> - application:set_env(Envs). - -unset_application_envs(Envs) -> - lists:foreach(fun({App, Es}) -> - lists:foreach(fun({K, _}) -> - application:unset_env(App, K) - end, Es) end, Envs). - -cuttlefish_conf_file(Ls) when is_list(Ls) -> - [cuttlefish_conf_option(K,V) || {K, V} <- Ls]. - -cuttlefish_conf_option(K, V) - when is_list(K) -> - {re:split(K, "[.]", [{return, list}]), V}. - -%%-------------------------------------------------------------------- -%% Generators -%%-------------------------------------------------------------------- - -confs() -> - nof([{"web.hook.headers.content-type", - oneof(["application/json"])}, - {"web.hook.body.encoding_of_payload_field", - oneof(["plain", "base64", "base62"])}, - {"web.hook.rule.client.connect.1", rule_spec()}, - {"web.hook.rule.client.connack.1", rule_spec()}, - {"web.hook.rule.client.connected.1", rule_spec()}, - {"web.hook.rule.client.disconnected.1", rule_spec()}, - {"web.hook.rule.client.subscribe.1", rule_spec()}, - {"web.hook.rule.client.unsubscribe.1", rule_spec()}, - {"web.hook.rule.session.subscribed.1", rule_spec()}, - {"web.hook.rule.session.unsubscribed.1", rule_spec()}, - {"web.hook.rule.session.terminated.1", rule_spec()}, - {"web.hook.rule.message.publish.1", rule_spec()}, - {"web.hook.rule.message.delivered.1", rule_spec()}, - {"web.hook.rule.message.acked.1", rule_spec()} - ]). - -rule_spec() -> - ?LET(Action, action_names(), - begin - binary_to_list(emqx_json:encode(#{action => Action})) - end). - -action_names() -> - oneof([on_client_connect, on_client_connack, on_client_connected, - on_client_connected, on_client_disconnected, on_client_subscribe, on_client_unsubscribe, - on_session_subscribed, on_session_unsubscribed, on_session_terminated, - on_message_publish, on_message_delivered, on_message_acked]). - diff --git a/apps/emqx_web_hook/test/props/prop_webhook_hooks.erl b/apps/emqx_web_hook/test/props/prop_webhook_hooks.erl deleted file mode 100644 index 311585287..000000000 --- a/apps/emqx_web_hook/test/props/prop_webhook_hooks.erl +++ /dev/null @@ -1,397 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(prop_webhook_hooks). - --include_lib("proper/include/proper.hrl"). - --import(emqx_ct_proper_types, - [ conninfo/0 - , clientinfo/0 - , sessioninfo/0 - , message/0 - , connack_return_code/0 - , topictab/0 - , topic/0 - , subopts/0 - ]). - --define(ALL(Vars, Types, Exprs), - ?SETUP(fun() -> - State = do_setup(), - fun() -> do_teardown(State) end - end, ?FORALL(Vars, Types, Exprs))). - -%%-------------------------------------------------------------------- -%% Properties -%%-------------------------------------------------------------------- - -prop_client_connect() -> - ?ALL({ConnInfo, ConnProps, Env}, - {conninfo(), conn_properties(), empty_env()}, - begin - ok = emqx_web_hook:on_client_connect(ConnInfo, ConnProps, Env), - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => client_connect, - node => stringfy(node()), - clientid => maps:get(clientid, ConnInfo), - username => maybe(maps:get(username, ConnInfo)), - ipaddress => peer2addr(maps:get(peername, ConnInfo)), - keepalive => maps:get(keepalive, ConnInfo), - proto_ver => maps:get(proto_ver, ConnInfo) - }), - true - end). - -prop_client_connack() -> - ?ALL({ConnInfo, Rc, AckProps, Env}, - {conninfo(), connack_return_code(), ack_properties(), empty_env()}, - begin - ok = emqx_web_hook:on_client_connack(ConnInfo, Rc, AckProps, Env), - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => client_connack, - node => stringfy(node()), - clientid => maps:get(clientid, ConnInfo), - username => maybe(maps:get(username, ConnInfo)), - ipaddress => peer2addr(maps:get(peername, ConnInfo)), - keepalive => maps:get(keepalive, ConnInfo), - proto_ver => maps:get(proto_ver, ConnInfo), - conn_ack => Rc - }), - true - end). - -prop_client_connected() -> - ?ALL({ClientInfo, ConnInfo, Env}, - {clientinfo(), conninfo(), empty_env()}, - begin - ok = emqx_web_hook:on_client_connected(ClientInfo, ConnInfo, Env), - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => client_connected, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - ipaddress => peer2addr(maps:get(peerhost, ClientInfo)), - keepalive => maps:get(keepalive, ConnInfo), - proto_ver => maps:get(proto_ver, ConnInfo), - connected_at => maps:get(connected_at, ConnInfo) - }), - true - end). - -prop_client_disconnected() -> - ?ALL({ClientInfo, Reason, ConnInfo, Env}, - {clientinfo(), shutdown_reason(), disconnected_conninfo(), empty_env()}, - begin - ok = emqx_web_hook:on_client_disconnected(ClientInfo, Reason, ConnInfo, Env), - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => client_disconnected, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - disconnected_at => maps:get(disconnected_at, ConnInfo), - reason => stringfy(Reason) - }), - true - end). - -prop_client_subscribe() -> - ?ALL({ClientInfo, SubProps, TopicTab, Env}, - {clientinfo(), sub_properties(), topictab(), topic_filter_env()}, - begin - ok = emqx_web_hook:on_client_subscribe(ClientInfo, SubProps, TopicTab, Env), - - Matched = filter_topictab(TopicTab, Env), - - lists:foreach(fun({Topic, Opts}) -> - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => client_subscribe, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - topic => Topic, - opts => Opts}) - end, Matched), - true - end). - -prop_client_unsubscribe() -> - ?ALL({ClientInfo, SubProps, TopicTab, Env}, - {clientinfo(), unsub_properties(), topictab(), topic_filter_env()}, - begin - ok = emqx_web_hook:on_client_unsubscribe(ClientInfo, SubProps, TopicTab, Env), - - Matched = filter_topictab(TopicTab, Env), - - lists:foreach(fun({Topic, Opts}) -> - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => client_unsubscribe, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - topic => Topic, - opts => Opts}) - end, Matched), - true - end). - -prop_session_subscribed() -> - ?ALL({ClientInfo, Topic, SubOpts, Env}, - {clientinfo(), topic(), subopts(), topic_filter_env()}, - begin - ok = emqx_web_hook:on_session_subscribed(ClientInfo, Topic, SubOpts, Env), - filter_topic_match(Topic, Env) andalso begin - Body = receive_http_request_body(), - Body1 = emqx_json:encode( - #{action => session_subscribed, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - topic => Topic, - opts => SubOpts - }), - Body = Body1 - end, - true - end). - -prop_session_unsubscribed() -> - ?ALL({ClientInfo, Topic, SubOpts, Env}, - {clientinfo(), topic(), subopts(), empty_env()}, - begin - ok = emqx_web_hook:on_session_unsubscribed(ClientInfo, Topic, SubOpts, Env), - filter_topic_match(Topic, Env) andalso begin - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => session_unsubscribed, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - topic => Topic - }) - end, - true - end). - -prop_session_terminated() -> - ?ALL({ClientInfo, Reason, SessInfo, Env}, - {clientinfo(), shutdown_reason(), sessioninfo(), empty_env()}, - begin - ok = emqx_web_hook:on_session_terminated(ClientInfo, Reason, SessInfo, Env), - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => session_terminated, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - reason => stringfy(Reason) - }), - true - end). - -prop_message_publish() -> - ?ALL({Msg, Env, Encode}, {message(), topic_filter_env(), payload_encode()}, - begin - application:set_env(emqx_web_hook, encoding_of_payload_field, Encode), - {ok, Msg} = emqx_web_hook:on_message_publish(Msg, Env), - application:unset_env(emqx_web_hook, encoding_of_payload_field), - - (not emqx_message:is_sys(Msg)) - andalso filter_topic_match(emqx_message:topic(Msg), Env) - andalso begin - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => message_publish, - node => stringfy(node()), - from_client_id => emqx_message:from(Msg), - from_username => maybe(emqx_message:get_header(username, Msg)), - topic => emqx_message:topic(Msg), - qos => emqx_message:qos(Msg), - retain => emqx_message:get_flag(retain, Msg), - payload => encode(emqx_message:payload(Msg), Encode), - ts => emqx_message:timestamp(Msg) - }) - end, - true - end). - -prop_message_delivered() -> - ?ALL({ClientInfo, Msg, Env, Encode}, {clientinfo(), message(), topic_filter_env(), payload_encode()}, - begin - application:set_env(emqx_web_hook, encoding_of_payload_field, Encode), - ok = emqx_web_hook:on_message_delivered(ClientInfo, Msg, Env), - application:unset_env(emqx_web_hook, encoding_of_payload_field), - - (not emqx_message:is_sys(Msg)) - andalso filter_topic_match(emqx_message:topic(Msg), Env) - andalso begin - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => message_delivered, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - from_client_id => emqx_message:from(Msg), - from_username => maybe(emqx_message:get_header(username, Msg)), - topic => emqx_message:topic(Msg), - qos => emqx_message:qos(Msg), - retain => emqx_message:get_flag(retain, Msg), - payload => encode(emqx_message:payload(Msg), Encode), - ts => emqx_message:timestamp(Msg) - }) - end, - true - end). - -prop_message_acked() -> - ?ALL({ClientInfo, Msg, Env, Encode}, {clientinfo(), message(), empty_env(), payload_encode()}, - begin - application:set_env(emqx_web_hook, encoding_of_payload_field, Encode), - ok = emqx_web_hook:on_message_acked(ClientInfo, Msg, Env), - application:unset_env(emqx_web_hook, encoding_of_payload_field), - - (not emqx_message:is_sys(Msg)) - andalso filter_topic_match(emqx_message:topic(Msg), Env) - andalso begin - Body = receive_http_request_body(), - Body = emqx_json:encode( - #{action => message_acked, - node => stringfy(node()), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo)), - from_client_id => emqx_message:from(Msg), - from_username => maybe(emqx_message:get_header(username, Msg)), - topic => emqx_message:topic(Msg), - qos => emqx_message:qos(Msg), - retain => emqx_message:get_flag(retain, Msg), - payload => encode(emqx_message:payload(Msg), Encode), - ts => emqx_message:timestamp(Msg) - }) - end, - true - end). - -%%-------------------------------------------------------------------- -%% Helper -%%-------------------------------------------------------------------- -do_setup() -> - %% Pre-defined envs - application:set_env(emqx_web_hook, path, "path"), - application:set_env(emqx_web_hook, headers, []), - - meck:new(ehttpc_pool, [passthrough, no_history]), - meck:expect(ehttpc_pool, pick_worker, fun(_, _) -> ok end), - - Self = self(), - meck:new(ehttpc, [passthrough, no_history]), - meck:expect(ehttpc, request, - fun(_ClientId, Method, {Path, Headers, Body}) -> - Self ! {Method, Path, Headers, Body}, {ok, 200, ok} - end), - - meck:new(emqx_metrics, [passthrough, no_history]), - meck:expect(emqx_metrics, inc, fun(_) -> ok end), - ok. - -do_teardown(_) -> - meck:unload(ehttpc_pool), - meck:unload(ehttpc), - meck:unload(emqx_metrics). - -maybe(undefined) -> null; -maybe(T) -> T. - -peer2addr({Host, _}) -> - list_to_binary(inet:ntoa(Host)); -peer2addr(Host) -> - list_to_binary(inet:ntoa(Host)). - -stringfy({shutdown, Reason}) -> - stringfy(Reason); -stringfy(Term) when is_binary(Term) -> - Term; -stringfy(Term) when is_atom(Term) -> - atom_to_binary(Term, utf8); -stringfy(Term) -> - unicode:characters_to_binary(io_lib:format("~0p", [Term])). - -receive_http_request_body() -> - receive - {post, _, _, Body} -> - Body - after 100 -> - exit(waiting_message_timeout) - end. - -filter_topictab(TopicTab, {undefined}) -> - TopicTab; -filter_topictab(TopicTab, {TopicFilter}) -> - lists:filter(fun({Topic, _}) -> emqx_topic:match(Topic, TopicFilter) end, TopicTab). - -filter_topic_match(_Topic, {undefined}) -> - true; -filter_topic_match(Topic, {TopicFilter}) -> - emqx_topic:match(Topic, TopicFilter). - -encode(Bin, base64) -> - base64:encode(Bin); -encode(Bin, base62) -> - emqx_base62:encode(Bin); -encode(Bin, _) -> - Bin. - -%%-------------------------------------------------------------------- -%% Generators -%%-------------------------------------------------------------------- - -conn_properties() -> - #{}. - -ack_properties() -> - #{}. - -sub_properties() -> - #{}. - -unsub_properties() -> - #{}. - -shutdown_reason() -> - oneof([disconnected, not_autherised, - "list_reason", <<"binary_reason">>, - {tuple, reason}, - {shutdown, emqx_ct_proper_types:limited_atom()}]). - -empty_env() -> - {undefined}. - -topic_filter_env() -> - oneof([{<<"#">>}, {undefined}, {topic()}]). - -payload_encode() -> - oneof([base62, base64, plain]). - -disconnected_conninfo() -> - ?LET(Info, conninfo(), - begin - Info#{disconnected_at => erlang:system_time(millisecond)} - end). diff --git a/rebar.config.erl b/rebar.config.erl index f28b43856..901749d72 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -286,8 +286,8 @@ relx_plugin_apps(ReleaseType) -> , emqx_coap , emqx_stomp , emqx_authentication - , emqx_web_hook , emqx_statsd + , emqx_rule_actions ] ++ relx_plugin_apps_per_rel(ReleaseType) ++ relx_plugin_apps_enterprise(is_enterprise()) From 64ce0d0e4f4c5b0c824a029ca61c9afbe92844fe Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 28 Jun 2021 15:08:33 +0800 Subject: [PATCH 031/379] chore(acl): delete acl nomatch config item --- apps/emqx/src/emqx_access_control.erl | 5 ++--- apps/emqx/test/emqx_access_control_SUITE.erl | 7 ------- apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 6 +++--- apps/emqx_authz/src/emqx_authz.erl | 12 ++++++------ apps/emqx_authz/test/emqx_authz_SUITE.erl | 3 +-- apps/emqx_coap/test/emqx_coap_SUITE.erl | 1 + 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 1ef885ed5..a679e6a5e 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -59,9 +59,8 @@ check_acl_cache(ClientInfo, PubSub, Topic) -> AclResult -> AclResult end. -do_check_acl(ClientInfo = #{zone := Zone}, PubSub, Topic) -> - Default = emqx_zone:get_env(Zone, acl_nomatch, deny), - case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], Default) of +do_check_acl(ClientInfo, PubSub, Topic) -> + case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], allow) of allow -> allow; _Other -> deny end. diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index e4a888d14..ffe3f4fac 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -39,13 +39,6 @@ t_authenticate(_) -> ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). t_check_acl(_) -> - emqx_zone:set_env(zone, acl_nomatch, deny), - application:set_env(emqx, enable_acl_cache, false), - Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), - ?assertEqual(deny, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)), - - emqx_zone:set_env(zone, acl_nomatch, allow), - application:set_env(emqx, enable_acl_cache, true), Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), ?assertEqual(allow, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)). diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index 8ce35b50c..250a959eb 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -197,8 +197,8 @@ t_connect_will_message(_) -> t_batch_subscribe(_) -> {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>}]), {ok, _} = emqtt:connect(Client), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), + ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> deny end), {ok, _, [?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED]} = emqtt:subscribe(Client, [{<<"t1">>, qos1}, @@ -209,7 +209,7 @@ t_batch_subscribe(_) -> ?RC_NO_SUBSCRIPTION_EXISTED]} = emqtt:unsubscribe(Client, [<<"t1">>, <<"t2">>, <<"t3">>]), - application:set_env(emqx, acl_nomatch, allow), + meck:unload(emqx_access_control), emqtt:disconnect(Client). t_connect_will_retain(_) -> diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 24393a4b0..fde1169de 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -150,22 +150,22 @@ b2l(B) when is_binary(B) -> binary_to_list(B). %% @doc Check ACL -spec(check_authz(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) - -> {ok, allow} | {ok, deny} | deny). + -> {stop, allow} | {ok, deny}). check_authz(#{username := Username, peerhost := IpAddress - } = Client, PubSub, Topic, DefaultResult, Rules) -> + } = Client, PubSub, Topic, _DefaultResult, Rules) -> case do_check_authz(Client, PubSub, Topic, Rules) of {matched, allow} -> - ?LOG(info, "Client succeeded authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), + ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; {matched, deny} -> - ?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]), + ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]), emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny}; nomatch -> - ?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), - DefaultResult + ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), + {stop, deny} end. do_check_authz(Client, PubSub, Topic, diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 88e250377..d036d1dec 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -39,7 +39,6 @@ end_per_suite(_Config) -> set_special_configs(emqx) -> application:set_env(emqx, allow_anonymous, true), application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), ok; set_special_configs(emqx_authz) -> application:set_env(emqx, plugins_etc_dir, @@ -145,7 +144,7 @@ t_authz(_) -> Rules3 = [emqx_authz:compile(Rule) || Rule <- [?RULE3, ?RULE4]], Rules4 = [emqx_authz:compile(Rule) || Rule <- [?RULE4, ?RULE1]], - ?assertEqual(deny, + ?assertEqual({stop, deny}, emqx_authz:check_authz(ClientInfo1, subscribe, <<"#">>, deny, [])), ?assertEqual({stop, deny}, emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl index 73c9ef162..416c99018 100644 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_SUITE.erl @@ -289,6 +289,7 @@ t_acl(Config) -> ok end, + ok = emqx_hooks:del('client.check_acl', {emqx_authz, check_authz}), file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), application:set_env(emqx, plugins_etc_dir, OldPath), application:stop(emqx_authz). From 98739224f6a639a67759d8efd974682d12bdba49 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 12:59:35 +0200 Subject: [PATCH 032/379] chore(emqx_sn): Add SN shard --- apps/emqx_sn/src/emqx_sn_registry.erl | 16 ++++++++++------ apps/emqx_sn/test/emqx_sn_registry_SUITE.erl | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/emqx_sn/src/emqx_sn_registry.erl b/apps/emqx_sn/src/emqx_sn_registry.erl index 4a3b22585..4d2e656b3 100644 --- a/apps/emqx_sn/src/emqx_sn_registry.erl +++ b/apps/emqx_sn/src/emqx_sn_registry.erl @@ -44,6 +44,8 @@ , code_change/3 ]). +-define(SN_SHARD, emqx_sn_shard). + -define(TAB, ?MODULE). -record(state, {max_predef_topic_id = 0}). @@ -56,6 +58,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). +-rlog_shard({?SN_SHARD, ?TAB}). %% @doc Create or replicate tables. -spec(mnesia(boot | copy) -> ok). @@ -74,6 +77,7 @@ mnesia(copy) -> -spec(start_link(list()) -> {ok, pid()} | ignore | {error, Reason :: term()}). start_link(PredefTopics) -> + ekka_mnesia:wait_for_shards([?SN_SHARD], infinity), gen_server:start_link({local, ?MODULE}, ?MODULE, [PredefTopics], []). -spec(stop() -> ok). @@ -129,10 +133,10 @@ init([PredefTopics]) -> %% {ClientId, TopicName} -> TopicId MaxPredefId = lists:foldl( fun({TopicId, TopicName}, AccId) -> - mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicId}, - value = TopicName}), - mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicName}, - value = TopicId}), + ekka_mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicId}, + value = TopicName}), + ekka_mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicName}, + value = TopicId}), if TopicId > AccId -> TopicId; true -> AccId end end, 0, PredefTopics), {ok, #state{max_predef_topic_id = MaxPredefId}}. @@ -157,7 +161,7 @@ handle_call({register, ClientId, TopicName}, _From, mnesia:write(#emqx_sn_registry{key = {ClientId, TopicId}, value = TopicName}) end, - case mnesia:transaction(Fun) of + case ekka_mnesia:transaction(?SN_SHARD, Fun) of {atomic, ok} -> {reply, TopicId, State}; {aborted, Error} -> @@ -168,7 +172,7 @@ handle_call({register, ClientId, TopicName}, _From, handle_call({unregister, ClientId}, _From, State) -> Registry = mnesia:dirty_match_object({?TAB, {ClientId, '_'}, '_'}), - lists:foreach(fun(R) -> mnesia:dirty_delete_object(R) end, Registry), + lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(R) end, Registry), {reply, ok, State}; handle_call(Req, _From, State) -> diff --git a/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl b/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl index 8d320d8ed..58a458ecc 100644 --- a/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl +++ b/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl @@ -41,9 +41,10 @@ end_per_suite(_Config) -> ok. init_per_testcase(_TestCase, Config) -> + application:set_env(ekka, strict_mode, true), ekka_mnesia:start(), emqx_sn_registry:mnesia(boot), - mnesia:clear_table(emqx_sn_registry), + ekka_mnesia:clear_table(emqx_sn_registry), PredefTopics = application:get_env(emqx_sn, predefined, []), {ok, _Pid} = ?REGISTRY:start_link(PredefTopics), Config. @@ -118,4 +119,3 @@ register_a_lot(N, Max) when N < Max -> Topic = iolist_to_binary(["Topic", integer_to_list(N)]), ?assertEqual(N, ?REGISTRY:register_topic(<<"ClientId">>, Topic)), register_a_lot(N+1, Max). - From 7c9861dbaa7ff82be7805aadc06a13f4e9c6ee0d Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 14:04:47 +0200 Subject: [PATCH 033/379] chore(banned): Add banned shard --- apps/emqx/include/emqx.hrl | 3 ++- apps/emqx/src/emqx_app.erl | 2 +- apps/emqx/src/emqx_banned.erl | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index ba72a47b5..b38614562 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -23,6 +23,8 @@ -define(Otherwise, true). +-define(COMMON_SHARD, emqx_common_shard). + %%-------------------------------------------------------------------- %% Banner %%-------------------------------------------------------------------- @@ -134,4 +136,3 @@ }). -endif. - diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 234f42645..61a5a5633 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -28,7 +28,7 @@ -define(APP, emqx). --define(EMQX_SHARDS, [route_shard]). +-define(EMQX_SHARDS, [?ROUTE_SHARD, ?COMMON_SHARD]). -include("emqx_release.hrl"). diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index 762a2b61b..16804d329 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -51,6 +51,8 @@ -define(BANNED_TAB, ?MODULE). +-rlog_shard({?COMMON_SHARD, ?BANNED_TAB}). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -96,19 +98,19 @@ create(#{who := Who, reason := Reason, at := At, until := Until}) -> - mnesia:dirty_write(?BANNED_TAB, #banned{who = Who, - by = By, - reason = Reason, - at = At, - until = Until}); + ekka_mnesia:dirty_write(?BANNED_TAB, #banned{who = Who, + by = By, + reason = Reason, + at = At, + until = Until}); create(Banned) when is_record(Banned, banned) -> - mnesia:dirty_write(?BANNED_TAB, Banned). + ekka_mnesia:dirty_write(?BANNED_TAB, Banned). -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). delete(Who) -> - mnesia:dirty_delete(?BANNED_TAB, Who). + ekka_mnesia:dirty_delete(?BANNED_TAB, Who). info(InfoKey) -> mnesia:table_info(?BANNED_TAB, InfoKey). @@ -129,7 +131,7 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> - mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]), + ekka_mnesia:transaction(?COMMON_SHARD, fun expire_banned_items/1, [erlang:system_time(second)]), {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> @@ -160,4 +162,3 @@ expire_banned_items(Now) -> mnesia:delete_object(?BANNED_TAB, B, sticky_write); (_, _Acc) -> ok end, ok, ?BANNED_TAB). - From ce4800e6ae27cda0580d48863e3ecefc0b33d1dd Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 14:18:04 +0200 Subject: [PATCH 034/379] chore(alarm): Alarm shard --- apps/emqx/src/emqx_alarm.erl | 29 ++++++++++++++++++--------- apps/emqx_sn/src/emqx_sn_registry.erl | 4 ++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 62ce1af8b..a3a7420e3 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -90,6 +90,10 @@ -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). +-rlog_shard({?COMMON_SHARD, ?ACTIVATED_ALARM}). +-rlog_shard({?COMMON_SHARD, ?DEACTIVATED_ALARM}). + + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -182,7 +186,7 @@ handle_call({activate_alarm, Name, Details}, _From, State = #state{actions = Act details = Details, message = normalize_message(Name, Details), activate_at = erlang:system_time(microsecond)}, - mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), + ekka_mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), do_actions(activate, Alarm, Actions), {reply, ok, State} end; @@ -202,9 +206,14 @@ handle_call(delete_all_deactivated_alarms, _From, State) -> {reply, ok, State}; handle_call({get_alarms, all}, _From, State) -> - Alarms = [normalize(Alarm) || - Alarm <- ets:tab2list(?ACTIVATED_ALARM) - ++ ets:tab2list(?DEACTIVATED_ALARM)], + {atomic, Alarms} = + ekka_mnesia:ro_transaction( + ?COMMON_SHARD, + fun() -> + [normalize(Alarm) || + Alarm <- ets:tab2list(?ACTIVATED_ALARM) + ++ ets:tab2list(?DEACTIVATED_ALARM)] + end), {reply, Alarms, State}; handle_call({get_alarms, activated}, _From, State) -> @@ -252,7 +261,7 @@ deactivate_alarm(Details, SizeLimit, Actions, #activated_alarm{ case mnesia:dirty_first(?DEACTIVATED_ALARM) of '$end_of_table' -> ok; ActivateAt2 -> - mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivateAt2) + ekka_mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivateAt2) end; false -> ok end, @@ -261,8 +270,8 @@ deactivate_alarm(Details, SizeLimit, Actions, #activated_alarm{ DeActAlarm = make_deactivated_alarm(ActivateAt, Name, Details, normalize_message(Name, Details), erlang:system_time(microsecond)), - mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), - mnesia:dirty_delete(?ACTIVATED_ALARM, Name), + ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), + ekka_mnesia:dirty_delete(?ACTIVATED_ALARM, Name), do_actions(deactivate, DeActAlarm, Actions). make_deactivated_alarm(ActivateAt, Name, Details, Message, DeActivateAt) -> @@ -279,7 +288,7 @@ deactivate_all_alarms() -> details = Details, message = Message, activate_at = ActivateAt}) -> - mnesia:dirty_write(?DEACTIVATED_ALARM, + ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, #deactivated_alarm{ activate_at = ActivateAt, name = Name, @@ -291,7 +300,7 @@ deactivate_all_alarms() -> %% Delete all records from the given table, ignore result. clear_table(TableName) -> - case mnesia:clear_table(TableName) of + case ekka_mnesia:clear_table(TableName) of {aborted, Reason} -> ?LOG(warning, "Faile to clear table ~p reason: ~p", [TableName, Reason]); @@ -311,7 +320,7 @@ delete_expired_deactivated_alarms('$end_of_table', _Checkpoint) -> delete_expired_deactivated_alarms(ActivatedAt, Checkpoint) -> case ActivatedAt =< Checkpoint of true -> - mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivatedAt), + ekka_mnesia:dirty_delete(?DEACTIVATED_ALARM, ActivatedAt), NActivatedAt = mnesia:dirty_next(?DEACTIVATED_ALARM, ActivatedAt), delete_expired_deactivated_alarms(NActivatedAt, Checkpoint); false -> diff --git a/apps/emqx_sn/src/emqx_sn_registry.erl b/apps/emqx_sn/src/emqx_sn_registry.erl index 4d2e656b3..903f61c70 100644 --- a/apps/emqx_sn/src/emqx_sn_registry.erl +++ b/apps/emqx_sn/src/emqx_sn_registry.erl @@ -77,7 +77,7 @@ mnesia(copy) -> -spec(start_link(list()) -> {ok, pid()} | ignore | {error, Reason :: term()}). start_link(PredefTopics) -> - ekka_mnesia:wait_for_shards([?SN_SHARD], infinity), + ekka_rlog:wait_for_shards([?SN_SHARD], infinity), gen_server:start_link({local, ?MODULE}, ?MODULE, [PredefTopics], []). -spec(stop() -> ok). @@ -172,7 +172,7 @@ handle_call({register, ClientId, TopicName}, _From, handle_call({unregister, ClientId}, _From, State) -> Registry = mnesia:dirty_match_object({?TAB, {ClientId, '_'}, '_'}), - lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(R) end, Registry), + lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(?TAB, R) end, Registry), {reply, ok, State}; handle_call(Req, _From, State) -> From 26b2216e25aed075418994a499f36d2f0c61f149 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 15:08:25 +0200 Subject: [PATCH 035/379] chore(shared_sub): Add shared_sub shard --- apps/emqx/include/emqx.hrl | 1 + apps/emqx/src/emqx_app.erl | 2 +- apps/emqx/src/emqx_shared_sub.erl | 10 ++++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index b38614562..9fe69fd30 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -24,6 +24,7 @@ -define(Otherwise, true). -define(COMMON_SHARD, emqx_common_shard). +-define(SHARED_SUB_SHARD, emqx_shared_sub_shard). %%-------------------------------------------------------------------- %% Banner diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 61a5a5633..dfdc9c0f8 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -28,7 +28,7 @@ -define(APP, emqx). --define(EMQX_SHARDS, [?ROUTE_SHARD, ?COMMON_SHARD]). +-define(EMQX_SHARDS, [?ROUTE_SHARD, ?COMMON_SHARD, ?SHARED_SUB_SHARD]). -include("emqx_release.hrl"). diff --git a/apps/emqx/src/emqx_shared_sub.erl b/apps/emqx/src/emqx_shared_sub.erl index 97aa778f3..c002653ba 100644 --- a/apps/emqx/src/emqx_shared_sub.erl +++ b/apps/emqx/src/emqx_shared_sub.erl @@ -77,6 +77,8 @@ -define(NACK(Reason), {shared_sub_nack, Reason}). -define(NO_ACK, no_ack). +-rlog_shard({?SHARED_SUB_SHARD, ?TAB}). + -record(state, {pmon}). -record(emqx_shared_subscription, {group, topic, subpid}). @@ -297,7 +299,7 @@ subscribers(Group, Topic) -> init([]) -> {ok, _} = mnesia:subscribe({table, ?TAB, simple}), - {atomic, PMon} = mnesia:transaction(fun init_monitors/0), + {atomic, PMon} = ekka_mnesia:transaction(?SHARED_SUB_SHARD, fun init_monitors/0), ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]), ok = emqx_tables:new(?ALIVE_SUBS, [protected, set, {read_concurrency, true}]), {ok, update_stats(#state{pmon = PMon})}. @@ -309,7 +311,7 @@ init_monitors() -> end, emqx_pmon:new(), ?TAB). handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon}) -> - mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), + ekka_mnesia:dirty_write(?TAB, record(Group, Topic, SubPid)), case ets:member(?SHARED_SUBS, {Group, Topic}) of true -> ok; false -> ok = emqx_router:do_add_route(Topic, {Group, node()}) @@ -319,7 +321,7 @@ handle_call({subscribe, Group, Topic, SubPid}, _From, State = #state{pmon = PMon {reply, ok, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; handle_call({unsubscribe, Group, Topic, SubPid}, _From, State) -> - mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)), + ekka_mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)), true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), delete_route_if_needed({Group, Topic}), {reply, ok, State}; @@ -373,7 +375,7 @@ cleanup_down(SubPid) -> ?IS_LOCAL_PID(SubPid) orelse ets:delete(?ALIVE_SUBS, SubPid), lists:foreach( fun(Record = #emqx_shared_subscription{topic = Topic, group = Group}) -> - ok = mnesia:dirty_delete_object(?TAB, Record), + ok = ekka_mnesia:dirty_delete_object(?TAB, Record), true = ets:delete_object(?SHARED_SUBS, {{Group, Topic}, SubPid}), delete_route_if_needed({Group, Topic}) end, mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})). From 1618a90ddd810228f4973457ba457c566c3197ca Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 18:14:36 +0200 Subject: [PATCH 036/379] chore(telemetry): Add an RLOG shard --- apps/emqx_telemetry/src/emqx_telemetry.erl | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/emqx_telemetry/src/emqx_telemetry.erl b/apps/emqx_telemetry/src/emqx_telemetry.erl index ea4017dc9..dd6e7aa4c 100644 --- a/apps/emqx_telemetry/src/emqx_telemetry.erl +++ b/apps/emqx_telemetry/src/emqx_telemetry.erl @@ -90,6 +90,8 @@ -define(TELEMETRY, emqx_telemetry). +-rlog_shard({?COMMON_SHARD, ?TELEMETRY}). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -146,9 +148,9 @@ init([Opts]) -> [] -> Enabled = proplists:get_value(enabled, Opts, true), UUID = generate_uuid(), - mnesia:dirty_write(?TELEMETRY, #telemetry{id = ?UNIQUE_ID, - uuid = UUID, - enabled = Enabled}), + ekka_mnesia:dirty_write(?TELEMETRY, #telemetry{id = ?UNIQUE_ID, + uuid = UUID, + enabled = Enabled}), State#state{enabled = Enabled, uuid = UUID}; [#telemetry{uuid = UUID, enabled = Enabled} | _] -> State#state{enabled = Enabled, uuid = UUID} @@ -162,16 +164,16 @@ init([Opts]) -> end. handle_call(enable, _From, State = #state{uuid = UUID}) -> - mnesia:dirty_write(?TELEMETRY, #telemetry{id = ?UNIQUE_ID, - uuid = UUID, - enabled = true}), + ekka_mnesia:dirty_write(?TELEMETRY, #telemetry{id = ?UNIQUE_ID, + uuid = UUID, + enabled = true}), _ = erlang:send(self(), first_report), {reply, ok, State#state{enabled = true}}; handle_call(disable, _From, State = #state{uuid = UUID}) -> - mnesia:dirty_write(?TELEMETRY, #telemetry{id = ?UNIQUE_ID, - uuid = UUID, - enabled = false}), + ekka_mnesia:dirty_write(?TELEMETRY, #telemetry{id = ?UNIQUE_ID, + uuid = UUID, + enabled = false}), {reply, ok, State#state{enabled = false}}; handle_call(is_enabled, _From, State = #state{enabled = Enabled}) -> From 9ab5c88d209c08222bb877a8d10fb38212b524f1 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 20:22:06 +0200 Subject: [PATCH 037/379] chore(retainer): Add RLOG shard --- apps/emqx_retainer/include/emqx_retainer.hrl | 2 +- apps/emqx_retainer/src/emqx_retainer.erl | 19 +++++++++++-------- apps/emqx_retainer/src/emqx_retainer_cli.erl | 3 +-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/emqx_retainer/include/emqx_retainer.hrl b/apps/emqx_retainer/include/emqx_retainer.hrl index a1e229cfb..a9978e206 100644 --- a/apps/emqx_retainer/include/emqx_retainer.hrl +++ b/apps/emqx_retainer/include/emqx_retainer.hrl @@ -17,4 +17,4 @@ -define(APP, emqx_retainer). -define(TAB, ?APP). -record(retained, {topic, msg, expiry_time}). - +-define(RETAINER_SHARD, emqx_retainer_shard). diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 9e6f60013..94c561b39 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -51,6 +51,8 @@ -record(state, {stats_fun, stats_timer, expiry_timer}). +-rlog_shard({?RETAINER_SHARD, ?TAB}). + %%-------------------------------------------------------------------- %% Load/Unload %%-------------------------------------------------------------------- @@ -84,7 +86,7 @@ dispatch(Pid, Topic) -> on_message_publish(Msg = #message{flags = #{retain := true}, topic = Topic, payload = <<>>}, _Env) -> - mnesia:dirty_delete(?TAB, topic2tokens(Topic)), + ekka_mnesia:dirty_delete(?TAB, topic2tokens(Topic)), {ok, Msg}; on_message_publish(Msg = #message{flags = #{retain := true}}, Env) -> @@ -115,7 +117,7 @@ clean(Topic) when is_binary(Topic) -> [_M] -> mnesia:delete({?TAB, Tokens}), 1 end end, - {atomic, N} = mnesia:transaction(Fun), N + {atomic, N} = ekka_mnesia:transaction(?RETAINER_SHARD, Fun), N end. %%-------------------------------------------------------------------- @@ -139,6 +141,7 @@ init([Env]) -> {attributes, record_info(fields, retained)}, {storage_properties, StoreProps}]), ok = ekka_mnesia:copy_table(?TAB, Copies), + ok = ekka_rlog:wait_for_shards([?RETAINER_SHARD], infinity), case mnesia:table_info(?TAB, storage_type) of Copies -> ok; _Other -> @@ -201,11 +204,11 @@ store_retained(Msg = #message{topic = Topic, payload = Payload}, Env) -> case {is_table_full(Env), is_too_big(size(Payload), Env)} of {false, false} -> ok = emqx_metrics:inc('messages.retained'), - mnesia:dirty_write(?TAB, #retained{topic = topic2tokens(Topic), - msg = Msg, - expiry_time = get_expiry_time(Msg, Env)}); + ekka_mnesia:dirty_write(?TAB, #retained{topic = topic2tokens(Topic), + msg = Msg, + expiry_time = get_expiry_time(Msg, Env)}); {true, false} -> - {atomic, _} = mnesia:transaction( + {atomic, _} = ekka_mnesia:transaction(?RETAINER_SHARD, fun() -> case mnesia:read(?TAB, Topic) of [_] -> @@ -256,7 +259,7 @@ expire_messages() -> NowMs = erlang:system_time(millisecond), MsHd = #retained{topic = '$1', msg = '_', expiry_time = '$3'}, Ms = [{MsHd, [{'=/=','$3',0}, {'<','$3',NowMs}], ['$1']}], - {atomic, _} = mnesia:transaction( + {atomic, _} = ekka_mnesia:transaction(?RETAINER_SHARD, fun() -> Keys = mnesia:select(?TAB, Ms, write), lists:foreach(fun(Key) -> mnesia:delete({?TAB, Key}) end, Keys) @@ -293,7 +296,7 @@ match_delete_messages(Filter) -> MsHd = #retained{topic = Cond, msg = '_', expiry_time = '_'}, Ms = [{MsHd, [], ['$_']}], Rs = mnesia:dirty_select(?TAB, Ms), - lists:foreach(fun(R) -> mnesia:dirty_delete_object(?TAB, R) end, Rs), + lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(?TAB, R) end, Rs), length(Rs). %% @private diff --git a/apps/emqx_retainer/src/emqx_retainer_cli.erl b/apps/emqx_retainer/src/emqx_retainer_cli.erl index fe8fa9578..1e965946f 100644 --- a/apps/emqx_retainer/src/emqx_retainer_cli.erl +++ b/apps/emqx_retainer/src/emqx_retainer_cli.erl @@ -38,7 +38,7 @@ cmd(["topics"]) -> cmd(["clean"]) -> Size = mnesia:table_info(?TAB, size), - case mnesia:clear_table(?TAB) of + case ekka_mnesia:clear_table(?TAB) of {atomic, ok} -> emqx_ctl:print("Cleaned ~p retained messages~n", [Size]); {aborted, R} -> emqx_ctl:print("Aborted ~p~n", [R]) end; @@ -55,4 +55,3 @@ cmd(_) -> unload() -> emqx_ctl:unregister_command(retainer). - From 73ec8c47cc3200f7733d9d71062a3fa7a937e94e Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 18:56:13 +0200 Subject: [PATCH 038/379] chore(rule_engine): Add an RLOG shard --- apps/emqx/include/emqx.hrl | 3 +++ apps/emqx/src/emqx_app.erl | 6 +++++- .../src/emqx_rule_registry.erl | 19 ++++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 9fe69fd30..67744306e 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -89,6 +89,9 @@ -define(ROUTE_SHARD, route_shard). + +-define(RULE_ENGINE_SHARD, emqx_rule_engine_shard). + -record(route, { topic :: binary(), dest :: node() | {binary(), node()} diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index dfdc9c0f8..06bebe465 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -28,7 +28,11 @@ -define(APP, emqx). --define(EMQX_SHARDS, [?ROUTE_SHARD, ?COMMON_SHARD, ?SHARED_SUB_SHARD]). +-define(EMQX_SHARDS, [ ?ROUTE_SHARD + , ?COMMON_SHARD + , ?SHARED_SUB_SHARD + , ?RULE_ENGINE_SHARD + ]). -include("emqx_release.hrl"). diff --git a/apps/emqx_rule_engine/src/emqx_rule_registry.erl b/apps/emqx_rule_engine/src/emqx_rule_registry.erl index 2d029f8e3..f2d717dba 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_registry.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_registry.erl @@ -19,6 +19,7 @@ -behaviour(gen_server). -include("rule_engine.hrl"). +-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("stdlib/include/qlc.hrl"). @@ -95,6 +96,11 @@ -define(T_CALL, 10000). +-rlog_shard({?RULE_ENGINE_SHARD, ?RULE_TAB}). +-rlog_shard({?RULE_ENGINE_SHARD, ?ACTION_TAB}). +-rlog_shard({?RULE_ENGINE_SHARD, ?RES_TAB}). +-rlog_shard({?RULE_ENGINE_SHARD, ?RES_TYPE_TAB}). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -174,7 +180,7 @@ get_rules_ordered_by_ts() -> Query = qlc:q([E || E <- mnesia:table(?RULE_TAB)]), qlc:e(qlc:keysort(#rule.created_at, Query, [{order, ascending}])) end, - {atomic, List} = mnesia:transaction(F), + {atomic, List} = ekka_mnesia:transaction(?RULE_ENGINE_SHARD, F), List. -spec(get_rules_for(Topic :: binary()) -> list(emqx_rule_engine:rule())). @@ -471,11 +477,18 @@ code_change(_OldVsn, State, _Extra) -> get_all_records(Tab) -> %mnesia:dirty_match_object(Tab, mnesia:table_info(Tab, wild_pattern)). - ets:tab2list(Tab). + %% Wrapping ets to a r/o transaction to avoid reading inconsistent + %% data during shard bootstrap + {atomic, Ret} = + ekka_mnesia:ro_transaction(?RULE_ENGINE_SHARD, + fun() -> + ets:tab2list(Tab) + end), + Ret. trans(Fun) -> trans(Fun, []). trans(Fun, Args) -> - case mnesia:transaction(Fun, Args) of + case ekka_mnesia:transaction(?RULE_ENGINE_SHARD, Fun, Args) of {atomic, Result} -> Result; {aborted, Reason} -> error(Reason) end. From e8e956b074745c094c5d203d33382186e41a80da Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 28 Jun 2021 21:02:26 +0200 Subject: [PATCH 039/379] chore(mod_delayed): Add RLOG shard --- apps/emqx/include/emqx.hrl | 1 + apps/emqx/src/emqx_app.erl | 1 + apps/emqx_modules/src/emqx_mod_delayed.erl | 7 ++++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 67744306e..d148e01a3 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -25,6 +25,7 @@ -define(COMMON_SHARD, emqx_common_shard). -define(SHARED_SUB_SHARD, emqx_shared_sub_shard). +-define(MOD_DELAYED_SHARD, emqx_delayed_shard). %%-------------------------------------------------------------------- %% Banner diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 06bebe465..60f0fc40d 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -32,6 +32,7 @@ , ?COMMON_SHARD , ?SHARED_SUB_SHARD , ?RULE_ENGINE_SHARD + , ?MOD_DELAYED_SHARD ]). -include("emqx_release.hrl"). diff --git a/apps/emqx_modules/src/emqx_mod_delayed.erl b/apps/emqx_modules/src/emqx_mod_delayed.erl index ac5be58b2..925952078 100644 --- a/apps/emqx_modules/src/emqx_mod_delayed.erl +++ b/apps/emqx_modules/src/emqx_mod_delayed.erl @@ -58,6 +58,8 @@ -define(SERVER, ?MODULE). -define(MAX_INTERVAL, 4294967). +-rlog_shard({?MOD_DELAYED_SHARD, ?TAB}). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -137,7 +139,7 @@ init([]) -> ensure_publish_timer(#{timer => undefined, publish_at => 0}))}. handle_call({store, DelayedMsg = #delayed_message{key = Key}}, _From, State) -> - ok = mnesia:dirty_write(?TAB, DelayedMsg), + ok = ekka_mnesia:dirty_write(?TAB, DelayedMsg), emqx_metrics:inc('messages.delayed'), {reply, ok, ensure_publish_timer(Key, State)}; @@ -152,7 +154,7 @@ handle_cast(Msg, State) -> %% Do Publish... handle_info({timeout, TRef, do_publish}, State = #{timer := TRef}) -> DeletedKeys = do_publish(mnesia:dirty_first(?TAB), os:system_time(seconds)), - lists:foreach(fun(Key) -> mnesia:dirty_delete(?TAB, Key) end, DeletedKeys), + lists:foreach(fun(Key) -> ekka_mnesia:dirty_delete(?TAB, Key) end, DeletedKeys), {noreply, ensure_publish_timer(State#{timer := undefined, publish_at := 0})}; handle_info(stats, State = #{stats_fun := StatsFun}) -> @@ -222,4 +224,3 @@ do_publish(Key = {Ts, _Id}, Now, Acc) when Ts =< Now -> -spec(delayed_count() -> non_neg_integer()). delayed_count() -> mnesia:table_info(?TAB, size). - From cc5bc4f7ca3e831210d37dde3b3befd2ae013f24 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Tue, 29 Jun 2021 00:27:53 +0200 Subject: [PATCH 040/379] build(rlog): Add xref check for forbidden mnesia APIs --- rebar.config | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rebar.config b/rebar.config index 205d892aa..e5f46125a 100644 --- a/rebar.config +++ b/rebar.config @@ -15,6 +15,15 @@ {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, deprecated_function_calls,warnings_as_errors,deprecated_functions]}. +%% Check for the mnesia calls forbidden by Ekka: +{xref_queries, + [ {"E || \"mnesia\":\"dirty_write\"/\".*\" : Fun", []} + , {"E || \"mnesia\":\"dirty_delete.*\"/\".*\" : Fun", []} + , {"E || \"mnesia\":\"transaction\"/\".*\" : Fun", []} + , {"E || \"mnesia\":\"async_dirty\"/\".*\" : Fun", []} + , {"E || \"mnesia\":\"clear_table\"/\".*\" : Fun", []} + ]}. + {dialyzer, [ {warnings, [unmatched_returns, error_handling, race_conditions]}, {plt_location, "."}, From c63bdc355a444f637edeac874778f80a9896685f Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 28 Jun 2021 15:17:21 +0800 Subject: [PATCH 041/379] chore: rename check_acl to check_authz update emqx_coap vsn rename OnClientCheckAcl to OnClientCheckAuthz in exhook --- apps/emqx/src/emqx_access_control.erl | 18 +++++++------- apps/emqx/src/emqx_channel.erl | 4 ++-- apps/emqx/src/emqx_metrics.erl | 4 ++-- apps/emqx/test/emqx_access_control_SUITE.erl | 4 ++-- apps/emqx/test/emqx_acl_test_mod.erl | 4 ++-- apps/emqx/test/emqx_channel_SUITE.erl | 2 +- .../emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 2 +- apps/emqx/test/emqx_ws_connection_SUITE.erl | 2 +- apps/emqx_authz/src/emqx_authz.erl | 8 +++---- .../test/emqx_authz_mysql_SUITE.erl | 24 +++++++++---------- .../test/emqx_authz_pgsql_SUITE.erl | 24 +++++++++---------- .../test/emqx_authz_redis_SUITE.erl | 16 ++++++------- apps/emqx_coap/src/emqx_coap.app.src | 2 +- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 4 ++-- apps/emqx_coap/test/emqx_coap_SUITE.erl | 6 ++--- apps/emqx_exhook/include/emqx_exhook.hrl | 2 +- apps/emqx_exhook/priv/protos/exhook.proto | 12 +++++----- apps/emqx_exhook/src/emqx_exhook_handler.erl | 6 ++--- apps/emqx_exhook/src/emqx_exhook_server.erl | 6 ++--- .../emqx_exhook/test/emqx_exhook_demo_svr.erl | 8 +++---- .../test/props/prop_exhook_hooks.erl | 6 ++--- .../emqx_exproto/src/emqx_exproto_channel.erl | 4 ++-- apps/emqx_exproto/test/emqx_exproto_SUITE.erl | 2 +- apps/emqx_prometheus/src/emqx_prometheus.erl | 6 ++--- 24 files changed, 88 insertions(+), 88 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index a679e6a5e..8ca9f3893 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -20,7 +20,7 @@ -export([authenticate/1]). --export([ check_acl/3 +-export([ check_authz/3 ]). -type(result() :: #{auth_result := emqx_types:auth_result(), @@ -42,25 +42,25 @@ authenticate(ClientInfo = #{zone := Zone}) -> end. %% @doc Check ACL --spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) +-spec(check_authz(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). -check_acl(ClientInfo, PubSub, Topic) -> +check_authz(ClientInfo, PubSub, Topic) -> case emqx_acl_cache:is_enabled() of - true -> check_acl_cache(ClientInfo, PubSub, Topic); - false -> do_check_acl(ClientInfo, PubSub, Topic) + true -> check_authz_cache(ClientInfo, PubSub, Topic); + false -> do_check_authz(ClientInfo, PubSub, Topic) end. -check_acl_cache(ClientInfo, PubSub, Topic) -> +check_authz_cache(ClientInfo, PubSub, Topic) -> case emqx_acl_cache:get_acl_cache(PubSub, Topic) of not_found -> - AclResult = do_check_acl(ClientInfo, PubSub, Topic), + AclResult = do_check_authz(ClientInfo, PubSub, Topic), emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), AclResult; AclResult -> AclResult end. -do_check_acl(ClientInfo, PubSub, Topic) -> - case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], allow) of +do_check_authz(ClientInfo, PubSub, Topic) -> + case run_hooks('client.check_authz', [ClientInfo, PubSub, Topic], allow) of allow -> allow; _Other -> deny end. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index e3cbff692..2fcad5b21 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1406,7 +1406,7 @@ check_pub_alias(_Packet, _Channel) -> ok. check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #channel{clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, publish, Topic) of + emqx_access_control:check_authz(ClientInfo, publish, Topic) of false -> ok; allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} @@ -1440,7 +1440,7 @@ check_sub_acls([], _Channel, Acc) -> check_sub_acl(TopicFilter, #channel{clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of + emqx_access_control:check_authz(ClientInfo, subscribe, TopicFilter) of false -> allow; Result -> Result end. diff --git a/apps/emqx/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl index c3ce14d83..c583276d8 100644 --- a/apps/emqx/src/emqx_metrics.erl +++ b/apps/emqx/src/emqx_metrics.erl @@ -172,7 +172,7 @@ {counter, 'client.connected'}, {counter, 'client.authenticate'}, {counter, 'client.auth.anonymous'}, - {counter, 'client.check_acl'}, + {counter, 'client.check_authz'}, {counter, 'client.subscribe'}, {counter, 'client.unsubscribe'}, {counter, 'client.disconnected'} @@ -563,7 +563,7 @@ reserved_idx('client.connected') -> 202; reserved_idx('client.authenticate') -> 203; reserved_idx('client.enhanced_authenticate') -> 204; reserved_idx('client.auth.anonymous') -> 205; -reserved_idx('client.check_acl') -> 206; +reserved_idx('client.check_authz') -> 206; reserved_idx('client.subscribe') -> 207; reserved_idx('client.unsubscribe') -> 208; reserved_idx('client.disconnected') -> 209; diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index ffe3f4fac..82a0c1669 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -38,9 +38,9 @@ t_authenticate(_) -> emqx_zone:set_env(zone, allow_anonymous, true), ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). -t_check_acl(_) -> +t_check_authz(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), - ?assertEqual(allow, emqx_access_control:check_acl(clientinfo(), Publish, <<"t">>)). + ?assertEqual(allow, emqx_access_control:check_authz(clientinfo(), Publish, <<"t">>)). t_bypass_auth_plugins(_) -> ClientInfo = clientinfo(), diff --git a/apps/emqx/test/emqx_acl_test_mod.erl b/apps/emqx/test/emqx_acl_test_mod.erl index da400f076..be461d584 100644 --- a/apps/emqx/test/emqx_acl_test_mod.erl +++ b/apps/emqx/test/emqx_acl_test_mod.erl @@ -18,14 +18,14 @@ %% ACL callbacks -export([ init/1 - , check_acl/2 + , check_authz/2 , description/0 ]). init(AclOpts) -> {ok, AclOpts}. -check_acl({_User, _PubSub, _Topic}, _State) -> +check_authz({_User, _PubSub, _Topic}, _State) -> allow. description() -> diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 9558dfd28..cc77acb56 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -37,7 +37,7 @@ init_per_suite(Config) -> ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, fun(_) -> {ok, #{auth_result => success}} end), - ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end), + ok = meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), %% Hooks Meck diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index 250a959eb..e60d52a86 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -198,7 +198,7 @@ t_batch_subscribe(_) -> {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>}]), {ok, _} = emqtt:connect(Client), ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> deny end), + meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> deny end), {ok, _, [?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED]} = emqtt:subscribe(Client, [{<<"t1">>, qos1}, diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index 6db831972..b5020439b 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -64,7 +64,7 @@ init_per_testcase(TestCase, Config) when end), %% Mock emqx_access_control ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end), + ok = meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> allow end), %% Mock emqx_hooks ok = meck:new(emqx_hooks, [passthrough, no_history, no_link]), ok = meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end), diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index fde1169de..b757c8908 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -41,7 +41,7 @@ init() -> #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf), ok = application:set_env(?APP, rules, Rules), NRules = [compile(Rule) || Rule <- Rules], - ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1). + ok = emqx_hooks:add('client.check_authz', {?MODULE, check_authz, [NRules]}, -1). lookup() -> application:get_env(?APP, rules, []). @@ -50,8 +50,8 @@ update(Rules) -> ok = application:set_env(?APP, rules, Rules), NRules = [compile(Rule) || Rule <- Rules], Action = find_action_in_hooks(), - ok = emqx_hooks:del('client.check_acl', Action), - ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1), + ok = emqx_hooks:del('client.check_authz', Action), + ok = emqx_hooks:add('client.check_authz', {?MODULE, check_authz, [NRules]}, -1), ok = emqx_acl_cache:empty_acl_cache(). %%-------------------------------------------------------------------- @@ -59,7 +59,7 @@ update(Rules) -> %%-------------------------------------------------------------------- find_action_in_hooks() -> - Callbacks = emqx_hooks:lookup('client.check_acl'), + Callbacks = emqx_hooks:lookup('client.check_authz'), [Action] = [Action || {callback,{?MODULE, check_authz, _} = Action, _, _} <- Callbacks ], Action. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 4f2148522..e4704c5f3 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -95,23 +95,23 @@ t_authz(_) -> }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"#">>)), % nomatch meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch + ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, publish, <<"test">>)), % nomatch ok. diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index dcc820a4c..d03cd3338 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -95,23 +95,23 @@ t_authz(_) -> }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"#">>)), % nomatch meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"+">>)), + ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)), - ?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)), - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch + ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, publish, <<"test">>)), % nomatch ok. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 3f8bea166..ab8465ffa 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -84,30 +84,30 @@ t_authz(_) -> meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)), + emqx_access_control:check_authz(ClientInfo, subscribe, <<"#">>)), ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)), + emqx_access_control:check_authz(ClientInfo, publish, <<"#">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"+">>)), + emqx_access_control:check_authz(ClientInfo, subscribe, <<"+">>)), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"test/username">>)), + emqx_access_control:check_authz(ClientInfo, subscribe, <<"test/username">>)), ?assertEqual(allow, - emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)), + emqx_access_control:check_authz(ClientInfo, publish, <<"test/clientid">>)), ?assertEqual(allow, - emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)), + emqx_access_control:check_authz(ClientInfo, publish, <<"test/clientid">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end), ?assertEqual(allow, - emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)), + emqx_access_control:check_authz(ClientInfo, subscribe, <<"#">>)), % nomatch ?assertEqual(deny, - emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)), + emqx_access_control:check_authz(ClientInfo, publish, <<"#">>)), ok. diff --git a/apps/emqx_coap/src/emqx_coap.app.src b/apps/emqx_coap/src/emqx_coap.app.src index 2b5fcbb6a..bb6d0431f 100644 --- a/apps/emqx_coap/src/emqx_coap.app.src +++ b/apps/emqx_coap/src/emqx_coap.app.src @@ -1,6 +1,6 @@ {application, emqx_coap, [{description, "EMQ X CoAP Gateway"}, - {vsn, "4.3.0"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, {applications, [kernel,stdlib,gen_coap]}, diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index d465f9ca3..a1633a9be 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -222,7 +222,7 @@ code_change(_OldVsn, State, _Extra) -> chann_subscribe(Topic, State = #state{clientid = ClientId}) -> ?LOG(debug, "subscribe Topic=~p", [Topic]), - case emqx_access_control:check_acl(clientinfo(State), subscribe, Topic) of + case emqx_access_control:check_authz(clientinfo(State), subscribe, Topic) of allow -> emqx_broker:subscribe(Topic, ClientId, ?SUBOPTS), emqx_hooks:run('session.subscribed', [clientinfo(State), Topic, ?SUBOPTS]), @@ -241,7 +241,7 @@ chann_unsubscribe(Topic, State) -> chann_publish(Topic, Payload, State = #state{clientid = ClientId}) -> ?LOG(debug, "publish Topic=~p, Payload=~p", [Topic, Payload]), - case emqx_access_control:check_acl(clientinfo(State), publish, Topic) of + case emqx_access_control:check_authz(clientinfo(State), publish, Topic) of allow -> _ = emqx_broker:publish( emqx_message:set_flag(retain, false, diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl index 416c99018..35975621b 100644 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_SUITE.erl @@ -77,7 +77,7 @@ t_publish_acl_deny(_Config) -> emqx:subscribe(Topic), ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, check_acl, 3, deny), + ok = meck:expect(emqx_access_control, check_authz, 3, deny), Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), ?assertEqual({error,forbidden}, Reply), ok = meck:unload(emqx_access_control), @@ -114,7 +114,7 @@ t_observe_acl_deny(_Config) -> Topic = <<"abc">>, TopicStr = binary_to_list(Topic), Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, check_acl, 3, deny), + ok = meck:expect(emqx_access_control, check_authz, 3, deny), ?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)), [] = emqx:subscribers(Topic), ok = meck:unload(emqx_access_control). @@ -289,7 +289,7 @@ t_acl(Config) -> ok end, - ok = emqx_hooks:del('client.check_acl', {emqx_authz, check_authz}), + ok = emqx_hooks:del('client.check_authz', {emqx_authz, check_authz}), file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), application:set_env(emqx, plugins_etc_dir, OldPath), application:stop(emqx_authz). diff --git a/apps/emqx_exhook/include/emqx_exhook.hrl b/apps/emqx_exhook/include/emqx_exhook.hrl index 7301fdcbb..f640a5916 100644 --- a/apps/emqx_exhook/include/emqx_exhook.hrl +++ b/apps/emqx_exhook/include/emqx_exhook.hrl @@ -25,7 +25,7 @@ , {'client.connected', {emqx_exhook_handler, on_client_connected, []}} , {'client.disconnected', {emqx_exhook_handler, on_client_disconnected, []}} , {'client.authenticate', {emqx_exhook_handler, on_client_authenticate, []}} - , {'client.check_acl', {emqx_exhook_handler, on_client_check_acl, []}} + , {'client.check_authz', {emqx_exhook_handler, on_client_check_authz, []}} , {'client.subscribe', {emqx_exhook_handler, on_client_subscribe, []}} , {'client.unsubscribe', {emqx_exhook_handler, on_client_unsubscribe, []}} , {'session.created', {emqx_exhook_handler, on_session_created, []}} diff --git a/apps/emqx_exhook/priv/protos/exhook.proto b/apps/emqx_exhook/priv/protos/exhook.proto index 72ba26581..3b8fa8861 100644 --- a/apps/emqx_exhook/priv/protos/exhook.proto +++ b/apps/emqx_exhook/priv/protos/exhook.proto @@ -40,7 +40,7 @@ service HookProvider { rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {}; - rpc OnClientCheckAcl(ClientCheckAclRequest) returns (ValuedResponse) {}; + rpc OnClientCheckAuthz(ClientCheckAuthzRequest) returns (ValuedResponse) {}; rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {}; @@ -123,18 +123,18 @@ message ClientAuthenticateRequest { bool result = 2; } -message ClientCheckAclRequest { +message ClientCheckAuthzRequest { ClientInfo clientinfo = 1; - enum AclReqType { + enum AuthzReqType { PUBLISH = 0; SUBSCRIBE = 1; } - AclReqType type = 2; + AuthzReqType type = 2; string topic = 3; @@ -253,7 +253,7 @@ message ValuedResponse { oneof value { - // Boolean result, used on the 'client.authenticate', 'client.check_acl' hooks + // Boolean result, used on the 'client.authenticate', 'client.check_authz' hooks bool bool_result = 3; // Message result, used on the 'message.*' hooks @@ -279,7 +279,7 @@ message HookSpec { // Available value: // "client.connect", "client.connack" // "client.connected", "client.disconnected" - // "client.authenticate", "client.check_acl" + // "client.authenticate", "client.check_authz" // "client.subscribe", "client.unsubscribe" // // "session.created", "session.subscribed" diff --git a/apps/emqx_exhook/src/emqx_exhook_handler.erl b/apps/emqx_exhook/src/emqx_exhook_handler.erl index f3964dc42..695b1116b 100644 --- a/apps/emqx_exhook/src/emqx_exhook_handler.erl +++ b/apps/emqx_exhook/src/emqx_exhook_handler.erl @@ -27,7 +27,7 @@ , on_client_connected/2 , on_client_disconnected/3 , on_client_authenticate/2 - , on_client_check_acl/4 + , on_client_check_authz/4 , on_client_subscribe/3 , on_client_unsubscribe/3 ]). @@ -109,7 +109,7 @@ on_client_authenticate(ClientInfo, AuthResult) -> {ok, AuthResult} end. -on_client_check_acl(ClientInfo, PubSub, Topic, Result) -> +on_client_check_authz(ClientInfo, PubSub, Topic, Result) -> Bool = Result == allow, Type = case PubSub of publish -> 'PUBLISH'; @@ -120,7 +120,7 @@ on_client_check_acl(ClientInfo, PubSub, Topic, Result) -> topic => Topic, result => Bool }, - case call_fold('client.check_acl', Req, + case call_fold('client.check_authz', Req, fun merge_responsed_bool/2) of {StopOrOk, #{result := Result0}} when is_boolean(Result0) -> NResult = case Result0 of true -> allow; _ -> deny end, diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl index f4965e4ca..5c5da4a85 100644 --- a/apps/emqx_exhook/src/emqx_exhook_server.erl +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -58,7 +58,7 @@ | 'client.connected' | 'client.disconnected' | 'client.authenticate' - | 'client.check_acl' + | 'client.check_authz' | 'client.subscribe' | 'client.unsubscribe' | 'session.created' @@ -297,7 +297,7 @@ hk2func('client.connack') -> 'on_client_connack'; hk2func('client.connected') -> 'on_client_connected'; hk2func('client.disconnected') -> 'on_client_disconnected'; hk2func('client.authenticate') -> 'on_client_authenticate'; -hk2func('client.check_acl') -> 'on_client_check_acl'; +hk2func('client.check_authz') -> 'on_client_check_authz'; hk2func('client.subscribe') -> 'on_client_subscribe'; hk2func('client.unsubscribe') -> 'on_client_unsubscribe'; hk2func('session.created') -> 'on_session_created'; @@ -320,7 +320,7 @@ message_hooks() -> -compile({inline, [available_hooks/0]}). available_hooks() -> ['client.connect', 'client.connack', 'client.connected', - 'client.disconnected', 'client.authenticate', 'client.check_acl', + 'client.disconnected', 'client.authenticate', 'client.check_authz', 'client.subscribe', 'client.unsubscribe', 'session.created', 'session.subscribed', 'session.unsubscribed', 'session.resumed', 'session.discarded', 'session.takeovered', diff --git a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl index c2db04dd4..da32a9cf1 100644 --- a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl +++ b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl @@ -33,7 +33,7 @@ , on_client_connected/2 , on_client_disconnected/2 , on_client_authenticate/2 - , on_client_check_acl/2 + , on_client_check_authz/2 , on_client_subscribe/2 , on_client_unsubscribe/2 , on_session_created/2 @@ -122,7 +122,7 @@ on_provider_loaded(Req, Md) -> #{name => <<"client.connected">>}, #{name => <<"client.disconnected">>}, #{name => <<"client.authenticate">>}, - #{name => <<"client.check_acl">>}, + #{name => <<"client.check_authz">>}, #{name => <<"client.subscribe">>}, #{name => <<"client.unsubscribe">>}, #{name => <<"session.created">>}, @@ -197,10 +197,10 @@ on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) -> {ok, #{type => 'IGNORE'}, Md} end. --spec on_client_check_acl(emqx_exhook_pb:client_check_acl_request(), grpc:metadata()) +-spec on_client_check_authz(emqx_exhook_pb:client_check_authz_request(), grpc:metadata()) -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} | {error, grpc_cowboy_h:error_response()}. -on_client_check_acl(#{clientinfo := #{username := Username}} = Req, Md) -> +on_client_check_authz(#{clientinfo := #{username := Username}} = Req, Md) -> ?MODULE:in({?FUNCTION_NAME, Req}), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %% some cases for testing diff --git a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl index 24f45c8b0..f276333dc 100644 --- a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl +++ b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl @@ -109,14 +109,14 @@ prop_client_authenticate() -> true end). -prop_client_check_acl() -> +prop_client_check_authz() -> ?ALL({ClientInfo0, PubSub, Topic, Result}, {clientinfo(), oneof([publish, subscribe]), topic(), oneof([allow, deny])}, begin ClientInfo = inject_magic_into(username, ClientInfo0), OutResult = emqx_hooks:run_fold( - 'client.check_acl', + 'client.check_authz', [ClientInfo, PubSub, Topic], Result), ExpectedOutResult = case maps:get(username, ClientInfo) of @@ -127,7 +127,7 @@ prop_client_check_acl() -> end, ?assertEqual(ExpectedOutResult, OutResult), - {'on_client_check_acl', Resp} = emqx_exhook_demo_svr:take(), + {'on_client_check_authz', Resp} = emqx_exhook_demo_svr:take(), Expected = #{result => aclresult_to_bool(Result), type => pubsub_to_enum(PubSub), diff --git a/apps/emqx_exproto/src/emqx_exproto_channel.erl b/apps/emqx_exproto/src/emqx_exproto_channel.erl index 229e6f930..c76617047 100644 --- a/apps/emqx_exproto/src/emqx_exproto_channel.erl +++ b/apps/emqx_exproto/src/emqx_exproto_channel.erl @@ -305,7 +305,7 @@ handle_call({subscribe, TopicFilter, Qos}, conn_state = connected, clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of + emqx_access_control:check_authz(ClientInfo, subscribe, TopicFilter) of deny -> {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel}; _ -> @@ -325,7 +325,7 @@ handle_call({publish, Topic, Qos, Payload}, = #{clientid := From, mountpoint := Mountpoint}}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_acl(ClientInfo, publish, Topic) of + emqx_access_control:check_authz(ClientInfo, publish, Topic) of deny -> {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel}; _ -> diff --git a/apps/emqx_exproto/test/emqx_exproto_SUITE.erl b/apps/emqx_exproto/test/emqx_exproto_SUITE.erl index db64c7438..fe6fbbb08 100644 --- a/apps/emqx_exproto/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_exproto/test/emqx_exproto_SUITE.erl @@ -167,7 +167,7 @@ t_acl_deny(Cfg) -> Password = <<"123456">>, ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> deny end), + ok = meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> deny end), ConnBin = frame_connect(Client, Password), ConnAckBin = frame_connack(0), diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index c11c8f0d7..94f5baa4f 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -414,8 +414,8 @@ emqx_collect(emqx_client_authenticate, Stats) -> counter_metric(?C('client.authenticate', Stats)); emqx_collect(emqx_client_auth_anonymous, Stats) -> counter_metric(?C('client.auth.anonymous', Stats)); -emqx_collect(emqx_client_check_acl, Stats) -> - counter_metric(?C('client.check_acl', Stats)); +emqx_collect(emqx_client_check_authz, Stats) -> + counter_metric(?C('client.check_authz', Stats)); emqx_collect(emqx_client_subscribe, Stats) -> counter_metric(?C('client.subscribe', Stats)); emqx_collect(emqx_client_unsubscribe, Stats) -> @@ -567,7 +567,7 @@ emqx_metrics_client() -> [ emqx_client_connected , emqx_client_authenticate , emqx_client_auth_anonymous - , emqx_client_check_acl + , emqx_client_check_authz , emqx_client_subscribe , emqx_client_unsubscribe , emqx_client_disconnected From e1b0f44a8a00ac6b8abaaa432b1dee3dc63df2c7 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 28 Jun 2021 18:40:04 +0800 Subject: [PATCH 042/379] chore: rename check_authz to authorize --- apps/emqx/src/emqx_access_control.erl | 18 +++++------ apps/emqx/src/emqx_channel.erl | 4 +-- apps/emqx/src/emqx_metrics.erl | 4 +-- apps/emqx/test/emqx_access_control_SUITE.erl | 4 +-- apps/emqx/test/emqx_acl_test_mod.erl | 4 +-- apps/emqx/test/emqx_channel_SUITE.erl | 2 +- .../emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 2 +- apps/emqx/test/emqx_ws_connection_SUITE.erl | 2 +- apps/emqx_authz/src/emqx_authz.erl | 32 +++++++++---------- apps/emqx_authz/src/emqx_authz_mysql.erl | 12 +++---- apps/emqx_authz/src/emqx_authz_pgsql.erl | 12 +++---- apps/emqx_authz/src/emqx_authz_redis.erl | 12 +++---- apps/emqx_authz/test/emqx_authz_SUITE.erl | 20 ++++++------ .../test/emqx_authz_mysql_SUITE.erl | 24 +++++++------- .../test/emqx_authz_pgsql_SUITE.erl | 24 +++++++------- .../test/emqx_authz_redis_SUITE.erl | 16 +++++----- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 4 +-- apps/emqx_coap/test/emqx_coap_SUITE.erl | 6 ++-- apps/emqx_exhook/include/emqx_exhook.hrl | 2 +- apps/emqx_exhook/priv/protos/exhook.proto | 8 ++--- apps/emqx_exhook/src/emqx_exhook_handler.erl | 6 ++-- apps/emqx_exhook/src/emqx_exhook_server.erl | 6 ++-- .../emqx_exhook/test/emqx_exhook_demo_svr.erl | 8 ++--- .../test/props/prop_exhook_hooks.erl | 6 ++-- .../emqx_exproto/src/emqx_exproto_channel.erl | 4 +-- apps/emqx_exproto/test/emqx_exproto_SUITE.erl | 2 +- apps/emqx_prometheus/src/emqx_prometheus.erl | 6 ++-- 27 files changed, 125 insertions(+), 125 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 8ca9f3893..7b7138bef 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -20,7 +20,7 @@ -export([authenticate/1]). --export([ check_authz/3 +-export([ authorize/3 ]). -type(result() :: #{auth_result := emqx_types:auth_result(), @@ -42,25 +42,25 @@ authenticate(ClientInfo = #{zone := Zone}) -> end. %% @doc Check ACL --spec(check_authz(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) +-spec(authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). -check_authz(ClientInfo, PubSub, Topic) -> +authorize(ClientInfo, PubSub, Topic) -> case emqx_acl_cache:is_enabled() of - true -> check_authz_cache(ClientInfo, PubSub, Topic); - false -> do_check_authz(ClientInfo, PubSub, Topic) + true -> authorize_cache(ClientInfo, PubSub, Topic); + false -> do_authorize(ClientInfo, PubSub, Topic) end. -check_authz_cache(ClientInfo, PubSub, Topic) -> +authorize_cache(ClientInfo, PubSub, Topic) -> case emqx_acl_cache:get_acl_cache(PubSub, Topic) of not_found -> - AclResult = do_check_authz(ClientInfo, PubSub, Topic), + AclResult = do_authorize(ClientInfo, PubSub, Topic), emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), AclResult; AclResult -> AclResult end. -do_check_authz(ClientInfo, PubSub, Topic) -> - case run_hooks('client.check_authz', [ClientInfo, PubSub, Topic], allow) of +do_authorize(ClientInfo, PubSub, Topic) -> + case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], allow) of allow -> allow; _Other -> deny end. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 2fcad5b21..99f8cb5df 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1406,7 +1406,7 @@ check_pub_alias(_Packet, _Channel) -> ok. check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #channel{clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_authz(ClientInfo, publish, Topic) of + emqx_access_control:authorize(ClientInfo, publish, Topic) of false -> ok; allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} @@ -1440,7 +1440,7 @@ check_sub_acls([], _Channel, Acc) -> check_sub_acl(TopicFilter, #channel{clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_authz(ClientInfo, subscribe, TopicFilter) of + emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of false -> allow; Result -> Result end. diff --git a/apps/emqx/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl index c583276d8..cd0039791 100644 --- a/apps/emqx/src/emqx_metrics.erl +++ b/apps/emqx/src/emqx_metrics.erl @@ -172,7 +172,7 @@ {counter, 'client.connected'}, {counter, 'client.authenticate'}, {counter, 'client.auth.anonymous'}, - {counter, 'client.check_authz'}, + {counter, 'client.authorize'}, {counter, 'client.subscribe'}, {counter, 'client.unsubscribe'}, {counter, 'client.disconnected'} @@ -563,7 +563,7 @@ reserved_idx('client.connected') -> 202; reserved_idx('client.authenticate') -> 203; reserved_idx('client.enhanced_authenticate') -> 204; reserved_idx('client.auth.anonymous') -> 205; -reserved_idx('client.check_authz') -> 206; +reserved_idx('client.authorize') -> 206; reserved_idx('client.subscribe') -> 207; reserved_idx('client.unsubscribe') -> 208; reserved_idx('client.disconnected') -> 209; diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index 82a0c1669..b356402fb 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -38,9 +38,9 @@ t_authenticate(_) -> emqx_zone:set_env(zone, allow_anonymous, true), ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). -t_check_authz(_) -> +t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), - ?assertEqual(allow, emqx_access_control:check_authz(clientinfo(), Publish, <<"t">>)). + ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish, <<"t">>)). t_bypass_auth_plugins(_) -> ClientInfo = clientinfo(), diff --git a/apps/emqx/test/emqx_acl_test_mod.erl b/apps/emqx/test/emqx_acl_test_mod.erl index be461d584..f88e0354b 100644 --- a/apps/emqx/test/emqx_acl_test_mod.erl +++ b/apps/emqx/test/emqx_acl_test_mod.erl @@ -18,14 +18,14 @@ %% ACL callbacks -export([ init/1 - , check_authz/2 + , authorize/2 , description/0 ]). init(AclOpts) -> {ok, AclOpts}. -check_authz({_User, _PubSub, _Topic}, _State) -> +authorize({_User, _PubSub, _Topic}, _State) -> allow. description() -> diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index cc77acb56..09ac7a683 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -37,7 +37,7 @@ init_per_suite(Config) -> ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, fun(_) -> {ok, #{auth_result => success}} end), - ok = meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> allow end), + ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), %% Hooks Meck diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index e60d52a86..ab4d96eea 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -198,7 +198,7 @@ t_batch_subscribe(_) -> {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>}]), {ok, _} = emqtt:connect(Client), ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> deny end), + meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end), {ok, _, [?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED]} = emqtt:subscribe(Client, [{<<"t1">>, qos1}, diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index b5020439b..93c192b86 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -64,7 +64,7 @@ init_per_testcase(TestCase, Config) when end), %% Mock emqx_access_control ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> allow end), + ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Mock emqx_hooks ok = meck:new(emqx_hooks, [passthrough, no_history, no_link]), ok = meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end), diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index b757c8908..8f6ae5f2b 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -26,7 +26,7 @@ , compile/1 , lookup/0 , update/1 - , check_authz/5 + , authorize/5 , match/4 ]). @@ -41,7 +41,7 @@ init() -> #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf), ok = application:set_env(?APP, rules, Rules), NRules = [compile(Rule) || Rule <- Rules], - ok = emqx_hooks:add('client.check_authz', {?MODULE, check_authz, [NRules]}, -1). + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> application:get_env(?APP, rules, []). @@ -50,8 +50,8 @@ update(Rules) -> ok = application:set_env(?APP, rules, Rules), NRules = [compile(Rule) || Rule <- Rules], Action = find_action_in_hooks(), - ok = emqx_hooks:del('client.check_authz', Action), - ok = emqx_hooks:add('client.check_authz', {?MODULE, check_authz, [NRules]}, -1), + ok = emqx_hooks:del('client.authorize', Action), + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1), ok = emqx_acl_cache:empty_acl_cache(). %%-------------------------------------------------------------------- @@ -59,8 +59,8 @@ update(Rules) -> %%-------------------------------------------------------------------- find_action_in_hooks() -> - Callbacks = emqx_hooks:lookup('client.check_authz'), - [Action] = [Action || {callback,{?MODULE, check_authz, _} = Action, _, _} <- Callbacks ], + Callbacks = emqx_hooks:lookup('client.authorize'), + [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], Action. create_resource(#{<<"type">> := DB, @@ -149,12 +149,12 @@ b2l(B) when is_binary(B) -> binary_to_list(B). %%-------------------------------------------------------------------- %% @doc Check ACL --spec(check_authz(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) +-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) -> {stop, allow} | {ok, deny}). -check_authz(#{username := Username, +authorize(#{username := Username, peerhost := IpAddress } = Client, PubSub, Topic, _DefaultResult, Rules) -> - case do_check_authz(Client, PubSub, Topic, Rules) of + case do_authorize(Client, PubSub, Topic, Rules) of {matched, allow} -> ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), emqx_metrics:inc(?ACL_METRICS(allow)), @@ -168,25 +168,25 @@ check_authz(#{username := Username, {stop, deny} end. -do_check_authz(Client, PubSub, Topic, +do_authorize(Client, PubSub, Topic, [Connector = #{<<"principal">> := Principal, <<"type">> := DB} | Tail] ) -> case match_principal(Client, Principal) of true -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])), - case Mod:check_authz(Client, PubSub, Topic, Connector) of - nomatch -> do_check_authz(Client, PubSub, Topic, Tail); + case Mod:authorize(Client, PubSub, Topic, Connector) of + nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched end; - false -> do_check_authz(Client, PubSub, Topic, Tail) + false -> do_authorize(Client, PubSub, Topic, Tail) end; -do_check_authz(Client, PubSub, Topic, +do_authorize(Client, PubSub, Topic, [#{<<"permission">> := Permission} = Rule | Tail]) -> case match(Client, PubSub, Topic, Rule) of true -> {matched, Permission}; - false -> do_check_authz(Client, PubSub, Topic, Tail) + false -> do_authorize(Client, PubSub, Topic, Tail) end; -do_check_authz(_Client, _PubSub, _Topic, []) -> nomatch. +do_authorize(_Client, _PubSub, _Topic, []) -> nomatch. match(Client, PubSub, Topic, #{<<"principal">> := Principal, diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index c1ab20125..6acb154fb 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -23,7 +23,7 @@ %% ACL Callbacks -export([ description/0 , parse_query/1 - , check_authz/4 + , authorize/4 ]). -ifdef(TEST). @@ -45,25 +45,25 @@ parse_query(Sql) -> {Sql, []} end. -check_authz(Client, PubSub, Topic, +authorize(Client, PubSub, Topic, #{<<"resource_id">> := ResourceID, <<"sql">> := {SQL, Params} }) -> case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> - do_check_authz(Client, PubSub, Topic, Columns, Rows); + do_authorize(Client, PubSub, Topic, Columns, Rows); {error, Reason} -> ?LOG(error, "[AuthZ] Query mysql error: ~p~n", [Reason]), nomatch end. -do_check_authz(_Client, _PubSub, _Topic, _Columns, []) -> +do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; -do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) -> +do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> case match(Client, PubSub, Topic, format_result(Columns, Row)) of {matched, Permission} -> {matched, Permission}; - nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail) + nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. format_result(Columns, Row) -> diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index edea8102f..c7cebf1e2 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -23,7 +23,7 @@ %% ACL Callbacks -export([ description/0 , parse_query/1 - , check_authz/4 + , authorize/4 ]). -ifdef(TEST). @@ -49,25 +49,25 @@ parse_query(Sql) -> {Sql, []} end. -check_authz(Client, PubSub, Topic, +authorize(Client, PubSub, Topic, #{<<"resource_id">> := ResourceID, <<"sql">> := {SQL, Params} }) -> case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> - do_check_authz(Client, PubSub, Topic, Columns, Rows); + do_authorize(Client, PubSub, Topic, Columns, Rows); {error, Reason} -> ?LOG(error, "[AuthZ] Query pgsql error: ~p~n", [Reason]), nomatch end. -do_check_authz(_Client, _PubSub, _Topic, _Columns, []) -> +do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; -do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) -> +do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> case match(Client, PubSub, Topic, format_result(Columns, Row)) of {matched, Permission} -> {matched, Permission}; - nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail) + nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. format_result(Columns, Row) -> diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 7a85b26af..1b99dc2ec 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -21,7 +21,7 @@ -include_lib("emqx/include/logger.hrl"). %% ACL Callbacks --export([ check_authz/4 +-export([ authorize/4 , description/0 ]). @@ -33,7 +33,7 @@ description() -> "AuthZ with redis". -check_authz(Client, PubSub, Topic, +authorize(Client, PubSub, Topic, #{<<"resource_id">> := ResourceID, <<"cmd">> := CMD }) -> @@ -41,22 +41,22 @@ check_authz(Client, PubSub, Topic, case emqx_resource:query(ResourceID, {cmd, NCMD}) of {ok, []} -> nomatch; {ok, Rows} -> - do_check_authz(Client, PubSub, Topic, Rows); + do_authorize(Client, PubSub, Topic, Rows); {error, Reason} -> ?LOG(error, "[AuthZ] Query redis error: ~p", [Reason]), nomatch end. -do_check_authz(_Client, _PubSub, _Topic, []) -> +do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; -do_check_authz(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> +do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> case match(Client, PubSub, Topic, #{topics => TopicFilter, action => Action }) of {matched, Permission} -> {matched, Permission}; - nomatch -> do_check_authz(Client, PubSub, Topic, Tail) + nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. match(Client, PubSub, Topic, diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index d036d1dec..93be27146 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -145,23 +145,23 @@ t_authz(_) -> Rules4 = [emqx_authz:compile(Rule) || Rule <- [?RULE4, ?RULE1]], ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo1, subscribe, <<"#">>, deny, [])), + emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), ?assertEqual({stop, allow}, - emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), ?assertEqual({stop, allow}, - emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), + emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo3, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo3, publish, <<"fake">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo4, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:check_authz(ClientInfo4, publish, <<"fake">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index e4704c5f3..6ee229ec7 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -95,23 +95,23 @@ t_authz(_) -> }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"+">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), - ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), - ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_username">>)), - ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_username">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, subscribe, <<"test">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, publish, <<"test">>)), % nomatch + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, publish, <<"test">>)), % nomatch ok. diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index d03cd3338..8fb9cd3e0 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -95,23 +95,23 @@ t_authz(_) -> }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"+">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo1, publish, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end), - ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo1, subscribe, <<"#">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"+">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"+">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end), - ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_clientid">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo2, subscribe, <<"test/test_username">>)), - ?assertEqual(allow, emqx_access_control:check_authz(ClientInfo2, publish, <<"test/test_username">>)), - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, subscribe, <<"test">>)), % nomatch - ?assertEqual(deny, emqx_access_control:check_authz(ClientInfo3, publish, <<"test">>)), % nomatch + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, publish, <<"test">>)), % nomatch ok. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index ab8465ffa..6e5015b7e 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -84,30 +84,30 @@ t_authz(_) -> meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), % nomatch ?assertEqual(deny, - emqx_access_control:check_authz(ClientInfo, subscribe, <<"#">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"#">>)), ?assertEqual(deny, - emqx_access_control:check_authz(ClientInfo, publish, <<"#">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"#">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end), % nomatch ?assertEqual(deny, - emqx_access_control:check_authz(ClientInfo, subscribe, <<"+">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"+">>)), % nomatch ?assertEqual(deny, - emqx_access_control:check_authz(ClientInfo, subscribe, <<"test/username">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"test/username">>)), ?assertEqual(allow, - emqx_access_control:check_authz(ClientInfo, publish, <<"test/clientid">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"test/clientid">>)), ?assertEqual(allow, - emqx_access_control:check_authz(ClientInfo, publish, <<"test/clientid">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"test/clientid">>)), meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end), ?assertEqual(allow, - emqx_access_control:check_authz(ClientInfo, subscribe, <<"#">>)), + emqx_access_control:authorize(ClientInfo, subscribe, <<"#">>)), % nomatch ?assertEqual(deny, - emqx_access_control:check_authz(ClientInfo, publish, <<"#">>)), + emqx_access_control:authorize(ClientInfo, publish, <<"#">>)), ok. diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index a1633a9be..b93d1c640 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -222,7 +222,7 @@ code_change(_OldVsn, State, _Extra) -> chann_subscribe(Topic, State = #state{clientid = ClientId}) -> ?LOG(debug, "subscribe Topic=~p", [Topic]), - case emqx_access_control:check_authz(clientinfo(State), subscribe, Topic) of + case emqx_access_control:authorize(clientinfo(State), subscribe, Topic) of allow -> emqx_broker:subscribe(Topic, ClientId, ?SUBOPTS), emqx_hooks:run('session.subscribed', [clientinfo(State), Topic, ?SUBOPTS]), @@ -241,7 +241,7 @@ chann_unsubscribe(Topic, State) -> chann_publish(Topic, Payload, State = #state{clientid = ClientId}) -> ?LOG(debug, "publish Topic=~p, Payload=~p", [Topic, Payload]), - case emqx_access_control:check_authz(clientinfo(State), publish, Topic) of + case emqx_access_control:authorize(clientinfo(State), publish, Topic) of allow -> _ = emqx_broker:publish( emqx_message:set_flag(retain, false, diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl index 35975621b..9618425a3 100644 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_SUITE.erl @@ -77,7 +77,7 @@ t_publish_acl_deny(_Config) -> emqx:subscribe(Topic), ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, check_authz, 3, deny), + ok = meck:expect(emqx_access_control, authorize, 3, deny), Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), ?assertEqual({error,forbidden}, Reply), ok = meck:unload(emqx_access_control), @@ -114,7 +114,7 @@ t_observe_acl_deny(_Config) -> Topic = <<"abc">>, TopicStr = binary_to_list(Topic), Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, check_authz, 3, deny), + ok = meck:expect(emqx_access_control, authorize, 3, deny), ?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)), [] = emqx:subscribers(Topic), ok = meck:unload(emqx_access_control). @@ -289,7 +289,7 @@ t_acl(Config) -> ok end, - ok = emqx_hooks:del('client.check_authz', {emqx_authz, check_authz}), + ok = emqx_hooks:del('client.authorize', {emqx_authz, authorize}), file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), application:set_env(emqx, plugins_etc_dir, OldPath), application:stop(emqx_authz). diff --git a/apps/emqx_exhook/include/emqx_exhook.hrl b/apps/emqx_exhook/include/emqx_exhook.hrl index f640a5916..64131735e 100644 --- a/apps/emqx_exhook/include/emqx_exhook.hrl +++ b/apps/emqx_exhook/include/emqx_exhook.hrl @@ -25,7 +25,7 @@ , {'client.connected', {emqx_exhook_handler, on_client_connected, []}} , {'client.disconnected', {emqx_exhook_handler, on_client_disconnected, []}} , {'client.authenticate', {emqx_exhook_handler, on_client_authenticate, []}} - , {'client.check_authz', {emqx_exhook_handler, on_client_check_authz, []}} + , {'client.authorize', {emqx_exhook_handler, on_client_authorize, []}} , {'client.subscribe', {emqx_exhook_handler, on_client_subscribe, []}} , {'client.unsubscribe', {emqx_exhook_handler, on_client_unsubscribe, []}} , {'session.created', {emqx_exhook_handler, on_session_created, []}} diff --git a/apps/emqx_exhook/priv/protos/exhook.proto b/apps/emqx_exhook/priv/protos/exhook.proto index 3b8fa8861..97a011352 100644 --- a/apps/emqx_exhook/priv/protos/exhook.proto +++ b/apps/emqx_exhook/priv/protos/exhook.proto @@ -40,7 +40,7 @@ service HookProvider { rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {}; - rpc OnClientCheckAuthz(ClientCheckAuthzRequest) returns (ValuedResponse) {}; + rpc OnClientAuthorize(ClientAuthorizeRequest) returns (ValuedResponse) {}; rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {}; @@ -123,7 +123,7 @@ message ClientAuthenticateRequest { bool result = 2; } -message ClientCheckAuthzRequest { +message ClientAuthorizeRequest { ClientInfo clientinfo = 1; @@ -253,7 +253,7 @@ message ValuedResponse { oneof value { - // Boolean result, used on the 'client.authenticate', 'client.check_authz' hooks + // Boolean result, used on the 'client.authenticate', 'client.authorize' hooks bool bool_result = 3; // Message result, used on the 'message.*' hooks @@ -279,7 +279,7 @@ message HookSpec { // Available value: // "client.connect", "client.connack" // "client.connected", "client.disconnected" - // "client.authenticate", "client.check_authz" + // "client.authenticate", "client.authorize" // "client.subscribe", "client.unsubscribe" // // "session.created", "session.subscribed" diff --git a/apps/emqx_exhook/src/emqx_exhook_handler.erl b/apps/emqx_exhook/src/emqx_exhook_handler.erl index 695b1116b..db653c52b 100644 --- a/apps/emqx_exhook/src/emqx_exhook_handler.erl +++ b/apps/emqx_exhook/src/emqx_exhook_handler.erl @@ -27,7 +27,7 @@ , on_client_connected/2 , on_client_disconnected/3 , on_client_authenticate/2 - , on_client_check_authz/4 + , on_client_authorize/4 , on_client_subscribe/3 , on_client_unsubscribe/3 ]). @@ -109,7 +109,7 @@ on_client_authenticate(ClientInfo, AuthResult) -> {ok, AuthResult} end. -on_client_check_authz(ClientInfo, PubSub, Topic, Result) -> +on_client_authorize(ClientInfo, PubSub, Topic, Result) -> Bool = Result == allow, Type = case PubSub of publish -> 'PUBLISH'; @@ -120,7 +120,7 @@ on_client_check_authz(ClientInfo, PubSub, Topic, Result) -> topic => Topic, result => Bool }, - case call_fold('client.check_authz', Req, + case call_fold('client.authorize', Req, fun merge_responsed_bool/2) of {StopOrOk, #{result := Result0}} when is_boolean(Result0) -> NResult = case Result0 of true -> allow; _ -> deny end, diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl index 5c5da4a85..a3b132065 100644 --- a/apps/emqx_exhook/src/emqx_exhook_server.erl +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -58,7 +58,7 @@ | 'client.connected' | 'client.disconnected' | 'client.authenticate' - | 'client.check_authz' + | 'client.authorize' | 'client.subscribe' | 'client.unsubscribe' | 'session.created' @@ -297,7 +297,7 @@ hk2func('client.connack') -> 'on_client_connack'; hk2func('client.connected') -> 'on_client_connected'; hk2func('client.disconnected') -> 'on_client_disconnected'; hk2func('client.authenticate') -> 'on_client_authenticate'; -hk2func('client.check_authz') -> 'on_client_check_authz'; +hk2func('client.authorize') -> 'on_client_authorize'; hk2func('client.subscribe') -> 'on_client_subscribe'; hk2func('client.unsubscribe') -> 'on_client_unsubscribe'; hk2func('session.created') -> 'on_session_created'; @@ -320,7 +320,7 @@ message_hooks() -> -compile({inline, [available_hooks/0]}). available_hooks() -> ['client.connect', 'client.connack', 'client.connected', - 'client.disconnected', 'client.authenticate', 'client.check_authz', + 'client.disconnected', 'client.authenticate', 'client.authorize', 'client.subscribe', 'client.unsubscribe', 'session.created', 'session.subscribed', 'session.unsubscribed', 'session.resumed', 'session.discarded', 'session.takeovered', diff --git a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl index da32a9cf1..656788b5e 100644 --- a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl +++ b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl @@ -33,7 +33,7 @@ , on_client_connected/2 , on_client_disconnected/2 , on_client_authenticate/2 - , on_client_check_authz/2 + , on_client_authorize/2 , on_client_subscribe/2 , on_client_unsubscribe/2 , on_session_created/2 @@ -122,7 +122,7 @@ on_provider_loaded(Req, Md) -> #{name => <<"client.connected">>}, #{name => <<"client.disconnected">>}, #{name => <<"client.authenticate">>}, - #{name => <<"client.check_authz">>}, + #{name => <<"client.authorize">>}, #{name => <<"client.subscribe">>}, #{name => <<"client.unsubscribe">>}, #{name => <<"session.created">>}, @@ -197,10 +197,10 @@ on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) -> {ok, #{type => 'IGNORE'}, Md} end. --spec on_client_check_authz(emqx_exhook_pb:client_check_authz_request(), grpc:metadata()) +-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} | {error, grpc_cowboy_h:error_response()}. -on_client_check_authz(#{clientinfo := #{username := Username}} = Req, Md) -> +on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) -> ?MODULE:in({?FUNCTION_NAME, Req}), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %% some cases for testing diff --git a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl index f276333dc..12f54eef6 100644 --- a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl +++ b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl @@ -109,14 +109,14 @@ prop_client_authenticate() -> true end). -prop_client_check_authz() -> +prop_client_authorize() -> ?ALL({ClientInfo0, PubSub, Topic, Result}, {clientinfo(), oneof([publish, subscribe]), topic(), oneof([allow, deny])}, begin ClientInfo = inject_magic_into(username, ClientInfo0), OutResult = emqx_hooks:run_fold( - 'client.check_authz', + 'client.authorize', [ClientInfo, PubSub, Topic], Result), ExpectedOutResult = case maps:get(username, ClientInfo) of @@ -127,7 +127,7 @@ prop_client_check_authz() -> end, ?assertEqual(ExpectedOutResult, OutResult), - {'on_client_check_authz', Resp} = emqx_exhook_demo_svr:take(), + {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(), Expected = #{result => aclresult_to_bool(Result), type => pubsub_to_enum(PubSub), diff --git a/apps/emqx_exproto/src/emqx_exproto_channel.erl b/apps/emqx_exproto/src/emqx_exproto_channel.erl index c76617047..d45f445ab 100644 --- a/apps/emqx_exproto/src/emqx_exproto_channel.erl +++ b/apps/emqx_exproto/src/emqx_exproto_channel.erl @@ -305,7 +305,7 @@ handle_call({subscribe, TopicFilter, Qos}, conn_state = connected, clientinfo = ClientInfo}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_authz(ClientInfo, subscribe, TopicFilter) of + emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of deny -> {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel}; _ -> @@ -325,7 +325,7 @@ handle_call({publish, Topic, Qos, Payload}, = #{clientid := From, mountpoint := Mountpoint}}) -> case is_acl_enabled(ClientInfo) andalso - emqx_access_control:check_authz(ClientInfo, publish, Topic) of + emqx_access_control:authorize(ClientInfo, publish, Topic) of deny -> {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel}; _ -> diff --git a/apps/emqx_exproto/test/emqx_exproto_SUITE.erl b/apps/emqx_exproto/test/emqx_exproto_SUITE.erl index fe6fbbb08..e38347e5e 100644 --- a/apps/emqx_exproto/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_exproto/test/emqx_exproto_SUITE.erl @@ -167,7 +167,7 @@ t_acl_deny(Cfg) -> Password = <<"123456">>, ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_access_control, check_authz, fun(_, _, _) -> deny end), + ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end), ConnBin = frame_connect(Client, Password), ConnAckBin = frame_connack(0), diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 94f5baa4f..29acc72f6 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -414,8 +414,8 @@ emqx_collect(emqx_client_authenticate, Stats) -> counter_metric(?C('client.authenticate', Stats)); emqx_collect(emqx_client_auth_anonymous, Stats) -> counter_metric(?C('client.auth.anonymous', Stats)); -emqx_collect(emqx_client_check_authz, Stats) -> - counter_metric(?C('client.check_authz', Stats)); +emqx_collect(emqx_client_authorize, Stats) -> + counter_metric(?C('client.authorize', Stats)); emqx_collect(emqx_client_subscribe, Stats) -> counter_metric(?C('client.subscribe', Stats)); emqx_collect(emqx_client_unsubscribe, Stats) -> @@ -567,7 +567,7 @@ emqx_metrics_client() -> [ emqx_client_connected , emqx_client_authenticate , emqx_client_auth_anonymous - , emqx_client_check_authz + , emqx_client_authorize , emqx_client_subscribe , emqx_client_unsubscribe , emqx_client_disconnected From c9acf423ba156648912f8139506793b522250365 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Tue, 29 Jun 2021 09:25:13 +0800 Subject: [PATCH 043/379] chore: rename authorze_cache function name to check_authorization_cache --- apps/emqx/src/emqx_access_control.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 7b7138bef..8da3277a9 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -46,11 +46,11 @@ authenticate(ClientInfo = #{zone := Zone}) -> -> allow | deny). authorize(ClientInfo, PubSub, Topic) -> case emqx_acl_cache:is_enabled() of - true -> authorize_cache(ClientInfo, PubSub, Topic); + true -> check_authorization_cache(ClientInfo, PubSub, Topic); false -> do_authorize(ClientInfo, PubSub, Topic) end. -authorize_cache(ClientInfo, PubSub, Topic) -> +check_authorization_cache(ClientInfo, PubSub, Topic) -> case emqx_acl_cache:get_acl_cache(PubSub, Topic) of not_found -> AclResult = do_authorize(ClientInfo, PubSub, Topic), From 2b082f9cf9f606e08fa3106d6ee582993771926b Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Tue, 29 Jun 2021 09:58:02 +0800 Subject: [PATCH 044/379] chore(connector): update connector ssl schema --- .../docker-compose-mysql-tls.yaml | 8 ++-- .../docker-compose-redis-cluster-tls.yaml | 4 +- .../docker-compose-redis-sentinel-tls.yaml | 4 +- .../docker-compose-redis-single-tls.yaml | 10 +++-- .ci/docker-compose-file/pgsql/Dockerfile | 6 +-- .ci/docker-compose-file/redis/redis-tls.conf | 6 +-- .ci/docker-compose-file/redis/redis.sh | 8 ++-- apps/emqx_authz/README.md | 11 +++-- apps/emqx_authz/etc/emqx_authz.conf | 11 +++-- apps/emqx_authz/src/emqx_authz.erl | 6 ++- .../test/emqx_authz_redis_SUITE.erl | 3 +- .../emqx_connector/src/emqx_connector.app.src | 6 ++- .../src/emqx_connector_ldap.erl | 30 ++++++------- .../src/emqx_connector_mongo.erl | 18 ++++---- .../src/emqx_connector_mysql.erl | 10 ++--- .../src/emqx_connector_pgsql.erl | 12 +++--- .../src/emqx_connector_redis.erl | 19 ++++---- .../src/emqx_connector_schema_lib.erl | 43 ++++++++++++------- .../src/emqx_plugin_libs_ssl.erl | 2 +- 19 files changed, 128 insertions(+), 89 deletions(-) diff --git a/.ci/docker-compose-file/docker-compose-mysql-tls.yaml b/.ci/docker-compose-file/docker-compose-mysql-tls.yaml index c4d5bd500..17dfdcc8e 100644 --- a/.ci/docker-compose-file/docker-compose-mysql-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-mysql-tls.yaml @@ -11,9 +11,11 @@ services: MYSQL_USER: ssluser MYSQL_PASSWORD: public volumes: - - ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/ca.pem:/etc/certs/ca-cert.pem - - ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-cert.pem:/etc/certs/server-cert.pem - - ../../apps/emqx_auth_mysql/test/emqx_auth_mysql_SUITE_data/server-key.pem:/etc/certs/server-key.pem + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca-cert.pem + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/server-cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/server-key.pem + ports: + - "3306:3306" networks: - emqx_bridge command: diff --git a/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml index 78b655946..c5cefd9e6 100644 --- a/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml @@ -5,7 +5,9 @@ services: container_name: redis image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key - ./redis/:/data/conf command: bash -c "/bin/bash /data/conf/redis.sh --node cluster --tls-enabled && tail -f /var/log/redis-server.log" networks: diff --git a/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml index 7c7f46ce2..045570d5c 100644 --- a/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml @@ -5,7 +5,9 @@ services: container_name: redis image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key - ./redis/:/data/conf command: bash -c "/bin/bash /data/conf/redis.sh --node sentinel --tls-enabled && tail -f /var/log/redis-server.log" networks: diff --git a/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml index 814a0f1cb..bb6c3ff15 100644 --- a/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-single-tls.yaml @@ -5,15 +5,17 @@ services: container_name: redis image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx_auth_redis/test/emqx_auth_redis_SUITE_data/certs:/tls + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key command: - redis-server - "--bind 0.0.0.0 ::" - --requirepass public - --tls-port 6380 - - --tls-cert-file /tls/redis.crt - - --tls-key-file /tls/redis.key - - --tls-ca-cert-file /tls/ca.crt + - --tls-cert-file /etc/certs/redis.crt + - --tls-key-file /etc/certs/redis.key + - --tls-ca-cert-file /etc/certs/ca.crt restart: always networks: - emqx_bridge diff --git a/.ci/docker-compose-file/pgsql/Dockerfile b/.ci/docker-compose-file/pgsql/Dockerfile index e4c973258..db2cd59fe 100644 --- a/.ci/docker-compose-file/pgsql/Dockerfile +++ b/.ci/docker-compose-file/pgsql/Dockerfile @@ -2,9 +2,9 @@ ARG BUILD_FROM=postgres:11 FROM ${BUILD_FROM} ARG POSTGRES_USER=postgres COPY --chown=$POSTGRES_USER .ci/docker-compose-file/pgsql/pg_hba.conf /var/lib/postgresql/pg_hba.conf -COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-key.pem /var/lib/postgresql/server.key -COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/server-cert.pem /var/lib/postgresql/server.crt -COPY --chown=$POSTGRES_USER apps/emqx_auth_pgsql/test/emqx_auth_pgsql_SUITE_data/ca.pem /var/lib/postgresql/root.crt +COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/key.pem /var/lib/postgresql/server.key +COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/cert.pem /var/lib/postgresql/server.crt +COPY --chown=$POSTGRES_USER apps/emqx/etc/certs/cacert.pem /var/lib/postgresql/root.crt RUN chmod 600 /var/lib/postgresql/pg_hba.conf RUN chmod 600 /var/lib/postgresql/server.key RUN chmod 600 /var/lib/postgresql/server.crt diff --git a/.ci/docker-compose-file/redis/redis-tls.conf b/.ci/docker-compose-file/redis/redis-tls.conf index 325c200c3..e304c814f 100644 --- a/.ci/docker-compose-file/redis/redis-tls.conf +++ b/.ci/docker-compose-file/redis/redis-tls.conf @@ -1,9 +1,9 @@ daemonize yes bind 0.0.0.0 :: logfile /var/log/redis-server.log -tls-cert-file /tls/redis.crt -tls-key-file /tls/redis.key -tls-ca-cert-file /tls/ca.crt +tls-cert-file /etc/certs/redis.crt +tls-key-file /etc/certs/redis.key +tls-ca-cert-file /etc/certs/ca.crt tls-replication yes tls-cluster yes protected-mode no diff --git a/.ci/docker-compose-file/redis/redis.sh b/.ci/docker-compose-file/redis/redis.sh index 272a5b443..6cc7ce98b 100755 --- a/.ci/docker-compose-file/redis/redis.sh +++ b/.ci/docker-compose-file/redis/redis.sh @@ -91,7 +91,7 @@ do fi if [ "${node}" = "cluster" ] ; then if $tls ; then - yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" --pass public --no-auth-warning --tls true --cacert /tls/ca.crt --cert /tls/redis.crt --key /tls/redis.key; + yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" --pass public --no-auth-warning --tls true --cacert /etc/certs/ca.crt --cert /etc/certs/redis.crt --key /etc/certs/redis.key; else yes "yes" | redis-cli --cluster create "$LOCAL_IP:7000" "$LOCAL_IP:7001" "$LOCAL_IP:7002" --pass public --no-auth-warning; fi @@ -107,9 +107,9 @@ EOF cat >>/_sentinel.conf<> := DB, <<"config">> := Config } = Rule) -> ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]), + NConfig = case DB of + redis -> #{<<"config">> => Config }; + _ -> Config + end, case emqx_resource:check_and_create( ResourceID, list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - #{<<"config">> => Config }) + NConfig) of {ok, _} -> Rule#{<<"resource_id">> => ResourceID}; diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 6e5015b7e..a4045a9e4 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -55,7 +55,8 @@ set_special_configs(emqx_authz) -> <<"server">> => <<"127.0.0.1:6379">>, <<"password">> => <<"public">>, <<"pool_size">> => 1, - <<"auto_reconnect">> => true + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false} }, <<"principal">> => all, <<"cmd">> => <<"fake cmd">>, diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 5ff7d0828..6eb22cfe5 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -6,9 +6,13 @@ {applications, [kernel, stdlib, + ecpool, emqx_resource, eredis_cluster, - ecpool + eredis, + epgsql, + mysql, + mongodb ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index ca5ff2482..8c0504d53 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -38,7 +38,7 @@ structs() -> [""]. fields("") -> - redis_fields() ++ + ldap_fields() ++ emqx_connector_schema_lib:ssl_fields(). on_jsonify(Config) -> @@ -51,10 +51,17 @@ on_start(InstId, #{servers := Servers0, bind_password := BindPassword, timeout := Timeout, pool_size := PoolSize, - auto_reconnect := AutoReconn} = Config) -> - logger:info("starting redis connector: ~p, config: ~p", [InstId, Config]), + auto_reconnect := AutoReconn, + ssl := SSL} = Config) -> + logger:info("starting ldap connector: ~p, config: ~p", [InstId, Config]), Servers = [begin proplists:get_value(host, S) end || S <- Servers0], - SslOpts = init_ssl_opts(Config, InstId), + SslOpts = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {sslopts, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end, Opts = [{servers, Servers}, {port, Port}, {bind_dn, BindDn}, @@ -68,14 +75,14 @@ on_start(InstId, #{servers := Servers0, {ok, #{poolname => PoolName}}. on_stop(InstId, #{poolname := PoolName}) -> - logger:info("stopping redis connector: ~p", [InstId]), + logger:info("stopping ldap connector: ~p", [InstId]), emqx_plugin_libs_pool:stop_pool(PoolName). on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) -> - logger:debug("redis connector ~p received request: ~p, at state: ~p", [InstId, {Base, Filter, Attributes}, State]), + logger:debug("ldap connector ~p received request: ~p, at state: ~p", [InstId, {Base, Filter, Attributes}, State]), case Result = ecpool:pick_and_do(PoolName, {?MODULE, search, [Base, Filter, Attributes]}, no_handover) of {error, Reason} -> - logger:debug("redis connector ~p do request failed, request: ~p, reason: ~p", [InstId, {Base, Filter, Attributes}, Reason]), + logger:debug("ldap connector ~p do request failed, request: ~p, reason: ~p", [InstId, {Base, Filter, Attributes}, Reason]), emqx_resource:query_failed(AfterQuery); _ -> emqx_resource:query_success(AfterQuery) @@ -116,14 +123,7 @@ connect(Opts) -> ok = eldap2:simple_bind(LDAP, BindDn, BindPassword), {ok, LDAP}. -init_ssl_opts(#{ssl := true} = Config, InstId) -> - [{ssl, true}, - {sslopts, emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)} - ]; -init_ssl_opts(_Config, _InstId) -> - [{ssl, false}]. - -redis_fields() -> +ldap_fields() -> [ {servers, fun emqx_connector_schema_lib:servers/1} , {port, fun port/1} , {pool_size, fun emqx_connector_schema_lib:pool_size/1} diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 841cc0a2a..9b5609c2f 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -48,9 +48,16 @@ on_jsonify(Config) -> on_start(InstId, #{servers := Servers, mongo_type := Type, database := Database, - pool_size := PoolSize} = Config) -> + pool_size := PoolSize, + ssl := SSL} = Config) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), - SslOpts = init_ssl_opts(Config, InstId), + SslOpts = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end, Hosts = [string:trim(H) || H <- string:tokens(binary_to_list(Servers), ",")], Opts = [{type, init_type(Type, Config)}, {hosts, Hosts}, @@ -157,13 +164,6 @@ init_worker_options([_ | R], Acc) -> init_worker_options(R, Acc); init_worker_options([], Acc) -> Acc. -init_ssl_opts(#{ssl := true} = Config, InstId) -> - [{ssl, true}, - {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)} - ]; -init_ssl_opts(_Config, _InstId) -> - [{ssl, false}]. - host_port(HostPort) -> case string:split(HostPort, ":") of [Host, Port] -> diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index afe249682..a606bb82d 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -51,14 +51,14 @@ on_start(InstId, #{server := {Host, Port}, username := User, password := Password, auto_reconnect := AutoReconn, - pool_size := PoolSize} = Config) -> + pool_size := PoolSize, + ssl := SSL } = Config) -> logger:info("starting mysql connector: ~p, config: ~p", [InstId, Config]), - SslOpts = case maps:get(ssl, Config) of + SslOpts = case maps:get(enable, SSL) of true -> [{ssl, [{server_name_indication, disable} | - emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)]}]; - false -> - [] + emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)]}]; + false -> [] end, Options = [{host, Host}, {port, Port}, diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 827a9606c..ddcc2a7c7 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -50,14 +50,14 @@ on_start(InstId, #{server := {Host, Port}, username := User, password := Password, auto_reconnect := AutoReconn, - pool_size := PoolSize} = Config) -> + pool_size := PoolSize, + ssl := SSL } = Config) -> logger:info("starting postgresql connector: ~p, config: ~p", [InstId, Config]), - SslOpts = case maps:get(ssl, Config) of + SslOpts = case maps:get(enable, SSL) of true -> - [{ssl_opts, [{server_name_indication, disable} | - emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)]}]; - false -> - [] + [{ssl, [{server_name_indication, disable} | + emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)]}]; + false -> [] end, Options = [{host, Host}, {port, Port}, diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 1aa6263b6..4e1dc1773 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -81,7 +81,8 @@ on_jsonify(Config) -> on_start(InstId, #{config :=#{redis_type := Type, database := Database, pool_size := PoolSize, - auto_reconnect := AutoReconn} = Config}) -> + auto_reconnect := AutoReconn, + ssl := SSL } = Config}) -> logger:info("starting redis connector: ~p, config: ~p", [InstId, Config]), Servers = case Type of single -> [{servers, [maps:get(server, Config)]}]; @@ -92,8 +93,13 @@ on_start(InstId, #{config :=#{redis_type := Type, {password, maps:get(password, Config, "")}, {auto_reconnect, reconn_interval(AutoReconn)} ] ++ Servers, - Options = init_ssl_opts(Config, InstId) ++ - [{sentinel, maps:get(sentinel, Config, undefined)}], + Options = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {ssl_options, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case Type of cluster -> @@ -157,13 +163,6 @@ cmd(Conn, _Type, Command) -> connect(Opts) -> eredis:start_link(Opts). -init_ssl_opts(#{ssl := true} = Config, InstId) -> - [{ssl, true}, - {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(Config, "connectors", InstId)} - ]; -init_ssl_opts(_Config, _InstId) -> - [{ssl, false}]. - redis_fields() -> [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} , {password, fun emqx_connector_schema_lib:password/1} diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 03572d91d..17069c7f0 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -51,6 +51,31 @@ , servers/0 ]). +-export([structs/0, fields/1]). + +structs() -> [ssl_on, ssl_off]. + +fields(ssl_on) -> + [ {enable, #{type => true}} + , {cacertfile, fun cacertfile/1} + , {keyfile, fun keyfile/1} + , {certfile, fun certfile/1} + , {verify, fun verify/1} + ]; + +fields(ssl_off) -> + [ {enable, #{type => false}} ]. + +ssl_fields() -> + [ {ssl, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, ssl_on) + , hoconsc:ref(?MODULE, ssl_off) + ]), + default => hoconsc:ref(?MODULE, ssl_off) + } + } + ]. + relational_db_fields() -> [ {server, fun server/1} , {database, fun database/1} @@ -60,14 +85,6 @@ relational_db_fields() -> , {auto_reconnect, fun auto_reconnect/1} ]. -ssl_fields() -> - [ {ssl, fun ssl/1} - , {cacertfile, fun cacertfile/1} - , {keyfile, fun keyfile/1} - , {certfile, fun certfile/1} - , {verify, fun verify/1} - ]. - server(type) -> emqx_schema:ip_port(); server(validator) -> [?REQUIRED("the field 'server' is required")]; server(_) -> undefined. @@ -93,19 +110,15 @@ auto_reconnect(type) -> boolean(); auto_reconnect(default) -> true; auto_reconnect(_) -> undefined. -ssl(type) -> boolean(); -ssl(default) -> false; -ssl(_) -> undefined. - -cacertfile(type) -> binary(); +cacertfile(type) -> string(); cacertfile(default) -> ""; cacertfile(_) -> undefined. -keyfile(type) -> binary(); +keyfile(type) -> string(); keyfile(default) -> ""; keyfile(_) -> undefined. -certfile(type) -> binary(); +certfile(type) -> string(); certfile(default) -> ""; certfile(_) -> undefined. diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl index 9a17765c6..f6f449f06 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl @@ -57,7 +57,7 @@ save_files_return_opts(Options, Dir) -> Get = fun(Key) -> GetD(Key, undefined) end, KeyFile = Get(keyfile), CertFile = Get(certfile), - CAFile = GetD(cacertfile, Get(cafile)), + CAFile = Get(cacertfile), Key = do_save_file(KeyFile, Dir), Cert = do_save_file(CertFile, Dir), CA = do_save_file(CAFile, Dir), From 02c9a3163b9ad1bb2ee5865f36749338545cf708 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 30 Jun 2021 09:39:03 +0800 Subject: [PATCH 045/379] refactor(config): emqx.conf for 5.0 --- apps/emqx/etc/emqx.conf | 4581 ++++++++++++++++----------------- apps/emqx/etc/emqx.conf.old | 2467 ++++++++++++++++++ apps/emqx/src/emqx_schema.erl | 507 ++-- 3 files changed, 4853 insertions(+), 2702 deletions(-) create mode 100644 apps/emqx/etc/emqx.conf.old diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index d2b5fd11d..68d8f359f 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1,2467 +1,2182 @@ -## EMQ X Configuration 4.3 +## master-88df1713 -## NOTE: Do not change format of CONFIG_SECTION_{BGN,END} comments! +## NOTE: The configurations in this file will be overridden by +## `/data/emqx_overrides.conf` -## CONFIG_SECTION_BGN=cluster ================================================== - -## Cluster name. -## -## Value: String -cluster.name = emqxcl - -## Specify the erlang distributed protocol. -## -## Value: Enum -## - inet_tcp: the default; handles TCP streams with IPv4 addressing. -## - inet6_tcp: handles TCP with IPv6 addressing. -## - inet_tls: using TLS for Erlang Distribution. -## -## vm.args: -proto_dist inet_tcp -cluster.proto_dist = inet_tcp - -## Cluster auto-discovery strategy. -## -## Value: Enum -## - manual: Manual join command -## - static: Static node list -## - mcast: IP Multicast -## - dns: DNS A Record -## - etcd: etcd -## - k8s: Kubernetes -## -## Default: manual -cluster.discovery = manual - -## Enable cluster autoheal from network partition. -## -## Value: on | off -## -## Default: on -cluster.autoheal = on - -## Autoclean down node. A down node will be removed from the cluster -## if this value > 0. -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 5m -cluster.autoclean = 5m - -##-------------------------------------------------------------------- -## Cluster using static node list - -## Node list of the cluster. -## -## Value: String -## cluster.static.seeds = "emqx1@127.0.0.1,emqx2@127.0.0.1" - -##-------------------------------------------------------------------- -## Cluster using IP Multicast. - -## IP Multicast Address. -## -## Value: IP Address -## cluster.mcast.addr = "239.192.0.1" - -## Multicast Ports. -## -## Value: Port List -## cluster.mcast.ports = "4369,4370" - -## Multicast Iface. -## -## Value: Iface Address -## -## Default: "0.0.0.0" -## cluster.mcast.iface = "0.0.0.0" - -## Multicast Ttl. -## -## Value: 0-255 -## cluster.mcast.ttl = 255 - -## Multicast loop. -## -## Value: on | off -## cluster.mcast.loop = on - -##-------------------------------------------------------------------- -## Cluster using DNS A records. - -## DNS name. -## -## Value: String -## cluster.dns.name = localhost - -## The App name is used to build 'node.name' with IP address. -## -## Value: String -## cluster.dns.app = emqx - -##-------------------------------------------------------------------- -## Cluster using etcd - -## Etcd server list, seperated by ','. -## -## Value: String -## cluster.etcd.server = "http://127.0.0.1:2379" - -## Etcd api version -## -## Value: Enum -## - v2 -## - v3 -## cluster.etcd.version = v3 - -## The prefix helps build nodes path in etcd. Each node in the cluster -## will create a path in etcd: v2/keys/// -## -## Value: String -## cluster.etcd.prefix = emqxcl - -## The TTL for node's path in etcd. -## -## Value: Duration -## -## Default: 1m, 1 minute -## cluster.etcd.node_ttl = 1m - -## Path to a file containing the client's private PEM-encoded key. -## -## Value: File -## cluster.etcd.ssl.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" - -## The path to a file containing the client's certificate. -## -## Value: File -## cluster.etcd.ssl.certfile = "{{ platform_etc_dir }}/certs/client.pem" - -## Path to the file containing PEM-encoded CA certificates. The CA certificates -## are used during server authentication and when building the client certificate chain. -## -## Value: File -## cluster.etcd.ssl.cacertfile = "{{ platform_etc_dir }}/certs/ca.pem" - -##-------------------------------------------------------------------- -## Cluster using Kubernetes - -## Kubernetes API server list, seperated by ','. -## -## Value: String -## cluster.k8s.apiserver = "http://10.110.111.204:8080" - -## The service name helps lookup EMQ nodes in the cluster. -## -## Value: String -## cluster.k8s.service_name = emqx - -## The address type is used to extract host from k8s service. -## -## Value: ip | dns | hostname -## cluster.k8s.address_type = ip - -## The app name helps build 'node.name'. -## -## Value: String -## cluster.k8s.app_name = emqx - -## The suffix added to dns and hostname get from k8s service -## -## Value: String -## cluster.k8s.suffix = pod.cluster.local - -## Kubernetes Namespace -## -## Value: String -## cluster.k8s.namespace = default - -## CONFIG_SECTION_END=cluster ================================================== - -##-------------------------------------------------------------------- +##================================================================== ## Node -##-------------------------------------------------------------------- - -## Node name. -## -## See: http://erlang.org/doc/reference_manual/distributed.html -## -## Value: @ -## -## Default: emqx@127.0.0.1 -node.name = "emqx@127.0.0.1" - -## Cookie for distributed node communication. -## -## Value: String -node.cookie = "emqxsecretcookie" - -## Data dir for the node -## -## Value: Folder -node.data_dir = "{{ platform_data_dir }}" - -## The config file dir for the node -## -## Value: Folder -node.etc_dir = "{{ platform_etc_dir }}" - -## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable -## heartbeat, or set the value as 'on' -## -## Value: on -## -## vm.args: -heart -## node.heartbeat = on - -## Sets the number of threads in async thread pool. Valid range is 0-1024. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: 0-1024 -## -## vm.args: +A Number -## node.async_threads = 4 - -## Sets the maximum number of simultaneously existing processes for this -## system if a Number is passed as value. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1024-134217727] -## -## vm.args: +P Number -## node.process_limit = 2097152 - -## Sets the maximum number of simultaneously existing ports for this system. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1024-134217727] -## -## vm.args: +Q Number -## node.max_ports = 1048576 - -## Sets the distribution buffer busy limit (dist_buf_busy_limit). -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1KB-2GB] -## -## vm.args: +zdbbl size -## node.dist_buffer_size = 8MB - -## Sets the maximum number of ETS tables. Note that mnesia and SSL will -## create temporary ETS tables. -## -## Value: Number -## -## vm.args: +e Number -## node.max_ets_tables = 262144 - -## Global GC Interval. -## -## Value: Duration -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 20s: 20 seconds -## -## Defaut: 15 minutes -node.global_gc_interval = 15m - -## Tweak GC to run more often. -## -## Value: Number [0-65535] -## -## vm.args: -env ERL_FULLSWEEP_AFTER Number -## node.fullsweep_after = 1000 - -## Crash dump log file. -## -## Value: Log file -node.crash_dump = "{{ platform_log_dir }}/crash.dump" - -## Specify SSL Options in the file if using SSL for Erlang Distribution. -## -## Value: File -## -## vm.args: -ssl_dist_optfile -## node.ssl_dist_optfile = "{{ platform_etc_dir }}/ssl_dist.conf" - -## Sets the net_kernel tick time. TickTime is specified in seconds. -## Notice that all communicating nodes are to have the same TickTime -## value specified. -## -## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime -## -## Value: Number -## -## vm.args: -kernel net_ticktime Number -## node.dist_net_ticktime = 120 - -## Sets the port range for the listener socket of a distributed Erlang node. -## Note that if there are firewalls between clustered nodes, this port segment -## for nodes’ communication should be allowed. -## -## See: http://www.erlang.org/doc/man/kernel_app.html -## -## Value: Port [1024-65535] -node.dist_listen_min = 6369 -node.dist_listen_max = 6369 - -node.backtrace_depth = 16 - -## CONFIG_SECTION_BGN=rpc ====================================================== - -## RPC Mode. -## -## Value: sync | async -rpc.mode = async - -## Max batch size of async RPC requests. -## -## Value: Integer -## Zero or negative value disables rpc batching. -## -## NOTE: RPC batch won't work when rpc.mode = sync -rpc.async_batch_size = 256 - -## RPC port discovery -## -## The strategy for discovering the RPC listening port of other nodes. -## -## Value: Enum -## - manual: discover ports by `tcp_server_port` and `tcp_client_port`. -## - stateless: discover ports in a stateless manner. -## If node name is `emqx@127.0.0.1`, where the `` is an integer, -## then the listening port will be `5370 + ` -## -## Defaults to `stateless`. -rpc.port_discovery = stateless - -## TCP port number for RPC server to listen on. -## -## Only takes effect when `rpc.port_discovery` = `manual`. -## -## NOTE: All nodes in the cluster should agree to this same config. -## -## Value: Port [1024-65535] -#rpc.tcp_server_port = 5369 - -## Number of outgoing RPC connections. -## -## Value: Interger [0-256] -## Defaults to NumberOfCPUSchedulers / 2 when set to 0 -#rpc.tcp_client_num = 0 - -## RCP Client connect timeout. -## -## Value: Seconds -rpc.connect_timeout = 5s - -## TCP send timeout of RPC client and server. -## -## Value: Seconds -rpc.send_timeout = 5s - -## Authentication timeout -## -## Value: Seconds -rpc.authentication_timeout = 5s - -## Default receive timeout for call() functions -## -## Value: Seconds -rpc.call_receive_timeout = 15s - -## Socket idle keepalive. -## -## Value: Seconds -rpc.socket_keepalive_idle = 900s - -## TCP Keepalive probes interval. -## -## Value: Seconds -rpc.socket_keepalive_interval = 75s - -## Probes lost to close the connection -## -## Value: Integer -rpc.socket_keepalive_count = 9 - -## Size of TCP send buffer. -## -## Value: Bytes -rpc.socket_sndbuf = 1MB - -## Size of TCP receive buffer. -## -## Value: Seconds -rpc.socket_recbuf = 1MB - -## Size of user-level software socket buffer. -## -## Value: Seconds -rpc.socket_buffer = 1MB - -## CONFIG_SECTION_END=rpc ====================================================== - -## CONFIG_SECTION_BGN=logger =================================================== - -## Where to emit the logs. -## Enable the console (standard output) logs. -## -## Value: file | console | both -## - file: write logs only to file -## - console: write logs only to standard I/O -## - both: write logs both to file and standard I/O -log.to = file - -## The log severity level. -## -## Value: debug | info | notice | warning | error | critical | alert | emergency -## -## Note: Only the messages with severity level higher than or equal to -## this level will be logged. -## -## Default: warning -log.level = warning - -## Timezone offset to display in logs -## Value: -## - "system" use system zone -## - "utc" for Universal Coordinated Time (UTC) -## - "+hh:mm" or "-hh:mm" for a specified offset -log.time_offset = system - -## The dir for log files. -## -## Value: Folder -log.dir = "{{ platform_log_dir }}" - -## The log filename for logs of level specified in "log.level". -## -## If `log.rotation` is enabled, this is the base name of the -## files. Each file in a rotated log is named .N, where N is an integer. -## -## Value: String -## Default: emqx.log -log.file = emqx.log - -## Limits the total number of characters printed for each log event. -## -## Value: Integer -## Default: No Limit -#log.chars_limit = 8192 - -## Maximum depth for Erlang term log formatting -## and Erlang process message queue inspection. -## -## Value: Integer or 'unlimited' (without quotes) -## Default: 80 -#log.max_depth = 80 - -## Log formatter -## Value: text | json -#log.formatter = text - -## Log to single line -## Value: Boolean -#log.single_line = true - -## Enables the log rotation. -## With this enabled, new log files will be created when the current -## log file is full, max to `log.rotation.size` files will be created. -## -## Value: on | off -## Default: on -log.rotation.enable = on - -## Maximum size of each log file. -## -## Value: Number -## Default: 10M -## Supported Unit: KB | MB | GB -log.rotation.size = 10MB - -## Maximum rotation count of log files. -## -## Value: Number -## Default: 5 -log.rotation.count = 5 - -## To create additional log files for specific log levels. -## -## Value: File Name -## Format: log.$level.file = $filename, -## where "$level" can be one of: debug, info, notice, warning, -## error, critical, alert, emergency -## Note: Log files for a specific log level will only contain all the logs -## that higher than or equal to that level -## -#log.info.file = info.log -#log.error.file = error.log - -## The max allowed queue length before switching to sync mode. -## -## Log overload protection parameter. If the message queue grows -## larger than this value the handler switches from anync to sync mode. -## -## Default: 100 -## -#log.sync_mode_qlen = 100 - -## The max allowed queue length before switching to drop mode. -## -## Log overload protection parameter. When the message queue grows -## larger than this threshold, the handler switches to a mode in which -## it drops all new events that senders want to log. -## -## Default: 3000 -## -#log.drop_mode_qlen = 3000 - -## The max allowed queue length before switching to flush mode. -## -## Log overload protection parameter. If the length of the message queue -## grows larger than this threshold, a flush (delete) operation takes place. -## To flush events, the handler discards the messages in the message queue -## by receiving them in a loop without logging. -## -## Default: 8000 -## -#log.flush_qlen = 8000 - -## Kill the log handler when it gets overloaded. -## -## Log overload protection parameter. It is possible that a handler, -## even if it can successfully manage peaks of high load without crashing, -## can build up a large message queue, or use a large amount of memory. -## We could kill the log handler in these cases and restart it after a -## few seconds. -## -## Default: on -## -#log.overload_kill = on - -## The max allowed queue length before killing the log hanlder. -## -## Log overload protection parameter. This is the maximum allowed queue -## length. If the message queue grows larger than this, the handler -## process is terminated. -## -## Default: 20000 -## -#log.overload_kill_qlen = 20000 - -## The max allowed memory size before killing the log hanlder. -## -## Log overload protection parameter. This is the maximum memory size -## that the handler process is allowed to use. If the handler grows -## larger than this, the process is terminated. -## -## Default: 30MB -## -#log.overload_kill_mem_size = 30MB - -## Restart the log hanlder after some seconds. -## -## Log overload protection parameter. If the handler is terminated, -## it restarts automatically after a delay specified in seconds. -## The value "infinity" prevents restarts. -## -## Default: 5s -## -#log.overload_kill_restart_after = 5s - -## Max burst count and time window for burst control. -## -## Log overload protection parameter. Large bursts of log events - many -## events received by the handler under a short period of time - can -## potentially cause problems. By specifying the maximum number of events -## to be handled within a certain time frame, the handler can avoid -## choking the log with massive amounts of printouts. -## -## This config controls the maximum number of events to handle within -## a time frame. After the limit is reached, successive events are -## dropped until the end of the time frame. -## -## Note that there would be no warning if any messages were -## dropped because of burst control. -## -## Comment this config out to disable the burst control feature. -## -## Value: MaxBurstCount,TimeWindow -## Default: disabled -## -#log.burst_limit = "20000, 1s" - -## CONFIG_SECTION_END=logger =================================================== - -##-------------------------------------------------------------------- -## Authentication/Access Control -##-------------------------------------------------------------------- - -## Allow anonymous authentication by default if no auth plugins loaded. -## Notice: Disable the option in production deployment! -## -## Value: true | false -acl.allow_anonymous = true - -## Allow or deny if no ACL rules matched. -## -## Value: allow | deny -acl.acl_nomatch = allow - -## Default ACL File. -## -## Value: File Name -acl.acl_file = "{{ platform_etc_dir }}/acl.conf" - -## Whether to enable ACL cache. -## -## If enabled, ACLs roles for each client will be cached in the memory -## -## Value: on | off -acl.enable_acl_cache = on - -## The maximum count of ACL entries can be cached for a client. -## -## Value: Integer greater than 0 -## Default: 32 -acl.acl_cache_max_size = 32 - -## The time after which an ACL cache entry will be deleted -## -## Value: Duration -## Default: 1 minute -acl.acl_cache_ttl = 1m - -## The action when acl check reject current operation -## -## Value: ignore | disconnect -## Default: ignore -acl.acl_deny_action = ignore - -## Specify the global flapping detect policy. -## The value is a string composed of flapping threshold, duration and banned interval. -## 1. threshold: an integer to specfify the disconnected times of a MQTT Client; -## 2. duration: the time window for flapping detect; -## 3. banned interval: the banned interval if a flapping is detected. -## -## Value: Integer,Duration,Duration -acl.flapping_detect_policy = "30, 1m, 5m" - -##-------------------------------------------------------------------- -## MQTT Protocol -##-------------------------------------------------------------------- - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## Default: 1MB -mqtt.max_packet_size = 1MB - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -mqtt.max_clientid_len = 65535 - -## Maximum topic levels allowed. 0 means no limit. -## -## Value: Number -mqtt.max_topic_levels = 0 - -## Maximum QoS allowed. -## -## Value: 0 | 1 | 2 -mqtt.max_qos_allowed = 2 - -## Maximum Topic Alias, 0 means no topic alias supported. -## -## Value: 0-65535 -mqtt.max_topic_alias = 65535 - -## Whether the Server supports MQTT retained messages. -## -## Value: boolean -mqtt.retain_available = true - -## Whether the Server supports MQTT Wildcard Subscriptions -## -## Value: boolean -mqtt.wildcard_subscription = true - -## Whether the Server supports MQTT Shared Subscriptions. -## -## Value: boolean -mqtt.shared_subscription = true - -## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -## -## Value: true | false -mqtt.ignore_loop_deliver = false - -## Whether to parse the MQTT frame in strict mode -## -## Value: true | false -mqtt.strict_mode = false - -## Specify the response information returned to the client -## -## Value: String -## mqtt.response_information = example - -## CONFIG_SECTION_BGN=zones =================================================== - -##-------------------------------------------------------------------- -## External Zone - -## Idle timeout of the external MQTT connections. -## -## Value: duration -zone.external.idle_timeout = 15s - -## Enable ACL check. -## -## Value: Flag -zone.external.enable_acl = on - -## Enable ban check. -## -## Value: Flag -zone.external.enable_ban = on - -## Enable per connection statistics. -## -## Value: on | off -zone.external.enable_stats = on - -## The action when acl check reject current operation -## -## Value: ignore | disconnect -## Default: ignore -zone.external.acl_deny_action = ignore - -## Force the MQTT connection process GC after this number of -## messages | bytes passed through. -## -## Numbers delimited by `|'. Zero or negative is to disable. -zone.external.force_gc_policy = "16000|16MB" - -## Max message queue length and total heap size to force shutdown -## connection/session process. -## Message queue here is the Erlang process mailbox, but not the number -## of queued MQTT messages of QoS 1 and 2. -## -## Numbers delimited by `|'. Zero or negative is to disable. -## -## Default: -## - "10000|64MB" on ARCH_64 system -## - "1000|32MB" on ARCH_32 sytem -#zone.external.force_shutdown_policy = "10000|64MB" - -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## Default: 1MB -## zone.external.max_packet_size = 64KB - -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -## zone.external.max_clientid_len = 1024 - -## Maximum topic levels allowed. 0 means no limit. -## -## Value: Number -## zone.external.max_topic_levels = 7 - -## Maximum QoS allowed. -## -## Value: 0 | 1 | 2 -## zone.external.max_qos_allowed = 2 - -## Maximum Topic Alias, 0 means no limit. -## -## Value: 0-65535 -## zone.external.max_topic_alias = 65535 - -## Whether the Server supports retained messages. -## -## Value: boolean -## zone.external.retain_available = true - -## Whether the Server supports Wildcard Subscriptions -## -## Value: boolean -## zone.external.wildcard_subscription = false - -## Whether the Server supports Shared Subscriptions -## -## Value: boolean -## zone.external.shared_subscription = false - -## Server Keep Alive -## -## Value: Number -## zone.external.server_keepalive = 0 - -## The backoff for MQTT keepalive timeout. The broker will kick a connection out -## until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -zone.external.keepalive_backoff = 0.75 - -## Maximum number of subscriptions allowed, 0 means no limit. -## -## Value: Number -zone.external.max_subscriptions = 0 - -## Force to upgrade QoS according to subscription. -## -## Value: on | off -zone.external.upgrade_qos = off - -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -zone.external.max_inflight = 32 - -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -zone.external.retry_interval = 30s - -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -zone.external.max_awaiting_rel = 100 - -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -zone.external.await_rel_timeout = 300s - -## Default session expiry interval for MQTT V3.1.1 connections. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -zone.external.session_expiry_interval = 2h - -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -zone.external.max_mqueue_len = 1000 - -## Topic priorities. -## 'none' to indicate no priority table (by default), hence all messages -## are treated equal -## -## Priority number [1-255] -## Example: "topic/1=10,topic/2=8" -## NOTE: comma and equal signs are not allowed for priority topic names -## NOTE: messages for topics not in the priority table are treated as -## either highest or lowest priority depending on the configured -## value for mqueue_default_priority -## -zone.external.mqueue_priorities = none - -## Default to highest priority for topics not matching priority table -## -## Value: highest | lowest -zone.external.mqueue_default_priority = highest - -## Whether to enqueue QoS0 messages. -## -## Value: false | true -zone.external.mqueue_store_qos0 = true - -## Whether to turn on flapping detect -## -## Value: on | off -zone.external.enable_flapping_detect = off - -## Message limit for the a external MQTT connection. -## -## Value: Number,Duration -## Example: 100 messages per 10 seconds. -#zone.external.rate_limit.conn_messages_in = "100,10s" - -## Bytes limit for a external MQTT connections. -## -## Value: Number,Duration -## Example: 100KB incoming per 10 seconds. -#zone.external.rate_limit.conn_bytes_in = "100KB,10s" - -## Whether to alarm the congested connections. -## -## Sometimes the mqtt connection (usually an MQTT subscriber) may get "congested" because -## there're too many packets to sent. The socket trys to buffer the packets until the buffer is -## full. If more packets comes after that, the packets will be "pending" in a queue -## and we consider the connection is "congested". -## -## Enable this to send an alarm when there's any bytes pending in the queue. You could set -## the `listener.tcp..sndbuf` to a larger value if the alarm is triggered too often. -## -## The name of the alarm is of format "conn_congestion//". -## Where the is the client-id of the congested MQTT connection. -## And the is the username or "unknown_user" of not provided by the client. -## Default: off -#zone.external.conn_congestion.alarm = off - -## Won't clear the congested alarm in how long time. -## The alarm is cleared only when there're no pending bytes in the queue, and also it has been -## `min_alarm_sustain_duration` time since the last time we considered the connection is "congested". -## -## This is to avoid clearing and sending the alarm again too often. -## Default: 1m -#zone.external.conn_congestion.min_alarm_sustain_duration = 1m - -## Messages quota for the each of external MQTT connection. -## This value consumed by the number of recipient on a message. -## -## Value: Number, Duration -## -## Example: 100 messages per 1s -#zone.external.quota.conn_messages_routing = "100,1s" - -## Messages quota for the all of external MQTT connections. -## This value consumed by the number of recipient on a message. -## -## Value: Number, Duration -## -## Example: 200000 messages per 1s -#zone.external.quota.overall_messages_routing = "200000,1s" - -## All the topics will be prefixed with the mountpoint path if this option is enabled. -## -## Variables in mountpoint path: -## - %c: clientid -## - %u: username -## -## Value: String -## zone.external.mountpoint = "devicebound/" - -## Whether use username replace client id -## -## Value: boolean -## Default: false -zone.external.use_username_as_clientid = false - -## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -## -## Value: true | false -zone.external.ignore_loop_deliver = false - -## Whether to parse the MQTT frame in strict mode -## -## Value: true | false -zone.external.strict_mode = false - -## Specify the response information returned to the client -## -## Value: String -## zone.external.response_information = example - -##-------------------------------------------------------------------- -## Internal Zone - -zone.internal.allow_anonymous = true - -## Enable per connection stats. -## -## Value: Flag -zone.internal.enable_stats = on - -## Enable ACL check. -## -## Value: Flag -zone.internal.enable_acl = off - -## The action when acl check reject current operation -## -## Value: ignore | disconnect -## Default: ignore -zone.internal.acl_deny_action = ignore - -## See zone.$name.force_gc_policy -## zone.internal.force_gc_policy = "128000|128MB" - -## See zone.$name.wildcard_subscription. -## -## Value: boolean -## zone.internal.wildcard_subscription = true - -## See zone.$name.shared_subscription. -## -## Value: boolean -## zone.internal.shared_subscription = true - -## See zone.$name.max_subscriptions. -## -## Value: Integer -zone.internal.max_subscriptions = 0 - -## See zone.$name.max_inflight -## -## Value: Number -zone.internal.max_inflight = 128 - -## See zone.$name.max_awaiting_rel -## -## Value: Number -zone.internal.max_awaiting_rel = 1000 - -## See zone.$name.max_mqueue_len -## -## Value: Number >= 0 -zone.internal.max_mqueue_len = 10000 - -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.internal.mqueue_store_qos0 = true - -## Whether to turn on flapping detect -## -## Value: on | off -zone.internal.enable_flapping_detect = off - -## See zone.$name.force_shutdown_policy -## -## Default: -## - "10000|64MB" on ARCH_64 system -## - "1000|32MB" on ARCH_32 sytem -#zone.internal.force_shutdown_policy = 10000|64MB - -## All the topics will be prefixed with the mountpoint path if this option is enabled. -## -## Variables in mountpoint path: -## - %c: clientid -## - %u: username -## -## Value: String -## zone.internal.mountpoint = "cloudbound/" - -## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -## -## Value: true | false -zone.internal.ignore_loop_deliver = false - -## Whether to parse the MQTT frame in strict mode -## -## Value: true | false -zone.internal.strict_mode = false - -## Specify the response information returned to the client -## -## Value: String -## zone.internal.response_information = example - -## Allow the zone's clients to bypass authentication step -## -## Value: true | false -zone.internal.bypass_auth_plugins = true - -## CONFIG_SECTION_END=zones ==================================================== - -## CONFIG_SECTION_BGN=listeners ================================================ - -##-------------------------------------------------------------------- -## MQTT/TCP - External TCP Listener for MQTT Protocol - -## listener.tcp.$name is the IP address and port that the MQTT/TCP -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 1883, "127.0.0.1:1883", "::1:1883" -listener.tcp.external.endpoint = "0.0.0.0:1883" - -## The acceptor pool for external MQTT/TCP listener. -## -## Value: Number -listener.tcp.external.acceptors = 8 - -## Maximum number of concurrent MQTT/TCP connections. -## -## Value: Number -listener.tcp.external.max_connections = 1024000 - -## Maximum external connections per second. -## -## Value: Number -listener.tcp.external.max_conn_rate = 1000 - -## Specify the {active, N} option for the external MQTT/TCP Socket. -## -## Value: Number -listener.tcp.external.active_n = 100 - -## Zone of the external MQTT/TCP listener belonged to. -## -## See: zone.$name.* -## -## Value: String -listener.tcp.external.zone = external - -## The access control rules for the MQTT/TCP listener. -## -## See: https://github.com/emqtt/esockd#allowdeny -## -## Value: ACL Rule -## -## Example: "allow 192.168.0.0/24" -listener.tcp.external.access.1 = "allow all" - -## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed -## behind HAProxy or Nginx. -## -## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ -## -## Value: on | off -## listener.tcp.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. EMQ X will close the TCP connection -## if no proxy protocol packet recevied within the timeout. -## -## Value: Duration -## listener.tcp.external.proxy_protocol_timeout = 3s - -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT username. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.tcp.external.peer_cert_as_username = cn - -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT clientid. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.tcp.external.peer_cert_as_clientid = cn - -## The TCP backlog defines the maximum length that the queue of pending -## connections can grow to. -## -## Value: Number >= 0 -listener.tcp.external.backlog = 1024 - -## The TCP send timeout for external MQTT connections. -## -## Value: Duration -listener.tcp.external.send_timeout = 15s - -## Close the TCP connection if send timeout. -## -## Value: on | off -listener.tcp.external.send_timeout_close = on - -## The TCP receive buffer(os kernel) for MQTT connections. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -## listener.tcp.external.recbuf = 2KB - -## The TCP send buffer(os kernel) for MQTT connections. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -## listener.tcp.external.sndbuf = 2KB - -## The size of the user-level software buffer used by the driver. -## Not to be confused with options sndbuf and recbuf, which correspond -## to the Kernel socket buffers. It is recommended to have val(buffer) -## >= max(val(sndbuf),val(recbuf)) to avoid performance issues because -## of unnecessary copying. val(buffer) is automatically set to the above -## maximum when values sndbuf or recbuf are set. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -## listener.tcp.external.buffer = 2KB - -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## Value: on | off -## listener.tcp.external.tune_buffer = off - -## The socket is set to a busy state when the amount of data queued internally -## by the ERTS socket implementation reaches this limit. -## -## Value: on | off -## Defaults to 1MB -## listener.tcp.external.high_watermark = 1MB - -## The TCP_NODELAY flag for MQTT connections. Small amounts of data are -## sent immediately if the option is enabled. -## -## Value: true | false -listener.tcp.external.nodelay = true - -## The SO_REUSEADDR flag for TCP listener. -## -## Value: true | false -listener.tcp.external.reuseaddr = true - -##-------------------------------------------------------------------- -## Internal TCP Listener for MQTT Protocol - -## The IP address and port that the internal MQTT/TCP protocol listener -## will bind. -## -## Value: IP:Port, Port -## -## Examples: 11883, "127.0.0.1:11883", "::1:11883" -listener.tcp.internal.endpoint = "127.0.0.1:11883" - -## The acceptor pool for internal MQTT/TCP listener. -## -## Value: Number -listener.tcp.internal.acceptors = 4 - -## Maximum number of concurrent MQTT/TCP connections. -## -## Value: Number -listener.tcp.internal.max_connections = 1024000 - -## Maximum internal connections per second. -## -## Value: Number -listener.tcp.internal.max_conn_rate = 1000 - -## Specify the {active, N} option for the internal MQTT/TCP Socket. -## -## Value: Number -listener.tcp.internal.active_n = 1000 - -## Zone of the internal MQTT/TCP listener belonged to. -## -## Value: String -listener.tcp.internal.zone = internal - -## The TCP backlog of internal MQTT/TCP Listener. -## -## See: listener.tcp.$name.backlog -## -## Value: Number >= 0 -listener.tcp.internal.backlog = 512 - -## The TCP send timeout for internal MQTT connections. -## -## See: listener.tcp.$name.send_timeout -## -## Value: Duration -listener.tcp.internal.send_timeout = 5s - -## Close the MQTT/TCP connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -listener.tcp.internal.send_timeout_close = on - -## The TCP receive buffer(os kernel) for internal MQTT connections. -## -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -listener.tcp.internal.recbuf = 64KB - -## The TCP send buffer(os kernel) for internal MQTT connections. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -listener.tcp.internal.sndbuf = 64KB - -## The size of the user-level software buffer used by the driver. -## -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.tcp.internal.buffer = 16KB - -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## See: listener.tcp.$name.tune_buffer -## -## Value: on | off -## listener.tcp.internal.tune_buffer = off - -## The TCP_NODELAY flag for internal MQTT connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -listener.tcp.internal.nodelay = false - -## The SO_REUSEADDR flag for MQTT/TCP Listener. -## -## Value: true | false -listener.tcp.internal.reuseaddr = true - -##-------------------------------------------------------------------- -## MQTT/SSL - External SSL Listener for MQTT Protocol - -## listener.ssl.$name is the IP address and port that the MQTT/SSL -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 8883, "127.0.0.1:8883", "::1:8883" -listener.ssl.external.endpoint = 8883 - -## The acceptor pool for external MQTT/SSL listener. -## -## Value: Number -listener.ssl.external.acceptors = 16 - -## Maximum number of concurrent MQTT/SSL connections. -## -## Value: Number -listener.ssl.external.max_connections = 102400 - -## Maximum MQTT/SSL connections per second. -## -## Value: Number -listener.ssl.external.max_conn_rate = 500 - -## Specify the {active, N} option for the internal MQTT/SSL Socket. -## -## Value: Number -listener.ssl.external.active_n = 100 - -## Zone of the external MQTT/SSL listener belonged to. -## -## Value: String -listener.ssl.external.zone = external - -## The access control rules for the MQTT/SSL listener. -## -## See: listener.tcp.$name.access -## -## Value: ACL Rule -listener.ssl.external.access.1 = "allow all" - -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind -## HAProxy or Nginx. -## -## See: listener.tcp.$name.proxy_protocol -## -## Value: on | off -## listener.ssl.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.tcp.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.ssl.external.proxy_protocol_timeout = 3s - -## TLS versions only to protect from POODLE attack. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: String, seperated by ',' -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## listener.ssl.external.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" - -## TLS Handshake timeout. -## -## Value: Duration -listener.ssl.external.handshake_timeout = 15s - -## Maximum number of non-self-issued intermediate certificates that -## can follow the peer certificate in a valid certification path. -## -## Value: Number -## listener.ssl.external.depth = 10 - -## String containing the user's password. Only used if the private keyfile -## is password-protected. -## -## Value: String -## listener.ssl.external.key_password = yourpass - -## Path to the file containing the user's private PEM-encoded key. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: File -listener.ssl.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" - -## Path to a file containing the user certificate. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: File -listener.ssl.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" - -## Path to the file containing PEM-encoded CA certificates. The CA certificates -## are used during server authentication and when building the client certificate chain. -## -## Value: File -## listener.ssl.external.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - -## The Ephemeral Diffie-Helman key exchange is a very effective way of -## ensuring Forward Secrecy by exchanging a set of keys that never hit -## the wire. Since the DH key is effectively signed by the private key, -## it needs to be at least as strong as the private key. In addition, -## the default DH groups that most of the OpenSSL installations have -## are only a handful (since they are distributed with the OpenSSL -## package that has been built for the operating system it’s running on) -## and hence predictable (not to mention, 1024 bits only). -## In order to escape this situation, first we need to generate a fresh, -## strong DH group, store it in a file and then use the option above, -## to force our SSL application to use the new DH group. Fortunately, -## OpenSSL provides us with a tool to do that. Simply run: -## openssl dhparam -out dh-params.pem 2048 -## -## Value: File -## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" - -## A server only does x509-path validation in mode verify_peer, -## as it then sends a certificate request to the client (this -## message is not sent if the verify option is verify_none). -## You can then also want to specify option fail_if_no_peer_cert. -## More information at: http://erlang.org/doc/man/ssl.html -## -## Value: verify_peer | verify_none -## listener.ssl.external.verify = verify_peer - -## Used together with {verify, verify_peer} by an SSL server. If set to true, -## the server fails if the client does not have a certificate to send, that is, -## sends an empty certificate. -## -## Value: true | false -## listener.ssl.external.fail_if_no_peer_cert = true - -## This is the single most important configuration option of an Erlang SSL -## application. Ciphers (and their ordering) define the way the client and -## server encrypt information over the wire, from the initial Diffie-Helman -## key exchange, the session key encryption ## algorithm and the message -## digest algorithm. Selecting a good cipher suite is critical for the -## application’s data security, confidentiality and performance. -## -## The cipher list above offers: -## -## A good balance between compatibility with older browsers. -## It can get stricter for Machine-To-Machine scenarios. -## Perfect Forward Secrecy. -## No old/insecure encryption and HMAC algorithms -## -## Most of it was copied from Mozilla’s Server Side TLS article -## -## Value: Ciphers -listener.ssl.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" - -## Ciphers for TLS PSK. -## Note that 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -#listener.ssl.external.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" - -## SSL parameter renegotiation is a feature that allows a client and a server -## to renegotiate the parameters of the SSL connection on the fly. -## RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, -## you drop support for the insecure renegotiation, prone to MitM attacks. -## -## Value: on | off -## listener.ssl.external.secure_renegotiate = off - -## A performance optimization setting, it allows clients to reuse -## pre-existing sessions, instead of initializing new ones. -## Read more about it here. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: on | off -## listener.ssl.external.reuse_sessions = on - -## An important security setting, it forces the cipher to be set based -## on the server-specified order instead of the client-specified order, -## hence enforcing the (usually more properly configured) security -## ordering of the server administrator. -## -## Value: on | off -## listener.ssl.external.honor_cipher_order = on - -## Use the CN, DN or CRT field from the client certificate as a username. -## Notice that 'verify' should be set as 'verify_peer'. -## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. -## -## Value: cn | dn | crt | pem | md5 -## listener.ssl.external.peer_cert_as_username = cn - -## Use the CN, DN or CRT field from the client certificate as a username. -## Notice that 'verify' should be set as 'verify_peer'. -## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. -## -## Value: cn | dn | crt | pem | md5 -## listener.ssl.external.peer_cert_as_clientid = cn - -## TCP backlog for the SSL connection. -## -## See listener.tcp.$name.backlog -## -## Value: Number >= 0 -## listener.ssl.external.backlog = 1024 - -## The TCP send timeout for the SSL connection. -## -## See listener.tcp.$name.send_timeout -## -## Value: Duration -## listener.ssl.external.send_timeout = 15s - -## Close the SSL connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -## listener.ssl.external.send_timeout_close = on - -## The TCP receive buffer(os kernel) for the SSL connections. -## -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -## listener.ssl.external.recbuf = 4KB - -## The TCP send buffer(os kernel) for internal MQTT connections. -## -## See: listener.tcp.$name.sndbuf -## -## Value: Bytes -## listener.ssl.external.sndbuf = 4KB - -## The size of the user-level software buffer used by the driver. -## -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.ssl.external.buffer = 4KB - -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## See: listener.tcp.$name.tune_buffer -## -## Value: on | off -## listener.ssl.external.tune_buffer = off - -## The TCP_NODELAY flag for SSL connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -## listener.ssl.external.nodelay = true - -## The SO_REUSEADDR flag for MQTT/SSL Listener. -## -## Value: true | false -listener.ssl.external.reuseaddr = true - -##-------------------------------------------------------------------- -## External WebSocket listener for MQTT protocol - -## listener.ws.$name is the IP address and port that the MQTT/WebSocket -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 8083, "127.0.0.1:8083", "::1:8083" -listener.ws.external.endpoint = 8083 - -## The path of WebSocket MQTT endpoint -## -## Value: URL Path -listener.ws.external.mqtt_path = "/mqtt" - -## The acceptor pool for external MQTT/WebSocket listener. -## -## Value: Number -listener.ws.external.acceptors = 4 - -## Maximum number of concurrent MQTT/WebSocket connections. -## -## Value: Number -listener.ws.external.max_connections = 102400 - -## Maximum MQTT/WebSocket connections per second. -## -## Value: Number -listener.ws.external.max_conn_rate = 1000 - -## Simulate the {active, N} option for the MQTT/WebSocket connections. -## -## Value: Number -listener.ws.external.active_n = 100 - -## Zone of the external MQTT/WebSocket listener belonged to. -## -## Value: String -listener.ws.external.zone = external - -## The access control for the MQTT/WebSocket listener. -## -## See: listener.ws.$name.access -## -## Value: ACL Rule -listener.ws.external.access.1 = "allow all" - -## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. -## Set to false for WeChat MiniApp. -## -## Value: true | false -## listener.ws.external.fail_if_no_subprotocol = true - -## Supported subprotocols -## -## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 -## listener.ws.external.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" - -## Specify which HTTP header for real source IP if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-For -## listener.ws.external.proxy_address_header = X-Forwarded-For - -## Specify which HTTP header for real source port if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-Port -## listener.ws.external.proxy_port_header = X-Forwarded-Port - -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind -## HAProxy or Nginx. -## -## See: listener.ws.$name.proxy_protocol -## -## Value: on | off -## listener.ws.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.ws.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.ws.external.proxy_protocol_timeout = 3s - -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT username. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.ws.external.peer_cert_as_username = cn - -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT clientid. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.ws.external.peer_cert_as_clientid = cn - -## The TCP backlog of external MQTT/WebSocket Listener. -## -## See: listener.ws.$name.backlog -## -## Value: Number >= 0 -listener.ws.external.backlog = 1024 - -## The TCP send timeout for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.send_timeout -## -## Value: Duration -listener.ws.external.send_timeout = 15s - -## Close the MQTT/WebSocket connection if send timeout. -## -## See: listener.ws.$name.send_timeout_close -## -## Value: on | off -listener.ws.external.send_timeout_close = on - -## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.recbuf -## -## Value: Bytes -## listener.ws.external.recbuf = 2KB - -## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.sndbuf -## -## Value: Bytes -## listener.ws.external.sndbuf = 2KB - -## The size of the user-level software buffer used by the driver. -## -## See: listener.ws.$name.buffer -## -## Value: Bytes -## listener.ws.external.buffer = 2KB - -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## See: listener.ws.$name.tune_buffer -## -## Value: on | off -## listener.ws.external.tune_buffer = off - -## The TCP_NODELAY flag for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.nodelay -## -## Value: true | false -listener.ws.external.nodelay = true - -## The compress flag for external MQTT/WebSocket connections. -## -## If this Value is set true,the websocket message would be compressed -## -## Value: true | false -## listener.ws.external.compress = true - -## The level of deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.level -## -## Value: none | default | best_compression | best_speed -## listener.ws.external.deflate_opts.level = default - -## The mem_level of deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.mem_level -## -## Valid range is 1-9 -## listener.ws.external.deflate_opts.mem_level = 8 - -## The strategy of deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.strategy -## -## Value: default | filtered | huffman_only | rle -## listener.ws.external.deflate_opts.strategy = default - -## The deflate option for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.server_context_takeover -## -## Value: takeover | no_takeover -## listener.ws.external.deflate_opts.server_context_takeover = takeover - -## The deflate option for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.client_context_takeover -## -## Value: takeover | no_takeover -## listener.ws.external.deflate_opts.client_context_takeover = takeover - -## The deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.server_max_window_bits -## -## Valid range is 8-15 -## listener.ws.external.deflate_opts.server_max_window_bits = 15 - -## The deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.client_max_window_bits -## -## Valid range is 8-15 -## listener.ws.external.deflate_opts.client_max_window_bits = 15 - -## The idle timeout for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.idle_timeout -## -## Value: Duration -## listener.ws.external.idle_timeout = 60s - -## The max frame size for external MQTT/WebSocket connections. -## -## -## Value: Number -## listener.ws.external.max_frame_size = 0 - -## Whether a WebSocket message is allowed to contain multiple MQTT packets -## -## Value: single | multiple -listener.ws.external.mqtt_piggyback = multiple - -## By default, EMQX web socket connection does not restrict connections to specific origins. -## It also, by default, does not enforce the presence of origin in request headers for WebSocket connections. -## Because of this, a malicious user could potentially hijack an existing web-socket connection to EMQX. - -## To prevent this, users can set allowed origin headers in their ws connection to EMQX. -## WS configs are set in listener.ws.external.* -## WSS configs are set in listener.wss.external.* - -## Example for WS connection -## To enables origin check in header for websocket connnection, -## set `listener.ws.external.check_origin_enable = true`. By default it is false, -## When it is set to true and no origin is present in the header of a ws connection request, the request fails. - -## To allow origins to be absent in header in the websocket connection when check_origin_enable is true, -## set `listener.ws.external.allow_origin_absence = true` - -## Enabling origin check implies there are specific valid origins allowed for ws connection. -## To set the list of allowed origins in header for websocket connection -## listener.ws.external.check_origins = http://localhost:18083(localhost dashboard url), http://yourapp.com` -## check_origins config allows a comma separated list of origins so you can specify as many origins are you want. -## With these configs, you can allow only connections from only authorized origins to your broker - -## Enable origin check in header for websocket connection -## -## Value: true | false (default false) -listener.ws.external.check_origin_enable = false - -## Allow origin to be absent in header in websocket connection when check_origin_enable is true -## -## Value: true | false (default true) -listener.ws.external.allow_origin_absence = true - -## Comma separated list of allowed origin in header for websocket connection -## -## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 -listener.ws.external.check_origins = "http://localhost:18083, http://127.0.0.1:18083" - -##-------------------------------------------------------------------- -## External WebSocket/SSL listener for MQTT Protocol - -## listener.wss.$name is the IP address and port that the MQTT/WebSocket/SSL -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 8084, "127.0.0.1:8084", "::1:8084" -listener.wss.external.endpoint = 8084 - -## The path of WebSocket MQTT endpoint -## -## Value: URL Path -listener.wss.external.mqtt_path = "/mqtt" - -## The acceptor pool for external MQTT/WebSocket/SSL listener. -## -## Value: Number -listener.wss.external.acceptors = 4 - -## Maximum number of concurrent MQTT/Webwocket/SSL connections. -## -## Value: Number -listener.wss.external.max_connections = 16 - -## Maximum MQTT/WebSocket/SSL connections per second. -## -## See: listener.tcp.$name.max_conn_rate -## -## Value: Number -listener.wss.external.max_conn_rate = 1000 - -## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. -## -## Value: Number -listener.wss.external.active_n = 100 - -## Zone of the external MQTT/WebSocket/SSL listener belonged to. -## -## Value: String -listener.wss.external.zone = external - -## The access control rules for the MQTT/WebSocket/SSL listener. -## -## See: listener.tcp.$name.access. -## -## Value: ACL Rule -listener.wss.external.access.1 = "allow all" - -## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. -## Set to false for WeChat MiniApp. -## -## Value: true | false -## listener.wss.external.fail_if_no_subprotocol = true - -## Supported subprotocols -## -## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 -## listener.wss.external.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" - -## Specify which HTTP header for real source IP if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-For -## listener.wss.external.proxy_address_header = X-Forwarded-For - -## Specify which HTTP header for real source port if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-Port -## listener.wss.external.proxy_port_header = X-Forwarded-Port - -## Enable the Proxy Protocol V1/2 support. -## -## See: listener.tcp.$name.proxy_protocol -## -## Value: on | off -## listener.wss.external.proxy_protocol = on - -## Sets the timeout for proxy protocol. -## -## See: listener.tcp.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.wss.external.proxy_protocol_timeout = 3s - -## TLS versions only to protect from POODLE attack. -## -## See: listener.ssl.$name.tls_versions -## -## Value: String, seperated by ',' -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## listener.wss.external.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" - -## Path to the file containing the user's private PEM-encoded key. -## -## See: listener.ssl.$name.keyfile -## -## Value: File -listener.wss.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" - -## Path to a file containing the user certificate. -## -## See: listener.ssl.$name.certfile -## -## Value: File -listener.wss.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" - -## Path to the file containing PEM-encoded CA certificates. -## -## See: listener.ssl.$name.cacert -## -## Value: File -## listener.wss.external.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - -## Maximum number of non-self-issued intermediate certificates that -## can follow the peer certificate in a valid certification path. -## -## See: listener.ssl.external.depth -## -## Value: Number -## listener.wss.external.depth = 10 - -## String containing the user's password. Only used if the private keyfile -## is password-protected. -## -## See: listener.ssl.$name.key_password -## -## Value: String -## listener.wss.external.key_password = yourpass - -## See: listener.ssl.$name.dhfile -## -## Value: File -## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" - -## See: listener.ssl.$name.verify -## -## Value: verify_peer | verify_none -## listener.wss.external.verify = verify_peer - -## See: listener.ssl.$name.fail_if_no_peer_cert -## -## Value: false | true -## listener.wss.external.fail_if_no_peer_cert = true - -## See: listener.ssl.$name.ciphers -## -## Value: Ciphers -listener.wss.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" - -## Ciphers for TLS PSK. -## Note that 'listener.wss.external.ciphers' and 'listener.wss.external.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -## listener.wss.external.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" - -## See: listener.ssl.$name.secure_renegotiate -## -## Value: on | off -## listener.wss.external.secure_renegotiate = off - -## See: listener.ssl.$name.reuse_sessions -## -## Value: on | off -## listener.wss.external.reuse_sessions = on - -## See: listener.ssl.$name.honor_cipher_order -## -## Value: on | off -## listener.wss.external.honor_cipher_order = on - -## See: listener.ssl.$name.peer_cert_as_username -## -## Value: cn | dn | crt | pem | md5 -## listener.wss.external.peer_cert_as_username = cn - -## See: listener.ssl.$name.peer_cert_as_clientid -## -## Value: cn | dn | crt | pem | md5 -## listener.wss.external.peer_cert_as_clientid = cn - -## TCP backlog for the WebSocket/SSL connection. -## -## See: listener.tcp.$name.backlog -## -## Value: Number >= 0 -listener.wss.external.backlog = 1024 - -## The TCP send timeout for the WebSocket/SSL connection. -## -## See: listener.tcp.$name.send_timeout -## -## Value: Duration -listener.wss.external.send_timeout = 15s - -## Close the WebSocket/SSL connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -listener.wss.external.send_timeout_close = on - -## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. -## -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -## listener.wss.external.recbuf = 4KB - -## The TCP send buffer(os kernel) for the WebSocket/SSL connections. -## -## See: listener.tcp.$name.sndbuf -## -## Value: Bytes -## listener.wss.external.sndbuf = 4KB - -## The size of the user-level software buffer used by the driver. -## -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.wss.external.buffer = 4KB - -## The TCP_NODELAY flag for WebSocket/SSL connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -## listener.wss.external.nodelay = true - -## The compress flag for external WebSocket/SSL connections. -## -## If this Value is set true,the websocket message would be compressed -## -## Value: true | false -## listener.wss.external.compress = true - -## The level of deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.level -## -## Value: none | default | best_compression | best_speed -## listener.wss.external.deflate_opts.level = default - -## The mem_level of deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.mem_level -## -## Valid range is 1-9 -## listener.wss.external.deflate_opts.mem_level = 8 - -## The strategy of deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.strategy -## -## Value: default | filtered | huffman_only | rle -## listener.wss.external.deflate_opts.strategy = default - -## The deflate option for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.server_context_takeover -## -## Value: takeover | no_takeover -## listener.wss.external.deflate_opts.server_context_takeover = takeover - -## The deflate option for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.client_context_takeover -## -## Value: takeover | no_takeover -## listener.wss.external.deflate_opts.client_context_takeover = takeover - -## The deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.server_max_window_bits -## -## Valid range is 8-15 -## listener.wss.external.deflate_opts.server_max_window_bits = 15 - -## The deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.client_max_window_bits -## -## Valid range is 8-15 -## listener.wss.external.deflate_opts.client_max_window_bits = 15 - -## The idle timeout for external WebSocket/SSL connections. -## -## See: listener.wss.$name.idle_timeout -## -## Value: Duration -## listener.wss.external.idle_timeout = 60s - -## The max frame size for external WebSocket/SSL connections. -## -## Value: Number -## listener.wss.external.max_frame_size = 0 - -## Whether a WebSocket message is allowed to contain multiple MQTT packets -## -## Value: single | multiple -listener.wss.external.mqtt_piggyback = multiple -## Enable origin check in header for secure websocket connection -## -## Value: true | false (default false) -listener.wss.external.check_origin_enable = false -## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true -## -## Value: true | false (default true) -listener.wss.external.allow_origin_absence = true -## Comma separated list of allowed origin in header for secure websocket connection -## -## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 -listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" - -## CONFIG_SECTION_END=listeners ================================================ - -## CONFIG_SECTION_BGN=modules ================================================== - -## The file to store loaded module names. -## -## Value: File -module.loaded_file = "{{ platform_data_dir }}/loaded_modules" - -##-------------------------------------------------------------------- -## Presence Module - -## Sets the QoS for presence MQTT message. -## -## Value: 0 | 1 | 2 -module.presence.qos = 1 - -##-------------------------------------------------------------------- -## Subscription Module - -## Subscribe the Topics automatically when client connected. -## -## Value: String -## module.subscription.1.topic = "connected/%c/%u" - -## Qos of the proxy subscription. -## -## Value: 0 | 1 | 2 -## Default: 0 -## module.subscription.1.qos = 0 - -## No Local of the proxy subscription options. -## This configuration only takes effect in the MQTT V5 protocol. -## -## Value: 0 | 1 -## Default: 0 -## module.subscription.1.nl = 0 - -## Retain As Published of the proxy subscription options. -## This configuration only takes effect in the MQTT V5 protocol. -## -## Value: 0 | 1 -## Default: 0 -## module.subscription.1.rap = 0 - -## Retain Handling of the proxy subscription options. -## This configuration only takes effect in the MQTT V5 protocol. -## -## Value: 0 | 1 | 2 -## Default: 0 -## module.subscription.1.rh = 0 - -##-------------------------------------------------------------------- -## Rewrite Module - -## {rewrite, Topic, Re, Dest} -## module.rewrite.pub_rule.1 = "x/# ^x/y/(.+)$ z/y/$1" -## module.rewrite.sub_rule.1 = "y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2" - -## CONFIG_SECTION_END=modules ================================================== - -##------------------------------------------------------------------- -## Plugins -##------------------------------------------------------------------- - -## The etc dir for plugins' config. -## -## Value: Folder -plugins.etc_dir = "{{ platform_etc_dir }}/plugins/" - -## The file to store loaded plugin names. -## -## Value: File -plugins.loaded_file = "{{ platform_data_dir }}/loaded_plugins" - -## The directory of extension plugins. -## -## Value: File -plugins.expand_plugins_dir = "{{ platform_plugins_dir }}/" - -##-------------------------------------------------------------------- +##================================================================== +node { + ## Node name. + ## See: http://erlang.org/doc/reference_manual/distributed.html + ## + ## @doc node.name + ## ValueType: NodeName + ## Default: emqx@127.0.0.1 + name: "emqx@127.0.0.1" + + ## Cookie for distributed node communication. + ## + ## @doc node.cookie + ## ValueType: String + ## Default: emqxsecretcookie + cookie: emqxsecretcookie + + ## Data dir for the node + ## + ## @doc node.data_dir + ## ValueType: Folder + ## Default: "{{ platform_data_dir }}/" + data_dir: "{{ platform_data_dir }}/" + + ## The config file dir where the emqx.conf can be found + ## + ## @doc node.etc_dir + ## ValueType: Folder + ## Default: "{{ platform_etc_dir }}/" + etc_dir: "{{ platform_etc_dir }}/" + + ## Dir of crash dump file. + ## + ## @doc node.crash_dump_dir + ## ValueType: Folder + ## Default: "{{ platform_log_dir }}/" + crash_dump_dir: "{{ platform_log_dir }}/" + + ## Global GC Interval. + ## + ## @doc node.global_gc_interval + ## ValueType: Duration + ## Default: 15m + global_gc_interval: 15m + + ## Sets the net_kernel tick time in seconds. + ## Notice that all communicating nodes are to have the same + ## TickTime value specified. + ## + ## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime + ## + ## @doc node.dist_net_ticktime + ## ValueType: Number + ## Default: 15m + dist_net_ticktime: 120 + + ## Sets the port range for the listener socket of a distributed + ## Erlang node. + ## Note that if there are firewalls between clustered nodes, this + ## port segment for nodes’ communication should be allowed. + ## + ## See: http://www.erlang.org/doc/man/kernel_app.html + ## + ## @doc node.dist_listen_min + ## ValueType: Integer + ## Range: [1024,65535] + ## Default: 6369 + dist_listen_min: 6369 + + ## Sets the port range for the listener socket of a distributed + ## Erlang node. + ## Note that if there are firewalls between clustered nodes, this + ## port segment for nodes’ communication should be allowed. + ## + ## See: http://www.erlang.org/doc/man/kernel_app.html + ## + ## @doc node.dist_listen_max + ## ValueType: Integer + ## Range: [1024,65535] + ## Default: 6369 + dist_listen_max: 6369 + + ## Sets the maximum depth of call stack back-traces in the exit + ## reason element of 'EXIT' tuples. + ## The flag also limits the stacktrace depth returned by + ## process_info item current_stacktrace. + ## + ## @doc node.backtrace_depth + ## ValueType: Integer + ## Range: [0,1024] + ## Default: 16 + backtrace_depth: 16 + +} + +##================================================================== +## Cluster +##================================================================== +cluster { + ## Cluster name. + ## + ## @doc cluster.name + ## ValueType: String + ## Default: emqxcl + name: emqxcl + + ## Enable cluster autoheal from network partition. + ## + ## @doc cluster.autoheal + ## ValueType: Boolean + ## Default: true + autoheal: true + + ## Autoclean down node. A down node will be removed from the cluster + ## if this value > 0. + ## + ## @doc cluster.autoclean + ## ValueType: Duration + ## Default: 5m + autoclean: 5m + + ## Node discovery strategy to join the cluster. + ## + ## @doc cluster.discovery_strategy + ## ValueType: manual | static | mcast | dns | etcd | k8s + ## - manual: Manual join command + ## - static: Static node list + ## - mcast: IP Multicast + ## - dns: DNS A Record + ## - etcd: etcd + ## - k8s: Kubernetes + ## + ## Default: manual + discovery_strategy: manual + + ##---------------------------------------------------------------- + ## Cluster using static node list + ##---------------------------------------------------------------- + static { + ## Node list of the cluster + ## + ## @doc cluster.static.seeds + ## ValueType: Array + ## Default: ["emqx1@127.0.0.1", "emqx2@127.0.0.1"] + seeds: ["emqx1@127.0.0.1", "emqx2@127.0.0.1"] + } + + ##---------------------------------------------------------------- + ## Cluster using IP Multicast + ##---------------------------------------------------------------- + mcast { + ## IP Multicast Address. + ## + ## @doc cluster.mcast.addr + ## ValueType: IPAddress + ## Default: "239.192.0.1" + addr: "239.192.0.1" + + ## Multicast Ports. + ## + ## @doc cluster.mcast.ports + ## ValueType: Array + ## Default: [4369, 4370] + ports: [4369, 4370] + + ## Multicast Iface. + ## + ## @doc cluster.mcast.iface + ## ValueType: IPAddress + ## Default: "0.0.0.0" + iface: "0.0.0.0" + + ## Multicast Ttl. + ## + ## @doc cluster.mcast.ttl + ## ValueType: Integer + ## Range: [0,255] + ## Default: 255 + ttl: 255 + + ## Multicast loop. + ## + ## @doc cluster.mcast.loop + ## ValueType: Boolean + ## Default: true + loop: true + } + + ##---------------------------------------------------------------- + ## Cluster using DNS A records + ##---------------------------------------------------------------- + dns { + ## DNS name. + ## + ## @doc cluster.dns.name + ## ValueType: String + ## Default: localhost + name: localhost + + ## The App name is used to build 'node.name' with IP address. + ## + ## @doc cluster.dns.app + ## ValueType: String + ## Default: emqx + app: emqx + } + + ##---------------------------------------------------------------- + ## Cluster using etcd + ##---------------------------------------------------------------- + etcd { + ## Etcd server list, seperated by ','. + ## + ## @doc cluster.etcd.server + ## ValueType: URL + ## Required: true + server: "http://127.0.0.1:2379" + + ## The prefix helps build nodes path in etcd. Each node in the cluster + ## will create a path in etcd: v2/keys/// + ## + ## @doc cluster.etcd.prefix + ## ValueType: String + ## Default: emqxcl + prefix: emqxcl + + ## The TTL for node's path in etcd. + ## + ## @doc cluster.etcd.node_ttl + ## ValueType: Duration + ## Default: 1m + node_ttl: 1m + + ## Path to the file containing the user's private PEM-encoded key. + ## + ## @doc cluster.etcd.ssl.keyfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/key.pem" + ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + + ## Path to a file containing the user certificate. + ## + ## @doc cluster.etcd.ssl.certfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/cert.pem" + ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + + ## Path to the file containing PEM-encoded CA certificates. The CA certificates + ## are used during server authentication and when building the client certificate chain. + ## + ## @doc cluster.etcd.ssl.cacertfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/cacert.pem" + ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + } + + ##---------------------------------------------------------------- + ## Cluster using Kubernetes + ##---------------------------------------------------------------- + k8s { + ## Kubernetes API server list, seperated by ','. + ## + ## @doc cluster.k8s.apiserver + ## ValueType: URL + ## Required: true + apiserver: "http://10.110.111.204:8080" + + ## The service name helps lookup EMQ nodes in the cluster. + ## + ## @doc cluster.k8s.service_name + ## ValueType: String + ## Default: emqx + service_name: emqx + + ## The address type is used to extract host from k8s service. + ## + ## @doc cluster.k8s.address_type + ## ValueType: ip | dns | hostname + ## Default: ip + address_type: ip + + ## The app name helps build 'node.name'. + ## + ## @doc cluster.k8s.app_name + ## ValueType: String + ## Default: emqx + app_name: emqx + + ## The suffix added to dns and hostname get from k8s service + ## + ## @doc cluster.k8s.suffix + ## ValueType: String + ## Default: "pod.local" + suffix: "pod.local" + + ## Kubernetes Namespace + ## + ## @doc cluster.k8s.namespace + ## ValueType: String + ## Default: default + namespace: default + } +} + +##================================================================== +## Log +##================================================================== +log { + ## The primary log level + ## + ## - all the log messages with levels lower than this level will + ## be dropped. + ## - all the log messages with levels higher than this level will + ## go into the log handlers. The handlers then decide to log it + ## out or drop it according to the level setting of the handler. + ## + ## Note: Only the messages with severity level higher than or + ## equal to this level will be logged. + ## + ## @doc log.level + ## ValueType: debug | info | notice | warning | error | critical | alert | emergency + ## Default: warning + primary_level: warning + + ##---------------------------------------------------------------- + ## The console log handler send log messages to emqx console + ##---------------------------------------------------------------- + ## Log to single line + ## @doc log.console_handler.enable + ## ValueType: Boolean + ## Default: false + console_handler.enable = false + + ## The log level of this handler + ## All the log messages with levels lower than this level will + ## be dropped. + ## + ## @doc log.console_handler.level + ## ValueType: debug | info | notice | warning | error | critical | alert | emergency + ## Default: debug + console_handler.level = debug + + ##---------------------------------------------------------------- + ## The file log handlers send log messages to files + ##---------------------------------------------------------------- + ## file_handlers. + file_handlers.emqx_default: { + ## The log level filter of this handler + ## All the log messages with levels lower than this level will + ## be dropped. + ## + ## @doc log.file_handlers..level + ## ValueType: debug | info | notice | warning | error | critical | alert | emergency + ## Default: debug + level: debug + + ## The log file for specified level. + ## + ## If `rotation` is disabled, this is the file of the log files. + ## + ## If `rotation` is enabled, this is the base name of the files. + ## Each file in a rotated log is named .N, where N is an integer. + ## + ## Note: Log files for a specific log level will only contain all the logs + ## that higher than or equal to that level + ## + ## @doc log.file_handlers..file + ## ValueType: File + ## Required: true + file: "{{ platform_log_dir }}/emqx.log" + + ## Enables the log rotation. + ## With this enabled, new log files will be created when the current + ## log file is full, max to `rotation_count` files will be created. + ## + ## @doc log.file_handlers..rotation + ## ValueType: Boolean + ## Default: true + rotation = true + + ## Maximum rotation count of log files. + ## + ## @doc log.file_handlers..rotation_count + ## ValueType: Integer + ## Range: [1, 2048] + ## Default: 10 + rotation_count: 10 + + ## Maximum size of each log file. + ## + ## If the max_size reached and `rotation` is disabled, the handler + ## will stop sending log messages, if the `rotation` is enabled, + ## the file rotates. + ## + ## @doc log.file_handlers..max_size + ## ValueType: Size | infinity + ## Default: 10MB + max_size: 10MB + } + + ## file_handlers. + ## + ## You could also create multiple file handlers for different + ## log level for example: + file_handlers.emqx_error: { + level: error + file: "{{ platform_log_dir }}/emqx.error.log" + } + + ## Limits the total number of characters printed for each log event. + ## + ## @doc log.chars_limit + ## ValueType: Integer | infinity + ## Default: infinity + chars_limit: 8192 + + ## Log formatter + ## @doc log.formatter + ## ValueType: text | json + ## Default: text + formatter: text + + ## Log to single line + ## @doc log.single_line + ## ValueType: Boolean + ## Default: true + single_line: true + + ## The max allowed queue length before switching to sync mode. + ## + ## Log overload protection parameter. If the message queue grows + ## larger than this value the handler switches from anync to sync mode. + ## + ## @doc log.sync_mode_qlen + ## ValueType: Integer + ## Range: [0, ${log.drop_mode_qlen}] + ## Default: 100 + sync_mode_qlen: 100 + + ## The max allowed queue length before switching to drop mode. + ## + ## Log overload protection parameter. When the message queue grows + ## larger than this threshold, the handler switches to a mode in which + ## it drops all new events that senders want to log. + ## + ## @doc log.drop_mode_qlen + ## ValueType: Integer + ## Range: [${log.sync_mode_qlen}, ${log.flush_qlen}] + ## Default: 3000 + drop_mode_qlen: 3000 + + ## The max allowed queue length before switching to flush mode. + ## + ## Log overload protection parameter. If the length of the message queue + ## grows larger than this threshold, a flush (delete) operation takes place. + ## To flush events, the handler discards the messages in the message queue + ## by receiving them in a loop without logging. + ## + ## @doc log.flush_qlen + ## ValueType: Integer + ## Range: [${log.drop_mode_qlen}, ) + ## Default: 8000 + flush_qlen: 8000 + + ## Kill the log handler when it gets overloaded. + ## + ## Log overload protection parameter. It is possible that a handler, + ## even if it can successfully manage peaks of high load without crashing, + ## can build up a large message queue, or use a large amount of memory. + ## We could kill the log handler in these cases and restart it after a + ## few seconds. + ## + ## @doc log.overload_kill + ## ValueType: Boolean + ## Default: true + overload_kill: true + + ## The max allowed queue length before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum allowed queue + ## length. If the message queue grows larger than this, the handler + ## process is terminated. + ## + ## @doc log.overload_kill_qlen + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 20000 + overload_kill_qlen: 20000 + + ## The max allowed memory size before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum memory size + ## that the handler process is allowed to use. If the handler grows + ## larger than this, the process is terminated. + ## + ## @doc log.overload_kill_mem_size + ## ValueType: Size + ## Default: 30MB + overload_kill_mem_size: 30MB + + ## Restart the log hanlder after some seconds. + ## + ## Log overload protection parameter. If the handler is terminated, + ## it restarts automatically after a delay specified in seconds. + ## + ## @doc log.overload_kill_restart_after + ## ValueType: Duration + ## Default: 5s + overload_kill_restart_after: 5s + + ## Controlling Bursts of Log Requests. + ## + ## Log overload protection parameter. Large bursts of log events - many + ## events received by the handler under a short period of time - can + ## potentially cause problems. By specifying the maximum number of events + ## to be handled within a certain time frame, the handler can avoid + ## choking the log with massive amounts of printouts. + ## + ## Note that there would be no warning if any messages were + ## dropped because of burst control. + ## + ## @doc log.burst_limit + ## ValueType: Boolean + ## Default: false + burst_limit: false + + ## This config controls the maximum number of events to handle within + ## a time frame. After the limit is reached, successive events are + ## dropped until the end of the time frame defined by `window_time`. + ## + ## @doc log.burst_limit_max_count + ## ValueType: Integer + ## Default: 10000 + burst_limit_max_count: 10000 + + ## See the previous description of burst_limit_max_count. + ## + ## @doc log.burst_limit_window_time + ## ValueType: duration + ## Default: 1s + burst_limit_window_time: 1s +} + +##================================================================== +## RPC +##================================================================== +rpc { + ## RPC Mode. + ## + ## @doc rpc.mode + ## ValueType: sync | async + ## Default: async + mode: async + + ## Max batch size of async RPC requests. + ## + ## NOTE: RPC batch won't work when rpc.mode = sync + ## Zero value disables rpc batching. + ## + ## @doc rpc.async_batch_size + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 0 + async_batch_size: 256 + + ## RPC port discovery + ## + ## The strategy for discovering the RPC listening port of + ## other nodes. + ## + ## @doc cluster.discovery_strategy + ## ValueType: manual | stateless + ## - manual: discover ports by `tcp_server_port` and + ## `tcp_client_port`. + ## - stateless: discover ports in a stateless manner. + ## If node name is `emqx@127.0.0.1`, where the `` is + ## an integer, then the listening port will be `5370 + ` + ## + ## Default: `stateless`. + port_discovery: stateless + + ## TCP server port for RPC. + ## + ## Only takes effect when `rpc.port_discovery` = `manual`. + ## + ## @doc rpc.tcp_server_port + ## ValueType: Integer + ## Range: [1024-65535] + ## Defaults: 5369 + tcp_server_port: 5369 + + ## TCP port for outgoing RPC connections. + ## + ## Only takes effect when `rpc.port_discovery` = `manual`. + ## + ## @doc rpc.tcp_client_port + ## ValueType: Integer + ## Range: [1024-65535] + ## Defaults: 5369 + tcp_client_port: 5369 + + ## Number of outgoing RPC connections. + ## + ## Defaults to "num_cpu_cores", that is, the number of CPU cores. + ## Set this to 1 to keep the message order sent from the same + ## client. + ## + ## @doc rpc.tcp_client_num + ## ValueType: Integer | num_cpu_cores + ## Range: [1, 256] + ## Defaults: num_cpu_cores + tcp_client_num: 1 + + ## RCP Client connect timeout. + ## + ## @doc rpc.connect_timeout + ## ValueType: Duration + ## Default: 5s + connect_timeout: 5s + + ## TCP send timeout of RPC client and server. + ## + ## @doc rpc.send_timeout + ## ValueType: Duration + ## Default: 5s + send_timeout: 5s + + ## Authentication timeout + ## + ## @doc rpc.authentication_timeout + ## ValueType: Duration + ## Default: 5s + authentication_timeout: 5s + + ## Default receive timeout for call() functions + ## + ## @doc rpc.call_receive_timeout + ## ValueType: Duration + ## Default: 15s + call_receive_timeout: 15s + + ## Socket idle keepalive. + ## + ## @doc rpc.socket_keepalive_idle + ## ValueType: Duration + ## Default: 900s + socket_keepalive_idle: 900s + + ## TCP Keepalive probes interval. + ## + ## @doc rpc.socket_keepalive_interval + ## ValueType: Duration + ## Default: 75s + socket_keepalive_interval: 75s + + ## Probes lost to close the connection + ## + ## @doc rpc.socket_keepalive_count + ## ValueType: Integer + ## Default: 9 + socket_keepalive_count: 9 + + ## Size of TCP send buffer. + ## + ## @doc rpc.socket_sndbuf + ## ValueType: Size + ## Default: 1MB + socket_sndbuf: 1MB + + ## Size of TCP receive buffer. + ## + ## @doc rpc.socket_recbuf + ## ValueType: Size + ## Default: 1MB + socket_recbuf: 1MB + + ## Size of user-level software socket buffer. + ## + ## @doc rpc.socket_buffer + ## ValueType: Size + ## Default: 1MB + socket_buffer: 1MB +} + +##================================================================== ## Broker -##-------------------------------------------------------------------- +##================================================================== +broker { + ## System interval of publishing $SYS messages. + ## + ## @doc broker.sys_msg_interval + ## ValueType: Duration | disabled + ## Default: 1m + sys_msg_interval: 1m -## System interval of publishing $SYS messages. -## -## Value: Duration -## Default: 1m, 1 minute -broker.sys_interval = 1m + ## System heartbeat interval of publishing following heart beat message: + ## - "$SYS/brokers//uptime" + ## - "$SYS/brokers//datetime" + ## + ## @doc broker.sys_heartbeat_interval + ## ValueType: Duration + ## Default: 30s | disabled + sys_heartbeat_interval: 30s -## System heartbeat interval of publishing following heart beat message: -## - "$SYS/brokers//uptime" -## - "$SYS/brokers//datetime" -## -## Value: Duration -## Default: 30s -broker.sys_heartbeat = 30s + ## Session locking strategy in a cluster. + ## + ## @doc broker.session_locking_strategy + ## ValueType: local | one | quorum | all + ## - local: only lock the session locally on the current node + ## - one: select only one remove node to lock the session + ## - quorum: select some nodes to lock the session + ## - all: lock the session on all of the nodes in the cluster + ## Default: quorum + session_locking_strategy: quorum -## Session locking strategy in a cluster. -## -## Value: Enum -## - local -## - leader -## - quorum -## - all -broker.session_locking_strategy = quorum + ## Dispatch strategy for shared subscription + ## + ## @doc broker.shared_subscription_strategy + ## ValueType: random | round_robin | sticky | hash + ## - random: dispatch the message to a random selected subscriber + ## - round_robin: select the subscribers in a round-robin manner + ## - sticky: always use the last selected subscriber to dispatch, + ## until the susbcriber disconnected. + ## - hash: select the subscribers by the hash of clientIds + ## Default: round_robin + shared_subscription_strategy: round_robin -## Dispatch strategy for shared subscription -## -## Value: Enum -## - random -## - round_robin -## - sticky -## - hash # same as hash_clientid -## - hash_clientid -## - hash_topic -broker.shared_subscription_strategy = random + ## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages + ## This should allow messages to be dispatched to a different subscriber in + ## the group in case the picked (based on shared_subscription_strategy) one # is offline + ## + ## @doc broker.shared_dispatch_ack_enabled + ## ValueType: Boolean + ## Default: false + shared_dispatch_ack_enabled: false -## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages -## This should allow messages to be dispatched to a different subscriber in -## the group in case the picked (based on shared_subscription_strategy) one # is offline -## -## Value: Enum -## - true -## - false -broker.shared_dispatch_ack_enabled = false + ## Enable batch clean for deleted routes. + ## + ## @doc broker.route_batch_clean + ## ValueType: Boolean + ## Default: true + route_batch_clean: true -## Enable batch clean for deleted routes. -## -## Value: Flag -broker.route_batch_clean = off + ## Performance toggle for subscribe/unsubscribe wildcard topic. + ## Change this toggle only when there are many wildcard topics. + ## + ## NOTE: when changing from/to 'global' lock, it requires all + ## nodes in the cluster to be stopped before the change. + ## + ## @doc broker.perf.route_lock_type + ## ValueType: key | tab | global + ## - key: mnesia translational updates with per-key locks. recommended for single node setup. + ## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. + ## - global: global lock protected updates. recommended for larger cluster. + ## Default: key + perf.route_lock_type: key -## Performance toggle for subscribe/unsubscribe wildcard topic. -## Change this toggle only when there are many wildcard topics. -## Value: Enum -## - key: mnesia translational updates with per-key locks. recommended for single node setup. -## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. -## - global: global lock protected updates. recommended for larger cluster. -## NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster -## to be stopped before the change. -# broker.perf.route_lock_type = key + ## Enable trie path compaction. + ## Enabling it significantly improves wildcard topic subscribe + ## rate, if wildcard topics have unique prefixes like: + ## 'sensor/{{id}}/+/', where ID is unique per subscriber. + ## + ## Topic match performance (when publishing) may degrade if messages + ## are mostly published to topics with large number of levels. + ## + ## NOTE: This is a cluster-wide configuration. + ## It rquires all nodes to be stopped before changing it. + ## + ## @doc broker.perf.trie_compaction + ## ValueType: Boolean + ## Default: true + perf.trie_compaction: true +} -## Enable trie path compaction. -## Enabling it significantly improves wildcard topic subscribe rate, -## if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', -## where ID is unique per subscriber. +##================================================================== +## Zones and Listeners +##================================================================== +## A zone contains a set of configurations for listeners. ## -## Topic match performance (when publishing) may degrade if messages -## are mostly published to topics with large number of levels. +## The configurations defined in zone can be overridden by the ones +## defined in listeners with the same key. ## -## NOTE: This is a cluster-wide configuration. -## It rquires all nodes to be stopped before changing it. +## For example given the following config: +## ``` ## -## Value: Enum -## - true: enable trie path compaction -## - false: disable trie path compaction -# broker.perf.trie_compaction = true +## zone.x { +## a: {b:1, c: 1} +## listeners.y { +## a: {b: 2} +## } +## } +## ``` +## The config "a" in zone "x" is overridden by the configs inside +## the listener "y". So the value of config "a" in listener "y" +## is `a: {b:2, c: 1}`. +## +## All the configs that can be set in zones and be overridden in listenser are: +## - `auth.*` +## - `stats.*` +## - `mqtt.*` +## - `acl.*` +## - `flapping_detect.*` +## - `force_shutdown.*` +## - `conn_congestion.*` +## - `overall_max_connections` +## +## Syntax: zone. {} +zone.default { + ## Enable authentication + ## + ## @doc zone..auth.enable + ## ValueType: Boolean + ## Default: false + auth.enable: false -## CONFIG_SECTION_BGN=sys_mon ================================================== + ## Enable per connection statistics. + ## + ## @doc zone..stats.enable + ## ValueType: Boolean + ## Default: true + stats.enable: true -## Enable Long GC monitoring. Disable if the value is 0. -## Notice: don't enable the monitor in production for: -## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## - ms: milliseconds -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 0.1s: 0.1 seconds -## - 100ms : 100 milliseconds -## -## Default: 0ms -sysmon.long_gc = 0 + mqtt { + ## When publishing or subscribing, prefix all topics with a mountpoint string. + ## The prefixed string will be removed from the topic name when the message + ## is delivered to the subscriber. The mountpoint is a way that users can use + ## to implement isolation of message routing between different listeners. + ## + ## For example if a clientA subscribes to "t" with `zone..mqtt.mountpoint` + ## set to "some_tenant", then the client accually subscribes to the topic + ## "some_tenant/t". Similarly if another clientB (connected to the same listener + ## with the clientA) send a message to topic "t", the message is accually route + ## to all the clients subscribed "some_tenant/t", so clientA will receive the + ## message, with topic name "t". + ## + ## Set to "" to disable the feature. + ## + ## Variables in mountpoint string: + ## - %c: clientid + ## - %u: username + ## + ## @doc zone..listeners..mountpoint + ## ValueType: String + ## Default: "" + mountpoint: "" -## Enable Long Schedule(ms) monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## - ms: milliseconds -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 100ms: 100 milliseconds -## -## Default: 0ms -sysmon.long_schedule = 240ms + ## How long time the MQTT connection will be disconnected if the + ## TCP connection is established but MQTT CONNECT has not been + ## received. + ## + ## @doc zone..mqtt.idle_timeout + ## ValueType: Duration | infinity + ## Default: 15s + idle_timeout: 15s -## Enable Large Heap monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: bytes -## -## Default: 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. -sysmon.large_heap = 8MB + ## Maximum MQTT packet size allowed. + ## + ## @doc zone..mqtt.max_packet_size + ## ValueType: Bytes | infinity + ## Default: 1MB + max_packet_size: 1MB -## Enable Busy Port monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: true | false -sysmon.busy_port = false + ## Maximum length of MQTT clientId allowed. + ## + ## @doc zone..mqtt.max_clientid_len + ## ValueType: Integer | infinity + ## Range: [23, 65535] + ## Default: 65535 + max_clientid_len: 65535 -## Enable Busy Dist Port monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: true | false -sysmon.busy_dist_port = true + ## Maximum topic levels allowed. + ## + ## @doc zone..mqtt.max_topic_levels + ## ValueType: Integer | infinity + ## Range: [1, 65535] + ## Default: infinity + max_topic_levels: infinity -## The time interval for the periodic cpu check -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 60s -os_mon.cpu_check_interval = 60s + ## Maximum QoS allowed. + ## + ## @doc zone..mqtt.max_qos_allowed + ## ValueType: 0 | 1 | 2 + ## Default: 2 + max_qos_allowed: 2 -## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. -## -## Default: 80% -os_mon.cpu_high_watermark = 80% + ## Maximum Topic Alias, 0 means no topic alias supported. + ## + ## @doc zone..mqtt.max_topic_alias + ## ValueType: Integer | infinity + ## Range: [0, 65535] + ## Default: 65535 + max_topic_alias: 65535 -## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. -## -## Default: 60% -os_mon.cpu_low_watermark = 60% + ## Whether the Server supports MQTT retained messages. + ## + ## @doc zone..mqtt.retain_available + ## ValueType: Boolean + ## Default: true + retain_available: true -## The time interval for the periodic memory check -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 60s -os_mon.mem_check_interval = 60s + ## Whether the Server supports MQTT Wildcard Subscriptions + ## + ## @doc zone..mqtt.wildcard_subscription + ## ValueType: Boolean + ## Default: true + wildcard_subscription: true -## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. -## -## Default: 70% -os_mon.sysmem_high_watermark = 70% + ## Whether the Server supports MQTT Shared Subscriptions. + ## + ## @doc zone..mqtt.shared_subscription + ## ValueType: Boolean + ## Default: true + shared_subscription: true -## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. -## -## Default: 5% -os_mon.procmem_high_watermark = 5% + ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) + ## + ## @doc zone..mqtt.ignore_loop_deliver + ## ValueType: Boolean + ## Default: false + ignore_loop_deliver: false -## The time interval for the periodic process limit check -## -## Value: Duration -## -## Default: 30s -vm_mon.check_interval = 30s + ## Whether to parse the MQTT frame in strict mode + ## + ## @doc zone..mqtt.strict_mode + ## ValueType: Boolean + ## Default: false + strict_mode: false -## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. -## -## Default: 80% -vm_mon.process_high_watermark = 80% + ## Specify the response information returned to the client + ## + ## This feature is disabled if not set + ## + ## @doc zone..mqtt.response_information + ## ValueType: String + ## Default: "" + response_information: "" -## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. -## -## Default: 60% -vm_mon.process_low_watermark = 60% + ## Server Keep Alive of MQTT 5.0 + ## + ## @doc zone..mqtt.server_keepalive + ## ValueType: Number | disabled + ## Default: disabled + server_keepalive: disabled -## Specifies the actions to take when an alarm is activated -## -## Value: String -## - log -## - publish -## -## Default: "log,publish" -alarm.actions = "log,publish" + ## The backoff for MQTT keepalive timeout. The broker will kick a connection out + ## until 'Keepalive * backoff * 2' timeout. + ## + ## @doc zone..mqtt.keepalive_backoff + ## ValueType: Float + ## Range: (0.5, 1] + ## Default: 0.75 + keepalive_backoff: 0.75 -## The maximum number of deactivated alarms -## -## Value: Integer -## -## Default: 1000 -alarm.size_limit = 1000 + ## Maximum number of subscriptions allowed, 0 means no limit. + ## + ## @doc zone..mqtt.max_subscriptions + ## ValueType: Integer | infinity + ## Range: [0, ) + ## Default: infinity + max_subscriptions: infinity -## Validity Period of deactivated alarms -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## - ms: milliseconds -## -## Default: 24h -alarm.validity_period = 24h + ## Force to upgrade QoS according to subscription. + ## + ## @doc zone..mqtt.upgrade_qos + ## ValueType: Boolean + ## Default: false + upgrade_qos: false -## CONFIG_SECTION_END=sys_mon ================================================== + ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. + ## + ## @doc zone..mqtt.max_inflight + ## ValueType: Integer | infinity + ## Range: [0, ) + ## Default: 32 + max_inflight: 32 + + ## Retry interval for QoS1/2 message delivering. + ## + ## @doc zone..mqtt.retry_interval + ## ValueType: Duration + ## Default: 30s + retry_interval: 30s + + ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. + ## + ## @doc zone..mqtt.max_awaiting_rel + ## ValueType: Integer | infinity + ## Range: [0, ) + ## Default: 100 + max_awaiting_rel: 100 + + ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. + ## + ## @doc zone..mqtt.await_rel_timeout + ## ValueType: Duration + ## Default: 300s + await_rel_timeout: 300s + + ## Default session expiry interval for MQTT V3.1.1 connections. + ## + ## @doc zone..mqtt.session_expiry_interval + ## ValueType: Duration + ## Default: 2h + session_expiry_interval: 2h + + ## Maximum queue length. Enqueued messages when persistent client disconnected, + ## or inflight window is full. 0 means no limit. + ## + ## @doc zone..mqtt.max_mqueue_len + ## ValueType: Integer | infinity + ## Range: [0, ) + ## Default: 1000 + max_mqueue_len: 1000 + + ## Topic priorities. + ## + ## There's no priority table by default, hence all messages + ## are treated equal. + ## The top topicname in the table has the highest priority, and then + ## the next one has the second highest priority, etc. + ## Messages for topics not in the priority table are treated as + ## either highest or lowest priority depending on the configured + ## value for mqtt.mqueue_default_priority + ## + ## @doc zone..mqtt.mqueue_priorities + ## ValueType: Array + ## Examples: + ## To configure "t/1" > "t/2" > "t/3": + ## mqueue_priorities: [t/1,t/2,t/3] + ## Default: [] + mqueue_priorities: [] + + ## Default to highest priority for topics not matching priority table + ## + ## @doc zone..mqtt.mqueue_default_priority + ## ValueType: highest | lowest + ## Default: highest + mqueue_default_priority: highest + + ## Whether to enqueue QoS0 messages. + ## + ## @doc zone..mqtt.mqueue_store_qos0 + ## ValueType: Boolean + ## Default: true + mqueue_store_qos0: true + + ## Whether use username replace client id + ## + ## @doc zone..mqtt.use_username_as_clientid + ## ValueType: Boolean + ## Default: false + use_username_as_clientid: false + + ## Use the CN, DN or CRT field from the client certificate as a username. + ## Only works for SSL connection. + ## + ## @doc zone..mqtt.peer_cert_as_username + ## ValueType: cn | dn | crt | disabled + ## Default: disabled + peer_cert_as_username: disabled + + ## Use the CN, DN or CRT field from the client certificate as a clientid. + ## Only works for SSL connection. + ## + ## @doc zone..mqtt.peer_cert_as_clientid + ## ValueType: cn | dn | crt | disabled + ## Default: disabled + peer_cert_as_clientid: disabled + + } + + acl { + + ## Enable ACL check. + ## + ## @doc zone..acl.enable + ## ValueType: Boolean + ## Default: false + enable: false + + ## The action when acl check reject current operation + ## + ## @doc zone..acl.deny_action + ## ValueType: ignore | disconnect + ## Default: ignore + deny_action: ignore + + ## Whether to enable ACL cache. + ## + ## If enabled, ACLs roles for each client will be cached in the memory + ## + ## @doc zone..acl.cache.enable + ## ValueType: Boolean + ## Default: true + cache.enable: true + + ## The maximum count of ACL entries can be cached for a client. + ## + ## @doc zone..acl.cache.max_size + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 32 + cache.max_size: 32 + + ## The time after which an ACL cache entry will be deleted + ## + ## @doc zone..acl.cache.ttl + ## ValueType: Duration + ## Default: 1m + cache.ttl: 1m + } + + rate_limit { + ## Maximum connections per second. + ## + ## @doc zone..max_conn_rate + ## ValueType: Number | infinity + ## Default: 1000 + ## Examples: + ## max_conn_rate: 1000 + max_conn_rate: 1000 + + ## Message limit for the a external MQTT connection. + ## + ## @doc zone..rate_limit.conn_messages_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messages per 10 seconds. + ## conn_messages_in: "100,10s" + conn_messages_in: "100,10s" + + ## Limit the rate of receiving packets for a MQTT connection. + ## The rate is counted by bytes of packets per second. + ## + ## The connection won't accept more messages if the messages come + ## faster than the limit. + ## + ## @doc zone..rate_limit.conn_bytes_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100KB incoming per 10 seconds. + ## conn_bytes_in: "100KB,10s" + ## + conn_bytes_in: "100KB,10s" + + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + quota.conn_messages_routing: "100,1s" + + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + quota.overall_messages_routing: "200000,1s" + } + + flapping_detect { + ## Enable Flapping Detection. + ## + ## This config controls the allowed maximum number of CONNECT received + ## from the same clientid in a time frame defined by `window_time`. + ## After the limit is reached, successive CONNECT requests are forbidden + ## (banned) until the end of the time period defined by `ban_time`. + ## + ## @doc zone..flapping_detect.enable + ## ValueType: Boolean + ## Default: true + enable: true + + ## The max disconnect allowed of a MQTT Client in `window_time` + ## + ## @doc zone..flapping_detect.max_count + ## ValueType: Integer + ## Default: 15 + max_count: 15 + + ## The time window for flapping detect + ## + ## @doc zone..flapping_detect.window_time + ## ValueType: Duration + ## Default: 1m + window_time: 1m + + ## How long the clientid will be banned + ## + ## @doc zone..flapping_detect.ban_time + ## ValueType: Duration + ## Default: 5m + ban_time: 5m + + } + + force_shutdown: { + ## Enable force_shutdown + ## + ## @doc zone..force_shutdown.enable + ## ValueType: Boolean + ## Default: true + enable: true + + ## Max message queue length + ## @doc zone..force_shutdown.max_message_queue_len + ## ValueType: Integer + ## Range: (0, ) + ## Default: 1000 + max_message_queue_len: 1000 + + ## Total heap size + ## + ## @doc zone..force_shutdown.max_heap_size + ## ValueType: Size + ## Default: 32MB + max_heap_size: 32MB + } + + force_gc: { + ## Force the MQTT connection process GC after this number of + ## messages or bytes passed through. + ## + ## @doc zone..force_gc.enable + ## ValueType: Boolean + ## Default: true + enable: true + + ## GC the process after how many messages received + ## @doc zone..force_gc.max_message_queue_len + ## ValueType: Integer + ## Range: (0, ) + ## Default: 16000 + count: 16000 + + ## GC the process after how much bytes passed through + ## + ## @doc zone..force_gc.bytes + ## ValueType: Size + ## Default: 16MB + bytes: 16MB + } + + conn_congestion: { + ## Whether to alarm the congested connections. + ## + ## Sometimes the mqtt connection (usually an MQTT subscriber) may + ## get "congested" because there're too many packets to sent. + ## The socket trys to buffer the packets until the buffer is + ## full. If more packets comes after that, the packets will be + ## "pending" in a queue and we consider the connection is + ## "congested". + ## + ## Enable this to send an alarm when there's any bytes pending in + ## the queue. You could set the `sndbuf` to a larger value if the + ## alarm is triggered too often. + ## + ## The name of the alarm is of format "conn_congestion//". + ## Where the is the client-id of the congested MQTT connection. + ## And the is the username or "unknown_user" of not provided by the client. + ## + ## @doc zone..conn_congestion.enable_alarm + ## ValueType: Boolean + ## Default: true + enable_alarm: true + + ## Won't clear the congested alarm in how long time. + ## The alarm is cleared only when there're no pending bytes in + ## the queue, and also it has been `min_alarm_sustain_duration` + ## time since the last time we considered the connection is "congested". + ## + ## This is to avoid clearing and sending the alarm again too often. + ## + ## @doc zone..conn_congestion.min_alarm_sustain_duration + ## ValueType: Duration + ## Default: 1m + min_alarm_sustain_duration: 1m + } + + + listeners.mqtt_tcp: + #${example_common_tcp_options} # common options can be written in a separate config entry and reference it from here. + { + + ## The type of the listener. + ## + ## @doc zone..listeners..type + ## ValueType: tcp | ssl | ws | wss + ## - tcp: MQTT over TCP + ## - ssl: MQTT over TLS + ## - ws: MQTT over Websocket + ## - wss: MQTT over WebSocket Secure + ## Required: true + type: tcp + + ## The IP address and port that the listener will bind. + ## + ## @doc zone..listeners..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 1883, 127.0.0.1:1883, ::1:1883 + bind: "0.0.0.0:1883" + + ## The size of the acceptor pool for this listener. + ## + ## @doc zone..listeners..acceptors + ## ValueType: Number | num_cpu_cores + ## Default: num_cpu_cores + acceptors: num_cpu_cores + + ## Maximum number of concurrent connections. + ## + ## @doc zone..listeners..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections: 1024000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc zone..listeners..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules: [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc zone..listeners..proxy_protocol + ## ValueType: Boolean + ## Default: false + proxy_protocol: false + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet recevied within the timeout. + ## + ## @doc zone..listeners..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout: 3s + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog: 1024 + tcp.buffer: 4KB + } + + ## MQTT/SSL - SSL Listener for MQTT Protocol + listeners.mqtt_ssl: + #${example_common_tcp_options} ${example_common_ssl_options} # common options can be written in a separate config entry and reference it from here. + { + + ## The type of the listener. + ## + ## @doc zone..listeners..type + ## ValueType: tcp | ssl | ws | wss + ## - tcp: MQTT over TCP + ## - ssl: MQTT over TLS + ## - ws: MQTT over Websocket + ## - wss: MQTT over WebSocket Secure + ## Required: true + type: ssl + + ## The IP address and port that the listener will bind. + ## + ## @doc zone..listeners..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8883, 127.0.0.1:8883, ::1:8883 + bind: "0.0.0.0:8883" + + ## The size of the acceptor pool for this listener. + ## + ## @doc zone..listeners..acceptors + ## ValueType: Number | num_cpu_cores + ## Default: num_cpu_cores + acceptors: num_cpu_cores + + ## Maximum number of concurrent connections. + ## + ## @doc zone..listeners..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections: 512000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc zone..listeners..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules: [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc zone..listeners..proxy_protocol + ## ValueType: Boolean + ## Default: true + proxy_protocol: true + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet recevied within the timeout. + ## + ## @doc zone..listeners..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout: 3s + + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog: 1024 + tcp.buffer: 4KB + } + + listeners.mqtt_ws: + #${example_common_tcp_options} ${example_common_websocket_options} # common options can be written in a separate config entry and reference it from here. + { + + ## The type of the listener. + ## + ## @doc zone..listeners..type + ## ValueType: tcp | ssl | ws | wss + ## - tcp: MQTT over TCP + ## - ssl: MQTT over TLS + ## - ws: MQTT over Websocket + ## - wss: MQTT over WebSocket Secure + ## Required: true + type: ws + + ## The IP address and port that the listener will bind. + ## + ## @doc zone..listeners..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8083, 127.0.0.1:8083, ::1:8083 + bind: "0.0.0.0:8083" + + ## The size of the acceptor pool for this listener. + ## + ## @doc zone..listeners..acceptors + ## ValueType: Number | num_cpu_cores + ## Default: num_cpu_cores + acceptors: num_cpu_cores + + ## Maximum number of concurrent connections. + ## + ## @doc zone..listeners..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections: 1024000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc zone..listeners..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules: [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc zone..listeners..proxy_protocol + ## ValueType: Boolean + ## Default: true + proxy_protocol: true + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet recevied within the timeout. + ## + ## @doc zone..listeners..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout: 3s + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog: 1024 + tcp.buffer: 4KB + + ## Websocket options + ## See ${example_common_websocket_options} for more information + websocket.idle_timeout: 86400s + } + + listeners.mqtt_wss: + #${example_common_tcp_options} ${example_common_ssl_options} ${example_common_websocket_options} # common options can be written in a separate config entry and reference it from here. + { + ## The name of the listener. + ## + ## @doc zone..listeners..name + ## ValueType: String + ## Required: true + name: mqtt_over_wss + + ## The type of the listener. + ## + ## @doc zone..listeners..type + ## ValueType: tcp | ssl | ws | wss + ## - tcp: MQTT over TCP + ## - ssl: MQTT over TLS + ## - ws: MQTT over Websocket + ## - wss: MQTT over WebSocket Secure + ## Required: true + type: wss + + ## The IP address and port that the listener will bind. + ## + ## @doc zone..listeners..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8084, 127.0.0.1:8084, ::1:8084 + bind: "0.0.0.0:8084" + + ## The size of the acceptor pool for this listener. + ## + ## @doc zone..listeners..acceptors + ## ValueType: Number | num_cpu_cores + ## Default: num_cpu_cores + acceptors: num_cpu_cores + + ## Maximum number of concurrent connections. + ## + ## @doc zone..listeners..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections: 512000 + + ## The access control rules for this listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## @doc zone..listeners..access_rules + ## ValueType: Array + ## Default: [] + ## Examples: + ## access_rules: [ + ## "deny 192.168.0.0/24", + ## "all all" + ## ] + access_rules: [ + "allow all" + ] + + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## @doc zone..listeners..proxy_protocol + ## ValueType: Boolean + ## Default: true + proxy_protocol: true + + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet recevied within the timeout. + ## + ## @doc zone..listeners..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout: 3s + + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog: 1024 + tcp.buffer: 4KB + + ## Websocket options + ## See ${example_common_websocket_options} for more information + websocket.idle_timeout: 86400s + } + +} + +#This is an example zone which has less "strict" settings. +#It's useful to clients connecting the broker from trusted networks. +zone.internal { + acl.enable: false + auth.enable: false + listeners.mqtt_internal: { + type: tcp + bind: "127.0.0.1:11883" + acceptors: 4 + max_connections: 1024000 + tcp.active_n: 1000 + tcp.backlog: 512 + } +} + +##================================================================== +## System Monitor +##================================================================== +sysmon { + ## The time interval for the periodic process limit check + ## + ## @doc sysmon.vm.process_check_interval + ## ValueType: Duration + ## Default: 30s + vm.process_check_interval: 30s + + ## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. + ## + ## @doc sysmon.vm.process_high_watermark + ## ValueType: Percentage + ## Default: 80% + vm.process_high_watermark: 80% + + ## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. + ## + ## @doc sysmon.vm.process_low_watermark + ## ValueType: Percentage + ## Default: 60% + vm.process_low_watermark: 60% + + ## Enable Long GC monitoring. + ## Notice: don't enable the monitor in production for: + ## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 + ## + ## @doc sysmon.vm.long_gc + ## ValueType: Duration | disabled + ## Default: disabled + vm.long_gc: disabled + + ## Enable Long Schedule(ms) monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## @doc sysmon.vm.long_schedule + ## ValueType: Duration | disabled + ## Default: disabled + vm.long_schedule: 240ms + + ## Enable Large Heap monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## @doc sysmon.vm.large_heap + ## ValueType: Size | disabled + ## Default: 32MB + vm.large_heap: 32MB + + ## Enable Busy Port monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## @doc sysmon.vm.busy_port + ## ValueType: Boolean + ## Default: true + vm.busy_port: true + + ## Enable Busy Dist Port monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## @doc sysmon.vm.busy_dist_port + ## ValueType: Boolean + ## Default: true + vm.busy_dist_port: true + + ## The time interval for the periodic cpu check + ## + ## @doc sysmon.os.cpu_check_interval + ## ValueType: Duration + ## Default: 60s + os.cpu_check_interval: 60s + + ## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. + ## + ## @doc sysmon.os.cpu_high_watermark + ## ValueType: Percentage + ## Default: 80% + os.cpu_high_watermark: 80% + + ## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. + ## + ## @doc sysmon.os.cpu_low_watermark + ## ValueType: Percentage + ## Default: 60% + os.cpu_low_watermark: 60% + + ## The time interval for the periodic memory check + ## + ## @doc sysmon.os.mem_check_interval + ## ValueType: Duration | disabled + ## Default: 60s + os.mem_check_interval: 60s + + ## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. + ## + ## @doc sysmon.os.sysmem_high_watermark + ## ValueType: Percentage + ## Default: 70% + os.sysmem_high_watermark: 70% + + ## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. + ## + ## @doc sysmon.os.procmem_high_watermark + ## ValueType: Percentage + ## Default: 5% + os.procmem_high_watermark: 5% +} + +##================================================================== +## Alarm +##================================================================== +alarm { + ## Specifies the actions to take when an alarm is activated + ## + ## @doc alarm.actions + ## ValueType: Array + ## Default: [log, publish] + actions: [log, publish] + + ## The maximum number of deactivated alarms + ## + ## @doc alarm.size_limit + ## ValueType: Integer + ## Default: 1000 + size_limit: 1000 + + ## Validity Period of deactivated alarms + ## + ## @doc alarm.validity_period + ## ValueType: Duration + ## Default: 24h + validity_period: 24h +} + +## Config references for listeners + +## Socket options for TCP connections +## See: http://erlang.org/doc/man/inet.html +example_common_tcp_options { + ## Specify the {active, N} option for this Socket. + ## + ## See: https://erlang.org/doc/man/inet.html#setopts-2 + ## + ## @doc listeners..tcp.active_n + ## ValueType: Number + ## Default: 100 + tcp.active_n: 100 + + ## TCP backlog defines the maximum length that the queue of + ## pending connections can grow to. + ## + ## @doc listeners..tcp.backlog + ## ValueType: Number + ## Range: [0, 1048576] + ## Default: 128 + tcp.backlog: 128 + + ## The TCP send timeout for the connections. + ## + ## @doc listeners..tcp.send_timeout + ## ValueType: Duration + ## Default: 15s + tcp.send_timeout: 15s + + ## Close the connection if send timeout. + ## + ## @doc listeners..tcp.send_timeout_close + ## ValueType: Boolean + ## Default: true + tcp.send_timeout_close: true + + ## The TCP receive buffer(os kernel) for the connections. + ## + ## @doc listeners..tcp.recbuf + ## ValueType: Size + ## Default: 2KB + tcp.recbuf: 2KB + + ## The TCP send buffer(os kernel) for the connections. + ## + ## @doc listeners..tcp.sndbuf + ## ValueType: Size + ## Default: 2KB + tcp.sndbuf: 2KB + + ## The size of the user-level software buffer used by the driver. + ## + ## @doc listeners..tcp.buffer + ## ValueType: Size + ## Default: 2KB + tcp.buffer: 2KB + + ## Sets the 'buffer: max(sndbuf, recbuf)' if this option is enabled. + ## + ## @doc listeners..tcp.tune_buffer + ## ValueType: Boolean + ## Default: false + tcp.tune_buffer: false + + ## The socket is set to a busy state when the amount of data queued internally + ## by the ERTS socket implementation reaches this limit. + ## + ## @doc listeners..tcp.high_watermark + ## ValueType: Size + ## Default: 1MB + tcp.high_watermark: 1MB + + ## The TCP_NODELAY flag for the connections. + ## + ## @doc listeners..tcp.nodelay + ## ValueType: Boolean + ## Default: true + tcp.nodelay: true + + ## The SO_REUSEADDR flag for the connections. + ## + ## @doc listeners..tcp.reuseaddr + ## ValueType: Boolean + ## Default: true + tcp.reuseaddr: true +} + +## Socket options for SSL connections +## See: http://erlang.org/doc/man/ssl.html +example_common_ssl_options { + + ## A performance optimization setting, it allows clients to reuse + ## pre-existing sessions, instead of initializing new ones. + ## Read more about it here. + ## + ## @doc listeners..ssl.reuse_sessions + ## ValueType: Boolean + ## Default: true + ssl.reuse_sessions: true + + ## SSL parameter renegotiation is a feature that allows a client and a server + ## to renegotiate the parameters of the SSL connection on the fly. + ## RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, + ## you drop support for the insecure renegotiation, prone to MitM attacks. + ## + ## @doc listeners..ssl.secure_renegotiate + ## ValueType: Boolean + ## Default: true + ssl.secure_renegotiate: true + + ## An important security setting, it forces the cipher to be set based + ## on the server-specified order instead of the client-specified order, + ## hence enforcing the (usually more properly configured) security + ## ordering of the server administrator. + ## + ## @doc listeners..ssl.honor_cipher_order + ## ValueType: Boolean + ## Default: true + ssl.honor_cipher_order: true + + ## TLS versions only to protect from POODLE attack. + ## + ## @doc listeners..ssl.tls_versions + ## ValueType: Array + ## Default: [tlsv1.2,tlsv1.1,tlsv1] + ssl.tls_versions: [tlsv1.2,tlsv1.1,tlsv1] + + ## TLS Handshake timeout. + ## + ## @doc listeners..ssl.handshake_timeout + ## ValueType: Duration + ## Default: 15s + ssl.handshake_timeout: 15s + + ## Maximum number of non-self-issued intermediate certificates that + ## can follow the peer certificate in a valid certification path. + ## + ## @doc listeners..ssl.depth + ## ValueType: Integer + ## Default: 10 + ssl.depth: 10 + + ## Path to the file containing the user's private PEM-encoded key. + ## + ## @doc listeners..ssl.keyfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/key.pem" + ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" + + ## Path to a file containing the user certificate. + ## + ## @doc listeners..ssl.certfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/cert.pem" + ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" + + ## Path to the file containing PEM-encoded CA certificates. The CA certificates + ## are used during server authentication and when building the client certificate chain. + ## + ## @doc listeners..ssl.cacertfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/cacert.pem" + ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + + ## Maximum number of non-self-issued intermediate certificates that + ## can follow the peer certificate in a valid certification path. + ## + ## @doc listeners..ssl.depth + ## ValueType: Number + ## Default: 10 + ssl.depth = 10 + + ## String containing the user's password. Only used if the private keyfile + ## is password-protected. + ## + ## See: listener.ssl.$name.key_password + ## + ## @doc listeners..ssl.depth + ## ValueType: String + ## Default: "" + #ssl.key_password = "" + + ## The Ephemeral Diffie-Helman key exchange is a very effective way of + ## ensuring Forward Secrecy by exchanging a set of keys that never hit + ## the wire. Since the DH key is effectively signed by the private key, + ## it needs to be at least as strong as the private key. In addition, + ## the default DH groups that most of the OpenSSL installations have + ## are only a handful (since they are distributed with the OpenSSL + ## package that has been built for the operating system it’s running on) + ## and hence predictable (not to mention, 1024 bits only). + ## In order to escape this situation, first we need to generate a fresh, + ## strong DH group, store it in a file and then use the option above, + ## to force our SSL application to use the new DH group. Fortunately, + ## OpenSSL provides us with a tool to do that. Simply run: + ## openssl dhparam -out dh-params.pem 2048 + ## + ## @doc listeners..ssl.dhfile + ## ValueType: File + ## Default: "{{ platform_etc_dir }}/certs/dh-params.pem" + #ssl.dhfile: "{{ platform_etc_dir }}/certs/dh-params.pem" + + ## A server only does x509-path validation in mode verify_peer, + ## as it then sends a certificate request to the client (this + ## message is not sent if the verify option is verify_none). + ## You can then also want to specify option fail_if_no_peer_cert. + ## More information at: http://erlang.org/doc/man/ssl.html + ## + ## @doc listeners..ssl.verify + ## ValueType: verify_peer | verify_none + ## Default: verify_none + ssl.verify: verify_none + + ## Used together with {verify, verify_peer} by an SSL server. If set to true, + ## the server fails if the client does not have a certificate to send, that is, + ## sends an empty certificate. + ## + ## @doc listeners..ssl.fail_if_no_peer_cert + ## ValueType: Boolean + ## Default: true + ssl.fail_if_no_peer_cert: false + + ## This is the single most important configuration option of an Erlang SSL + ## application. Ciphers (and their ordering) define the way the client and + ## server encrypt information over the wire, from the initial Diffie-Helman + ## key exchange, the session key encryption ## algorithm and the message + ## digest algorithm. Selecting a good cipher suite is critical for the + ## application’s data security, confidentiality and performance. + ## + ## The cipher list above offers: + ## + ## A good balance between compatibility with older browsers. + ## It can get stricter for Machine-To-Machine scenarios. + ## Perfect Forward Secrecy. + ## No old/insecure encryption and HMAC algorithms + ## + ## Most of it was copied from Mozilla’s Server Side TLS article + ## + ## @doc listeners..ssl.ciphers + ## ValueType: Array + ## Default: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA] + ssl.ciphers: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA] + + ## Ciphers for TLS PSK. See 'https://tools.ietf.org/html/rfc4279#section-2'. + ## + ## Note that 'ciphers' and 'psk_ciphers' cannot be configured at the same time. + ## + ## @doc listeners..ssl.psk_ciphers + ## ValueType: Array + ## Default: [PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] + ssl.psk_ciphers: [PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] +} + +## Socket options for websocket connections +example_common_websocket_options { + ## The path of WebSocket MQTT endpoint + ## + ## @doc listeners..websocket.mqtt_path + ## ValueType: Path + ## Default: "/mqtt" + websocket.mqtt_path: "/mqtt" + + ## Whether a WebSocket message is allowed to contain multiple MQTT packets + ## + ## @doc listeners..websocket.mqtt_piggyback + ## ValueType: single | multiple + ## Default: multiple + websocket.mqtt_piggyback: multiple + + ## The compress flag for external WebSocket connections. + ## + ## If this Value is set true,the websocket message would be compressed + ## + ## @doc listeners..websocket.compress + ## ValueType: Boolean + ## Default: true + websocket.compress: true + + ## The idle timeout for external WebSocket connections. + ## + ## @doc listeners..websocket.idle_timeout + ## ValueType: Duration | infinity + ## Default: infinity + websocket.idle_timeout: infinity + + ## The max frame size for external WebSocket connections. + ## + ## @doc listeners..websocket.max_frame_size + ## ValueType: Size + ## Default: infinity + websocket.max_frame_size: infinity + + ## If set to true, the server fails if the client does not + ## have a Sec-WebSocket-Protocol to send. + ## Set to false for WeChat MiniApp. + ## + ## @doc listeners..websocket.fail_if_no_subprotocol + ## ValueType: Boolean + ## Default: true + websocket.fail_if_no_subprotocol = true + + ## Enable origin check in header for websocket connection + ## + ## @doc listeners..websocket.check_origin_enable + ## ValueType: Boolean + ## Default: false + websocket.check_origin_enable = false + + ## Allow origin to be absent in header in websocket connection + ## when check_origin_enable is true + ## + ## @doc listeners..websocket.allow_origin_absence + ## ValueType: Boolean + ## Default: true + websocket.allow_origin_absence = true + + ## Comma separated list of allowed origin in header for websocket connection + ## + ## @doc listeners..websocket.check_origins + ## ValueType: String + ## Examples: + ## local http dashboard url + ## check_origins: "http://localhost:18083, http://127.0.0.1:18083" + ## Default: "" + websocket.check_origins = "http://localhost:18083, http://127.0.0.1:18083" + + ## Specify which HTTP header for real source IP if the EMQ X cluster is + ## deployed behind NGINX or HAProxy. + ## + ## @doc listeners..websocket.proxy_address_header + ## ValueType: String + ## Default: X-Forwarded-For + websocket.proxy_address_header = X-Forwarded-For + + ## Specify which HTTP header for real source port if the EMQ X cluster is + ## deployed behind NGINX or HAProxy. + ## + ## @doc listeners..websocket.proxy_port_header + ## ValueType: String + ## Default: X-Forwarded-Port + websocket.proxy_port_header = X-Forwarded-Port + + websocket.deflate_opts { + ## The level of deflate options for external WebSocket connections. + ## + ## @doc listeners..websocket.deflate_opts.level + ## ValueType: none | default | best_compression | best_speed + ## Default: default + level: default + + ## The mem_level of deflate options for external WebSocket connections. + ## + ## @doc listeners..websocket.deflate_opts.mem_level + ## ValueType: Integer + ## Range: [1,9] + ## Default: 8 + mem_level: 8 + + ## The strategy of deflate options for external WebSocket connections. + ## + ## @doc listeners..websocket.deflate_opts.strategy + ## ValueType: default | filtered | huffman_only | rle + ## Default: default + strategy: default + + ## The deflate option for external WebSocket connections. + ## + ## @doc listeners..websocket.deflate_opts.server_context_takeover + ## ValueType: takeover | no_takeover + ## Default: takeover + server_context_takeover: takeover + + ## The deflate option for external WebSocket connections. + ## + ## @doc listeners..websocket.deflate_opts.client_context_takeover + ## ValueType: takeover | no_takeover + ## Default: takeover + client_context_takeover: takeover + + ## The deflate options for external WebSocket connections. + ## + ## + ## @doc listeners..websocket.deflate_opts.server_max_window_bits + ## ValueType: Integer + ## Range: [8,15] + ## Default: 15 + server_max_window_bits: 15 + + ## The deflate options for external WebSocket connections. + ## + ## @doc listeners..websocket.deflate_opts.client_max_window_bits + ## ValueType: Integer + ## Range: [8,15] + ## Default: 15 + client_max_window_bits: 15 + } +} \ No newline at end of file diff --git a/apps/emqx/etc/emqx.conf.old b/apps/emqx/etc/emqx.conf.old new file mode 100644 index 000000000..d2b5fd11d --- /dev/null +++ b/apps/emqx/etc/emqx.conf.old @@ -0,0 +1,2467 @@ +## EMQ X Configuration 4.3 + +## NOTE: Do not change format of CONFIG_SECTION_{BGN,END} comments! + +## CONFIG_SECTION_BGN=cluster ================================================== + +## Cluster name. +## +## Value: String +cluster.name = emqxcl + +## Specify the erlang distributed protocol. +## +## Value: Enum +## - inet_tcp: the default; handles TCP streams with IPv4 addressing. +## - inet6_tcp: handles TCP with IPv6 addressing. +## - inet_tls: using TLS for Erlang Distribution. +## +## vm.args: -proto_dist inet_tcp +cluster.proto_dist = inet_tcp + +## Cluster auto-discovery strategy. +## +## Value: Enum +## - manual: Manual join command +## - static: Static node list +## - mcast: IP Multicast +## - dns: DNS A Record +## - etcd: etcd +## - k8s: Kubernetes +## +## Default: manual +cluster.discovery = manual + +## Enable cluster autoheal from network partition. +## +## Value: on | off +## +## Default: on +cluster.autoheal = on + +## Autoclean down node. A down node will be removed from the cluster +## if this value > 0. +## +## Value: Duration +## -h: hour, e.g. '2h' for 2 hours +## -m: minute, e.g. '5m' for 5 minutes +## -s: second, e.g. '30s' for 30 seconds +## +## Default: 5m +cluster.autoclean = 5m + +##-------------------------------------------------------------------- +## Cluster using static node list + +## Node list of the cluster. +## +## Value: String +## cluster.static.seeds = "emqx1@127.0.0.1,emqx2@127.0.0.1" + +##-------------------------------------------------------------------- +## Cluster using IP Multicast. + +## IP Multicast Address. +## +## Value: IP Address +## cluster.mcast.addr = "239.192.0.1" + +## Multicast Ports. +## +## Value: Port List +## cluster.mcast.ports = "4369,4370" + +## Multicast Iface. +## +## Value: Iface Address +## +## Default: "0.0.0.0" +## cluster.mcast.iface = "0.0.0.0" + +## Multicast Ttl. +## +## Value: 0-255 +## cluster.mcast.ttl = 255 + +## Multicast loop. +## +## Value: on | off +## cluster.mcast.loop = on + +##-------------------------------------------------------------------- +## Cluster using DNS A records. + +## DNS name. +## +## Value: String +## cluster.dns.name = localhost + +## The App name is used to build 'node.name' with IP address. +## +## Value: String +## cluster.dns.app = emqx + +##-------------------------------------------------------------------- +## Cluster using etcd + +## Etcd server list, seperated by ','. +## +## Value: String +## cluster.etcd.server = "http://127.0.0.1:2379" + +## Etcd api version +## +## Value: Enum +## - v2 +## - v3 +## cluster.etcd.version = v3 + +## The prefix helps build nodes path in etcd. Each node in the cluster +## will create a path in etcd: v2/keys/// +## +## Value: String +## cluster.etcd.prefix = emqxcl + +## The TTL for node's path in etcd. +## +## Value: Duration +## +## Default: 1m, 1 minute +## cluster.etcd.node_ttl = 1m + +## Path to a file containing the client's private PEM-encoded key. +## +## Value: File +## cluster.etcd.ssl.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" + +## The path to a file containing the client's certificate. +## +## Value: File +## cluster.etcd.ssl.certfile = "{{ platform_etc_dir }}/certs/client.pem" + +## Path to the file containing PEM-encoded CA certificates. The CA certificates +## are used during server authentication and when building the client certificate chain. +## +## Value: File +## cluster.etcd.ssl.cacertfile = "{{ platform_etc_dir }}/certs/ca.pem" + +##-------------------------------------------------------------------- +## Cluster using Kubernetes + +## Kubernetes API server list, seperated by ','. +## +## Value: String +## cluster.k8s.apiserver = "http://10.110.111.204:8080" + +## The service name helps lookup EMQ nodes in the cluster. +## +## Value: String +## cluster.k8s.service_name = emqx + +## The address type is used to extract host from k8s service. +## +## Value: ip | dns | hostname +## cluster.k8s.address_type = ip + +## The app name helps build 'node.name'. +## +## Value: String +## cluster.k8s.app_name = emqx + +## The suffix added to dns and hostname get from k8s service +## +## Value: String +## cluster.k8s.suffix = pod.cluster.local + +## Kubernetes Namespace +## +## Value: String +## cluster.k8s.namespace = default + +## CONFIG_SECTION_END=cluster ================================================== + +##-------------------------------------------------------------------- +## Node +##-------------------------------------------------------------------- + +## Node name. +## +## See: http://erlang.org/doc/reference_manual/distributed.html +## +## Value: @ +## +## Default: emqx@127.0.0.1 +node.name = "emqx@127.0.0.1" + +## Cookie for distributed node communication. +## +## Value: String +node.cookie = "emqxsecretcookie" + +## Data dir for the node +## +## Value: Folder +node.data_dir = "{{ platform_data_dir }}" + +## The config file dir for the node +## +## Value: Folder +node.etc_dir = "{{ platform_etc_dir }}" + +## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable +## heartbeat, or set the value as 'on' +## +## Value: on +## +## vm.args: -heart +## node.heartbeat = on + +## Sets the number of threads in async thread pool. Valid range is 0-1024. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: 0-1024 +## +## vm.args: +A Number +## node.async_threads = 4 + +## Sets the maximum number of simultaneously existing processes for this +## system if a Number is passed as value. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1024-134217727] +## +## vm.args: +P Number +## node.process_limit = 2097152 + +## Sets the maximum number of simultaneously existing ports for this system. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1024-134217727] +## +## vm.args: +Q Number +## node.max_ports = 1048576 + +## Sets the distribution buffer busy limit (dist_buf_busy_limit). +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1KB-2GB] +## +## vm.args: +zdbbl size +## node.dist_buffer_size = 8MB + +## Sets the maximum number of ETS tables. Note that mnesia and SSL will +## create temporary ETS tables. +## +## Value: Number +## +## vm.args: +e Number +## node.max_ets_tables = 262144 + +## Global GC Interval. +## +## Value: Duration +## +## Examples: +## - 2h: 2 hours +## - 30m: 30 minutes +## - 20s: 20 seconds +## +## Defaut: 15 minutes +node.global_gc_interval = 15m + +## Tweak GC to run more often. +## +## Value: Number [0-65535] +## +## vm.args: -env ERL_FULLSWEEP_AFTER Number +## node.fullsweep_after = 1000 + +## Crash dump log file. +## +## Value: Log file +node.crash_dump = "{{ platform_log_dir }}/crash.dump" + +## Specify SSL Options in the file if using SSL for Erlang Distribution. +## +## Value: File +## +## vm.args: -ssl_dist_optfile +## node.ssl_dist_optfile = "{{ platform_etc_dir }}/ssl_dist.conf" + +## Sets the net_kernel tick time. TickTime is specified in seconds. +## Notice that all communicating nodes are to have the same TickTime +## value specified. +## +## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime +## +## Value: Number +## +## vm.args: -kernel net_ticktime Number +## node.dist_net_ticktime = 120 + +## Sets the port range for the listener socket of a distributed Erlang node. +## Note that if there are firewalls between clustered nodes, this port segment +## for nodes’ communication should be allowed. +## +## See: http://www.erlang.org/doc/man/kernel_app.html +## +## Value: Port [1024-65535] +node.dist_listen_min = 6369 +node.dist_listen_max = 6369 + +node.backtrace_depth = 16 + +## CONFIG_SECTION_BGN=rpc ====================================================== + +## RPC Mode. +## +## Value: sync | async +rpc.mode = async + +## Max batch size of async RPC requests. +## +## Value: Integer +## Zero or negative value disables rpc batching. +## +## NOTE: RPC batch won't work when rpc.mode = sync +rpc.async_batch_size = 256 + +## RPC port discovery +## +## The strategy for discovering the RPC listening port of other nodes. +## +## Value: Enum +## - manual: discover ports by `tcp_server_port` and `tcp_client_port`. +## - stateless: discover ports in a stateless manner. +## If node name is `emqx@127.0.0.1`, where the `` is an integer, +## then the listening port will be `5370 + ` +## +## Defaults to `stateless`. +rpc.port_discovery = stateless + +## TCP port number for RPC server to listen on. +## +## Only takes effect when `rpc.port_discovery` = `manual`. +## +## NOTE: All nodes in the cluster should agree to this same config. +## +## Value: Port [1024-65535] +#rpc.tcp_server_port = 5369 + +## Number of outgoing RPC connections. +## +## Value: Interger [0-256] +## Defaults to NumberOfCPUSchedulers / 2 when set to 0 +#rpc.tcp_client_num = 0 + +## RCP Client connect timeout. +## +## Value: Seconds +rpc.connect_timeout = 5s + +## TCP send timeout of RPC client and server. +## +## Value: Seconds +rpc.send_timeout = 5s + +## Authentication timeout +## +## Value: Seconds +rpc.authentication_timeout = 5s + +## Default receive timeout for call() functions +## +## Value: Seconds +rpc.call_receive_timeout = 15s + +## Socket idle keepalive. +## +## Value: Seconds +rpc.socket_keepalive_idle = 900s + +## TCP Keepalive probes interval. +## +## Value: Seconds +rpc.socket_keepalive_interval = 75s + +## Probes lost to close the connection +## +## Value: Integer +rpc.socket_keepalive_count = 9 + +## Size of TCP send buffer. +## +## Value: Bytes +rpc.socket_sndbuf = 1MB + +## Size of TCP receive buffer. +## +## Value: Seconds +rpc.socket_recbuf = 1MB + +## Size of user-level software socket buffer. +## +## Value: Seconds +rpc.socket_buffer = 1MB + +## CONFIG_SECTION_END=rpc ====================================================== + +## CONFIG_SECTION_BGN=logger =================================================== + +## Where to emit the logs. +## Enable the console (standard output) logs. +## +## Value: file | console | both +## - file: write logs only to file +## - console: write logs only to standard I/O +## - both: write logs both to file and standard I/O +log.to = file + +## The log severity level. +## +## Value: debug | info | notice | warning | error | critical | alert | emergency +## +## Note: Only the messages with severity level higher than or equal to +## this level will be logged. +## +## Default: warning +log.level = warning + +## Timezone offset to display in logs +## Value: +## - "system" use system zone +## - "utc" for Universal Coordinated Time (UTC) +## - "+hh:mm" or "-hh:mm" for a specified offset +log.time_offset = system + +## The dir for log files. +## +## Value: Folder +log.dir = "{{ platform_log_dir }}" + +## The log filename for logs of level specified in "log.level". +## +## If `log.rotation` is enabled, this is the base name of the +## files. Each file in a rotated log is named .N, where N is an integer. +## +## Value: String +## Default: emqx.log +log.file = emqx.log + +## Limits the total number of characters printed for each log event. +## +## Value: Integer +## Default: No Limit +#log.chars_limit = 8192 + +## Maximum depth for Erlang term log formatting +## and Erlang process message queue inspection. +## +## Value: Integer or 'unlimited' (without quotes) +## Default: 80 +#log.max_depth = 80 + +## Log formatter +## Value: text | json +#log.formatter = text + +## Log to single line +## Value: Boolean +#log.single_line = true + +## Enables the log rotation. +## With this enabled, new log files will be created when the current +## log file is full, max to `log.rotation.size` files will be created. +## +## Value: on | off +## Default: on +log.rotation.enable = on + +## Maximum size of each log file. +## +## Value: Number +## Default: 10M +## Supported Unit: KB | MB | GB +log.rotation.size = 10MB + +## Maximum rotation count of log files. +## +## Value: Number +## Default: 5 +log.rotation.count = 5 + +## To create additional log files for specific log levels. +## +## Value: File Name +## Format: log.$level.file = $filename, +## where "$level" can be one of: debug, info, notice, warning, +## error, critical, alert, emergency +## Note: Log files for a specific log level will only contain all the logs +## that higher than or equal to that level +## +#log.info.file = info.log +#log.error.file = error.log + +## The max allowed queue length before switching to sync mode. +## +## Log overload protection parameter. If the message queue grows +## larger than this value the handler switches from anync to sync mode. +## +## Default: 100 +## +#log.sync_mode_qlen = 100 + +## The max allowed queue length before switching to drop mode. +## +## Log overload protection parameter. When the message queue grows +## larger than this threshold, the handler switches to a mode in which +## it drops all new events that senders want to log. +## +## Default: 3000 +## +#log.drop_mode_qlen = 3000 + +## The max allowed queue length before switching to flush mode. +## +## Log overload protection parameter. If the length of the message queue +## grows larger than this threshold, a flush (delete) operation takes place. +## To flush events, the handler discards the messages in the message queue +## by receiving them in a loop without logging. +## +## Default: 8000 +## +#log.flush_qlen = 8000 + +## Kill the log handler when it gets overloaded. +## +## Log overload protection parameter. It is possible that a handler, +## even if it can successfully manage peaks of high load without crashing, +## can build up a large message queue, or use a large amount of memory. +## We could kill the log handler in these cases and restart it after a +## few seconds. +## +## Default: on +## +#log.overload_kill = on + +## The max allowed queue length before killing the log hanlder. +## +## Log overload protection parameter. This is the maximum allowed queue +## length. If the message queue grows larger than this, the handler +## process is terminated. +## +## Default: 20000 +## +#log.overload_kill_qlen = 20000 + +## The max allowed memory size before killing the log hanlder. +## +## Log overload protection parameter. This is the maximum memory size +## that the handler process is allowed to use. If the handler grows +## larger than this, the process is terminated. +## +## Default: 30MB +## +#log.overload_kill_mem_size = 30MB + +## Restart the log hanlder after some seconds. +## +## Log overload protection parameter. If the handler is terminated, +## it restarts automatically after a delay specified in seconds. +## The value "infinity" prevents restarts. +## +## Default: 5s +## +#log.overload_kill_restart_after = 5s + +## Max burst count and time window for burst control. +## +## Log overload protection parameter. Large bursts of log events - many +## events received by the handler under a short period of time - can +## potentially cause problems. By specifying the maximum number of events +## to be handled within a certain time frame, the handler can avoid +## choking the log with massive amounts of printouts. +## +## This config controls the maximum number of events to handle within +## a time frame. After the limit is reached, successive events are +## dropped until the end of the time frame. +## +## Note that there would be no warning if any messages were +## dropped because of burst control. +## +## Comment this config out to disable the burst control feature. +## +## Value: MaxBurstCount,TimeWindow +## Default: disabled +## +#log.burst_limit = "20000, 1s" + +## CONFIG_SECTION_END=logger =================================================== + +##-------------------------------------------------------------------- +## Authentication/Access Control +##-------------------------------------------------------------------- + +## Allow anonymous authentication by default if no auth plugins loaded. +## Notice: Disable the option in production deployment! +## +## Value: true | false +acl.allow_anonymous = true + +## Allow or deny if no ACL rules matched. +## +## Value: allow | deny +acl.acl_nomatch = allow + +## Default ACL File. +## +## Value: File Name +acl.acl_file = "{{ platform_etc_dir }}/acl.conf" + +## Whether to enable ACL cache. +## +## If enabled, ACLs roles for each client will be cached in the memory +## +## Value: on | off +acl.enable_acl_cache = on + +## The maximum count of ACL entries can be cached for a client. +## +## Value: Integer greater than 0 +## Default: 32 +acl.acl_cache_max_size = 32 + +## The time after which an ACL cache entry will be deleted +## +## Value: Duration +## Default: 1 minute +acl.acl_cache_ttl = 1m + +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +acl.acl_deny_action = ignore + +## Specify the global flapping detect policy. +## The value is a string composed of flapping threshold, duration and banned interval. +## 1. threshold: an integer to specfify the disconnected times of a MQTT Client; +## 2. duration: the time window for flapping detect; +## 3. banned interval: the banned interval if a flapping is detected. +## +## Value: Integer,Duration,Duration +acl.flapping_detect_policy = "30, 1m, 5m" + +##-------------------------------------------------------------------- +## MQTT Protocol +##-------------------------------------------------------------------- + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## Default: 1MB +mqtt.max_packet_size = 1MB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +mqtt.max_clientid_len = 65535 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number +mqtt.max_topic_levels = 0 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +mqtt.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no topic alias supported. +## +## Value: 0-65535 +mqtt.max_topic_alias = 65535 + +## Whether the Server supports MQTT retained messages. +## +## Value: boolean +mqtt.retain_available = true + +## Whether the Server supports MQTT Wildcard Subscriptions +## +## Value: boolean +mqtt.wildcard_subscription = true + +## Whether the Server supports MQTT Shared Subscriptions. +## +## Value: boolean +mqtt.shared_subscription = true + +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +mqtt.ignore_loop_deliver = false + +## Whether to parse the MQTT frame in strict mode +## +## Value: true | false +mqtt.strict_mode = false + +## Specify the response information returned to the client +## +## Value: String +## mqtt.response_information = example + +## CONFIG_SECTION_BGN=zones =================================================== + +##-------------------------------------------------------------------- +## External Zone + +## Idle timeout of the external MQTT connections. +## +## Value: duration +zone.external.idle_timeout = 15s + +## Enable ACL check. +## +## Value: Flag +zone.external.enable_acl = on + +## Enable ban check. +## +## Value: Flag +zone.external.enable_ban = on + +## Enable per connection statistics. +## +## Value: on | off +zone.external.enable_stats = on + +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +zone.external.acl_deny_action = ignore + +## Force the MQTT connection process GC after this number of +## messages | bytes passed through. +## +## Numbers delimited by `|'. Zero or negative is to disable. +zone.external.force_gc_policy = "16000|16MB" + +## Max message queue length and total heap size to force shutdown +## connection/session process. +## Message queue here is the Erlang process mailbox, but not the number +## of queued MQTT messages of QoS 1 and 2. +## +## Numbers delimited by `|'. Zero or negative is to disable. +## +## Default: +## - "10000|64MB" on ARCH_64 system +## - "1000|32MB" on ARCH_32 sytem +#zone.external.force_shutdown_policy = "10000|64MB" + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## Default: 1MB +## zone.external.max_packet_size = 64KB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +## zone.external.max_clientid_len = 1024 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number +## zone.external.max_topic_levels = 7 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +## zone.external.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no limit. +## +## Value: 0-65535 +## zone.external.max_topic_alias = 65535 + +## Whether the Server supports retained messages. +## +## Value: boolean +## zone.external.retain_available = true + +## Whether the Server supports Wildcard Subscriptions +## +## Value: boolean +## zone.external.wildcard_subscription = false + +## Whether the Server supports Shared Subscriptions +## +## Value: boolean +## zone.external.shared_subscription = false + +## Server Keep Alive +## +## Value: Number +## zone.external.server_keepalive = 0 + +## The backoff for MQTT keepalive timeout. The broker will kick a connection out +## until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.external.keepalive_backoff = 0.75 + +## Maximum number of subscriptions allowed, 0 means no limit. +## +## Value: Number +zone.external.max_subscriptions = 0 + +## Force to upgrade QoS according to subscription. +## +## Value: on | off +zone.external.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.external.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.external.retry_interval = 30s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.external.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.external.await_rel_timeout = 300s + +## Default session expiry interval for MQTT V3.1.1 connections. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.external.session_expiry_interval = 2h + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.external.max_mqueue_len = 1000 + +## Topic priorities. +## 'none' to indicate no priority table (by default), hence all messages +## are treated equal +## +## Priority number [1-255] +## Example: "topic/1=10,topic/2=8" +## NOTE: comma and equal signs are not allowed for priority topic names +## NOTE: messages for topics not in the priority table are treated as +## either highest or lowest priority depending on the configured +## value for mqueue_default_priority +## +zone.external.mqueue_priorities = none + +## Default to highest priority for topics not matching priority table +## +## Value: highest | lowest +zone.external.mqueue_default_priority = highest + +## Whether to enqueue QoS0 messages. +## +## Value: false | true +zone.external.mqueue_store_qos0 = true + +## Whether to turn on flapping detect +## +## Value: on | off +zone.external.enable_flapping_detect = off + +## Message limit for the a external MQTT connection. +## +## Value: Number,Duration +## Example: 100 messages per 10 seconds. +#zone.external.rate_limit.conn_messages_in = "100,10s" + +## Bytes limit for a external MQTT connections. +## +## Value: Number,Duration +## Example: 100KB incoming per 10 seconds. +#zone.external.rate_limit.conn_bytes_in = "100KB,10s" + +## Whether to alarm the congested connections. +## +## Sometimes the mqtt connection (usually an MQTT subscriber) may get "congested" because +## there're too many packets to sent. The socket trys to buffer the packets until the buffer is +## full. If more packets comes after that, the packets will be "pending" in a queue +## and we consider the connection is "congested". +## +## Enable this to send an alarm when there's any bytes pending in the queue. You could set +## the `listener.tcp..sndbuf` to a larger value if the alarm is triggered too often. +## +## The name of the alarm is of format "conn_congestion//". +## Where the is the client-id of the congested MQTT connection. +## And the is the username or "unknown_user" of not provided by the client. +## Default: off +#zone.external.conn_congestion.alarm = off + +## Won't clear the congested alarm in how long time. +## The alarm is cleared only when there're no pending bytes in the queue, and also it has been +## `min_alarm_sustain_duration` time since the last time we considered the connection is "congested". +## +## This is to avoid clearing and sending the alarm again too often. +## Default: 1m +#zone.external.conn_congestion.min_alarm_sustain_duration = 1m + +## Messages quota for the each of external MQTT connection. +## This value consumed by the number of recipient on a message. +## +## Value: Number, Duration +## +## Example: 100 messages per 1s +#zone.external.quota.conn_messages_routing = "100,1s" + +## Messages quota for the all of external MQTT connections. +## This value consumed by the number of recipient on a message. +## +## Value: Number, Duration +## +## Example: 200000 messages per 1s +#zone.external.quota.overall_messages_routing = "200000,1s" + +## All the topics will be prefixed with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username +## +## Value: String +## zone.external.mountpoint = "devicebound/" + +## Whether use username replace client id +## +## Value: boolean +## Default: false +zone.external.use_username_as_clientid = false + +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +zone.external.ignore_loop_deliver = false + +## Whether to parse the MQTT frame in strict mode +## +## Value: true | false +zone.external.strict_mode = false + +## Specify the response information returned to the client +## +## Value: String +## zone.external.response_information = example + +##-------------------------------------------------------------------- +## Internal Zone + +zone.internal.allow_anonymous = true + +## Enable per connection stats. +## +## Value: Flag +zone.internal.enable_stats = on + +## Enable ACL check. +## +## Value: Flag +zone.internal.enable_acl = off + +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +zone.internal.acl_deny_action = ignore + +## See zone.$name.force_gc_policy +## zone.internal.force_gc_policy = "128000|128MB" + +## See zone.$name.wildcard_subscription. +## +## Value: boolean +## zone.internal.wildcard_subscription = true + +## See zone.$name.shared_subscription. +## +## Value: boolean +## zone.internal.shared_subscription = true + +## See zone.$name.max_subscriptions. +## +## Value: Integer +zone.internal.max_subscriptions = 0 + +## See zone.$name.max_inflight +## +## Value: Number +zone.internal.max_inflight = 128 + +## See zone.$name.max_awaiting_rel +## +## Value: Number +zone.internal.max_awaiting_rel = 1000 + +## See zone.$name.max_mqueue_len +## +## Value: Number >= 0 +zone.internal.max_mqueue_len = 10000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.internal.mqueue_store_qos0 = true + +## Whether to turn on flapping detect +## +## Value: on | off +zone.internal.enable_flapping_detect = off + +## See zone.$name.force_shutdown_policy +## +## Default: +## - "10000|64MB" on ARCH_64 system +## - "1000|32MB" on ARCH_32 sytem +#zone.internal.force_shutdown_policy = 10000|64MB + +## All the topics will be prefixed with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username +## +## Value: String +## zone.internal.mountpoint = "cloudbound/" + +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +zone.internal.ignore_loop_deliver = false + +## Whether to parse the MQTT frame in strict mode +## +## Value: true | false +zone.internal.strict_mode = false + +## Specify the response information returned to the client +## +## Value: String +## zone.internal.response_information = example + +## Allow the zone's clients to bypass authentication step +## +## Value: true | false +zone.internal.bypass_auth_plugins = true + +## CONFIG_SECTION_END=zones ==================================================== + +## CONFIG_SECTION_BGN=listeners ================================================ + +##-------------------------------------------------------------------- +## MQTT/TCP - External TCP Listener for MQTT Protocol + +## listener.tcp.$name is the IP address and port that the MQTT/TCP +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 1883, "127.0.0.1:1883", "::1:1883" +listener.tcp.external.endpoint = "0.0.0.0:1883" + +## The acceptor pool for external MQTT/TCP listener. +## +## Value: Number +listener.tcp.external.acceptors = 8 + +## Maximum number of concurrent MQTT/TCP connections. +## +## Value: Number +listener.tcp.external.max_connections = 1024000 + +## Maximum external connections per second. +## +## Value: Number +listener.tcp.external.max_conn_rate = 1000 + +## Specify the {active, N} option for the external MQTT/TCP Socket. +## +## Value: Number +listener.tcp.external.active_n = 100 + +## Zone of the external MQTT/TCP listener belonged to. +## +## See: zone.$name.* +## +## Value: String +listener.tcp.external.zone = external + +## The access control rules for the MQTT/TCP listener. +## +## See: https://github.com/emqtt/esockd#allowdeny +## +## Value: ACL Rule +## +## Example: "allow 192.168.0.0/24" +listener.tcp.external.access.1 = "allow all" + +## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed +## behind HAProxy or Nginx. +## +## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ +## +## Value: on | off +## listener.tcp.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. EMQ X will close the TCP connection +## if no proxy protocol packet recevied within the timeout. +## +## Value: Duration +## listener.tcp.external.proxy_protocol_timeout = 3s + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT username. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.tcp.external.peer_cert_as_username = cn + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT clientid. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.tcp.external.peer_cert_as_clientid = cn + +## The TCP backlog defines the maximum length that the queue of pending +## connections can grow to. +## +## Value: Number >= 0 +listener.tcp.external.backlog = 1024 + +## The TCP send timeout for external MQTT connections. +## +## Value: Duration +listener.tcp.external.send_timeout = 15s + +## Close the TCP connection if send timeout. +## +## Value: on | off +listener.tcp.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for MQTT connections. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +## listener.tcp.external.recbuf = 2KB + +## The TCP send buffer(os kernel) for MQTT connections. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +## listener.tcp.external.sndbuf = 2KB + +## The size of the user-level software buffer used by the driver. +## Not to be confused with options sndbuf and recbuf, which correspond +## to the Kernel socket buffers. It is recommended to have val(buffer) +## >= max(val(sndbuf),val(recbuf)) to avoid performance issues because +## of unnecessary copying. val(buffer) is automatically set to the above +## maximum when values sndbuf or recbuf are set. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +## listener.tcp.external.buffer = 2KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## Value: on | off +## listener.tcp.external.tune_buffer = off + +## The socket is set to a busy state when the amount of data queued internally +## by the ERTS socket implementation reaches this limit. +## +## Value: on | off +## Defaults to 1MB +## listener.tcp.external.high_watermark = 1MB + +## The TCP_NODELAY flag for MQTT connections. Small amounts of data are +## sent immediately if the option is enabled. +## +## Value: true | false +listener.tcp.external.nodelay = true + +## The SO_REUSEADDR flag for TCP listener. +## +## Value: true | false +listener.tcp.external.reuseaddr = true + +##-------------------------------------------------------------------- +## Internal TCP Listener for MQTT Protocol + +## The IP address and port that the internal MQTT/TCP protocol listener +## will bind. +## +## Value: IP:Port, Port +## +## Examples: 11883, "127.0.0.1:11883", "::1:11883" +listener.tcp.internal.endpoint = "127.0.0.1:11883" + +## The acceptor pool for internal MQTT/TCP listener. +## +## Value: Number +listener.tcp.internal.acceptors = 4 + +## Maximum number of concurrent MQTT/TCP connections. +## +## Value: Number +listener.tcp.internal.max_connections = 1024000 + +## Maximum internal connections per second. +## +## Value: Number +listener.tcp.internal.max_conn_rate = 1000 + +## Specify the {active, N} option for the internal MQTT/TCP Socket. +## +## Value: Number +listener.tcp.internal.active_n = 1000 + +## Zone of the internal MQTT/TCP listener belonged to. +## +## Value: String +listener.tcp.internal.zone = internal + +## The TCP backlog of internal MQTT/TCP Listener. +## +## See: listener.tcp.$name.backlog +## +## Value: Number >= 0 +listener.tcp.internal.backlog = 512 + +## The TCP send timeout for internal MQTT connections. +## +## See: listener.tcp.$name.send_timeout +## +## Value: Duration +listener.tcp.internal.send_timeout = 5s + +## Close the MQTT/TCP connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +listener.tcp.internal.send_timeout_close = on + +## The TCP receive buffer(os kernel) for internal MQTT connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +listener.tcp.internal.recbuf = 64KB + +## The TCP send buffer(os kernel) for internal MQTT connections. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +listener.tcp.internal.sndbuf = 64KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.tcp.internal.buffer = 16KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## See: listener.tcp.$name.tune_buffer +## +## Value: on | off +## listener.tcp.internal.tune_buffer = off + +## The TCP_NODELAY flag for internal MQTT connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +listener.tcp.internal.nodelay = false + +## The SO_REUSEADDR flag for MQTT/TCP Listener. +## +## Value: true | false +listener.tcp.internal.reuseaddr = true + +##-------------------------------------------------------------------- +## MQTT/SSL - External SSL Listener for MQTT Protocol + +## listener.ssl.$name is the IP address and port that the MQTT/SSL +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8883, "127.0.0.1:8883", "::1:8883" +listener.ssl.external.endpoint = 8883 + +## The acceptor pool for external MQTT/SSL listener. +## +## Value: Number +listener.ssl.external.acceptors = 16 + +## Maximum number of concurrent MQTT/SSL connections. +## +## Value: Number +listener.ssl.external.max_connections = 102400 + +## Maximum MQTT/SSL connections per second. +## +## Value: Number +listener.ssl.external.max_conn_rate = 500 + +## Specify the {active, N} option for the internal MQTT/SSL Socket. +## +## Value: Number +listener.ssl.external.active_n = 100 + +## Zone of the external MQTT/SSL listener belonged to. +## +## Value: String +listener.ssl.external.zone = external + +## The access control rules for the MQTT/SSL listener. +## +## See: listener.tcp.$name.access +## +## Value: ACL Rule +listener.ssl.external.access.1 = "allow all" + +## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind +## HAProxy or Nginx. +## +## See: listener.tcp.$name.proxy_protocol +## +## Value: on | off +## listener.ssl.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.tcp.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.ssl.external.proxy_protocol_timeout = 3s + +## TLS versions only to protect from POODLE attack. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: String, seperated by ',' +## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier +## listener.ssl.external.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" + +## TLS Handshake timeout. +## +## Value: Duration +listener.ssl.external.handshake_timeout = 15s + +## Maximum number of non-self-issued intermediate certificates that +## can follow the peer certificate in a valid certification path. +## +## Value: Number +## listener.ssl.external.depth = 10 + +## String containing the user's password. Only used if the private keyfile +## is password-protected. +## +## Value: String +## listener.ssl.external.key_password = yourpass + +## Path to the file containing the user's private PEM-encoded key. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: File +listener.ssl.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + +## Path to a file containing the user certificate. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: File +listener.ssl.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + +## Path to the file containing PEM-encoded CA certificates. The CA certificates +## are used during server authentication and when building the client certificate chain. +## +## Value: File +## listener.ssl.external.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + +## The Ephemeral Diffie-Helman key exchange is a very effective way of +## ensuring Forward Secrecy by exchanging a set of keys that never hit +## the wire. Since the DH key is effectively signed by the private key, +## it needs to be at least as strong as the private key. In addition, +## the default DH groups that most of the OpenSSL installations have +## are only a handful (since they are distributed with the OpenSSL +## package that has been built for the operating system it’s running on) +## and hence predictable (not to mention, 1024 bits only). +## In order to escape this situation, first we need to generate a fresh, +## strong DH group, store it in a file and then use the option above, +## to force our SSL application to use the new DH group. Fortunately, +## OpenSSL provides us with a tool to do that. Simply run: +## openssl dhparam -out dh-params.pem 2048 +## +## Value: File +## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" + +## A server only does x509-path validation in mode verify_peer, +## as it then sends a certificate request to the client (this +## message is not sent if the verify option is verify_none). +## You can then also want to specify option fail_if_no_peer_cert. +## More information at: http://erlang.org/doc/man/ssl.html +## +## Value: verify_peer | verify_none +## listener.ssl.external.verify = verify_peer + +## Used together with {verify, verify_peer} by an SSL server. If set to true, +## the server fails if the client does not have a certificate to send, that is, +## sends an empty certificate. +## +## Value: true | false +## listener.ssl.external.fail_if_no_peer_cert = true + +## This is the single most important configuration option of an Erlang SSL +## application. Ciphers (and their ordering) define the way the client and +## server encrypt information over the wire, from the initial Diffie-Helman +## key exchange, the session key encryption ## algorithm and the message +## digest algorithm. Selecting a good cipher suite is critical for the +## application’s data security, confidentiality and performance. +## +## The cipher list above offers: +## +## A good balance between compatibility with older browsers. +## It can get stricter for Machine-To-Machine scenarios. +## Perfect Forward Secrecy. +## No old/insecure encryption and HMAC algorithms +## +## Most of it was copied from Mozilla’s Server Side TLS article +## +## Value: Ciphers +listener.ssl.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" + +## Ciphers for TLS PSK. +## Note that 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot +## be configured at the same time. +## See 'https://tools.ietf.org/html/rfc4279#section-2'. +#listener.ssl.external.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" + +## SSL parameter renegotiation is a feature that allows a client and a server +## to renegotiate the parameters of the SSL connection on the fly. +## RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, +## you drop support for the insecure renegotiation, prone to MitM attacks. +## +## Value: on | off +## listener.ssl.external.secure_renegotiate = off + +## A performance optimization setting, it allows clients to reuse +## pre-existing sessions, instead of initializing new ones. +## Read more about it here. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: on | off +## listener.ssl.external.reuse_sessions = on + +## An important security setting, it forces the cipher to be set based +## on the server-specified order instead of the client-specified order, +## hence enforcing the (usually more properly configured) security +## ordering of the server administrator. +## +## Value: on | off +## listener.ssl.external.honor_cipher_order = on + +## Use the CN, DN or CRT field from the client certificate as a username. +## Notice that 'verify' should be set as 'verify_peer'. +## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. +## +## Value: cn | dn | crt | pem | md5 +## listener.ssl.external.peer_cert_as_username = cn + +## Use the CN, DN or CRT field from the client certificate as a username. +## Notice that 'verify' should be set as 'verify_peer'. +## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. +## +## Value: cn | dn | crt | pem | md5 +## listener.ssl.external.peer_cert_as_clientid = cn + +## TCP backlog for the SSL connection. +## +## See listener.tcp.$name.backlog +## +## Value: Number >= 0 +## listener.ssl.external.backlog = 1024 + +## The TCP send timeout for the SSL connection. +## +## See listener.tcp.$name.send_timeout +## +## Value: Duration +## listener.ssl.external.send_timeout = 15s + +## Close the SSL connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +## listener.ssl.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for the SSL connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +## listener.ssl.external.recbuf = 4KB + +## The TCP send buffer(os kernel) for internal MQTT connections. +## +## See: listener.tcp.$name.sndbuf +## +## Value: Bytes +## listener.ssl.external.sndbuf = 4KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.ssl.external.buffer = 4KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## See: listener.tcp.$name.tune_buffer +## +## Value: on | off +## listener.ssl.external.tune_buffer = off + +## The TCP_NODELAY flag for SSL connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +## listener.ssl.external.nodelay = true + +## The SO_REUSEADDR flag for MQTT/SSL Listener. +## +## Value: true | false +listener.ssl.external.reuseaddr = true + +##-------------------------------------------------------------------- +## External WebSocket listener for MQTT protocol + +## listener.ws.$name is the IP address and port that the MQTT/WebSocket +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8083, "127.0.0.1:8083", "::1:8083" +listener.ws.external.endpoint = 8083 + +## The path of WebSocket MQTT endpoint +## +## Value: URL Path +listener.ws.external.mqtt_path = "/mqtt" + +## The acceptor pool for external MQTT/WebSocket listener. +## +## Value: Number +listener.ws.external.acceptors = 4 + +## Maximum number of concurrent MQTT/WebSocket connections. +## +## Value: Number +listener.ws.external.max_connections = 102400 + +## Maximum MQTT/WebSocket connections per second. +## +## Value: Number +listener.ws.external.max_conn_rate = 1000 + +## Simulate the {active, N} option for the MQTT/WebSocket connections. +## +## Value: Number +listener.ws.external.active_n = 100 + +## Zone of the external MQTT/WebSocket listener belonged to. +## +## Value: String +listener.ws.external.zone = external + +## The access control for the MQTT/WebSocket listener. +## +## See: listener.ws.$name.access +## +## Value: ACL Rule +listener.ws.external.access.1 = "allow all" + +## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. +## Set to false for WeChat MiniApp. +## +## Value: true | false +## listener.ws.external.fail_if_no_subprotocol = true + +## Supported subprotocols +## +## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 +## listener.ws.external.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + +## Specify which HTTP header for real source IP if the EMQ X cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-For +## listener.ws.external.proxy_address_header = X-Forwarded-For + +## Specify which HTTP header for real source port if the EMQ X cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-Port +## listener.ws.external.proxy_port_header = X-Forwarded-Port + +## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind +## HAProxy or Nginx. +## +## See: listener.ws.$name.proxy_protocol +## +## Value: on | off +## listener.ws.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.ws.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.ws.external.proxy_protocol_timeout = 3s + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT username. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.ws.external.peer_cert_as_username = cn + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT clientid. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.ws.external.peer_cert_as_clientid = cn + +## The TCP backlog of external MQTT/WebSocket Listener. +## +## See: listener.ws.$name.backlog +## +## Value: Number >= 0 +listener.ws.external.backlog = 1024 + +## The TCP send timeout for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.send_timeout +## +## Value: Duration +listener.ws.external.send_timeout = 15s + +## Close the MQTT/WebSocket connection if send timeout. +## +## See: listener.ws.$name.send_timeout_close +## +## Value: on | off +listener.ws.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.recbuf +## +## Value: Bytes +## listener.ws.external.recbuf = 2KB + +## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.sndbuf +## +## Value: Bytes +## listener.ws.external.sndbuf = 2KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.ws.$name.buffer +## +## Value: Bytes +## listener.ws.external.buffer = 2KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## See: listener.ws.$name.tune_buffer +## +## Value: on | off +## listener.ws.external.tune_buffer = off + +## The TCP_NODELAY flag for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.nodelay +## +## Value: true | false +listener.ws.external.nodelay = true + +## The compress flag for external MQTT/WebSocket connections. +## +## If this Value is set true,the websocket message would be compressed +## +## Value: true | false +## listener.ws.external.compress = true + +## The level of deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.level +## +## Value: none | default | best_compression | best_speed +## listener.ws.external.deflate_opts.level = default + +## The mem_level of deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.mem_level +## +## Valid range is 1-9 +## listener.ws.external.deflate_opts.mem_level = 8 + +## The strategy of deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.strategy +## +## Value: default | filtered | huffman_only | rle +## listener.ws.external.deflate_opts.strategy = default + +## The deflate option for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.server_context_takeover +## +## Value: takeover | no_takeover +## listener.ws.external.deflate_opts.server_context_takeover = takeover + +## The deflate option for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.client_context_takeover +## +## Value: takeover | no_takeover +## listener.ws.external.deflate_opts.client_context_takeover = takeover + +## The deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.server_max_window_bits +## +## Valid range is 8-15 +## listener.ws.external.deflate_opts.server_max_window_bits = 15 + +## The deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.client_max_window_bits +## +## Valid range is 8-15 +## listener.ws.external.deflate_opts.client_max_window_bits = 15 + +## The idle timeout for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.idle_timeout +## +## Value: Duration +## listener.ws.external.idle_timeout = 60s + +## The max frame size for external MQTT/WebSocket connections. +## +## +## Value: Number +## listener.ws.external.max_frame_size = 0 + +## Whether a WebSocket message is allowed to contain multiple MQTT packets +## +## Value: single | multiple +listener.ws.external.mqtt_piggyback = multiple + +## By default, EMQX web socket connection does not restrict connections to specific origins. +## It also, by default, does not enforce the presence of origin in request headers for WebSocket connections. +## Because of this, a malicious user could potentially hijack an existing web-socket connection to EMQX. + +## To prevent this, users can set allowed origin headers in their ws connection to EMQX. +## WS configs are set in listener.ws.external.* +## WSS configs are set in listener.wss.external.* + +## Example for WS connection +## To enables origin check in header for websocket connnection, +## set `listener.ws.external.check_origin_enable = true`. By default it is false, +## When it is set to true and no origin is present in the header of a ws connection request, the request fails. + +## To allow origins to be absent in header in the websocket connection when check_origin_enable is true, +## set `listener.ws.external.allow_origin_absence = true` + +## Enabling origin check implies there are specific valid origins allowed for ws connection. +## To set the list of allowed origins in header for websocket connection +## listener.ws.external.check_origins = http://localhost:18083(localhost dashboard url), http://yourapp.com` +## check_origins config allows a comma separated list of origins so you can specify as many origins are you want. +## With these configs, you can allow only connections from only authorized origins to your broker + +## Enable origin check in header for websocket connection +## +## Value: true | false (default false) +listener.ws.external.check_origin_enable = false + +## Allow origin to be absent in header in websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.ws.external.allow_origin_absence = true + +## Comma separated list of allowed origin in header for websocket connection +## +## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 +listener.ws.external.check_origins = "http://localhost:18083, http://127.0.0.1:18083" + +##-------------------------------------------------------------------- +## External WebSocket/SSL listener for MQTT Protocol + +## listener.wss.$name is the IP address and port that the MQTT/WebSocket/SSL +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8084, "127.0.0.1:8084", "::1:8084" +listener.wss.external.endpoint = 8084 + +## The path of WebSocket MQTT endpoint +## +## Value: URL Path +listener.wss.external.mqtt_path = "/mqtt" + +## The acceptor pool for external MQTT/WebSocket/SSL listener. +## +## Value: Number +listener.wss.external.acceptors = 4 + +## Maximum number of concurrent MQTT/Webwocket/SSL connections. +## +## Value: Number +listener.wss.external.max_connections = 16 + +## Maximum MQTT/WebSocket/SSL connections per second. +## +## See: listener.tcp.$name.max_conn_rate +## +## Value: Number +listener.wss.external.max_conn_rate = 1000 + +## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. +## +## Value: Number +listener.wss.external.active_n = 100 + +## Zone of the external MQTT/WebSocket/SSL listener belonged to. +## +## Value: String +listener.wss.external.zone = external + +## The access control rules for the MQTT/WebSocket/SSL listener. +## +## See: listener.tcp.$name.access. +## +## Value: ACL Rule +listener.wss.external.access.1 = "allow all" + +## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. +## Set to false for WeChat MiniApp. +## +## Value: true | false +## listener.wss.external.fail_if_no_subprotocol = true + +## Supported subprotocols +## +## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 +## listener.wss.external.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + +## Specify which HTTP header for real source IP if the EMQ X cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-For +## listener.wss.external.proxy_address_header = X-Forwarded-For + +## Specify which HTTP header for real source port if the EMQ X cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-Port +## listener.wss.external.proxy_port_header = X-Forwarded-Port + +## Enable the Proxy Protocol V1/2 support. +## +## See: listener.tcp.$name.proxy_protocol +## +## Value: on | off +## listener.wss.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.tcp.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.wss.external.proxy_protocol_timeout = 3s + +## TLS versions only to protect from POODLE attack. +## +## See: listener.ssl.$name.tls_versions +## +## Value: String, seperated by ',' +## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier +## listener.wss.external.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" + +## Path to the file containing the user's private PEM-encoded key. +## +## See: listener.ssl.$name.keyfile +## +## Value: File +listener.wss.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + +## Path to a file containing the user certificate. +## +## See: listener.ssl.$name.certfile +## +## Value: File +listener.wss.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + +## Path to the file containing PEM-encoded CA certificates. +## +## See: listener.ssl.$name.cacert +## +## Value: File +## listener.wss.external.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + +## Maximum number of non-self-issued intermediate certificates that +## can follow the peer certificate in a valid certification path. +## +## See: listener.ssl.external.depth +## +## Value: Number +## listener.wss.external.depth = 10 + +## String containing the user's password. Only used if the private keyfile +## is password-protected. +## +## See: listener.ssl.$name.key_password +## +## Value: String +## listener.wss.external.key_password = yourpass + +## See: listener.ssl.$name.dhfile +## +## Value: File +## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" + +## See: listener.ssl.$name.verify +## +## Value: verify_peer | verify_none +## listener.wss.external.verify = verify_peer + +## See: listener.ssl.$name.fail_if_no_peer_cert +## +## Value: false | true +## listener.wss.external.fail_if_no_peer_cert = true + +## See: listener.ssl.$name.ciphers +## +## Value: Ciphers +listener.wss.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" + +## Ciphers for TLS PSK. +## Note that 'listener.wss.external.ciphers' and 'listener.wss.external.psk_ciphers' cannot +## be configured at the same time. +## See 'https://tools.ietf.org/html/rfc4279#section-2'. +## listener.wss.external.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" + +## See: listener.ssl.$name.secure_renegotiate +## +## Value: on | off +## listener.wss.external.secure_renegotiate = off + +## See: listener.ssl.$name.reuse_sessions +## +## Value: on | off +## listener.wss.external.reuse_sessions = on + +## See: listener.ssl.$name.honor_cipher_order +## +## Value: on | off +## listener.wss.external.honor_cipher_order = on + +## See: listener.ssl.$name.peer_cert_as_username +## +## Value: cn | dn | crt | pem | md5 +## listener.wss.external.peer_cert_as_username = cn + +## See: listener.ssl.$name.peer_cert_as_clientid +## +## Value: cn | dn | crt | pem | md5 +## listener.wss.external.peer_cert_as_clientid = cn + +## TCP backlog for the WebSocket/SSL connection. +## +## See: listener.tcp.$name.backlog +## +## Value: Number >= 0 +listener.wss.external.backlog = 1024 + +## The TCP send timeout for the WebSocket/SSL connection. +## +## See: listener.tcp.$name.send_timeout +## +## Value: Duration +listener.wss.external.send_timeout = 15s + +## Close the WebSocket/SSL connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +listener.wss.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +## listener.wss.external.recbuf = 4KB + +## The TCP send buffer(os kernel) for the WebSocket/SSL connections. +## +## See: listener.tcp.$name.sndbuf +## +## Value: Bytes +## listener.wss.external.sndbuf = 4KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.wss.external.buffer = 4KB + +## The TCP_NODELAY flag for WebSocket/SSL connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +## listener.wss.external.nodelay = true + +## The compress flag for external WebSocket/SSL connections. +## +## If this Value is set true,the websocket message would be compressed +## +## Value: true | false +## listener.wss.external.compress = true + +## The level of deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.level +## +## Value: none | default | best_compression | best_speed +## listener.wss.external.deflate_opts.level = default + +## The mem_level of deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.mem_level +## +## Valid range is 1-9 +## listener.wss.external.deflate_opts.mem_level = 8 + +## The strategy of deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.strategy +## +## Value: default | filtered | huffman_only | rle +## listener.wss.external.deflate_opts.strategy = default + +## The deflate option for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.server_context_takeover +## +## Value: takeover | no_takeover +## listener.wss.external.deflate_opts.server_context_takeover = takeover + +## The deflate option for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.client_context_takeover +## +## Value: takeover | no_takeover +## listener.wss.external.deflate_opts.client_context_takeover = takeover + +## The deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.server_max_window_bits +## +## Valid range is 8-15 +## listener.wss.external.deflate_opts.server_max_window_bits = 15 + +## The deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.client_max_window_bits +## +## Valid range is 8-15 +## listener.wss.external.deflate_opts.client_max_window_bits = 15 + +## The idle timeout for external WebSocket/SSL connections. +## +## See: listener.wss.$name.idle_timeout +## +## Value: Duration +## listener.wss.external.idle_timeout = 60s + +## The max frame size for external WebSocket/SSL connections. +## +## Value: Number +## listener.wss.external.max_frame_size = 0 + +## Whether a WebSocket message is allowed to contain multiple MQTT packets +## +## Value: single | multiple +listener.wss.external.mqtt_piggyback = multiple +## Enable origin check in header for secure websocket connection +## +## Value: true | false (default false) +listener.wss.external.check_origin_enable = false +## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.wss.external.allow_origin_absence = true +## Comma separated list of allowed origin in header for secure websocket connection +## +## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 +listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" + +## CONFIG_SECTION_END=listeners ================================================ + +## CONFIG_SECTION_BGN=modules ================================================== + +## The file to store loaded module names. +## +## Value: File +module.loaded_file = "{{ platform_data_dir }}/loaded_modules" + +##-------------------------------------------------------------------- +## Presence Module + +## Sets the QoS for presence MQTT message. +## +## Value: 0 | 1 | 2 +module.presence.qos = 1 + +##-------------------------------------------------------------------- +## Subscription Module + +## Subscribe the Topics automatically when client connected. +## +## Value: String +## module.subscription.1.topic = "connected/%c/%u" + +## Qos of the proxy subscription. +## +## Value: 0 | 1 | 2 +## Default: 0 +## module.subscription.1.qos = 0 + +## No Local of the proxy subscription options. +## This configuration only takes effect in the MQTT V5 protocol. +## +## Value: 0 | 1 +## Default: 0 +## module.subscription.1.nl = 0 + +## Retain As Published of the proxy subscription options. +## This configuration only takes effect in the MQTT V5 protocol. +## +## Value: 0 | 1 +## Default: 0 +## module.subscription.1.rap = 0 + +## Retain Handling of the proxy subscription options. +## This configuration only takes effect in the MQTT V5 protocol. +## +## Value: 0 | 1 | 2 +## Default: 0 +## module.subscription.1.rh = 0 + +##-------------------------------------------------------------------- +## Rewrite Module + +## {rewrite, Topic, Re, Dest} +## module.rewrite.pub_rule.1 = "x/# ^x/y/(.+)$ z/y/$1" +## module.rewrite.sub_rule.1 = "y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2" + +## CONFIG_SECTION_END=modules ================================================== + +##------------------------------------------------------------------- +## Plugins +##------------------------------------------------------------------- + +## The etc dir for plugins' config. +## +## Value: Folder +plugins.etc_dir = "{{ platform_etc_dir }}/plugins/" + +## The file to store loaded plugin names. +## +## Value: File +plugins.loaded_file = "{{ platform_data_dir }}/loaded_plugins" + +## The directory of extension plugins. +## +## Value: File +plugins.expand_plugins_dir = "{{ platform_plugins_dir }}/" + +##-------------------------------------------------------------------- +## Broker +##-------------------------------------------------------------------- + +## System interval of publishing $SYS messages. +## +## Value: Duration +## Default: 1m, 1 minute +broker.sys_interval = 1m + +## System heartbeat interval of publishing following heart beat message: +## - "$SYS/brokers//uptime" +## - "$SYS/brokers//datetime" +## +## Value: Duration +## Default: 30s +broker.sys_heartbeat = 30s + +## Session locking strategy in a cluster. +## +## Value: Enum +## - local +## - leader +## - quorum +## - all +broker.session_locking_strategy = quorum + +## Dispatch strategy for shared subscription +## +## Value: Enum +## - random +## - round_robin +## - sticky +## - hash # same as hash_clientid +## - hash_clientid +## - hash_topic +broker.shared_subscription_strategy = random + +## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages +## This should allow messages to be dispatched to a different subscriber in +## the group in case the picked (based on shared_subscription_strategy) one # is offline +## +## Value: Enum +## - true +## - false +broker.shared_dispatch_ack_enabled = false + +## Enable batch clean for deleted routes. +## +## Value: Flag +broker.route_batch_clean = off + +## Performance toggle for subscribe/unsubscribe wildcard topic. +## Change this toggle only when there are many wildcard topics. +## Value: Enum +## - key: mnesia translational updates with per-key locks. recommended for single node setup. +## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. +## - global: global lock protected updates. recommended for larger cluster. +## NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster +## to be stopped before the change. +# broker.perf.route_lock_type = key + +## Enable trie path compaction. +## Enabling it significantly improves wildcard topic subscribe rate, +## if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', +## where ID is unique per subscriber. +## +## Topic match performance (when publishing) may degrade if messages +## are mostly published to topics with large number of levels. +## +## NOTE: This is a cluster-wide configuration. +## It rquires all nodes to be stopped before changing it. +## +## Value: Enum +## - true: enable trie path compaction +## - false: disable trie path compaction +# broker.perf.trie_compaction = true + +## CONFIG_SECTION_BGN=sys_mon ================================================== + +## Enable Long GC monitoring. Disable if the value is 0. +## Notice: don't enable the monitor in production for: +## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 +## +## Value: Duration +## - h: hour +## - m: minute +## - s: second +## - ms: milliseconds +## +## Examples: +## - 2h: 2 hours +## - 30m: 30 minutes +## - 0.1s: 0.1 seconds +## - 100ms : 100 milliseconds +## +## Default: 0ms +sysmon.long_gc = 0 + +## Enable Long Schedule(ms) monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: Duration +## - h: hour +## - m: minute +## - s: second +## - ms: milliseconds +## +## Examples: +## - 2h: 2 hours +## - 30m: 30 minutes +## - 100ms: 100 milliseconds +## +## Default: 0ms +sysmon.long_schedule = 240ms + +## Enable Large Heap monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: bytes +## +## Default: 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. +sysmon.large_heap = 8MB + +## Enable Busy Port monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: true | false +sysmon.busy_port = false + +## Enable Busy Dist Port monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: true | false +sysmon.busy_dist_port = true + +## The time interval for the periodic cpu check +## +## Value: Duration +## -h: hour, e.g. '2h' for 2 hours +## -m: minute, e.g. '5m' for 5 minutes +## -s: second, e.g. '30s' for 30 seconds +## +## Default: 60s +os_mon.cpu_check_interval = 60s + +## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. +## +## Default: 80% +os_mon.cpu_high_watermark = 80% + +## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. +## +## Default: 60% +os_mon.cpu_low_watermark = 60% + +## The time interval for the periodic memory check +## +## Value: Duration +## -h: hour, e.g. '2h' for 2 hours +## -m: minute, e.g. '5m' for 5 minutes +## -s: second, e.g. '30s' for 30 seconds +## +## Default: 60s +os_mon.mem_check_interval = 60s + +## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. +## +## Default: 70% +os_mon.sysmem_high_watermark = 70% + +## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. +## +## Default: 5% +os_mon.procmem_high_watermark = 5% + +## The time interval for the periodic process limit check +## +## Value: Duration +## +## Default: 30s +vm_mon.check_interval = 30s + +## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. +## +## Default: 80% +vm_mon.process_high_watermark = 80% + +## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. +## +## Default: 60% +vm_mon.process_low_watermark = 60% + +## Specifies the actions to take when an alarm is activated +## +## Value: String +## - log +## - publish +## +## Default: "log,publish" +alarm.actions = "log,publish" + +## The maximum number of deactivated alarms +## +## Value: Integer +## +## Default: 1000 +alarm.size_limit = 1000 + +## Validity Period of deactivated alarms +## +## Value: Duration +## - h: hour +## - m: minute +## - s: second +## - ms: milliseconds +## +## Default: 24h +alarm.validity_period = 24h + +## CONFIG_SECTION_END=sys_mon ================================================== diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 4570528c2..a2644f353 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -14,6 +14,7 @@ -type duration_s() :: integer(). -type duration_ms() :: integer(). -type bytesize() :: integer(). +-type wordsize() :: bytesize(). -type percent() :: float(). -type file() :: string(). -type comma_separated_list() :: list(). @@ -26,6 +27,7 @@ -typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}). -typerefl_from_string({duration_ms/0, emqx_schema, to_duration_ms}). -typerefl_from_string({bytesize/0, emqx_schema, to_bytesize}). +-typerefl_from_string({wordsize/0, emqx_schema, to_wordsize}). -typerefl_from_string({percent/0, emqx_schema, to_percent}). -typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}). -typerefl_from_string({bar_separated_list/0, emqx_schema, to_bar_separated_list}). @@ -33,7 +35,8 @@ -typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). % workaround: prevent being recognized as unused functions --export([to_duration/1, to_duration_s/1, to_duration_ms/1, to_bytesize/1, +-export([to_duration/1, to_duration_s/1, to_duration_ms/1, + to_bytesize/1, to_wordsize/1, to_flag/1, to_percent/1, to_comma_separated_list/1, to_bar_separated_list/1, to_ip_port/1, to_comma_separated_atoms/1]). @@ -41,21 +44,21 @@ -behaviour(hocon_schema). -reflect_type([ log_level/0, flag/0, duration/0, duration_s/0, duration_ms/0, - bytesize/0, percent/0, file/0, + bytesize/0, wordsize/0, percent/0, file/0, comma_separated_list/0, bar_separated_list/0, ip_port/0, comma_separated_atoms/0]). -export([structs/0, fields/1, translations/0, translation/1]). -export([t/1, t/3, t/4, ref/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). --export([ssl/2, tr_ssl/2, tr_password_hash/2]). +-export([ssl/1, tr_ssl/2, tr_password_hash/2]). %% will be used by emqx_ct_helper to find the dependent apps -export([includes/0]). structs() -> ["cluster", "node", "rpc", "log", "lager", - "acl", "mqtt", "zone", "listener", "module", "broker", - "plugins", "sysmon", "os_mon", "vm_mon", "alarm", "telemetry"] + "acl", "mqtt", "zone", "listeners", "module", "broker", + "plugins", "sysmon", "alarm", "telemetry"] ++ includes(). -ifdef(TEST). @@ -69,7 +72,8 @@ includes() -> fields("cluster") -> [ {"name", t(atom(), "ekka.cluster_name", emqxcl)} - , {"discovery", t(atom(), undefined, manual)} + , {"discovery_strategy", t(union([manual, static, mcast, dns, etcd, k8s]), + undefined, manual)} , {"autoclean", t(duration(), "ekka.cluster_autoclean", undefined)} , {"autoheal", t(flag(), "ekka.cluster_autoheal", false)} , {"static", ref("static")} @@ -83,7 +87,7 @@ fields("cluster") -> ]; fields("static") -> - [ {"seeds", t(comma_separated_list())}]; + [ {"seeds", t(list(string()))}]; fields("mcast") -> [ {"addr", t(string(), undefined, "239.192.0.1")} @@ -97,7 +101,8 @@ fields("mcast") -> ]; fields("dns") -> - [ {"app", t(string())}]; + [ {"name", t(string())} + , {"app", t(string())}]; fields("etcd") -> [ {"server", t(comma_separated_list())} @@ -107,7 +112,7 @@ fields("etcd") -> ]; fields("etcd_ssl") -> - ssl(undefined, #{}); + ssl(#{}); fields("k8s") -> [ {"apiserver", t(string())} @@ -125,7 +130,6 @@ fields("rlog") -> fields("node") -> [ {"name", t(string(), "vm_args.-name", "emqx@127.0.0.1", "EMQX_NODE_NAME")} - , {"ssl_dist_optfile", t(string(), "vm_args.-ssl_dist_optfile", undefined)} , {"cookie", hoconsc:t(string(), #{mapping => "vm_args.-setcookie", default => "emqxsecretcookie", sensitive => true, @@ -133,16 +137,8 @@ fields("node") -> })} , {"data_dir", t(string(), "emqx.data_dir", undefined)} , {"etc_dir", t(string(), "emqx.etc_dir", undefined)} - , {"heartbeat", t(flag(), undefined, false)} - , {"async_threads", t(range(1, 1024), "vm_args.+A", undefined)} - , {"process_limit", t(integer(), "vm_args.+P", undefined)} - , {"max_ports", t(range(1024, 134217727), "vm_args.+Q", undefined, "EMQX_MAX_PORTS")} - , {"dist_buffer_size", fun node__dist_buffer_size/1} , {"global_gc_interval", t(duration_s(), "emqx.global_gc_interval", undefined)} - , {"fullsweep_after", t(non_neg_integer(), - "vm_args.-env ERL_FULLSWEEP_AFTER", 1000)} - , {"max_ets_tables", t(integer(), "vm_args.+e", 256000)} - , {"crash_dump", t(file(), "vm_args.-env ERL_CRASH_DUMP", undefined)} + , {"crash_dump_dir", t(file(), "vm_args.-env ERL_CRASH_DUMP", undefined)} , {"dist_net_ticktime", t(integer(), "vm_args.-kernel net_ticktime", undefined)} , {"dist_listen_min", t(integer(), "kernel.inet_dist_listen_min", undefined)} , {"dist_listen_max", t(integer(), "kernel.inet_dist_listen_max", undefined)} @@ -215,50 +211,40 @@ fields("lager") -> , {"crash_log", t(flag(), "lager.crash_log", false)} ]; +fields("stats") -> + [ {"enable", t(boolean(), undefined, true)} + ]; + +fields("auth") -> + [ {"enable", t(boolean(), undefined, false)} + ]; + fields("acl") -> - [ {"allow_anonymous", t(boolean(), "emqx.allow_anonymous", false)} - , {"acl_nomatch", t(union(allow, deny), "emqx.acl_nomatch", deny)} - , {"acl_file", t(string(), "emqx.acl_file", undefined)} - , {"enable_acl_cache", t(flag(), "emqx.enable_acl_cache", true)} - , {"acl_cache_ttl", t(duration(), "emqx.acl_cache_ttl", "1m")} - , {"acl_cache_max_size", t(range(1, inf), "emqx.acl_cache_max_size", 32)} - , {"acl_deny_action", t(union(ignore, disconnect), "emqx.acl_deny_action", ignore)} - , {"flapping_detect_policy", t(comma_separated_list(), undefined, "30,1m,5m")} + [ {"enable", t(boolean(), undefined, false)} + , {"cache", ref("acl_cache")} + , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} + ]; + +fields("acl_cache") -> + [ {"enable", t(boolean(), undefined, true)} + , {"max_size", t(range(1, 1048576), undefined, 32)} + , {"ttl", t(duration(), undefined, "1m")} ]; fields("mqtt") -> - [ {"max_packet_size", t(bytesize(), "emqx.max_packet_size", "1MB", "EMQX_MAX_PACKET_SIZE")} - , {"max_clientid_len", t(integer(), "emqx.max_clientid_len", 65535)} - , {"max_topic_levels", t(integer(), "emqx.max_topic_levels", 0)} - , {"max_qos_allowed", t(range(0, 2), "emqx.max_qos_allowed", 2)} - , {"max_topic_alias", t(integer(), "emqx.max_topic_alias", 65535)} - , {"retain_available", t(boolean(), "emqx.retain_available", true)} - , {"wildcard_subscription", t(boolean(), "emqx.wildcard_subscription", true)} - , {"shared_subscription", t(boolean(), "emqx.shared_subscription", true)} - , {"ignore_loop_deliver", t(boolean(), "emqx.ignore_loop_deliver", true)} - , {"strict_mode", t(boolean(), "emqx.strict_mode", false)} - , {"response_information", t(string(), "emqx.response_information", undefined)} - ]; - -fields("zone") -> - [ {"$name", ref("zone_settings")}]; - -fields("zone_settings") -> - [ {"idle_timeout", t(duration(), undefined, "15s")} - , {"allow_anonymous", t(boolean())} - , {"acl_nomatch", t(union(allow, deny))} - , {"enable_acl", t(flag(), undefined, false)} - , {"acl_deny_action", t(union(ignore, disconnect), undefined, ignore)} - , {"enable_ban", t(flag(), undefined, false)} - , {"enable_stats", t(flag(), undefined, false)} - , {"max_packet_size", t(bytesize())} - , {"max_clientid_len", t(integer())} - , {"max_topic_levels", t(integer())} - , {"max_qos_allowed", t(range(0, 2))} - , {"max_topic_alias", t(integer())} - , {"retain_available", t(boolean())} - , {"wildcard_subscription", t(boolean())} - , {"shared_subscription", t(boolean())} + [ {"mountpoint", t(binary(), undefined, <<"">>)} + , {"idle_timeout", t(duration(), undefined, "15s")} + , {"max_packet_size", t(bytesize(), undefined, "1MB")} + , {"max_clientid_len", t(integer(), undefined, 65535)} + , {"max_topic_levels", t(integer(), undefined, 0)} + , {"max_qos_allowed", t(range(0, 2), undefined, 2)} + , {"max_topic_alias", t(integer(), undefined, 65535)} + , {"retain_available", t(boolean(), undefined, true)} + , {"wildcard_subscription", t(boolean(), undefined, true)} + , {"shared_subscription", t(boolean(), undefined, true)} + , {"ignore_loop_deliver", t(boolean())} + , {"strict_mode", t(boolean(), undefined, false)} + , {"response_information", t(string(), undefined, undefined)} , {"server_keepalive", t(integer())} , {"keepalive_backoff", t(float(), undefined, 0.75)} , {"max_subscriptions", t(integer(), undefined, 0)} @@ -267,121 +253,150 @@ fields("zone_settings") -> , {"retry_interval", t(duration_s(), undefined, "30s")} , {"max_awaiting_rel", t(duration(), undefined, 0)} , {"await_rel_timeout", t(duration_s(), undefined, "300s")} - , {"ignore_loop_deliver", t(boolean())} , {"session_expiry_interval", t(duration_s(), undefined, "2h")} , {"max_mqueue_len", t(integer(), undefined, 1000)} , {"mqueue_priorities", t(comma_separated_list(), undefined, "none")} , {"mqueue_default_priority", t(union(highest, lowest), undefined, lowest)} , {"mqueue_store_qos0", t(boolean(), undefined, true)} - , {"enable_flapping_detect", t(flag(), undefined, false)} - , {"rate_limit", ref("rate_limit")} - , {"conn_congestion", ref("conn_congestion")} - , {"quota", ref("quota")} - , {"force_gc_policy", t(bar_separated_list())} - , {"force_shutdown_policy", t(bar_separated_list(), undefined, "default")} - , {"mountpoint", t(string())} , {"use_username_as_clientid", t(boolean(), undefined, false)} - , {"strict_mode", t(boolean(), undefined, false)} - , {"response_information", t(string())} - , {"bypass_auth_plugins", t(boolean(), undefined, false)} + , {"peer_cert_as_username", maybe_disabled(union([cn, dn, crt, pem, md5]))} + , {"peer_cert_as_clientid", maybe_disabled(union([cn, dn, crt, pem, md5]))} + ]; + +fields("zone") -> + [ {"$name", ref("zone_settings")}]; + +fields("zone_settings") -> + [ {"mqtt", ref("mqtt")} + , {"acl", ref("acl")} + , {"auth", ref("auth")} + , {"stats", ref("stats")} + , {"flapping_detect", ref("flapping_detect")} + , {"force_shutdown", ref("force_shutdown")} + , {"conn_congestion", ref("conn_congestion")} + , {"force_gc", ref("force_gc")} + , {"overall_max_connections", t(integer(), undefined, 2048000)} + , {"listeners", t("listeners")} ]; fields("rate_limit") -> - [ {"conn_messages_in", t(comma_separated_list())} + [ {"max_conn_rate", maybe_infinity(integer(), 1000)} + , {"conn_messages_in", t(comma_separated_list())} , {"conn_bytes_in", t(comma_separated_list())} + , {"quota", ref("rate_limit_quota")} ]; -fields("conn_congestion") -> - [ {"alarm", t(flag(), undefined, false)} - , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} - ]; - -fields("quota") -> +fields("rate_limit_quota") -> [ {"conn_messages_routing", t(comma_separated_list())} , {"overall_messages_routing", t(comma_separated_list())} ]; -fields("listener") -> - [ {"tcp", ref("tcp_listener")} - , {"ssl", ref("ssl_listener")} - , {"ws", ref("ws_listener")} - , {"wss", ref("wss_listener")} +fields("flapping_detect") -> + [ {"enable", t(boolean(), undefined, true)} + , {"max_count", t(integer(), undefined, 15)} + , {"window_time", t(duration(), undefined, "1m")} + , {"ban_time", t(duration(), undefined, "5m")} ]; -fields("tcp_listener") -> - [ {"$name", ref("tcp_listener_settings")}]; +fields("force_shutdown") -> + [ {"enable", t(boolean(), undefined, true)} + , {"max_message_queue_len", t(range(0, inf), undefined, 1000)} + , {"max_heap_size", t(wordsize(), undefined, "32MB", undefined, + fun(Siz) -> + MaxSiz = case erlang:system_info(wordsize) of + 8 -> % arch_64 + (1 bsl 59) - 1; + 4 -> % arch_32 + (1 bsl 27) - 1 + end, + case Siz > MaxSiz of + true -> + error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); + false -> + ok + end + end)} + ]; -fields("ssl_listener") -> - [ {"$name", ref("ssl_listener_settings")}]; +fields("conn_congestion") -> + [ {"enable_alarm", t(flag(), undefined, false)} + , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} + ]; -fields("ws_listener") -> - [ {"$name", ref("ws_listener_settings")}]; +fields("force_gc") -> + [ {"enable", t(boolean(), undefined, true)} + , {"count", t(range(0, inf), undefined, 16000)} + , {"bytes", t(bytesize(), undefined, "16MB")} + ]; -fields("wss_listener") -> - [ {"$name", ref("wss_listener_settings")}]; +fields("listeners") -> + [ {"$name", hoconsc:union( + [ ref("mqtt_tcp_listener") + , ref("mqtt_ssl_listener") + , ref("mqtt_ws_listener") + , ref("mqtt_wss_listener") + ])} + ]; -fields("listener_settings") -> - [ {"endpoint", t(union(ip_port(), integer()))} - , {"acceptors", t(integer(), undefined, 8)} - , {"max_connections", t(integer(), undefined, 1024)} - , {"max_conn_rate", t(integer())} - , {"active_n", t(integer(), undefined, 100)} - , {"zone", t(string())} - , {"rate_limit", t(comma_separated_list())} - , {"access", ref("access")} - , {"proxy_protocol", t(flag())} - , {"proxy_protocol_timeout", t(duration())} +fields("mqtt_tcp_listener") -> + [ {"type", t(typerefl:atom(tcp))} + , {"tcp", ref("tcp_opts")} + ] ++ mqtt_listener(); + +fields("mqtt_ssl_listener") -> + [ {"type", t(typerefl:atom(ssl))} + , {"ssl", ref("ssl_opts")} + , {"tcp", ref("tcp_opts")} + ] ++ mqtt_listener(); + +fields("mqtt_ws_listener") -> + [ {"type", t(typerefl:atom(ws))} + , {"tcp", ref("tcp_opts")} + , {"websocket", ref("ws_opts")} + ] ++ mqtt_listener(); + +fields("mqtt_wss_listener") -> + [ {"type", t(typerefl:atom(ws))} + , {"tcp", ref("tcp_opts")} + , {"ssl", ref("ssl_opts")} + , {"websocket", ref("ws_opts")} + ] ++ mqtt_listener(); + +fields("ws_opts") -> + [ {"mqtt_path", t(string(), undefined, "/mqtt")} + , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} + , {"compress", t(boolean())} + , {"idle_timeout", t(duration())} + , {"max_frame_size", t(integer())} + , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} + , {"supported_subprotocols", t(string(), undefined, + "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} + , {"check_origin_enable", t(boolean(), undefined, false)} + , {"allow_origin_absence", t(boolean(), undefined, true)} + , {"check_origins", t(comma_separated_list())} + , {"proxy_address_header", t(string(), undefined, "x-forwarded-for")} + , {"proxy_port_header", t(string(), undefined, "x-forwarded-port")} + , {"deflate_opts", ref("deflate_opts")} + ]; + +fields("tcp_opts") -> + [ {"active_n", t(integer(), undefined, 100)} , {"backlog", t(integer(), undefined, 1024)} , {"send_timeout", t(duration(), undefined, "15s")} , {"send_timeout_close", t(flag(), undefined, true)} , {"recbuf", t(bytesize())} , {"sndbuf", t(bytesize())} , {"buffer", t(bytesize())} - , {"high_watermark", t(bytesize(), undefined, "1MB")} , {"tune_buffer", t(flag())} + , {"high_watermark", t(bytesize(), undefined, "1MB")} , {"nodelay", t(boolean())} , {"reuseaddr", t(boolean())} ]; -fields("tcp_listener_settings") -> - [ {"peer_cert_as_username", t(cn)} - , {"peer_cert_as_clientid", t(cn)} - ] ++ fields("listener_settings"); - -fields("ssl_listener_settings") -> - [ {"peer_cert_as_username", t(union([cn, dn, crt, pem, md5]))} - , {"peer_cert_as_clientid", t(union([cn, dn, crt, pem, md5]))} - ] ++ - ssl(undefined, #{handshake_timeout => "15s" - , depth => 10 - , reuse_sessions => true}) ++ fields("listener_settings"); - -fields("ws_listener_settings") -> - [ {"mqtt_path", t(string(), undefined, "/mqtt")} - , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} - , {"supported_subprotocols", t(string(), undefined, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} - , {"proxy_address_header", t(string(), undefined, "x-forwarded-for")} - , {"proxy_port_header", t(string(), undefined, "x-forwarded-port")} - , {"compress", t(boolean())} - , {"deflate_opts", ref("deflate_opts")} - , {"idle_timeout", t(duration())} - , {"max_frame_size", t(integer())} - , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} - , {"check_origin_enable", t(boolean(), undefined, false)} - , {"allow_origin_absence", t(boolean(), undefined, true)} - , {"check_origins", t(comma_separated_list())} - % @fixme - ] ++ lists:keydelete("high_watermark", 1, fields("tcp_listener_settings")); - -fields("wss_listener_settings") -> - % @fixme - Ssl = ssl(undefined, #{depth => 10 - , reuse_sessions => true}) ++ fields("listener_settings"), - Settings = lists:ukeymerge(1, Ssl, fields("ws_listener_settings")), - lists:keydelete("high_watermark", 1, Settings); - -fields("access") -> - [ {"$id", t(string(), undefined, undefined)}]; +fields("ssl_opts") -> + ssl(#{handshake_timeout => "15s" + , depth => 10 + , reuse_sessions => true}); fields("deflate_opts") -> [ {"level", t(union([none, default, best_compression, best_speed]))} @@ -414,7 +429,6 @@ fields("subscription_settings") -> , {"rh", t(range(0, 2), undefined, 0)} ]; - fields("rewrite") -> [ {"rule", ref("rule")} , {"pub_rule", ref("rule")} @@ -431,15 +445,13 @@ fields("plugins") -> ]; fields("broker") -> - [ {"sys_interval", t(duration(), "emqx.broker_sys_interval", "1m")} - , {"sys_heartbeat", t(duration(), "emqx.broker_sys_heartbeat", "30s")} - , {"enable_session_registry", t(flag(), "emqx.enable_session_registry", true)} - , {"session_locking_strategy", t(union([local, leader, quorum, all]), - "emqx.session_locking_strategy", quorum)} - , {"shared_subscription_strategy", t(union(random, round_robin), - "emqx.shared_subscription_strategy", round_robin)} - , {"shared_dispatch_ack_enabled", t(boolean(), "emqx.shared_dispatch_ack_enabled", false)} - , {"route_batch_clean", t(flag(), "emqx.route_batch_clean", true)} + [ {"sys_msg_interval", maybe_disabled(duration(), "1m")} + , {"sys_heartbeat_interval", maybe_disabled(duration(), "30s")} + , {"enable_session_registry", t(flag(), undefined, true)} + , {"session_locking_strategy", t(union([local, leader, quorum, all]), undefined, quorum)} + , {"shared_subscription_strategy", t(union(random, round_robin), undefined, round_robin)} + , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} + , {"route_batch_clean", t(flag(), undefined, true)} , {"perf", ref("perf")} ]; @@ -449,14 +461,22 @@ fields("perf") -> ]; fields("sysmon") -> - [ {"long_gc", t(duration(), undefined, 0)} - , {"long_schedule", t(duration(), undefined, 240)} - , {"large_heap", t(bytesize(), undefined, "8MB")} + [ {"vm", ref("sysmon_vm")} + , {"os", ref("sysmon_os")} + ]; + +fields("sysmon_vm") -> + [ {"process_check_interval", t(duration_s(), undefined, 30)} + , {"process_high_watermark", t(percent(), undefined, "80%")} + , {"process_low_watermark", t(percent(), undefined, "60%")} + , {"long_gc", maybe_disabled(duration())} + , {"long_schedule", maybe_disabled(duration(), 240)} + , {"large_heap", maybe_disabled(bytesize(), "8MB")} , {"busy_dist_port", t(boolean(), undefined, true)} , {"busy_port", t(boolean(), undefined, false)} ]; -fields("os_mon") -> +fields("sysmon_os") -> [ {"cpu_check_interval", t(duration_s(), undefined, 60)} , {"cpu_high_watermark", t(percent(), undefined, "80%")} , {"cpu_low_watermark", t(percent(), undefined, "60%")} @@ -465,12 +485,6 @@ fields("os_mon") -> , {"procmem_high_watermark", t(percent(), undefined, "5%")} ]; -fields("vm_mon") -> - [ {"check_interval", t(duration_s(), undefined, 30)} - , {"process_high_watermark", t(percent(), undefined, "80%")} - , {"process_low_watermark", t(percent(), undefined, "60%")} - ]; - fields("alarm") -> [ {"actions", t(comma_separated_list(), undefined, "log,publish")} , {"size_limit", t(integer(), undefined, 1000)} @@ -487,6 +501,15 @@ fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). +mqtt_listener() -> + [ {"bind", t(union(ip_port(), integer()))} + , {"acceptors", t(integer(), undefined, 8)} + , {"max_connections", t(integer(), undefined, 1024000)} + , {"access", t(list(string()))} + , {"proxy_protocol", t(flag())} + , {"proxy_protocol_timeout", t(duration())} + ]. + translations() -> ["ekka", "vm_args", "gen_rpc", "kernel", "emqx"]. translation("ekka") -> @@ -509,9 +532,6 @@ translation("emqx") -> , {"zones", fun tr_zones/1} , {"listeners", fun tr_listeners/1} , {"modules", fun tr_modules/1} - , {"sysmon", fun tr_sysmon/1} - , {"os_mon", fun tr_os_mon/1} - , {"vm_mon", fun tr_vm_mon/1} , {"alarm", fun tr_alarm/1} , {"telemetry", fun tr_telemetry/1} ]. @@ -527,19 +547,6 @@ tr_heart(Conf) -> _ -> undefined end. -%% @doc http://www.erlang.org/doc/man/erl.html#%2bzdbbl -node__dist_buffer_size(type) -> bytesize(); -node__dist_buffer_size(validator) -> - fun(ZDBBL) -> - case ZDBBL >= 1024 andalso ZDBBL =< 2147482624 of - true -> - ok; - false -> - {error, "must be between 1KB and 2097151KB"} - end - end; -node__dist_buffer_size(_) -> undefined. - tr_zdbbl(Conf) -> case conf_get("node.dist_buffer_size", Conf) of undefined -> undefined; @@ -834,25 +841,6 @@ tr_modules(Conf) -> [{emqx_mod_acl_internal, [{acl_file, conf_get("acl.acl_file", Conf)}]}] ]). -tr_sysmon(Conf) -> - Keys = maps:to_list(conf_get("sysmon", Conf, #{})), - [{binary_to_atom(K), maps:get(value, V)} || {K, V} <- Keys]. - -tr_os_mon(Conf) -> - [{cpu_check_interval, conf_get("os_mon.cpu_check_interval", Conf)} - , {cpu_high_watermark, conf_get("os_mon.cpu_high_watermark", Conf) * 100} - , {cpu_low_watermark, conf_get("os_mon.cpu_low_watermark", Conf) * 100} - , {mem_check_interval, conf_get("os_mon.mem_check_interval", Conf)} - , {sysmem_high_watermark, conf_get("os_mon.sysmem_high_watermark", Conf) * 100} - , {procmem_high_watermark, conf_get("os_mon.procmem_high_watermark", Conf) * 100} - ]. - -tr_vm_mon(Conf) -> - [ {check_interval, conf_get("vm_mon.check_interval", Conf)} - , {process_high_watermark, conf_get("vm_mon.process_high_watermark", Conf) * 100} - , {process_low_watermark, conf_get("vm_mon.process_low_watermark", Conf) * 100} - ]. - tr_alarm(Conf) -> [ {actions, [list_to_atom(Action) || Action <- conf_get("alarm.actions", Conf)]} , {size_limit, conf_get("alarm.size_limit", Conf)} @@ -966,46 +954,7 @@ rate_limit_num_dur([L, D]) -> map_zones(_, undefined) -> {undefined, undefined}; -map_zones("force_gc_policy", [Count, Bytes]) -> - GcPolicy = case to_bytesize(Bytes) of - {error, Reason} -> - error({bytesize, Reason}); - {ok, Bytes1} -> - #{bytes => Bytes1, - count => list_to_integer(Count)} - end, - {force_gc_policy, GcPolicy}; -map_zones("force_shutdown_policy", ["default"]) -> - WordSize = erlang:system_info(wordsize), - {DefaultLen, DefaultSize} = - case WordSize of - 8 -> % arch_64 - {10000, hocon_postprocess:bytesize("64MB")}; - 4 -> % arch_32 - {1000, hocon_postprocess:bytesize("32MB")} - end, - {force_shutdown_policy, #{message_queue_len => DefaultLen, - max_heap_size => DefaultSize div WordSize - }}; -map_zones("force_shutdown_policy", [Len, Siz]) -> - WordSize = erlang:system_info(wordsize), - MaxSiz = case WordSize of - 8 -> % arch_64 - (1 bsl 59) - 1; - 4 -> % arch_32 - (1 bsl 27) - 1 - end, - ShutdownPolicy = - case to_bytesize(Siz) of - {error, Reason} -> - error(Reason); - {ok, Siz1} when Siz1 > MaxSiz -> - error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); - {ok, Siz1} -> - #{message_queue_len => list_to_integer(Len), - max_heap_size => Siz1 div WordSize} - end, - {force_shutdown_policy, ShutdownPolicy}; + map_zones("mqueue_priorities", Val) -> case Val of ["none"] -> {mqueue_priorities, none}; % NO_PRIORITY_TABLE @@ -1019,8 +968,6 @@ map_zones("mqueue_priorities", Val) -> end, #{}, Val), {mqueue_priorities, MqueuePriorities} end; -map_zones("mountpoint", Val) -> - {mountpoint, iolist_to_binary(Val)}; map_zones("response_information", Val) -> {response_information, iolist_to_binary(Val)}; map_zones("rate_limit", Conf) -> @@ -1094,41 +1041,34 @@ filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. %% generate a ssl field. -%% ssl("emqx", #{"verify" => verify_peer}) will return -%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)} -%% , {"certfile", t(string(), "emqx.certfile", undefined)} -%% , {"keyfile", t(string(), "emqx.keyfile", undefined)} -%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)} -%% , {"server_name_indication", "emqx.server_name_indication", undefined)} -%% ... -ssl(Mapping, Defaults) -> - M = fun (Field) -> - case (Mapping) of - undefined -> undefined; - _ -> Mapping ++ "." ++ Field - end end, +%% ssl(#{"verify" => verify_peer}) will return: +%% [ {"cacertfile", t(string(), undefined, undefined)} +%% , {"certfile", t(string(), undefined, undefined)} +%% , {"keyfile", t(string(), undefined, undefined)} +%% , {"verify", t(union(verify_peer, verify_none), undefined, verify_peer)} +%% , {"server_name_indication", undefined, undefined)} +%% ...] +ssl(Defaults) -> D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"enable", t(flag(), M("enable"), D("enable"))} - , {"cacertfile", t(string(), M("cacertfile"), D("cacertfile"))} - , {"certfile", t(string(), M("certfile"), D("certfile"))} - , {"keyfile", t(string(), M("keyfile"), D("keyfile"))} - , {"verify", t(union(verify_peer, verify_none), M("verify"), D("verify"))} - , {"fail_if_no_peer_cert", t(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", t(flag(), M("secure_renegotiate"), D("secure_renegotiate"))} - , {"reuse_sessions", t(flag(), M("reuse_sessions"), D("reuse_sessions"))} - , {"honor_cipher_order", t(flag(), M("honor_cipher_order"), D("honor_cipher_order"))} - , {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))} - , {"depth", t(integer(), M("depth"), D("depth"))} - , {"password", hoconsc:t(string(), #{mapping => M("key_password"), - default => D("key_password"), + [ {"cacertfile", t(string(), undefined, D("cacertfile"))} + , {"certfile", t(string(), undefined, D("certfile"))} + , {"keyfile", t(string(), undefined, D("keyfile"))} + , {"verify", t(union(verify_peer, verify_none), undefined, D("verify"))} + , {"fail_if_no_peer_cert", t(boolean(), undefined, D("fail_if_no_peer_cert"))} + , {"secure_renegotiate", t(flag(), undefined, D("secure_renegotiate"))} + , {"reuse_sessions", t(flag(), undefined, D("reuse_sessions"))} + , {"honor_cipher_order", t(flag(), undefined, D("honor_cipher_order"))} + , {"handshake_timeout", t(duration(), undefined, D("handshake_timeout"))} + , {"depth", t(integer(), undefined, D("depth"))} + , {"password", hoconsc:t(string(), #{default => D("key_password"), sensitive => true })} - , {"dhfile", t(string(), M("dhfile"), D("dhfile"))} - , {"server_name_indication", t(union(disable, string()), M("server_name_indication"), + , {"dhfile", t(string(), undefined, D("dhfile"))} + , {"server_name_indication", t(union(disable, string()), undefined, D("server_name_indication"))} - , {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))} - , {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))} - , {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}]. + , {"tls_versions", t(comma_separated_list(), undefined, D("tls_versions"))} + , {"ciphers", t(comma_separated_list(), undefined, D("ciphers"))} + , {"psk_ciphers", t(comma_separated_list(), undefined, D("ciphers"))}]. tr_ssl(Field, Conf) -> Versions = case conf_get([Field, "tls_versions"], Conf) of @@ -1220,9 +1160,31 @@ t(Type, Mapping, Default, OverrideEnv) -> , override_env => OverrideEnv }). +t(Type, Mapping, Default, OverrideEnv, Validator) -> + hoconsc:t(Type, #{ mapping => Mapping + , default => Default + , override_env => OverrideEnv + , validator => Validator + }). + ref(Field) -> fun (type) -> Field; (_) -> undefined end. +maybe_disabled(T) -> + maybe_sth(disabled, T, disabled). + +maybe_disabled(T, Default) -> + maybe_sth(disabled, T, Default). + +% maybe_infinity(T) -> +% maybe_sth(infinity, T, infinity). + +maybe_infinity(T, Default) -> + maybe_sth(infinity, T, Default). + +maybe_sth(What, Type, Default) -> + t(union([What, Type]), undefined, Default). + to_flag(Str) -> {ok, hocon_postprocess:onoff(Str)}. @@ -1250,6 +1212,13 @@ to_bytesize(Str) -> _ -> {error, Str} end. +to_wordsize(Str) -> + WordSize = erlang:system_info(wordsize), + case to_bytesize(Str) of + {ok, Bytes} -> Bytes div WordSize; + Error -> Error + end. + to_percent(Str) -> {ok, hocon_postprocess:percent(Str)}. From cdcb63374a1fd22be58575ba1016aa5ea0cb3879 Mon Sep 17 00:00:00 2001 From: tigercl Date: Wed, 30 Jun 2021 17:04:28 +0800 Subject: [PATCH 046/379] refactor(authn): support hocon for authn (#5068) * refactor(use hocon): rename to authn, , support hocon, support two types of chains and support bind listener to chain --- .../etc/emqx_authentication.conf | 0 .../include/emqx_authentication.hrl | 43 -- .../priv/emqx_authentication.schema | 0 .../src/emqx_authentication.erl | 522 ----------------- .../src/emqx_authentication_api.erl | 407 ------------- .../src/emqx_authentication_jwt.erl | 409 ------------- .../test/emqx_authentication_SUITE.erl | 189 ------ .../data/user-credentials.csv | 0 .../data/user-credentials.json | 0 apps/emqx_authn/etc/emqx_authn.conf | 26 + apps/emqx_authn/include/emqx_authn.hrl | 67 +++ .../rebar.config | 0 .../src/emqx_authn.app.src} | 6 +- apps/emqx_authn/src/emqx_authn.erl | 490 ++++++++++++++++ apps/emqx_authn/src/emqx_authn_api.erl | 544 ++++++++++++++++++ apps/emqx_authn/src/emqx_authn_app.erl | 80 +++ apps/emqx_authn/src/emqx_authn_schema.erl | 114 ++++ .../src/emqx_authn_sup.erl} | 2 +- apps/emqx_authn/src/emqx_authn_utils.erl | 55 ++ .../emqx_enhanced_authn_mnesia.erl} | 22 +- .../emqx_authn_jwks_connector.erl} | 26 +- .../src/simple_authn/emqx_authn_jwt.erl | 343 +++++++++++ .../src/simple_authn/emqx_authn_mnesia.erl} | 107 ++-- .../src/simple_authn/emqx_authn_mysql.erl | 160 ++++++ .../src/simple_authn/emqx_authn_pgsql.erl | 155 +++++ apps/emqx_authn/test/data/private_key.pem | 15 + apps/emqx_authn/test/data/public_key.pem | 6 + .../test/data/user-credentials.csv | 0 .../test/data/user-credentials.json | 0 apps/emqx_authn/test/emqx_authn_SUITE.erl | 142 +++++ apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 182 ++++++ .../test/emqx_authn_mnesia_SUITE.erl | 187 ++++++ rebar.config.erl | 3 +- 33 files changed, 2652 insertions(+), 1650 deletions(-) delete mode 100644 apps/emqx_authentication/etc/emqx_authentication.conf delete mode 100644 apps/emqx_authentication/include/emqx_authentication.hrl delete mode 100644 apps/emqx_authentication/priv/emqx_authentication.schema delete mode 100644 apps/emqx_authentication/src/emqx_authentication.erl delete mode 100644 apps/emqx_authentication/src/emqx_authentication_api.erl delete mode 100644 apps/emqx_authentication/src/emqx_authentication_jwt.erl delete mode 100644 apps/emqx_authentication/test/emqx_authentication_SUITE.erl rename apps/{emqx_authentication => emqx_authn}/data/user-credentials.csv (100%) rename apps/{emqx_authentication => emqx_authn}/data/user-credentials.json (100%) create mode 100644 apps/emqx_authn/etc/emqx_authn.conf create mode 100644 apps/emqx_authn/include/emqx_authn.hrl rename apps/{emqx_authentication => emqx_authn}/rebar.config (100%) rename apps/{emqx_authentication/src/emqx_authentication.app.src => emqx_authn/src/emqx_authn.app.src} (63%) create mode 100644 apps/emqx_authn/src/emqx_authn.erl create mode 100644 apps/emqx_authn/src/emqx_authn_api.erl create mode 100644 apps/emqx_authn/src/emqx_authn_app.erl create mode 100644 apps/emqx_authn/src/emqx_authn_schema.erl rename apps/{emqx_authentication/src/emqx_authentication_sup.erl => emqx_authn/src/emqx_authn_sup.erl} (96%) create mode 100644 apps/emqx_authn/src/emqx_authn_utils.erl rename apps/{emqx_authentication/src/emqx_authentication_app.erl => emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl} (63%) rename apps/{emqx_authentication/src/emqx_authentication_jwks_connector.erl => emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl} (87%) create mode 100644 apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl rename apps/{emqx_authentication/src/emqx_authentication_mnesia.erl => emqx_authn/src/simple_authn/emqx_authn_mnesia.erl} (80%) create mode 100644 apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl create mode 100644 apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl create mode 100644 apps/emqx_authn/test/data/private_key.pem create mode 100644 apps/emqx_authn/test/data/public_key.pem rename apps/{emqx_authentication => emqx_authn}/test/data/user-credentials.csv (100%) rename apps/{emqx_authentication => emqx_authn}/test/data/user-credentials.json (100%) create mode 100644 apps/emqx_authn/test/emqx_authn_SUITE.erl create mode 100644 apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl create mode 100644 apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl diff --git a/apps/emqx_authentication/etc/emqx_authentication.conf b/apps/emqx_authentication/etc/emqx_authentication.conf deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl deleted file mode 100644 index 814c03eb4..000000000 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ /dev/null @@ -1,43 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020 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. -%%-------------------------------------------------------------------- - --define(APP, emqx_authentication). - --type(service_type_name() :: atom()). --type(service_name() :: binary()). --type(chain_id() :: binary()). - --record(service_type, - { name :: service_type_name() - , provider :: module() - , params_spec :: #{atom() => term()} - }). - --record(service, - { name :: service_name() - , type :: service_type_name() - , provider :: module() - , params :: map() - , state :: map() - }). - --record(chain, - { id :: chain_id() - , services :: [{service_name(), #service{}}] - , created_at :: integer() - }). - --define(AUTH_SHARD, emqx_authentication_shard). diff --git a/apps/emqx_authentication/priv/emqx_authentication.schema b/apps/emqx_authentication/priv/emqx_authentication.schema deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl deleted file mode 100644 index a3d0241c0..000000000 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ /dev/null @@ -1,522 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication). - --include("emqx_authentication.hrl"). - --export([ enable/0 - , disable/0 - ]). - --export([authenticate/1]). - --export([register_service_types/0]). - --export([ create_chain/1 - , delete_chain/1 - , lookup_chain/1 - , list_chains/0 - , add_services/2 - , delete_services/2 - , update_service/3 - , lookup_service/2 - , list_services/1 - , move_service_to_the_front/2 - , move_service_to_the_end/2 - , move_service_to_the_nth/3 - ]). - --export([ import_users/3 - , add_user/3 - , delete_user/3 - , update_user/4 - , lookup_user/3 - , list_users/2 - ]). - --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - --define(CHAIN_TAB, emqx_authentication_chain). --define(SERVICE_TYPE_TAB, emqx_authentication_service_type). - --rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). --rlog_shard({?AUTH_SHARD, ?SERVICE_TYPE_TAB}). - -%%------------------------------------------------------------------------------ -%% Mnesia bootstrap -%%------------------------------------------------------------------------------ - -%% @doc Create or replicate tables. --spec(mnesia(boot | copy) -> ok). -mnesia(boot) -> - %% Optimize storage - StoreProps = [{ets, [{read_concurrency, true}]}], - %% Chain table - ok = ekka_mnesia:create_table(?CHAIN_TAB, [ - {disc_copies, [node()]}, - {record_name, chain}, - {attributes, record_info(fields, chain)}, - {storage_properties, StoreProps}]), - %% Service type table - ok = ekka_mnesia:create_table(?SERVICE_TYPE_TAB, [ - {ram_copies, [node()]}, - {record_name, service_type}, - {attributes, record_info(fields, service_type)}, - {storage_properties, StoreProps}]); - -mnesia(copy) -> - %% Copy chain table - ok = ekka_mnesia:copy_table(?CHAIN_TAB, disc_copies), - %% Copy service type table - ok = ekka_mnesia:copy_table(?SERVICE_TYPE_TAB, ram_copies). - -enable() -> - case emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}) of - ok -> ok; - {error, already_exists} -> ok - end. - -disable() -> - emqx:unhook('client.authenticate', {emqx_authentication, authenticate}), - ok. - -authenticate(#{chain_id := ChainID} = ClientInfo) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [#chain{services = []}] -> - {error, no_services}; - [#chain{services = Services}] -> - do_authenticate(Services, ClientInfo); - [] -> - {error, todo} - end. - -do_authenticate([], _) -> - {error, user_not_found}; -do_authenticate([{_, #service{provider = Provider, state = State}} | More], ClientInfo) -> - case Provider:authenticate(ClientInfo, State) of - ignore -> do_authenticate(More, ClientInfo); - ok -> ok; - {ok, NewClientInfo} -> {ok, NewClientInfo}; - {stop, Reason} -> {error, Reason} - end. - -register_service_types() -> - Attrs = find_attrs(?APP, service_type), - register_service_types(Attrs). - -register_service_types(Attrs) -> - register_service_types(Attrs, []). - -register_service_types([], Acc) -> - do_register_service_types(Acc); -register_service_types([{_App, Mod, #{name := Name, - params_spec := ParamsSpec}} | Types], Acc) -> - %% TODO: Temporary realization - ok = emqx_rule_validator:validate_spec(ParamsSpec), - ServiceType = #service_type{name = Name, - provider = Mod, - params_spec = ParamsSpec}, - register_service_types(Types, [ServiceType | Acc]). - -create_chain(#{id := ID}) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - Chain = #chain{id = ID, - services = [], - created_at = erlang:system_time(millisecond)}, - mnesia:write(?CHAIN_TAB, Chain, write), - {ok, serialize_chain(Chain)}; - [_ | _] -> - {error, {already_exists, {chain, ID}}} - end - end). - -delete_chain(ID) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - {error, {not_found, {chain, ID}}}; - [#chain{services = Services}] -> - ok = delete_services_(Services), - mnesia:delete(?CHAIN_TAB, ID, write) - end - end). - -lookup_chain(ID) -> - case mnesia:dirty_read(?CHAIN_TAB, ID) of - [] -> - {error, {not_found, {chain, ID}}}; - [Chain] -> - {ok, serialize_chain(Chain)} - end. - -list_chains() -> - Chains = ets:tab2list(?CHAIN_TAB), - {ok, [serialize_chain(Chain) || Chain <- Chains]}. - -add_services(ChainID, ServiceParams) -> - case validate_service_params(ServiceParams) of - {ok, NServiceParams} -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - Names = [Name || {Name, _} <- Services] ++ [Name || #{name := Name} <- NServiceParams], - case no_duplicate_names(Names) of - ok -> - case create_services(ChainID, NServiceParams) of - {ok, NServices} -> - NChain = Chain#chain{services = Services ++ NServices}, - ok = mnesia:write(?CHAIN_TAB, NChain, write), - {ok, serialize_services(NServices)}; - {error, Reason} -> - {error, Reason} - end; - {error, {duplicate, Name}} -> - {error, {already_exists, {service, Name}}} - end - end, - update_chain(ChainID, UpdateFun); - {error, Reason} -> - {error, Reason} - end. - -delete_services(ChainID, ServiceNames) -> - case no_duplicate_names(ServiceNames) of - ok -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case extract_services(ServiceNames, Services) of - {ok, Extracted, Rest} -> - ok = delete_services_(Extracted), - NChain = Chain#chain{services = Rest}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun); - {error, Reason} -> - {error, Reason} - end. - -update_service(ChainID, ServiceName, NewParams) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case proplists:get_value(ServiceName, Services, undefined) of - undefined -> - {error, {not_found, {service, ServiceName}}}; - #service{type = Type, - provider = Provider, - params = OriginalParams, - state = State} = Service -> - Params = maps:merge(OriginalParams, NewParams), - {ok, #service_type{params_spec = ParamsSpec}} = find_service_type(Type), - NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), - case Provider:update(ChainID, ServiceName, NParams, State) of - {ok, NState} -> - NService = Service#service{params = Params, - state = NState}, - NServices = lists:keyreplace(ServiceName, 1, Services, {ServiceName, NService}), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write), - {ok, serialize_service({ServiceName, NService})}; - {error, Reason} -> - {error, Reason} - end - end - end, - update_chain(ChainID, UpdateFun). - -lookup_service(ChainID, ServiceName) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - case lists:keytake(ServiceName, 1, Services) of - {value, Service, _} -> - {ok, serialize_service(Service)}; - false -> - {error, {not_found, {service, ServiceName}}} - end - end. - -list_services(ChainID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - {ok, serialize_services(Services)} - end. - -move_service_to_the_front(ChainID, ServiceName) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_front_(ServiceName, Services) of - {ok, NServices} -> - NChain = Chain#chain{services = NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -move_service_to_the_end(ChainID, ServiceName) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_end_(ServiceName, Services) of - {ok, NServices} -> - NChain = Chain#chain{services = NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -move_service_to_the_nth(ChainID, ServiceName, N) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_nth_(ServiceName, Services, N) of - {ok, NServices} -> - NChain = Chain#chain{services = NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). - -import_users(ChainID, ServiceName, Filename) -> - call_service(ChainID, ServiceName, import_users, [Filename]). - -add_user(ChainID, ServiceName, UserInfo) -> - call_service(ChainID, ServiceName, add_user, [UserInfo]). - -delete_user(ChainID, ServiceName, UserID) -> - call_service(ChainID, ServiceName, delete_user, [UserID]). - -update_user(ChainID, ServiceName, UserID, NewUserInfo) -> - call_service(ChainID, ServiceName, update_user, [UserID, NewUserInfo]). - -lookup_user(ChainID, ServiceName, UserID) -> - call_service(ChainID, ServiceName, lookup_user, [UserID]). - -list_users(ChainID, ServiceName) -> - call_service(ChainID, ServiceName, list_users, []). - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -find_attrs(App, AttrName) -> - [{App, Mod, Attr} || {ok, Modules} <- [application:get_key(App, modules)], - Mod <- Modules, - {Name, Attrs} <- module_attributes(Mod), Name =:= AttrName, - Attr <- Attrs]. - -module_attributes(Module) -> - try Module:module_info(attributes) - catch - error:undef -> [] - end. - -do_register_service_types(ServiceTypes) -> - trans(fun lists:foreach/2, [fun insert_service_type/1, ServiceTypes]). - -insert_service_type(ServiceType) -> - mnesia:write(?SERVICE_TYPE_TAB, ServiceType, write). - -find_service_type(Name) -> - case mnesia:dirty_read(?SERVICE_TYPE_TAB, Name) of - [ServiceType] -> {ok, ServiceType}; - [] -> {error, not_found} - end. - -validate_service_params(ServiceParams) -> - case validate_service_names(ServiceParams) of - ok -> - validate_other_service_params(ServiceParams); - {error, Reason} -> - {error, Reason} - end. - -validate_service_names(ServiceParams) -> - Names = [Name || #{name := Name} <- ServiceParams], - no_duplicate_names(Names). - -validate_other_service_params(ServiceParams) -> - validate_other_service_params(ServiceParams, []). - -validate_other_service_params([], Acc) -> - {ok, lists:reverse(Acc)}; -validate_other_service_params([#{type := Type, params := Params} = ServiceParams | More], Acc) -> - case find_service_type(Type) of - {ok, #service_type{provider = Provider, params_spec = ParamsSpec}} -> - NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), - validate_other_service_params(More, - [ServiceParams#{params => NParams, - original_params => Params, - provider => Provider} | Acc]); - {error, not_found} -> - {error, {not_found, {service_type, Type}}} - end. - -no_duplicate_names(Names) -> - no_duplicate_names(Names, #{}). - -no_duplicate_names([], _) -> - ok; -no_duplicate_names([Name | More], Acc) -> - case maps:is_key(Name, Acc) of - false -> no_duplicate_names(More, Acc#{Name => true}); - true -> {error, {duplicate, Name}} - end. - -create_services(ChainID, ServiceParams) -> - create_services(ChainID, ServiceParams, []). - -create_services(_ChainID, [], Acc) -> - {ok, lists:reverse(Acc)}; -create_services(ChainID, [#{name := Name, - type := Type, - provider := Provider, - params := Params, - original_params := OriginalParams} | More], Acc) -> - case Provider:create(ChainID, Name, Params) of - {ok, State} -> - Service = #service{name = Name, - type = Type, - provider = Provider, - params = OriginalParams, - state = State}, - create_services(ChainID, More, [{Name, Service} | Acc]); - {error, Reason} -> - delete_services_(Acc), - {error, Reason} - end. - -delete_services_([]) -> - ok; -delete_services_([{_, #service{provider = Provider, state = State}} | More]) -> - Provider:destroy(State), - delete_services_(More). - -extract_services(ServiceNames, Services) -> - extract_services(ServiceNames, Services, []). - -extract_services([], Rest, Extracted) -> - {ok, lists:reverse(Extracted), Rest}; -extract_services([ServiceName | More], Services, Acc) -> - case lists:keytake(ServiceName, 1, Services) of - {value, Extracted, Rest} -> - extract_services(More, Rest, [Extracted | Acc]); - false -> - {error, {not_found, {service, ServiceName}}} - end. - -move_service_to_the_front_(ServiceName, Services) -> - move_service_to_the_front_(ServiceName, Services, []). - -move_service_to_the_front_(ServiceName, [], _) -> - {error, {not_found, {service, ServiceName}}}; -move_service_to_the_front_(ServiceName, [{ServiceName, _} = Service | More], Passed) -> - {ok, [Service | (lists:reverse(Passed) ++ More)]}; -move_service_to_the_front_(ServiceName, [Service | More], Passed) -> - move_service_to_the_front_(ServiceName, More, [Service | Passed]). - -move_service_to_the_end_(ServiceName, Services) -> - move_service_to_the_end_(ServiceName, Services, []). - -move_service_to_the_end_(ServiceName, [], _) -> - {error, {not_found, {service, ServiceName}}}; -move_service_to_the_end_(ServiceName, [{ServiceName, _} = Service | More], Passed) -> - {ok, lists:reverse(Passed) ++ More ++ [Service]}; -move_service_to_the_end_(ServiceName, [Service | More], Passed) -> - move_service_to_the_end_(ServiceName, More, [Service | Passed]). - -move_service_to_the_nth_(ServiceName, Services, N) - when N =< length(Services) andalso N > 0 -> - move_service_to_the_nth_(ServiceName, Services, N, []); -move_service_to_the_nth_(_, _, _) -> - {error, out_of_range}. - -move_service_to_the_nth_(ServiceName, [], _, _) -> - {error, {not_found, {service, ServiceName}}}; -move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) - when N =< length(Passed) -> - {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), - {ok, L1 ++ [Service] ++ L2 ++ More}; -move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) -> - {L1, L2} = lists:split(N - length(Passed) - 1, More), - {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2}; -move_service_to_the_nth_(ServiceName, [Service | More], N, Passed) -> - move_service_to_the_nth_(ServiceName, More, N, [Service | Passed]). - -update_chain(ChainID, UpdateFun) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end - end). - -call_service(ChainID, ServiceName, Func, Args) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - case proplists:get_value(ServiceName, Services, undefined) of - undefined -> - {error, {not_found, {service, ServiceName}}}; - #service{provider = Provider, - state = State} -> - case erlang:function_exported(Provider, Func, length(Args) + 1) of - true -> - erlang:apply(Provider, Func, Args ++ [State]); - false -> - {error, unsupported_feature} - end - end - end. - -serialize_chain(#chain{id = ID, - services = Services, - created_at = CreatedAt}) -> - #{id => ID, - services => serialize_services(Services), - created_at => CreatedAt}. - -serialize_services(Services) -> - [serialize_service(Service) || Service <- Services]. - -serialize_service({_, #service{name = Name, - type = Type, - params = Params}}) -> - #{name => Name, - type => Type, - params => Params}. - -trans(Fun) -> - trans(Fun, []). - -trans(Fun, Args) -> - case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of - {atomic, Res} -> Res; - {aborted, Reason} -> {error, Reason} - end. diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl deleted file mode 100644 index 74887a0b2..000000000 --- a/apps/emqx_authentication/src/emqx_authentication_api.erl +++ /dev/null @@ -1,407 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_api). - --export([ create_chain/2 - , delete_chain/2 - , lookup_chain/2 - , list_chains/2 - , add_service/2 - , delete_service/2 - , update_service/2 - , lookup_service/2 - , list_services/2 - , move_service/2 - , import_users/2 - , add_user/2 - , delete_user/2 - , update_user/2 - , lookup_user/2 - , list_users/2 - ]). - --import(minirest, [return/1]). - --rest_api(#{name => create_chain, - method => 'POST', - path => "/authentication/chains", - func => create_chain, - descr => "Create a chain" - }). - --rest_api(#{name => delete_chain, - method => 'DELETE', - path => "/authentication/chains/:bin:id", - func => delete_chain, - descr => "Delete chain" - }). - --rest_api(#{name => lookup_chain, - method => 'GET', - path => "/authentication/chains/:bin:id", - func => lookup_chain, - descr => "Lookup chain" - }). - --rest_api(#{name => list_chains, - method => 'GET', - path => "/authentication/chains", - func => list_chains, - descr => "List all chains" - }). - --rest_api(#{name => add_service, - method => 'POST', - path => "/authentication/chains/:bin:id/services", - func => add_service, - descr => "Add service to chain" - }). - --rest_api(#{name => delete_service, - method => 'DELETE', - path => "/authentication/chains/:bin:id/services/:bin:service_name", - func => delete_service, - descr => "Delete service from chain" - }). - --rest_api(#{name => update_service, - method => 'PUT', - path => "/authentication/chains/:bin:id/services/:bin:service_name", - func => update_service, - descr => "Update service in chain" - }). - --rest_api(#{name => lookup_service, - method => 'GET', - path => "/authentication/chains/:bin:id/services/:bin:service_name", - func => lookup_service, - descr => "Lookup service in chain" - }). - --rest_api(#{name => list_services, - method => 'GET', - path => "/authentication/chains/:bin:id/services", - func => list_services, - descr => "List services in chain" - }). - --rest_api(#{name => move_service, - method => 'POST', - path => "/authentication/chains/:bin:id/services/:bin:service_name/position", - func => move_service, - descr => "Change the order of services" - }). - --rest_api(#{name => import_users, - method => 'POST', - path => "/authentication/chains/:bin:id/services/:bin:service_name/import-users", - func => import_users, - descr => "Import users" - }). - --rest_api(#{name => add_user, - method => 'POST', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users", - func => add_user, - descr => "Add user" - }). - --rest_api(#{name => delete_user, - method => 'DELETE', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", - func => delete_user, - descr => "Delete user" - }). - --rest_api(#{name => update_user, - method => 'PUT', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", - func => update_user, - descr => "Update user" - }). - --rest_api(#{name => lookup_user, - method => 'GET', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", - func => lookup_user, - descr => "Lookup user" - }). - -%% TODO: Support pagination --rest_api(#{name => list_users, - method => 'GET', - path => "/authentication/chains/:bin:id/services/:bin:service_name/users", - func => list_users, - descr => "List all users" - }). - -create_chain(Binding, Params) -> - do_create_chain(uri_decode(Binding), maps:from_list(Params)). - -do_create_chain(_Binding, #{<<"id">> := ChainID}) -> - case emqx_authentication:create_chain(#{id => ChainID}) of - {ok, Chain} -> - return({ok, Chain}); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_create_chain(_Binding, _Params) -> - return(serialize_error({missing_parameter, id})). - -delete_chain(Binding, Params) -> - do_delete_chain(uri_decode(Binding), maps:from_list(Params)). - -do_delete_chain(#{id := ChainID}, _Params) -> - case emqx_authentication:delete_chain(ChainID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_chain(Binding, Params) -> - do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_chain(#{id := ChainID}, _Params) -> - case emqx_authentication:lookup_chain(ChainID) of - {ok, Chain} -> - return({ok, Chain}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_chains(Binding, Params) -> - do_list_chains(uri_decode(Binding), maps:from_list(Params)). - -do_list_chains(_Binding, _Params) -> - {ok, Chains} = emqx_authentication:list_chains(), - return({ok, Chains}). - -add_service(Binding, Params) -> - do_add_service(uri_decode(Binding), maps:from_list(Params)). - -do_add_service(#{id := ChainID}, #{<<"name">> := Name, - <<"type">> := Type, - <<"params">> := Params}) -> - case emqx_authentication:add_services(ChainID, [#{name => Name, - type => binary_to_existing_atom(Type, utf8), - params => maps:from_list(Params)}]) of - {ok, Services} -> - return({ok, Services}); - {error, Reason} -> - return(serialize_error(Reason)) - end; -%% TODO: Check missed field in params -do_add_service(_Binding, Params) -> - Missed = get_missed_params(Params, [<<"name">>, <<"type">>, <<"params">>]), - return(serialize_error({missing_parameter, Missed})). - -delete_service(Binding, Params) -> - do_delete_service(uri_decode(Binding), maps:from_list(Params)). - -do_delete_service(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:delete_services(ChainID, [ServiceName]) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -update_service(Binding, Params) -> - do_update_service(uri_decode(Binding), maps:from_list(Params)). - -%% TOOD: PUT method supports creation and update -do_update_service(#{id := ChainID, - service_name := ServiceName}, Params) -> - case emqx_authentication:update_service(ChainID, ServiceName, Params) of - {ok, Service} -> - return({ok, Service}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_service(Binding, Params) -> - do_lookup_service(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_service(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:lookup_service(ChainID, ServiceName) of - {ok, Service} -> - return({ok, Service}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_services(Binding, Params) -> - do_list_services(uri_decode(Binding), maps:from_list(Params)). - -do_list_services(#{id := ChainID}, _Params) -> - case emqx_authentication:list_services(ChainID) of - {ok, Services} -> - return({ok, Services}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -move_service(Binding, Params) -> - do_move_service(uri_decode(Binding), maps:from_list(Params)). - -do_move_service(#{id := ChainID, - service_name := ServiceName}, #{<<"position">> := <<"the front">>}) -> - case emqx_authentication:move_service_to_the_front(ChainID, ServiceName) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_service(#{id := ChainID, - service_name := ServiceName}, #{<<"position">> := <<"the end">>}) -> - case emqx_authentication:move_service_to_the_end(ChainID, ServiceName) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_service(#{id := ChainID, - service_name := ServiceName}, #{<<"position">> := N}) when is_number(N) -> - case emqx_authentication:move_service_to_the_nth(ChainID, ServiceName, N) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_service(_Binding, _Params) -> - return(serialize_error({missing_parameter, <<"position">>})). - -import_users(Binding, Params) -> - do_import_users(uri_decode(Binding), maps:from_list(Params)). - -do_import_users(#{id := ChainID, service_name := ServiceName}, - #{<<"filename">> := Filename}) -> - case emqx_authentication:import_users(ChainID, ServiceName, Filename) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_import_users(_Binding, Params) -> - Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]), - return(serialize_error({missing_parameter, Missed})). - -add_user(Binding, Params) -> - do_add_user(uri_decode(Binding), maps:from_list(Params)). - -do_add_user(#{id := ChainID, - service_name := ServiceName}, UserInfo) -> - case emqx_authentication:add_user(ChainID, ServiceName, UserInfo) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -delete_user(Binding, Params) -> - do_delete_user(uri_decode(Binding), maps:from_list(Params)). - -do_delete_user(#{id := ChainID, - service_name := ServiceName, - user_id := UserID}, _Params) -> - case emqx_authentication:delete_user(ChainID, ServiceName, UserID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -update_user(Binding, Params) -> - do_update_user(uri_decode(Binding), maps:from_list(Params)). - -do_update_user(#{id := ChainID, - service_name := ServiceName, - user_id := UserID}, NewUserInfo) -> - case emqx_authentication:update_user(ChainID, ServiceName, UserID, NewUserInfo) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_user(Binding, Params) -> - do_lookup_user(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_user(#{id := ChainID, - service_name := ServiceName, - user_id := UserID}, _Params) -> - case emqx_authentication:lookup_user(ChainID, ServiceName, UserID) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_users(Binding, Params) -> - do_list_users(uri_decode(Binding), maps:from_list(Params)). - -do_list_users(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:list_users(ChainID, ServiceName) of - {ok, Users} -> - return({ok, Users}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -uri_decode(Params) -> - maps:fold(fun(K, V, Acc) -> - Acc#{K => emqx_http_lib:uri_decode(V)} - end, #{}, Params). - -serialize_error({already_exists, {Type, ID}}) -> - {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))}; -serialize_error({not_found, {Type, ID}}) -> - {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))}; -serialize_error({duplicate, Name}) -> - {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name '~s' is duplicated", [Name]))}; -serialize_error({missing_parameter, Names = [_ | Rest]}) -> - Format = ["~s," || _ <- Rest] ++ ["~s"], - NFormat = binary_to_list(iolist_to_binary(Format)), - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; -serialize_error({missing_parameter, Name}) -> - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))}; -serialize_error(_) -> - {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. - -serialize_type(service) -> - "Service"; -serialize_type(chain) -> - "Chain"; -serialize_type(service_type) -> - "Service type". - -get_missed_params(Actual, Expected) -> - Keys = lists:foldl(fun(Key, Acc) -> - case maps:is_key(Key, Actual) of - true -> Acc; - false -> [Key | Acc] - end - end, [], Expected), - lists:reverse(Keys). diff --git a/apps/emqx_authentication/src/emqx_authentication_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl deleted file mode 100644 index 2b8024e1c..000000000 --- a/apps/emqx_authentication/src/emqx_authentication_jwt.erl +++ /dev/null @@ -1,409 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_jwt). - --export([ create/3 - , update/4 - , authenticate/2 - , destroy/1 - ]). - --service_type(#{ - name => jwt, - params_spec => #{ - use_jwks => #{ - order => 1, - type => boolean - }, - jwks_endpoint => #{ - order => 2, - type => string - }, - refresh_interval => #{ - order => 3, - type => number - }, - algorithm => #{ - order => 3, - type => string, - enum => [<<"hmac-based">>, <<"public-key">>] - }, - secret => #{ - order => 4, - type => string - }, - secret_base64_encoded => #{ - order => 5, - type => boolean - }, - jwt_certfile => #{ - order => 6, - type => file - }, - cacertfile => #{ - order => 7, - type => file - }, - keyfile => #{ - order => 8, - type => file - }, - certfile => #{ - order => 9, - type => file - }, - verify => #{ - order => 10, - type => boolean - }, - server_name_indication => #{ - order => 11, - type => string - } - } -}). - --define(RULES, - #{ - use_jwks => [], - jwks_endpoint => [use_jwks], - refresh_interval => [use_jwks], - algorithm => [use_jwks], - secret => [algorithm], - secret_base64_encoded => [algorithm], - jwt_certfile => [algorithm], - cacertfile => [jwks_endpoint], - keyfile => [jwks_endpoint], - certfile => [jwks_endpoint], - verify => [jwks_endpoint], - server_name_indication => [jwks_endpoint], - verify_claims => [] - }). - -create(_ChainID, _ServiceName, Params) -> - try handle_options(Params) of - Opts -> - do_create(Opts) - catch - {error, Reason} -> - {error, Reason} - end. - -update(_ChainID, _ServiceName, Params, State) -> - try handle_options(Params) of - Opts -> - do_update(Opts, State) - catch - {error, Reason} -> - {error, Reason} - end. - -authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, - verify_claims := VerifyClaims0}) -> - JWKs = case erlang:is_pid(JWK) of - false -> - [JWK]; - true -> - {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(JWK), - JWKs0 - end, - VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), - case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; - {error, invalid_signature} -> ignore; - {error, {claims, _}} -> {stop, bad_passowrd} - end. - -destroy(#{jwks_connector := undefined}) -> - ok; -destroy(#{jwks_connector := Connector}) -> - _ = emqx_authentication_jwks_connector:stop(Connector), - ok. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -do_create(#{use_jwks := false, - algorithm := 'hmac-based', - secret := Secret0, - secret_base64_encoded := Base64Encoded} = Opts) -> - Secret = case Base64Encoded of - true -> - base64:decode(Secret0); - false -> - Secret0 - end, - JWK = jose_jwk:from_oct(Secret), - {ok, #{jwk => JWK, - verify_claims => maps:get(verify_claims, Opts)}}; - -do_create(#{use_jwks := false, - algorithm := 'public-key', - jwt_certfile := Certfile} = Opts) -> - JWK = jose_jwk:from_pem_file(Certfile), - {ok, #{jwk => JWK, - verify_claims => maps:get(verify_claims, Opts)}}; - -do_create(#{use_jwks := true} = Opts) -> - case emqx_authentication_jwks_connector:start_link(Opts) of - {ok, Connector} -> - {ok, #{jwk => Connector, - verify_claims => maps:get(verify_claims, Opts)}}; - {error, Reason} -> - {error, Reason} - end. - -do_update(Opts, #{jwk_connector := undefined}) -> - do_create(Opts); -do_update(#{use_jwks := false} = Opts, #{jwk_connector := Connector}) -> - _ = emqx_authentication_jwks_connector:stop(Connector), - do_create(Opts); -do_update(#{use_jwks := true} = Opts, #{jwk_connector := Connector} = State) -> - ok = emqx_authentication_jwks_connector:update(Connector, Opts), - {ok, State}. - -replace_placeholder(L, Variables) -> - replace_placeholder(L, Variables, []). - -replace_placeholder([], _Variables, Acc) -> - Acc; -replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) -> - Value = maps:get(PL, Variables), - replace_placeholder(More, Variables, [{Name, Value} | Acc]); -replace_placeholder([{Name, Value} | More], Variables, Acc) -> - replace_placeholder(More, Variables, [{Name, Value} | Acc]). - -verify(_JWS, [], _VerifyClaims) -> - {error, invalid_signature}; -verify(JWS, [JWK | More], VerifyClaims) -> - case jose_jws:verify(JWK, JWS) of - {true, Payload, _JWS} -> - Claims = emqx_json:decode(Payload, [return_maps]), - verify_claims(Claims, VerifyClaims); - {false, _, _} -> - verify(JWS, More, VerifyClaims) - end. - -verify_claims(Claims, VerifyClaims0) -> - Now = os:system_time(seconds), - VerifyClaims = [{<<"exp">>, fun(ExpireTime) -> - Now < ExpireTime - end}, - {<<"iat">>, fun(IssueAt) -> - IssueAt =< Now - end}, - {<<"nbf">>, fun(NotBefore) -> - NotBefore =< Now - end}] ++ VerifyClaims0, - do_verify_claims(Claims, VerifyClaims). - -do_verify_claims(_Claims, []) -> - ok; -do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) -> - case maps:take(Name, Claims) of - error -> - do_verify_claims(Claims, More); - {Value, NClaims} -> - case Fun(Value) of - true -> - do_verify_claims(NClaims, More); - _ -> - {error, {claims, {Name, Value}}} - end - end; -do_verify_claims(Claims, [{Name, Value} | More]) -> - case maps:take(Name, Claims) of - error -> - do_verify_claims(Claims, More); - {Value, NClaims} -> - do_verify_claims(NClaims, More); - {Value0, _} -> - {error, {claims, {Name, Value0}}} - end. - -handle_options(Opts0) when is_map(Opts0) -> - Ks = maps:fold(fun(K, _, Acc) -> - [atom_to_binary(K, utf8) | Acc] - end, [], ?RULES), - Opts1 = maps:to_list(maps:with(Ks, Opts0)), - handle_options([{binary_to_existing_atom(K, utf8), V} || {K, V} <- Opts1]); - -handle_options(Opts0) when is_list(Opts0) -> - Opts1 = add_missing_options(Opts0), - process_options({Opts1, [], length(Opts1)}, #{}). - -add_missing_options(Opts) -> - AllOpts = maps:keys(?RULES), - Fun = fun(K, Acc) -> - case proplists:is_defined(K, Acc) of - true -> - Acc; - false -> - [{K, unbound} | Acc] - end - end, - lists:foldl(Fun, Opts, AllOpts). - -process_options({[], [], _}, OptsMap) -> - OptsMap; -process_options({[], Skipped, Counter}, OptsMap) - when length(Skipped) < Counter -> - process_options({Skipped, [], length(Skipped)}, OptsMap); -process_options({[], _Skipped, _Counter}, _OptsMap) -> - throw({error, faulty_configuration}); -process_options({[{K, V} = Opt | More], Skipped, Counter}, OptsMap0) -> - case check_dependencies(K, OptsMap0) of - true -> - OptsMap1 = handle_option(K, V, OptsMap0), - process_options({More, Skipped, Counter}, OptsMap1); - false -> - process_options({More, [Opt | Skipped], Counter}, OptsMap0) - end. - -%% TODO: This is not a particularly good implementation(K => needless), it needs to be improved -handle_option(use_jwks, true, OptsMap) -> - OptsMap#{use_jwks => true, - algorithm => needless}; -handle_option(use_jwks, false, OptsMap) -> - OptsMap#{use_jwks => false, - jwks_endpoint => needless}; -handle_option(jwks_endpoint = Opt, unbound, #{use_jwks := true}) -> - throw({error, {options, {Opt, unbound}}}); -handle_option(jwks_endpoint, Value, #{use_jwks := true} = OptsMap) - when Value =/= unbound -> - case emqx_http_lib:uri_parse(Value) of - {ok, #{scheme := http}} -> - OptsMap#{enable_ssl => false, - jwks_endpoint => Value}; - {ok, #{scheme := https}} -> - OptsMap#{enable_ssl => true, - jwks_endpoint => Value}; - {error, _Reason} -> - throw({error, {options, {jwks_endpoint, Value}}}) - end; -handle_option(refresh_interval = Opt, Value0, #{use_jwks := true} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(algorithm = Opt, Value0, #{use_jwks := false} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(secret = Opt, unbound, #{algorithm := 'hmac-based'}) -> - throw({error, {options, {Opt, unbound}}}); -handle_option(secret = Opt, Value, #{algorithm := 'hmac-based'} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(secret_base64_encoded = Opt, Value0, #{algorithm := 'hmac-based'} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(jwt_certfile = Opt, unbound, #{algorithm := 'public-key'}) -> - throw({error, {options, {Opt, unbound}}}); -handle_option(jwt_certfile = Opt, Value, #{algorithm := 'public-key'} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(verify = Opt, Value0, #{enable_ssl := true} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(cacertfile = Opt, Value, #{enable_ssl := true} = OptsMap) - when Value =/= unbound -> - OptsMap#{Opt => Value}; -handle_option(certfile, unbound, #{enable_ssl := true} = OptsMap) -> - OptsMap; -handle_option(certfile = Opt, Value, #{enable_ssl := true} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(keyfile, unbound, #{enable_ssl := true} = OptsMap) -> - OptsMap; -handle_option(keyfile = Opt, Value, #{enable_ssl := true} = OptsMap) -> - OptsMap#{Opt => Value}; -handle_option(server_name_indication = Opt, Value0, #{enable_ssl := true} = OptsMap) -> - Value = validate_option(Opt, Value0), - OptsMap#{Opt => Value}; -handle_option(verify_claims = Opt, Value0, OptsMap) -> - Value = handle_verify_claims(Value0), - OptsMap#{Opt => Value}; -handle_option(_Opt, _Value, OptsMap) -> - OptsMap. - -validate_option(refresh_interval, unbound) -> - 300; -validate_option(refresh_interval, Value) when is_integer(Value) -> - Value; -validate_option(algorithm, <<"hmac-based">>) -> - 'hmac-based'; -validate_option(algorithm, <<"public-key">>) -> - 'public-key'; -validate_option(secret_base64_encoded, unbound) -> - false; -validate_option(secret_base64_encoded, Value) when is_boolean(Value) -> - Value; -validate_option(verify, unbound) -> - verify_none; -validate_option(verify, true) -> - verify_peer; -validate_option(verify, false) -> - verify_none; -validate_option(server_name_indication, unbound) -> - disable; -validate_option(server_name_indication, <<"disable">>) -> - disable; -validate_option(server_name_indication, Value) when is_list(Value) -> - Value; -validate_option(Opt, Value) -> - throw({error, {options, {Opt, Value}}}). - -handle_verify_claims(Opts0) -> - try handle_verify_claims(Opts0, []) - catch - error:_ -> - throw({error, {options, {verify_claims, Opts0}}}) - end. - -handle_verify_claims([], Acc) -> - Acc; -handle_verify_claims([{Name, Expected0} | More], Acc) - when is_binary(Name) andalso is_binary(Expected0) -> - Expected = handle_placeholder(Expected0), - handle_verify_claims(More, [{Name, Expected} | Acc]). - -handle_placeholder(Placeholder0) -> - case re:run(Placeholder0, "^\\$\\{[a-z0-9\\_]+\\}$", [{capture, all}]) of - {match, [{Offset, Length}]} -> - Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3), - Placeholder2 = validate_placeholder(Placeholder1), - {placeholder, Placeholder2}; - nomatch -> - Placeholder0 - end. - -validate_placeholder(<<"clientid">>) -> - clientid; -validate_placeholder(<<"username">>) -> - username. - -check_dependencies(Opt, OptsMap) -> - case maps:get(Opt, ?RULES) of - [] -> - true; - Deps -> - option_already_defined(Opt, OptsMap) orelse - dependecies_already_defined(Deps, OptsMap) - end. - -option_already_defined(Opt, OptsMap) -> - maps:get(Opt, OptsMap, unbound) =/= unbound. - -dependecies_already_defined(Deps, OptsMap) -> - Fun = fun(Opt) -> option_already_defined(Opt, OptsMap) end, - lists:all(Fun, Deps). diff --git a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl deleted file mode 100644 index d9f9ace8b..000000000 --- a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl +++ /dev/null @@ -1,189 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --define(AUTH, emqx_authentication). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - application:set_env(ekka, strict_mode, true), - emqx_ct_helpers:start_apps([emqx_authentication]), - Config. - -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authentication]), - ok. - -t_chain(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(#{id => ChainID})), - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), - ok. - -t_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), - - ServiceName1 = <<"myservice1">>, - ServiceParams1 = #{name => ServiceName1, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), - ?assertEqual({ok, ServiceParams1}, ?AUTH:lookup_service(ChainID, ServiceName1)), - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services(ChainID, [ServiceParams1])), - - ServiceName2 = <<"myservice2">>, - ServiceParams2 = ServiceParams1#{name => ServiceName2}, - ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), - ?assertMatch({ok, #{id := ChainID, services := [ServiceParams1, ServiceParams2]}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual({ok, ServiceParams2}, ?AUTH:lookup_service(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), - - ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_end(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 1)), - ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 0)), - ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName1, ServiceName2])), - ?assertEqual({ok, []}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - -t_mnesia_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName = <<"myservice">>, - ServiceParams = #{name => ServiceName, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - - UserInfo = #{<<"user_id">> => <<"myuser">>, - <<"password">> => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - ClientInfo = #{chain_id => ChainID, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), - ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), - ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), - UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, ServiceName, <<"myuser">>, UserInfo2)), - ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), - ?assertEqual(ok, ?AUTH:delete_user(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName])), - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertEqual([], ets:tab2list(mnesia_basic_auth)), - ok. - -t_import(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName = <<"myservice">>, - ServiceParams = #{name => ServiceName, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - - Dir = code:lib_dir(emqx_authentication, test), - ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser3">>)), - ClientInfo1 = #{chain_id => ChainID, - username => <<"myuser1">>, - password => <<"mypassword1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, - password => <<"mypassword3">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - -t_multi_mnesia_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName1 = <<"myservice1">>, - ServiceParams1 = #{name => ServiceName1, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ServiceName2 = <<"myservice2">>, - ServiceParams2 = #{name => ServiceName2, - type => mnesia, - params => #{ - user_id_type => <<"clientid">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), - ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), - - ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(ChainID, ServiceName1, - #{<<"user_id">> => <<"myuser">>, - <<"password">> => <<"mypass1">>})), - ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(ChainID, ServiceName2, - #{<<"user_id">> => <<"myclient">>, - <<"password">> => <<"mypass2">>})), - ClientInfo1 = #{chain_id => ChainID, - username => <<"myuser">>, - clientid => <<"myclient">>, - password => <<"mypass1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), - ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv similarity index 100% rename from apps/emqx_authentication/data/user-credentials.csv rename to apps/emqx_authn/data/user-credentials.csv diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json similarity index 100% rename from apps/emqx_authentication/data/user-credentials.json rename to apps/emqx_authn/data/user-credentials.json diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf new file mode 100644 index 000000000..092bd5404 --- /dev/null +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -0,0 +1,26 @@ +authn: { + chains: [ + # { + # id: "chain1" + # type: simple + # authenticators: [ + # { + # name: "authenticator1" + # type: built-in-database + # config: { + # user_id_type: clientid + # password_hash_algorithm: { + # name: sha256 + # } + # } + # } + # ] + # } + ] + bindings: [ + # { + # chain_id: "chain1" + # listeners: ["mqtt-tcp", "mqtt-ssl"] + # } + ] +} diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl new file mode 100644 index 000000000..46c1cf7ca --- /dev/null +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -0,0 +1,67 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 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. +%%-------------------------------------------------------------------- + +-define(APP, emqx_authn). + +-type chain_id() :: binary(). +-type authn_type() :: simple | enhanced. +-type authenticator_name() :: binary(). +-type authenticator_type() :: mnesia | jwt | mysql | postgresql. +-type listener_id() :: binary(). + +-record(authenticator, + { name :: authenticator_name() + , type :: authenticator_type() + , provider :: module() + , config :: map() + , state :: map() + }). + +-record(chain, + { id :: chain_id() + , type :: authn_type() + , authenticators :: [{authenticator_name(), #authenticator{}}] + , created_at :: integer() + }). + +-record(binding, + { bound :: {listener_id(), authn_type()} + , chain_id :: chain_id() + }). + +-define(AUTH_SHARD, emqx_authn_shard). + +-define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)). + +-define(CLUSTER_CALL(Module, Func, Args, ResParttern), + fun() -> + case LocalResult = erlang:apply(Module, Func, Args) of + ResParttern -> + Nodes = nodes(), + {ResL, BadNodes} = rpc:multicall(Nodes, Module, Func, Args, 5000), + NResL = lists:zip(Nodes - BadNodes, ResL), + Errors = lists:filter(fun({_, ResParttern}) -> false; + (_) -> true + end, NResL), + OtherErrors = [{BadNode, node_does_not_exist} || BadNode <- BadNodes], + case Errors ++ OtherErrors of + [] -> LocalResult; + NErrors -> {error, NErrors} + end; + ErrorResult -> + {error, ErrorResult} + end + end()). diff --git a/apps/emqx_authentication/rebar.config b/apps/emqx_authn/rebar.config similarity index 100% rename from apps/emqx_authentication/rebar.config rename to apps/emqx_authn/rebar.config diff --git a/apps/emqx_authentication/src/emqx_authentication.app.src b/apps/emqx_authn/src/emqx_authn.app.src similarity index 63% rename from apps/emqx_authentication/src/emqx_authentication.app.src rename to apps/emqx_authn/src/emqx_authn.app.src index 4f55ca0a7..c997582ec 100644 --- a/apps/emqx_authentication/src/emqx_authentication.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -1,10 +1,10 @@ -{application, emqx_authentication, +{application, emqx_authn, [{description, "EMQ X Authentication"}, {vsn, "0.1.0"}, {modules, []}, - {registered, [emqx_authentication_sup, emqx_authentication_registry]}, + {registered, [emqx_authn_sup, emqx_authn_registry]}, {applications, [kernel,stdlib]}, - {mod, {emqx_authentication_app,[]}}, + {mod, {emqx_authn_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, {maintainers, ["EMQ X Team "]}, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl new file mode 100644 index 000000000..24bdb21e7 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -0,0 +1,490 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn). + +-include("emqx_authn.hrl"). + +-export([ enable/0 + , disable/0 + ]). + +-export([authenticate/1]). + +-export([ create_chain/1 + , delete_chain/1 + , lookup_chain/1 + , list_chains/0 + , bind/2 + , unbind/2 + , list_bindings/1 + , list_bound_chains/1 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/3 + , lookup_authenticator/2 + , list_authenticators/1 + , move_authenticator_to_the_front/2 + , move_authenticator_to_the_end/2 + , move_authenticator_to_the_nth/3 + ]). + +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 + ]). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). + +-define(CHAIN_TAB, emqx_authn_chain). +-define(BINDING_TAB, emqx_authn_binding). + +-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). +-rlog_shard({?AUTH_SHARD, ?BINDING_TAB}). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot) -> ok). +mnesia(boot) -> + %% Optimize storage + StoreProps = [{ets, [{read_concurrency, true}]}], + %% Chain table + ok = ekka_mnesia:create_table(?CHAIN_TAB, [ + {ram_copies, [node()]}, + {record_name, chain}, + {local_content, true}, + {attributes, record_info(fields, chain)}, + {storage_properties, StoreProps}]), + %% Binding table + ok = ekka_mnesia:create_table(?BINDING_TAB, [ + {ram_copies, [node()]}, + {record_name, binding}, + {local_content, true}, + {attributes, record_info(fields, binding)}, + {storage_properties, StoreProps}]). + +enable() -> + case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of + ok -> ok; + {error, already_exists} -> ok + end. + +disable() -> + emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), + ok. + +authenticate(#{listener_id := ListenerID} = ClientInfo) -> + case lookup_chain_by_listener(ListenerID, simple) of + {error, _} -> + {error, no_authenticators}; + {ok, ChainID} -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [#chain{authenticators = []}] -> + {error, no_authenticators}; + [#chain{authenticators = Authenticators}] -> + do_authenticate(Authenticators, ClientInfo); + [] -> + {error, no_authenticators} + end + end. + +do_authenticate([], _) -> + {error, user_not_found}; +do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], ClientInfo) -> + case Provider:authenticate(ClientInfo, State) of + ignore -> do_authenticate(More, ClientInfo); + ok -> ok; + {ok, NewClientInfo} -> {ok, NewClientInfo}; + {stop, Reason} -> {error, Reason} + end. + +create_chain(#{id := ID, + type := Type}) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + Chain = #chain{id = ID, + type = Type, + authenticators = [], + created_at = erlang:system_time(millisecond)}, + mnesia:write(?CHAIN_TAB, Chain, write), + {ok, serialize_chain(Chain)}; + [_ | _] -> + {error, {already_exists, {chain, ID}}} + end + end). + +delete_chain(ID) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + {error, {not_found, {chain, ID}}}; + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators], + mnesia:delete(?CHAIN_TAB, ID, write) + end + end). + +lookup_chain(ID) -> + case mnesia:dirty_read(?CHAIN_TAB, ID) of + [] -> + {error, {not_found, {chain, ID}}}; + [Chain] -> + {ok, serialize_chain(Chain)} + end. + +list_chains() -> + Chains = ets:tab2list(?CHAIN_TAB), + {ok, [serialize_chain(Chain) || Chain <- Chains]}. + +bind(ChainID, Listeners) -> + %% TODO: ensure listener id is valid + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{type = AuthNType}] -> + Result = lists:foldl( + fun(ListenerID, Acc) -> + case mnesia:read(?BINDING_TAB, {ListenerID, AuthNType}, write) of + [] -> + Binding = #binding{bound = {ListenerID, AuthNType}, chain_id = ChainID}, + mnesia:write(?BINDING_TAB, Binding, write), + Acc; + _ -> + [ListenerID | Acc] + end + end, [], Listeners), + case Result of + [] -> ok; + Listeners0 -> {error, {already_bound, Listeners0}} + end + end + end). + +unbind(ChainID, Listeners) -> + trans( + fun() -> + Result = lists:foldl( + fun(ListenerID, Acc) -> + MatchSpec = [{{binding, {ListenerID, '_'}, ChainID}, [], ['$_']}], + case mnesia:select(?BINDING_TAB, MatchSpec, write) of + [] -> + [ListenerID | Acc]; + [#binding{bound = Bound}] -> + mnesia:delete(?BINDING_TAB, Bound, write), + Acc + end + end, [], Listeners), + case Result of + [] -> ok; + Listeners0 -> + {error, {not_found, Listeners0}} + end + end). + +list_bindings(ChainID) -> + trans( + fun() -> + MatchSpec = [{{binding, {'$1', '_'}, ChainID}, [], ['$1']}], + Listeners = mnesia:select(?BINDING_TAB, MatchSpec), + {ok, #{chain_id => ChainID, listeners => Listeners}} + end). + +list_bound_chains(ListenerID) -> + trans( + fun() -> + MatchSpec = [{{binding, {ListenerID, '_'}, '_'}, [], ['$_']}], + Bindings = mnesia:select(?BINDING_TAB, MatchSpec), + Chains = [{AuthNType, ChainID} || #binding{bound = {_, AuthNType}, + chain_id = ChainID} <- Bindings], + {ok, maps:from_list(Chains)} + end). + +create_authenticator(ChainID, #{name := Name, + type := Type, + config := Config}) -> + UpdateFun = + fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) -> + case lists:keymember(Name, 1, Authenticators) of + true -> + {error, {already_exists, {authenticator, Name}}}; + false -> + Provider = authenticator_provider(AuthNType, Type), + case Provider:create(ChainID, Name, Config) of + {ok, State} -> + Authenticator = #authenticator{name = Name, + type = Type, + provider = Provider, + config = Config, + state = State}, + NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]}, + ok = mnesia:write(?CHAIN_TAB, NChain, write), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + +delete_authenticator(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case lists:keytake(AuthenticatorName, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + {value, {_, Authenticator}, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write) + end + end, + update_chain(ChainID, UpdateFun). + +update_authenticator(ChainID, AuthenticatorName, Config) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + #authenticator{provider = Provider, + config = OriginalConfig, + state = State} = Authenticator -> + NewConfig = maps:merge(OriginalConfig, Config), + case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of + {ok, NState} -> + NAuthenticator = Authenticator#authenticator{config = NewConfig, + state = NState}, + NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators), + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), + {ok, serialize_authenticator(NAuthenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + +lookup_authenticator(ChainID, AuthenticatorName) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + Authenticator -> + {ok, serialize_authenticator(Authenticator)} + end + end. + +list_authenticators(ChainID) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + {ok, serialize_authenticators(Authenticators)} + end. + +move_authenticator_to_the_front(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_front_(AuthenticatorName, Authenticators) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_authenticator_to_the_end(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_end_(AuthenticatorName, Authenticators) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +import_users(ChainID, AuthenticatorName, Filename) -> + call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]). + +add_user(ChainID, AuthenticatorName, UserInfo) -> + call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]). + +delete_user(ChainID, AuthenticatorName, UserID) -> + call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]). + +update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) -> + call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]). + +lookup_user(ChainID, AuthenticatorName, UserID) -> + call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]). + +list_users(ChainID, AuthenticatorName) -> + call_authenticator(ChainID, AuthenticatorName, list_users, []). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +authenticator_provider(simple, 'built-in-database') -> emqx_authn_mnesia; +authenticator_provider(simple, jwt) -> emqx_authn_jwt; +authenticator_provider(simple, mysql) -> emqx_authn_mysql; +authenticator_provider(simple, postgresql) -> emqx_authn_pgsql. + +% authenticator_provider(enhanced, 'enhanced-built-in-database') -> emqx_enhanced_authn_mnesia. + +do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> + Provider:destroy(State). + +update_value(Key, Value, List) -> + lists:keyreplace(Key, 1, List, {Key, Value}). + +move_authenticator_to_the_front_(AuthenticatorName, Authenticators) -> + move_authenticator_to_the_front_(AuthenticatorName, Authenticators, []). + +move_authenticator_to_the_front_(AuthenticatorName, [], _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_front_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> + {ok, [Authenticator | (lists:reverse(Passed) ++ More)]}; +move_authenticator_to_the_front_(AuthenticatorName, [Authenticator | More], Passed) -> + move_authenticator_to_the_front_(AuthenticatorName, More, [Authenticator | Passed]). + +move_authenticator_to_the_end_(AuthenticatorName, Authenticators) -> + move_authenticator_to_the_end_(AuthenticatorName, Authenticators, []). + +move_authenticator_to_the_end_(AuthenticatorName, [], _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_end_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> + {ok, lists:reverse(Passed) ++ More ++ [Authenticator]}; +move_authenticator_to_the_end_(AuthenticatorName, [Authenticator | More], Passed) -> + move_authenticator_to_the_end_(AuthenticatorName, More, [Authenticator | Passed]). + +move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) + when N =< length(Authenticators) andalso N > 0 -> + move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N, []); +move_authenticator_to_the_nth_(_, _, _) -> + {error, out_of_range}. + +move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) + when N =< length(Passed) -> + {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), + {ok, L1 ++ [Authenticator] ++ L2 ++ More}; +move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) -> + {L1, L2} = lists:split(N - length(Passed) - 1, More), + {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; +move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) -> + move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]). + +update_chain(ChainID, UpdateFun) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [Chain] -> + UpdateFun(Chain) + end + end). + +lookup_chain_by_listener(ListenerID, AuthNType) -> + case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of + [] -> + {error, not_found}; + [#binding{chain_id = ChainID}] -> + {ok, ChainID} + end. + + +call_authenticator(ChainID, AuthenticatorName, Func, Args) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + #authenticator{provider = Provider, state = State} -> + case erlang:function_exported(Provider, Func, length(Args) + 1) of + true -> + erlang:apply(Provider, Func, Args ++ [State]); + false -> + {error, unsupported_feature} + end + end + end. + +serialize_chain(#chain{id = ID, + type = Type, + authenticators = Authenticators, + created_at = CreatedAt}) -> + #{id => ID, + type => Type, + authenticators => serialize_authenticators(Authenticators), + created_at => CreatedAt}. + +% serialize_binding(#binding{bound = {ListenerID, _}, +% chain_id = ChainID}) -> +% #{listener_id => ListenerID, +% chain_id => ChainID}. + +serialize_authenticators(Authenticators) -> + [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. + +serialize_authenticator(#authenticator{name = Name, + type = Type, + config = Config}) -> + #{name => Name, + type => Type, + config => Config}. + +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl new file mode 100644 index 000000000..ad9542958 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -0,0 +1,544 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_api). + +-include("emqx_authn.hrl"). + +-export([ create_chain/2 + , delete_chain/2 + , lookup_chain/2 + , list_chains/2 + , bind/2 + , unbind/2 + , list_bindings/2 + , list_bound_chains/2 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/2 + , lookup_authenticator/2 + , list_authenticators/2 + , move_authenticator/2 + , import_users/2 + , add_user/2 + , delete_user/2 + , update_user/2 + , lookup_user/2 + , list_users/2 + ]). + +-import(minirest, [return/1]). + +-rest_api(#{name => create_chain, + method => 'POST', + path => "/authentication/chains", + func => create_chain, + descr => "Create a chain" + }). + +-rest_api(#{name => delete_chain, + method => 'DELETE', + path => "/authentication/chains/:bin:id", + func => delete_chain, + descr => "Delete chain" + }). + +-rest_api(#{name => lookup_chain, + method => 'GET', + path => "/authentication/chains/:bin:id", + func => lookup_chain, + descr => "Lookup chain" + }). + +-rest_api(#{name => list_chains, + method => 'GET', + path => "/authentication/chains", + func => list_chains, + descr => "List all chains" + }). + +-rest_api(#{name => bind, + method => 'POST', + path => "/authentication/chains/:bin:id/bindings/bulk", + func => bind, + descr => "Bind" + }). + +-rest_api(#{name => unbind, + method => 'DELETE', + path => "/authentication/chains/:bin:id/bindings/bulk", + func => unbind, + descr => "Unbind" + }). + +-rest_api(#{name => list_bindings, + method => 'GET', + path => "/authentication/chains/:bin:id/bindings", + func => list_bindings, + descr => "List bindings" + }). + +-rest_api(#{name => list_bound_chains, + method => 'GET', + path => "/authentication/listeners/:bin:listener_id/bound_chains", + func => list_bound_chains, + descr => "List bound chains" + }). + +-rest_api(#{name => create_authenticator, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators", + func => create_authenticator, + descr => "Create authenticator to chain" + }). + +-rest_api(#{name => delete_authenticator, + method => 'DELETE', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => delete_authenticator, + descr => "Delete authenticator from chain" + }). + +-rest_api(#{name => update_authenticator, + method => 'PUT', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => update_authenticator, + descr => "Update authenticator in chain" + }). + +-rest_api(#{name => lookup_authenticator, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => lookup_authenticator, + descr => "Lookup authenticator in chain" + }). + +-rest_api(#{name => list_authenticators, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators", + func => list_authenticators, + descr => "List authenticators in chain" + }). + +-rest_api(#{name => move_authenticator, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position", + func => move_authenticator, + descr => "Change the order of authenticators" + }). + +-rest_api(#{name => import_users, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/import-users", + func => import_users, + descr => "Import users" + }). + +-rest_api(#{name => add_user, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + func => add_user, + descr => "Add user" + }). + +-rest_api(#{name => delete_user, + method => 'DELETE', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => delete_user, + descr => "Delete user" + }). + +-rest_api(#{name => update_user, + method => 'PUT', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => update_user, + descr => "Update user" + }). + +-rest_api(#{name => lookup_user, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => lookup_user, + descr => "Lookup user" + }). + +%% TODO: Support pagination +-rest_api(#{name => list_users, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + func => list_users, + descr => "List all users" + }). + +create_chain(Binding, Params) -> + do_create_chain(uri_decode(Binding), maps:from_list(Params)). + +do_create_chain(_Binding, Chain0) -> + Config = #{<<"authn">> => #{<<"chains">> => [Chain0#{<<"authenticators">> => []}], + <<"bindings">> => []}}, + #{authn := #{chains := [Chain1]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_chain(Chain1) of + {ok, Chain2} -> + return({ok, Chain2}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_chain(Binding, Params) -> + do_delete_chain(uri_decode(Binding), maps:from_list(Params)). + +do_delete_chain(#{id := ChainID}, _Params) -> + case emqx_authn:delete_chain(ChainID) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_chain(Binding, Params) -> + do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_chain(#{id := ChainID}, _Params) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, Chain} -> + return({ok, Chain}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_chains(Binding, Params) -> + do_list_chains(uri_decode(Binding), maps:from_list(Params)). + +do_list_chains(_Binding, _Params) -> + {ok, Chains} = emqx_authn:list_chains(), + return({ok, Chains}). + +bind(Binding, Params) -> + do_bind(uri_decode(Binding), lists_to_map(Params)). + +do_bind(#{id := ChainID}, #{<<"listeners">> := Listeners}) -> + % Config = #{<<"authn">> => #{<<"chains">> => [], + % <<"bindings">> => [#{<<"chain">> := ChainID, + % <<"listeners">> := Listeners}]}}, + % #{authn := #{bindings := [#{listeners := Listeners}]}} + % = hocon_schema:check_plain(emqx_authn_schema, Config, + % #{atom_key => true, nullable => true}), + case emqx_authn:bind(ChainID, Listeners) of + ok -> + return(ok); + {error, {alread_bound, Listeners}} -> + {ok, #{code => <<"ALREADY_EXISTS">>, + message => <<"ALREADY_BOUND">>, + detail => Listeners}}; + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_bind(_, _) -> + return(serialize_error({missing_parameter, <<"listeners">>})). + +unbind(Binding, Params) -> + do_unbind(uri_decode(Binding), lists_to_map(Params)). + +do_unbind(#{id := ChainID}, #{<<"listeners">> := Listeners0}) -> + case emqx_authn:unbind(ChainID, Listeners0) of + ok -> + return(ok); + {error, {not_found, Listeners1}} -> + {ok, #{code => <<"NOT_FOUND">>, + detail => Listeners1}}; + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_unbind(_, _) -> + return(serialize_error({missing_parameter, <<"listeners">>})). + +list_bindings(Binding, Params) -> + do_list_bindings(uri_decode(Binding), lists_to_map(Params)). + +do_list_bindings(#{id := ChainID}, _) -> + {ok, Binding} = emqx_authn:list_bindings(ChainID), + return({ok, Binding}). + +list_bound_chains(Binding, Params) -> + do_list_bound_chains(uri_decode(Binding), lists_to_map(Params)). + +do_list_bound_chains(#{listener_id := ListenerID}, _) -> + {ok, Chains} = emqx_authn:list_bound_chains(ListenerID), + return({ok, Chains}). + +create_authenticator(Binding, Params) -> + do_create_authenticator(uri_decode(Binding), lists_to_map(Params)). + +do_create_authenticator(#{id := ChainID}, Authenticator0) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, #{type := Type}} -> + Chain = #{<<"id">> => ChainID, + <<"type">> => Type, + <<"authenticators">> => [Authenticator0]}, + Config = #{<<"authn">> => #{<<"chains">> => [Chain], + <<"bindings">> => []}}, + #{authn := #{chains := [#{authenticators := [Authenticator1]}]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_authenticator(ChainID, Authenticator1) of + {ok, Authenticator2} -> + return({ok, Authenticator2}); + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_authenticator(Binding, Params) -> + do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_delete_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:delete_authenticator(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +%% TODO: Support incremental update +update_authenticator(Binding, Params) -> + do_update_authenticator(uri_decode(Binding), lists_to_map(Params)). + +%% TOOD: PUT method supports creation and update +do_update_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, AuthenticatorConfig0) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, #{type := ChainType}} -> + case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of + {ok, #{type := Type}} -> + Authenticator = #{<<"name">> => AuthenticatorName, + <<"type">> => Type, + <<"config">> => AuthenticatorConfig0}, + Chain = #{<<"id">> => ChainID, + <<"type">> => ChainType, + <<"authenticators">> => [Authenticator]}, + Config = #{<<"authn">> => #{<<"chains">> => [Chain], + <<"bindings">> => []}}, + #{ + authn := #{ + chains := [#{ + authenticators := [#{ + config := AuthenticatorConfig1 + }] + }] + } + } = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:update_authenticator(ChainID, AuthenticatorName, AuthenticatorConfig1) of + {ok, NAuthenticator} -> + return({ok, NAuthenticator}); + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_authenticator(Binding, Params) -> + do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of + {ok, Authenticator} -> + return({ok, Authenticator}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_authenticators(Binding, Params) -> + do_list_authenticators(uri_decode(Binding), maps:from_list(Params)). + +do_list_authenticators(#{id := ChainID}, _Params) -> + case emqx_authn:list_authenticators(ChainID) of + {ok, Authenticators} -> + return({ok, Authenticators}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +move_authenticator(Binding, Params) -> + do_move_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the front">>}) -> + case emqx_authn:move_authenticator_to_the_front(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the end">>}) -> + case emqx_authn:move_authenticator_to_the_end(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := N}) when is_number(N) -> + case emqx_authn:move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(_Binding, _Params) -> + return(serialize_error({missing_parameter, <<"position">>})). + +import_users(Binding, Params) -> + do_import_users(uri_decode(Binding), maps:from_list(Params)). + +do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName}, + #{<<"filename">> := Filename}) -> + case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_import_users(_Binding, Params) -> + Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]), + return(serialize_error({missing_parameter, Missed})). + +add_user(Binding, Params) -> + do_add_user(uri_decode(Binding), maps:from_list(Params)). + +do_add_user(#{id := ChainID, + authenticator_name := AuthenticatorName}, UserInfo) -> + case emqx_authn:add_user(ChainID, AuthenticatorName, UserInfo) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_user(Binding, Params) -> + do_delete_user(uri_decode(Binding), maps:from_list(Params)). + +do_delete_user(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, _Params) -> + case emqx_authn:delete_user(ChainID, AuthenticatorName, UserID) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +update_user(Binding, Params) -> + do_update_user(uri_decode(Binding), maps:from_list(Params)). + +do_update_user(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, NewUserInfo) -> + case emqx_authn:update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_user(Binding, Params) -> + do_lookup_user(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_user(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, _Params) -> + case emqx_authn:lookup_user(ChainID, AuthenticatorName, UserID) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_users(Binding, Params) -> + do_list_users(uri_decode(Binding), maps:from_list(Params)). + +do_list_users(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:list_users(ChainID, AuthenticatorName) of + {ok, Users} -> + return({ok, Users}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +uri_decode(Params) -> + maps:fold(fun(K, V, Acc) -> + Acc#{K => emqx_http_lib:uri_decode(V)} + end, #{}, Params). + +lists_to_map(L) -> + lists_to_map(L, #{}). + +lists_to_map([], Acc) -> + Acc; +lists_to_map([{K, V} | More], Acc) when is_list(V) -> + NV = lists_to_map(V), + lists_to_map(More, Acc#{K => NV}); +lists_to_map([{K, V} | More], Acc) -> + lists_to_map(More, Acc#{K => V}); +lists_to_map([_ | _] = L, _) -> + L. + +serialize_error({already_exists, {Type, ID}}) -> + {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))}; +serialize_error({not_found, {Type, ID}}) -> + {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))}; +serialize_error({duplicate, Name}) -> + {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Authenticator name '~s' is duplicated", [Name]))}; +serialize_error({missing_parameter, Names = [_ | Rest]}) -> + Format = ["~s," || _ <- Rest] ++ ["~s"], + NFormat = binary_to_list(iolist_to_binary(Format)), + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; +serialize_error({missing_parameter, Name}) -> + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))}; +serialize_error(_) -> + {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. + +serialize_type(authenticator) -> + "Authenticator"; +serialize_type(chain) -> + "Chain"; +serialize_type(authenticator_type) -> + "Authenticator type". + +get_missed_params(Actual, Expected) -> + Keys = lists:foldl(fun(Key, Acc) -> + case maps:is_key(Key, Actual) of + true -> Acc; + false -> [Key | Acc] + end + end, [], Expected), + lists:reverse(Keys). diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl new file mode 100644 index 000000000..9e60e5dda --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -0,0 +1,80 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_app). + +-include("emqx_authn.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-behaviour(application). + +-emqx_plugin(?MODULE). + +%% Application callbacks +-export([ start/2 + , stop/1 + ]). + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_authn_sup:start_link(), + ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), + initialize(), + {ok, Sup}. + +stop(_State) -> + ok. + +initialize() -> + ConfFile = filename:join([emqx:get_env(plugins_etc_dir), ?APP]) ++ ".conf", + {ok, RawConfig} = hocon:load(ConfFile), + #{authn := #{chains := Chains, + bindings := Bindings}} + = hocon_schema:check_plain(emqx_authn_schema, RawConfig, #{atom_key => true, nullable => true}), + initialize_chains(Chains), + initialize_bindings(Bindings). + +initialize_chains([]) -> + ok; +initialize_chains([#{id := ChainID, + type := Type, + authenticators := Authenticators} | More]) -> + case emqx_authn:create_chain(#{id => ChainID, + type => Type}) of + {ok, _} -> + initialize_authenticators(ChainID, Authenticators), + initialize_chains(More); + {error, Reason} -> + ?LOG(error, "Failed to create chain '~s': ~p", [ChainID, Reason]) + end. + +initialize_authenticators(_ChainID, []) -> + ok; +initialize_authenticators(ChainID, [#{name := Name} = Authenticator | More]) -> + case emqx_authn:create_authenticator(ChainID, Authenticator) of + {ok, _} -> + initialize_authenticators(ChainID, More); + {error, Reason} -> + ?LOG(error, "Failed to create authenticator '~s' in chain '~s': ~p", [Name, ChainID, Reason]) + end. + +initialize_bindings([]) -> + ok; +initialize_bindings([#{chain_id := ChainID, listeners := Listeners} | More]) -> + case emqx_authn:bind(Listeners, ChainID) of + ok -> initialize_bindings(More); + {error, Reason} -> + ?LOG(error, "Failed to bind: ~p", [Reason]) + end. diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl new file mode 100644 index 000000000..0464350dd --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -0,0 +1,114 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_schema). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([structs/0, fields/1]). + +-reflect_type([ chain_id/0 + , authenticator_name/0 + ]). + +structs() -> [authn]. + +fields(authn) -> + [ {chains, fun chains/1} + , {bindings, fun bindings/1}]; + +fields('simple-chain') -> + [ {id, fun chain_id/1} + , {type, {enum, [simple]}} + , {authenticators, fun simple_authenticators/1} + ]; + +% fields('enhanced-chain') -> +% [ {id, fun chain_id/1} +% , {type, {enum, [enhanced]}} +% , {authenticators, fun enhanced_authenticators/1} +% ]; + +fields(binding) -> + [ {chain_id, fun chain_id/1} + , {listeners, fun listeners/1} + ]; + +fields('built-in-database') -> + [ {name, fun authenticator_name/1} + , {type, {enum, ['built-in-database']}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_mnesia, config))} + ]; + +% fields('enhanced-built-in-database') -> +% [ {name, fun authenticator_name/1} +% , {type, {enum, ['built-in-database']}} +% , {config, hoconsc:t(hoconsc:ref(emqx_enhanced_authn_mnesia, config))} +% ]; + +fields(jwt) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [jwt]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_jwt, config))} + ]; + +fields(mysql) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [mysql]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_mysql, config))} + ]; + +fields(pgsql) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [postgresql]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))} + ]. + +chains(type) -> hoconsc:array({union, [hoconsc:ref('simple-chain')]}); +chains(default) -> []; +chains(_) -> undefined. + +chain_id(type) -> chain_id(); +chain_id(nullable) -> false; +chain_id(_) -> undefined. + +simple_authenticators(type) -> + hoconsc:array({union, [ hoconsc:ref('built-in-database') + , hoconsc:ref(jwt) + , hoconsc:ref(mysql) + , hoconsc:ref(pgsql)]}); +simple_authenticators(default) -> []; +simple_authenticators(_) -> undefined. + +% enhanced_authenticators(type) -> +% hoconsc:array({union, [hoconsc:ref('enhanced-built-in-database')]}); +% enhanced_authenticators(default) -> []; +% enhanced_authenticators(_) -> undefined. + +authenticator_name(type) -> authenticator_name(); +authenticator_name(nullable) -> false; +authenticator_name(_) -> undefined. + +bindings(type) -> hoconsc:array(hoconsc:ref(binding)); +bindings(default) -> []; +bindings(_) -> undefined. + +listeners(type) -> hoconsc:array(binary()); +listeners(default) -> []; +listeners(_) -> undefined. diff --git a/apps/emqx_authentication/src/emqx_authentication_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl similarity index 96% rename from apps/emqx_authentication/src/emqx_authentication_sup.erl rename to apps/emqx_authn/src/emqx_authn_sup.erl index 06e12ce6c..bb26af0ad 100644 --- a/apps/emqx_authentication/src/emqx_authentication_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_sup). +-module(emqx_authn_sup). -behaviour(supervisor). diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl new file mode 100644 index 000000000..98e27e76c --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_utils.erl @@ -0,0 +1,55 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_utils). + +-export([ replace_placeholder/2 + ]). + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +replace_placeholder(PlaceHolders, Data) -> + replace_placeholder(PlaceHolders, Data, []). + +replace_placeholder([], _Data, Acc) -> + lists:reverse(Acc); +replace_placeholder([<<"${mqtt-username}">> | More], #{username := Username} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(Username) | Acc]); +replace_placeholder([<<"${mqtt-clientid}">> | More], #{clientid := ClientID} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(ClientID) | Acc]); +replace_placeholder([<<"${ip-address}">> | More], #{peerhost := IPAddress} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(IPAddress) | Acc]); +replace_placeholder([<<"${cert-subject}">> | More], #{dn := Subject} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(Subject) | Acc]); +replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(CommonName) | Acc]); +replace_placeholder([_ | More], Data, Acc) -> + replace_placeholder(More, Data, [null | Acc]). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +convert_to_sql_param(undefined) -> + null; +convert_to_sql_param(V) -> + bin(V). + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl similarity index 63% rename from apps/emqx_authentication/src/emqx_authentication_app.erl rename to apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl index 3bea3c3d6..207e93495 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl @@ -14,24 +14,4 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_app). - --behaviour(application). - --emqx_plugin(?MODULE). - --include("emqx_authentication.hrl"). - -%% Application callbacks --export([ start/2 - , stop/1 - ]). - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_authentication_sup:start_link(), - ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), - ok = emqx_authentication:register_service_types(), - {ok, Sup}. - -stop(_State) -> - ok. +-module(emqx_enhanced_authn_mnesia). diff --git a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl similarity index 87% rename from apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl index 9dafc9f5e..95e4b3d6d 100644 --- a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_jwks_connector). +-module(emqx_authn_jwks_connector). -behaviour(gen_server). @@ -125,23 +125,17 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -handle_options(Opts) -> - #{endpoint => proplists:get_value(jwks_endpoint, Opts), - refresh_interval => limit_refresh_interval(proplists:get_value(refresh_interval, Opts)), - ssl_opts => get_ssl_opts(Opts), +handle_options(#{endpoint := Endpoint, + refresh_interval := RefreshInterval0, + ssl_opts := SSLOpts}) -> + #{endpoint => Endpoint, + refresh_interval => limit_refresh_interval(RefreshInterval0), + ssl_opts => maps:to_list(SSLOpts), jwks => [], - request_id => undefined}. + request_id => undefined}; -get_ssl_opts(Opts) -> - case proplists:get_value(enable_ssl, Opts) of - false -> []; - true -> - maps:to_list(maps:with([cacertfile, - keyfile, - certfile, - verify, - server_name_indication], maps:from_list(Opts))) - end. +handle_options(#{enable_ssl := false} = Opts) -> + handle_options(Opts#{ssl_opts => #{}}). refresh_jwks(#{endpoint := Endpoint, ssl_opts := SSLOpts} = State) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl new file mode 100644 index 000000000..f737d5168 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -0,0 +1,343 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_jwt). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + , validations/0 + ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields("") -> + [{config, {union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + , hoconsc:t('jwks-using-ssl') + ]}}]; + +fields(config) -> + [{union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + , hoconsc:t('jwks-using-ssl') + ]}]; + +fields('hmac-based') -> + [ {use_jwks, {enum, [false]}} + , {algorithm, {enum, ['hmac-based']}} + , {secret, fun secret/1} + , {secret_base64_encoded, fun secret_base64_encoded/1} + , {verify_claims, fun verify_claims/1} + ]; + +fields('public-key') -> + [ {use_jwks, {enum, [false]}} + , {algorithm, {enum, ['public-key']}} + , {certificate, fun certificate/1} + , {verify_claims, fun verify_claims/1} + ]; + +fields('jwks') -> + [ {enable_ssl, {enum, [false]}} + ] ++ jwks_fields(); + +fields('jwks-using-ssl') -> + [ {enable_ssl, {enum, [true]}} + , {ssl_opts, fun ssl_opts/1} + ] ++ jwks_fields(); + +fields(ssl_opts) -> + [ {cacertfile, fun cacertfile/1} + , {certfile, fun certfile/1} + , {keyfile, fun keyfile/1} + , {verify, fun verify/1} + , {server_name_indication, fun server_name_indication/1} + ]; + +fields(claim) -> + [ {"$name", fun expected_claim_value/1} ]. + +validations() -> + [ {check_verify_claims, fun check_verify_claims/1} ]. + +jwks_fields() -> + [ {use_jwks, {enum, [true]}} + , {endpoint, fun endpoint/1} + , {refresh_interval, fun refresh_interval/1} + , {verify_claims, fun verify_claims/1} + ]. + +secret(type) -> string(); +secret(_) -> undefined. + +secret_base64_encoded(type) -> boolean(); +secret_base64_encoded(defualt) -> false; +secret_base64_encoded(_) -> undefined. + +certificate(type) -> string(); +certificate(_) -> undefined. + +endpoint(type) -> string(); +endpoint(_) -> undefined. + +ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); +ssl_opts(default) -> []; +ssl_opts(_) -> undefined. + +refresh_interval(type) -> integer(); +refresh_interval(default) -> 300; +refresh_interval(validator) -> [fun(I) -> I > 0 end]; +refresh_interval(_) -> undefined. + +cacertfile(type) -> string(); +cacertfile(_) -> undefined. + +certfile(type) -> string(); +certfile(_) -> undefined. + +keyfile(type) -> string(); +keyfile(_) -> undefined. + +verify(type) -> boolean(); +verify(default) -> false; +verify(_) -> undefined. + +server_name_indication(type) -> string(); +server_name_indication(_) -> undefined. + +verify_claims(type) -> hoconsc:array(hoconsc:ref(claim)); +verify_claims(default) -> []; +verify_claims(_) -> undefined. + +expected_claim_value(type) -> string(); +expected_claim_value(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(_ChainID, _AuthenticatorName, Config) -> + create(Config). + +update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, #{jwk := Connector}) + when is_pid(Connector) -> + _ = emqx_authn_jwks_connector:stop(Connector), + create(Config); + +update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) -> + create(Config); + +update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Connector} = State) + when is_pid(Connector) -> + ok = emqx_authn_jwks_connector:update(Connector, Config), + case maps:get(verify_cliams, Config, undefined) of + undefined -> + {ok, State}; + VerifyClaims -> + {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}} + end; + +update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> + create(Config). + +authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, + verify_claims := VerifyClaims0}) -> + JWKs = case erlang:is_pid(JWK) of + false -> + [JWK]; + true -> + {ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK), + JWKs0 + end, + VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), + case verify(JWT, JWKs, VerifyClaims) of + ok -> ok; + {error, invalid_signature} -> ignore; + {error, {claims, _}} -> {stop, bad_password} + end. + +destroy(#{jwk := Connector}) when is_pid(Connector) -> + _ = emqx_authn_jwks_connector:stop(Connector), + ok; +destroy(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +create(#{verify_claims := VerifyClaims} = Config) -> + create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). + +create2(#{use_jwks := false, + algorithm := 'hmac-based', + secret := Secret0, + secret_base64_encoded := Base64Encoded, + verify_claims := VerifyClaims}) -> + Secret = case Base64Encoded of + true -> + base64:decode(Secret0); + false -> + Secret0 + end, + JWK = jose_jwk:from_oct(Secret), + {ok, #{jwk => JWK, + verify_claims => VerifyClaims}}; + +create2(#{use_jwks := false, + algorithm := 'public-key', + certificate := Certificate, + verify_claims := VerifyClaims}) -> + JWK = jose_jwk:from_pem_file(Certificate), + {ok, #{jwk => JWK, + verify_claims => VerifyClaims}}; + +create2(#{use_jwks := true, + verify_claims := VerifyClaims} = Config) -> + case emqx_authn_jwks_connector:start_link(Config) of + {ok, Connector} -> + {ok, #{jwk => Connector, + verify_claims => VerifyClaims}}; + {error, Reason} -> + {error, Reason} + end. + +replace_placeholder(L, Variables) -> + replace_placeholder(L, Variables, []). + +replace_placeholder([], _Variables, Acc) -> + Acc; +replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) -> + Value = maps:get(PL, Variables), + replace_placeholder(More, Variables, [{Name, Value} | Acc]); +replace_placeholder([{Name, Value} | More], Variables, Acc) -> + replace_placeholder(More, Variables, [{Name, Value} | Acc]). + +verify(_JWS, [], _VerifyClaims) -> + {error, invalid_signature}; +verify(JWS, [JWK | More], VerifyClaims) -> + try jose_jws:verify(JWK, JWS) of + {true, Payload, _JWS} -> + Claims = emqx_json:decode(Payload, [return_maps]), + verify_claims(Claims, VerifyClaims); + {false, _, _} -> + verify(JWS, More, VerifyClaims) + catch + _:_Reason:_Stacktrace -> + %% TODO: Add log + {error, invalid_signature} + end. + +verify_claims(Claims, VerifyClaims0) -> + Now = os:system_time(seconds), + VerifyClaims = [{<<"exp">>, fun(ExpireTime) -> + Now < ExpireTime + end}, + {<<"iat">>, fun(IssueAt) -> + IssueAt =< Now + end}, + {<<"nbf">>, fun(NotBefore) -> + NotBefore =< Now + end}] ++ VerifyClaims0, + do_verify_claims(Claims, VerifyClaims). + +do_verify_claims(_Claims, []) -> + ok; +do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) -> + case maps:take(Name, Claims) of + error -> + do_verify_claims(Claims, More); + {Value, NClaims} -> + case Fun(Value) of + true -> + do_verify_claims(NClaims, More); + _ -> + {error, {claims, {Name, Value}}} + end + end; +do_verify_claims(Claims, [{Name, Value} | More]) -> + case maps:take(Name, Claims) of + error -> + {error, {missing_claim, Name}}; + {Value, NClaims} -> + do_verify_claims(NClaims, More); + {Value0, _} -> + {error, {claims, {Name, Value0}}} + end. + +check_verify_claims([]) -> + false; +check_verify_claims([{Name, Expected} | More]) -> + check_claim_name(Name) andalso + check_claim_expected(Expected) andalso + check_verify_claims(More). + +check_claim_name(exp) -> + false; +check_claim_name(iat) -> + false; +check_claim_name(nbf) -> + false; +check_claim_name(_) -> + true. + +check_claim_expected(Expected) -> + try handle_placeholder(Expected) of + _ -> true + catch + _:_ -> + false + end. + +handle_verify_claims(VerifyClaims) -> + handle_verify_claims(VerifyClaims, []). + +handle_verify_claims([], Acc) -> + Acc; +handle_verify_claims([{Name, Expected0} | More], Acc) -> + Expected = handle_placeholder(Expected0), + handle_verify_claims(More, [{Name, Expected} | Acc]). + +handle_placeholder(Placeholder0) -> + case re:run(Placeholder0, "^\\$\\{[a-z0-9\\-]+\\}$", [{capture, all}]) of + {match, [{Offset, Length}]} -> + Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3), + Placeholder2 = validate_placeholder(Placeholder1), + {placeholder, Placeholder2}; + nomatch -> + Placeholder0 + end. + +validate_placeholder(<<"mqtt-clientid">>) -> + clientid; +validate_placeholder(<<"mqtt-username">>) -> + username. diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl similarity index 80% rename from apps/emqx_authentication/src/emqx_authentication_mnesia.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 09307e38f..26b20c517 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -14,9 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_mnesia). +-module(emqx_authn_mnesia). --include("emqx_authentication.hrl"). +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). -export([ create/3 , update/4 @@ -32,29 +37,10 @@ , list_users/1 ]). -%% TODO: support bcrypt --service_type(#{ - name => mnesia, - params_spec => #{ - user_id_type => #{ - order => 1, - type => string, - enum => [<<"username">>, <<"clientid">>, <<"ip">>, <<"common name">>, <<"issuer">>], - default => <<"username">> - }, - password_hash_algorithm => #{ - order => 2, - type => string, - enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], - default => <<"sha256">> - }, - salt_rounds => #{ - order => 3, - type => number, - default => 10 - } - } -}). +-type user_id_type() :: clientid | username. + +-type user_group() :: {chain_id(), authenticator_name()}. +-type user_id() :: binary(). -record(user_info, { user_id :: {user_group(), user_id()} @@ -62,8 +48,7 @@ , salt :: binary() }). --type(user_group() :: {chain_id(), service_name()}). --type(user_id() :: binary()). +-reflect_type([ user_id_type/0 ]). -export([mnesia/1]). @@ -73,7 +58,6 @@ -define(TAB, mnesia_basic_auth). -rlog_shard({?AUTH_SHARD, ?TAB}). - %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -90,18 +74,61 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB, disc_copies). -create(ChainID, ServiceName, #{<<"user_id_type">> := Type, - <<"password_hash_algorithm">> := Algorithm, - <<"salt_rounds">> := SaltRounds}) -> - Algorithm =:= <<"bcrypt">> andalso ({ok, _} = application:ensure_all_started(bcrypt)), - State = #{user_group => {ChainID, ServiceName}, - user_id_type => binary_to_atom(Type, utf8), - password_hash_algorithm => binary_to_atom(Algorithm, utf8), +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {user_id_type, fun user_id_type/1} + , {password_hash_algorithm, fun password_hash_algorithm/1} + ]; + +fields(bcrypt) -> + [ {name, {enum, [bcrypt]}} + , {salt_rounds, fun salt_rounds/1} + ]; + +fields(other_algorithms) -> + [ {name, {enum, [plain, md5, sha, sha256, sha512]}} + ]. + +user_id_type(type) -> user_id_type(); +user_id_type(default) -> clientid; +user_id_type(_) -> undefined. + +password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(default) -> sha256; +password_hash_algorithm(_) -> undefined. + +salt_rounds(type) -> integer(); +salt_rounds(default) -> 10; +salt_rounds(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, AuthenticatorName, #{user_id_type := Type, + password_hash_algorithm := #{name := bcrypt, + salt_rounds := SaltRounds}}) -> + {ok, _} = application:ensure_all_started(bcrypt), + State = #{user_group => {ChainID, AuthenticatorName}, + user_id_type => Type, + password_hash_algorithm => bcrypt, salt_rounds => SaltRounds}, + {ok, State}; + +create(ChainID, AuthenticatorName, #{user_id_type := Type, + password_hash_algorithm := #{name := Name}}) -> + State = #{user_group => {ChainID, AuthenticatorName}, + user_id_type => Type, + password_hash_algorithm => Name}, {ok, State}. -update(ChainID, ServiceName, Params, _State) -> - create(ChainID, ServiceName, Params). +update(ChainID, AuthenticatorName, Config, _State) -> + create(ChainID, AuthenticatorName, Config). authenticate(ClientInfo = #{password := Password}, #{user_group := UserGroup, @@ -111,7 +138,11 @@ authenticate(ClientInfo = #{password := Password}, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + Salt = case Algorithm of + bcrypt -> PasswordHash; + _ -> Salt0 + end, case PasswordHash =:= hash(Algorithm, Password, Salt) of true -> ok; false -> {stop, bad_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl new file mode 100644 index 000000000..4fddcc3f7 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -0,0 +1,160 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_mysql). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +%% Config: +%% host +%% port +%% username +%% password +%% database +%% pool_size +%% connect_timeout +%% enable_ssl +%% ssl_opts +%% cacertfile +%% certfile +%% keyfile +%% verify +%% servce_name_indication +%% tls_versions +%% ciphers +%% password_hash_algorithm +%% salt_position +%% query +%% query_timeout +structs() -> [config]. + +fields(config) -> + [ {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} + , {query_timeout, fun query_timeout/1} + ]. + +password_hash_algorithm(type) -> string(); +password_hash_algorithm(_) -> undefined. + +query(type) -> string(); +query(nullable) -> false; +query(_) -> undefined. + +query_timeout(type) -> integer(); +query_timeout(defualt) -> 5000; +query_timeout(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, ServiceName, #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> + {Query, PlaceHolders} = parse_query(Query0), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + State = #{query => Query, + placeholders => PlaceHolders, + password_hash_algorithm => Algorithm}, + case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(#{password := Password} = ClientInfo, + #{resource_id := ResourceID, + placeholders := PlaceHolders, + query := Query, + query_timeout := Timeout} = State) -> + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of + {ok, _Columns, []} -> ignore; + {ok, Columns, Rows} -> + %% TODO: Support superuser + Selected = maps:from_list(lists:zip(Columns, Rows)), + check_password(Password, Selected, State); + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +check_password(undefined, _Algorithm, _Selected) -> + {stop, bad_password}; +check_password(Password, + #{password_hash := Hash}, + #{password_hash_algorithm := bcrypt}) -> + {ok, Hash0} = bcrypt:hashpw(Password, Hash), + case list_to_binary(Hash0) =:= Hash of + true -> ok; + false -> {stop, bad_password} + end; +check_password(Password, + #{password_hash := Hash} = Selected, + #{password_hash_algorithm := Algorithm, + salt_position := SaltPosition}) -> + Salt = maps:get(salt, Selected, <<>>), + Hash0 = case SaltPosition of + prefix -> emqx_passwd:hash(Algorithm, <>); + suffix -> emqx_passwd:hash(Algorithm, <>) + end, + case Hash0 =:= Hash of + true -> ok; + false -> {stop, bad_password} + end. + +%% TODO: Support prepare +parse_query(Query) -> + case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of + {match, Captured} -> + PlaceHolders = [PlaceHolder || PlaceHolder <- Captured], + NQuery = re:replace(Query, "'\\$\\{[a-z0-9\\_]+\\}'", "?", [global, {return, binary}]), + {NQuery, PlaceHolders}; + nomatch -> + {Query, []} + end. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl new file mode 100644 index 000000000..8c250e216 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -0,0 +1,155 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_pgsql). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +%% Config: +%% host +%% port +%% username +%% password +%% database +%% pool_size +%% connect_timeout +%% enable_ssl +%% cacertfile +%% certfile +%% keyfile +%% verify +%% servce_name_indication +%% tls_versions +%% ciphers +%% password_hash_algorithm +%% salt_position +%% query +structs() -> [config]. + +fields(config) -> + [ {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} + ]. + +password_hash_algorithm(type) -> string(); +password_hash_algorithm(_) -> undefined. + +query(type) -> string(); +query(nullable) -> false; +query(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, ServiceName, #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> + {Query, PlaceHolders} = parse_query(Query0), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + State = #{query => Query, + placeholders => PlaceHolders, + password_hash_algorithm => Algorithm}, + case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(#{password := Password} = ClientInfo, + #{resource_id := ResourceID, + query := Query, + placeholders := PlaceHolders} = State) -> + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + case emqx_resource:query(ResourceID, {sql, Query, Params}) of + {ok, _Columns, []} -> ignore; + {ok, Columns, Rows} -> + %% TODO: Support superuser + Selected = maps:from_list(lists:zip(Columns, Rows)), + check_password(Password, Selected, State); + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +check_password(undefined, _Algorithm, _Selected) -> + {stop, bad_password}; +check_password(Password, + #{password_hash := Hash}, + #{password_hash_algorithm := bcrypt}) -> + {ok, Hash0} = bcrypt:hashpw(Password, Hash), + case list_to_binary(Hash0) =:= Hash of + true -> ok; + false -> {stop, bad_password} + end; +check_password(Password, + #{password_hash := Hash} = Selected, + #{password_hash_algorithm := Algorithm, + salt_position := SaltPosition}) -> + Salt = maps:get(salt, Selected, <<>>), + Hash0 = case SaltPosition of + prefix -> emqx_passwd:hash(Algorithm, <>); + suffix -> emqx_passwd:hash(Algorithm, <>) + end, + case Hash0 =:= Hash of + true -> ok; + false -> {stop, bad_password} + end. + +%% TODO: Support prepare +parse_query(Query) -> + case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of + {match, Captured} -> + PlaceHolders = [PlaceHolder || PlaceHolder <- Captured], + Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))], + NQuery = lists:foldl(fun({PlaceHolder, Replacement}, Query0) -> + re:replace(Query0, <<"'\\", PlaceHolder/binary, "'">>, Replacement, [{return, binary}]) + end, Query, lists:zip(PlaceHolders, Replacements)), + {NQuery, PlaceHolders}; + nomatch -> + {Query, []} + end. diff --git a/apps/emqx_authn/test/data/private_key.pem b/apps/emqx_authn/test/data/private_key.pem new file mode 100644 index 000000000..318eefe27 --- /dev/null +++ b/apps/emqx_authn/test/data/private_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZNF0IeaTmJcBooxsY6Ze8PGFS +QJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVDeYsu15Htxpqig1L/+MphbZlF +ClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8GNp+/j2yir1Z9E2xAgMBAAEC +gYBX7HsLncMWexux6nddbl0nWwyhyPZcvgvT4TjHTPAfhNdOtfQyZCUdbv5+mqip +j6O8BE7ar2TMz5FgvVrF+O97LkYHNmZk0q3xtZlCYXp4BQqD6Wq65H5U4fAomalK +xm7HsTCSVXx5CvnZK/JbkPw18QsgwrSHEFs+4Pf2noH+FQJBAL/bpPrkDOB476Iy +RGnuCckUN1pdCU+UINC8oOWGNwsG6EE5ywlIWRXHtp4VMksG6mCLNJwGUAv2zWIs +iEjZVfsCQQCVxOciTajTtYO5bPjkXoZoe4VKKXWMYv9AXXVCjq0ff/LjrnKJjbRm +aoKQGhzjKHk5rgd9+Ydl6FnJw5K4B9dDAkEAtaHfQpZ7ildzpf4ovpBYO0EkViwW +EHyPxI2PVTwHCC1126o3CYawr+nufSJcBqN5aAThvYRMa8cvEW5PZ4g52QJALF5L +tt7Yz/crEciVp1nVaaiGISVNHIzLX28QaOpJoVZPR2ILrnJbaifNjBEgU69O0maa +85fzo54E03/rvDcebwJAfTMgIyzFQK/ESnM43bUCI/Y5XAeKFBiN1YhCioNR4Hj7 +Lkw2RdrrPC9LV+gVJK0b7VUqR5odjdj7PN6SipuXNw== +-----END RSA PRIVATE KEY----- diff --git a/apps/emqx_authn/test/data/public_key.pem b/apps/emqx_authn/test/data/public_key.pem new file mode 100644 index 000000000..b0b151981 --- /dev/null +++ b/apps/emqx_authn/test/data/public_key.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZ +NF0IeaTmJcBooxsY6Ze8PGFSQJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVD +eYsu15Htxpqig1L/+MphbZlFClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8 +GNp+/j2yir1Z9E2xAgMBAAE= +-----END PUBLIC KEY----- diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv similarity index 100% rename from apps/emqx_authentication/test/data/user-credentials.csv rename to apps/emqx_authn/test/data/user-credentials.csv diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json similarity index 100% rename from apps/emqx_authentication/test/data/user-credentials.json rename to apps/emqx_authn/test/data/user-credentials.json diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl new file mode 100644 index 000000000..17c08cc70 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -0,0 +1,142 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_chain(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), + ok. + +t_binding(_) -> + Listener1 = <<"listener1">>, + Listener2 = <<"listener2">>, + ChainID = <<"mychain">>, + + ?assertEqual({error, {not_found, {chain, ChainID}}}, ?AUTH:bind(ChainID, [Listener1])), + + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1])), + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener2])), + ?assertEqual({error, {already_bound, [Listener1]}}, ?AUTH:bind(ChainID, [Listener1])), + {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), + ?assertEqual(2, length(Listeners)), + ?assertMatch({ok, #{simple := ChainID}}, ?AUTH:list_bound_chains(Listener1)), + + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1])), + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener2])), + ?assertEqual({error, {not_found, [Listener1]}}, ?AUTH:unbind(ChainID, [Listener1])), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_binding2(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + Listener1 = <<"listener1">>, + Listener2 = <<"listener2">>, + + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1, Listener2])), + {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), + ?assertEqual(2, length(Listeners)), + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1, Listener2])), + ?assertMatch({ok, #{listeners := []}}, ?AUTH:list_bindings(ChainID)), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), + + AuthenticatorName1 = <<"myauthenticator1">>, + AuthenticatorConfig1 = #{name => AuthenticatorName1, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName1)), + ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + + AuthenticatorName2 = <<"myauthenticator2">>, + AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + ?assertMatch({ok, #{id := ChainID, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 1)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName2)), + ?assertEqual({ok, []}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl new file mode 100644 index 000000000..27f34f936 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -0,0 +1,182 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_jwt_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_jwt_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + Config = #{use_jwks => false, + algorithm => 'hmac-based', + secret => <<"abcdef">>, + secret_base64_encoded => false, + verify_claims => []}, + AuthenticatorConfig = #{name => AuthenticatorName, + type => jwt, + config => Config}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + Payload = #{<<"username">> => <<"myuser">>}, + JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => JWS}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + + BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), + ClientInfo2 = ClientInfo#{password => BadJWS}, + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + + %% secret_base64_encoded + Config2 = Config#{secret => base64:encode(<<"abcdef">>), + secret_base64_encoded => true}, + ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config2)), + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + + Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, + ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config3)), + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>})), + + %% Expiration + Payload3 = #{ <<"username">> => <<"myuser">> + , <<"exp">> => erlang:system_time(second) - 60}, + JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), + ClientInfo3 = ClientInfo#{password => JWS3}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + + Payload4 = #{ <<"username">> => <<"myuser">> + , <<"exp">> => erlang:system_time(second) + 60}, + JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), + ClientInfo4 = ClientInfo#{password => JWS4}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + + %% Issued At + Payload5 = #{ <<"username">> => <<"myuser">> + , <<"iat">> => erlang:system_time(second) - 60}, + JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), + ClientInfo5 = ClientInfo#{password => JWS5}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo5)), + + Payload6 = #{ <<"username">> => <<"myuser">> + , <<"iat">> => erlang:system_time(second) + 60}, + JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), + ClientInfo6 = ClientInfo#{password => JWS6}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo6)), + + %% Not Before + Payload7 = #{ <<"username">> => <<"myuser">> + , <<"nbf">> => erlang:system_time(second) - 60}, + JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), + ClientInfo7 = ClientInfo#{password => JWS7}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo7)), + + Payload8 = #{ <<"username">> => <<"myuser">> + , <<"nbf">> => erlang:system_time(second) + 60}, + JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), + ClientInfo8 = ClientInfo#{password => JWS8}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo8)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_jwt_authenticator2(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + Dir = code:lib_dir(emqx_authn, test), + PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), + PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), + AuthenticatorName = <<"myauthenticator">>, + Config = #{use_jwks => false, + algorithm => 'public-key', + certificate => PublicKey, + verify_claims => []}, + AuthenticatorConfig = #{name => AuthenticatorName, + type => jwt, + config => Config}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + Payload = #{<<"username">> => <<"myuser">>}, + JWS = generate_jws('public-key', Payload, PrivateKey), + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => JWS}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +generate_jws('hmac-based', Payload, Secret) -> + JWK = jose_jwk:from_oct(Secret), + Header = #{ <<"alg">> => <<"HS256">> + , <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS; +generate_jws('public-key', Payload, PrivateKey) -> + JWK = jose_jwk:from_pem_file(PrivateKey), + Header = #{ <<"alg">> => <<"RS256">> + , <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl new file mode 100644 index 000000000..abc7ad149 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -0,0 +1,187 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_mnesia_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_mnesia_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + AuthenticatorConfig = #{name => AuthenticatorName, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + UserInfo = #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ClientInfo2 = ClientInfo#{username => <<"baduser">>}, + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ClientInfo3 = ClientInfo#{password => <<"badpass">>}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + + UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)), + ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + + ?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual([], ets:tab2list(mnesia_basic_auth)), + ok. + +t_import(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + AuthenticatorConfig = #{name => AuthenticatorName, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + Dir = code:lib_dir(emqx_authn, test), + ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser3">>)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo1 = #{listener_id => ListenerID, + username => <<"myuser1">>, + password => <<"mypassword1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + password => <<"mypassword3">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_multi_mnesia_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName1 = <<"myauthenticator1">>, + AuthenticatorConfig1 = #{name => AuthenticatorName1, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + AuthenticatorName2 = <<"myauthenticator2">>, + AuthenticatorConfig2 = #{name => AuthenticatorName2, + type => 'built-in-database', + config => #{ + user_id_type => clientid, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + + ?assertEqual({ok, #{user_id => <<"myuser">>}}, + ?AUTH:add_user(ChainID, AuthenticatorName1, + #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass1">>})), + ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?AUTH:add_user(ChainID, AuthenticatorName2, + #{<<"user_id">> => <<"myclient">>, + <<"password">> => <<"mypass2">>})), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo1 = #{listener_id => ListenerID, + username => <<"myuser">>, + clientid => <<"myclient">>, + password => <<"mypass1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. diff --git a/rebar.config.erl b/rebar.config.erl index 901749d72..ff66f746c 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -248,6 +248,7 @@ relx_apps(ReleaseType) -> , {mnesia, load} , {ekka, load} , {emqx_plugin_libs, load} + , emqx_authn , emqx_authz , observer_cli , emqx_http_lib @@ -285,7 +286,6 @@ relx_plugin_apps(ReleaseType) -> , emqx_sn , emqx_coap , emqx_stomp - , emqx_authentication , emqx_statsd , emqx_rule_actions ] @@ -373,6 +373,7 @@ emqx_etc_overlay_common() -> {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}, {"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"}, {"{{base_dir}}/lib/emqx_telemetry/etc/emqx_telemetry.conf", "etc/plugins/emqx_telemetry.conf"}, + {"{{base_dir}}/lib/emqx_authn/etc/emqx_authn.conf", "etc/plugins/emqx_authn.conf"}, {"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"}, %% TODO: check why it has to end with .paho %% and why it is put to etc/plugins dir From b63bba59d5aabf7cdd14c5024bb505bbabbbdf80 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 30 Jun 2021 17:26:07 +0800 Subject: [PATCH 047/379] feat(authn): update schema for mysql and postgresql --- .../src/simple_authn/emqx_authn_mysql.erl | 24 ++----------------- .../src/simple_authn/emqx_authn_pgsql.erl | 22 ++--------------- 2 files changed, 4 insertions(+), 42 deletions(-) diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index 4fddcc3f7..3b5384d9c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -33,27 +33,6 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -%% Config: -%% host -%% port -%% username -%% password -%% database -%% pool_size -%% connect_timeout -%% enable_ssl -%% ssl_opts -%% cacertfile -%% certfile -%% keyfile -%% verify -%% servce_name_indication -%% tls_versions -%% ciphers -%% password_hash_algorithm -%% salt_position -%% query -%% query_timeout structs() -> [config]. fields(config) -> @@ -61,7 +40,8 @@ fields(config) -> , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} , {query_timeout, fun query_timeout/1} - ]. + ] ++ emqx_connector_schema_lib:relational_db_fields() + ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> string(); password_hash_algorithm(_) -> undefined. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 8c250e216..c9046c606 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -33,32 +33,14 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -%% Config: -%% host -%% port -%% username -%% password -%% database -%% pool_size -%% connect_timeout -%% enable_ssl -%% cacertfile -%% certfile -%% keyfile -%% verify -%% servce_name_indication -%% tls_versions -%% ciphers -%% password_hash_algorithm -%% salt_position -%% query structs() -> [config]. fields(config) -> [ {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} - ]. + ] ++ emqx_connector_schema_lib:relational_db_fields() + ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> string(); password_hash_algorithm(_) -> undefined. From e0f10874903be9386dff81be176efcaa5acd94ec Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 30 Jun 2021 18:58:51 +0800 Subject: [PATCH 048/379] chore(mgmt): cancel plugins test case --- .../test/emqx_mgmt_api_SUITE.erl | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl index d372596ed..9e6347c65 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl @@ -284,62 +284,62 @@ t_nodes(_) -> ?assertEqual(<<"undefined">>, maps:get(<<"error">>, Error)), meck:unload(emqx_mgmt). -t_plugins(_) -> - application:ensure_all_started(emqx_retainer), - {ok, Plugins1} = request_api(get, api_path(["plugins"]), auth_header_()), - [Plugins11] = filter(get(<<"data">>, Plugins1), <<"node">>, atom_to_binary(node(), utf8)), - [Plugin1] = filter(maps:get(<<"plugins">>, Plugins11), <<"name">>, <<"emqx_retainer">>), - ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin1)), - ?assertEqual(true, maps:get(<<"active">>, Plugin1)), - - {ok, _} = request_api(put, - api_path(["plugins", - atom_to_list(emqx_retainer), - "unload"]), - auth_header_()), - {ok, Error1} = request_api(put, - api_path(["plugins", - atom_to_list(emqx_retainer), - "unload"]), - auth_header_()), - ?assertEqual(<<"not_started">>, get(<<"message">>, Error1)), - {ok, Plugins2} = request_api(get, - api_path(["nodes", atom_to_list(node()), "plugins"]), - auth_header_()), - [Plugin2] = filter(get(<<"data">>, Plugins2), <<"name">>, <<"emqx_retainer">>), - ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin2)), - ?assertEqual(false, maps:get(<<"active">>, Plugin2)), - - {ok, _} = request_api(put, - api_path(["nodes", - atom_to_list(node()), - "plugins", - atom_to_list(emqx_retainer), - "load"]), - auth_header_()), - {ok, Plugins3} = request_api(get, - api_path(["nodes", atom_to_list(node()), "plugins"]), - auth_header_()), - [Plugin3] = filter(get(<<"data">>, Plugins3), <<"name">>, <<"emqx_retainer">>), - ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin3)), - ?assertEqual(true, maps:get(<<"active">>, Plugin3)), - - {ok, _} = request_api(put, - api_path(["nodes", - atom_to_list(node()), - "plugins", - atom_to_list(emqx_retainer), - "unload"]), - auth_header_()), - {ok, Error2} = request_api(put, - api_path(["nodes", - atom_to_list(node()), - "plugins", - atom_to_list(emqx_retainer), - "unload"]), - auth_header_()), - ?assertEqual(<<"not_started">>, get(<<"message">>, Error2)), - application:stop(emqx_retainer). +% t_plugins(_) -> +% application:ensure_all_started(emqx_retainer), +% {ok, Plugins1} = request_api(get, api_path(["plugins"]), auth_header_()), +% [Plugins11] = filter(get(<<"data">>, Plugins1), <<"node">>, atom_to_binary(node(), utf8)), +% [Plugin1] = filter(maps:get(<<"plugins">>, Plugins11), <<"name">>, <<"emqx_retainer">>), +% ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin1)), +% ?assertEqual(true, maps:get(<<"active">>, Plugin1)), +% +% {ok, _} = request_api(put, +% api_path(["plugins", +% atom_to_list(emqx_retainer), +% "unload"]), +% auth_header_()), +% {ok, Error1} = request_api(put, +% api_path(["plugins", +% atom_to_list(emqx_retainer), +% "unload"]), +% auth_header_()), +% ?assertEqual(<<"not_started">>, get(<<"message">>, Error1)), +% {ok, Plugins2} = request_api(get, +% api_path(["nodes", atom_to_list(node()), "plugins"]), +% auth_header_()), +% [Plugin2] = filter(get(<<"data">>, Plugins2), <<"name">>, <<"emqx_retainer">>), +% ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin2)), +% ?assertEqual(false, maps:get(<<"active">>, Plugin2)), +% +% {ok, _} = request_api(put, +% api_path(["nodes", +% atom_to_list(node()), +% "plugins", +% atom_to_list(emqx_retainer), +% "load"]), +% auth_header_()), +% {ok, Plugins3} = request_api(get, +% api_path(["nodes", atom_to_list(node()), "plugins"]), +% auth_header_()), +% [Plugin3] = filter(get(<<"data">>, Plugins3), <<"name">>, <<"emqx_retainer">>), +% ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin3)), +% ?assertEqual(true, maps:get(<<"active">>, Plugin3)), +% +% {ok, _} = request_api(put, +% api_path(["nodes", +% atom_to_list(node()), +% "plugins", +% atom_to_list(emqx_retainer), +% "unload"]), +% auth_header_()), +% {ok, Error2} = request_api(put, +% api_path(["nodes", +% atom_to_list(node()), +% "plugins", +% atom_to_list(emqx_retainer), +% "unload"]), +% auth_header_()), +% ?assertEqual(<<"not_started">>, get(<<"message">>, Error2)), +% application:stop(emqx_retainer). t_acl_cache(_) -> ClientId = <<"client1">>, From d2c50baf9321999b8b9fb9bc32f42ed899c3c306 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 30 Jun 2021 19:20:58 +0800 Subject: [PATCH 049/379] fix(config): set console_handler.enable to turewhen start from console --- apps/emqx/etc/emqx.conf | 440 +++++++++++++++++++++++----------- apps/emqx/src/emqx_schema.erl | 135 ++++++----- bin/emqx | 4 +- 3 files changed, 372 insertions(+), 207 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 68d8f359f..4c8113cd5 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -344,8 +344,8 @@ log { ## ## @doc log.console_handler.level ## ValueType: debug | info | notice | warning | error | critical | alert | emergency - ## Default: debug - console_handler.level = debug + ## Default: warning + console_handler.level = warning ##---------------------------------------------------------------- ## The file log handlers send log messages to files @@ -358,8 +358,8 @@ log { ## ## @doc log.file_handlers..level ## ValueType: debug | info | notice | warning | error | critical | alert | emergency - ## Default: debug - level: debug + ## Default: warning + level: warning ## The log file for specified level. ## @@ -380,18 +380,18 @@ log { ## With this enabled, new log files will be created when the current ## log file is full, max to `rotation_count` files will be created. ## - ## @doc log.file_handlers..rotation + ## @doc log.file_handlers..rotation.enable ## ValueType: Boolean ## Default: true - rotation = true + rotation.enable = true ## Maximum rotation count of log files. ## - ## @doc log.file_handlers..rotation_count + ## @doc log.file_handlers..rotation.count ## ValueType: Integer ## Range: [1, 2048] ## Default: 10 - rotation_count: 10 + rotation.count: 10 ## Maximum size of each log file. ## @@ -411,9 +411,19 @@ log { ## log level for example: file_handlers.emqx_error: { level: error - file: "{{ platform_log_dir }}/emqx.error.log" + file: "{{ platform_log_dir }}/error.log" } + ## Timezone offset to display in logs + ## + ## @doc log.time_offset + ## ValueType: system | utc | String + ## - "system" use system zone + ## - "utc" for Universal Coordinated Time (UTC) + ## - "+hh:mm" or "-hh:mm" for a specified offset + ## Default: system + time_offset = system + ## Limits the total number of characters printed for each log event. ## ## @doc log.chars_limit @@ -421,6 +431,14 @@ log { ## Default: infinity chars_limit: 8192 + ## Maximum depth for Erlang term log formatting + ## and Erlang process message queue inspection. + ## + ## @doc log.max_depth + ## ValueType: Integer | infinity + ## Default: 80 + max_depth = 80 + ## Log formatter ## @doc log.formatter ## ValueType: text | json @@ -477,10 +495,10 @@ log { ## We could kill the log handler in these cases and restart it after a ## few seconds. ## - ## @doc log.overload_kill + ## @doc log.overload_kill.enable ## ValueType: Boolean ## Default: true - overload_kill: true + overload_kill.enable: true ## The max allowed queue length before killing the log hanlder. ## @@ -488,11 +506,11 @@ log { ## length. If the message queue grows larger than this, the handler ## process is terminated. ## - ## @doc log.overload_kill_qlen + ## @doc log.overload_kill.qlen ## ValueType: Integer ## Range: [0, 1048576] ## Default: 20000 - overload_kill_qlen: 20000 + overload_kill.qlen: 20000 ## The max allowed memory size before killing the log hanlder. ## @@ -500,20 +518,20 @@ log { ## that the handler process is allowed to use. If the handler grows ## larger than this, the process is terminated. ## - ## @doc log.overload_kill_mem_size + ## @doc log.overload_kill.mem_size ## ValueType: Size ## Default: 30MB - overload_kill_mem_size: 30MB + overload_kill.mem_size: 30MB ## Restart the log hanlder after some seconds. ## ## Log overload protection parameter. If the handler is terminated, ## it restarts automatically after a delay specified in seconds. ## - ## @doc log.overload_kill_restart_after + ## @doc log.overload_kill.restart_after ## ValueType: Duration ## Default: 5s - overload_kill_restart_after: 5s + overload_kill.restart_after: 5s ## Controlling Bursts of Log Requests. ## @@ -526,26 +544,26 @@ log { ## Note that there would be no warning if any messages were ## dropped because of burst control. ## - ## @doc log.burst_limit + ## @doc log.burst_limit.enable ## ValueType: Boolean ## Default: false - burst_limit: false + burst_limit.enable: false ## This config controls the maximum number of events to handle within ## a time frame. After the limit is reached, successive events are ## dropped until the end of the time frame defined by `window_time`. ## - ## @doc log.burst_limit_max_count + ## @doc log.burst_limit.max_count ## ValueType: Integer ## Default: 10000 - burst_limit_max_count: 10000 + burst_limit.max_count: 10000 ## See the previous description of burst_limit_max_count. ## - ## @doc log.burst_limit_window_time + ## @doc log.burst_limit.window_time ## ValueType: duration ## Default: 1s - burst_limit_window_time: 1s + burst_limit.window_time: 1s } ##================================================================== @@ -577,8 +595,7 @@ rpc { ## ## @doc cluster.discovery_strategy ## ValueType: manual | stateless - ## - manual: discover ports by `tcp_server_port` and - ## `tcp_client_port`. + ## - manual: discover ports by `tcp_server_port`. ## - stateless: discover ports in a stateless manner. ## If node name is `emqx@127.0.0.1`, where the `` is ## an integer, then the listening port will be `5370 + ` @@ -596,26 +613,15 @@ rpc { ## Defaults: 5369 tcp_server_port: 5369 - ## TCP port for outgoing RPC connections. - ## - ## Only takes effect when `rpc.port_discovery` = `manual`. - ## - ## @doc rpc.tcp_client_port - ## ValueType: Integer - ## Range: [1024-65535] - ## Defaults: 5369 - tcp_client_port: 5369 - ## Number of outgoing RPC connections. ## - ## Defaults to "num_cpu_cores", that is, the number of CPU cores. ## Set this to 1 to keep the message order sent from the same ## client. ## ## @doc rpc.tcp_client_num - ## ValueType: Integer | num_cpu_cores + ## ValueType: Integer ## Range: [1, 256] - ## Defaults: num_cpu_cores + ## Defaults: 1 tcp_client_num: 1 ## RCP Client connect timeout. @@ -827,6 +833,13 @@ zone.default { ## Default: true stats.enable: true + ## Maximum number of concurrent connections. + ## + ## @doc zone..listeners..overall_max_connections + ## ValueType: Number | infinity + ## Default: infinity + overall_max_connections: infinity + mqtt { ## When publishing or subscribing, prefix all topics with a mountpoint string. ## The prefixed string will be removed from the topic name when the message @@ -870,7 +883,7 @@ zone.default { ## Maximum length of MQTT clientId allowed. ## ## @doc zone..mqtt.max_clientid_len - ## ValueType: Integer | infinity + ## ValueType: Integer ## Range: [23, 65535] ## Default: 65535 max_clientid_len: 65535 @@ -878,10 +891,10 @@ zone.default { ## Maximum topic levels allowed. ## ## @doc zone..mqtt.max_topic_levels - ## ValueType: Integer | infinity + ## ValueType: Integer ## Range: [1, 65535] - ## Default: infinity - max_topic_levels: infinity + ## Default: 65535 + max_topic_levels: 65535 ## Maximum QoS allowed. ## @@ -893,7 +906,7 @@ zone.default { ## Maximum Topic Alias, 0 means no topic alias supported. ## ## @doc zone..mqtt.max_topic_alias - ## ValueType: Integer | infinity + ## ValueType: Integer ## Range: [0, 65535] ## Default: 65535 max_topic_alias: 65535 @@ -958,11 +971,11 @@ zone.default { ## Default: 0.75 keepalive_backoff: 0.75 - ## Maximum number of subscriptions allowed, 0 means no limit. + ## Maximum number of subscriptions allowed. ## ## @doc zone..mqtt.max_subscriptions ## ValueType: Integer | infinity - ## Range: [0, ) + ## Range: [1, ) ## Default: infinity max_subscriptions: infinity @@ -976,8 +989,8 @@ zone.default { ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. ## ## @doc zone..mqtt.max_inflight - ## ValueType: Integer | infinity - ## Range: [0, ) + ## ValueType: Integer + ## Range: [1, 65535] ## Default: 32 max_inflight: 32 @@ -988,11 +1001,11 @@ zone.default { ## Default: 30s retry_interval: 30s - ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. + ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. ## ## @doc zone..mqtt.max_awaiting_rel ## ValueType: Integer | infinity - ## Range: [0, ) + ## Range: [1, ) ## Default: 100 max_awaiting_rel: 100 @@ -1011,7 +1024,7 @@ zone.default { session_expiry_interval: 2h ## Maximum queue length. Enqueued messages when persistent client disconnected, - ## or inflight window is full. 0 means no limit. + ## or inflight window is full. ## ## @doc zone..mqtt.max_mqueue_len ## ValueType: Integer | infinity @@ -1117,61 +1130,6 @@ zone.default { cache.ttl: 1m } - rate_limit { - ## Maximum connections per second. - ## - ## @doc zone..max_conn_rate - ## ValueType: Number | infinity - ## Default: 1000 - ## Examples: - ## max_conn_rate: 1000 - max_conn_rate: 1000 - - ## Message limit for the a external MQTT connection. - ## - ## @doc zone..rate_limit.conn_messages_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messages per 10 seconds. - ## conn_messages_in: "100,10s" - conn_messages_in: "100,10s" - - ## Limit the rate of receiving packets for a MQTT connection. - ## The rate is counted by bytes of packets per second. - ## - ## The connection won't accept more messages if the messages come - ## faster than the limit. - ## - ## @doc zone..rate_limit.conn_bytes_in - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100KB incoming per 10 seconds. - ## conn_bytes_in: "100KB,10s" - ## - conn_bytes_in: "100KB,10s" - - ## Messages quota for the each of external MQTT connection. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zone..rate_limit.quota.conn_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 100 messaegs per 1s: - ## quota.conn_messages_routing: "100,1s" - quota.conn_messages_routing: "100,1s" - - ## Messages quota for the all of external MQTT connections. - ## This value consumed by the number of recipient on a message. - ## - ## @doc zone..rate_limit.quota.overall_messages_routing - ## ValueType: String | infinity - ## Default: infinity - ## Examples: 200000 messages per 1s: - ## quota.overall_messages_routing: "200000,1s" - ## - quota.overall_messages_routing: "200000,1s" - } - flapping_detect { ## Enable Flapping Detection. ## @@ -1299,11 +1257,9 @@ zone.default { ## The type of the listener. ## ## @doc zone..listeners..type - ## ValueType: tcp | ssl | ws | wss + ## ValueType: tcp | ws ## - tcp: MQTT over TCP - ## - ssl: MQTT over TLS ## - ws: MQTT over Websocket - ## - wss: MQTT over WebSocket Secure ## Required: true type: tcp @@ -1318,9 +1274,9 @@ zone.default { ## The size of the acceptor pool for this listener. ## ## @doc zone..listeners..acceptors - ## ValueType: Number | num_cpu_cores - ## Default: num_cpu_cores - acceptors: num_cpu_cores + ## ValueType: Number + ## Default: 16 + acceptors: 16 ## Maximum number of concurrent connections. ## @@ -1363,6 +1319,61 @@ zone.default { ## Default: 3s proxy_protocol_timeout: 3s + rate_limit { + ## Maximum connections per second. + ## + ## @doc zone..max_conn_rate + ## ValueType: Number | infinity + ## Default: 1000 + ## Examples: + ## max_conn_rate: 1000 + max_conn_rate: 1000 + + ## Message limit for the a external MQTT connection. + ## + ## @doc zone..rate_limit.conn_messages_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messages per 10 seconds. + ## conn_messages_in: "100,10s" + conn_messages_in: "100,10s" + + ## Limit the rate of receiving packets for a MQTT connection. + ## The rate is counted by bytes of packets per second. + ## + ## The connection won't accept more messages if the messages come + ## faster than the limit. + ## + ## @doc zone..rate_limit.conn_bytes_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100KB incoming per 10 seconds. + ## conn_bytes_in: "100KB,10s" + ## + conn_bytes_in: "100KB,10s" + + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + quota.conn_messages_routing: "100,1s" + + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + quota.overall_messages_routing: "200000,1s" + } + ## TCP options ## See ${example_common_tcp_options} for more information tcp.backlog: 1024 @@ -1377,13 +1388,11 @@ zone.default { ## The type of the listener. ## ## @doc zone..listeners..type - ## ValueType: tcp | ssl | ws | wss + ## ValueType: tcp | ws ## - tcp: MQTT over TCP - ## - ssl: MQTT over TLS ## - ws: MQTT over Websocket - ## - wss: MQTT over WebSocket Secure ## Required: true - type: ssl + type: tcp ## The IP address and port that the listener will bind. ## @@ -1396,9 +1405,9 @@ zone.default { ## The size of the acceptor pool for this listener. ## ## @doc zone..listeners..acceptors - ## ValueType: Number | num_cpu_cores - ## Default: num_cpu_cores - acceptors: num_cpu_cores + ## ValueType: Number + ## Default: 16 + acceptors: 16 ## Maximum number of concurrent connections. ## @@ -1441,8 +1450,64 @@ zone.default { ## Default: 3s proxy_protocol_timeout: 3s + rate_limit { + ## Maximum connections per second. + ## + ## @doc zone..max_conn_rate + ## ValueType: Number | infinity + ## Default: 1000 + ## Examples: + ## max_conn_rate: 1000 + max_conn_rate: 1000 + + ## Message limit for the a external MQTT connection. + ## + ## @doc zone..rate_limit.conn_messages_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messages per 10 seconds. + ## conn_messages_in: "100,10s" + conn_messages_in: "100,10s" + + ## Limit the rate of receiving packets for a MQTT connection. + ## The rate is counted by bytes of packets per second. + ## + ## The connection won't accept more messages if the messages come + ## faster than the limit. + ## + ## @doc zone..rate_limit.conn_bytes_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100KB incoming per 10 seconds. + ## conn_bytes_in: "100KB,10s" + ## + conn_bytes_in: "100KB,10s" + + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + quota.conn_messages_routing: "100,1s" + + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + quota.overall_messages_routing: "200000,1s" + } + ## SSL options ## See ${example_common_ssl_options} for more information + ssl.enable: true ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" @@ -1460,11 +1525,9 @@ zone.default { ## The type of the listener. ## ## @doc zone..listeners..type - ## ValueType: tcp | ssl | ws | wss + ## ValueType: tcp | ws ## - tcp: MQTT over TCP - ## - ssl: MQTT over TLS ## - ws: MQTT over Websocket - ## - wss: MQTT over WebSocket Secure ## Required: true type: ws @@ -1479,9 +1542,9 @@ zone.default { ## The size of the acceptor pool for this listener. ## ## @doc zone..listeners..acceptors - ## ValueType: Number | num_cpu_cores - ## Default: num_cpu_cores - acceptors: num_cpu_cores + ## ValueType: Number + ## Default: 16 + acceptors: 16 ## Maximum number of concurrent connections. ## @@ -1524,6 +1587,61 @@ zone.default { ## Default: 3s proxy_protocol_timeout: 3s + rate_limit { + ## Maximum connections per second. + ## + ## @doc zone..max_conn_rate + ## ValueType: Number | infinity + ## Default: 1000 + ## Examples: + ## max_conn_rate: 1000 + max_conn_rate: 1000 + + ## Message limit for the a external MQTT connection. + ## + ## @doc zone..rate_limit.conn_messages_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messages per 10 seconds. + ## conn_messages_in: "100,10s" + conn_messages_in: "100,10s" + + ## Limit the rate of receiving packets for a MQTT connection. + ## The rate is counted by bytes of packets per second. + ## + ## The connection won't accept more messages if the messages come + ## faster than the limit. + ## + ## @doc zone..rate_limit.conn_bytes_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100KB incoming per 10 seconds. + ## conn_bytes_in: "100KB,10s" + ## + conn_bytes_in: "100KB,10s" + + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + quota.conn_messages_routing: "100,1s" + + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + quota.overall_messages_routing: "200000,1s" + } + ## TCP options ## See ${example_common_tcp_options} for more information tcp.backlog: 1024 @@ -1537,23 +1655,15 @@ zone.default { listeners.mqtt_wss: #${example_common_tcp_options} ${example_common_ssl_options} ${example_common_websocket_options} # common options can be written in a separate config entry and reference it from here. { - ## The name of the listener. - ## - ## @doc zone..listeners..name - ## ValueType: String - ## Required: true - name: mqtt_over_wss ## The type of the listener. ## ## @doc zone..listeners..type - ## ValueType: tcp | ssl | ws | wss + ## ValueType: tcp | ws ## - tcp: MQTT over TCP - ## - ssl: MQTT over TLS ## - ws: MQTT over Websocket - ## - wss: MQTT over WebSocket Secure ## Required: true - type: wss + type: ws ## The IP address and port that the listener will bind. ## @@ -1566,9 +1676,9 @@ zone.default { ## The size of the acceptor pool for this listener. ## ## @doc zone..listeners..acceptors - ## ValueType: Number | num_cpu_cores - ## Default: num_cpu_cores - acceptors: num_cpu_cores + ## ValueType: Number + ## Default: 16 + acceptors: 16 ## Maximum number of concurrent connections. ## @@ -1611,8 +1721,64 @@ zone.default { ## Default: 3s proxy_protocol_timeout: 3s + rate_limit { + ## Maximum connections per second. + ## + ## @doc zone..max_conn_rate + ## ValueType: Number | infinity + ## Default: 1000 + ## Examples: + ## max_conn_rate: 1000 + max_conn_rate: 1000 + + ## Message limit for the a external MQTT connection. + ## + ## @doc zone..rate_limit.conn_messages_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messages per 10 seconds. + ## conn_messages_in: "100,10s" + conn_messages_in: "100,10s" + + ## Limit the rate of receiving packets for a MQTT connection. + ## The rate is counted by bytes of packets per second. + ## + ## The connection won't accept more messages if the messages come + ## faster than the limit. + ## + ## @doc zone..rate_limit.conn_bytes_in + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100KB incoming per 10 seconds. + ## conn_bytes_in: "100KB,10s" + ## + conn_bytes_in: "100KB,10s" + + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + quota.conn_messages_routing: "100,1s" + + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## @doc zone..rate_limit.quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + quota.overall_messages_routing: "200000,1s" + } + ## SSL options ## See ${example_common_ssl_options} for more information + ssl.enable: true ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index a2644f353..292d5569a 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -87,7 +87,7 @@ fields("cluster") -> ]; fields("static") -> - [ {"seeds", t(list(string()))}]; + [ {"seeds", t(hoconsc:array(string()))}]; fields("mcast") -> [ {"addr", t(string(), undefined, "239.192.0.1")} @@ -164,46 +164,56 @@ fields("rpc") -> ]; fields("log") -> - [ {"to", t(union([file, console, both]), undefined, file)} - , {"level", t(log_level(), undefined, warning)} + [ {"primary_level", t(log_level(), undefined, warning)} + , {"console_handler", ref("console_handler")} + , {"file_handlers", ref("file_handlers")} , {"time_offset", t(string(), undefined, "system")} - , {"primary_log_level", t(log_level(), undefined, warning)} - , {"dir", t(string(), undefined, "log")} - , {"file", t(file(), undefined, "emqx.log")} - , {"chars_limit", t(integer(), undefined, -1)} + , {"chars_limit", maybe_infinity(integer())} , {"supervisor_reports", t(union([error, progress]), undefined, error)} , {"max_depth", t(union([infinity, integer()]), "kernel.error_logger_format_depth", 80)} , {"formatter", t(union([text, json]), undefined, text)} , {"single_line", t(boolean(), undefined, true)} - , {"rotation", ref("rotation")} - , {"size", t(union(bytesize(), infinity), undefined, infinity)} , {"sync_mode_qlen", t(integer(), undefined, 100)} , {"drop_mode_qlen", t(integer(), undefined, 3000)} , {"flush_qlen", t(integer(), undefined, 8000)} - , {"overload_kill", t(flag(), undefined, true)} - , {"overload_kill_mem_size", t(bytesize(), undefined, "30MB")} - , {"overload_kill_qlen", t(integer(), undefined, 20000)} - , {"overload_kill_restart_after", t(union(duration(), infinity), undefined, "5s")} - , {"burst_limit", t(comma_separated_list(), undefined, "disabled")} + , {"overload_kill", ref("log_overload_kill")} + , {"burst_limit", ref("log_burst_limit")} , {"error_logger", t(atom(), "kernel.error_logger", silent)} - , {"debug", ref("additional_log_file")} - , {"info", ref("additional_log_file")} - , {"notice", ref("additional_log_file")} - , {"warning", ref("additional_log_file")} - , {"error", ref("additional_log_file")} - , {"critical", ref("additional_log_file")} - , {"alert", ref("additional_log_file")} - , {"emergency", ref("additional_log_file")} ]; -fields("additional_log_file") -> - [ {"file", t(string())}]; +fields("console_handler") -> + [ {"enable", t(flag(), undefined, false)} + , {"level", t(log_level(), undefined, warning)} + ]; -fields("rotation") -> +fields("file_handlers") -> + [ {"$name", ref("log_file_handler")} + ]; + +fields("log_file_handler") -> + [ {"level", t(log_level(), undefined, warning)} + , {"file", t(file(), undefined, undefined)} + , {"rotation", ref("log_rotation")} + , {"max_size", maybe_infinity(bytesize(), "10MB")} + ]; + +fields("log_rotation") -> [ {"enable", t(flag(), undefined, true)} - , {"size", t(bytesize(), undefined, "10MB")} - , {"count", t(integer(), undefined, 5)} + , {"count", t(range(1, 2048), undefined, 10)} + ]; + +fields("log_overload_kill") -> + [ {"enable", t(flag(), undefined, true)} + , {"mem_size", t(bytesize(), undefined, "30MB")} + , {"qlen", t(integer(), undefined, 20000)} + , {"restart_after", t(union(duration(), infinity), undefined, "5s")} + ]; + +fields("log_burst_limit") -> + [ {"enable", t(flag(), undefined, true)} + , {"max_count", t(integer(), undefined, 10000)} + , {"window_time", t(duration(), undefined, "1s")} ]; fields("lager") -> @@ -227,16 +237,16 @@ fields("acl") -> fields("acl_cache") -> [ {"enable", t(boolean(), undefined, true)} - , {"max_size", t(range(1, 1048576), undefined, 32)} + , {"max_size", maybe_infinity(range(1, 1048576), 32)} , {"ttl", t(duration(), undefined, "1m")} ]; fields("mqtt") -> [ {"mountpoint", t(binary(), undefined, <<"">>)} - , {"idle_timeout", t(duration(), undefined, "15s")} - , {"max_packet_size", t(bytesize(), undefined, "1MB")} + , {"idle_timeout", maybe_infinity(duration(), "15s")} + , {"max_packet_size", maybe_infinity(bytesize(), "1MB")} , {"max_clientid_len", t(integer(), undefined, 65535)} - , {"max_topic_levels", t(integer(), undefined, 0)} + , {"max_topic_levels", t(integer(), undefined, 65535)} , {"max_qos_allowed", t(range(0, 2), undefined, 2)} , {"max_topic_alias", t(integer(), undefined, 65535)} , {"retain_available", t(boolean(), undefined, true)} @@ -245,16 +255,16 @@ fields("mqtt") -> , {"ignore_loop_deliver", t(boolean())} , {"strict_mode", t(boolean(), undefined, false)} , {"response_information", t(string(), undefined, undefined)} - , {"server_keepalive", t(integer())} + , {"server_keepalive", maybe_disabled(integer())} , {"keepalive_backoff", t(float(), undefined, 0.75)} - , {"max_subscriptions", t(integer(), undefined, 0)} + , {"max_subscriptions", maybe_infinity(integer())} , {"upgrade_qos", t(flag(), undefined, false)} - , {"max_inflight", t(range(0, 65535))} + , {"max_inflight", t(range(1, 65535))} , {"retry_interval", t(duration_s(), undefined, "30s")} - , {"max_awaiting_rel", t(duration(), undefined, 0)} + , {"max_awaiting_rel", maybe_infinity(duration())} , {"await_rel_timeout", t(duration_s(), undefined, "300s")} , {"session_expiry_interval", t(duration_s(), undefined, "2h")} - , {"max_mqueue_len", t(integer(), undefined, 1000)} + , {"max_mqueue_len", maybe_infinity(integer(), 1000)} , {"mqueue_priorities", t(comma_separated_list(), undefined, "none")} , {"mqueue_default_priority", t(union(highest, lowest), undefined, lowest)} , {"mqueue_store_qos0", t(boolean(), undefined, true)} @@ -275,20 +285,20 @@ fields("zone_settings") -> , {"force_shutdown", ref("force_shutdown")} , {"conn_congestion", ref("conn_congestion")} , {"force_gc", ref("force_gc")} - , {"overall_max_connections", t(integer(), undefined, 2048000)} + , {"overall_max_connections", maybe_infinity(integer())} , {"listeners", t("listeners")} ]; fields("rate_limit") -> [ {"max_conn_rate", maybe_infinity(integer(), 1000)} - , {"conn_messages_in", t(comma_separated_list())} - , {"conn_bytes_in", t(comma_separated_list())} + , {"conn_messages_in", maybe_infinity(comma_separated_list())} + , {"conn_bytes_in", maybe_infinity(comma_separated_list())} , {"quota", ref("rate_limit_quota")} ]; fields("rate_limit_quota") -> - [ {"conn_messages_routing", t(comma_separated_list())} - , {"overall_messages_routing", t(comma_separated_list())} + [ {"conn_messages_routing", maybe_infinity(comma_separated_list())} + , {"overall_messages_routing", maybe_infinity(comma_separated_list())} ]; fields("flapping_detect") -> @@ -331,32 +341,19 @@ fields("force_gc") -> fields("listeners") -> [ {"$name", hoconsc:union( - [ ref("mqtt_tcp_listener") - , ref("mqtt_ssl_listener") - , ref("mqtt_ws_listener") - , ref("mqtt_wss_listener") + [ hoconsc:ref("mqtt_tcp_listener") + , hoconsc:ref("mqtt_ws_listener") ])} ]; fields("mqtt_tcp_listener") -> - [ {"type", t(typerefl:atom(tcp))} + [ {"type", t(tcp)} , {"tcp", ref("tcp_opts")} - ] ++ mqtt_listener(); - -fields("mqtt_ssl_listener") -> - [ {"type", t(typerefl:atom(ssl))} , {"ssl", ref("ssl_opts")} - , {"tcp", ref("tcp_opts")} ] ++ mqtt_listener(); fields("mqtt_ws_listener") -> - [ {"type", t(typerefl:atom(ws))} - , {"tcp", ref("tcp_opts")} - , {"websocket", ref("ws_opts")} - ] ++ mqtt_listener(); - -fields("mqtt_wss_listener") -> - [ {"type", t(typerefl:atom(ws))} + [ {"type", t(ws)} , {"tcp", ref("tcp_opts")} , {"ssl", ref("ssl_opts")} , {"websocket", ref("ws_opts")} @@ -366,8 +363,8 @@ fields("ws_opts") -> [ {"mqtt_path", t(string(), undefined, "/mqtt")} , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} , {"compress", t(boolean())} - , {"idle_timeout", t(duration())} - , {"max_frame_size", t(integer())} + , {"idle_timeout", maybe_infinity(duration())} + , {"max_frame_size", maybe_infinity(integer())} , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} , {"supported_subprotocols", t(string(), undefined, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} @@ -480,7 +477,7 @@ fields("sysmon_os") -> [ {"cpu_check_interval", t(duration_s(), undefined, 60)} , {"cpu_high_watermark", t(percent(), undefined, "80%")} , {"cpu_low_watermark", t(percent(), undefined, "60%")} - , {"mem_check_interval", t(duration_s(), undefined, 60)} + , {"mem_check_interval", maybe_disabled(duration_s(), 60)} , {"sysmem_high_watermark", t(percent(), undefined, "70%")} , {"procmem_high_watermark", t(percent(), undefined, "5%")} ]; @@ -503,9 +500,10 @@ fields(ExtraField) -> mqtt_listener() -> [ {"bind", t(union(ip_port(), integer()))} - , {"acceptors", t(integer(), undefined, 8)} - , {"max_connections", t(integer(), undefined, 1024000)} - , {"access", t(list(string()))} + , {"acceptors", t(integer(), undefined, 16)} + , {"max_connections", maybe_infinity(integer(), infinity)} + , {"rate_limit", ref("rate_limit")} + , {"access_rules", t(hoconsc:array(string()))} , {"proxy_protocol", t(flag())} , {"proxy_protocol_timeout", t(duration())} ]. @@ -1050,7 +1048,8 @@ filter(Opts) -> %% ...] ssl(Defaults) -> D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"cacertfile", t(string(), undefined, D("cacertfile"))} + [ {"enable", t(flag(), undefined, D("enable"))} + , {"cacertfile", t(string(), undefined, D("cacertfile"))} , {"certfile", t(string(), undefined, D("certfile"))} , {"keyfile", t(string(), undefined, D("keyfile"))} , {"verify", t(union(verify_peer, verify_none), undefined, D("verify"))} @@ -1176,8 +1175,8 @@ maybe_disabled(T) -> maybe_disabled(T, Default) -> maybe_sth(disabled, T, Default). -% maybe_infinity(T) -> -% maybe_sth(infinity, T, infinity). +maybe_infinity(T) -> + maybe_sth(infinity, T, infinity). maybe_infinity(T, Default) -> maybe_sth(infinity, T, Default). @@ -1215,7 +1214,7 @@ to_bytesize(Str) -> to_wordsize(Str) -> WordSize = erlang:system_info(wordsize), case to_bytesize(Str) of - {ok, Bytes} -> Bytes div WordSize; + {ok, Bytes} -> {ok, Bytes div WordSize}; Error -> Error end. diff --git a/bin/emqx b/bin/emqx index a5f76ac72..70b878715 100755 --- a/bin/emqx +++ b/bin/emqx @@ -582,7 +582,7 @@ case "$1" in # set before generate_config if [ "${_EMQX_START_MODE:-}" = '' ]; then - export EMQX_LOG__TO="${EMQX_LOG__TO:-console}" + export EMQX_CONSOLE_HANDLER__ENABLE="${EMQX_CONSOLE_HANDLER__ENABLE:-true}" fi #generate app.config and vm.args @@ -626,7 +626,7 @@ case "$1" in # or other supervision services # set before generate_config - export EMQX_LOG__TO="${EMQX_LOG__TO:-console}" + export EMQX_CONSOLE_HANDLER__ENABLE="${EMQX_CONSOLE_HANDLER__ENABLE:-true}" #generate app.config and vm.args generate_config From 860aea50db20d36850ea94dde4eb4ead6ab3921f Mon Sep 17 00:00:00 2001 From: lafirest Date: Tue, 29 Jun 2021 21:59:54 +0800 Subject: [PATCH 050/379] chore(emqx_retainer): change config of emqx_retainer to use hocon --- apps/emqx/src/emqx_schema.erl | 1 + apps/emqx_retainer/etc/emqx_retainer.conf | 64 +++++------ apps/emqx_retainer/priv/emqx_retainer.schema | 30 ----- apps/emqx_retainer/src/emqx_retainer.erl | 105 ++++++++++-------- apps/emqx_retainer/src/emqx_retainer_app.erl | 5 +- .../src/emqx_retainer_schema.erl | 31 ++++++ apps/emqx_retainer/src/emqx_retainer_sup.erl | 12 +- .../test/emqx_retainer_SUITE.erl | 26 +++-- 8 files changed, 151 insertions(+), 123 deletions(-) delete mode 100644 apps/emqx_retainer/priv/emqx_retainer.schema create mode 100644 apps/emqx_retainer/src/emqx_retainer_schema.erl diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 11ffe664a..cdc20762e 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -64,6 +64,7 @@ includes() ->[]. includes() -> [ "emqx_data_bridge" , "emqx_telemetry" + , "emqx_retainer" ]. -endif. diff --git a/apps/emqx_retainer/etc/emqx_retainer.conf b/apps/emqx_retainer/etc/emqx_retainer.conf index 4db438a98..08220a207 100644 --- a/apps/emqx_retainer/etc/emqx_retainer.conf +++ b/apps/emqx_retainer/etc/emqx_retainer.conf @@ -5,37 +5,39 @@ ## Where to store the retained messages. ## ## Notice that all nodes in the same cluster have to be configured to -## use the same storage_type. -## -## Value: ram | disc | disc_only -## - ram: memory only -## - disc: both memory and disc -## - disc_only: disc only -## -## Default: ram -retainer.storage_type = ram +emqx_retainer: { + ## use the same storage_type. + ## + ## Value: ram | disc | disc_only + ## - ram: memory only + ## - disc: both memory and disc + ## - disc_only: disc only + ## + ## Default: ram + storage_type: ram -## Maximum number of retained messages. 0 means no limit. -## -## Value: Number >= 0 -retainer.max_retained_messages = 0 + ## Maximum number of retained messages. 0 means no limit. + ## + ## Value: Number >= 0 + max_retained_messages: 0 -## Maximum retained message size. -## -## Value: Bytes -retainer.max_payload_size = 1MB + ## Maximum retained message size. + ## + ## Value: Bytes + max_payload_size: 1MB -## Expiry interval of the retained messages. Never expire if the value is 0. -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 20s: 20 seconds -## -## Default: 0 -retainer.expiry_interval = 0 + ## Expiry interval of the retained messages. Never expire if the value is 0. + ## + ## Value: Duration + ## - h: hour + ## - m: minute + ## - s: second + ## + ## Examples: + ## - 2h: 2 hours + ## - 30m: 30 minutes + ## - 20s: 20 seconds + ## + ## Default: 0s + expiry_interval: 0s +} diff --git a/apps/emqx_retainer/priv/emqx_retainer.schema b/apps/emqx_retainer/priv/emqx_retainer.schema deleted file mode 100644 index e598864e1..000000000 --- a/apps/emqx_retainer/priv/emqx_retainer.schema +++ /dev/null @@ -1,30 +0,0 @@ -%%-*- mode: erlang -*- -%% Retainer config mapping - -%% Storage Type -%% {$configurable} -{mapping, "retainer.storage_type", "emqx_retainer.storage_type", [ - {default, ram}, - {datatype, {enum, [ram, disc, disc_only]}} -]}. - -%% Maximum number of retained messages. -%% {$configurable} -{mapping, "retainer.max_retained_messages", "emqx_retainer.max_retained_messages", [ - {default, 0}, - {datatype, integer} -]}. - -%% Maximum payload size of retained message. -%% {$configurable} -{mapping, "retainer.max_payload_size", "emqx_retainer.max_payload_size", [ - {default, "1MB"}, - {datatype, bytesize} -]}. - -%% Expiry interval of retained message -%% {$configurable} -{mapping, "retainer.expiry_interval", "emqx_retainer.expiry_interval", [ - {default, 0}, - {datatype, [integer, {duration, ms}]} -]}. diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 94c561b39..affbc5ca3 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -25,14 +25,14 @@ -logger_header("[Retainer]"). --export([start_link/1]). +-export([start_link/0]). --export([ load/1 +-export([ load/0 , unload/0 ]). -export([ on_session_subscribed/3 - , on_message_publish/2 + , on_message_publish/1 ]). -export([clean/1]). @@ -51,15 +51,25 @@ -record(state, {stats_fun, stats_timer, expiry_timer}). +-define(STATS_INTERVAL, timer:seconds(1)). +-define(DEF_STORAGE_TYPE, ram). +-define(DEF_MAX_RETAINED_MESSAGES, 0). +-define(DEF_MAX_PAYLOAD_SIZE, (1024 * 1024)). +-define(DEF_EXPIRY_INTERVAL, 0). + +%% convenient to generate stats_timer/expiry_timer +-define(MAKE_TIMER(State, Timer, Interval, Msg), + State#state{Timer = erlang:send_after(Interval, self(), Msg)}). + -rlog_shard({?RETAINER_SHARD, ?TAB}). %%-------------------------------------------------------------------- %% Load/Unload %%-------------------------------------------------------------------- -load(Env) -> +load() -> _ = emqx:hook('session.subscribed', {?MODULE, on_session_subscribed, []}), - _ = emqx:hook('message.publish', {?MODULE, on_message_publish, [Env]}), + _ = emqx:hook('message.publish', {?MODULE, on_message_publish, []}), ok. unload() -> @@ -85,15 +95,15 @@ dispatch(Pid, Topic) -> %% RETAIN flag set to 1 and payload containing zero bytes on_message_publish(Msg = #message{flags = #{retain := true}, topic = Topic, - payload = <<>>}, _Env) -> + payload = <<>>}) -> ekka_mnesia:dirty_delete(?TAB, topic2tokens(Topic)), {ok, Msg}; -on_message_publish(Msg = #message{flags = #{retain := true}}, Env) -> +on_message_publish(Msg = #message{flags = #{retain := true}}) -> Msg1 = emqx_message:set_header(retained, true, Msg), - store_retained(Msg1, Env), + store_retained(Msg1), {ok, Msg}; -on_message_publish(Msg, _Env) -> +on_message_publish(Msg) -> {ok, Msg}. %%-------------------------------------------------------------------- @@ -101,9 +111,9 @@ on_message_publish(Msg, _Env) -> %%-------------------------------------------------------------------- %% @doc Start the retainer --spec(start_link(Env :: list()) -> emqx_types:startlink_ret()). -start_link(Env) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [Env], []). +-spec(start_link() -> emqx_types:startlink_ret()). +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec(clean(emqx_types:topic()) -> non_neg_integer()). clean(Topic) when is_binary(Topic) -> @@ -124,8 +134,10 @@ clean(Topic) when is_binary(Topic) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([Env]) -> - Copies = case proplists:get_value(storage_type, Env, disc) of +init([]) -> + StorageType = emqx_config:get([?MODULE, storage_type], ?DEF_STORAGE_TYPE), + ExpiryInterval = emqx_config:get([?MODULE, expiry_interval], ?DEF_EXPIRY_INTERVAL), + Copies = case StorageType of ram -> ram_copies; disc -> disc_copies; disc_only -> disc_only_copies @@ -149,17 +161,15 @@ init([Env]) -> ok end, StatsFun = emqx_stats:statsfun('retained.count', 'retained.max'), - {ok, StatsTimer} = timer:send_interval(timer:seconds(1), stats), - State = #state{stats_fun = StatsFun, stats_timer = StatsTimer}, - {ok, start_expire_timer(proplists:get_value(expiry_interval, Env, 0), State)}. + State = ?MAKE_TIMER(#state{stats_fun = StatsFun}, stats_timer, ?STATS_INTERVAL, stats), + {ok, start_expire_timer(ExpiryInterval, State)}. start_expire_timer(0, State) -> State; start_expire_timer(undefined, State) -> State; start_expire_timer(Ms, State) -> - {ok, Timer} = timer:send_interval(Ms, expire), - State#state{expiry_timer = Timer}. + ?MAKE_TIMER(State, expiry_timer, Ms, expire). handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), @@ -171,19 +181,20 @@ handle_cast(Msg, State) -> handle_info(stats, State = #state{stats_fun = StatsFun}) -> StatsFun(retained_count()), - {noreply, State, hibernate}; + {noreply, ?MAKE_TIMER(State, stats_timer, ?STATS_INTERVAL, stats), hibernate}; handle_info(expire, State) -> ok = expire_messages(), - {noreply, State, hibernate}; + Interval = emqx_config:get([?MODULE, expiry_interval], ?DEF_EXPIRY_INTERVAL), + {noreply, start_expire_timer(Interval, State), hibernate}; handle_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{stats_timer = TRef1, expiry_timer = TRef2}) -> - _ = timer:cancel(TRef1), - _ = timer:cancel(TRef2), + _ = erlang:cancel_timer(TRef1), + _ = erlang:cancel_timer(TRef2), ok. code_change(_OldVsn, State, _Extra) -> @@ -192,31 +203,33 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- - sort_retained([]) -> []; sort_retained([Msg]) -> [Msg]; sort_retained(Msgs) -> lists:sort(fun(#message{timestamp = Ts1}, #message{timestamp = Ts2}) -> - Ts1 =< Ts2 - end, Msgs). + Ts1 =< Ts2 end, + Msgs). -store_retained(Msg = #message{topic = Topic, payload = Payload}, Env) -> - case {is_table_full(Env), is_too_big(size(Payload), Env)} of +store_retained(Msg = #message{topic = Topic, payload = Payload}) -> + case {is_table_full(), is_too_big(size(Payload))} of {false, false} -> ok = emqx_metrics:inc('messages.retained'), ekka_mnesia:dirty_write(?TAB, #retained{topic = topic2tokens(Topic), msg = Msg, - expiry_time = get_expiry_time(Msg, Env)}); + expiry_time = get_expiry_time(Msg)}); {true, false} -> {atomic, _} = ekka_mnesia:transaction(?RETAINER_SHARD, fun() -> - case mnesia:read(?TAB, Topic) of - [_] -> - mnesia:write(?TAB, #retained{topic = topic2tokens(Topic), - msg = Msg, - expiry_time = get_expiry_time(Msg, Env)}, write); - [] -> - ?LOG(error, "Cannot retain message(topic=~s) for table is full!", [Topic]) + case mnesia:read(?TAB, Topic) of + [_] -> + mnesia:write(?TAB, + #retained{topic = topic2tokens(Topic), + msg = Msg, + expiry_time = get_expiry_time(Msg)}, + write); + [] -> + ?LOG(error, + "Cannot retain message(topic=~s) for table is full!", [Topic]) end end), ok; @@ -227,22 +240,24 @@ store_retained(Msg = #message{topic = Topic, payload = Payload}, Env) -> "for payload is too big!", [Topic, iolist_size(Payload)]) end. -is_table_full(Env) -> - Limit = proplists:get_value(max_retained_messages, Env, 0), +is_table_full() -> + Limit = emqx_config:get([?MODULE, max_retained_messages], ?DEF_MAX_RETAINED_MESSAGES), Limit > 0 andalso (retained_count() > Limit). -is_too_big(Size, Env) -> - Limit = proplists:get_value(max_payload_size, Env, 0), +is_too_big(Size) -> + Limit = emqx_config:get([?MODULE, max_payload_size], ?DEF_MAX_PAYLOAD_SIZE), Limit > 0 andalso (Size > Limit). -get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}, _Env) -> +get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}) -> 0; -get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, timestamp = Ts}, _Env) -> +get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, + timestamp = Ts}) -> Ts + Interval * 1000; -get_expiry_time(#message{timestamp = Ts}, Env) -> - case proplists:get_value(expiry_interval, Env, 0) of +get_expiry_time(#message{timestamp = Ts}) -> + Interval = emqx_config:get([?MODULE, expiry_interval], ?DEF_EXPIRY_INTERVAL), + case Interval of 0 -> 0; - Interval -> Ts + Interval + _ -> Ts + Interval end. %%-------------------------------------------------------------------- diff --git a/apps/emqx_retainer/src/emqx_retainer_app.erl b/apps/emqx_retainer/src/emqx_retainer_app.erl index adca5ae7a..3f42ddbd6 100644 --- a/apps/emqx_retainer/src/emqx_retainer_app.erl +++ b/apps/emqx_retainer/src/emqx_retainer_app.erl @@ -25,9 +25,8 @@ ]). start(_Type, _Args) -> - Env = application:get_all_env(emqx_retainer), - {ok, Sup} = emqx_retainer_sup:start_link(Env), - emqx_retainer:load(Env), + {ok, Sup} = emqx_retainer_sup:start_link(), + emqx_retainer:load(), emqx_retainer_cli:load(), {ok, Sup}. diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl new file mode 100644 index 000000000..ece873dae --- /dev/null +++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl @@ -0,0 +1,31 @@ +-module(emqx_retainer_schema). + +-include_lib("typerefl/include/types.hrl"). + +-type storage_type() :: ram | disc | disc_only. + +-reflect_type([storage_type/0]). + +-export([structs/0, fields/1]). + +structs() -> ["emqx_retainer"]. + +fields("emqx_retainer") -> + [ {storage_type, t(storage_type(), ram)} + , {max_retained_messages, t(integer(), 0, fun is_pos_integer/1)} + , {max_payload_size, t(emqx_schema:bytesize(), "1MB")} + , {expiry_interval, t(emqx_schema:duration_ms(), "0s")} + ]. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +t(Type, Default) -> + hoconsc:t(Type, #{default => Default}). + +t(Type, Default, Validator) -> + hoconsc:t(Type, #{default => Default, + validator => Validator}). + +is_pos_integer(V) -> + V >= 0. diff --git a/apps/emqx_retainer/src/emqx_retainer_sup.erl b/apps/emqx_retainer/src/emqx_retainer_sup.erl index fef245489..d01c20975 100644 --- a/apps/emqx_retainer/src/emqx_retainer_sup.erl +++ b/apps/emqx_retainer/src/emqx_retainer_sup.erl @@ -18,17 +18,17 @@ -behaviour(supervisor). --export([start_link/1]). +-export([start_link/0]). -export([init/1]). -start_link(Env) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [Env]). +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). -init([Env]) -> - {ok, {{one_for_one, 10, 3600}, +init([]) -> + {ok, {{one_for_one, 10, 3600}, [#{id => retainer, - start => {emqx_retainer, start_link, [Env]}, + start => {emqx_retainer, start_link, []}, restart => permanent, shutdown => 5000, type => worker, diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index 1df042dd9..1a0a00a1c 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl @@ -31,7 +31,7 @@ all() -> emqx_ct:all(?MODULE). %%-------------------------------------------------------------------- init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_retainer]), + emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), Config. end_per_suite(_Config) -> @@ -39,16 +39,26 @@ end_per_suite(_Config) -> init_per_testcase(TestCase, Config) -> emqx_retainer:clean(<<"#">>), - case TestCase of - t_message_expiry_2 -> - application:set_env(emqx_retainer, expiry_interval, 2000); - _ -> - application:set_env(emqx_retainer, expiry_interval, 0) - end, - application:stop(emqx_retainer), + Interval = case TestCase of + t_message_expiry_2 -> 2000; + _ -> 0 + end, + init_emqx_retainer_conf(Interval), application:ensure_all_started(emqx_retainer), Config. +set_special_configs(emqx_retainer) -> + init_emqx_retainer_conf(0); +set_special_configs(_) -> + ok. + +init_emqx_retainer_conf(Expiry) -> + emqx_config:put([emqx_retainer], + #{storage_type => ram, + max_retained_messages => 0, + max_payload_size => 1024 * 1024, + expiry_interval => Expiry}). + %%-------------------------------------------------------------------- %% Test Cases %%-------------------------------------------------------------------- From d4f726419f456d2674fe28a35ece06a54d66d210 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Thu, 1 Jul 2021 10:51:59 +0800 Subject: [PATCH 051/379] feat(bridge-mqtt): Update the configuration file to hocon (#5142) --- .../etc/emqx_bridge_mqtt.conf | 222 ++------ .../priv/emqx_bridge_mqtt.schema | 244 --------- .../src/emqx_bridge_connect.erl | 74 --- .../emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl | 90 ++-- .../src/emqx_bridge_mqtt_schema.erl | 89 ++++ .../src/emqx_bridge_mqtt_sup.erl | 39 +- apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl | 25 +- .../src/emqx_bridge_worker.erl | 293 +++++------ .../test/emqx_bridge_mqtt_tests.erl | 2 +- .../test/emqx_bridge_rpc_tests.erl | 4 +- .../test/emqx_bridge_stub_conn.erl | 3 - .../test/emqx_bridge_worker_SUITE.erl | 475 ++++++++++-------- .../test/emqx_bridge_worker_tests.erl | 39 +- .../src/emqx_bridge_mqtt_actions.erl | 2 +- data/loaded_plugins.tmpl | 1 - rebar.config.erl | 7 +- 16 files changed, 615 insertions(+), 994 deletions(-) delete mode 100644 apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema delete mode 100644 apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl create mode 100644 apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl diff --git a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf index 3cc719e40..4593e04f0 100644 --- a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf +++ b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf @@ -2,173 +2,55 @@ ## Configuration for EMQ X MQTT Broker Bridge ##==================================================================== -##-------------------------------------------------------------------- -## Bridges to aws -##-------------------------------------------------------------------- - -## Bridge address: node name for local bridge, host:port for remote. -## -## Value: String -## Example: emqx@127.0.0.1, "127.0.0.1:1883" -bridge.mqtt.aws.address = "127.0.0.1:1883" - -## Protocol version of the bridge. -## -## Value: Enum -## - mqttv5 -## - mqttv4 -## - mqttv3 -bridge.mqtt.aws.proto_ver = mqttv4 - -## Start type of the bridge. -## -## Value: enum -## manual -## auto -bridge.mqtt.aws.start_type = manual - -## Whether to enable bridge mode for mqtt bridge -## -## This option is prepared for the mqtt broker which does not -## support bridge_mode such as the mqtt-plugin of the rabbitmq -## -## Value: boolean -#bridge.mqtt.aws.bridge_mode = false - -## The ClientId of a remote bridge. -## -## Placeholders: -## ${node}: Node name -## -## Value: String -bridge.mqtt.aws.clientid = bridge_aws - -## The Clean start flag of a remote bridge. -## -## Value: boolean -## Default: true -## -## NOTE: Some IoT platforms require clean_start -## must be set to 'true' -bridge.mqtt.aws.clean_start = true - -## The username for a remote bridge. -## -## Value: String -bridge.mqtt.aws.username = user - -## The password for a remote bridge. -## -## Value: String -bridge.mqtt.aws.password = passwd - -## Topics that need to be forward to AWS IoTHUB -## -## Value: String -## Example: "topic1/#,topic2/#" -bridge.mqtt.aws.forwards = "topic1/#,topic2/#" - -## Forward messages to the mountpoint of an AWS IoTHUB -## -## Value: String -bridge.mqtt.aws.forward_mountpoint = "bridge/aws/${node}/" - -## Need to subscribe to AWS topics -## -## Value: String -## bridge.mqtt.aws.subscription.1.topic = "cmd/topic1" - -## Need to subscribe to AWS topics QoS. -## -## Value: Number -## bridge.mqtt.aws.subscription.1.qos = 1 - -## A mountpoint that receives messages from AWS IoTHUB -## -## Value: String -## bridge.mqtt.aws.receive_mountpoint = "receive/aws/" - - -## Bribge to remote server via SSL. -## -## Value: on | off -bridge.mqtt.aws.ssl = off - -## PEM-encoded CA certificates of the bridge. -## -## Value: File -bridge.mqtt.aws.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - -## Client SSL Certfile of the bridge. -## -## Value: File -bridge.mqtt.aws.certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" - -## Client SSL Keyfile of the bridge. -## -## Value: File -bridge.mqtt.aws.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" - -## SSL Ciphers used by the bridge. -## -## Value: String -bridge.mqtt.aws.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" - -## Ciphers for TLS PSK. -## Note that 'bridge.${BridgeName}.ciphers' and 'bridge.${BridgeName}.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -#bridge.mqtt.aws.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" - -## Ping interval of a down bridge. -## -## Value: Duration -## Default: 10 seconds -bridge.mqtt.aws.keepalive = 60s - -## TLS versions used by the bridge. -## -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## Value: String -bridge.mqtt.aws.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" - -## Bridge reconnect time. -## -## Value: Duration -## Default: 30 seconds -bridge.mqtt.aws.reconnect_interval = 30s - -## Retry interval for bridge QoS1 message delivering. -## -## Value: Duration -bridge.mqtt.aws.retry_interval = 20s - -## Publish messages in batches, only RPC Bridge supports -## -## Value: Integer -## default: 32 -bridge.mqtt.aws.batch_size = 32 - -## Inflight size. -## 0 means infinity (no limit on the inflight window) -## -## Value: Integer -bridge.mqtt.aws.max_inflight_size = 32 - -## Base directory for replayq to store messages on disk -## If this config entry is missing or set to undefined, -## replayq works in a mem-only manner. -## -## Value: String -bridge.mqtt.aws.queue.replayq_dir = "{{ platform_data_dir }}/replayq/emqx_aws_bridge/" - -## Replayq segment size -## -## Value: Bytesize -bridge.mqtt.aws.queue.replayq_seg_bytes = 10MB - -## Replayq max total size -## -## Value: Bytesize -bridge.mqtt.aws.queue.max_total_size = 5GB - +emqx_bridge_mqtt:{ + bridges:[{ + name: "mqtt1" + start_type: auto + forwards: ["test/#"], + forward_mountpoint: "" + reconnect_interval: "30s" + batch_size: 100 + queue:{ + replayq_dir: false + # replayq_seg_bytes: "100MB" + # replayq_offload_mode: false + # replayq_max_total_bytes: "1GB" + }, + config:{ + conn_type: mqtt + address: "127.0.0.1:1883" + proto_ver: v4 + bridge_mode: true + clientid: "client1" + clean_start: true + username: "username1" + password: "" + keepalive: 300 + subscriptions: [{ + topic: "t/#" + qos: 1 + }] + receive_mountpoint: "" + retry_interval: "30s" + max_inflight: 32 + } + }, + { + name: "rpc1" + start_type: auto + forwards: ["test/#"], + forward_mountpoint: "" + reconnect_interval: "30s" + batch_size: 100 + queue:{ + replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" + replayq_seg_bytes: "100MB" + replayq_offload_mode: false + replayq_max_total_bytes: "1GB" + }, + config:{ + conn_type: rpc + node: "emqx@127.0.0.1" + } + }] +} diff --git a/apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema b/apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema deleted file mode 100644 index 3168bfc14..000000000 --- a/apps/emqx_bridge_mqtt/priv/emqx_bridge_mqtt.schema +++ /dev/null @@ -1,244 +0,0 @@ -%%-*- mode: erlang -*- -%%-------------------------------------------------------------------- -%% Bridges -%%-------------------------------------------------------------------- -{mapping, "bridge.mqtt.$name.address", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.proto_ver", "emqx_bridge_mqtt.bridges", [ - {datatype, {enum, [mqttv3, mqttv4, mqttv5]}} -]}. - -{mapping, "bridge.mqtt.$name.bridge_mode", "emqx_bridge_mqtt.bridges", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "bridge.mqtt.$name.start_type", "emqx_bridge_mqtt.bridges", [ - {datatype, {enum, [manual, auto]}}, - {default, auto} -]}. - -{mapping, "bridge.mqtt.$name.clientid", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.clean_start", "emqx_bridge_mqtt.bridges", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "bridge.mqtt.$name.username", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.password", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.forwards", "emqx_bridge_mqtt.bridges", [ - {datatype, string}, - {default, ""} -]}. - -{mapping, "bridge.mqtt.$name.forward_mountpoint", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.subscription.$id.topic", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.subscription.$id.qos", "emqx_bridge_mqtt.bridges", [ - {datatype, integer} -]}. - -{mapping, "bridge.mqtt.$name.receive_mountpoint", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.ssl", "emqx_bridge_mqtt.bridges", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "bridge.mqtt.$name.cacertfile", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.certfile", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.keyfile", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.ciphers", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.psk_ciphers", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.keepalive", "emqx_bridge_mqtt.bridges", [ - {default, "10s"}, - {datatype, {duration, s}} -]}. - -{mapping, "bridge.mqtt.$name.tls_versions", "emqx_bridge_mqtt.bridges", [ - {datatype, string}, - {default, "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1"} -]}. - -{mapping, "bridge.mqtt.$name.reconnect_interval", "emqx_bridge_mqtt.bridges", [ - {default, "30s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "bridge.mqtt.$name.retry_interval", "emqx_bridge_mqtt.bridges", [ - {default, "20s"}, - {datatype, {duration, s}} -]}. - -{mapping, "bridge.mqtt.$name.max_inflight_size", "emqx_bridge_mqtt.bridges", [ - {default, 0}, - {datatype, integer} - ]}. - -{mapping, "bridge.mqtt.$name.batch_size", "emqx_bridge_mqtt.bridges", [ - {default, 0}, - {datatype, integer} -]}. - -{mapping, "bridge.mqtt.$name.queue.replayq_dir", "emqx_bridge_mqtt.bridges", [ - {datatype, string} -]}. - -{mapping, "bridge.mqtt.$name.queue.replayq_seg_bytes", "emqx_bridge_mqtt.bridges", [ - {datatype, bytesize} -]}. - -{mapping, "bridge.mqtt.$name.queue.max_total_size", "emqx_bridge_mqtt.bridges", [ - {datatype, bytesize} -]}. - -{translation, "emqx_bridge_mqtt.bridges", fun(Conf) -> - - MapPSKCiphers = fun(PSKCiphers) -> - lists:map( - fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha}; - ("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha}; - ("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha}; - ("PSK-RC4-SHA") -> {psk, rc4_128, sha} - end, PSKCiphers) - end, - - Split = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, - - IsSsl = fun(cacertfile) -> true; - (certfile) -> true; - (keyfile) -> true; - (ciphers) -> true; - (psk_ciphers) -> true; - (tls_versions) -> true; - (_Opt) -> false - end, - - Parse = fun(tls_versions, Vers) -> - [{versions, [list_to_atom(S) || S <- Split(Vers)]}]; - (ciphers, Ciphers) -> - [{ciphers, Split(Ciphers)}]; - (psk_ciphers, Ciphers) -> - [{ciphers, MapPSKCiphers(Split(Ciphers))}, {user_lookup_fun, {fun emqx_psk:lookup/3, <<>>}}]; - (Opt, Val) -> - [{Opt, Val}] - end, - - Merge = fun(forwards, Val, Opts) -> - [{forwards, string:tokens(Val, ",")}|Opts]; - (Opt, Val, Opts) -> - case IsSsl(Opt) of - true -> - SslOpts = Parse(Opt, Val) ++ proplists:get_value(ssl_opts, Opts, []), - lists:ukeymerge(1, [{ssl_opts, SslOpts}], lists:usort(Opts)); - false -> - [{Opt, Val}|Opts] - end - end, - Queue = fun(Name) -> - Configs = cuttlefish_variable:filter_by_prefix("bridge.mqtt." ++ Name ++ ".queue", Conf), - - QOpts = [{list_to_atom(QOpt), QValue}|| {[_, _, _, "queue", QOpt], QValue} <- Configs], - maps:from_list(QOpts) - end, - Subscriptions = fun(Name) -> - Configs = cuttlefish_variable:filter_by_prefix("bridge.mqtt." ++ Name ++ ".subscription", Conf), - lists:zip([Topic || {_, Topic} <- lists:sort([{I, Topic} || {[_, _, _, "subscription", I, "topic"], Topic} <- Configs])], - [QoS || {_, QoS} <- lists:sort([{I, QoS} || {[_, _, _, "subscription", I, "qos"], QoS} <- Configs])]) - end, - IsNodeAddr = fun(Addr) -> - case string:tokens(Addr, "@") of - [_NodeName, _Hostname] -> true; - _ -> false - end - end, - ConnMod = fun(Name) -> - - [AddrConfig] = cuttlefish_variable:filter_by_prefix("bridge.mqtt." ++ Name ++ ".address", Conf), - {_, Addr} = AddrConfig, - - Subs = Subscriptions(Name), - case IsNodeAddr(Addr) of - true when Subs =/= [] -> - error({"subscriptions are not supported when bridging between emqx nodes", Name, Subs}); - true -> - emqx_bridge_rpc; - false -> - emqx_bridge_mqtt - end - end, - - %% to be backward compatible - Translate = - fun Tr(queue, Q, Cfg) -> - NewQ = maps:fold(Tr, #{}, Q), - Cfg#{queue => NewQ}; - Tr(address, Addr0, Cfg) -> - Addr = case IsNodeAddr(Addr0) of - true -> list_to_atom(Addr0); - false -> Addr0 - end, - Cfg#{address => Addr}; - Tr(reconnect_interval, Ms, Cfg) -> - Cfg#{reconnect_delay_ms => Ms}; - Tr(proto_ver, Ver, Cfg) -> - Cfg#{proto_ver => - case Ver of - mqttv3 -> v3; - mqttv4 -> v4; - mqttv5 -> v5; - _ -> v4 - end}; - Tr(max_inflight_size, Size, Cfg) -> - Cfg#{max_inflight => Size}; - Tr(Key, Value, Cfg) -> - Cfg#{Key => Value} - end, - C = lists:foldl( - fun({["bridge", "mqtt", Name, Opt], Val}, Acc) -> - %% e.g #{aws => [{OptKey, OptVal}]} - Init = [{list_to_atom(Opt), Val}, - {connect_module, ConnMod(Name)}, - {subscriptions, Subscriptions(Name)}, - {queue, Queue(Name)}], - maps:update_with(list_to_atom(Name), fun(Opts) -> Merge(list_to_atom(Opt), Val, Opts) end, Init, Acc); - (_, Acc) -> Acc - end, #{}, lists:usort(cuttlefish_variable:filter_by_prefix("bridge.mqtt", Conf))), - C1 = maps:map(fun(Bn, Bc) -> - maps:to_list(maps:fold(Translate, #{}, maps:from_list(Bc))) - end, C), - maps:to_list(C1) -end}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl deleted file mode 100644 index ece6002a7..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_connect.erl +++ /dev/null @@ -1,74 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_connect). - --export([start/2]). - --export_type([config/0, connection/0]). - --optional_callbacks([ensure_subscribed/3, ensure_unsubscribed/2]). - -%% map fields depend on implementation --type(config() :: map()). --type(connection() :: term()). --type(batch() :: emqx_protal:batch()). --type(ack_ref() :: emqx_bridge_worker:ack_ref()). --type(topic() :: emqx_topic:topic()). --type(qos() :: emqx_mqtt_types:qos()). - --include_lib("emqx/include/logger.hrl"). - --logger_header("[Bridge Connect]"). - -%% establish the connection to remote node/cluster -%% protal worker (the caller process) should be expecting -%% a message {disconnected, conn_ref()} when disconnected. --callback start(config()) -> {ok, connection()} | {error, any()}. - -%% send to remote node/cluster -%% bridge worker (the caller process) should be expecting -%% a message {batch_ack, reference()} when batch is acknowledged by remote node/cluster --callback send(connection(), batch()) -> {ok, ack_ref()} | {ok, integer()} | {error, any()}. - -%% called when owner is shutting down. --callback stop(connection()) -> ok. - --callback ensure_subscribed(connection(), topic(), qos()) -> ok. - --callback ensure_unsubscribed(connection(), topic()) -> ok. - -start(Module, Config) -> - case Module:start(Config) of - {ok, Conn} -> - {ok, Conn}; - {error, Reason} -> - Config1 = obfuscate(Config), - ?LOG(error, "Failed to connect with module=~p\n" - "config=~p\nreason:~p", [Module, Config1, Reason]), - {error, Reason} - end. - -obfuscate(Map) -> - maps:fold(fun(K, V, Acc) -> - case is_sensitive(K) of - true -> [{K, '***'} | Acc]; - false -> [{K, V} | Acc] - end - end, [], Map). - -is_sensitive(password) -> true; -is_sensitive(_) -> false. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl index d612af668..8d442463b 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl @@ -18,15 +18,11 @@ -module(emqx_bridge_mqtt). --behaviour(emqx_bridge_connect). - -%% behaviour callbacks -export([ start/1 , send/2 , stop/1 ]). -%% optional behaviour callbacks -export([ ensure_subscribed/3 , ensure_unsubscribed/2 ]). @@ -37,6 +33,9 @@ , handle_disconnected/2 ]). +-export([ check_subscriptions/1 + ]). + -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -49,26 +48,31 @@ %% emqx_bridge_connect callbacks %%-------------------------------------------------------------------- -start(Config = #{address := Address}) -> +start(Config) -> Parent = self(), + Address = maps:get(address, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), + Subscriptions = maps:get(subscriptions, Config, []), + Subscriptions1 = check_subscriptions(Subscriptions), Handlers = make_hdlr(Parent, Mountpoint), {Host, Port} = case string:tokens(Address, ":") of [H] -> {H, 1883}; [H, P] -> {H, list_to_integer(P)} end, - ClientConfig = Config#{msg_handler => Handlers, - host => Host, - port => Port, - force_ping => true - }, - case emqtt:start_link(replvar(ClientConfig)) of + Config1 = Config#{ + msg_handler => Handlers, + host => Host, + port => Port, + force_ping => true, + proto_ver => maps:get(proto_ver, Config, v4) + }, + case emqtt:start_link(without_config(Config1)) of {ok, Pid} -> case emqtt:connect(Pid) of {ok, _} -> try - subscribe_remote_topics(Pid, maps:get(subscriptions, Config, [])), - {ok, #{client_pid => Pid}} + Subscriptions2 = subscribe_remote_topics(Pid, Subscriptions1), + {ok, #{client_pid => Pid, subscriptions => Subscriptions2}} catch throw : Reason -> ok = stop(#{client_pid => Pid}), @@ -86,25 +90,25 @@ stop(#{client_pid := Pid}) -> safe_stop(Pid, fun() -> emqtt:stop(Pid) end, 1000), ok. -ensure_subscribed(#{client_pid := Pid}, Topic, QoS) when is_pid(Pid) -> +ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when is_pid(Pid) -> case emqtt:subscribe(Pid, Topic, QoS) of - {ok, _, _} -> ok; - Error -> Error + {ok, _, _} -> Conn#{subscriptions => [{Topic, QoS}|Subs]}; + Error -> {error, Error} end; ensure_subscribed(_Conn, _Topic, _QoS) -> %% return ok for now %% next re-connect should should call start with new topic added to config ok. -ensure_unsubscribed(#{client_pid := Pid}, Topic) when is_pid(Pid) -> +ensure_unsubscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic) when is_pid(Pid) -> case emqtt:unsubscribe(Pid, Topic) of - {ok, _, _} -> ok; - Error -> Error + {ok, _, _} -> Conn#{subscriptions => lists:keydelete(Topic, 1, Subs)}; + Error -> {error, Error} end; -ensure_unsubscribed(_, _) -> +ensure_unsubscribed(Conn, _) -> %% return ok for now %% next re-connect should should call start with this topic deleted from config - ok. + Conn. safe_stop(Pid, StopF, Timeout) -> MRef = monitor(process, Pid), @@ -169,36 +173,18 @@ make_hdlr(Parent, Mountpoint) -> }. subscribe_remote_topics(ClientPid, Subscriptions) -> - lists:foreach(fun({Topic, Qos}) -> - case emqtt:subscribe(ClientPid, Topic, Qos) of - {ok, _, _} -> ok; - Error -> throw(Error) - end - end, Subscriptions). + lists:map(fun({Topic, Qos}) -> + case emqtt:subscribe(ClientPid, Topic, Qos) of + {ok, _, _} -> {Topic, Qos}; + Error -> throw(Error) + end + end, Subscriptions). -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- +without_config(Config) -> + maps:without([conn_type, address, receive_mountpoint, subscriptions], Config). -replvar(Options) -> - replvar([clientid, max_inflight], Options). - -replvar([], Options) -> - Options; -replvar([Key|More], Options) -> - case maps:get(Key, Options, undefined) of - undefined -> - replvar(More, Options); - Val -> - replvar(More, maps:put(Key, feedvar(Key, Val, Options), Options)) - end. - -%% ${node} => node() -feedvar(clientid, ClientId, _) -> - iolist_to_binary(re:replace(ClientId, "\\${node}", atom_to_list(node()))); - -feedvar(max_inflight, 0, _) -> - infinity; - -feedvar(max_inflight, Size, _) -> - Size. +check_subscriptions(Subscriptions) -> + lists:map(fun(#{qos := QoS, topic := Topic}) -> + true = emqx_topic:validate({filter, Topic}), + {Topic, QoS} + end, Subscriptions). diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl new file mode 100644 index 000000000..fcd2f2c1d --- /dev/null +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -0,0 +1,89 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_bridge_mqtt_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1]). + +structs() -> ["emqx_bridge_mqtt"]. + +fields("emqx_bridge_mqtt") -> + [ {bridges, hoconsc:array("bridges")} + ]; + +fields("bridges") -> + [ {name, emqx_schema:t(string(), undefined, true)} + , {start_type, fun start_type/1} + , {forwards, fun forwards/1} + , {forward_mountpoint, emqx_schema:t(string())} + , {reconnect_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} + , {batch_size, emqx_schema:t(integer(), undefined, 100)} + , {queue, emqx_schema:t(hoconsc:ref("queue"))} + , {config, hoconsc:union([hoconsc:ref("mqtt"), hoconsc:ref("rpc")])} + ]; + +fields("mqtt") -> + [ {conn_type, fun conn_type/1} + , {address, emqx_schema:t(string(), undefined, "127.0.0.1:1883")} + , {proto_ver, fun proto_ver/1} + , {bridge_mode, emqx_schema:t(boolean(), undefined, true)} + , {clientid, emqx_schema:t(string())} + , {username, emqx_schema:t(string())} + , {password, emqx_schema:t(string())} + , {clean_start, emqx_schema:t(boolean(), undefined, true)} + , {keepalive, emqx_schema:t(integer(), undefined, 300)} + , {subscriptions, hoconsc:array("subscriptions")} + , {receive_mountpoint, emqx_schema:t(string())} + , {retry_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} + , {max_inflight, emqx_schema:t(integer(), undefined, 32)} + ]; + +fields("rpc") -> + [ {conn_type, fun conn_type/1} + , {node, emqx_schema:t(atom(), undefined, 'emqx@127.0.0.1')} + ]; + +fields("subscriptions") -> + [ {topic, #{type => binary(), nullable => false}} + , {qos, emqx_schema:t(integer(), undefined, 1)} + ]; + +fields("queue") -> + [ {replayq_dir, hoconsc:union([boolean(), string()])} + , {replayq_seg_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "100MB")} + , {replayq_offload_mode, emqx_schema:t(boolean(), undefined, false)} + , {replayq_max_total_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "1024MB")} + ]. + +conn_type(type) -> hoconsc:enum([mqtt, rpc]); +conn_type(_) -> undefined. + +proto_ver(type) -> hoconsc:enum([v3, v4, v5]); +proto_ver(default) -> v4; +proto_ver(_) -> undefined. + +start_type(type) -> hoconsc:enum([auto, manual]); +start_type(default) -> auto; +start_type(_) -> undefined. + +forwards(type) -> hoconsc:array(binary()); +forwards(default) -> []; +forwards(_) -> undefined. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl index 80a11c1c0..0075b4a1d 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl @@ -24,37 +24,33 @@ %% APIs -export([ start_link/0 - , start_link/1 ]). --export([ create_bridge/2 +-export([ create_bridge/1 , drop_bridge/1 , bridges/0 - , is_bridge_exist/1 ]). %% supervisor callbacks -export([init/1]). --define(SUP, ?MODULE). -define(WORKER_SUP, emqx_bridge_worker_sup). -start_link() -> start_link(?SUP). +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). -start_link(Name) -> - supervisor:start_link({local, Name}, ?MODULE, Name). - -init(?SUP) -> - BridgesConf = application:get_env(?APP, bridges, []), +init([]) -> + BridgesConf = emqx_config:get([?APP, bridges], []), BridgeSpec = lists:map(fun bridge_spec/1, BridgesConf), SupFlag = #{strategy => one_for_one, intensity => 100, period => 10}, {ok, {SupFlag, BridgeSpec}}. -bridge_spec({Name, Config}) -> +bridge_spec(Config) -> + Name = list_to_atom(maps:get(name, Config)), #{id => Name, - start => {emqx_bridge_worker, start_link, [Name, Config]}, + start => {emqx_bridge_worker, start_link, [Config]}, restart => permanent, shutdown => 5000, type => worker, @@ -62,22 +58,15 @@ bridge_spec({Name, Config}) -> -spec(bridges() -> [{node(), map()}]). bridges() -> - [{Name, emqx_bridge_worker:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?SUP)]. + [{Name, emqx_bridge_worker:status(Name)} || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. --spec(is_bridge_exist(atom() | pid()) -> boolean()). -is_bridge_exist(Id) -> - case supervisor:get_childspec(?SUP, Id) of - {ok, _ChildSpec} -> true; - {error, _Error} -> false - end. +create_bridge(Config) -> + supervisor:start_child(?MODULE, bridge_spec(Config)). -create_bridge(Id, Config) -> - supervisor:start_child(?SUP, bridge_spec({Id, Config})). - -drop_bridge(Id) -> - case supervisor:terminate_child(?SUP, Id) of +drop_bridge(Name) -> + case supervisor:terminate_child(?MODULE, Name) of ok -> - supervisor:delete_child(?SUP, Id); + supervisor:delete_child(?MODULE, Name); {error, Error} -> ?LOG(error, "Delete bridge failed, error : ~p", [Error]), {error, Error} diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl index 0cf4b5bc5..33511cc03 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl @@ -18,9 +18,6 @@ -module(emqx_bridge_rpc). --behaviour(emqx_bridge_connect). - -%% behaviour callbacks -export([ start/1 , send/2 , stop/1 @@ -33,17 +30,15 @@ -type ack_ref() :: emqx_bridge_worker:ack_ref(). -type batch() :: emqx_bridge_worker:batch(). --type node_or_tuple() :: atom() | {atom(), term()}. - -define(HEARTBEAT_INTERVAL, timer:seconds(1)). -define(RPC, emqx_rpc). -start(#{address := Remote}) -> - case poke(Remote) of +start(#{node := RemoteNode}) -> + case poke(RemoteNode) of ok -> - Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), Remote]), - {ok, #{client_pid => Pid, address => Remote}}; + Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), RemoteNode]), + {ok, #{client_pid => Pid, remote_node => RemoteNode}}; Error -> Error end. @@ -62,9 +57,9 @@ stop(#{client_pid := Pid}) when is_pid(Pid) -> ok. %% @doc Callback for `emqx_bridge_connect' behaviour --spec send(#{address := node_or_tuple(), _ => _}, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{address := Remote}, Batch) -> - case ?RPC:call(Remote, ?MODULE, handle_send, [Batch]) of +-spec send(#{remote_node := atom(), _ => _}, batch()) -> {ok, ack_ref()} | {error, any()}. +send(#{remote_node := RemoteNode}, Batch) -> + case ?RPC:call(RemoteNode, ?MODULE, handle_send, [Batch]) of ok -> Ref = make_ref(), self() ! {batch_ack, Ref}, @@ -93,8 +88,8 @@ heartbeat(Parent, RemoteNode) -> end end. -poke(Node) -> - case ?RPC:call(Node, erlang, node, []) of - Node -> ok; +poke(RemoteNode) -> + case ?RPC:call(RemoteNode, erlang, node, []) of + RemoteNode -> ok; {badrpc, Reason} -> {error, Reason} end. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl index e7414683c..dfef6973e 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl @@ -66,7 +66,6 @@ %% APIs -export([ start_link/1 - , start_link/2 , register_metrics/0 , stop/1 ]). @@ -86,7 +85,6 @@ %% management APIs -export([ ensure_started/1 , ensure_stopped/1 - , ensure_stopped/2 , status/1 ]). @@ -125,14 +123,13 @@ -define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)). -define(DEFAULT_SEG_BYTES, (1 bsl 20)). -define(DEFAULT_MAX_TOTAL_SIZE, (1 bsl 31)). --define(NO_BRIDGE_HANDLER, undefined). %% @doc Start a bridge worker. Supported configs: %% start_type: 'manual' (default) or 'auto', when manual, bridge will stay %% at 'idle' state until a manual call to start it. %% connect_module: The module which implements emqx_bridge_connect behaviour %% and work as message batch transport layer -%% reconnect_delay_ms: Delay in milli-seconds for the bridge worker to retry +%% reconnect_interval: Delay in milli-seconds for the bridge worker to retry %% in case of transportation failure. %% max_inflight: Max number of batches allowed to send-ahead before receiving %% confirmation from remote node/cluster @@ -148,128 +145,98 @@ %% %% Find more connection specific configs in the callback modules %% of emqx_bridge_connect behaviour. -start_link(Config) when is_list(Config) -> - start_link(maps:from_list(Config)); -start_link(Config) -> - gen_statem:start_link(?MODULE, Config, []). - -start_link(Name, Config) when is_list(Config) -> - start_link(Name, maps:from_list(Config)); -start_link(Name, Config) -> - Name1 = name(Name), - gen_statem:start_link({local, Name1}, ?MODULE, Config#{name => Name1}, []). +start_link(Opts) when is_list(Opts) -> + start_link(maps:from_list(Opts)); +start_link(Opts) -> + case maps:get(name, Opts, undefined) of + undefined -> + gen_statem:start_link(?MODULE, Opts, []); + Name -> + Name1 = name(Name), + gen_statem:start_link({local, Name1}, ?MODULE, Opts#{name => Name1}, []) + end. ensure_started(Name) -> gen_statem:call(name(Name), ensure_started). %% @doc Manually stop bridge worker. State idempotency ensured. -ensure_stopped(Id) -> - ensure_stopped(Id, 1000). - -ensure_stopped(Id, Timeout) -> - Pid = case id(Id) of - P when is_pid(P) -> P; - N -> whereis(N) - end, - case Pid of - undefined -> - ok; - _ -> - MRef = monitor(process, Pid), - unlink(Pid), - _ = gen_statem:call(id(Id), ensure_stopped, Timeout), - receive - {'DOWN', MRef, _, _, _} -> - ok - after - Timeout -> - exit(Pid, kill) - end - end. +ensure_stopped(Name) -> + gen_statem:call(name(Name), ensure_stopped, 5000). stop(Pid) -> gen_statem:stop(Pid). status(Pid) when is_pid(Pid) -> gen_statem:call(Pid, status); -status(Id) -> - gen_statem:call(name(Id), status). +status(Name) -> + gen_statem:call(name(Name), status). %% @doc Return all forwards (local subscriptions). -spec get_forwards(id()) -> [topic()]. -get_forwards(Id) -> gen_statem:call(id(Id), get_forwards, timer:seconds(1000)). +get_forwards(Name) -> gen_statem:call(name(Name), get_forwards, timer:seconds(1000)). %% @doc Return all subscriptions (subscription over mqtt connection to remote broker). -spec get_subscriptions(id()) -> [{emqx_topic:topic(), qos()}]. -get_subscriptions(Id) -> gen_statem:call(id(Id), get_subscriptions). +get_subscriptions(Name) -> gen_statem:call(name(Name), get_subscriptions). %% @doc Add a new forward (local topic subscription). -spec ensure_forward_present(id(), topic()) -> ok. -ensure_forward_present(Id, Topic) -> - gen_statem:call(id(Id), {ensure_present, forwards, topic(Topic)}). +ensure_forward_present(Name, Topic) -> + gen_statem:call(name(Name), {ensure_forward_present, topic(Topic)}). %% @doc Ensure a forward topic is deleted. -spec ensure_forward_absent(id(), topic()) -> ok. -ensure_forward_absent(Id, Topic) -> - gen_statem:call(id(Id), {ensure_absent, forwards, topic(Topic)}). +ensure_forward_absent(Name, Topic) -> + gen_statem:call(name(Name), {ensure_forward_absent, topic(Topic)}). %% @doc Ensure subscribed to remote topic. %% NOTE: only applicable when connection module is emqx_bridge_mqtt %% return `{error, no_remote_subscription_support}' otherwise. -spec ensure_subscription_present(id(), topic(), qos()) -> ok | {error, any()}. -ensure_subscription_present(Id, Topic, QoS) -> - gen_statem:call(id(Id), {ensure_present, subscriptions, {topic(Topic), QoS}}). +ensure_subscription_present(Name, Topic, QoS) -> + gen_statem:call(name(Name), {ensure_subscription_present, topic(Topic), QoS}). %% @doc Ensure unsubscribed from remote topic. %% NOTE: only applicable when connection module is emqx_bridge_mqtt -spec ensure_subscription_absent(id(), topic()) -> ok. -ensure_subscription_absent(Id, Topic) -> - gen_statem:call(id(Id), {ensure_absent, subscriptions, topic(Topic)}). +ensure_subscription_absent(Name, Topic) -> + gen_statem:call(name(Name), {ensure_subscription_absent, topic(Topic)}). callback_mode() -> [state_functions]. %% @doc Config should be a map(). -init(Config) -> +init(Opts) -> erlang:process_flag(trap_exit, true), - ConnectModule = maps:get(connect_module, Config), - Subscriptions = maps:get(subscriptions, Config, []), - Forwards = maps:get(forwards, Config, []), - Queue = open_replayq(Config), - State = init_opts(Config), - Topics = [iolist_to_binary(T) || T <- Forwards], - Subs = check_subscriptions(Subscriptions), - ConnectCfg = get_conn_cfg(Config), + ConnectOpts = maps:get(config, Opts), + ConnectModule = conn_type(maps:get(conn_type, ConnectOpts)), + Forwards = maps:get(forwards, Opts, []), + Queue = open_replayq(maps:get(queue, Opts, #{})), + State = init_opts(Opts), self() ! idle, {ok, idle, State#{connect_module => ConnectModule, - connect_cfg => ConnectCfg, - forwards => Topics, - subscriptions => Subs, + connect_opts => ConnectOpts, + forwards => Forwards, replayq => Queue }}. -init_opts(Config) -> - IfRecordMetrics = maps:get(if_record_metrics, Config, true), - ReconnDelayMs = maps:get(reconnect_delay_ms, Config, ?DEFAULT_RECONNECT_DELAY_MS), - StartType = maps:get(start_type, Config, manual), - BridgeHandler = maps:get(bridge_handler, Config, ?NO_BRIDGE_HANDLER), - Mountpoint = maps:get(forward_mountpoint, Config, undefined), - ReceiveMountpoint = maps:get(receive_mountpoint, Config, undefined), - MaxInflightSize = maps:get(max_inflight, Config, ?DEFAULT_BATCH_SIZE), - BatchSize = maps:get(batch_size, Config, ?DEFAULT_BATCH_SIZE), - Name = maps:get(name, Config, undefined), +init_opts(Opts) -> + IfRecordMetrics = maps:get(if_record_metrics, Opts, true), + ReconnDelayMs = maps:get(reconnect_interval, Opts, ?DEFAULT_RECONNECT_DELAY_MS), + StartType = maps:get(start_type, Opts, manual), + Mountpoint = maps:get(forward_mountpoint, Opts, undefined), + MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_BATCH_SIZE), + BatchSize = maps:get(batch_size, Opts, ?DEFAULT_BATCH_SIZE), + Name = maps:get(name, Opts, undefined), #{start_type => StartType, - reconnect_delay_ms => ReconnDelayMs, + reconnect_interval => ReconnDelayMs, batch_size => BatchSize, mountpoint => format_mountpoint(Mountpoint), - receive_mountpoint => ReceiveMountpoint, inflight => [], max_inflight => MaxInflightSize, connection => undefined, - bridge_handler => BridgeHandler, if_record_metrics => IfRecordMetrics, name => Name}. -open_replayq(Config) -> - QCfg = maps:get(queue, Config, #{}), +open_replayq(QCfg) -> Dir = maps:get(replayq_dir, QCfg, undefined), SegBytes = maps:get(replayq_seg_bytes, QCfg, ?DEFAULT_SEG_BYTES), MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE), @@ -280,22 +247,6 @@ open_replayq(Config) -> replayq:open(QueueConfig#{sizer => fun emqx_bridge_msg:estimate_size/1, marshaller => fun ?MODULE:msg_marshaller/1}). -check_subscriptions(Subscriptions) -> - lists:map(fun({Topic, QoS}) -> - Topic1 = iolist_to_binary(Topic), - true = emqx_topic:validate({filter, Topic1}), - {Topic1, QoS} - end, Subscriptions). - -get_conn_cfg(Config) -> - maps:without([connect_module, - queue, - reconnect_delay_ms, - forwards, - mountpoint, - name - ], Config). - code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. @@ -321,14 +272,10 @@ idle(info, idle, #{start_type := auto} = State) -> idle(state_timeout, reconnect, State) -> connecting(State); -idle(info, {batch_ack, Ref}, State) -> - NewState = handle_batch_ack(State, Ref), - {keep_state, NewState}; - idle(Type, Content, State) -> common(idle, Type, Content, State). -connecting(#{reconnect_delay_ms := ReconnectDelayMs} = State) -> +connecting(#{reconnect_interval := ReconnectDelayMs} = State) -> case do_connect(State) of {ok, State1} -> {next_state, connected, State1, {state_timeout, 0, connected}}; @@ -348,7 +295,7 @@ connected(internal, maybe_send, State) -> {keep_state, NewState}; connected(info, {disconnected, Conn, Reason}, - #{connection := Connection, name := Name, reconnect_delay_ms := ReconnectDelayMs} = State) -> + #{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State) -> ?tp(info, disconnected, #{name => Name, reason => Reason}), case Conn =:= maps:get(client_pid, Connection, undefined) of true -> @@ -365,19 +312,27 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> {keep_state_and_data, [{reply, From, StateName}]}; -common(_StateName, {call, From}, ensure_started, _State) -> - {keep_state_and_data, [{reply, From, connected}]}; -common(_StateName, {call, From}, ensure_stopped, _State) -> - {stop_and_reply, {shutdown, manual}, [{reply, From, ok}]}; +common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> + {keep_state_and_data, [{reply, From, ok}]}; +common(_StateName, {call, From}, ensure_stopped, #{connection := Conn, + connect_module := ConnectModule} = State) -> + Reply = ConnectModule:stop(Conn), + {next_state, idle, State#{connection => undefined}, [{reply, From, Reply}]}; common(_StateName, {call, From}, get_forwards, #{forwards := Forwards}) -> {keep_state_and_data, [{reply, From, Forwards}]}; -common(_StateName, {call, From}, get_subscriptions, #{subscriptions := Subs}) -> - {keep_state_and_data, [{reply, From, Subs}]}; -common(_StateName, {call, From}, {ensure_present, What, Topic}, State) -> - {Result, NewState} = ensure_present(What, Topic, State), +common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> + {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, [])}]}; +common(_StateName, {call, From}, {ensure_forward_present, Topic}, State) -> + {Result, NewState} = do_ensure_forward_present(Topic, State), {keep_state, NewState, [{reply, From, Result}]}; -common(_StateName, {call, From}, {ensure_absent, What, Topic}, State) -> - {Result, NewState} = ensure_absent(What, Topic, State), +common(_StateName, {call, From}, {ensure_subscription_present, Topic, QoS}, State) -> + {Result, NewState} = do_ensure_subscription_present(Topic, QoS, State), + {keep_state, NewState, [{reply, From, Result}]}; +common(_StateName, {call, From}, {ensure_forward_absent, Topic}, State) -> + {Result, NewState} = do_ensure_forward_absent(Topic, State), + {keep_state, NewState, [{reply, From, Result}]}; +common(_StateName, {call, From}, {ensure_subscription_absent, Topic}, State) -> + {Result, NewState} = do_ensure_subscription_absent(Topic, State), {keep_state, NewState, [{reply, From, Result}]}; common(_StateName, info, {deliver, _, Msg}, State = #{replayq := Q, if_record_metrics := IfRecordMetric}) -> @@ -395,77 +350,79 @@ common(StateName, Type, Content, #{name := Name} = State) -> [Name, Type, StateName, Content]), {keep_state, State}. -eval_bridge_handler(State = #{bridge_handler := ?NO_BRIDGE_HANDLER}, _Msg) -> - State; -eval_bridge_handler(State = #{bridge_handler := Handler}, Msg) -> - Handler(Msg), - State. - -ensure_present(Key, Topic, State) -> - Topics = maps:get(Key, State), - case is_topic_present(Topic, Topics) of +do_ensure_forward_present(Topic, #{forwards := Forwards, name := Name} = State) -> + case is_topic_present(Topic, Forwards) of true -> {ok, State}; false -> - R = do_ensure_present(Key, Topic, State), - {R, State#{Key := lists:usort([Topic | Topics])}} + R = subscribe_local_topic(Topic, Name), + {R, State#{forwards => [Topic | Forwards]}} end. -ensure_absent(Key, Topic, State) -> - Topics = maps:get(Key, State), - case is_topic_present(Topic, Topics) of +do_ensure_subscription_present(_Topic, _QoS, #{connection := undefined} = State) -> + {{error, no_connection}, State}; +do_ensure_subscription_present(_Topic, _QoS, #{connect_module := emqx_bridge_rpc} = State) -> + {{error, no_remote_subscription_support}, State}; +do_ensure_subscription_present(Topic, QoS, #{connect_module := ConnectModule, + connection := Conn} = State) -> + case is_topic_present(Topic, maps:get(subscriptions, Conn, [])) of true -> - R = do_ensure_absent(Key, Topic, State), - {R, State#{Key := ensure_topic_absent(Topic, Topics)}}; + {ok, State}; + false -> + case ConnectModule:ensure_subscribed(Conn, Topic, QoS) of + {error, Error} -> + {{error, Error}, State}; + Conn1 -> + {ok, State#{connection => Conn1}} + end + end. + +do_ensure_forward_absent(Topic, #{forwards := Forwards} = State) -> + case is_topic_present(Topic, Forwards) of + true -> + R = do_unsubscribe(Topic), + {R, State#{forwards => lists:delete(Topic, Forwards)}}; + false -> + {ok, State} + end. +do_ensure_subscription_absent(_Topic, #{connection := undefined} = State) -> + {{error, no_connection}, State}; +do_ensure_subscription_absent(_Topic, #{connect_module := emqx_bridge_rpc} = State) -> + {{error, no_remote_subscription_support}, State}; +do_ensure_subscription_absent(Topic, #{connect_module := ConnectModule, + connection := Conn} = State) -> + case is_topic_present(Topic, maps:get(subscriptions, Conn, [])) of + true -> + case ConnectModule:ensure_unsubscribed(Conn, Topic) of + {error, Error} -> + {{error, Error}, State}; + Conn1 -> + {ok, State#{connection => Conn1}} + end; false -> {ok, State} end. -ensure_topic_absent(_Topic, []) -> []; -ensure_topic_absent(Topic, [{_, _} | _] = L) -> lists:keydelete(Topic, 1, L); -ensure_topic_absent(Topic, L) -> lists:delete(Topic, L). - -is_topic_present({Topic, _QoS}, Topics) -> - is_topic_present(Topic, Topics); is_topic_present(Topic, Topics) -> lists:member(Topic, Topics) orelse false =/= lists:keyfind(Topic, 1, Topics). do_connect(#{forwards := Forwards, - subscriptions := Subs, connect_module := ConnectModule, - connect_cfg := ConnectCfg, + connect_opts := ConnectOpts, inflight := Inflight, name := Name} = State) -> ok = subscribe_local_topics(Forwards, Name), - case emqx_bridge_connect:start(ConnectModule, ConnectCfg#{subscriptions => Subs}) of + case ConnectModule:start(ConnectOpts) of {ok, Conn} -> - Res = eval_bridge_handler(State#{connection => Conn}, connected), ?tp(info, connected, #{name => Name, inflight => length(Inflight)}), - {ok, Res}; + {ok, State#{connection => Conn}}; {error, Reason} -> + ConnectOpts1 = obfuscate(ConnectOpts), + ?LOG(error, "Failed to connect with module=~p\n" + "config=~p\nreason:~p", [ConnectModule, ConnectOpts1, Reason]), {error, Reason, State} end. -do_ensure_present(forwards, Topic, #{name := Name}) -> - subscribe_local_topic(Topic, Name); -do_ensure_present(subscriptions, _Topic, #{connection := undefined}) -> - {error, no_connection}; -do_ensure_present(subscriptions, _Topic, #{connect_module := emqx_bridge_rpc}) -> - {error, no_remote_subscription_support}; -do_ensure_present(subscriptions, {Topic, QoS}, #{connect_module := ConnectModule, - connection := Conn}) -> - ConnectModule:ensure_subscribed(Conn, Topic, QoS). - -do_ensure_absent(forwards, Topic, _) -> - do_unsubscribe(Topic); -do_ensure_absent(subscriptions, _Topic, #{connection := undefined}) -> - {error, no_connection}; -do_ensure_absent(subscriptions, _Topic, #{connect_module := emqx_bridge_rpc}) -> - {error, no_remote_subscription_support}; -do_ensure_absent(subscriptions, Topic, #{connect_module := ConnectModule, - connection := Conn}) -> - ConnectModule:ensure_unsubscribed(Conn, Topic). - collect(Acc) -> receive {deliver, _, Msg} -> @@ -605,10 +562,9 @@ disconnect(#{connection := Conn, connect_module := Module } = State) when Conn =/= undefined -> Module:stop(Conn), - State0 = State#{connection => undefined}, - eval_bridge_handler(State0, disconnected); + State#{connection => undefined}; disconnect(State) -> - eval_bridge_handler(State, disconnected). + State. %% Called only when replayq needs to dump it to disk. msg_marshaller(Bin) when is_binary(Bin) -> emqx_bridge_msg:from_binary(Bin); @@ -621,9 +577,6 @@ format_mountpoint(Prefix) -> name(Id) -> list_to_atom(lists:concat([?MODULE, "_", Id])). -id(Pid) when is_pid(Pid) -> Pid; -id(Name) -> name(Name). - register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, ['bridge.mqtt.message_sent', @@ -639,3 +592,21 @@ bridges_metrics_inc(true, Metric, Value) -> emqx_metrics:inc(Metric, Value); bridges_metrics_inc(_IsRecordMetric, _Metric, _Value) -> ok. + +obfuscate(Map) -> + maps:fold(fun(K, V, Acc) -> + case is_sensitive(K) of + true -> [{K, '***'} | Acc]; + false -> [{K, V} | Acc] + end + end, [], Map). + +is_sensitive(password) -> true; +is_sensitive(_) -> false. + +conn_type(rpc) -> + emqx_bridge_rpc; +conn_type(mqtt) -> + emqx_bridge_mqtt; +conn_type(Mod) when is_atom(Mod) -> + Mod. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl index 830fb1fe0..5babe0ed9 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl @@ -44,4 +44,4 @@ send_and_ack_test() -> ok = emqx_bridge_mqtt:stop(Conn) after meck:unload(emqtt) - end. \ No newline at end of file + end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl index fdcc25d5f..cbd80ba3d 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl @@ -30,8 +30,8 @@ send_and_ack_test() -> end), meck:new(emqx_bridge_worker, [passthrough, no_history]), try - {ok, #{client_pid := Pid, address := Node}} = emqx_bridge_rpc:start(#{address => node()}), - {ok, Ref} = emqx_bridge_rpc:send(#{address => Node}, []), + {ok, #{client_pid := Pid, remote_node := Node}} = emqx_bridge_rpc:start(#{node => node()}), + {ok, Ref} = emqx_bridge_rpc:send(#{remote_node => Node}, []), receive {batch_ack, Ref} -> ok diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl index d38663fcd..4c2fde6dd 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl @@ -16,9 +16,6 @@ -module(emqx_bridge_stub_conn). --behaviour(emqx_bridge_connect). - -%% behaviour callbacks -export([ start/1 , send/2 , stop/1 diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl index 680756742..f3f5d5ceb 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl @@ -59,13 +59,12 @@ init_per_suite(Config) -> nonode@nohost -> net_kernel:start(['emqx@127.0.0.1', longnames]); _ -> ok end, - ok = application:set_env(gen_rpc, tcp_client_num, 1), - emqx_ct_helpers:start_apps([emqx_modules, emqx_bridge_mqtt]), + emqx_ct_helpers:start_apps([emqx_bridge_mqtt]), emqx_logger:set_log_level(error), [{log_level, error} | Config]. end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_bridge_mqtt, emqx_modules]). + emqx_ct_helpers:stop_apps([emqx_bridge_mqtt]). init_per_testcase(_TestCase, Config) -> ok = snabbkaffe:start_trace(), @@ -74,260 +73,290 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> ok = snabbkaffe:stop(). -t_mngr(Config) when is_list(Config) -> - Subs = [{<<"a">>, 1}, {<<"b">>, 2}], - Cfg = #{address => node(), - forwards => [<<"mngr">>], - connect_module => emqx_bridge_rpc, - mountpoint => <<"forwarded">>, - subscriptions => Subs, - start_type => auto}, - Name = ?FUNCTION_NAME, - {ok, Pid} = emqx_bridge_worker:start_link(Name, Cfg), - try - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), - ?assertEqual([<<"mngr">>, <<"mngr2">>], emqx_bridge_worker:get_forwards(Pid)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Pid)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_present(Pid, <<"t">>, 0)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_absent(Pid, <<"t">>)), - ?assertEqual(Subs, emqx_bridge_worker:get_subscriptions(Pid)) - after - ok = emqx_bridge_worker:stop(Pid) - end. +t_rpc_mngr(_Config) -> + Name = "rpc_name", + Cfg = #{ + name => Name, + forwards => [<<"mngr">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + conn_type => rpc, + node => node() + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), + ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual({error, no_remote_subscription_support}, + emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), + ?assertEqual({error, no_remote_subscription_support}, + emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), + ok = emqx_bridge_worker:stop(Pid). + +t_mqtt_mngr(_Config) -> + Name = "mqtt_name", + Cfg = #{ + name => Name, + forwards => [<<"mngr">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + address => "127.0.0.1:1883", + conn_type => mqtt, + clientid => <<"client1">>, + keepalive => 300, + subscriptions => [#{topic => <<"t/#">>, qos => 1}] + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), + ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), + ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), + ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), + ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), + ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), + ?assertEqual([{<<"t/#">>,1}], emqx_bridge_worker:get_subscriptions(Name)), + ok = emqx_bridge_worker:stop(Pid). %% A loopback RPC to local node -t_rpc(Config) when is_list(Config) -> - Cfg = #{address => node(), - forwards => [<<"t_rpc/#">>], - connect_module => emqx_bridge_rpc, - forward_mountpoint => <<"forwarded">>, - start_type => auto}, - {ok, Pid} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), - ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _Props} = emqtt:connect(ConnPid), - {ok, _Props, [1]} = emqtt:subscribe(ConnPid, {<<"forwarded/t_rpc/one">>, ?QOS_1}), - timer:sleep(100), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_rpc/one">>, <<"hello">>, ?QOS_1), - timer:sleep(100), - ?assertEqual(1, length(receive_messages(1))), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Pid) - end. +t_rpc(_Config) -> + Name = "rpc", + Cfg = #{ + name => Name, + forwards => [<<"t_rpc/#">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + conn_type => rpc, + node => node() + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), + {ok, _Props} = emqtt:connect(ConnPid), + {ok, _Props, [1]} = emqtt:subscribe(ConnPid, {<<"forwarded/t_rpc/one">>, ?QOS_1}), + timer:sleep(100), + {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_rpc/one">>, <<"hello">>, ?QOS_1), + timer:sleep(100), + ?assertEqual(1, length(receive_messages(1))), + emqtt:disconnect(ConnPid), + emqx_bridge_worker:stop(Pid). %% Full data loopback flow explained: %% mqtt-client ----> local-broker ---(local-subscription)---> %% bridge(export) --- (mqtt-connection)--> local-broker ---(remote-subscription) --> %% bridge(import) --> mqtt-client -t_mqtt(Config) when is_list(Config) -> +t_mqtt(_Config) -> SendToTopic = <<"t_mqtt/one">>, SendToTopic2 = <<"t_mqtt/two">>, SendToTopic3 = <<"t_mqtt/three">>, Mountpoint = <<"forwarded/${node}/">>, - Cfg = #{address => "127.0.0.1:1883", - forwards => [SendToTopic], - connect_module => emqx_bridge_mqtt, - forward_mountpoint => Mountpoint, - username => "user", - clean_start => true, - clientid => "bridge_aws", - keepalive => 60000, - password => "passwd", - proto_ver => mqttv4, - queue => #{replayq_dir => "data/t_mqtt/", - replayq_seg_bytes => 10000, - batch_bytes_limit => 1000, - batch_count_limit => 10 - }, - reconnect_delay_ms => 1000, - ssl => false, - %% Consume back to forwarded message for verification - %% NOTE: this is a indefenite loopback without mocking emqx_bridge_worker:import_batch/1 - subscriptions => [{SendToTopic2, _QoS = 1}], - receive_mountpoint => <<"receive/aws/">>, - start_type => auto}, - {ok, Pid} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), - ClientId = <<"client-1">>, - try - ?assertEqual([{SendToTopic2, 1}], emqx_bridge_worker:get_subscriptions(Pid)), - ok = emqx_bridge_worker:ensure_subscription_present(Pid, SendToTopic3, _QoS = 1), - ?assertEqual([{SendToTopic3, 1},{SendToTopic2, 1}], - emqx_bridge_worker:get_subscriptions(Pid)), - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _Props} = emqtt:connect(ConnPid), - emqtt:subscribe(ConnPid, <<"forwarded/+/t_mqtt/one">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), + Name = "mqtt", + Cfg = #{ + name => Name, + forwards => [SendToTopic], + forward_mountpoint => Mountpoint, + start_type => auto, + config => #{ + address => "127.0.0.1:1883", + conn_type => mqtt, + clientid => <<"client1">>, + keepalive => 300, + subscriptions => [#{topic => SendToTopic2, qos => 1}], + receive_mountpoint => <<"receive/aws/">> + }, + queue => #{ + replayq_dir => "data/t_mqtt/", + replayq_seg_bytes => 10000, + batch_bytes_limit => 1000, + batch_count_limit => 10 + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + ?assertEqual([{SendToTopic2, 1}], emqx_bridge_worker:get_subscriptions(Name)), + ok = emqx_bridge_worker:ensure_subscription_present(Name, SendToTopic3, _QoS = 1), + ?assertEqual([{SendToTopic3, 1},{SendToTopic2, 1}], + emqx_bridge_worker:get_subscriptions(Name)), + {ok, ConnPid} = emqtt:start_link([{clientid, <<"client-1">>}]), + {ok, _Props} = emqtt:connect(ConnPid), + emqtt:subscribe(ConnPid, <<"forwarded/+/t_mqtt/one">>, 1), + %% message from a different client, to avoid getting terminated by no-local + Max = 10, + Msgs = lists:seq(1, Max), + lists:foreach(fun(I) -> + {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic, integer_to_binary(I), ?QOS_1) + end, Msgs), + ?assertEqual(10, length(receive_messages(200))), - emqtt:subscribe(ConnPid, <<"receive/aws/t_mqtt/two">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic2, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), + emqtt:subscribe(ConnPid, <<"receive/aws/t_mqtt/two">>, 1), + %% message from a different client, to avoid getting terminated by no-local + Max = 10, + Msgs = lists:seq(1, Max), + lists:foreach(fun(I) -> + {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic2, integer_to_binary(I), ?QOS_1) + end, Msgs), + ?assertEqual(10, length(receive_messages(200))), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Pid) - end. + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Pid). t_stub_normal(Config) when is_list(Config) -> - Cfg = #{forwards => [<<"t_stub_normal/#">>], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - start_type => auto, + Name = "stub_normal", + Cfg = #{ + name => Name, + forwards => [<<"t_stub_normal/#">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + config => #{ + conn_type => emqx_bridge_stub_conn, client_pid => self() - }, - {ok, Pid} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), + } + }, + {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), receive {Pid, emqx_bridge_stub_conn, ready} -> ok after 5000 -> error(timeout) end, - ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_stub_normal/one">>, <<"hello">>, ?QOS_1), - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - WorkerPid ! {batch_ack, BatchRef}, - ok - after - 5000 -> - error(timeout) - end, - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) + {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), + {ok, _} = emqtt:connect(ConnPid), + {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_stub_normal/one">>, <<"hello">>, ?QOS_1), + receive + {stub_message, WorkerPid, BatchRef, _Batch} -> + WorkerPid ! {batch_ack, BatchRef}, + ok after - ok = emqx_bridge_worker:stop(Pid) - end. + 5000 -> + error(timeout) + end, + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Pid). -t_stub_overflow(Config) when is_list(Config) -> +t_stub_overflow(_Config) -> Topic = <<"t_stub_overflow/one">>, MaxInflight = 20, - Cfg = #{forwards => [Topic], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - start_type => auto, - client_pid => self(), - max_inflight => MaxInflight - }, - {ok, Worker} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), - ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight * 2)), - ?SNK_WAIT(inflight_full), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks), - Acks2 = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks2), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Worker) - end. + Name = "stub_overflow", + Cfg = #{ + name => Name, + forwards => [<<"t_stub_overflow/one">>], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + max_inflight => MaxInflight, + config => #{ + conn_type => emqx_bridge_stub_conn, + client_pid => self() + } + }, + {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), + {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), + {ok, _} = emqtt:connect(ConnPid), + lists:foreach( + fun(I) -> + Data = integer_to_binary(I), + _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) + end, lists:seq(1, MaxInflight * 2)), + ?SNK_WAIT(inflight_full), + Acks = stub_receive(MaxInflight), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks), + Acks2 = stub_receive(MaxInflight), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks2), + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Worker). -t_stub_random_order(Config) when is_list(Config) -> +t_stub_random_order(_Config) -> Topic = <<"t_stub_random_order/a">>, MaxInflight = 10, - Cfg = #{forwards => [Topic], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - start_type => auto, - client_pid => self(), - max_inflight => MaxInflight - }, - {ok, Worker} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), + Name = "stub_random_order", + Cfg = #{ + name => Name, + forwards => [Topic], + forward_mountpoint => <<"forwarded">>, + start_type => auto, + max_inflight => MaxInflight, + config => #{ + conn_type => emqx_bridge_stub_conn, + client_pid => self() + } + }, + {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), ClientId = <<"ClientId">>, - try - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Worker) - end. + {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), + {ok, _} = emqtt:connect(ConnPid), + lists:foreach( + fun(I) -> + Data = integer_to_binary(I), + _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) + end, lists:seq(1, MaxInflight)), + Acks = stub_receive(MaxInflight), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, + lists:reverse(Acks)), + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Worker). -t_stub_retry_inflight(Config) when is_list(Config) -> +t_stub_retry_inflight(_Config) -> Topic = <<"to_stub_retry_inflight/a">>, MaxInflight = 10, - Cfg = #{forwards => [Topic], - connect_module => emqx_bridge_stub_conn, - forward_mountpoint => <<"forwarded">>, - reconnect_delay_ms => 10, - start_type => auto, - client_pid => self(), - max_inflight => MaxInflight - }, - {ok, Worker} = emqx_bridge_worker:start_link(?FUNCTION_NAME, Cfg), + Name = "stub_retry_inflight", + Cfg = #{ + name => Name, + forwards => [Topic], + forward_mountpoint => <<"forwarded">>, + reconnect_interval => 10, + start_type => auto, + max_inflight => MaxInflight, + config => #{ + conn_type => emqx_bridge_stub_conn, + client_pid => self() + } + }, + {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), ClientId = <<"ClientId2">>, - try - case ?block_until(#{?snk_kind := connected, inflight := 0}, 2000, 1000) of - {ok, #{inflight := 0}} -> ok; - Other -> ct:fail("~p", [Other]) - end, - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - %% receive acks but do not ack - Acks1 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks1)), - %% simulate a disconnect - Worker ! {disconnected, self(), test}, - ?SNK_WAIT(disconnected), - case ?block_until(#{?snk_kind := connected, inflight := MaxInflight}, 2000, 20) of - {ok, _} -> ok; - Error -> ct:fail("~p", [Error]) - end, - %% expect worker to retry inflight, so to receive acks again - Acks2 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks2)), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks2)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid) - after - ok = emqx_bridge_worker:stop(Worker) - end. + case ?block_until(#{?snk_kind := connected, inflight := 0}, 2000, 1000) of + {ok, #{inflight := 0}} -> ok; + Other -> ct:fail("~p", [Other]) + end, + {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), + {ok, _} = emqtt:connect(ConnPid), + lists:foreach( + fun(I) -> + Data = integer_to_binary(I), + _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) + end, lists:seq(1, MaxInflight)), + %% receive acks but do not ack + Acks1 = stub_receive(MaxInflight), + ?assertEqual(MaxInflight, length(Acks1)), + %% simulate a disconnect + Worker ! {disconnected, self(), test}, + ?SNK_WAIT(disconnected), + case ?block_until(#{?snk_kind := connected, inflight := MaxInflight}, 2000, 20) of + {ok, _} -> ok; + Error -> ct:fail("~p", [Error]) + end, + %% expect worker to retry inflight, so to receive acks again + Acks2 = stub_receive(MaxInflight), + ?assertEqual(MaxInflight, length(Acks2)), + lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, + lists:reverse(Acks2)), + ?SNK_WAIT(inflight_drained), + ?SNK_WAIT(replayq_drained), + emqtt:disconnect(ConnPid), + ok = emqx_bridge_worker:stop(Worker). stub_receive(N) -> stub_receive(N, []). diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl index 69ff87356..ffa2e9ee5 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl +++ b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl @@ -15,7 +15,6 @@ %%-------------------------------------------------------------------- -module(emqx_bridge_worker_tests). --behaviour(emqx_bridge_connect). -include_lib("eunit/include/eunit.hrl"). -include_lib("emqx/include/emqx.hrl"). @@ -69,14 +68,14 @@ disturbance_test() -> emqx_bridge_worker:register_metrics(), Ref = make_ref(), TestPid = self(), - Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), - ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), + Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => disturbance}), + ?assertEqual(Pid, whereis(emqx_bridge_worker_disturbance)), ?WAIT({connection_start_attempt, Ref}, 1000), Pid ! {disconnected, TestPid, test}, ?WAIT({connection_start_attempt, Ref}, 1000), emqx_metrics:stop(), - ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME). + ok = emqx_bridge_worker:stop(Pid). % % %% buffer should continue taking in messages when disconnected % buffer_when_disconnected_test_() -> @@ -113,22 +112,24 @@ manual_start_stop_test() -> emqx_bridge_worker:register_metrics(), Ref = make_ref(), TestPid = self(), + BridgeName = manual_start_stop, Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), Config = Config0#{start_type := manual}, - {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), + {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => BridgeName}), %% call ensure_started again should yeld the same result - ok = emqx_bridge_worker:ensure_started(?BRIDGE_NAME), - ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), - emqx_bridge_worker:ensure_stopped(unknown), - emqx_bridge_worker:ensure_stopped(Pid), - emqx_bridge_worker:ensure_stopped(?BRIDGE_REG_NAME), - emqx_metrics:stop(). + ok = emqx_bridge_worker:ensure_started(BridgeName), + emqx_bridge_worker:ensure_stopped(BridgeName), + emqx_metrics:stop(), + ok = emqx_bridge_worker:stop(Pid). make_config(Ref, TestPid, Result) -> - #{test_pid => TestPid, - test_ref => Ref, - connect_module => ?MODULE, - reconnect_delay_ms => 50, - connect_result => Result, - start_type => auto - }. + #{ + start_type => auto, + reconnect_interval => 50, + config => #{ + test_pid => TestPid, + test_ref => Ref, + conn_type => ?MODULE, + connect_result => Result + } + }. diff --git a/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl index 3f685a72a..80f51d2cc 100644 --- a/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl +++ b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl @@ -526,7 +526,7 @@ connect(Options = #{disk_cache := DiskCache, ecpool_worker_id := Id, pool_name : end end, Options2 = maps:without([ecpool_worker_id, pool_name, append], Options1), - emqx_bridge_worker:start_link(name(Pool, Id), Options2). + emqx_bridge_worker:start_link(Options2#{name => name(Pool, Id)}). name(Pool, Id) -> list_to_atom(atom_to_list(Pool) ++ ":" ++ integer_to_list(Id)). pool_name(ResId) -> diff --git a/data/loaded_plugins.tmpl b/data/loaded_plugins.tmpl index d26d56abf..80a0c832c 100644 --- a/data/loaded_plugins.tmpl +++ b/data/loaded_plugins.tmpl @@ -2,4 +2,3 @@ {emqx_dashboard, true}. {emqx_modules, {{enable_plugin_emqx_modules}}}. {emqx_retainer, {{enable_plugin_emqx_retainer}}}. -{emqx_bridge_mqtt, {{enable_plugin_emqx_bridge_mqtt}}}. diff --git a/rebar.config.erl b/rebar.config.erl index ff66f746c..5d5d02d05 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -192,8 +192,7 @@ overlay_vars_rel(RelType) -> cloud -> "vm.args"; edge -> "vm.args.edge" end, - [ {enable_plugin_emqx_bridge_mqtt, RelType =:= edge} - , {enable_plugin_emqx_modules, false} %% modules is not a plugin in ce + [ {enable_plugin_emqx_modules, false} %% modules is not a plugin in ce , {enable_plugin_emqx_retainer, true} , {vm_args_file, VmArgs} ]. @@ -256,6 +255,7 @@ relx_apps(ReleaseType) -> , emqx_connector , emqx_data_bridge , emqx_rule_engine + , emqx_bridge_mqtt ] ++ [emqx_telemetry || not is_enterprise()] ++ [emqx_modules || not is_enterprise()] @@ -282,7 +282,6 @@ relx_plugin_apps(ReleaseType) -> [ emqx_retainer , emqx_management , emqx_dashboard - , emqx_bridge_mqtt , emqx_sn , emqx_coap , emqx_stomp @@ -375,6 +374,8 @@ emqx_etc_overlay_common() -> {"{{base_dir}}/lib/emqx_telemetry/etc/emqx_telemetry.conf", "etc/plugins/emqx_telemetry.conf"}, {"{{base_dir}}/lib/emqx_authn/etc/emqx_authn.conf", "etc/plugins/emqx_authn.conf"}, {"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"}, + {"{{base_dir}}/lib/emqx_rule_engine/etc/emqx_rule_engine.conf", "etc/plugins/emqx_rule_engine.conf"}, + {"{{base_dir}}/lib/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf", "etc/plugins/emqx_bridge_mqtt.conf"}, %% TODO: check why it has to end with .paho %% and why it is put to etc/plugins dir {"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}]. From 66aaed1f8770a3b393b5f6caaa228d83cac4de88 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 1 Jul 2021 11:47:33 +0800 Subject: [PATCH 052/379] feat(config): update emqx_schema for logger --- apps/emqx/etc/emqx.conf | 32 +-- apps/emqx/src/emqx_schema.erl | 498 +++------------------------------- 2 files changed, 55 insertions(+), 475 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 4c8113cd5..3ea51ec3f 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -336,7 +336,7 @@ log { ## @doc log.console_handler.enable ## ValueType: Boolean ## Default: false - console_handler.enable = false + console_handler.enable: false ## The log level of this handler ## All the log messages with levels lower than this level will @@ -345,13 +345,13 @@ log { ## @doc log.console_handler.level ## ValueType: debug | info | notice | warning | error | critical | alert | emergency ## Default: warning - console_handler.level = warning + console_handler.level: warning ##---------------------------------------------------------------- ## The file log handlers send log messages to files ##---------------------------------------------------------------- ## file_handlers. - file_handlers.emqx_default: { + file_handlers.emqx_log: { ## The log level filter of this handler ## All the log messages with levels lower than this level will ## be dropped. @@ -383,7 +383,7 @@ log { ## @doc log.file_handlers..rotation.enable ## ValueType: Boolean ## Default: true - rotation.enable = true + rotation.enable: true ## Maximum rotation count of log files. ## @@ -409,7 +409,7 @@ log { ## ## You could also create multiple file handlers for different ## log level for example: - file_handlers.emqx_error: { + file_handlers.emqx_error_log: { level: error file: "{{ platform_log_dir }}/error.log" } @@ -422,7 +422,7 @@ log { ## - "utc" for Universal Coordinated Time (UTC) ## - "+hh:mm" or "-hh:mm" for a specified offset ## Default: system - time_offset = system + time_offset: system ## Limits the total number of characters printed for each log event. ## @@ -437,7 +437,7 @@ log { ## @doc log.max_depth ## ValueType: Integer | infinity ## Default: 80 - max_depth = 80 + max_depth: 80 ## Log formatter ## @doc log.formatter @@ -2120,7 +2120,7 @@ example_common_ssl_options { ## @doc listeners..ssl.depth ## ValueType: Number ## Default: 10 - ssl.depth = 10 + ssl.depth: 10 ## String containing the user's password. Only used if the private keyfile ## is password-protected. @@ -2130,7 +2130,7 @@ example_common_ssl_options { ## @doc listeners..ssl.depth ## ValueType: String ## Default: "" - #ssl.key_password = "" + #ssl.key_password: "" ## The Ephemeral Diffie-Helman key exchange is a very effective way of ## ensuring Forward Secrecy by exchanging a set of keys that never hit @@ -2248,14 +2248,14 @@ example_common_websocket_options { ## @doc listeners..websocket.fail_if_no_subprotocol ## ValueType: Boolean ## Default: true - websocket.fail_if_no_subprotocol = true + websocket.fail_if_no_subprotocol: true ## Enable origin check in header for websocket connection ## ## @doc listeners..websocket.check_origin_enable ## ValueType: Boolean ## Default: false - websocket.check_origin_enable = false + websocket.check_origin_enable: false ## Allow origin to be absent in header in websocket connection ## when check_origin_enable is true @@ -2263,7 +2263,7 @@ example_common_websocket_options { ## @doc listeners..websocket.allow_origin_absence ## ValueType: Boolean ## Default: true - websocket.allow_origin_absence = true + websocket.allow_origin_absence: true ## Comma separated list of allowed origin in header for websocket connection ## @@ -2273,7 +2273,7 @@ example_common_websocket_options { ## local http dashboard url ## check_origins: "http://localhost:18083, http://127.0.0.1:18083" ## Default: "" - websocket.check_origins = "http://localhost:18083, http://127.0.0.1:18083" + websocket.check_origins: "http://localhost:18083, http://127.0.0.1:18083" ## Specify which HTTP header for real source IP if the EMQ X cluster is ## deployed behind NGINX or HAProxy. @@ -2281,7 +2281,7 @@ example_common_websocket_options { ## @doc listeners..websocket.proxy_address_header ## ValueType: String ## Default: X-Forwarded-For - websocket.proxy_address_header = X-Forwarded-For + websocket.proxy_address_header: X-Forwarded-For ## Specify which HTTP header for real source port if the EMQ X cluster is ## deployed behind NGINX or HAProxy. @@ -2289,7 +2289,7 @@ example_common_websocket_options { ## @doc listeners..websocket.proxy_port_header ## ValueType: String ## Default: X-Forwarded-Port - websocket.proxy_port_header = X-Forwarded-Port + websocket.proxy_port_header: X-Forwarded-Port websocket.deflate_opts { ## The level of deflate options for external WebSocket connections. @@ -2345,4 +2345,4 @@ example_common_websocket_options { ## Default: 15 client_max_window_bits: 15 } -} \ No newline at end of file +} diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 292d5569a..3ec691bc0 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -508,72 +508,15 @@ mqtt_listener() -> , {"proxy_protocol_timeout", t(duration())} ]. -translations() -> ["ekka", "vm_args", "gen_rpc", "kernel", "emqx"]. - -translation("ekka") -> - [ {"cluster_discovery", fun tr_cluster__discovery/1}]; - -translation("vm_args") -> - [ {"+zdbbl", fun tr_zdbbl/1} - , {"-heart", fun tr_heart/1}]; - -translation("gen_rpc") -> - [ {"tcp_client_num", fun tr_tcp_client_num/1} - , {"tcp_client_port", fun tr_tcp_client_port/1}]; +translations() -> ["kernel"]. translation("kernel") -> [ {"logger_level", fun tr_logger_level/1} - , {"logger", fun tr_logger/1}]; + , {"logger", fun tr_logger/1}]. -translation("emqx") -> - [ {"flapping_detect_policy", fun tr_flapping_detect_policy/1} - , {"zones", fun tr_zones/1} - , {"listeners", fun tr_listeners/1} - , {"modules", fun tr_modules/1} - , {"alarm", fun tr_alarm/1} - , {"telemetry", fun tr_telemetry/1} - ]. - -tr_cluster__discovery(Conf) -> - Strategy = conf_get("cluster.discovery", Conf), - {Strategy, filter(options(Strategy, Conf))}. - -tr_heart(Conf) -> - case conf_get("node.heartbeat", Conf) of - true -> ""; - "on" -> ""; - _ -> undefined - end. - -tr_zdbbl(Conf) -> - case conf_get("node.dist_buffer_size", Conf) of - undefined -> undefined; - X when is_integer(X) -> ceiling(X / 1024); %% Bytes to Kilobytes; - _ -> undefined - end. - -%% Force client to use server listening port, because we do no provide -%% per-node listening port manual mapping from configs. -%% i.e. all nodes in the cluster should agree to the same -%% listening port number. -tr_tcp_client_num(Conf) -> - case conf_get("rpc.tcp_client_num", Conf) of - 0 -> max(1, erlang:system_info(schedulers) div 2); - V -> V - end. - -tr_tcp_client_port(Conf) -> - conf_get("rpc.tcp_server_port", Conf). - -tr_logger_level(Conf) -> conf_get("log.level", Conf). +tr_logger_level(Conf) -> conf_get("log.primary_level", Conf). tr_logger(Conf) -> - LogTo = conf_get("log.to", Conf), - LogLevel = conf_get("log.level", Conf), - LogType = case conf_get("log.rotation.enable", Conf) of - true -> wrap; - _ -> halt - end, CharsLimit = case conf_get("log.chars_limit", Conf) of -1 -> unlimited; V -> V @@ -581,309 +524,56 @@ tr_logger(Conf) -> SingleLine = conf_get("log.single_line", Conf), FmtName = conf_get("log.formatter", Conf), Formatter = formatter(FmtName, CharsLimit, SingleLine), - BurstLimit = conf_get("log.burst_limit", Conf), - {BustLimitOn, {MaxBurstCount, TimeWindow}} = burst_limit(BurstLimit), - FileConf = fun (Filename) -> - BasicConf = - #{type => LogType, - file => filename:join(conf_get("log.dir", Conf), Filename), - max_no_files => conf_get("log.rotation.count", Conf), - sync_mode_qlen => conf_get("log.sync_mode_qlen", Conf), - drop_mode_qlen => conf_get("log.drop_mode_qlen", Conf), - flush_qlen => conf_get("log.flush_qlen", Conf), - overload_kill_enable => conf_get("log.overload_kill", Conf), - overload_kill_qlen => conf_get("log.overload_kill_qlen", Conf), - overload_kill_mem_size => conf_get("log.overload_kill_mem_size", Conf), - overload_kill_restart_after => conf_get("log.overload_kill_restart_after", Conf), - burst_limit_enable => BustLimitOn, - burst_limit_max_count => MaxBurstCount, - burst_limit_window_time => TimeWindow - }, - MaxNoBytes = case LogType of - wrap -> conf_get("log.rotation.size", Conf); - halt -> conf_get("log.size", Conf) - end, - BasicConf#{max_no_bytes => MaxNoBytes} end, - + BasicConf = #{ + sync_mode_qlen => conf_get("log.sync_mode_qlen", Conf), + drop_mode_qlen => conf_get("log.drop_mode_qlen", Conf), + flush_qlen => conf_get("log.flush_qlen", Conf), + overload_kill_enable => conf_get("log.overload_kill.enable", Conf), + overload_kill_qlen => conf_get("log.overload_kill.qlen", Conf), + overload_kill_mem_size => conf_get("log.overload_kill.mem_size", Conf), + overload_kill_restart_after => conf_get("log.overload_kill.restart_after", Conf), + burst_limit_enable => conf_get("log.burst_limit.enable", Conf), + burst_limit_max_count => conf_get("log.burst_limit.max_count", Conf), + burst_limit_window_time => conf_get("log.burst_limit.window_time", Conf) + }, Filters = case conf_get("log.supervisor_reports", Conf) of error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; progress -> [] end, - %% For the default logger that outputs to console - DefaultHandler = - if LogTo =:= console orelse LogTo =:= both -> - [{handler, console, logger_std_h, - #{level => LogLevel, - config => #{type => standard_io}, - formatter => Formatter, - filters => Filters - } - }]; + ConsoleHandler = + case conf_get("log.console_handler.enable", Conf) of true -> - [{handler, default, undefined}] - end, - - %% For the file logger - FileHandler = - if LogTo =:= file orelse LogTo =:= both -> - [{handler, file, logger_disk_log_h, - #{level => LogLevel, - config => FileConf(conf_get("log.file", Conf)), + [{handler, console, logger_std_h, #{ + level => conf_get("log.console_handler.level", Conf), + config => BasicConf#{type => standard_io}, formatter => Formatter, - filesync_repeat_interval => no_repeat, filters => Filters }}]; - true -> [] + false -> [] end, - - AdditionalLogFiles = additional_log_files(Conf), - AdditionalHandlers = - [{handler, list_to_atom("file_for_"++Level), logger_disk_log_h, - #{level => list_to_atom(Level), - config => FileConf(Filename), + %% For the file logger + FileHandlers = + [{handler, binary_to_atom(HandlerName, latin1), logger_disk_log_h, #{ + level => conf_get("level", SubConf), + config => BasicConf#{ + type => case conf_get("rotation.enable", SubConf) of + true -> wrap; + _ -> halt + end, + file => conf_get("file", SubConf), + max_no_files => conf_get("rotation.count", SubConf), + max_no_bytes => conf_get("max_size", SubConf) + }, formatter => Formatter, - filesync_repeat_interval => no_repeat}} - || {Level, Filename} <- AdditionalLogFiles], + filters => Filters, + filesync_repeat_interval => no_repeat + }} + || {HandlerName, SubConf} <- maps:to_list(conf_get("log.file_handlers", Conf))], - DefaultHandler ++ FileHandler ++ AdditionalHandlers. - -tr_flapping_detect_policy(Conf) -> - [Threshold, Duration, Interval] = conf_get("acl.flapping_detect_policy", Conf), - ParseDuration = fun(S, F) -> - case F(S) of - {ok, I} -> I; - {error, Reason} -> error({duration, Reason}) - end end, - #{threshold => list_to_integer(Threshold), - duration => ParseDuration(Duration, fun to_duration/1), - banned_interval => ParseDuration(Interval, fun to_duration_s/1) - }. - -tr_zones(Conf) -> - Names = lists:usort(keys("zone", Conf)), - lists:foldl( - fun(Name, Zones) -> - Zone = keys("zone." ++ Name, Conf), - Mapped = lists:flatten([map_zones(K, conf_get(["zone", Name, K], Conf)) || K <- Zone]), - [{list_to_atom(Name), lists:filter(fun ({K, []}) when K =:= ratelimit; K =:= quota -> false; - ({_, undefined}) -> false; - (_) -> true end, Mapped)} | Zones] - end, [], Names). - -tr_listeners(Conf) -> - Atom = fun(undefined) -> undefined; - (B) when is_binary(B)-> binary_to_atom(B); - (S) when is_list(S) -> list_to_atom(S) end, - - Access = fun(S) -> - [A, CIDR] = string:tokens(S, " "), - {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} - end, - - AccOpts = fun(Prefix) -> - case keys(Prefix ++ ".access", Conf) of - [] -> []; - Ids -> - [{access_rules, [Access(conf_get(Prefix ++ ".access." ++ Id, Conf)) || Id <- Ids]}] - end end, - - RateLimit = fun(undefined) -> - undefined; - ([L, D]) -> - Limit = case to_bytesize(L) of - {ok, I0} -> I0; - {error, R0} -> error({bytesize, R0}) - end, - Duration = case to_duration_s(D) of - {ok, I1} -> I1; - {error, R1} -> error({duration, R1}) - end, - {Limit, Duration} - end, - - CheckOrigin = fun(S) -> [ list_to_binary(string:trim(O)) || O <- S] end, - - WsOpts = fun(Prefix) -> - case conf_get(Prefix ++ ".check_origins", Conf) of - undefined -> undefined; - Rules -> lists:flatten(CheckOrigin(Rules)) - end - end, - - LisOpts = fun(Prefix) -> - filter([{acceptors, conf_get(Prefix ++ ".acceptors", Conf)}, - {mqtt_path, conf_get(Prefix ++ ".mqtt_path", Conf)}, - {max_connections, conf_get(Prefix ++ ".max_connections", Conf)}, - {max_conn_rate, conf_get(Prefix ++ ".max_conn_rate", Conf)}, - {active_n, conf_get(Prefix ++ ".active_n", Conf)}, - {tune_buffer, conf_get(Prefix ++ ".tune_buffer", Conf)}, - {zone, Atom(conf_get(Prefix ++ ".zone", Conf))}, - {rate_limit, RateLimit(conf_get(Prefix ++ ".rate_limit", Conf))}, - {proxy_protocol, conf_get(Prefix ++ ".proxy_protocol", Conf)}, - {proxy_address_header, list_to_binary(string:lowercase(conf_get(Prefix ++ ".proxy_address_header", Conf, <<"">>)))}, - {proxy_port_header, list_to_binary(string:lowercase(conf_get(Prefix ++ ".proxy_port_header", Conf, <<"">>)))}, - {proxy_protocol_timeout, conf_get(Prefix ++ ".proxy_protocol_timeout", Conf)}, - {fail_if_no_subprotocol, conf_get(Prefix ++ ".fail_if_no_subprotocol", Conf)}, - {supported_subprotocols, string:tokens(conf_get(Prefix ++ ".supported_subprotocols", Conf, ""), ", ")}, - {peer_cert_as_username, conf_get(Prefix ++ ".peer_cert_as_username", Conf)}, - {peer_cert_as_clientid, conf_get(Prefix ++ ".peer_cert_as_clientid", Conf)}, - {compress, conf_get(Prefix ++ ".compress", Conf)}, - {idle_timeout, conf_get(Prefix ++ ".idle_timeout", Conf)}, - {max_frame_size, conf_get(Prefix ++ ".max_frame_size", Conf)}, - {mqtt_piggyback, conf_get(Prefix ++ ".mqtt_piggyback", Conf)}, - {check_origin_enable, conf_get(Prefix ++ ".check_origin_enable", Conf)}, - {allow_origin_absence, conf_get(Prefix ++ ".allow_origin_absence", Conf)}, - {check_origins, WsOpts(Prefix)} | AccOpts(Prefix)]) - end, - DeflateOpts = fun(Prefix) -> - filter([{level, conf_get(Prefix ++ ".deflate_opts.level", Conf)}, - {mem_level, conf_get(Prefix ++ ".deflate_opts.mem_level", Conf)}, - {strategy, conf_get(Prefix ++ ".deflate_opts.strategy", Conf)}, - {server_context_takeover, conf_get(Prefix ++ ".deflate_opts.server_context_takeover", Conf)}, - {client_context_takeover, conf_get(Prefix ++ ".deflate_opts.client_context_takeover", Conf)}, - {server_max_windows_bits, conf_get(Prefix ++ ".deflate_opts.server_max_window_bits", Conf)}, - {client_max_windows_bits, conf_get(Prefix ++ ".deflate_opts.client_max_window_bits", Conf)}]) - end, - TcpOpts = fun(Prefix) -> - filter([{backlog, conf_get(Prefix ++ ".backlog", Conf)}, - {send_timeout, conf_get(Prefix ++ ".send_timeout", Conf)}, - {send_timeout_close, conf_get(Prefix ++ ".send_timeout_close", Conf)}, - {recbuf, conf_get(Prefix ++ ".recbuf", Conf)}, - {sndbuf, conf_get(Prefix ++ ".sndbuf", Conf)}, - {buffer, conf_get(Prefix ++ ".buffer", Conf)}, - {high_watermark, conf_get(Prefix ++ ".high_watermark", Conf)}, - {nodelay, conf_get(Prefix ++ ".nodelay", Conf, true)}, - {reuseaddr, conf_get(Prefix ++ ".reuseaddr", Conf)}]) - end, - - SslOpts = fun(Prefix) -> - Opts = tr_ssl(Prefix, Conf), - case lists:keyfind(ciphers, 1, Opts) of - false -> - error(Prefix ++ ".ciphers or " ++ Prefix ++ ".psk_ciphers is absent"); - _ -> - Opts - end end, - - TcpListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - ListenOnN = case conf_get(Prefix ++ ".endpoint", Conf) of - undefined -> []; - ListenOn -> ListenOn - end, - [#{ proto => Atom(Type) - , name => Name - , listen_on => ListenOnN - , opts => [ {deflate_options, DeflateOpts(Prefix)} - , {tcp_options, TcpOpts(Prefix)} - | LisOpts(Prefix) - ] - } - ] - end, - SslListeners = fun(Type, Name) -> - Prefix = string:join(["listener", Type, Name], "."), - case conf_get(Prefix ++ ".endpoint", Conf) of - undefined -> - []; - ListenOn -> - [#{ proto => Atom(Type) - , name => Name - , listen_on => ListenOn - , opts => [ {deflate_options, DeflateOpts(Prefix)} - , {tcp_options, TcpOpts(Prefix)} - , {ssl_options, SslOpts(Prefix)} - | LisOpts(Prefix) - ] - } - ] - end end, - - - lists:flatten([TcpListeners("tcp", Name) || Name <- keys("listener.tcp", Conf)] - ++ [TcpListeners("ws", Name) || Name <- keys("listener.ws", Conf)] - ++ [SslListeners("ssl", Name) || Name <- keys("listener.ssl", Conf)] - ++ [SslListeners("wss", Name) || Name <- keys("listener.wss", Conf)]). - -tr_modules(Conf) -> - Subscriptions = fun() -> - List = keys("module.subscription", Conf), - TopicList = [{N, conf_get(["module", "subscription", N, "topic"], Conf)}|| N <- List], - [{list_to_binary(T), #{ qos => conf_get("module.subscription." ++ N ++ ".qos", Conf, 0), - nl => conf_get("module.subscription." ++ N ++ ".nl", Conf, 0), - rap => conf_get("module.subscription." ++ N ++ ".rap", Conf, 0), - rh => conf_get("module.subscription." ++ N ++ ".rh", Conf, 0) - }} || {N, T} <- TopicList] - end, - Rewrites = fun() -> - Rules = keys("module.rewrite.rule", Conf), - PubRules = keys("module.rewrite.pub_rule", Conf), - SubRules = keys("module.rewrite.sub_rule", Conf), - TotalRules = - [ {["module", "rewrite", "pub", "rule", R], conf_get(["module.rewrite.rule", R], Conf)} || R <- Rules] ++ - [ {["module", "rewrite", "pub", "rule", R], conf_get(["module.rewrite.pub_rule", R], Conf)} || R <- PubRules] ++ - [ {["module", "rewrite", "sub", "rule", R], conf_get(["module.rewrite.rule", R], Conf)} || R <- Rules] ++ - [ {["module", "rewrite", "sub", "rule", R], conf_get(["module.rewrite.sub_rule", R], Conf)} || R <- SubRules], - lists:map(fun({[_, "rewrite", PubOrSub, "rule", _], Rule}) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, list_to_atom(PubOrSub), list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} - end, TotalRules) - end, - lists:append([ - [{emqx_mod_presence, [{qos, conf_get("module.presence.qos", Conf, 1)}]}], - [{emqx_mod_subscription, Subscriptions()}], - [{emqx_mod_rewrite, Rewrites()}], - [{emqx_mod_topic_metrics, []}], - [{emqx_mod_delayed, []}], - [{emqx_mod_acl_internal, [{acl_file, conf_get("acl.acl_file", Conf)}]}] - ]). - -tr_alarm(Conf) -> - [ {actions, [list_to_atom(Action) || Action <- conf_get("alarm.actions", Conf)]} - , {size_limit, conf_get("alarm.size_limit", Conf)} - , {validity_period, conf_get("alarm.validity_period", Conf)} - ]. - -tr_telemetry(Conf) -> - [ {enabled, conf_get("telemetry.enabled", Conf)} - , {url, conf_get("telemetry.url", Conf)} - , {report_interval, conf_get("telemetry.report_interval", Conf)} - ]. + [{handler, default, undefined}] ++ ConsoleHandler ++ FileHandlers. %% helpers - -options(static, Conf) -> - [{seeds, [list_to_atom(S) || S <- conf_get("cluster.static.seeds", Conf, "")]}]; -options(mcast, Conf) -> - {ok, Addr} = inet:parse_address(conf_get("cluster.mcast.addr", Conf)), - {ok, Iface} = inet:parse_address(conf_get("cluster.mcast.iface", Conf)), - Ports = [list_to_integer(S) || S <- conf_get("cluster.mcast.ports", Conf)], - [{addr, Addr}, {ports, Ports}, {iface, Iface}, - {ttl, conf_get("cluster.mcast.ttl", Conf, 1)}, - {loop, conf_get("cluster.mcast.loop", Conf, true)}]; -options(dns, Conf) -> - [{name, conf_get("cluster.dns.name", Conf)}, - {app, conf_get("cluster.dns.app", Conf)}]; -options(etcd, Conf) -> - Namespace = "cluster.etcd.ssl", - SslOpts = fun(C) -> - Options = keys(Namespace, C), - lists:map(fun(Key) -> {list_to_atom(Key), conf_get([Namespace, Key], Conf)} end, Options) end, - [{server, conf_get("cluster.etcd.server", Conf)}, - {prefix, conf_get("cluster.etcd.prefix", Conf, "emqxcl")}, - {node_ttl, conf_get("cluster.etcd.node_ttl", Conf, 60)}, - {ssl_options, filter(SslOpts(Conf))}]; -options(k8s, Conf) -> - [{apiserver, conf_get("cluster.k8s.apiserver", Conf)}, - {service_name, conf_get("cluster.k8s.service_name", Conf)}, - {address_type, conf_get("cluster.k8s.address_type", Conf, ip)}, - {app_name, conf_get("cluster.k8s.app_name", Conf)}, - {namespace, conf_get("cluster.k8s.namespace", Conf)}, - {suffix, conf_get("cluster.k8s.suffix", Conf, "")}]; -options(manual, _Conf) -> - []. - formatter(json, CharsLimit, SingleLine) -> {emqx_logger_jsonfmt, #{chars_limit => CharsLimit, @@ -905,117 +595,7 @@ formatter(text, CharsLimit, SingleLine) -> single_line => SingleLine }}. -burst_limit(["disabled"]) -> - {false, {20000, 1000}}; -burst_limit([Count, Window]) -> - {true, {list_to_integer(Count), - case to_duration(Window) of - {ok, I} -> I; - {error, R} -> error({duration, R}) - end}}. - -%% For creating additional log files for specific log levels. -additional_log_files(Conf) -> - LogLevel = ["debug", "info", "notice", "warning", - "error", "critical", "alert", "emergency"], - additional_log_files(Conf, LogLevel, []). - -additional_log_files(_Conf, [], Acc) -> - Acc; -additional_log_files(Conf, [L | More], Acc) -> - case conf_get(["log", L, "file"], Conf) of - undefined -> additional_log_files(Conf, More, Acc); - F -> additional_log_files(Conf, More, [{L, F} | Acc]) - end. - -rate_limit_byte_dur([L, D]) -> - Limit = case to_bytesize(L) of - {ok, I0} -> I0; - {error, R0} -> error({bytesize, R0}) - end, - Duration = case to_duration_s(D) of - {ok, I1} -> I1; - {error, R1} -> error({duration, R1}) - end, - {Limit, Duration}. - -rate_limit_num_dur([L, D]) -> - Limit = case string:to_integer(L) of - {Int, []} when is_integer(Int) -> Int; - _ -> error("failed to parse bytesize string") - end, - Duration = case to_duration_s(D) of - {ok, I} -> I; - {error, Reason} -> error(Reason) - end, - {Limit, Duration}. - -map_zones(_, undefined) -> - {undefined, undefined}; - -map_zones("mqueue_priorities", Val) -> - case Val of - ["none"] -> {mqueue_priorities, none}; % NO_PRIORITY_TABLE - _ -> - MqueuePriorities = lists:foldl(fun(T, Acc) -> - %% NOTE: space in "= " is intended - [Topic, Prio] = string:tokens(T, "= "), - P = list_to_integer(Prio), - (P < 0 orelse P > 255) andalso error({bad_priority, Topic, Prio}), - maps:put(iolist_to_binary(Topic), P, Acc) - end, #{}, Val), - {mqueue_priorities, MqueuePriorities} - end; -map_zones("response_information", Val) -> - {response_information, iolist_to_binary(Val)}; -map_zones("rate_limit", Conf) -> - Messages = case conf_get("conn_messages_in", #{value => Conf}) of - undefined -> - []; - M -> - [{conn_messages_in, rate_limit_num_dur(M)}] - end, - Bytes = case conf_get("conn_bytes_in", #{value => Conf}) of - undefined -> - []; - B -> - [{conn_bytes_in, rate_limit_byte_dur(B)}] - end, - {ratelimit, Messages ++ Bytes}; -map_zones("conn_congestion", Conf) -> - Alarm = case conf_get("alarm", #{value => Conf}) of - undefined -> - []; - A -> - [{conn_congestion_alarm_enabled, A}] - end, - MinAlarm = case conf_get("min_alarm_sustain_duration", #{value => Conf}) of - undefined -> - []; - M -> - [{conn_congestion_min_alarm_sustain_duration, M}] - end, - Alarm ++ MinAlarm; -map_zones("quota", Conf) -> - Conn = case conf_get("conn_messages_routing", #{value => Conf}) of - undefined -> - []; - C -> - [{conn_messages_routing, rate_limit_num_dur(C)}] - end, - Overall = case conf_get("overall_messages_routing", #{value => Conf}) of - undefined -> - []; - O -> - [{overall_messages_routing, rate_limit_num_dur(O)}] - end, - {quota, Conn ++ Overall}; -map_zones(Opt, Val) -> - {list_to_atom(Opt), Val}. - - %% utils - -spec(conf_get(string() | [string()], hocon:config()) -> term()). conf_get(Key, Conf) -> V = hocon_schema:deep_get(Key, Conf, value), From f2257cee33b936b9f30caeb7ca1b2a367071810f Mon Sep 17 00:00:00 2001 From: gaolihai Date: Thu, 1 Jul 2021 12:26:38 +0800 Subject: [PATCH 053/379] docs(README): Modify invalid links --- README-CN.md | 2 +- README-JP.md | 2 +- README-RU.md | 2 +- README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index 2a9dcebf6..67f1b0ff5 100644 --- a/README-CN.md +++ b/README-CN.md @@ -145,7 +145,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## 开源许可 diff --git a/README-JP.md b/README-JP.md index 580a76268..a3e1f5130 100644 --- a/README-JP.md +++ b/README-JP.md @@ -125,7 +125,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## License diff --git a/README-RU.md b/README-RU.md index 5001f3fd3..45f253f5b 100644 --- a/README-RU.md +++ b/README-RU.md @@ -135,7 +135,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_authz make dialyzer [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## Лицензия diff --git a/README.md b/README.md index f76d7ae0a..0f86fd188 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ You can read the mqtt protocol via the following links: [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) -[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) +[MQTT SN](https://www.oasis-open.org/committees/download.php/66091/MQTT-SN_spec_v1.2.pdf) ## License From db38137d5c8f52d4ab46c26504c258cd46ac8516 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 1 Jul 2021 15:00:59 +0800 Subject: [PATCH 054/379] feat(config): apply new config struct to emqx_alarm --- apps/emqx/src/emqx_alarm.erl | 59 ++++++++++----------------- apps/emqx/src/emqx_config_handler.erl | 1 + apps/emqx/src/emqx_kernel_sup.erl | 17 ++++---- apps/emqx/src/emqx_schema.erl | 45 +++++++++----------- apps/emqx/src/emqx_sys_sup.erl | 2 +- bin/emqx | 4 +- 6 files changed, 54 insertions(+), 74 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 62ce1af8b..9fa4b3c3a 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -29,7 +29,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --export([ start_link/1 +-export([ start_link/0 , stop/0 ]). @@ -75,17 +75,9 @@ }). -record(state, { - actions :: [action()], - - size_limit :: non_neg_integer(), - - validity_period :: non_neg_integer(), - timer = undefined :: undefined | reference() }). --type action() :: log | publish | event. - -define(ACTIVATED_ALARM, emqx_activated_alarm). -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). @@ -120,8 +112,8 @@ mnesia(copy) -> %% API %%-------------------------------------------------------------------- -start_link(Opts) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). stop() -> gen_server:stop(?MODULE). @@ -158,22 +150,15 @@ get_alarms(deactivated) -> %%-------------------------------------------------------------------- init([]) -> - Opts = [{actions, [log, publish]}], - init([Opts]); -init([Opts]) -> deactivate_all_alarms(), - Actions = proplists:get_value(actions, Opts), - SizeLimit = proplists:get_value(size_limit, Opts), - ValidityPeriod = timer:seconds(proplists:get_value(validity_period, Opts)), - {ok, ensure_delete_timer(#state{actions = Actions, - size_limit = SizeLimit, - validity_period = ValidityPeriod})}. + ensure_delete_timer(), + {ok, #state{}}. %% suppress dialyzer warning due to dirty read/write race condition. %% TODO: change from dirty_read/write to transactional. %% TODO: handle mnesia write errors. -dialyzer([{nowarn_function, [handle_call/3]}]). -handle_call({activate_alarm, Name, Details}, _From, State = #state{actions = Actions}) -> +handle_call({activate_alarm, Name, Details}, _From, State) -> case mnesia:dirty_read(?ACTIVATED_ALARM, Name) of [#activated_alarm{name = Name}] -> {reply, {error, already_existed}, State}; @@ -183,17 +168,16 @@ handle_call({activate_alarm, Name, Details}, _From, State = #state{actions = Act message = normalize_message(Name, Details), activate_at = erlang:system_time(microsecond)}, mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), - do_actions(activate, Alarm, Actions), + do_actions(activate, Alarm, emqx_config:get([alarm, actions])), {reply, ok, State} end; -handle_call({deactivate_alarm, Name, Details}, _From, State = #state{ - actions = Actions, size_limit = SizeLimit}) -> +handle_call({deactivate_alarm, Name, Details}, _From, State) -> case mnesia:dirty_read(?ACTIVATED_ALARM, Name) of [] -> {reply, {error, not_found}, State}; [Alarm] -> - deactivate_alarm(Details, SizeLimit, Actions, Alarm), + deactivate_alarm(Details, Alarm), {reply, ok, State} end; @@ -223,11 +207,11 @@ handle_cast(Msg, State) -> ?LOG(error, "Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({timeout, TRef, delete_expired_deactivated_alarm}, - State = #state{timer = TRef, - validity_period = ValidityPeriod}) -> +handle_info({timeout, _TRef, delete_expired_deactivated_alarm}, State) -> + ValidityPeriod = emqx_config:get([alarm, validity_period]), delete_expired_deactivated_alarms(erlang:system_time(microsecond) - ValidityPeriod * 1000), - {noreply, ensure_delete_timer(State)}; + ensure_delete_timer(), + {noreply, State}; handle_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), @@ -243,11 +227,10 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ -deactivate_alarm(Details, SizeLimit, Actions, #activated_alarm{ - activate_at = ActivateAt, name = Name, details = Details0, - message = Msg0}) -> - case SizeLimit > 0 andalso - (mnesia:table_info(?DEACTIVATED_ALARM, size) >= SizeLimit) of +deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name, + details = Details0, message = Msg0}) -> + SizeLimit = emqx_config:get([alarm, size_limit]), + case SizeLimit > 0 andalso (mnesia:table_info(?DEACTIVATED_ALARM, size) >= SizeLimit) of true -> case mnesia:dirty_first(?DEACTIVATED_ALARM) of '$end_of_table' -> ok; @@ -263,7 +246,7 @@ deactivate_alarm(Details, SizeLimit, Actions, #activated_alarm{ erlang:system_time(microsecond)), mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), mnesia:dirty_delete(?ACTIVATED_ALARM, Name), - do_actions(deactivate, DeActAlarm, Actions). + do_actions(deactivate, DeActAlarm, emqx_config:get([alarm, actions])). make_deactivated_alarm(ActivateAt, Name, Details, Message, DeActivateAt) -> #deactivated_alarm{ @@ -299,9 +282,9 @@ clear_table(TableName) -> ok end. -ensure_delete_timer(State = #state{validity_period = ValidityPeriod}) -> - TRef = emqx_misc:start_timer(ValidityPeriod, delete_expired_deactivated_alarm), - State#state{timer = TRef}. +ensure_delete_timer() -> + emqx_misc:start_timer(emqx_config:get([alarm, validity_period]), + delete_expired_deactivated_alarm). delete_expired_deactivated_alarms(Checkpoint) -> delete_expired_deactivated_alarms(mnesia:dirty_first(?DEACTIVATED_ALARM), Checkpoint). diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index bc915d778..138521929 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -160,6 +160,7 @@ save_configs(RootKeys, RawConf) -> % end, MappedEnvs). save_config_to_emqx(Conf, RawConf) -> + ?LOG(debug, "set config: ~p", [Conf]), emqx_config:put(emqx_config:unsafe_atom_key_map(Conf)), emqx_config:put_raw(RawConf). diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index 4e29431e2..1c6b0617e 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -27,14 +27,15 @@ start_link() -> init([]) -> {ok, {{one_for_one, 10, 100}, - [child_spec(emqx_global_gc, worker), - child_spec(emqx_pool_sup, supervisor), - child_spec(emqx_hooks, worker), - child_spec(emqx_stats, worker), - child_spec(emqx_metrics, worker), - child_spec(emqx_ctl, worker), - child_spec(emqx_zone, worker), - child_spec(emqx_config_handler, worker) + %% always start emqx_config_handler first to load the emqx.conf to emqx_config + [ child_spec(emqx_config_handler, worker) + , child_spec(emqx_global_gc, worker) + , child_spec(emqx_pool_sup, supervisor) + , child_spec(emqx_hooks, worker) + , child_spec(emqx_stats, worker) + , child_spec(emqx_metrics, worker) + , child_spec(emqx_ctl, worker) + , child_spec(emqx_zone, worker) ]}}. child_spec(M, Type) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 3ec691bc0..76993bb94 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -9,7 +9,6 @@ -include_lib("typerefl/include/types.hrl"). -type log_level() :: debug | info | notice | warning | error | critical | alert | emergency | all. --type flag() :: true | false. -type duration() :: integer(). -type duration_s() :: integer(). -type duration_ms() :: integer(). @@ -22,7 +21,6 @@ -type bar_separated_list() :: list(). -type ip_port() :: tuple(). --typerefl_from_string({flag/0, emqx_schema, to_flag}). -typerefl_from_string({duration/0, emqx_schema, to_duration}). -typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}). -typerefl_from_string({duration_ms/0, emqx_schema, to_duration_ms}). @@ -37,13 +35,13 @@ % workaround: prevent being recognized as unused functions -export([to_duration/1, to_duration_s/1, to_duration_ms/1, to_bytesize/1, to_wordsize/1, - to_flag/1, to_percent/1, to_comma_separated_list/1, + to_percent/1, to_comma_separated_list/1, to_bar_separated_list/1, to_ip_port/1, to_comma_separated_atoms/1]). -behaviour(hocon_schema). --reflect_type([ log_level/0, flag/0, duration/0, duration_s/0, duration_ms/0, +-reflect_type([ log_level/0, duration/0, duration_s/0, duration_ms/0, bytesize/0, wordsize/0, percent/0, file/0, comma_separated_list/0, bar_separated_list/0, ip_port/0, comma_separated_atoms/0]). @@ -75,7 +73,7 @@ fields("cluster") -> , {"discovery_strategy", t(union([manual, static, mcast, dns, etcd, k8s]), undefined, manual)} , {"autoclean", t(duration(), "ekka.cluster_autoclean", undefined)} - , {"autoheal", t(flag(), "ekka.cluster_autoheal", false)} + , {"autoheal", t(boolean(), "ekka.cluster_autoheal", false)} , {"static", ref("static")} , {"mcast", ref("mcast")} , {"proto_dist", t(union([inet_tcp, inet6_tcp, inet_tls]), "ekka.proto_dist", inet_tcp)} @@ -94,7 +92,7 @@ fields("mcast") -> , {"ports", t(comma_separated_list(), undefined, "4369")} , {"iface", t(string(), undefined, "0.0.0.0")} , {"ttl", t(integer(), undefined, 255)} - , {"loop", t(flag(), undefined, true)} + , {"loop", t(boolean(), undefined, true)} , {"sndbuf", t(bytesize(), undefined, "16KB")} , {"recbuf", t(bytesize(), undefined, "16KB")} , {"buffer", t(bytesize(), undefined, "32KB")} @@ -183,7 +181,7 @@ fields("log") -> ]; fields("console_handler") -> - [ {"enable", t(flag(), undefined, false)} + [ {"enable", t(boolean(), undefined, false)} , {"level", t(log_level(), undefined, warning)} ]; @@ -199,26 +197,26 @@ fields("log_file_handler") -> ]; fields("log_rotation") -> - [ {"enable", t(flag(), undefined, true)} + [ {"enable", t(boolean(), undefined, true)} , {"count", t(range(1, 2048), undefined, 10)} ]; fields("log_overload_kill") -> - [ {"enable", t(flag(), undefined, true)} + [ {"enable", t(boolean(), undefined, true)} , {"mem_size", t(bytesize(), undefined, "30MB")} , {"qlen", t(integer(), undefined, 20000)} , {"restart_after", t(union(duration(), infinity), undefined, "5s")} ]; fields("log_burst_limit") -> - [ {"enable", t(flag(), undefined, true)} + [ {"enable", t(boolean(), undefined, true)} , {"max_count", t(integer(), undefined, 10000)} , {"window_time", t(duration(), undefined, "1s")} ]; fields("lager") -> [ {"handlers", t(string(), "lager.handlers", "")} - , {"crash_log", t(flag(), "lager.crash_log", false)} + , {"crash_log", t(boolean(), "lager.crash_log", false)} ]; fields("stats") -> @@ -258,7 +256,7 @@ fields("mqtt") -> , {"server_keepalive", maybe_disabled(integer())} , {"keepalive_backoff", t(float(), undefined, 0.75)} , {"max_subscriptions", maybe_infinity(integer())} - , {"upgrade_qos", t(flag(), undefined, false)} + , {"upgrade_qos", t(boolean(), undefined, false)} , {"max_inflight", t(range(1, 65535))} , {"retry_interval", t(duration_s(), undefined, "30s")} , {"max_awaiting_rel", maybe_infinity(duration())} @@ -329,7 +327,7 @@ fields("force_shutdown") -> ]; fields("conn_congestion") -> - [ {"enable_alarm", t(flag(), undefined, false)} + [ {"enable_alarm", t(boolean(), undefined, false)} , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} ]; @@ -380,11 +378,11 @@ fields("tcp_opts") -> [ {"active_n", t(integer(), undefined, 100)} , {"backlog", t(integer(), undefined, 1024)} , {"send_timeout", t(duration(), undefined, "15s")} - , {"send_timeout_close", t(flag(), undefined, true)} + , {"send_timeout_close", t(boolean(), undefined, true)} , {"recbuf", t(bytesize())} , {"sndbuf", t(bytesize())} , {"buffer", t(bytesize())} - , {"tune_buffer", t(flag())} + , {"tune_buffer", t(boolean())} , {"high_watermark", t(bytesize(), undefined, "1MB")} , {"nodelay", t(boolean())} , {"reuseaddr", t(boolean())} @@ -444,11 +442,11 @@ fields("plugins") -> fields("broker") -> [ {"sys_msg_interval", maybe_disabled(duration(), "1m")} , {"sys_heartbeat_interval", maybe_disabled(duration(), "30s")} - , {"enable_session_registry", t(flag(), undefined, true)} + , {"enable_session_registry", t(boolean(), undefined, true)} , {"session_locking_strategy", t(union([local, leader, quorum, all]), undefined, quorum)} , {"shared_subscription_strategy", t(union(random, round_robin), undefined, round_robin)} , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} - , {"route_batch_clean", t(flag(), undefined, true)} + , {"route_batch_clean", t(boolean(), undefined, true)} , {"perf", ref("perf")} ]; @@ -504,7 +502,7 @@ mqtt_listener() -> , {"max_connections", maybe_infinity(integer(), infinity)} , {"rate_limit", ref("rate_limit")} , {"access_rules", t(hoconsc:array(string()))} - , {"proxy_protocol", t(flag())} + , {"proxy_protocol", t(boolean(), undefined, false)} , {"proxy_protocol_timeout", t(duration())} ]. @@ -628,15 +626,15 @@ filter(Opts) -> %% ...] ssl(Defaults) -> D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"enable", t(flag(), undefined, D("enable"))} + [ {"enable", t(boolean(), undefined, D("enable"))} , {"cacertfile", t(string(), undefined, D("cacertfile"))} , {"certfile", t(string(), undefined, D("certfile"))} , {"keyfile", t(string(), undefined, D("keyfile"))} , {"verify", t(union(verify_peer, verify_none), undefined, D("verify"))} , {"fail_if_no_peer_cert", t(boolean(), undefined, D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", t(flag(), undefined, D("secure_renegotiate"))} - , {"reuse_sessions", t(flag(), undefined, D("reuse_sessions"))} - , {"honor_cipher_order", t(flag(), undefined, D("honor_cipher_order"))} + , {"secure_renegotiate", t(boolean(), undefined, D("secure_renegotiate"))} + , {"reuse_sessions", t(boolean(), undefined, D("reuse_sessions"))} + , {"honor_cipher_order", t(boolean(), undefined, D("honor_cipher_order"))} , {"handshake_timeout", t(duration(), undefined, D("handshake_timeout"))} , {"depth", t(integer(), undefined, D("depth"))} , {"password", hoconsc:t(string(), #{default => D("key_password"), @@ -764,9 +762,6 @@ maybe_infinity(T, Default) -> maybe_sth(What, Type, Default) -> t(union([What, Type]), undefined, Default). -to_flag(Str) -> - {ok, hocon_postprocess:onoff(Str)}. - to_duration(Str) -> case hocon_postprocess:duration(Str) of I when is_integer(I) -> {ok, I}; diff --git a/apps/emqx/src/emqx_sys_sup.erl b/apps/emqx/src/emqx_sys_sup.erl index 50d086156..265184f05 100644 --- a/apps/emqx/src/emqx_sys_sup.erl +++ b/apps/emqx/src/emqx_sys_sup.erl @@ -27,7 +27,7 @@ start_link() -> init([]) -> Childs = [child_spec(emqx_sys), - child_spec(emqx_alarm, [config(alarm)]), + child_spec(emqx_alarm), child_spec(emqx_sys_mon, [config(sysmon)]), child_spec(emqx_os_mon, [config(os_mon)]), child_spec(emqx_vm_mon, [config(vm_mon)])], diff --git a/bin/emqx b/bin/emqx index 70b878715..22593ea82 100755 --- a/bin/emqx +++ b/bin/emqx @@ -582,7 +582,7 @@ case "$1" in # set before generate_config if [ "${_EMQX_START_MODE:-}" = '' ]; then - export EMQX_CONSOLE_HANDLER__ENABLE="${EMQX_CONSOLE_HANDLER__ENABLE:-true}" + export EMQX_LOG__CONSOLE_HANDLER__ENABLE="${EMQX_LOG__CONSOLE_HANDLER__ENABLE:-true}" fi #generate app.config and vm.args @@ -626,7 +626,7 @@ case "$1" in # or other supervision services # set before generate_config - export EMQX_CONSOLE_HANDLER__ENABLE="${EMQX_CONSOLE_HANDLER__ENABLE:-true}" + export EMQX_LOG__CONSOLE_HANDLER__ENABLE="${EMQX_LOG__CONSOLE_HANDLER__ENABLE:-true}" #generate app.config and vm.args generate_config From aad393f8d7abf5bf7c27484cc4feaf3ccc6f9d06 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 1 Jul 2021 16:27:59 +0800 Subject: [PATCH 055/379] feat(config): apply new config struct to emqx_sys_mon --- apps/emqx/src/emqx_sys_mon.erl | 62 +++++++++++++++------------------- apps/emqx/src/emqx_sys_sup.erl | 2 +- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/apps/emqx/src/emqx_sys_mon.erl b/apps/emqx/src/emqx_sys_mon.erl index 152f975eb..54a5c533a 100644 --- a/apps/emqx/src/emqx_sys_mon.erl +++ b/apps/emqx/src/emqx_sys_mon.erl @@ -23,7 +23,7 @@ -logger_header("[SYSMON]"). --export([start_link/1]). +-export([start_link/0]). %% compress unused warning -export([procinfo/1]). @@ -37,25 +37,19 @@ , code_change/3 ]). --type(option() :: {long_gc, non_neg_integer()} - | {long_schedule, non_neg_integer()} - | {large_heap, non_neg_integer()} - | {busy_port, boolean()} - | {busy_dist_port, boolean()}). - -define(SYSMON, ?MODULE). %% @doc Start the system monitor. --spec(start_link(list(option())) -> startlink_ret()). -start_link(Opts) -> - gen_server:start_link({local, ?SYSMON}, ?MODULE, [Opts], []). +-spec(start_link() -> startlink_ret()). +start_link() -> + gen_server:start_link({local, ?SYSMON}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([Opts]) -> - _ = erlang:system_monitor(self(), parse_opt(Opts)), +init([]) -> + _ = erlang:system_monitor(self(), sysm_opts()), emqx_logger:set_proc_metadata(#{sysmon => true}), %% Monitor cluster partition event @@ -66,30 +60,28 @@ init([Opts]) -> start_timer(State) -> State#{timer := emqx_misc:start_timer(timer:seconds(2), reset)}. -parse_opt(Opts) -> - parse_opt(Opts, []). -parse_opt([], Acc) -> +sysm_opts() -> + sysm_opts(maps:to_list(emqx_config:get([sysmon, vm])), []). +sysm_opts([], Acc) -> Acc; -parse_opt([{long_gc, 0}|Opts], Acc) -> - parse_opt(Opts, Acc); -parse_opt([{long_gc, Ms}|Opts], Acc) when is_integer(Ms) -> - parse_opt(Opts, [{long_gc, Ms}|Acc]); -parse_opt([{long_schedule, 0}|Opts], Acc) -> - parse_opt(Opts, Acc); -parse_opt([{long_schedule, Ms}|Opts], Acc) when is_integer(Ms) -> - parse_opt(Opts, [{long_schedule, Ms}|Acc]); -parse_opt([{large_heap, Size}|Opts], Acc) when is_integer(Size) -> - parse_opt(Opts, [{large_heap, Size}|Acc]); -parse_opt([{busy_port, true}|Opts], Acc) -> - parse_opt(Opts, [busy_port|Acc]); -parse_opt([{busy_port, false}|Opts], Acc) -> - parse_opt(Opts, Acc); -parse_opt([{busy_dist_port, true}|Opts], Acc) -> - parse_opt(Opts, [busy_dist_port|Acc]); -parse_opt([{busy_dist_port, false}|Opts], Acc) -> - parse_opt(Opts, Acc); -parse_opt([_Opt|Opts], Acc) -> - parse_opt(Opts, Acc). +sysm_opts([{_, disabled}|Opts], Acc) -> + sysm_opts(Opts, Acc); +sysm_opts([{long_gc, Ms}|Opts], Acc) when is_integer(Ms) -> + sysm_opts(Opts, [{long_gc, Ms}|Acc]); +sysm_opts([{long_schedule, Ms}|Opts], Acc) when is_integer(Ms) -> + sysm_opts(Opts, [{long_schedule, Ms}|Acc]); +sysm_opts([{large_heap, Size}|Opts], Acc) when is_integer(Size) -> + sysm_opts(Opts, [{large_heap, Size}|Acc]); +sysm_opts([{busy_port, true}|Opts], Acc) -> + sysm_opts(Opts, [busy_port|Acc]); +sysm_opts([{busy_port, false}|Opts], Acc) -> + sysm_opts(Opts, Acc); +sysm_opts([{busy_dist_port, true}|Opts], Acc) -> + sysm_opts(Opts, [busy_dist_port|Acc]); +sysm_opts([{busy_dist_port, false}|Opts], Acc) -> + sysm_opts(Opts, Acc); +sysm_opts([_Opt|Opts], Acc) -> + sysm_opts(Opts, Acc). handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), diff --git a/apps/emqx/src/emqx_sys_sup.erl b/apps/emqx/src/emqx_sys_sup.erl index 265184f05..e9d968d5a 100644 --- a/apps/emqx/src/emqx_sys_sup.erl +++ b/apps/emqx/src/emqx_sys_sup.erl @@ -28,7 +28,7 @@ start_link() -> init([]) -> Childs = [child_spec(emqx_sys), child_spec(emqx_alarm), - child_spec(emqx_sys_mon, [config(sysmon)]), + child_spec(emqx_sys_mon), child_spec(emqx_os_mon, [config(os_mon)]), child_spec(emqx_vm_mon, [config(vm_mon)])], {ok, {{one_for_one, 10, 100}, Childs}}. From 8dcb5ceb8635ac1b6fc95ce1e505663c12f6878e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 1 Jul 2021 18:01:38 +0800 Subject: [PATCH 056/379] fix(config): get config problems in sysmon --- apps/emqx/src/emqx_alarm_handler.erl | 16 ++--- apps/emqx/src/emqx_os_mon.erl | 54 +++++++++-------- apps/emqx/src/emqx_schema.erl | 2 +- apps/emqx/src/emqx_sys.erl | 4 +- apps/emqx/src/emqx_sys_sup.erl | 7 +-- apps/emqx/src/emqx_vm_mon.erl | 81 +++++--------------------- apps/emqx_authz/src/emqx_authz_app.erl | 2 +- 7 files changed, 57 insertions(+), 109 deletions(-) diff --git a/apps/emqx/src/emqx_alarm_handler.erl b/apps/emqx/src/emqx_alarm_handler.erl index a69913afd..2307b79db 100644 --- a/apps/emqx/src/emqx_alarm_handler.erl +++ b/apps/emqx/src/emqx_alarm_handler.erl @@ -56,20 +56,22 @@ init({_Args, {alarm_handler, _ExistingAlarms}}) -> init(_) -> {ok, []}. -handle_event({set_alarm, {system_memory_high_watermark, []}}, State) -> - emqx_alarm:activate(high_system_memory_usage, #{high_watermark => emqx_os_mon:get_sysmem_high_watermark()}), +handle_event({set_alarm, {system_memory_high_watermark, []}}, State) -> + emqx_alarm:activate(high_system_memory_usage, + #{high_watermark => emqx_os_mon:get_sysmem_high_watermark()}), {ok, State}; -handle_event({set_alarm, {process_memory_high_watermark, Pid}}, State) -> - emqx_alarm:activate(high_process_memory_usage, #{pid => Pid, - high_watermark => emqx_os_mon:get_procmem_high_watermark()}), +handle_event({set_alarm, {process_memory_high_watermark, Pid}}, State) -> + emqx_alarm:activate(high_process_memory_usage, + #{pid => list_to_binary(pid_to_list(Pid)), + high_watermark => emqx_os_mon:get_procmem_high_watermark()}), {ok, State}; -handle_event({clear_alarm, system_memory_high_watermark}, State) -> +handle_event({clear_alarm, system_memory_high_watermark}, State) -> emqx_alarm:deactivate(high_system_memory_usage), {ok, State}; -handle_event({clear_alarm, process_memory_high_watermark}, State) -> +handle_event({clear_alarm, process_memory_high_watermark}, State) -> emqx_alarm:deactivate(high_process_memory_usage), {ok, State}; diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index d6579cac9..8768586ce 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -22,7 +22,7 @@ -logger_header("[OS_MON]"). --export([start_link/1]). +-export([start_link/0]). -export([ get_cpu_check_interval/0 , set_cpu_check_interval/1 @@ -51,8 +51,8 @@ -define(OS_MON, ?MODULE). -start_link(Opts) -> - gen_server:start_link({local, ?OS_MON}, ?MODULE, [Opts], []). +start_link() -> + gen_server:start_link({local, ?OS_MON}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% API @@ -88,13 +88,13 @@ get_sysmem_high_watermark() -> memsup:get_sysmem_high_watermark(). set_sysmem_high_watermark(Float) -> - memsup:set_sysmem_high_watermark(Float / 100). + memsup:set_sysmem_high_watermark(Float). get_procmem_high_watermark() -> memsup:get_procmem_high_watermark(). set_procmem_high_watermark(Float) -> - memsup:set_procmem_high_watermark(Float / 100). + memsup:set_procmem_high_watermark(Float). call(Req) -> gen_server:call(?OS_MON, Req, infinity). @@ -103,14 +103,13 @@ call(Req) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([Opts]) -> - set_mem_check_interval(proplists:get_value(mem_check_interval, Opts)), - set_sysmem_high_watermark(proplists:get_value(sysmem_high_watermark, Opts)), - set_procmem_high_watermark(proplists:get_value(procmem_high_watermark, Opts)), - {ok, ensure_check_timer(#{cpu_high_watermark => proplists:get_value(cpu_high_watermark, Opts), - cpu_low_watermark => proplists:get_value(cpu_low_watermark, Opts), - cpu_check_interval => proplists:get_value(cpu_check_interval, Opts), - timer => undefined})}. +init([]) -> + Opts = emqx_config:get([sysmon, os]), + set_mem_check_interval(maps:get(mem_check_interval, Opts)), + set_sysmem_high_watermark(maps:get(sysmem_high_watermark, Opts)), + set_procmem_high_watermark(maps:get(procmem_high_watermark, Opts)), + start_check_timer(), + {ok, #{}}. handle_call(get_cpu_check_interval, _From, State) -> {reply, maps:get(cpu_check_interval, State, undefined), State}; @@ -138,32 +137,30 @@ handle_cast(Msg, State) -> ?LOG(error, "Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, Timer, check}, State = #{timer := Timer, - cpu_high_watermark := CPUHighWatermark, - cpu_low_watermark := CPULowWatermark}) -> - NState = +handle_info({timeout, _Timer, check}, State) -> + CPUHighWatermark = emqx_config:get([sysmon, os, cpu_high_watermark]) * 100, + CPULowWatermark = emqx_config:get([sysmon, os, cpu_low_watermark]) * 100, case emqx_vm:cpu_util() of %% TODO: should be improved? - 0 -> - State#{timer := undefined}; + 0 -> ok; Busy when Busy >= CPUHighWatermark -> emqx_alarm:activate(high_cpu_usage, #{usage => Busy, high_watermark => CPUHighWatermark, low_watermark => CPULowWatermark}), - ensure_check_timer(State); + start_check_timer(); Busy when Busy =< CPULowWatermark -> emqx_alarm:deactivate(high_cpu_usage), - ensure_check_timer(State); + start_check_timer(); _Busy -> - ensure_check_timer(State) + start_check_timer() end, - {noreply, NState}; + {noreply, State}; handle_info(Info, State) -> ?LOG(error, "unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #{timer := Timer}) -> - emqx_misc:cancel_timer(Timer). +terminate(_Reason, _State) -> + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -172,8 +169,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -ensure_check_timer(State = #{cpu_check_interval := Interval}) -> +start_check_timer() -> + Interval = emqx_config:get([sysmon, os, cpu_check_interval]), case erlang:system_info(system_architecture) of - "x86_64-pc-linux-musl" -> State; - _ -> State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)} + "x86_64-pc-linux-musl" -> ok; + _ -> emqx_misc:start_timer(timer:seconds(Interval), check) end. diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 76993bb94..f4900d36b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -481,7 +481,7 @@ fields("sysmon_os") -> ]; fields("alarm") -> - [ {"actions", t(comma_separated_list(), undefined, "log,publish")} + [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} , {"size_limit", t(integer(), undefined, 1000)} , {"validity_period", t(duration_s(), undefined, "24h")} ]; diff --git a/apps/emqx/src/emqx_sys.erl b/apps/emqx/src/emqx_sys.erl index 2d816569d..6b71b7807 100644 --- a/apps/emqx/src/emqx_sys.erl +++ b/apps/emqx/src/emqx_sys.erl @@ -107,12 +107,12 @@ datetime() -> %% @doc Get sys interval -spec(sys_interval() -> pos_integer()). sys_interval() -> - emqx:get_env(broker_sys_interval, 60000). + emqx_config:get([broker, sys_msg_interval]). %% @doc Get sys heatbeat interval -spec(sys_heatbeat_interval() -> pos_integer()). sys_heatbeat_interval() -> - emqx:get_env(broker_sys_heartbeat, 30000). + emqx_config:get([broker, sys_heartbeat_interval]). %% @doc Get sys info -spec(info() -> list(tuple())). diff --git a/apps/emqx/src/emqx_sys_sup.erl b/apps/emqx/src/emqx_sys_sup.erl index e9d968d5a..61342fd0e 100644 --- a/apps/emqx/src/emqx_sys_sup.erl +++ b/apps/emqx/src/emqx_sys_sup.erl @@ -29,8 +29,8 @@ init([]) -> Childs = [child_spec(emqx_sys), child_spec(emqx_alarm), child_spec(emqx_sys_mon), - child_spec(emqx_os_mon, [config(os_mon)]), - child_spec(emqx_vm_mon, [config(vm_mon)])], + child_spec(emqx_os_mon), + child_spec(emqx_vm_mon)], {ok, {{one_for_one, 10, 100}, Childs}}. %%-------------------------------------------------------------------- @@ -48,6 +48,3 @@ child_spec(Mod, Args) -> type => worker, modules => [Mod] }. - -config(Name) -> emqx:get_env(Name, []). - diff --git a/apps/emqx/src/emqx_vm_mon.erl b/apps/emqx/src/emqx_vm_mon.erl index ce34fff43..79b1537d4 100644 --- a/apps/emqx/src/emqx_vm_mon.erl +++ b/apps/emqx/src/emqx_vm_mon.erl @@ -21,15 +21,7 @@ -include("logger.hrl"). %% APIs --export([start_link/1]). - --export([ get_check_interval/0 - , set_check_interval/1 - , get_process_high_watermark/0 - , set_process_high_watermark/1 - , get_process_low_watermark/0 - , set_process_low_watermark/1 - ]). +-export([start_link/0]). %% gen_server callbacks -export([ init/1 @@ -42,61 +34,19 @@ -define(VM_MON, ?MODULE). -start_link(Opts) -> - gen_server:start_link({local, ?VM_MON}, ?MODULE, [Opts], []). - %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- - -get_check_interval() -> - call(get_check_interval). - -set_check_interval(Seconds) -> - call({set_check_interval, Seconds}). - -get_process_high_watermark() -> - call(get_process_high_watermark). - -set_process_high_watermark(Float) -> - call({set_process_high_watermark, Float}). - -get_process_low_watermark() -> - call(get_process_low_watermark). - -set_process_low_watermark(Float) -> - call({set_process_low_watermark, Float}). - -call(Req) -> - gen_server:call(?VM_MON, Req, infinity). +start_link() -> + gen_server:start_link({local, ?VM_MON}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([Opts]) -> - {ok, ensure_check_timer(#{check_interval => proplists:get_value(check_interval, Opts), - process_high_watermark => proplists:get_value(process_high_watermark, Opts), - process_low_watermark => proplists:get_value(process_low_watermark, Opts), - timer => undefined})}. - -handle_call(get_check_interval, _From, State) -> - {reply, maps:get(check_interval, State, undefined), State}; - -handle_call({set_check_interval, Seconds}, _From, State) -> - {reply, ok, State#{check_interval := Seconds}}; - -handle_call(get_process_high_watermark, _From, State) -> - {reply, maps:get(process_high_watermark, State, undefined), State}; - -handle_call({set_process_high_watermark, Float}, _From, State) -> - {reply, ok, State#{process_high_watermark := Float}}; - -handle_call(get_process_low_watermark, _From, State) -> - {reply, maps:get(process_low_watermark, State, undefined), State}; - -handle_call({set_process_low_watermark, Float}, _From, State) -> - {reply, ok, State#{process_low_watermark := Float}}; +init([]) -> + start_check_timer(), + {ok, #{}}. handle_call(Req, _From, State) -> ?LOG(error, "[VM_MON] Unexpected call: ~p", [Req]), @@ -106,10 +56,9 @@ handle_cast(Msg, State) -> ?LOG(error, "[VM_MON] Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, Timer, check}, - State = #{timer := Timer, - process_high_watermark := ProcHighWatermark, - process_low_watermark := ProcLowWatermark}) -> +handle_info({timeout, _Timer, check}, State) -> + ProcHighWatermark = emqx_config:get([sysmon, vm, process_high_watermark]), + ProcLowWatermark = emqx_config:get([sysmon, vm, process_low_watermark]), ProcessCount = erlang:system_info(process_count), case ProcessCount / erlang:system_info(process_limit) * 100 of Percent when Percent >= ProcHighWatermark -> @@ -121,14 +70,15 @@ handle_info({timeout, Timer, check}, _Precent -> ok end, - {noreply, ensure_check_timer(State)}; + start_check_timer(), + {noreply, State}; handle_info(Info, State) -> ?LOG(error, "[VM_MON] Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #{timer := Timer}) -> - emqx_misc:cancel_timer(Timer). +terminate(_Reason, _State) -> + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -137,5 +87,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -ensure_check_timer(State = #{check_interval := Interval}) -> - State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}. +start_check_timer() -> + Interval = emqx_config:get([sysmon, vm, process_check_interval]), + emqx_misc:start_timer(timer:seconds(Interval), check). diff --git a/apps/emqx_authz/src/emqx_authz_app.erl b/apps/emqx_authz/src/emqx_authz_app.erl index 460d7cbf9..dcce015c7 100644 --- a/apps/emqx_authz/src/emqx_authz_app.erl +++ b/apps/emqx_authz/src/emqx_authz_app.erl @@ -11,7 +11,7 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_authz_sup:start_link(), - ok = emqx_authz:init(), + %ok = emqx_authz:init(), {ok, Sup}. stop(_State) -> From 47ce507c07085951b229bf0c4c07baf8d1792a8b Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 1 Jul 2021 11:15:40 +0200 Subject: [PATCH 057/379] chore: disable auto assignee for github issues. save tigercl --- .github/ISSUE_TEMPLATE/bug-report.md | 1 - .github/ISSUE_TEMPLATE/feature-request.md | 1 - .github/ISSUE_TEMPLATE/support-needed.md | 1 - 3 files changed, 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 0258866dd..96d193913 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -3,7 +3,6 @@ name: Bug Report about: Create a report to help us improve title: '' labels: Support -assignees: tigercl --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 67b1dfa82..0519e5699 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -3,7 +3,6 @@ name: Feature Request about: Suggest an idea for this project title: '' labels: Feature -assignees: tigercl --- diff --git a/.github/ISSUE_TEMPLATE/support-needed.md b/.github/ISSUE_TEMPLATE/support-needed.md index 80b494077..18b47bfb5 100644 --- a/.github/ISSUE_TEMPLATE/support-needed.md +++ b/.github/ISSUE_TEMPLATE/support-needed.md @@ -3,7 +3,6 @@ name: Support Needed about: Asking a question about usages, docs or anything you're insterested in title: '' labels: Support -assignees: tigercl --- From bf4c31b745aa3438d3e31c5c37aac08e825cf6f1 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 1 Jul 2021 15:52:25 +0800 Subject: [PATCH 058/379] chore(authz): use atom key for hocon config --- apps/emqx_authz/etc/emqx_authz.conf | 2 +- apps/emqx_authz/include/emqx_authz.hrl | 2 +- apps/emqx_authz/src/emqx_authz.erl | 105 +++++++++--------- apps/emqx_authz/src/emqx_authz_api.erl | 7 +- apps/emqx_authz/src/emqx_authz_mysql.erl | 10 +- apps/emqx_authz/src/emqx_authz_pgsql.erl | 10 +- apps/emqx_authz/src/emqx_authz_redis.erl | 14 +-- apps/emqx_authz/src/emqx_authz_schema.erl | 34 +++--- apps/emqx_authz/test/emqx_authz_SUITE.erl | 103 ++++++++--------- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 5 +- .../test/emqx_authz_mysql_SUITE.erl | 8 +- .../test/emqx_authz_pgsql_SUITE.erl | 8 +- .../test/emqx_authz_redis_SUITE.erl | 8 +- 13 files changed, 167 insertions(+), 149 deletions(-) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index b2575e6d6..89515592f 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,4 +1,4 @@ -authz:{ +emqx_authz:{ rules: [ # { # type: mysql diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index ca595525d..9caf3d979 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -1,4 +1,4 @@ --type(rule() :: #{binary() => any()}). +-type(rule() :: #{atom() => any()}). -type(rules() :: [rule()]). -define(APP, emqx_authz). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 337e3b043..9be9fdce8 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -38,16 +38,17 @@ init() -> ok = register_metrics(), Conf = filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), {ok, RawConf} = hocon:load(Conf), - #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf), - ok = application:set_env(?APP, rules, Rules), + #{emqx_authz := #{rules := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf, #{atom_key => true}), + emqx_config:put([emqx_authz], #{rules => Rules}), + % Rules = emqx_config:get([emqx_authz, rules], []), NRules = [compile(Rule) || Rule <- Rules], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> - application:get_env(?APP, rules, []). + emqx_config:get([emqx_authz, rules], []). update(Rules) -> - ok = application:set_env(?APP, rules, Rules), + emqx_config:put([emqx_authz], #{rules => Rules}), NRules = [compile(Rule) || Rule <- Rules], Action = find_action_in_hooks(), ok = emqx_hooks:del('client.authorize', Action), @@ -63,12 +64,12 @@ find_action_in_hooks() -> [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], Action. -create_resource(#{<<"type">> := DB, - <<"config">> := Config +create_resource(#{type := DB, + config := Config } = Rule) -> ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]), NConfig = case DB of - redis -> #{<<"config">> => Config }; + redis -> #{config => Config }; _ -> Config end, case emqx_resource:check_and_create( @@ -77,63 +78,63 @@ create_resource(#{<<"type">> := DB, NConfig) of {ok, _} -> - Rule#{<<"resource_id">> => ResourceID}; + Rule#{resource_id => ResourceID}; {error, already_created} -> - Rule#{<<"resource_id">> => ResourceID}; + Rule#{resource_id => ResourceID}; {error, Reason} -> error({load_config_error, Reason}) end. -spec(compile(rule()) -> rule()). -compile(#{<<"topics">> := Topics, - <<"action">> := Action, - <<"permission">> := Permission, - <<"principal">> := Principal +compile(#{topics := Topics, + action := Action, + permission := Permission, + principal := Principal } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> NTopics = [compile_topic(Topic) || Topic <- Topics], - Rule#{<<"principal">> => compile_principal(Principal), - <<"topics">> => NTopics + Rule#{principal => compile_principal(Principal), + topics => NTopics }; -compile(#{<<"principal">> := Principal, - <<"type">> := redis +compile(#{principal := Principal, + type := redis } = Rule) -> NRule = create_resource(Rule), - NRule#{<<"principal">> => compile_principal(Principal)}; + NRule#{principal => compile_principal(Principal)}; -compile(#{<<"principal">> := Principal, - <<"type">> := DB, - <<"sql">> := SQL +compile(#{principal := Principal, + type := DB, + sql := SQL } = Rule) when DB =:= mysql; DB =:= pgsql -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), NRule = create_resource(Rule), - NRule#{<<"principal">> => compile_principal(Principal), - <<"sql">> => Mod:parse_query(SQL) + NRule#{principal => compile_principal(Principal), + sql => Mod:parse_query(SQL) }. compile_principal(all) -> all; -compile_principal(#{<<"username">> := Username}) -> +compile_principal(#{username := Username}) -> {ok, MP} = re:compile(bin(Username)), - #{<<"username">> => MP}; -compile_principal(#{<<"clientid">> := Clientid}) -> + #{username => MP}; +compile_principal(#{clientid := Clientid}) -> {ok, MP} = re:compile(bin(Clientid)), - #{<<"clientid">> => MP}; -compile_principal(#{<<"ipaddress">> := IpAddress}) -> - #{<<"ipaddress">> => esockd_cidr:parse(b2l(IpAddress), true)}; -compile_principal(#{<<"and">> := Principals}) when is_list(Principals) -> - #{<<"and">> => [compile_principal(Principal) || Principal <- Principals]}; -compile_principal(#{<<"or">> := Principals}) when is_list(Principals) -> - #{<<"or">> => [compile_principal(Principal) || Principal <- Principals]}. + #{clientid => MP}; +compile_principal(#{ipaddress := IpAddress}) -> + #{ipaddress => esockd_cidr:parse(b2l(IpAddress), true)}; +compile_principal(#{'and' := Principals}) when is_list(Principals) -> + #{'and' => [compile_principal(Principal) || Principal <- Principals]}; +compile_principal(#{'or' := Principals}) when is_list(Principals) -> + #{'or' => [compile_principal(Principal) || Principal <- Principals]}. compile_topic(<<"eq ", Topic/binary>>) -> - compile_topic(#{<<"eq">> => Topic}); -compile_topic(#{<<"eq">> := Topic}) -> - #{<<"eq">> => emqx_topic:words(bin(Topic))}; + compile_topic(#{'eq' => Topic}); +compile_topic(#{'eq' := Topic}) -> + #{'eq' => emqx_topic:words(bin(Topic))}; compile_topic(Topic) when is_binary(Topic)-> Words = emqx_topic:words(bin(Topic)), case pattern(Words) of - true -> #{<<"pattern">> => Words}; + true -> #{pattern => Words}; false -> Words end. @@ -173,8 +174,8 @@ authorize(#{username := Username, end. do_authorize(Client, PubSub, Topic, - [Connector = #{<<"principal">> := Principal, - <<"type">> := DB} | Tail] ) -> + [Connector = #{principal := Principal, + type := DB} | Tail] ) -> case match_principal(Client, Principal) of true -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])), @@ -185,7 +186,7 @@ do_authorize(Client, PubSub, Topic, false -> do_authorize(Client, PubSub, Topic, Tail) end; do_authorize(Client, PubSub, Topic, - [#{<<"permission">> := Permission} = Rule | Tail]) -> + [#{permission := Permission} = Rule | Tail]) -> case match(Client, PubSub, Topic, Rule) of true -> {matched, Permission}; false -> do_authorize(Client, PubSub, Topic, Tail) @@ -193,9 +194,9 @@ do_authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, []) -> nomatch. match(Client, PubSub, Topic, - #{<<"principal">> := Principal, - <<"topics">> := TopicFilters, - <<"action">> := Action + #{principal := Principal, + topics := TopicFilters, + action := Action }) -> match_action(PubSub, Action) andalso match_principal(Client, Principal) andalso @@ -207,27 +208,27 @@ match_action(_, all) -> true; match_action(_, _) -> false. match_principal(_, all) -> true; -match_principal(#{username := undefined}, #{<<"username">> := _MP}) -> +match_principal(#{username := undefined}, #{username := _MP}) -> false; -match_principal(#{username := Username}, #{<<"username">> := MP}) -> +match_principal(#{username := Username}, #{username := MP}) -> case re:run(Username, MP) of {match, _} -> true; _ -> false end; -match_principal(#{clientid := Clientid}, #{<<"clientid">> := MP}) -> +match_principal(#{clientid := Clientid}, #{clientid := MP}) -> case re:run(Clientid, MP) of {match, _} -> true; _ -> false end; -match_principal(#{peerhost := undefined}, #{<<"ipaddress">> := _CIDR}) -> +match_principal(#{peerhost := undefined}, #{ipaddress := _CIDR}) -> false; -match_principal(#{peerhost := IpAddress}, #{<<"ipaddress">> := CIDR}) -> +match_principal(#{peerhost := IpAddress}, #{ipaddress := CIDR}) -> esockd_cidr:match(IpAddress, CIDR); -match_principal(ClientInfo, #{<<"and">> := Principals}) when is_list(Principals) -> +match_principal(ClientInfo, #{'and' := Principals}) when is_list(Principals) -> lists:foldl(fun(Principal, Permission) -> match_principal(ClientInfo, Principal) andalso Permission end, true, Principals); -match_principal(ClientInfo, #{<<"or">> := Principals}) when is_list(Principals) -> +match_principal(ClientInfo, #{'or' := Principals}) when is_list(Principals) -> lists:foldl(fun(Principal, Permission) -> match_principal(ClientInfo, Principal) orelse Permission end, false, Principals); @@ -235,7 +236,7 @@ match_principal(_, _) -> false. match_topics(_ClientInfo, _Topic, []) -> false; -match_topics(ClientInfo, Topic, [#{<<"pattern">> := PatternFilter}|Filters]) -> +match_topics(ClientInfo, Topic, [#{pattern := PatternFilter}|Filters]) -> TopicFilter = feed_var(ClientInfo, PatternFilter), match_topic(emqx_topic:words(Topic), TopicFilter) orelse match_topics(ClientInfo, Topic, Filters); @@ -243,7 +244,7 @@ match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> match_topic(emqx_topic:words(Topic), TopicFilter) orelse match_topics(ClientInfo, Topic, Filters). -match_topic(Topic, #{<<"eq">> := TopicFilter}) -> +match_topic(Topic, #{'eq' := TopicFilter}) -> Topic == TopicFilter; match_topic(Topic, TopicFilter) -> emqx_topic:match(Topic, TopicFilter). diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 6b4ed6c74..08ff0a7d7 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -74,10 +74,9 @@ push_authz(_Bindings, Params) -> %%------------------------------------------------------------------------------ get_rules(Params) -> - % #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, #{<<"authz">> => Params}), - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authz">> => Params}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf), - #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:richmap_to_map(CheckConf), + {ok, Conf} = hocon:binary(jsx:encode(#{<<"emqx_authz">> => Params}), #{format => richmap}), + CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), + #{emqx_authz := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), Rules. %%-------------------------------------------------------------------- diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index 6acb154fb..672954841 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -46,8 +46,8 @@ parse_query(Sql) -> end. authorize(Client, PubSub, Topic, - #{<<"resource_id">> := ResourceID, - <<"sql">> := {SQL, Params} + #{resource_id := ResourceID, + sql := {SQL, Params} }) -> case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of {ok, _Columns, []} -> nomatch; @@ -87,12 +87,12 @@ match(Client, PubSub, Topic, <<"action">> => Action, <<"permission">> => Permission }, - #{<<"simple_rule">> := - #{<<"permission">> := NPermission} = NRule + #{simple_rule := + #{permission := NPermission} = NRule } = hocon_schema:check_plain( emqx_authz_schema, #{<<"simple_rule">> => Rule}, - #{}, + #{atom_key => true}, [simple_rule]), case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of true -> {matched, NPermission}; diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index c7cebf1e2..fa24c5f5e 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -50,8 +50,8 @@ parse_query(Sql) -> end. authorize(Client, PubSub, Topic, - #{<<"resource_id">> := ResourceID, - <<"sql">> := {SQL, Params} + #{resource_id := ResourceID, + sql := {SQL, Params} }) -> case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of {ok, _Columns, []} -> nomatch; @@ -91,12 +91,12 @@ match(Client, PubSub, Topic, <<"action">> => Action, <<"permission">> => Permission }, - #{<<"simple_rule">> := - #{<<"permission">> := NPermission} = NRule + #{simple_rule := + #{permission := NPermission} = NRule } = hocon_schema:check_plain( emqx_authz_schema, #{<<"simple_rule">> => Rule}, - #{}, + #{atom_key => true}, [simple_rule]), case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of true -> {matched, NPermission}; diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 1b99dc2ec..dc80d959c 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -34,8 +34,8 @@ description() -> "AuthZ with redis". authorize(Client, PubSub, Topic, - #{<<"resource_id">> := ResourceID, - <<"cmd">> := CMD + #{resource_id := ResourceID, + cmd := CMD }) -> NCMD = string:tokens(replvar(CMD, Client), " "), case emqx_resource:query(ResourceID, {cmd, NCMD}) of @@ -50,16 +50,16 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> - case match(Client, PubSub, Topic, + case match(Client, PubSub, Topic, #{topics => TopicFilter, action => Action - }) + }) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. -match(Client, PubSub, Topic, +match(Client, PubSub, Topic, #{topics := TopicFilter, action := Action }) -> @@ -68,11 +68,11 @@ match(Client, PubSub, Topic, <<"action">> => Action, <<"permission">> => allow }, - #{<<"simple_rule">> := NRule + #{simple_rule := NRule } = hocon_schema:check_plain( emqx_authz_schema, #{<<"simple_rule">> => Rule}, - #{}, + #{atom_key => true}, [simple_rule]), case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of true -> {matched, allow}; diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 04d1b2268..5836eb12b 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -11,9 +11,9 @@ -export([structs/0, fields/1]). -structs() -> [authz]. +structs() -> ["emqx_authz"]. -fields(authz) -> +fields("emqx_authz") -> [ {rules, rules()} ]; fields(redis_connector) -> @@ -39,7 +39,7 @@ fields(simple_rule) -> , {action, #{type => action()}} , {topics, #{type => union_array( [ binary() - , hoconsc:ref(eq_topic) + , hoconsc:ref(?MODULE, eq_topic) ] )}} , {principal, principal()} @@ -52,18 +52,18 @@ fields(ipaddress) -> [{ipaddress, #{type => string()}}]; fields(andlist) -> [{'and', #{type => union_array( - [ hoconsc:ref(username) - , hoconsc:ref(clientid) - , hoconsc:ref(ipaddress) + [ hoconsc:ref(?MODULE, username) + , hoconsc:ref(?MODULE, clientid) + , hoconsc:ref(?MODULE, ipaddress) ]) } } ]; fields(orlist) -> [{'or', #{type => union_array( - [ hoconsc:ref(username) - , hoconsc:ref(clientid) - , hoconsc:ref(ipaddress) + [ hoconsc:ref(?MODULE, username) + , hoconsc:ref(?MODULE, clientid) + , hoconsc:ref(?MODULE, ipaddress) ]) } } @@ -81,9 +81,9 @@ union_array(Item) when is_list(Item) -> rules() -> #{type => union_array( - [ hoconsc:ref(simple_rule) - , hoconsc:ref(sql_connector) - , hoconsc:ref(redis_connector) + [ hoconsc:ref(?MODULE, simple_rule) + , hoconsc:ref(?MODULE, sql_connector) + , hoconsc:ref(?MODULE, redis_connector) ]) }. @@ -91,11 +91,11 @@ principal() -> #{default => all, type => hoconsc:union( [ all - , hoconsc:ref(username) - , hoconsc:ref(clientid) - , hoconsc:ref(ipaddress) - , hoconsc:ref(andlist) - , hoconsc:ref(orlist) + , hoconsc:ref(?MODULE, username) + , hoconsc:ref(?MODULE, clientid) + , hoconsc:ref(?MODULE, ipaddress) + , hoconsc:ref(?MODULE, andlist) + , hoconsc:ref(?MODULE, orlist) ]) }. diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 93be27146..944053f4e 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -43,41 +43,42 @@ set_special_configs(emqx) -> set_special_configs(emqx_authz) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => #{<<"rules">> => []}}, + Conf = #{<<"emqx_authz">> => #{<<"rules">> => []}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + % emqx_config:put([emqx_authz], #{rules => []}), ok; set_special_configs(_App) -> ok. --define(RULE1, #{<<"principal">> => all, - <<"topics">> => [<<"#">>], - <<"action">> => all, - <<"permission">> => deny} +-define(RULE1, #{principal => all, + topics => [<<"#">>], + action => all, + permission => deny} ). --define(RULE2, #{<<"principal">> => - #{<<"ipaddress">> => <<"127.0.0.1">>}, - <<"topics">> => - [#{<<"eq">> => <<"#">>}, - #{<<"eq">> => <<"+">>} +-define(RULE2, #{principal => + #{ipaddress => <<"127.0.0.1">>}, + topics => + [#{eq => <<"#">>}, + #{eq => <<"+">>} ] , - <<"action">> => all, - <<"permission">> => allow} + action => all, + permission => allow} ). --define(RULE3,#{<<"principal">> => - #{<<"and">> => [#{<<"username">> => "^test?"}, - #{<<"clientid">> => "^test?"} +-define(RULE3,#{principal => + #{'and' => [#{username => "^test?"}, + #{clientid => "^test?"} ]}, - <<"topics">> => [<<"test">>], - <<"action">> => publish, - <<"permission">> => allow} + topics => [<<"test">>], + action => publish, + permission => allow} ). --define(RULE4,#{<<"principal">> => - #{<<"or">> => [#{<<"username">> => <<"^test">>}, - #{<<"clientid">> => <<"test?">>} - ]}, - <<"topics">> => [<<"%u">>,<<"%c">>], - <<"action">> => publish, - <<"permission">> => deny} +-define(RULE4,#{principal => + #{'or' => [#{username => <<"^test">>}, + #{clientid => <<"test?">>} + ]}, + topics => [<<"%u">>,<<"%c">>], + action => publish, + permission => deny} ). @@ -85,39 +86,39 @@ set_special_configs(_App) -> %% Testcases %%------------------------------------------------------------------------------ t_compile(_) -> - ?assertEqual(#{<<"permission">> => deny, - <<"action">> => all, - <<"principal">> => all, - <<"topics">> => [['#']] + ?assertEqual(#{permission => deny, + action => all, + principal => all, + topics => [['#']] },emqx_authz:compile(?RULE1)), - ?assertEqual(#{<<"permission">> => allow, - <<"action">> => all, - <<"principal">> => - #{<<"ipaddress">> => {{127,0,0,1},{127,0,0,1},32}}, - <<"topics">> => [#{<<"eq">> => ['#']}, - #{<<"eq">> => ['+']}] + ?assertEqual(#{permission => allow, + action => all, + principal => + #{ipaddress => {{127,0,0,1},{127,0,0,1},32}}, + topics => [#{eq => ['#']}, + #{eq => ['+']}] }, emqx_authz:compile(?RULE2)), ?assertMatch( - #{<<"permission">> := allow, - <<"action">> := publish, - <<"principal">> := - #{<<"and">> := [#{<<"username">> := {re_pattern, _, _, _, _}}, - #{<<"clientid">> := {re_pattern, _, _, _, _}} - ] + #{permission := allow, + action := publish, + principal := + #{'and' := [#{username := {re_pattern, _, _, _, _}}, + #{clientid := {re_pattern, _, _, _, _}} + ] }, - <<"topics">> := [[<<"test">>]] + topics := [[<<"test">>]] }, emqx_authz:compile(?RULE3)), ?assertMatch( - #{<<"permission">> := deny, - <<"action">> := publish, - <<"principal">> := - #{<<"or">> := [#{<<"username">> := {re_pattern, _, _, _, _}}, - #{<<"clientid">> := {re_pattern, _, _, _, _}} - ] + #{permission := deny, + action := publish, + principal := + #{'or' := [#{username := {re_pattern, _, _, _, _}}, + #{clientid := {re_pattern, _, _, _, _}} + ] }, - <<"topics">> := [#{<<"pattern">> := [<<"%u">>]}, - #{<<"pattern">> := [<<"%c">>]} - ] + topics := [#{pattern := [<<"%u">>]}, + #{pattern := [<<"%c">>]} + ] }, emqx_authz:compile(?RULE4)), ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 4871e0d7f..59dac3d71 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -57,11 +57,10 @@ set_special_configs(emqx) -> set_special_configs(emqx_authz) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => #{<<"rules">> => []}}, + Conf = #{<<"emqx_authz">> => #{<<"rules">> => []}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - + % emqx_config:put([emqx_authz], #{rules => []}), ok; - set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 6ee229ec7..039d50383 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -49,7 +49,7 @@ set_special_configs(emqx) -> set_special_configs(emqx_authz) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => + Conf = #{<<"emqx_authz">> => #{<<"rules">> => [#{<<"config">> =>#{<<"meck">> => <<"fake">>}, <<"principal">> => all, @@ -57,6 +57,12 @@ set_special_configs(emqx_authz) -> <<"type">> => mysql} ]}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + % Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + % principal => all, + % sql => <<"fake sql">>, + % type => mysql} + % ], + % emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 8fb9cd3e0..9dcdd7c18 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -49,7 +49,7 @@ set_special_configs(emqx) -> set_special_configs(emqx_authz) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => + Conf = #{<<"emqx_authz">> => #{<<"rules">> => [#{<<"config">> =>#{<<"meck">> => <<"fake">>}, <<"principal">> => all, @@ -57,6 +57,12 @@ set_special_configs(emqx_authz) -> <<"type">> => pgsql} ]}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + % Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + % principal => all, + % sql => <<"fake sql">>, + % type => pgsql} + % ], + % emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index a4045a9e4..8cb7a51b9 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -49,7 +49,7 @@ set_special_configs(emqx) -> set_special_configs(emqx_authz) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => + Conf = #{<<"emqx_authz">> => #{<<"rules">> => [#{<<"config">> =>#{ <<"server">> => <<"127.0.0.1:6379">>, @@ -63,6 +63,12 @@ set_special_configs(emqx_authz) -> <<"type">> => redis} ]}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), + % Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + % principal => all, + % cmd => <<"fake cmd">>, + % type => redis} + % ], + % emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. From c094e5ddcce1f4c474f06b7d0a9892d3f7abdcef Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 1 Jul 2021 18:29:53 +0800 Subject: [PATCH 059/379] chore(ct): cancel needless apps ct --- scripts/find-apps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/find-apps.sh b/scripts/find-apps.sh index 47b71c7ca..54467b62b 100755 --- a/scripts/find-apps.sh +++ b/scripts/find-apps.sh @@ -7,7 +7,7 @@ cd -P -- "$(dirname -- "$0")/.." find_app() { local appdir="$1" - find "${appdir}" -mindepth 1 -maxdepth 1 -type d + find "${appdir}" -mindepth 1 -maxdepth 1 -type d | grep -vE "emqx_exhook|emqx_exproto|emqx_lwm2m|emqx_sn|emqx_coap|emqx_stomp" } find_app 'apps' From c7e540f4f10ca99e53e0287f53ebe9674adfb273 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Thu, 1 Jul 2021 20:08:13 +0800 Subject: [PATCH 060/379] feat(modules): Update the configuration file to hocon (#5151) --- apps/emqx_modules/etc/emqx_modules.conf | 32 ++++ apps/emqx_modules/priv/emqx_modules.schema | 1 - .../src/emqx_mod_api_topic_metrics.erl | 6 +- apps/emqx_modules/src/emqx_mod_presence.erl | 2 +- apps/emqx_modules/src/emqx_mod_rewrite.erl | 27 ++-- .../src/emqx_mod_subscription.erl | 65 -------- apps/emqx_modules/src/emqx_mod_sup.erl | 1 - apps/emqx_modules/src/emqx_modules.app.src | 2 +- apps/emqx_modules/src/emqx_modules.appup.src | 23 --- apps/emqx_modules/src/emqx_modules.erl | 142 ++++++------------ apps/emqx_modules/src/emqx_modules_api.erl | 31 ++-- apps/emqx_modules/src/emqx_modules_app.erl | 3 - apps/emqx_modules/src/emqx_modules_schema.erl | 61 ++++++++ .../test/emqx_mod_delayed_SUITE.erl | 9 +- .../test/emqx_mod_presence_SUITE.erl | 4 +- .../test/emqx_mod_rewrite_SUITE.erl | 13 +- .../test/emqx_mod_subscription_SUITE.erl | 92 ------------ apps/emqx_modules/test/emqx_mod_sup_SUITE.erl | 49 ------ apps/emqx_modules/test/emqx_modules_SUITE.erl | 48 +++--- apps/emqx_telemetry/src/emqx_telemetry.erl | 7 +- data/loaded_modules.tmpl | 2 - data/loaded_plugins.tmpl | 1 - rebar.config.erl | 8 +- 23 files changed, 217 insertions(+), 412 deletions(-) delete mode 100644 apps/emqx_modules/priv/emqx_modules.schema delete mode 100644 apps/emqx_modules/src/emqx_mod_subscription.erl delete mode 100644 apps/emqx_modules/src/emqx_modules.appup.src create mode 100644 apps/emqx_modules/src/emqx_modules_schema.erl delete mode 100644 apps/emqx_modules/test/emqx_mod_subscription_SUITE.erl delete mode 100644 apps/emqx_modules/test/emqx_mod_sup_SUITE.erl delete mode 100644 data/loaded_modules.tmpl diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index 1bb8bf6d7..3f4681ec9 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -1 +1,33 @@ # empty +emqx_modules: { + modules:[ + { + type: delayed + enable: false + }, + { + type: presence + enable: true + qos: 1 + }, + { + type: recon + enable: true + }, + { + type: rewrite + enable: false + rules:[{ + action: publish + source_topic: "x/#" + re: "^x/y/(.+)$" + dest_topic: "z/y/$1" + }] + }, + { + type: topic_metrics + enable: false + topics: ["topic/#"] + } + ] +} diff --git a/apps/emqx_modules/priv/emqx_modules.schema b/apps/emqx_modules/priv/emqx_modules.schema deleted file mode 100644 index d7c52c644..000000000 --- a/apps/emqx_modules/priv/emqx_modules.schema +++ /dev/null @@ -1 +0,0 @@ -% empty diff --git a/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl b/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl index 5ccef4c6b..d78b3f18a 100644 --- a/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl +++ b/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl @@ -116,11 +116,7 @@ unregister(#{topic := Topic0}, _Params) -> end). execute_when_enabled(Fun) -> - Enabled = case emqx_modules:find_module(emqx_mod_topic_metrics) of - [{_, false}] -> false; - [{_, true}] -> true - end, - case Enabled of + case emqx_modules:find_module(topic_metrics) of true -> Fun(); false -> diff --git a/apps/emqx_modules/src/emqx_mod_presence.erl b/apps/emqx_modules/src/emqx_mod_presence.erl index 7ba147c9a..738d1c892 100644 --- a/apps/emqx_modules/src/emqx_mod_presence.erl +++ b/apps/emqx_modules/src/emqx_mod_presence.erl @@ -117,7 +117,7 @@ topic(connected, ClientId) -> topic(disconnected, ClientId) -> emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/disconnected"])). -qos(Env) -> proplists:get_value(qos, Env, 0). +qos(Env) -> maps:get(qos, Env, 0). -compile({inline, [reason/1]}). reason(Reason) when is_atom(Reason) -> Reason; diff --git a/apps/emqx_modules/src/emqx_mod_rewrite.erl b/apps/emqx_modules/src/emqx_mod_rewrite.erl index c3a550692..e1ee6e1b6 100644 --- a/apps/emqx_modules/src/emqx_mod_rewrite.erl +++ b/apps/emqx_modules/src/emqx_mod_rewrite.erl @@ -43,8 +43,8 @@ %% Load/Unload %%-------------------------------------------------------------------- -load(RawRules) -> - {PubRules, SubRules} = compile(RawRules), +load(Env) -> + {PubRules, SubRules} = compile(maps:get(rules, Env, [])), emqx_hooks:put('client.subscribe', {?MODULE, rewrite_subscribe, [SubRules]}), emqx_hooks:put('client.unsubscribe', {?MODULE, rewrite_unsubscribe, [SubRules]}), emqx_hooks:put('message.publish', {?MODULE, rewrite_publish, [PubRules]}). @@ -70,20 +70,23 @@ description() -> %%-------------------------------------------------------------------- compile(Rules) -> - PubRules = [ begin - {ok, MP} = re:compile(Re), - {rewrite, Topic, MP, Dest} - end || {rewrite, pub, Topic, Re, Dest}<- Rules ], - SubRules = [ begin - {ok, MP} = re:compile(Re), - {rewrite, Topic, MP, Dest} - end || {rewrite, sub, Topic, Re, Dest}<- Rules ], - {PubRules, SubRules}. + lists:foldl(fun(#{source_topic := Topic, + re := Re, + dest_topic := Dest, + action := Action}, {Acc1, Acc2}) -> + {ok, MP} = re:compile(Re), + case Action of + publish -> + {[{Topic, MP, Dest} | Acc1], Acc2}; + subscribe -> + {Acc1, [{Topic, MP, Dest} | Acc2]} + end + end, {[], []}, Rules). match_and_rewrite(Topic, []) -> Topic; -match_and_rewrite(Topic, [{rewrite, Filter, MP, Dest} | Rules]) -> +match_and_rewrite(Topic, [{Filter, MP, Dest} | Rules]) -> case emqx_topic:match(Topic, Filter) of true -> rewrite(Topic, MP, Dest); false -> match_and_rewrite(Topic, Rules) diff --git a/apps/emqx_modules/src/emqx_mod_subscription.erl b/apps/emqx_modules/src/emqx_mod_subscription.erl deleted file mode 100644 index 06178aee7..000000000 --- a/apps/emqx_modules/src/emqx_mod_subscription.erl +++ /dev/null @@ -1,65 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_mod_subscription). - --behaviour(emqx_gen_mod). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - -%% emqx_gen_mod callbacks --export([ load/1 - , unload/1 - , description/0 - ]). - -%% APIs --export([on_client_connected/3]). - -%%-------------------------------------------------------------------- -%% Load/Unload Hook -%%-------------------------------------------------------------------- - -load(Topics) -> - emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Topics]}). - -on_client_connected(#{clientid := ClientId, username := Username}, _ConnInfo = #{proto_ver := ProtoVer}, Topics) -> - Replace = fun(Topic) -> - rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) - end, - TopicFilters = case ProtoVer of - ?MQTT_PROTO_V5 -> [{Replace(Topic), SubOpts} || {Topic, SubOpts} <- Topics]; - _ -> [{Replace(Topic), #{qos => Qos}} || {Topic, #{qos := Qos}} <- Topics] - end, - self() ! {subscribe, TopicFilters}. - -unload(_) -> - emqx_hooks:del('client.connected', {?MODULE, on_client_connected}). - -description() -> - "EMQ X Subscription Module". -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -rep(<<"%c">>, ClientId, Topic) -> - emqx_topic:feed_var(<<"%c">>, ClientId, Topic); -rep(<<"%u">>, undefined, Topic) -> - Topic; -rep(<<"%u">>, Username, Topic) -> - emqx_topic:feed_var(<<"%u">>, Username, Topic). - diff --git a/apps/emqx_modules/src/emqx_mod_sup.erl b/apps/emqx_modules/src/emqx_mod_sup.erl index 755e52a60..c47d47a10 100644 --- a/apps/emqx_modules/src/emqx_mod_sup.erl +++ b/apps/emqx_modules/src/emqx_mod_sup.erl @@ -60,7 +60,6 @@ stop_child(ChildId) -> %%-------------------------------------------------------------------- init([]) -> - ok = emqx_tables:new(emqx_modules, [set, public, {write_concurrency, true}]), {ok, {{one_for_one, 10, 100}, []}}. %%-------------------------------------------------------------------- diff --git a/apps/emqx_modules/src/emqx_modules.app.src b/apps/emqx_modules/src/emqx_modules.app.src index 702652fc2..5d251abc9 100644 --- a/apps/emqx_modules/src/emqx_modules.app.src +++ b/apps/emqx_modules/src/emqx_modules.app.src @@ -1,6 +1,6 @@ {application, emqx_modules, [{description, "EMQ X Module Management"}, - {vsn, "4.3.2"}, + {vsn, "5.0.0"}, {modules, []}, {applications, [kernel,stdlib]}, {mod, {emqx_modules_app, []}}, diff --git a/apps/emqx_modules/src/emqx_modules.appup.src b/apps/emqx_modules/src/emqx_modules.appup.src deleted file mode 100644 index aa997c453..000000000 --- a/apps/emqx_modules/src/emqx_modules.appup.src +++ /dev/null @@ -1,23 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.1", [ - {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} - ]}, - {"4.3.0", [ - {update, emqx_mod_delayed, {advanced, []}}, - {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.1", [ - {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} - ]}, - {"4.3.0", [ - {update, emqx_mod_delayed, {advanced, []}}, - {load_module, emqx_mod_api_topic_metrics, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_modules/src/emqx_modules.erl b/apps/emqx_modules/src/emqx_modules.erl index 262e700ab..6610a4716 100644 --- a/apps/emqx_modules/src/emqx_modules.erl +++ b/apps/emqx_modules/src/emqx_modules.erl @@ -22,12 +22,11 @@ -export([ list/0 , load/0 - , load/1 + , load/2 , unload/0 , unload/1 , reload/1 , find_module/1 - , load_module/2 ]). -export([cli/1]). @@ -35,48 +34,51 @@ %% @doc List all available plugins -spec(list() -> [{atom(), boolean()}]). list() -> - ets:tab2list(?MODULE). + persistent_term:get(?MODULE, []). %% @doc Load all the extended modules. -spec(load() -> ok). load() -> - case emqx:get_env(modules_loaded_file) of - undefined -> ok; - File -> - load_modules(File) - end. + Modules = emqx_config:get([emqx_modules, modules], []), + lists:foreach(fun(#{type := Module, enable := Enable} = Config) -> + case Enable of + true -> + load(name(Module), maps:without([type, enable], Config)); + false -> + ok + end + end, Modules). -load(ModuleName) -> +load(Module, Env) -> + ModuleName = name(Module), case find_module(ModuleName) of - [] -> - ?LOG(alert, "Module ~s not found, cannot load it", [ModuleName]), - {error, not_found}; - [{ModuleName, true}] -> + false -> + load_mod(ModuleName, Env); + true -> ?LOG(notice, "Module ~s is already started", [ModuleName]), - {error, already_started}; - [{ModuleName, false}] -> - emqx_modules:load_module(ModuleName, true) + {error, already_started} end. %% @doc Unload all the extended modules. -spec(unload() -> ok). unload() -> - case emqx:get_env(modules_loaded_file) of - undefined -> ignore; - File -> - unload_modules(File) - end. + Modules = emqx_config:get([emqx_modules, modules], []), + lists:foreach(fun(#{type := Module, enable := Enable}) -> + case Enable of + true -> + unload_mod(name(Module)); + false -> + ok + end + end, Modules). unload(ModuleName) -> case find_module(ModuleName) of - [] -> + false -> ?LOG(alert, "Module ~s not found, cannot load it", [ModuleName]), - {error, not_found}; - [{ModuleName, false}] -> - ?LOG(error, "Module ~s is not started", [ModuleName]), {error, not_started}; - [{ModuleName, true}] -> - unload_module(ModuleName, true) + true -> + unload_mod(ModuleName) end. -spec(reload(module()) -> ok | ignore | {error, any()}). @@ -84,94 +86,39 @@ reload(_) -> ignore. find_module(ModuleName) -> - ets:lookup(?MODULE, ModuleName). + lists:member(ModuleName, persistent_term:get(?MODULE, [])). -filter_module(ModuleNames) -> - filter_module(ModuleNames, emqx:get_env(modules, [])). -filter_module([], Acc) -> - Acc; -filter_module([{ModuleName, true} | ModuleNames], Acc) -> - filter_module(ModuleNames, lists:keydelete(ModuleName, 1, Acc)); -filter_module([{_, false} | ModuleNames], Acc) -> - filter_module(ModuleNames, Acc). - -load_modules(File) -> - case file:consult(File) of - {ok, ModuleNames} -> - lists:foreach(fun({ModuleName, _}) -> - ets:insert(?MODULE, {ModuleName, false}) - end, filter_module(ModuleNames)), - lists:foreach(fun load_module/1, ModuleNames); - {error, Error} -> - ?LOG(alert, "Failed to read: ~p, error: ~p", [File, Error]) - end. - -load_module({ModuleName, true}) -> - emqx_modules:load_module(ModuleName, false); -load_module({ModuleName, false}) -> - ets:insert(?MODULE, {ModuleName, false}); -load_module(ModuleName) -> - load_module({ModuleName, true}). - -load_module(ModuleName, Persistent) -> - Modules = emqx:get_env(modules, []), - Env = proplists:get_value(ModuleName, Modules, undefined), +load_mod(ModuleName, Env) -> case ModuleName:load(Env) of ok -> - ets:insert(?MODULE, {ModuleName, true}), - ok = write_loaded(Persistent), + Modules = persistent_term:get(?MODULE, []), + persistent_term:put(?MODULE, [ModuleName| Modules]), ?LOG(info, "Load ~s module successfully.", [ModuleName]); {error, Error} -> ?LOG(error, "Load module ~s failed, cannot load for ~0p", [ModuleName, Error]), {error, Error} end. -unload_modules(File) -> - case file:consult(File) of - {ok, ModuleNames} -> - lists:foreach(fun unload_module/1, ModuleNames); - {error, Error} -> - ?LOG(alert, "Failed to read: ~p, error: ~p", [File, Error]) - end. -unload_module({ModuleName, true}) -> - unload_module(ModuleName, false); -unload_module({ModuleName, false}) -> - ets:insert(?MODULE, {ModuleName, false}); -unload_module(ModuleName) -> - unload_module({ModuleName, true}). - -unload_module(ModuleName, Persistent) -> - Modules = emqx:get_env(modules, []), - Env = proplists:get_value(ModuleName, Modules, undefined), - case ModuleName:unload(Env) of +unload_mod(ModuleName) -> + case ModuleName:unload(#{}) of ok -> - ets:insert(?MODULE, {ModuleName, false}), - ok = write_loaded(Persistent), + Modules = persistent_term:get(?MODULE, []), + persistent_term:put(?MODULE, Modules -- [ModuleName]), ?LOG(info, "Unload ~s module successfully.", [ModuleName]); {error, Error} -> ?LOG(error, "Unload module ~s failed, cannot unload for ~0p", [ModuleName, Error]) end. -write_loaded(true) -> - FilePath = emqx:get_env(modules_loaded_file), - case file:write_file(FilePath, [io_lib:format("~p.~n", [Name]) || Name <- list()]) of - ok -> ok; - {error, Error} -> - ?LOG(error, "Write File ~p Error: ~p", [FilePath, Error]), - ok - end; -write_loaded(false) -> ok. - %%-------------------------------------------------------------------- %% @doc Modules Command cli(["list"]) -> - lists:foreach(fun({Name, Active}) -> - emqx_ctl:print("Module(~s, description=~s, active=~s)~n", - [Name, Name:description(), Active]) + lists:foreach(fun(Name) -> + emqx_ctl:print("Module(~s, description=~s)~n", + [Name, Name:description()]) end, emqx_modules:list()); cli(["load", Name]) -> - case emqx_modules:load(list_to_atom(Name)) of + case emqx_modules:load(list_to_atom(Name), #{}) of ok -> emqx_ctl:print("Module ~s loaded successfully.~n", [Name]); {error, Reason} -> @@ -195,3 +142,10 @@ cli(_) -> {"modules unload ", "Unload module"}, {"modules reload ", "Reload module"} ]). + +name(delayed) -> emqx_mod_delayed; +name(presence) -> emqx_mod_presence; +name(recon) -> emqx_mod_recon; +name(rewrite) -> emqx_mod_rewrite; +name(topic_metrics) -> emqx_mod_topic_metrics; +name(Name) -> Name. diff --git a/apps/emqx_modules/src/emqx_modules_api.erl b/apps/emqx_modules/src/emqx_modules_api.erl index 3490c116c..3a4b05fd0 100644 --- a/apps/emqx_modules/src/emqx_modules_api.erl +++ b/apps/emqx_modules/src/emqx_modules_api.erl @@ -73,7 +73,7 @@ , reload/2 ]). --export([ do_load_module/2 +-export([ do_load_module/3 , do_unload_module/2 ]). @@ -83,11 +83,11 @@ list(#{node := Node}, _Params) -> list(_Bindings, _Params) -> return({ok, [format(Node, Modules) || {Node, Modules} <- list_modules()]}). -load(#{node := Node, module := Module}, _Params) -> - return(do_load_module(Node, Module)); +load(#{node := Node, module := Module}, Params) -> + return(do_load_module(Node, Module, Params)); -load(#{module := Module}, _Params) -> - Results = [do_load_module(Node, Module) || Node <- ekka_mnesia:running_nodes()], +load(#{module := Module}, Params) -> + Results = [do_load_module(Node, Module, Params) || Node <- ekka_mnesia:running_nodes()], case lists:filter(fun(Item) -> Item =/= ok end, Results) of [] -> return(ok); @@ -129,10 +129,9 @@ reload(#{module := Module}, _Params) -> format(Node, Modules) -> #{node => Node, modules => [format(Module) || Module <- Modules]}. -format({Name, Active}) -> - #{name => Name, - description => iolist_to_binary(Name:description()), - active => Active}. +format(Name) -> + #{name => name(Name), + description => iolist_to_binary(Name:description())}. list_modules() -> [{Node, list_modules(Node)} || Node <- ekka_mnesia:running_nodes()]. @@ -142,10 +141,10 @@ list_modules(Node) when Node =:= node() -> list_modules(Node) -> rpc_call(Node, list_modules, [Node]). -do_load_module(Node, Module) when Node =:= node() -> - emqx_modules:load(Module); -do_load_module(Node, Module) -> - rpc_call(Node, do_load_module, [Node, Module]). +do_load_module(Node, Module, Env) when Node =:= node() -> + emqx_modules:load(Module, Env); +do_load_module(Node, Module, Env) -> + rpc_call(Node, do_load_module, [Node, Module, Env]). do_unload_module(Node, Module) when Node =:= node() -> emqx_modules:unload(Module); @@ -162,3 +161,9 @@ rpc_call(Node, Fun, Args) -> {badrpc, Reason} -> {error, Reason}; Res -> Res end. + +name(emqx_mod_delayed) -> delayed; +name(emqx_mod_presence) -> presence; +name(emqx_mod_recon) -> recon; +name(emqx_mod_rewrite) -> rewrite; +name(emqx_mod_topic_metrics) -> topic_metrics. diff --git a/apps/emqx_modules/src/emqx_modules_app.erl b/apps/emqx_modules/src/emqx_modules_app.erl index a10176829..33f18459e 100644 --- a/apps/emqx_modules/src/emqx_modules_app.erl +++ b/apps/emqx_modules/src/emqx_modules_app.erl @@ -23,9 +23,6 @@ -export([stop/1]). start(_Type, _Args) -> - % the configs for emqx_modules is so far still in emqx application - % Ensure it's loaded - _ = application:load(emqx), {ok, Pid} = emqx_mod_sup:start_link(), ok = emqx_modules:load(), emqx_ctl:register_command(modules, {emqx_modules, cli}, []), diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl new file mode 100644 index 000000000..e7b26b7de --- /dev/null +++ b/apps/emqx_modules/src/emqx_modules_schema.erl @@ -0,0 +1,61 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_modules_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1]). + +structs() -> ["emqx_modules"]. + +fields("emqx_modules") -> + [{modules, hoconsc:array(hoconsc:union([ hoconsc:ref(?MODULE, "common") + , hoconsc:ref(?MODULE, "presence") + , hoconsc:ref(?MODULE, "rewrite") + , hoconsc:ref(?MODULE, "topic_metrics") + ]))}]; +fields("common") -> + [ {type, hoconsc:enum([delayed, recon])} + , {enable, emqx_schema:t(boolean(), undefined, false)} + ]; + +fields("presence") -> + [ {type, hoconsc:enum([presence])} + , {enable, emqx_schema:t(boolean(), undefined, false)} + , {qos, emqx_schema:t(integer(), undefined, 1)} + ]; +fields("rewrite") -> + [ {type, hoconsc:enum([rewrite])} + , {enable, emqx_schema:t(boolean(), undefined, false)} + , {rules, hoconsc:array(hoconsc:ref(?MODULE, "rules"))} + ]; + +fields("topic_metrics") -> + [ {type, hoconsc:enum([topic_metrics])} + , {enable, emqx_schema:t(boolean(), undefined, false)} + , {topics, hoconsc:array(binary())} + ]; + +fields("rules") -> + [ {action, hoconsc:enum([publish, subscribe])} + , {source_topic, emqx_schema:t(binary())} + , {re, emqx_schema:t(binary())} + , {dest_topic, emqx_schema:t(binary())} + ]. diff --git a/apps/emqx_modules/test/emqx_mod_delayed_SUITE.erl b/apps/emqx_modules/test/emqx_mod_delayed_SUITE.erl index fcd73bb61..2cd1107f6 100644 --- a/apps/emqx_modules/test/emqx_mod_delayed_SUITE.erl +++ b/apps/emqx_modules/test/emqx_mod_delayed_SUITE.erl @@ -35,19 +35,12 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_modules], fun set_special_configs/1), + emqx_ct_helpers:start_apps([emqx_modules]), Config. end_per_suite(_) -> emqx_ct_helpers:stop_apps([emqx_modules]). -set_special_configs(emqx) -> - application:set_env(emqx, modules, [{emqx_mod_delayed, []}]), - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false); -set_special_configs(_App) -> - ok. - %%-------------------------------------------------------------------- %% Test cases %%-------------------------------------------------------------------- diff --git a/apps/emqx_modules/test/emqx_mod_presence_SUITE.erl b/apps/emqx_modules/test/emqx_mod_presence_SUITE.erl index fafcc3c2f..e02fa6fdd 100644 --- a/apps/emqx_modules/test/emqx_mod_presence_SUITE.erl +++ b/apps/emqx_modules/test/emqx_mod_presence_SUITE.erl @@ -36,7 +36,7 @@ end_per_suite(_Config) -> %% Test case for emqx_mod_presence t_mod_presence(_) -> - ok = emqx_mod_presence:load([{qos, ?QOS_1}]), + ok = emqx_mod_presence:load(#{qos => ?QOS_1}), {ok, C1} = emqtt:start_link([{clientid, <<"monsys">>}]), {ok, _} = emqtt:connect(C1), {ok, _Props, [?QOS_1]} = emqtt:subscribe(C1, <<"$SYS/brokers/+/clients/#">>, qos1), @@ -49,7 +49,7 @@ t_mod_presence(_) -> ok = emqtt:disconnect(C2), ok = recv_and_check_presence(<<"clientid">>, <<"disconnected">>), ok = emqtt:disconnect(C1), - ok = emqx_mod_presence:unload([{qos, ?QOS_1}]). + ok = emqx_mod_presence:unload([]). t_mod_presence_reason(_) -> ?assertEqual(normal, emqx_mod_presence:reason(normal)), diff --git a/apps/emqx_modules/test/emqx_mod_rewrite_SUITE.erl b/apps/emqx_modules/test/emqx_mod_rewrite_SUITE.erl index 997eff1c2..9b94cba8c 100644 --- a/apps/emqx_modules/test/emqx_mod_rewrite_SUITE.erl +++ b/apps/emqx_modules/test/emqx_mod_rewrite_SUITE.erl @@ -22,8 +22,15 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). --define(RULES, [{rewrite, pub, <<"x/#">>,<<"^x/y/(.+)$">>,<<"z/y/$1">>}, - {rewrite, sub, <<"y/+/z/#">>,<<"^y/(.+)/z/(.+)$">>,<<"y/z/$2">>} +-define(RULES, [#{action => publish, + source_topic => <<"x/#">>, + re => <<"^x/y/(.+)$">>, + dest_topic => <<"z/y/$1">> + }, + #{action => subscribe, + source_topic => <<"y/+/z/#">>, + re => <<"^y/(.+)/z/(.+)$">>, + dest_topic => <<"y/z/$2">>} ]). all() -> emqx_ct:all(?MODULE). @@ -40,7 +47,7 @@ end_per_suite(_Config) -> %% Test case for emqx_mod_write t_mod_rewrite(_Config) -> - ok = emqx_mod_rewrite:load(?RULES), + ok = emqx_mod_rewrite:load(#{rules => ?RULES}), {ok, C} = emqtt:start_link([{clientid, <<"rewrite_client">>}]), {ok, _} = emqtt:connect(C), PubOrigTopics = [<<"x/y/2">>, <<"x/1/2">>], diff --git a/apps/emqx_modules/test/emqx_mod_subscription_SUITE.erl b/apps/emqx_modules/test/emqx_mod_subscription_SUITE.erl deleted file mode 100644 index c2905754b..000000000 --- a/apps/emqx_modules/test/emqx_mod_subscription_SUITE.erl +++ /dev/null @@ -1,92 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_mod_subscription_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:boot_modules(all), - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -t_on_client_connected(_) -> - ?assertEqual(ok, emqx_mod_subscription:load([{<<"connected/%c/%u">>, #{qos => ?QOS_0}}])), - {ok, C} = emqtt:start_link([{host, "localhost"}, - {clientid, "myclient"}, - {username, "admin"}]), - {ok, _} = emqtt:connect(C), - emqtt:publish(C, <<"connected/myclient/admin">>, <<"Hello world">>, ?QOS_0), - {ok, #{topic := Topic, payload := Payload}} = receive_publish(100), - ?assertEqual(<<"connected/myclient/admin">>, Topic), - ?assertEqual(<<"Hello world">>, Payload), - ok = emqtt:disconnect(C), - ?assertEqual(ok, emqx_mod_subscription:unload([{<<"connected/%c/%u">>, #{qos => ?QOS_0}}])). - -t_on_undefined_client_connected(_) -> - ?assertEqual(ok, emqx_mod_subscription:load([{<<"connected/undefined">>, #{qos => ?QOS_1}}])), - {ok, C} = emqtt:start_link([{host, "localhost"}]), - {ok, _} = emqtt:connect(C), - emqtt:publish(C, <<"connected/undefined">>, <<"Hello world">>, ?QOS_1), - {ok, #{topic := Topic, payload := Payload}} = receive_publish(100), - ?assertEqual(<<"connected/undefined">>, Topic), - ?assertEqual(<<"Hello world">>, Payload), - ok = emqtt:disconnect(C), - ?assertEqual(ok, emqx_mod_subscription:unload([{<<"connected/undefined">>, #{qos => ?QOS_1}}])). - -t_suboption(_) -> - Client_info = fun(Key, Client) -> maps:get(Key, maps:from_list(emqtt:info(Client)), undefined) end, - Suboption = #{qos => ?QOS_2, nl => 1, rap => 1, rh => 2}, - ?assertEqual(ok, emqx_mod_subscription:load([{<<"connected/%c/%u">>, Suboption}])), - {ok, C1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(C1), - timer:sleep(200), - [CPid1] = emqx_cm:lookup_channels(Client_info(clientid, C1)), - [ Sub1 | _ ] = ets:lookup(emqx_subscription,CPid1), - [ Suboption1 | _ ] = ets:lookup(emqx_suboption,Sub1), - ?assertMatch({Sub1, #{qos := 2, nl := 1, rap := 1, rh := 2, subid := _}}, Suboption1), - ok = emqtt:disconnect(C1), - %% The subscription option is not valid for MQTT V3.1.1 - {ok, C2} = emqtt:start_link([{proto_ver, v4}]), - {ok, _} = emqtt:connect(C2), - timer:sleep(200), - [CPid2] = emqx_cm:lookup_channels(Client_info(clientid, C2)), - [ Sub2 | _ ] = ets:lookup(emqx_subscription,CPid2), - [ Suboption2 | _ ] = ets:lookup(emqx_suboption,Sub2), - ok = emqtt:disconnect(C2), - ?assertMatch({Sub2, #{qos := 2, nl := 0, rap := 0, rh := 0, subid := _}}, Suboption2), - - ?assertEqual(ok, emqx_mod_subscription:unload([{<<"connected/undefined">>, Suboption}])). - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -receive_publish(Timeout) -> - receive - {publish, Publish} -> {ok, Publish} - after - Timeout -> {error, timeout} - end. diff --git a/apps/emqx_modules/test/emqx_mod_sup_SUITE.erl b/apps/emqx_modules/test/emqx_mod_sup_SUITE.erl deleted file mode 100644 index 59d0ffde2..000000000 --- a/apps/emqx_modules/test/emqx_mod_sup_SUITE.erl +++ /dev/null @@ -1,49 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_mod_sup_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). - -all() -> emqx_ct:all(?MODULE). - -%%-------------------------------------------------------------------- -%% Test cases -%%-------------------------------------------------------------------- - -t_start(_) -> - ?assertEqual([], supervisor:which_children(emqx_mod_sup)). - -t_start_child(_) -> - %% Set the emqx_mod_sup child with emqx_hooks for test - Mod = emqx_hooks, - Spec = #{id => Mod, - start => {Mod, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [Mod]}, - - ok = emqx_mod_sup:start_child(Mod, worker), - ?assertError({already_started, _}, emqx_mod_sup:start_child(Spec)), - - ok = emqx_mod_sup:stop_child(Mod), - {error, not_found} = emqx_mod_sup:stop_child(Mod), - ok. - diff --git a/apps/emqx_modules/test/emqx_modules_SUITE.erl b/apps/emqx_modules/test/emqx_modules_SUITE.erl index 0ce8b0c5f..e8e0b3218 100644 --- a/apps/emqx_modules/test/emqx_modules_SUITE.erl +++ b/apps/emqx_modules/test/emqx_modules_SUITE.erl @@ -37,7 +37,6 @@ init_per_suite(Config) -> Config. set_special_cfg(_) -> - application:set_env(emqx, modules_loaded_file, emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_modules")), ok. end_per_suite(_Config) -> @@ -47,70 +46,67 @@ end_per_suite(_Config) -> t_load(_) -> ?assertEqual(ok, emqx_modules:unload()), ?assertEqual(ok, emqx_modules:load()), - ?assertEqual({error, not_found}, emqx_modules:load(not_existed_module)), - ?assertEqual({error, not_started}, emqx_modules:unload(emqx_mod_rewrite)), - ?assertEqual(ignore, emqx_modules:reload(emqx_mod_rewrite)). + ?assertEqual({error, not_started}, emqx_modules:unload(rewrite)), + ?assertEqual(ignore, emqx_modules:reload(rewrite)). t_list(_) -> - ?assertMatch([{_, _} | _ ], emqx_modules:list()). + emqx_modules:load(presence, #{qos => 1}), + ?assertMatch([_ | _ ], emqx_modules:list()), + emqx_modules:unload(presence). t_modules_api(_) -> - emqx_modules:load_module(emqx_mod_presence, false), + emqx_modules:load(presence, #{qos => 1}), timer:sleep(50), {ok, Modules1} = request_api(get, api_path(["modules"]), auth_header_()), [Modules11] = filter(get(<<"data">>, Modules1), <<"node">>, atom_to_binary(node(), utf8)), - [Module1] = filter(maps:get(<<"modules">>, Modules11), <<"name">>, <<"emqx_mod_presence">>), - ?assertEqual(<<"emqx_mod_presence">>, maps:get(<<"name">>, Module1)), - ?assertEqual(true, maps:get(<<"active">>, Module1)), - + [Module1] = filter(maps:get(<<"modules">>, Modules11), <<"name">>, <<"presence">>), + ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module1)), {ok, _} = request_api(put, api_path(["modules", - atom_to_list(emqx_mod_presence), + atom_to_list(presence), "unload"]), auth_header_()), {ok, Error1} = request_api(put, api_path(["modules", - atom_to_list(emqx_mod_presence), + atom_to_list(presence), "unload"]), auth_header_()), ?assertEqual(<<"not_started">>, get(<<"message">>, Error1)), {ok, Modules2} = request_api(get, api_path(["nodes", atom_to_list(node()), "modules"]), auth_header_()), - [Module2] = filter(get(<<"data">>, Modules2), <<"name">>, <<"emqx_mod_presence">>), - ?assertEqual(<<"emqx_mod_presence">>, maps:get(<<"name">>, Module2)), - ?assertEqual(false, maps:get(<<"active">>, Module2)), + [Module2] = filter(get(<<"data">>, Modules2), <<"name">>, <<"presence">>), + ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module2)), {ok, _} = request_api(put, api_path(["nodes", atom_to_list(node()), "modules", - atom_to_list(emqx_mod_presence), + atom_to_list(presence), "load"]), auth_header_()), {ok, Modules3} = request_api(get, api_path(["nodes", atom_to_list(node()), "modules"]), auth_header_()), - [Module3] = filter(get(<<"data">>, Modules3), <<"name">>, <<"emqx_mod_presence">>), - ?assertEqual(<<"emqx_mod_presence">>, maps:get(<<"name">>, Module3)), - ?assertEqual(true, maps:get(<<"active">>, Module3)), + [Module3] = filter(get(<<"data">>, Modules3), <<"name">>, <<"presence">>), + ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module3)), {ok, _} = request_api(put, api_path(["nodes", atom_to_list(node()), "modules", - atom_to_list(emqx_mod_presence), + atom_to_list(presence), "unload"]), auth_header_()), {ok, Error2} = request_api(put, api_path(["nodes", atom_to_list(node()), "modules", - atom_to_list(emqx_mod_presence), + atom_to_list(presence), "unload"]), auth_header_()), ?assertEqual(<<"not_started">>, get(<<"message">>, Error2)), - emqx_modules:unload(emqx_mod_presence). + emqx_modules:unload(presence). t_modules_cmd(_) -> @@ -120,10 +116,10 @@ t_modules_cmd(_) -> meck:expect(emqx_modules, unload, fun(_) -> ok end), meck:expect(emqx_modules, reload, fun(_) -> ok end), ?assertEqual(emqx_modules:cli(["list"]), ok), - ?assertEqual(emqx_modules:cli(["load", "emqx_mod_presence"]), - "Module emqx_mod_presence loaded successfully.\n"), - ?assertEqual(emqx_modules:cli(["unload", "emqx_mod_presence"]), - "Module emqx_mod_presence unloaded successfully.\n"), + ?assertEqual(emqx_modules:cli(["load", "delayed"]), + "Module delayed loaded successfully.\n"), + ?assertEqual(emqx_modules:cli(["unload", "delayed"]), + "Module delayed unloaded successfully.\n"), unmock_print(). %% For: https://github.com/emqx/emqx/issues/4511 diff --git a/apps/emqx_telemetry/src/emqx_telemetry.erl b/apps/emqx_telemetry/src/emqx_telemetry.erl index dd6e7aa4c..9bf616b0a 100644 --- a/apps/emqx_telemetry/src/emqx_telemetry.erl +++ b/apps/emqx_telemetry/src/emqx_telemetry.erl @@ -307,12 +307,7 @@ active_plugins() -> end, [], emqx_plugins:list()). active_modules() -> - lists:foldl(fun({Name, Persistent}, Acc) -> - case Persistent of - true -> [Name | Acc]; - false -> Acc - end - end, [], emqx_modules:list()). + emqx_modules:list(). num_clients() -> emqx_stats:getstat('connections.max'). diff --git a/data/loaded_modules.tmpl b/data/loaded_modules.tmpl deleted file mode 100644 index 8dfe6453e..000000000 --- a/data/loaded_modules.tmpl +++ /dev/null @@ -1,2 +0,0 @@ -{emqx_mod_presence, true}. -{emqx_mod_recon, true}. diff --git a/data/loaded_plugins.tmpl b/data/loaded_plugins.tmpl index 80a0c832c..dc23386dc 100644 --- a/data/loaded_plugins.tmpl +++ b/data/loaded_plugins.tmpl @@ -1,4 +1,3 @@ {emqx_management, true}. {emqx_dashboard, true}. -{emqx_modules, {{enable_plugin_emqx_modules}}}. {emqx_retainer, {{enable_plugin_emqx_retainer}}}. diff --git a/rebar.config.erl b/rebar.config.erl index 5d5d02d05..13bbb1c15 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -192,8 +192,8 @@ overlay_vars_rel(RelType) -> cloud -> "vm.args"; edge -> "vm.args.edge" end, - [ {enable_plugin_emqx_modules, false} %% modules is not a plugin in ce - , {enable_plugin_emqx_retainer, true} + [ + {enable_plugin_emqx_retainer, true} , {vm_args_file, VmArgs} ]. @@ -256,9 +256,9 @@ relx_apps(ReleaseType) -> , emqx_data_bridge , emqx_rule_engine , emqx_bridge_mqtt + , emqx_modules ] ++ [emqx_telemetry || not is_enterprise()] - ++ [emqx_modules || not is_enterprise()] ++ [emqx_license || is_enterprise()] ++ [bcrypt || provide_bcrypt_release(ReleaseType)] ++ relx_apps_per_rel(ReleaseType) @@ -318,7 +318,6 @@ relx_overlay(ReleaseType) -> , {mkdir, "data/patches"} , {mkdir, "data/scripts"} , {template, "data/loaded_plugins.tmpl", "data/loaded_plugins"} - , {template, "data/loaded_modules.tmpl", "data/loaded_modules"} , {template, "data/emqx_vars", "releases/emqx_vars"} , {template, "data/BUILT_ON", "releases/{{release_version}}/BUILT_ON"} , {copy, "bin/emqx", "bin/emqx"} @@ -376,6 +375,7 @@ emqx_etc_overlay_common() -> {"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"}, {"{{base_dir}}/lib/emqx_rule_engine/etc/emqx_rule_engine.conf", "etc/plugins/emqx_rule_engine.conf"}, {"{{base_dir}}/lib/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf", "etc/plugins/emqx_bridge_mqtt.conf"}, + {"{{base_dir}}/lib/emqx_modules/etc/emqx_modules.conf", "etc/plugins/emqx_modules.conf"}, %% TODO: check why it has to end with .paho %% and why it is put to etc/plugins dir {"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}]. From 2c7fd0b547140e810d2e5e3ecca0ff14a54b5ebd Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 2 Jul 2021 00:02:09 +0800 Subject: [PATCH 061/379] chore: mgmt hoconf support (#5153) --- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 12 + .../test/emqx_dashboard_SUITE.erl | 17 +- apps/emqx_management/etc/emqx_management.conf | 91 +++---- .../priv/emqx_management.schema | 239 ------------------ .../src/emqx_management_schema.erl | 62 +++++ apps/emqx_management/src/emqx_mgmt_app.erl | 7 + apps/emqx_management/src/emqx_mgmt_auth.erl | 6 +- apps/emqx_management/src/emqx_mgmt_http.erl | 4 +- apps/emqx_management/test/emqx_mgmt_SUITE.erl | 28 +- .../test/emqx_mgmt_api_SUITE.erl | 16 +- .../test/etc/emqx_management.conf | 72 +++--- apps/emqx_modules/test/emqx_modules_SUITE.erl | 15 +- 12 files changed, 218 insertions(+), 351 deletions(-) delete mode 100644 apps/emqx_management/priv/emqx_management.schema create mode 100644 apps/emqx_management/src/emqx_management_schema.erl diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 59dac3d71..b813ad1f7 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -61,6 +61,18 @@ set_special_configs(emqx_authz) -> ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), % emqx_config:put([emqx_authz], #{rules => []}), ok; + +set_special_configs(emqx_management) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_management, "test")), + Conf = #{<<"emqx_management">> => #{ + <<"listeners">> => [#{ + <<"protocol">> => <<"http">> + }]} + }, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + ok; + set_special_configs(_App) -> ok. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index be77d474b..afec49419 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -54,13 +54,26 @@ groups() -> ]. init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_modules, emqx_management, emqx_dashboard]), + emqx_ct_helpers:start_apps([emqx_management, emqx_dashboard],fun set_special_configs/1), Config. end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_dashboard, emqx_management, emqx_modules]), + emqx_ct_helpers:stop_apps([emqx_dashboard, emqx_management]), ekka_mnesia:ensure_stopped(). +set_special_configs(emqx_management) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_management, "test")), + Conf = #{<<"emqx_management">> => #{ + <<"listeners">> => [#{ + <<"protocol">> => <<"http">> + }]} + }, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_) -> + ok. + t_overview(_) -> [?assert(request_dashboard(get, api_path(erlang:atom_to_list(Overview)), auth_header_()))|| Overview <- ?OVERVIEWS]. diff --git a/apps/emqx_management/etc/emqx_management.conf b/apps/emqx_management/etc/emqx_management.conf index 05a3008fa..62f26474c 100644 --- a/apps/emqx_management/etc/emqx_management.conf +++ b/apps/emqx_management/etc/emqx_management.conf @@ -1,53 +1,38 @@ -##-------------------------------------------------------------------- -## EMQ X Management Plugin -##-------------------------------------------------------------------- - -## Max Row Limit -management.max_row_limit = 10000 - -## Application default secret -## -## Value: String -## management.application.default_secret = public - -## Default Application ID -## -## Value: String -management.default_application.id = admin - -## Default Application Secret -## -## Value: String -management.default_application.secret = public - -##-------------------------------------------------------------------- -## HTTP Listener - -management.listener.http.port = 8081 -management.listener.http.acceptors = 2 -management.listener.http.max_clients = 512 -management.listener.http.backlog = 512 -management.listener.http.send_timeout = 15s -management.listener.http.send_timeout_close = on -management.listener.http.inet6 = false -management.listener.http.ipv6_v6only = false - -##-------------------------------------------------------------------- -## HTTPS Listener - -## management.listener.https.port = 8081 -## management.listener.https.acceptors = 2 -## management.listener.https.max_clients = 512 -## management.listener.https.backlog = 512 -## management.listener.https.send_timeout = 15s -## management.listener.https.send_timeout_close = on -## management.listener.https.certfile = "etc/certs/cert.pem" -## management.listener.https.keyfile = "etc/certs/key.pem" -## management.listener.https.cacertfile = "etc/certs/cacert.pem" -## management.listener.https.verify = verify_peer -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## management.listener.https.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" -## management.listener.https.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## management.listener.https.fail_if_no_peer_cert = true -## management.listener.https.inet6 = false -## management.listener.https.ipv6_v6only = false +emqx_management:{ + default_application_id: "admin" + default_application_secret: "public" + max_row_limit: 10000 + listeners: [ + { + num_acceptors: 4 + max_connections: 512 + protocol: "http" + port: 8081 + backlog: 512 + send_timeout: 15s + send_timeout_close: on + inet6: false + ipv6_v6only: false + } +## , +## { +## protocol: https +## port: 8081 +## acceptors: 2 +## backlog: 512 +## send_timeout: 15s +## send_timeout_close: on +## inet6: false +## ipv6_v6only: false +## certfile = "etc/certs/cert.pem" +## keyfile = "etc/certs/key.pem" +## cacertfile = "etc/certs/cacert.pem" +## verify = verify_peer +## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" +## ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" +## fail_if_no_peer_cert = true +## inet6 = false +## ipv6_v6only = false +## } + ] +} diff --git a/apps/emqx_management/priv/emqx_management.schema b/apps/emqx_management/priv/emqx_management.schema deleted file mode 100644 index 4e887809e..000000000 --- a/apps/emqx_management/priv/emqx_management.schema +++ /dev/null @@ -1,239 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_management config mapping - -{mapping, "management.max_row_limit", "emqx_management.max_row_limit", [ - {default, 10000}, - {datatype, integer} -]}. - -{mapping, "management.default_application.id", "emqx_management.default_application_id", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "management.default_application.secret", "emqx_management.default_application_secret", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "management.application.default_secret", "emqx_management.application", [ - {default, undefined}, - {datatype, string} -]}. - -{mapping, "management.listener.http.port", "emqx_management.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "management.listener.http.acceptors", "emqx_management.listeners", [ - {default, 4}, - {datatype, integer} -]}. - -{mapping, "management.listener.http.max_clients", "emqx_management.listeners", [ - {default, 512}, - {datatype, integer} -]}. - -{mapping, "management.listener.http.backlog", "emqx_management.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "management.listener.http.send_timeout", "emqx_management.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "management.listener.http.send_timeout_close", "emqx_management.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "management.listener.http.recbuf", "emqx_management.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "management.listener.http.sndbuf", "emqx_management.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "management.listener.http.buffer", "emqx_management.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "management.listener.http.tune_buffer", "emqx_management.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "management.listener.http.nodelay", "emqx_management.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "management.listener.http.inet6", "emqx_management.listeners", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "management.listener.http.ipv6_v6only", "emqx_management.listeners", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "management.listener.https.port", "emqx_management.listeners", [ - {datatype, [integer, ip]} -]}. - -{mapping, "management.listener.https.acceptors", "emqx_management.listeners", [ - {default, 8}, - {datatype, integer} -]}. - -{mapping, "management.listener.https.max_clients", "emqx_management.listeners", [ - {default, 64}, - {datatype, integer} -]}. - -{mapping, "management.listener.https.backlog", "emqx_management.listeners", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "management.listener.https.send_timeout", "emqx_management.listeners", [ - {datatype, {duration, ms}}, - {default, "15s"} -]}. - -{mapping, "management.listener.https.send_timeout_close", "emqx_management.listeners", [ - {datatype, flag}, - {default, on} -]}. - -{mapping, "management.listener.https.recbuf", "emqx_management.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "management.listener.https.sndbuf", "emqx_management.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "management.listener.https.buffer", "emqx_management.listeners", [ - {datatype, bytesize}, - hidden -]}. - -{mapping, "management.listener.https.tune_buffer", "emqx_management.listeners", [ - {datatype, flag}, - hidden -]}. - -{mapping, "management.listener.https.nodelay", "emqx_management.listeners", [ - {datatype, {enum, [true, false]}}, - hidden -]}. - -{mapping, "management.listener.https.keyfile", "emqx_management.listeners", [ - {datatype, string} -]}. - -{mapping, "management.listener.https.certfile", "emqx_management.listeners", [ - {datatype, string} -]}. - -{mapping, "management.listener.https.cacertfile", "emqx_management.listeners", [ - {datatype, string} -]}. - -{mapping, "management.listener.https.verify", "emqx_management.listeners", [ - {datatype, atom} -]}. - -{mapping, "management.listener.https.ciphers", "emqx_management.listeners", [ - {datatype, string} -]}. - -{mapping, "management.listener.https.tls_versions", "emqx_management.listeners", [ - {datatype, string} -]}. - -{mapping, "management.listener.https.fail_if_no_peer_cert", "emqx_management.listeners", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "management.listener.https.inet6", "emqx_management.listeners", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "management.listener.https.ipv6_v6only", "emqx_management.listeners", [ - {default, false}, - {datatype, {enum, [true, false]}} -]}. - -{translation, "emqx_management.application", fun(Conf) -> - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - Opts = fun(Prefix) -> - Filter([{default_secret, cuttlefish:conf_get(Prefix ++ ".default_secret", Conf)}]) - end, - Prefix = "management.application", - Transfer = fun(default_secret, V) -> list_to_binary(V); - (_, V) -> V - end, - [{K, Transfer(K, V)}|| {K, V} <- Opts(Prefix)] -end}. - -{translation, "emqx_management.listeners", fun(Conf) -> - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - Opts = fun(Prefix) -> - Filter([{num_acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, - {max_connections, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}]) - end, - TcpOpts = fun(Prefix) -> - Filter([{backlog, cuttlefish:conf_get(Prefix ++ ".backlog", Conf, undefined)}, - {send_timeout, cuttlefish:conf_get(Prefix ++ ".send_timeout", Conf, undefined)}, - {send_timeout_close, cuttlefish:conf_get(Prefix ++ ".send_timeout_close", Conf, undefined)}, - {recbuf, cuttlefish:conf_get(Prefix ++ ".recbuf", Conf, undefined)}, - {sndbuf, cuttlefish:conf_get(Prefix ++ ".sndbuf", Conf, undefined)}, - {buffer, cuttlefish:conf_get(Prefix ++ ".buffer", Conf, undefined)}, - {nodelay, cuttlefish:conf_get(Prefix ++ ".nodelay", Conf, true)}, - {inet6, cuttlefish:conf_get(Prefix ++ ".inet6", Conf)}, - {ipv6_v6only, cuttlefish:conf_get(Prefix ++ ".ipv6_v6only", Conf)}]) - end, - - SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, - - SslOpts = fun(Prefix) -> - Versions = case SplitFun(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf, undefined)) of - undefined -> undefined; - L -> [list_to_atom(V) || V <- L] - end, - Filter([{versions, Versions}, - {ciphers, SplitFun(cuttlefish:conf_get(Prefix ++ ".ciphers", Conf, undefined))}, - {keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, - {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, - {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}, - {verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)}, - {fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)}]) - end, - lists:foldl( - fun(Proto, Acc) -> - Prefix = "management.listener." ++ atom_to_list(Proto), - case cuttlefish:conf_get(Prefix ++ ".port", Conf, undefined) of - undefined -> Acc; - Port -> - [{Proto, Port, TcpOpts(Prefix) ++ Opts(Prefix) - ++ case Proto of - http -> []; - https -> SslOpts(Prefix) - end} | Acc] - end - end, [], [http, https]) -end}. - diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl new file mode 100644 index 000000000..02f3e2f1a --- /dev/null +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -0,0 +1,62 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_management_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1]). + +structs() -> ["emqx_management"]. + +fields("emqx_management") -> + [ {default_application_id, fun default_application_id/1} + , {default_application_secret, fun default_application_secret/1} + , {max_row_limit, fun max_row_limit/1} + , {listeners, hoconsc:array(hoconsc:union([hoconsc:ref("http"), hoconsc:ref("https")]))} + ]; + +fields("http") -> + [ {"protocol", emqx_schema:t(string(), undefined, "http")} + , {"port", emqx_schema:t(integer(), undefined, 8081)} + , {"num_acceptors", emqx_schema:t(integer(), undefined, 4)} + , {"max_connections", emqx_schema:t(integer(), undefined, 512)} + , {"backlog", emqx_schema:t(integer(), undefined, 1024)} + , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "15s")} + , {"send_timeout_close", emqx_schema:t(emqx_schema:flag(), undefined, true)} + , {"inet6", emqx_schema:t(boolean(), undefined, false)} + , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} + ]; + +fields("https") -> + emqx_schema:ssl(undefined, #{enable => true}) ++ fields("http"). + +default_application_id(type) -> string(); +default_application_id(default) -> "admin"; +default_application_id(nullable) -> true; +default_application_id(_) -> undefined. + +default_application_secret(type) -> string(); +default_application_secret(default) -> "public"; +default_application_secret(nullable) -> true; +default_application_secret(_) -> undefined. + +max_row_limit(type) -> integer(); +max_row_limit(default) -> 1000; +max_row_limit(nullable) -> false; +max_row_limit(_) -> undefined. diff --git a/apps/emqx_management/src/emqx_mgmt_app.erl b/apps/emqx_management/src/emqx_mgmt_app.erl index 824e218f2..7161435f6 100644 --- a/apps/emqx_management/src/emqx_mgmt_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_app.erl @@ -20,6 +20,8 @@ -emqx_plugin(?MODULE). +-define(APP, emqx_management). + -export([ start/2 , stop/1 ]). @@ -27,6 +29,11 @@ -include("emqx_mgmt.hrl"). start(_Type, _Args) -> + Conf = filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), + {ok, RawConf} = hocon:load(Conf), + #{emqx_management := Config} = + hocon_schema:check_plain(emqx_management_schema, RawConf, #{atom_key => true}), + [application:set_env(?APP, Key, maps:get(Key, Config)) || Key <- maps:keys(Config)], {ok, Sup} = emqx_mgmt_sup:start_link(), ok = ekka_rlog:wait_for_shards([?MANAGEMENT_SHARD], infinity), _ = emqx_mgmt_auth:add_default_app(), diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 3998f6006..297de0196 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -129,11 +129,7 @@ force_add_app(AppId, Name, Secret, Desc, Status, Expired) -> generate_appsecret_if_need(InSecrt) when is_binary(InSecrt), byte_size(InSecrt) > 0 -> InSecrt; generate_appsecret_if_need(_) -> - AppConf = application:get_env(?APP, application, []), - case proplists:get_value(default_secret, AppConf) of - undefined -> emqx_guid:to_base62(emqx_guid:gen()); - Secret when is_binary(Secret) -> Secret - end. + emqx_guid:to_base62(emqx_guid:gen()). -spec(get_appsecret(appid()) -> {appsecret() | undefined}). get_appsecret(AppId) when is_binary(AppId) -> diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index 4b927a982..c23219e5d 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -78,7 +78,9 @@ stop_listener({Proto, Port, _}) -> minirest:stop_http(listener_name(Proto)). listeners() -> - application:get_env(?APP, listeners, []). + [{list_to_atom(Protocol), Port, maps:to_list(maps:without([protocol, port], Map))} + || Map = #{protocol := Protocol,port := Port} + <- application:get_env(?APP, listeners, [])]. listener_name(Proto) -> list_to_atom(atom_to_list(Proto) ++ ":management"). diff --git a/apps/emqx_management/test/emqx_mgmt_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_SUITE.erl index 84d5f2ebb..f14cc0fef 100644 --- a/apps/emqx_management/test/emqx_mgmt_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_SUITE.erl @@ -53,18 +53,30 @@ groups() -> ]}]. apps() -> - [emqx_management, emqx_retainer, emqx_modules]. + [emqx_management, emqx_retainer]. init_per_suite(Config) -> - application:set_env(ekka, strict_mode, true), ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), - emqx_ct_helpers:start_apps(apps()), + emqx_ct_helpers:start_apps(apps(), fun set_special_configs/1), Config. end_per_suite(_Config) -> emqx_ct_helpers:stop_apps(apps()). +set_special_configs(emqx_management) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_management, "test")), + Conf = #{<<"emqx_management">> => #{ + <<"listeners">> => [#{ + <<"protocol">> => <<"http">> + }]} + }, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + t_app(_Config) -> {ok, AppSecret} = emqx_mgmt_auth:add_app(<<"app_id">>, <<"app_name">>), ?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret)), @@ -74,16 +86,6 @@ t_app(_Config) -> true, undefined}, lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())), emqx_mgmt_auth:del_app(<<"app_id">>), - %% Use the default application secret - application:set_env(emqx_management, application, [{default_secret, <<"public">>}]), - {ok, AppSecret1} = emqx_mgmt_auth:add_app( - <<"app_id">>, <<"app_name">>, <<"app_desc">>, true, undefined), - ?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret1)), - ?assertEqual(AppSecret1, emqx_mgmt_auth:get_appsecret(<<"app_id">>)), - ?assertEqual(AppSecret1, <<"public">>), - ?assertEqual({<<"app_id">>, AppSecret1, <<"app_name">>, <<"app_desc">>, true, undefined}, - lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())), - emqx_mgmt_auth:del_app(<<"app_id">>), application:set_env(emqx_management, application, []), %% Specify the application secret {ok, AppSecret2} = emqx_mgmt_auth:add_app( diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl index 9e6347c65..0785b9563 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl @@ -37,8 +37,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> - application:load(emqx_modules), - emqx_ct_helpers:start_apps([emqx_management]), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), Config. end_per_suite(Config) -> @@ -51,6 +50,19 @@ init_per_testcase(_, Config) -> end_per_testcase(_, Config) -> Config. +set_special_configs(emqx_management) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_management, "test")), + Conf = #{<<"emqx_management">> => #{ + <<"listeners">> => [#{ + <<"protocol">> => <<"http">> + }]} + }, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + get(Key, ResponseBody) -> maps:get(Key, jiffy:decode(list_to_binary(ResponseBody), [return_maps])). diff --git a/apps/emqx_management/test/etc/emqx_management.conf b/apps/emqx_management/test/etc/emqx_management.conf index cec70cc8e..e54164cbd 100644 --- a/apps/emqx_management/test/etc/emqx_management.conf +++ b/apps/emqx_management/test/etc/emqx_management.conf @@ -1,35 +1,39 @@ -##-------------------------------------------------------------------- -## EMQ X Management Plugin -##-------------------------------------------------------------------- +emqx_management:{ + default_application_id: "admin" + default_application_secret: "public" + max_row_limit: 10000 + listeners: [ + { + num_acceptors: 4 + max_connections: 512 + protocol: "http" + port: 8080 + backlog: 512 + send_timeout: 15s + send_timeout_close: on + inet6: false + ipv6_v6only: false + } +## , +## { +## protocol: https +## port: 8081 +## acceptors: 2 +## backlog: 512 +## send_timeout: 15s +## send_timeout_close: on +## inet6: false +## ipv6_v6only: false +## certfile = "etc/certs/cert.pem" +## keyfile = "etc/certs/key.pem" +## cacertfile = "etc/certs/cacert.pem" +## verify = verify_peer +## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" +## ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" +## fail_if_no_peer_cert = true +## inet6 = false +## ipv6_v6only = false +## } + ] +} -## Max Row Limit -management.max_row_limit = 10000 - -## Application default secret -# -# management.application.default_secret = public - -##-------------------------------------------------------------------- -## HTTP Listener - -management.listener.http = 8080 -management.listener.http.acceptors = 2 -management.listener.http.max_clients = 512 -management.listener.http.backlog = 512 -management.listener.http.send_timeout = 15s -management.listener.http.send_timeout_close = on - -##-------------------------------------------------------------------- -## HTTPS Listener - -## management.listener.https = 8081 -## management.listener.https.acceptors = 2 -## management.listener.https.max_clients = 512 -## management.listener.https.backlog = 512 -## management.listener.https.send_timeout = 15s -## management.listener.https.send_timeout_close = on -## management.listener.https.certfile = etc/certs/cert.pem -## management.listener.https.keyfile = etc/certs/key.pem -## management.listener.https.cacertfile = etc/certs/cacert.pem -## management.listener.https.verify = verify_peer -## management.listener.https.fail_if_no_peer_cert = true diff --git a/apps/emqx_modules/test/emqx_modules_SUITE.erl b/apps/emqx_modules/test/emqx_modules_SUITE.erl index e8e0b3218..db85cbd0e 100644 --- a/apps/emqx_modules/test/emqx_modules_SUITE.erl +++ b/apps/emqx_modules/test/emqx_modules_SUITE.erl @@ -32,11 +32,22 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_management, emqx_modules], fun set_special_cfg/1), + emqx_ct_helpers:start_apps([emqx_management, emqx_modules], fun set_special_configs/1), emqx_ct_http:create_default_app(), Config. -set_special_cfg(_) -> +set_special_configs(emqx_management) -> + application:set_env(emqx, modules_loaded_file, emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_modules")), + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_management, "test")), + Conf = #{<<"emqx_management">> => #{ + <<"listeners">> => [#{ + <<"protocol">> => <<"http">> + }]} + }, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_) -> ok. end_per_suite(_Config) -> From 8266a79867682e747b96c693bcd6f98d9f1675f5 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 12:11:47 +0800 Subject: [PATCH 062/379] feat(conf): cluster/node conf to hocon --- apps/emqx/etc/emqx.conf | 538 ++++++++++++++++++++-------------------- 1 file changed, 270 insertions(+), 268 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index c3368a0c7..d179b9a11 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1,319 +1,321 @@ ## EMQ X Configuration 4.3 -## NOTE: Do not change format of CONFIG_SECTION_{BGN,END} comments! - -## CONFIG_SECTION_BGN=cluster ================================================== - -## Cluster name. -## -## Value: String -cluster.name = emqxcl - -## Specify the erlang distributed protocol. -## -## Value: Enum -## - inet_tcp: the default; handles TCP streams with IPv4 addressing. -## - inet6_tcp: handles TCP with IPv6 addressing. -## - inet_tls: using TLS for Erlang Distribution. -## -## vm.args: -proto_dist inet_tcp -cluster.proto_dist = inet_tcp - -## Cluster auto-discovery strategy. -## -## Value: Enum -## - manual: Manual join command -## - static: Static node list -## - mcast: IP Multicast -## - dns: DNS A Record -## - etcd: etcd -## - k8s: Kubernetes -## -## Default: manual -cluster.discovery = manual - -## Enable cluster autoheal from network partition. -## -## Value: on | off -## -## Default: on -cluster.autoheal = on - -## Autoclean down node. A down node will be removed from the cluster -## if this value > 0. -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 5m -cluster.autoclean = 5m - ##-------------------------------------------------------------------- -## Cluster using static node list - -## Node list of the cluster. -## -## Value: String -## cluster.static.seeds = "emqx1@127.0.0.1,emqx2@127.0.0.1" - +## Cluster ##-------------------------------------------------------------------- -## Cluster using IP Multicast. +cluster: { + ## Cluster name. + ## + ## Value: String + name: emqxcl -## IP Multicast Address. -## -## Value: IP Address -## cluster.mcast.addr = "239.192.0.1" + ## Cluster auto-discovery strategy. + ## + ## Value: Enum + ## - manual: Manual join command + ## - static: Static node list + ## - mcast: IP Multicast + ## - dns: DNS A Record + ## - etcd: etcd + ## - k8s: Kubernetes + ## + ## Default: manual + discovery: manual -## Multicast Ports. -## -## Value: Port List -## cluster.mcast.ports = "4369,4370" + ## Autoclean down node. A down node will be removed from the cluster + ## if this value > 0. + ## + ## Value: Duration + ## -h: hour, e.g. '2h' for 2 hours + ## -m: minute, e.g. '5m' for 5 minutes + ## -s: second, e.g. '30s' for 30 seconds + ## + ## Default: 5m + autoclean: "5m" -## Multicast Iface. -## -## Value: Iface Address -## -## Default: "0.0.0.0" -## cluster.mcast.iface = "0.0.0.0" + ## Enable cluster autoheal from network partition. + ## + ## Value: on | off + ## + ## Default: on + autoheal: on -## Multicast Ttl. -## -## Value: 0-255 -## cluster.mcast.ttl = 255 + ## Specify the erlang distributed protocol. + ## + ## Value: Enum + ## - inet_tcp: the default; handles TCP streams with IPv4 addressing. + ## - inet6_tcp: handles TCP with IPv6 addressing. + ## - inet_tls: using TLS for Erlang Distribution. + ## + ## vm.args: -proto_dist inet_tcp + proto_dist: inet_tcp -## Multicast loop. -## -## Value: on | off -## cluster.mcast.loop = on + ## Cluster using static node list + static: { + ## Node list of the cluster. + ## + ## Value: String + ## seeds: "emqx1@127.0.0.1,emqx2@127.0.0.1" + } -##-------------------------------------------------------------------- -## Cluster using DNS A records. + ##-------------------------------------------------------------------- + ## Cluster using IP Multicast. + mcast: { + ## IP Multicast Address. + ## + ## Value: IP Address + ## addr = "239.192.0.1" -## DNS name. -## -## Value: String -## cluster.dns.name = localhost + ## Multicast Ports. + ## + ## Value: Port List + ## ports = "4369,4370" -## The App name is used to build 'node.name' with IP address. -## -## Value: String -## cluster.dns.app = emqx + ## Multicast Iface. + ## + ## Value: Iface Address + ## + ## Default: "0.0.0.0" + ## iface: "0.0.0.0" -##-------------------------------------------------------------------- -## Cluster using etcd + ## Multicast Ttl. + ## + ## Value: 0-255 + ## ttl: 255 -## Etcd server list, seperated by ','. -## -## Value: String -## cluster.etcd.server = "http://127.0.0.1:2379" + ## Multicast loop. + ## + ## Value: on | off + ## loop: on + } -## Etcd api version -## -## Value: Enum -## - v2 -## - v3 -## cluster.etcd.version = v3 + ##-------------------------------------------------------------------- + ## Cluster using DNS A records. + dns: { + ## DNS name. + ## + ## Value: String + ## name: localhost -## The prefix helps build nodes path in etcd. Each node in the cluster -## will create a path in etcd: v2/keys/// -## -## Value: String -## cluster.etcd.prefix = emqxcl + ## The App name is used to build 'node.name' with IP address. + ## + ## Value: String + ## app: emqx + } -## The TTL for node's path in etcd. -## -## Value: Duration -## -## Default: 1m, 1 minute -## cluster.etcd.node_ttl = 1m + ##-------------------------------------------------------------------- + ## Cluster using etcd + etcd: { + ## Etcd server list, seperated by ','. + ## + ## Value: String + ## server: "http://127.0.0.1:2379" -## Path to a file containing the client's private PEM-encoded key. -## -## Value: File -## cluster.etcd.ssl.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" + ## Etcd api version + ## + ## Value: Enum + ## - v2 + ## - v3 + ## version: v3 -## The path to a file containing the client's certificate. -## -## Value: File -## cluster.etcd.ssl.certfile = "{{ platform_etc_dir }}/certs/client.pem" + ## The prefix helps build nodes path in etcd. Each node in the cluster + ## will create a path in etcd: v2/keys/// + ## + ## Value: String + ## prefix: emqxcl -## Path to the file containing PEM-encoded CA certificates. The CA certificates -## are used during server authentication and when building the client certificate chain. -## -## Value: File -## cluster.etcd.ssl.cacertfile = "{{ platform_etc_dir }}/certs/ca.pem" + ## The TTL for node's path in etcd. + ## + ## Value: Duration + ## + ## Default: 1m, 1 minute + ## node_ttl: 1m -##-------------------------------------------------------------------- -## Cluster using Kubernetes + ## ssl:{ + ## Path to a file containing the client's private PEM-encoded key. + ## Value: File + ## keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" -## Kubernetes API server list, seperated by ','. -## -## Value: String -## cluster.k8s.apiserver = "http://10.110.111.204:8080" + ## The path to a file containing the client's certificate. + ## Value: File + ## certfile: "{{ platform_etc_dir }}/certs/client.pem" -## The service name helps lookup EMQ nodes in the cluster. -## -## Value: String -## cluster.k8s.service_name = emqx + ## Path to the file containing PEM-encoded CA certificates. The CA certificates + ## are used during server authentication and when building the client certificate chain. + ## Value: File + ## cacertfile: "{{ platform_etc_dir }}/certs/ca.pem" + # } + } + ##-------------------------------------------------------------------- + ## Cluster using Kubernetes + k8s: { + # Kubernetes API server list, seperated by ','. + # Value: String + # apiserver: "http://10.110.111.204:8080" -## The address type is used to extract host from k8s service. -## -## Value: ip | dns | hostname -## cluster.k8s.address_type = ip + # The service name helps lookup EMQ nodes in the cluster. + # Value: String + # service_name: emqx -## The app name helps build 'node.name'. -## -## Value: String -## cluster.k8s.app_name = emqx + # The address type is used to extract host from k8s service. + # Value: ip | dns | hostname + # address_type: ip -## The suffix added to dns and hostname get from k8s service -## -## Value: String -## cluster.k8s.suffix = pod.cluster.local + # The app name helps build 'node.name'. + # Value: String + # app_name: emqx -## Kubernetes Namespace -## -## Value: String -## cluster.k8s.namespace = default + # The suffix added to dns and hostname get from k8s service + # Value: String + # suffix: pod.cluster.local -## CONFIG_SECTION_END=cluster ================================================== + # Kubernetes Namespace + # Value: String + # namespace: default + } + db_backend: mnesia + rlog: { + # role: core + # core_nodes: [] + } + +} ##-------------------------------------------------------------------- ## Node ##-------------------------------------------------------------------- +node: { + ## Node name. + ## + ## See: http://erlang.org/doc/reference_manual/distributed.html + ## + ## Value: @ + ## + ## Default: emqx@127.0.0.1 + name: "emqx@127.0.0.1" -## Node name. -## -## See: http://erlang.org/doc/reference_manual/distributed.html -## -## Value: @ -## -## Default: emqx@127.0.0.1 -node.name = "emqx@127.0.0.1" + ## Cookie for distributed node communication. + ## + ## Value: String + cookie: "emqxsecretcookie" -## Cookie for distributed node communication. -## -## Value: String -node.cookie = "emqxsecretcookie" + ## Data dir for the node + ## + ## Value: Folder + data_dir: "{{ platform_data_dir }}" -## Data dir for the node -## -## Value: Folder -node.data_dir = "{{ platform_data_dir }}" + ## The config file dir for the node + ## + ## Value: Folder + etc_dir: "{{ platform_etc_dir }}" -## The config file dir for the node -## -## Value: Folder -node.etc_dir = "{{ platform_etc_dir }}" + ## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable + ## heartbeat, or set the value as 'on' + ## + ## Value: on + ## + ## vm.args: -heart + ## heartbeat: on -## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable -## heartbeat, or set the value as 'on' -## -## Value: on -## -## vm.args: -heart -## node.heartbeat = on + ## Sets the number of threads in async thread pool. Valid range is 0-1024. + ## + ## See: http://erlang.org/doc/man/erl.html + ## + ## Value: 0-1024 + ## + ## vm.args: +A Number + ## async_threads: 4 -## Sets the number of threads in async thread pool. Valid range is 0-1024. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: 0-1024 -## -## vm.args: +A Number -## node.async_threads = 4 + ## Sets the maximum number of simultaneously existing processes for this + ## system if a Number is passed as value. + ## + ## See: http://erlang.org/doc/man/erl.html + ## + ## Value: Number [1024-134217727] + ## + ## vm.args: +P Number + ## process_limit: 2097152 -## Sets the maximum number of simultaneously existing processes for this -## system if a Number is passed as value. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1024-134217727] -## -## vm.args: +P Number -## node.process_limit = 2097152 + ## Sets the maximum number of simultaneously existing ports for this system. + ## + ## See: http://erlang.org/doc/man/erl.html + ## + ## Value: Number [1024-134217727] + ## + ## vm.args: +Q Number + ## max_ports: 1048576 -## Sets the maximum number of simultaneously existing ports for this system. -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1024-134217727] -## -## vm.args: +Q Number -## node.max_ports = 1048576 + ## Sets the distribution buffer busy limit (dist_buf_busy_limit). + ## + ## See: http://erlang.org/doc/man/erl.html + ## + ## Value: Number [1KB-2GB] + ## + ## vm.args: +zdbbl size + ## dist_buffer_size: 8MB -## Sets the distribution buffer busy limit (dist_buf_busy_limit). -## -## See: http://erlang.org/doc/man/erl.html -## -## Value: Number [1KB-2GB] -## -## vm.args: +zdbbl size -## node.dist_buffer_size = 8MB + ## Sets the maximum number of ETS tables. Note that mnesia and SSL will + ## create temporary ETS tables. + ## + ## Value: Number + ## + ## vm.args: +e Number + ## max_ets_tables: 262144 -## Sets the maximum number of ETS tables. Note that mnesia and SSL will -## create temporary ETS tables. -## -## Value: Number -## -## vm.args: +e Number -## node.max_ets_tables = 262144 + ## Global GC Interval. + ## + ## Value: Duration + ## + ## Examples: + ## - 2h: 2 hours + ## - 30m: 30 minutes + ## - 20s: 20 seconds + ## + ## Defaut: 15 minutes + global_gc_interval: 15m -## Global GC Interval. -## -## Value: Duration -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 20s: 20 seconds -## -## Defaut: 15 minutes -node.global_gc_interval = 15m + ## Tweak GC to run more often. + ## + ## Value: Number [0-65535] + ## + ## vm.args: -env ERL_FULLSWEEP_AFTER Number + ## fullsweep_after: 1000 -## Tweak GC to run more often. -## -## Value: Number [0-65535] -## -## vm.args: -env ERL_FULLSWEEP_AFTER Number -## node.fullsweep_after = 1000 + ## Crash dump log file. + ## + ## Value: Log file + crash_dump: "{{ platform_log_dir }}/crash.dump" -## Crash dump log file. -## -## Value: Log file -node.crash_dump = "{{ platform_log_dir }}/crash.dump" + ## Specify SSL Options in the file if using SSL for Erlang Distribution. + ## + ## Value: File + ## + ## vm.args: -ssl_dist_optfile + ## node.ssl_dist_optfile = "{{ platform_etc_dir }}/ssl_dist.conf" -## Specify SSL Options in the file if using SSL for Erlang Distribution. -## -## Value: File -## -## vm.args: -ssl_dist_optfile -## node.ssl_dist_optfile = "{{ platform_etc_dir }}/ssl_dist.conf" + ## Sets the net_kernel tick time. TickTime is specified in seconds. + ## Notice that all communicating nodes are to have the same TickTime + ## value specified. + ## + ## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime + ## + ## Value: Number + ## + ## vm.args: -kernel net_ticktime Number + ## dist_net_ticktime: 120 -## Sets the net_kernel tick time. TickTime is specified in seconds. -## Notice that all communicating nodes are to have the same TickTime -## value specified. -## -## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime -## -## Value: Number -## -## vm.args: -kernel net_ticktime Number -## node.dist_net_ticktime = 120 + ## Sets the port range for the listener socket of a distributed Erlang node. + ## Note that if there are firewalls between clustered nodes, this port segment + ## for nodes’ communication should be allowed. + ## + ## See: http://www.erlang.org/doc/man/kernel_app.html + ## + ## Value: Port [1024-65535] + dist_listen_min: 6369 + dist_listen_max: 6369 + backtrace_depth: 16 +} -## Sets the port range for the listener socket of a distributed Erlang node. -## Note that if there are firewalls between clustered nodes, this port segment -## for nodes’ communication should be allowed. -## -## See: http://www.erlang.org/doc/man/kernel_app.html -## -## Value: Port [1024-65535] -node.dist_listen_min = 6369 -node.dist_listen_max = 6369 - -node.backtrace_depth = 16 ## CONFIG_SECTION_BGN=rpc ====================================================== From 092c5455c81af924cf17b9c34fec70c8b32d5a9d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 2 Jul 2021 12:59:24 +0800 Subject: [PATCH 063/379] feat(config): change configs of zone and listener to hocon format --- apps/emqx/etc/emqx.conf | 2655 ++++++++++++++++++++------------------- 1 file changed, 1329 insertions(+), 1326 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index d179b9a11..54b4b1078 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -718,1592 +718,1595 @@ mqtt.strict_mode = false ## Value: String ## mqtt.response_information = example -## CONFIG_SECTION_BGN=zones =================================================== - ##-------------------------------------------------------------------- ## External Zone +zone.external { + ## Idle timeout of the external MQTT connections. + ## + ## Value: duration + idle_timeout = 15s -## Idle timeout of the external MQTT connections. -## -## Value: duration -zone.external.idle_timeout = 15s + ## Enable ACL check. + ## + ## Value: Flag + enable_acl = on -## Enable ACL check. -## -## Value: Flag -zone.external.enable_acl = on + ## Enable ban check. + ## + ## Value: Flag + enable_ban = on -## Enable ban check. -## -## Value: Flag -zone.external.enable_ban = on + ## Enable per connection statistics. + ## + ## Value: on | off + enable_stats = on -## Enable per connection statistics. -## -## Value: on | off -zone.external.enable_stats = on + ## The action when acl check reject current operation + ## + ## Value: ignore | disconnect + ## Default: ignore + acl_deny_action = ignore -## The action when acl check reject current operation -## -## Value: ignore | disconnect -## Default: ignore -zone.external.acl_deny_action = ignore + ## Force the MQTT connection process GC after this number of + ## messages | bytes passed through. + ## + ## Numbers delimited by `|'. Zero or negative is to disable. + force_gc_policy = "16000|16MB" -## Force the MQTT connection process GC after this number of -## messages | bytes passed through. -## -## Numbers delimited by `|'. Zero or negative is to disable. -zone.external.force_gc_policy = "16000|16MB" + ## Max message queue length and total heap size to force shutdown + ## connection/session process. + ## Message queue here is the Erlang process mailbox, but not the number + ## of queued MQTT messages of QoS 1 and 2. + ## + ## Numbers delimited by `|'. Zero or negative is to disable. + ## + ## Default: + ## - "10000|64MB" on ARCH_64 system + ## - "1000|32MB" on ARCH_32 sytem + #force_shutdown_policy = "10000|64MB" -## Max message queue length and total heap size to force shutdown -## connection/session process. -## Message queue here is the Erlang process mailbox, but not the number -## of queued MQTT messages of QoS 1 and 2. -## -## Numbers delimited by `|'. Zero or negative is to disable. -## -## Default: -## - "10000|64MB" on ARCH_64 system -## - "1000|32MB" on ARCH_32 sytem -#zone.external.force_shutdown_policy = "10000|64MB" + ## Maximum MQTT packet size allowed. + ## + ## Value: Bytes + ## Default: 1MB + ## max_packet_size = 64KB -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## Default: 1MB -## zone.external.max_packet_size = 64KB + ## Maximum length of MQTT clientId allowed. + ## + ## Value: Number [23-65535] + ## max_clientid_len = 1024 -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -## zone.external.max_clientid_len = 1024 + ## Maximum topic levels allowed. 0 means no limit. + ## + ## Value: Number + ## max_topic_levels = 7 -## Maximum topic levels allowed. 0 means no limit. -## -## Value: Number -## zone.external.max_topic_levels = 7 + ## Maximum QoS allowed. + ## + ## Value: 0 | 1 | 2 + ## max_qos_allowed = 2 -## Maximum QoS allowed. -## -## Value: 0 | 1 | 2 -## zone.external.max_qos_allowed = 2 + ## Maximum Topic Alias, 0 means no limit. + ## + ## Value: 0-65535 + ## max_topic_alias = 65535 -## Maximum Topic Alias, 0 means no limit. -## -## Value: 0-65535 -## zone.external.max_topic_alias = 65535 + ## Whether the Server supports retained messages. + ## + ## Value: boolean + ## retain_available = true -## Whether the Server supports retained messages. -## -## Value: boolean -## zone.external.retain_available = true + ## Whether the Server supports Wildcard Subscriptions + ## + ## Value: boolean + ## wildcard_subscription = false -## Whether the Server supports Wildcard Subscriptions -## -## Value: boolean -## zone.external.wildcard_subscription = false + ## Whether the Server supports Shared Subscriptions + ## + ## Value: boolean + ## shared_subscription = false -## Whether the Server supports Shared Subscriptions -## -## Value: boolean -## zone.external.shared_subscription = false + ## Server Keep Alive + ## + ## Value: Number + ## server_keepalive = 0 -## Server Keep Alive -## -## Value: Number -## zone.external.server_keepalive = 0 + ## The backoff for MQTT keepalive timeout. The broker will kick a connection out + ## until 'Keepalive * backoff * 2' timeout. + ## + ## Value: Float > 0.5 + keepalive_backoff = 0.75 -## The backoff for MQTT keepalive timeout. The broker will kick a connection out -## until 'Keepalive * backoff * 2' timeout. -## -## Value: Float > 0.5 -zone.external.keepalive_backoff = 0.75 + ## Maximum number of subscriptions allowed, 0 means no limit. + ## + ## Value: Number + max_subscriptions = 0 -## Maximum number of subscriptions allowed, 0 means no limit. -## -## Value: Number -zone.external.max_subscriptions = 0 + ## Force to upgrade QoS according to subscription. + ## + ## Value: on | off + upgrade_qos = off -## Force to upgrade QoS according to subscription. -## -## Value: on | off -zone.external.upgrade_qos = off + ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. + ## + ## Value: Number + max_inflight = 32 -## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. -## -## Value: Number -zone.external.max_inflight = 32 + ## Retry interval for QoS1/2 message delivering. + ## + ## Value: Duration + retry_interval = 30s -## Retry interval for QoS1/2 message delivering. -## -## Value: Duration -zone.external.retry_interval = 30s + ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. + ## + ## Value: Number + max_awaiting_rel = 100 -## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. -## -## Value: Number -zone.external.max_awaiting_rel = 100 + ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. + ## + ## Value: Duration + await_rel_timeout = 300s -## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. -## -## Value: Duration -zone.external.await_rel_timeout = 300s + ## Default session expiry interval for MQTT V3.1.1 connections. + ## + ## Value: Duration + ## -d: day + ## -h: hour + ## -m: minute + ## -s: second + ## + ## Default: 2h, 2 hours + session_expiry_interval = 2h -## Default session expiry interval for MQTT V3.1.1 connections. -## -## Value: Duration -## -d: day -## -h: hour -## -m: minute -## -s: second -## -## Default: 2h, 2 hours -zone.external.session_expiry_interval = 2h + ## Maximum queue length. Enqueued messages when persistent client disconnected, + ## or inflight window is full. 0 means no limit. + ## + ## Value: Number >= 0 + max_mqueue_len = 1000 -## Maximum queue length. Enqueued messages when persistent client disconnected, -## or inflight window is full. 0 means no limit. -## -## Value: Number >= 0 -zone.external.max_mqueue_len = 1000 + ## Topic priorities. + ## 'none' to indicate no priority table (by default), hence all messages + ## are treated equal + ## + ## Priority number [1-255] + ## Example: "topic/1=10,topic/2=8" + ## NOTE: comma and equal signs are not allowed for priority topic names + ## NOTE: messages for topics not in the priority table are treated as + ## either highest or lowest priority depending on the configured + ## value for mqueue_default_priority + ## + mqueue_priorities = none -## Topic priorities. -## 'none' to indicate no priority table (by default), hence all messages -## are treated equal -## -## Priority number [1-255] -## Example: "topic/1=10,topic/2=8" -## NOTE: comma and equal signs are not allowed for priority topic names -## NOTE: messages for topics not in the priority table are treated as -## either highest or lowest priority depending on the configured -## value for mqueue_default_priority -## -zone.external.mqueue_priorities = none + ## Default to highest priority for topics not matching priority table + ## + ## Value: highest | lowest + mqueue_default_priority = highest -## Default to highest priority for topics not matching priority table -## -## Value: highest | lowest -zone.external.mqueue_default_priority = highest + ## Whether to enqueue QoS0 messages. + ## + ## Value: false | true + mqueue_store_qos0 = true -## Whether to enqueue QoS0 messages. -## -## Value: false | true -zone.external.mqueue_store_qos0 = true + ## Whether to turn on flapping detect + ## + ## Value: on | off + enable_flapping_detect = off -## Whether to turn on flapping detect -## -## Value: on | off -zone.external.enable_flapping_detect = off + ## Message limit for the a external MQTT connection. + ## + ## Value: Number,Duration + ## Example: 100 messages per 10 seconds. + #rate_limit.conn_messages_in = "100,10s" -## Message limit for the a external MQTT connection. -## -## Value: Number,Duration -## Example: 100 messages per 10 seconds. -#zone.external.rate_limit.conn_messages_in = "100,10s" + ## Bytes limit for a external MQTT connections. + ## + ## Value: Number,Duration + ## Example: 100KB incoming per 10 seconds. + #rate_limit.conn_bytes_in = "100KB,10s" -## Bytes limit for a external MQTT connections. -## -## Value: Number,Duration -## Example: 100KB incoming per 10 seconds. -#zone.external.rate_limit.conn_bytes_in = "100KB,10s" + ## Whether to alarm the congested connections. + ## + ## Sometimes the mqtt connection (usually an MQTT subscriber) may get "congested" because + ## there're too many packets to sent. The socket trys to buffer the packets until the buffer is + ## full. If more packets comes after that, the packets will be "pending" in a queue + ## and we consider the connection is "congested". + ## + ## Enable this to send an alarm when there's any bytes pending in the queue. You could set + ## the `listener.tcp..sndbuf` to a larger value if the alarm is triggered too often. + ## + ## The name of the alarm is of format "conn_congestion//". + ## Where the is the client-id of the congested MQTT connection. + ## And the is the username or "unknown_user" of not provided by the client. + ## Default: off + #conn_congestion.alarm = off -## Whether to alarm the congested connections. -## -## Sometimes the mqtt connection (usually an MQTT subscriber) may get "congested" because -## there're too many packets to sent. The socket trys to buffer the packets until the buffer is -## full. If more packets comes after that, the packets will be "pending" in a queue -## and we consider the connection is "congested". -## -## Enable this to send an alarm when there's any bytes pending in the queue. You could set -## the `listener.tcp..sndbuf` to a larger value if the alarm is triggered too often. -## -## The name of the alarm is of format "conn_congestion//". -## Where the is the client-id of the congested MQTT connection. -## And the is the username or "unknown_user" of not provided by the client. -## Default: off -#zone.external.conn_congestion.alarm = off + ## Won't clear the congested alarm in how long time. + ## The alarm is cleared only when there're no pending bytes in the queue, and also it has been + ## `min_alarm_sustain_duration` time since the last time we considered the connection is "congested". + ## + ## This is to avoid clearing and sending the alarm again too often. + ## Default: 1m + #conn_congestion.min_alarm_sustain_duration = 1m -## Won't clear the congested alarm in how long time. -## The alarm is cleared only when there're no pending bytes in the queue, and also it has been -## `min_alarm_sustain_duration` time since the last time we considered the connection is "congested". -## -## This is to avoid clearing and sending the alarm again too often. -## Default: 1m -#zone.external.conn_congestion.min_alarm_sustain_duration = 1m + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## Value: Number, Duration + ## + ## Example: 100 messages per 1s + #quota.conn_messages_routing = "100,1s" -## Messages quota for the each of external MQTT connection. -## This value consumed by the number of recipient on a message. -## -## Value: Number, Duration -## -## Example: 100 messages per 1s -#zone.external.quota.conn_messages_routing = "100,1s" + ## Messages quota for the all of external MQTT connections. + ## This value consumed by the number of recipient on a message. + ## + ## Value: Number, Duration + ## + ## Example: 200000 messages per 1s + #quota.overall_messages_routing = "200000,1s" -## Messages quota for the all of external MQTT connections. -## This value consumed by the number of recipient on a message. -## -## Value: Number, Duration -## -## Example: 200000 messages per 1s -#zone.external.quota.overall_messages_routing = "200000,1s" + ## All the topics will be prefixed with the mountpoint path if this option is enabled. + ## + ## Variables in mountpoint path: + ## - %c: clientid + ## - %u: username + ## + ## Value: String + ## mountpoint = "devicebound/" -## All the topics will be prefixed with the mountpoint path if this option is enabled. -## -## Variables in mountpoint path: -## - %c: clientid -## - %u: username -## -## Value: String -## zone.external.mountpoint = "devicebound/" + ## Whether use username replace client id + ## + ## Value: boolean + ## Default: false + use_username_as_clientid = false -## Whether use username replace client id -## -## Value: boolean -## Default: false -zone.external.use_username_as_clientid = false + ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) + ## + ## Value: true | false + ignore_loop_deliver = false -## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -## -## Value: true | false -zone.external.ignore_loop_deliver = false + ## Whether to parse the MQTT frame in strict mode + ## + ## Value: true | false + strict_mode = false -## Whether to parse the MQTT frame in strict mode -## -## Value: true | false -zone.external.strict_mode = false - -## Specify the response information returned to the client -## -## Value: String -## zone.external.response_information = example + ## Specify the response information returned to the client + ## + ## Value: String + #response_information = example +} ##-------------------------------------------------------------------- ## Internal Zone +zone.internal { + ## Allow anonymous authentication by default if no auth plugins loaded. + ## Notice: Disable the option in production deployment! + ## + ## Value: true | false + allow_anonymous = true -zone.internal.allow_anonymous = true + ## Enable per connection stats. + ## + ## Value: Flag + enable_stats = on -## Enable per connection stats. -## -## Value: Flag -zone.internal.enable_stats = on + ## Enable ACL check. + ## + ## Value: Flag + enable_acl = off -## Enable ACL check. -## -## Value: Flag -zone.internal.enable_acl = off + ## The action when acl check reject current operation + ## + ## Value: ignore | disconnect + ## Default: ignore + acl_deny_action = ignore -## The action when acl check reject current operation -## -## Value: ignore | disconnect -## Default: ignore -zone.internal.acl_deny_action = ignore + ## See zone.$name.force_gc_policy + ## force_gc_policy = "128000|128MB" -## See zone.$name.force_gc_policy -## zone.internal.force_gc_policy = "128000|128MB" + ## See zone.$name.wildcard_subscription. + ## + ## Value: boolean + ## wildcard_subscription = true -## See zone.$name.wildcard_subscription. -## -## Value: boolean -## zone.internal.wildcard_subscription = true + ## See zone.$name.shared_subscription. + ## + ## Value: boolean + ## shared_subscription = true -## See zone.$name.shared_subscription. -## -## Value: boolean -## zone.internal.shared_subscription = true + ## See zone.$name.max_subscriptions. + ## + ## Value: Integer + max_subscriptions = 0 -## See zone.$name.max_subscriptions. -## -## Value: Integer -zone.internal.max_subscriptions = 0 + ## See zone.$name.max_inflight + ## + ## Value: Number + max_inflight = 128 -## See zone.$name.max_inflight -## -## Value: Number -zone.internal.max_inflight = 128 + ## See zone.$name.max_awaiting_rel + ## + ## Value: Number + max_awaiting_rel = 1000 -## See zone.$name.max_awaiting_rel -## -## Value: Number -zone.internal.max_awaiting_rel = 1000 + ## See zone.$name.max_mqueue_len + ## + ## Value: Number >= 0 + max_mqueue_len = 10000 -## See zone.$name.max_mqueue_len -## -## Value: Number >= 0 -zone.internal.max_mqueue_len = 10000 + ## Whether to enqueue Qos0 messages. + ## + ## Value: false | true + mqueue_store_qos0 = true -## Whether to enqueue Qos0 messages. -## -## Value: false | true -zone.internal.mqueue_store_qos0 = true + ## Whether to turn on flapping detect + ## + ## Value: on | off + enable_flapping_detect = off -## Whether to turn on flapping detect -## -## Value: on | off -zone.internal.enable_flapping_detect = off + ## See zone.$name.force_shutdown_policy + ## + ## Default: + ## - "10000|64MB" on ARCH_64 system + ## - "1000|32MB" on ARCH_32 sytem + #force_shutdown_policy = 10000|64MB -## See zone.$name.force_shutdown_policy -## -## Default: -## - "10000|64MB" on ARCH_64 system -## - "1000|32MB" on ARCH_32 sytem -#zone.internal.force_shutdown_policy = 10000|64MB + ## All the topics will be prefixed with the mountpoint path if this option is enabled. + ## + ## Variables in mountpoint path: + ## - %c: clientid + ## - %u: username + ## + ## Value: String + ## mountpoint = "cloudbound/" -## All the topics will be prefixed with the mountpoint path if this option is enabled. -## -## Variables in mountpoint path: -## - %c: clientid -## - %u: username -## -## Value: String -## zone.internal.mountpoint = "cloudbound/" + ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) + ## + ## Value: true | false + ignore_loop_deliver = false -## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -## -## Value: true | false -zone.internal.ignore_loop_deliver = false + ## Whether to parse the MQTT frame in strict mode + ## + ## Value: true | false + strict_mode = false -## Whether to parse the MQTT frame in strict mode -## -## Value: true | false -zone.internal.strict_mode = false + ## Specify the response information returned to the client + ## + ## Value: String + ## response_information = example -## Specify the response information returned to the client -## -## Value: String -## zone.internal.response_information = example - -## Allow the zone's clients to bypass authentication step -## -## Value: true | false -zone.internal.bypass_auth_plugins = true - -## CONFIG_SECTION_END=zones ==================================================== - -## CONFIG_SECTION_BGN=listeners ================================================ + ## Allow the zone's clients to bypass authentication step + ## + ## Value: true | false + bypass_auth_plugins = true +} ##-------------------------------------------------------------------- ## MQTT/TCP - External TCP Listener for MQTT Protocol +listener.tcp.external { + ## listener.tcp.$name.endpoint is the IP address and port that the MQTT/TCP + ## listener will bind. + ## + ## Value: IP:Port | Port + ## + ## Examples: 1883, "127.0.0.1:1883", "::1:1883" + endpoint = "0.0.0.0:1883" -## listener.tcp.$name is the IP address and port that the MQTT/TCP -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 1883, "127.0.0.1:1883", "::1:1883" -listener.tcp.external.endpoint = "0.0.0.0:1883" + ## The acceptor pool for external MQTT/TCP listener. + ## + ## Value: Number + acceptors = 8 -## The acceptor pool for external MQTT/TCP listener. -## -## Value: Number -listener.tcp.external.acceptors = 8 + ## Maximum number of concurrent MQTT/TCP connections. + ## + ## Value: Number + max_connections = 1024000 -## Maximum number of concurrent MQTT/TCP connections. -## -## Value: Number -listener.tcp.external.max_connections = 1024000 + ## Maximum external connections per second. + ## + ## Value: Number + max_conn_rate = 1000 -## Maximum external connections per second. -## -## Value: Number -listener.tcp.external.max_conn_rate = 1000 + ## Specify the {active, N} option for the external MQTT/TCP Socket. + ## + ## Value: Number + active_n = 100 -## Specify the {active, N} option for the external MQTT/TCP Socket. -## -## Value: Number -listener.tcp.external.active_n = 100 + ## Zone of the external MQTT/TCP listener belonged to. + ## + ## See: zone.$name.* + ## + ## Value: String + zone = external -## Zone of the external MQTT/TCP listener belonged to. -## -## See: zone.$name.* -## -## Value: String -listener.tcp.external.zone = external + ## The access control rules for the MQTT/TCP listener. + ## + ## See: https://github.com/emqtt/esockd#allowdeny + ## + ## Value: ACL Rule + ## + ## Example: "allow 192.168.0.0/24" + access.1 = "allow all" -## The access control rules for the MQTT/TCP listener. -## -## See: https://github.com/emqtt/esockd#allowdeny -## -## Value: ACL Rule -## -## Example: "allow 192.168.0.0/24" -listener.tcp.external.access.1 = "allow all" + ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed + ## behind HAProxy or Nginx. + ## + ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ + ## + ## Value: on | off + ## proxy_protocol = on -## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed -## behind HAProxy or Nginx. -## -## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ -## -## Value: on | off -## listener.tcp.external.proxy_protocol = on + ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection + ## if no proxy protocol packet recevied within the timeout. + ## + ## Value: Duration + ## proxy_protocol_timeout = 3s -## Sets the timeout for proxy protocol. EMQ X will close the TCP connection -## if no proxy protocol packet recevied within the timeout. -## -## Value: Duration -## listener.tcp.external.proxy_protocol_timeout = 3s + ## Enable the option for X.509 certificate based authentication. + ## EMQX will use the common name of certificate as MQTT username. + ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info + ## + ## Value: cn + ## peer_cert_as_username = cn -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT username. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.tcp.external.peer_cert_as_username = cn + ## Enable the option for X.509 certificate based authentication. + ## EMQX will use the common name of certificate as MQTT clientid. + ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info + ## + ## Value: cn + ## peer_cert_as_clientid = cn -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT clientid. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.tcp.external.peer_cert_as_clientid = cn + ## The TCP backlog defines the maximum length that the queue of pending + ## connections can grow to. + ## + ## Value: Number >= 0 + backlog = 1024 -## The TCP backlog defines the maximum length that the queue of pending -## connections can grow to. -## -## Value: Number >= 0 -listener.tcp.external.backlog = 1024 + ## The TCP send timeout for external MQTT connections. + ## + ## Value: Duration + send_timeout = 15s -## The TCP send timeout for external MQTT connections. -## -## Value: Duration -listener.tcp.external.send_timeout = 15s + ## Close the TCP connection if send timeout. + ## + ## Value: on | off + send_timeout_close = on -## Close the TCP connection if send timeout. -## -## Value: on | off -listener.tcp.external.send_timeout_close = on + ## The TCP receive buffer(os kernel) for MQTT connections. + ## + ## See: http://erlang.org/doc/man/inet.html + ## + ## Value: Bytes + ## recbuf = 2KB -## The TCP receive buffer(os kernel) for MQTT connections. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -## listener.tcp.external.recbuf = 2KB + ## The TCP send buffer(os kernel) for MQTT connections. + ## + ## See: http://erlang.org/doc/man/inet.html + ## + ## Value: Bytes + ## sndbuf = 2KB -## The TCP send buffer(os kernel) for MQTT connections. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -## listener.tcp.external.sndbuf = 2KB + ## The size of the user-level software buffer used by the driver. + ## Not to be confused with options sndbuf and recbuf, which correspond + ## to the Kernel socket buffers. It is recommended to have val(buffer) + ## >= max(val(sndbuf),val(recbuf)) to avoid performance issues because + ## of unnecessary copying. val(buffer) is automatically set to the above + ## maximum when values sndbuf or recbuf are set. + ## + ## See: http://erlang.org/doc/man/inet.html + ## + ## Value: Bytes + ## buffer = 2KB -## The size of the user-level software buffer used by the driver. -## Not to be confused with options sndbuf and recbuf, which correspond -## to the Kernel socket buffers. It is recommended to have val(buffer) -## >= max(val(sndbuf),val(recbuf)) to avoid performance issues because -## of unnecessary copying. val(buffer) is automatically set to the above -## maximum when values sndbuf or recbuf are set. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -## listener.tcp.external.buffer = 2KB + ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## + ## Value: on | off + ## tune_buffer = off -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## Value: on | off -## listener.tcp.external.tune_buffer = off + ## The socket is set to a busy state when the amount of data queued internally + ## by the ERTS socket implementation reaches this limit. + ## + ## Value: on | off + ## Defaults to 1MB + ## high_watermark = 1MB -## The socket is set to a busy state when the amount of data queued internally -## by the ERTS socket implementation reaches this limit. -## -## Value: on | off -## Defaults to 1MB -## listener.tcp.external.high_watermark = 1MB + ## The TCP_NODELAY flag for MQTT connections. Small amounts of data are + ## sent immediately if the option is enabled. + ## + ## Value: true | false + nodelay = true -## The TCP_NODELAY flag for MQTT connections. Small amounts of data are -## sent immediately if the option is enabled. -## -## Value: true | false -listener.tcp.external.nodelay = true - -## The SO_REUSEADDR flag for TCP listener. -## -## Value: true | false -listener.tcp.external.reuseaddr = true + ## The SO_REUSEADDR flag for TCP listener. + ## + ## Value: true | false + reuseaddr = true +} ##-------------------------------------------------------------------- ## Internal TCP Listener for MQTT Protocol -## The IP address and port that the internal MQTT/TCP protocol listener -## will bind. -## -## Value: IP:Port, Port -## -## Examples: 11883, "127.0.0.1:11883", "::1:11883" -listener.tcp.internal.endpoint = "127.0.0.1:11883" +listener.tcp.internal { + ## The IP address and port that the internal MQTT/TCP protocol listener + ## will bind. + ## + ## Value: IP:Port, Port + ## + ## Examples: 11883, "127.0.0.1:11883", "::1:11883" + endpoint = "127.0.0.1:11883" -## The acceptor pool for internal MQTT/TCP listener. -## -## Value: Number -listener.tcp.internal.acceptors = 4 + ## The acceptor pool for internal MQTT/TCP listener. + ## + ## Value: Number + acceptors = 4 -## Maximum number of concurrent MQTT/TCP connections. -## -## Value: Number -listener.tcp.internal.max_connections = 1024000 + ## Maximum number of concurrent MQTT/TCP connections. + ## + ## Value: Number + max_connections = 1024000 -## Maximum internal connections per second. -## -## Value: Number -listener.tcp.internal.max_conn_rate = 1000 + ## Maximum internal connections per second. + ## + ## Value: Number + max_conn_rate = 1000 -## Specify the {active, N} option for the internal MQTT/TCP Socket. -## -## Value: Number -listener.tcp.internal.active_n = 1000 + ## Specify the {active, N} option for the internal MQTT/TCP Socket. + ## + ## Value: Number + active_n = 1000 -## Zone of the internal MQTT/TCP listener belonged to. -## -## Value: String -listener.tcp.internal.zone = internal + ## Zone of the internal MQTT/TCP listener belonged to. + ## + ## Value: String + zone = internal -## The TCP backlog of internal MQTT/TCP Listener. -## -## See: listener.tcp.$name.backlog -## -## Value: Number >= 0 -listener.tcp.internal.backlog = 512 + ## The TCP backlog of internal MQTT/TCP Listener. + ## + ## See: listener.tcp.$name.backlog + ## + ## Value: Number >= 0 + backlog = 512 -## The TCP send timeout for internal MQTT connections. -## -## See: listener.tcp.$name.send_timeout -## -## Value: Duration -listener.tcp.internal.send_timeout = 5s + ## The TCP send timeout for internal MQTT connections. + ## + ## See: listener.tcp.$name.send_timeout + ## + ## Value: Duration + send_timeout = 5s -## Close the MQTT/TCP connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -listener.tcp.internal.send_timeout_close = on + ## Close the MQTT/TCP connection if send timeout. + ## + ## See: listener.tcp.$name.send_timeout_close + ## + ## Value: on | off + send_timeout_close = on -## The TCP receive buffer(os kernel) for internal MQTT connections. -## -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -listener.tcp.internal.recbuf = 64KB + ## The TCP receive buffer(os kernel) for internal MQTT connections. + ## + ## See: listener.tcp.$name.recbuf + ## + ## Value: Bytes + recbuf = 64KB -## The TCP send buffer(os kernel) for internal MQTT connections. -## -## See: http://erlang.org/doc/man/inet.html -## -## Value: Bytes -listener.tcp.internal.sndbuf = 64KB + ## The TCP send buffer(os kernel) for internal MQTT connections. + ## + ## See: http://erlang.org/doc/man/inet.html + ## + ## Value: Bytes + sndbuf = 64KB -## The size of the user-level software buffer used by the driver. -## -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.tcp.internal.buffer = 16KB + ## The size of the user-level software buffer used by the driver. + ## + ## See: listener.tcp.$name.buffer + ## + ## Value: Bytes + ## buffer = 16KB -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## See: listener.tcp.$name.tune_buffer -## -## Value: on | off -## listener.tcp.internal.tune_buffer = off + ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## + ## See: listener.tcp.$name.tune_buffer + ## + ## Value: on | off + ## tune_buffer = off -## The TCP_NODELAY flag for internal MQTT connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -listener.tcp.internal.nodelay = false + ## The TCP_NODELAY flag for internal MQTT connections. + ## + ## See: listener.tcp.$name.nodelay + ## + ## Value: true | false + nodelay = false -## The SO_REUSEADDR flag for MQTT/TCP Listener. -## -## Value: true | false -listener.tcp.internal.reuseaddr = true + ## The SO_REUSEADDR flag for MQTT/TCP Listener. + ## + ## Value: true | false + reuseaddr = true +} ##-------------------------------------------------------------------- ## MQTT/SSL - External SSL Listener for MQTT Protocol +listener.ssl.external { + ## listener.ssl.$name is the IP address and port that the MQTT/SSL + ## listener will bind. + ## + ## Value: IP:Port | Port + ## + ## Examples: 8883, "127.0.0.1:8883", "::1:8883" + endpoint = 8883 -## listener.ssl.$name is the IP address and port that the MQTT/SSL -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 8883, "127.0.0.1:8883", "::1:8883" -listener.ssl.external.endpoint = 8883 + ## The acceptor pool for external MQTT/SSL listener. + ## + ## Value: Number + acceptors = 16 -## The acceptor pool for external MQTT/SSL listener. -## -## Value: Number -listener.ssl.external.acceptors = 16 + ## Maximum number of concurrent MQTT/SSL connections. + ## + ## Value: Number + max_connections = 102400 -## Maximum number of concurrent MQTT/SSL connections. -## -## Value: Number -listener.ssl.external.max_connections = 102400 + ## Maximum MQTT/SSL connections per second. + ## + ## Value: Number + max_conn_rate = 500 -## Maximum MQTT/SSL connections per second. -## -## Value: Number -listener.ssl.external.max_conn_rate = 500 + ## Specify the {active, N} option for the internal MQTT/SSL Socket. + ## + ## Value: Number + active_n = 100 -## Specify the {active, N} option for the internal MQTT/SSL Socket. -## -## Value: Number -listener.ssl.external.active_n = 100 + ## Zone of the external MQTT/SSL listener belonged to. + ## + ## Value: String + zone = external -## Zone of the external MQTT/SSL listener belonged to. -## -## Value: String -listener.ssl.external.zone = external + ## The access control rules for the MQTT/SSL listener. + ## + ## See: listener.tcp.$name.access + ## + ## Value: ACL Rule + access.1 = "allow all" -## The access control rules for the MQTT/SSL listener. -## -## See: listener.tcp.$name.access -## -## Value: ACL Rule -listener.ssl.external.access.1 = "allow all" + ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind + ## HAProxy or Nginx. + ## + ## See: listener.tcp.$name.proxy_protocol + ## + ## Value: on | off + ## proxy_protocol = on -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind -## HAProxy or Nginx. -## -## See: listener.tcp.$name.proxy_protocol -## -## Value: on | off -## listener.ssl.external.proxy_protocol = on + ## Sets the timeout for proxy protocol. + ## + ## See: listener.tcp.$name.proxy_protocol_timeout + ## + ## Value: Duration + ## proxy_protocol_timeout = 3s -## Sets the timeout for proxy protocol. -## -## See: listener.tcp.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.ssl.external.proxy_protocol_timeout = 3s + ## TLS versions only to protect from POODLE attack. + ## + ## See: http://erlang.org/doc/man/ssl.html + ## + ## Value: String, seperated by ',' + ## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier + ## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" -## TLS versions only to protect from POODLE attack. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: String, seperated by ',' -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## listener.ssl.external.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" + ## TLS Handshake timeout. + ## + ## Value: Duration + handshake_timeout = 15s -## TLS Handshake timeout. -## -## Value: Duration -listener.ssl.external.handshake_timeout = 15s + ## Maximum number of non-self-issued intermediate certificates that + ## can follow the peer certificate in a valid certification path. + ## + ## Value: Number + ## depth = 10 -## Maximum number of non-self-issued intermediate certificates that -## can follow the peer certificate in a valid certification path. -## -## Value: Number -## listener.ssl.external.depth = 10 + ## String containing the user's password. Only used if the private keyfile + ## is password-protected. + ## + ## Value: String + ## key_password = yourpass -## String containing the user's password. Only used if the private keyfile -## is password-protected. -## -## Value: String -## listener.ssl.external.key_password = yourpass + ## Path to the file containing the user's private PEM-encoded key. + ## + ## See: http://erlang.org/doc/man/ssl.html + ## + ## Value: File + keyfile = "{{ platform_etc_dir }}/certs/key.pem" -## Path to the file containing the user's private PEM-encoded key. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: File -listener.ssl.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ## Path to a file containing the user certificate. + ## + ## See: http://erlang.org/doc/man/ssl.html + ## + ## Value: File + certfile = "{{ platform_etc_dir }}/certs/cert.pem" -## Path to a file containing the user certificate. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: File -listener.ssl.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ## Path to the file containing PEM-encoded CA certificates. The CA certificates + ## are used during server authentication and when building the client certificate chain. + ## + ## Value: File + ## cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" -## Path to the file containing PEM-encoded CA certificates. The CA certificates -## are used during server authentication and when building the client certificate chain. -## -## Value: File -## listener.ssl.external.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + ## The Ephemeral Diffie-Helman key exchange is a very effective way of + ## ensuring Forward Secrecy by exchanging a set of keys that never hit + ## the wire. Since the DH key is effectively signed by the private key, + ## it needs to be at least as strong as the private key. In addition, + ## the default DH groups that most of the OpenSSL installations have + ## are only a handful (since they are distributed with the OpenSSL + ## package that has been built for the operating system it’s running on) + ## and hence predictable (not to mention, 1024 bits only). + ## In order to escape this situation, first we need to generate a fresh, + ## strong DH group, store it in a file and then use the option above, + ## to force our SSL application to use the new DH group. Fortunately, + ## OpenSSL provides us with a tool to do that. Simply run: + ## openssl dhparam -out dh-params.pem 2048 + ## + ## Value: File + ## dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" -## The Ephemeral Diffie-Helman key exchange is a very effective way of -## ensuring Forward Secrecy by exchanging a set of keys that never hit -## the wire. Since the DH key is effectively signed by the private key, -## it needs to be at least as strong as the private key. In addition, -## the default DH groups that most of the OpenSSL installations have -## are only a handful (since they are distributed with the OpenSSL -## package that has been built for the operating system it’s running on) -## and hence predictable (not to mention, 1024 bits only). -## In order to escape this situation, first we need to generate a fresh, -## strong DH group, store it in a file and then use the option above, -## to force our SSL application to use the new DH group. Fortunately, -## OpenSSL provides us with a tool to do that. Simply run: -## openssl dhparam -out dh-params.pem 2048 -## -## Value: File -## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" + ## A server only does x509-path validation in mode verify_peer, + ## as it then sends a certificate request to the client (this + ## message is not sent if the verify option is verify_none). + ## You can then also want to specify option fail_if_no_peer_cert. + ## More information at: http://erlang.org/doc/man/ssl.html + ## + ## Value: verify_peer | verify_none + ## verify = verify_peer -## A server only does x509-path validation in mode verify_peer, -## as it then sends a certificate request to the client (this -## message is not sent if the verify option is verify_none). -## You can then also want to specify option fail_if_no_peer_cert. -## More information at: http://erlang.org/doc/man/ssl.html -## -## Value: verify_peer | verify_none -## listener.ssl.external.verify = verify_peer + ## Used together with {verify, verify_peer} by an SSL server. If set to true, + ## the server fails if the client does not have a certificate to send, that is, + ## sends an empty certificate. + ## + ## Value: true | false + ## fail_if_no_peer_cert = true -## Used together with {verify, verify_peer} by an SSL server. If set to true, -## the server fails if the client does not have a certificate to send, that is, -## sends an empty certificate. -## -## Value: true | false -## listener.ssl.external.fail_if_no_peer_cert = true + ## This is the single most important configuration option of an Erlang SSL + ## application. Ciphers (and their ordering) define the way the client and + ## server encrypt information over the wire, from the initial Diffie-Helman + ## key exchange, the session key encryption ## algorithm and the message + ## digest algorithm. Selecting a good cipher suite is critical for the + ## application’s data security, confidentiality and performance. + ## + ## The cipher list above offers: + ## + ## A good balance between compatibility with older browsers. + ## It can get stricter for Machine-To-Machine scenarios. + ## Perfect Forward Secrecy. + ## No old/insecure encryption and HMAC algorithms + ## + ## Most of it was copied from Mozilla’s Server Side TLS article + ## + ## Value: Ciphers + ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## This is the single most important configuration option of an Erlang SSL -## application. Ciphers (and their ordering) define the way the client and -## server encrypt information over the wire, from the initial Diffie-Helman -## key exchange, the session key encryption ## algorithm and the message -## digest algorithm. Selecting a good cipher suite is critical for the -## application’s data security, confidentiality and performance. -## -## The cipher list above offers: -## -## A good balance between compatibility with older browsers. -## It can get stricter for Machine-To-Machine scenarios. -## Perfect Forward Secrecy. -## No old/insecure encryption and HMAC algorithms -## -## Most of it was copied from Mozilla’s Server Side TLS article -## -## Value: Ciphers -listener.ssl.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" + ## Ciphers for TLS PSK. + ## Note that 'ciphers' and 'psk_ciphers' cannot + ## be configured at the same time. + ## See 'https://tools.ietf.org/html/rfc4279#section-2'. + #psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" -## Ciphers for TLS PSK. -## Note that 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -#listener.ssl.external.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" + ## SSL parameter renegotiation is a feature that allows a client and a server + ## to renegotiate the parameters of the SSL connection on the fly. + ## RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, + ## you drop support for the insecure renegotiation, prone to MitM attacks. + ## + ## Value: on | off + ## secure_renegotiate = off -## SSL parameter renegotiation is a feature that allows a client and a server -## to renegotiate the parameters of the SSL connection on the fly. -## RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, -## you drop support for the insecure renegotiation, prone to MitM attacks. -## -## Value: on | off -## listener.ssl.external.secure_renegotiate = off + ## A performance optimization setting, it allows clients to reuse + ## pre-existing sessions, instead of initializing new ones. + ## Read more about it here. + ## + ## See: http://erlang.org/doc/man/ssl.html + ## + ## Value: on | off + ## reuse_sessions = on -## A performance optimization setting, it allows clients to reuse -## pre-existing sessions, instead of initializing new ones. -## Read more about it here. -## -## See: http://erlang.org/doc/man/ssl.html -## -## Value: on | off -## listener.ssl.external.reuse_sessions = on + ## An important security setting, it forces the cipher to be set based + ## on the server-specified order instead of the client-specified order, + ## hence enforcing the (usually more properly configured) security + ## ordering of the server administrator. + ## + ## Value: on | off + ## honor_cipher_order = on -## An important security setting, it forces the cipher to be set based -## on the server-specified order instead of the client-specified order, -## hence enforcing the (usually more properly configured) security -## ordering of the server administrator. -## -## Value: on | off -## listener.ssl.external.honor_cipher_order = on + ## Use the CN, DN or CRT field from the client certificate as a username. + ## Notice that 'verify' should be set as 'verify_peer'. + ## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. + ## + ## Value: cn | dn | crt | pem | md5 + ## peer_cert_as_username = cn -## Use the CN, DN or CRT field from the client certificate as a username. -## Notice that 'verify' should be set as 'verify_peer'. -## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. -## -## Value: cn | dn | crt | pem | md5 -## listener.ssl.external.peer_cert_as_username = cn + ## Use the CN, DN or CRT field from the client certificate as a username. + ## Notice that 'verify' should be set as 'verify_peer'. + ## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. + ## + ## Value: cn | dn | crt | pem | md5 + ## peer_cert_as_clientid = cn -## Use the CN, DN or CRT field from the client certificate as a username. -## Notice that 'verify' should be set as 'verify_peer'. -## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. -## -## Value: cn | dn | crt | pem | md5 -## listener.ssl.external.peer_cert_as_clientid = cn + ## TCP backlog for the SSL connection. + ## + ## See listener.tcp.$name.backlog + ## + ## Value: Number >= 0 + ## backlog = 1024 -## TCP backlog for the SSL connection. -## -## See listener.tcp.$name.backlog -## -## Value: Number >= 0 -## listener.ssl.external.backlog = 1024 + ## The TCP send timeout for the SSL connection. + ## + ## See listener.tcp.$name.send_timeout + ## + ## Value: Duration + ## send_timeout = 15s -## The TCP send timeout for the SSL connection. -## -## See listener.tcp.$name.send_timeout -## -## Value: Duration -## listener.ssl.external.send_timeout = 15s + ## Close the SSL connection if send timeout. + ## + ## See: listener.tcp.$name.send_timeout_close + ## + ## Value: on | off + ## send_timeout_close = on -## Close the SSL connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -## listener.ssl.external.send_timeout_close = on + ## The TCP receive buffer(os kernel) for the SSL connections. + ## + ## See: listener.tcp.$name.recbuf + ## + ## Value: Bytes + ## recbuf = 4KB -## The TCP receive buffer(os kernel) for the SSL connections. -## -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -## listener.ssl.external.recbuf = 4KB + ## The TCP send buffer(os kernel) for internal MQTT connections. + ## + ## See: listener.tcp.$name.sndbuf + ## + ## Value: Bytes + ## sndbuf = 4KB -## The TCP send buffer(os kernel) for internal MQTT connections. -## -## See: listener.tcp.$name.sndbuf -## -## Value: Bytes -## listener.ssl.external.sndbuf = 4KB + ## The size of the user-level software buffer used by the driver. + ## + ## See: listener.tcp.$name.buffer + ## + ## Value: Bytes + ## buffer = 4KB -## The size of the user-level software buffer used by the driver. -## -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.ssl.external.buffer = 4KB + ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## + ## See: listener.tcp.$name.tune_buffer + ## + ## Value: on | off + ## tune_buffer = off -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## See: listener.tcp.$name.tune_buffer -## -## Value: on | off -## listener.ssl.external.tune_buffer = off + ## The TCP_NODELAY flag for SSL connections. + ## + ## See: listener.tcp.$name.nodelay + ## + ## Value: true | false + ## nodelay = true -## The TCP_NODELAY flag for SSL connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -## listener.ssl.external.nodelay = true - -## The SO_REUSEADDR flag for MQTT/SSL Listener. -## -## Value: true | false -listener.ssl.external.reuseaddr = true + ## The SO_REUSEADDR flag for MQTT/SSL Listener. + ## + ## Value: true | false + reuseaddr = true +} ##-------------------------------------------------------------------- ## External WebSocket listener for MQTT protocol -## listener.ws.$name is the IP address and port that the MQTT/WebSocket -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 8083, "127.0.0.1:8083", "::1:8083" -listener.ws.external.endpoint = 8083 +listener.ws.external { + ## $name is the IP address and port that the MQTT/WebSocket + ## listener will bind. + ## + ## Value: IP:Port | Port + ## + ## Examples: 8083, "127.0.0.1:8083", "::1:8083" + endpoint = 8083 -## The path of WebSocket MQTT endpoint -## -## Value: URL Path -listener.ws.external.mqtt_path = "/mqtt" + ## The path of WebSocket MQTT endpoint + ## + ## Value: URL Path + mqtt_path = "/mqtt" -## The acceptor pool for external MQTT/WebSocket listener. -## -## Value: Number -listener.ws.external.acceptors = 4 + ## The acceptor pool for external MQTT/WebSocket listener. + ## + ## Value: Number + acceptors = 4 -## Maximum number of concurrent MQTT/WebSocket connections. -## -## Value: Number -listener.ws.external.max_connections = 102400 + ## Maximum number of concurrent MQTT/WebSocket connections. + ## + ## Value: Number + max_connections = 102400 -## Maximum MQTT/WebSocket connections per second. -## -## Value: Number -listener.ws.external.max_conn_rate = 1000 + ## Maximum MQTT/WebSocket connections per second. + ## + ## Value: Number + max_conn_rate = 1000 -## Simulate the {active, N} option for the MQTT/WebSocket connections. -## -## Value: Number -listener.ws.external.active_n = 100 + ## Simulate the {active, N} option for the MQTT/WebSocket connections. + ## + ## Value: Number + active_n = 100 -## Zone of the external MQTT/WebSocket listener belonged to. -## -## Value: String -listener.ws.external.zone = external + ## Zone of the external MQTT/WebSocket listener belonged to. + ## + ## Value: String + zone = external -## The access control for the MQTT/WebSocket listener. -## -## See: listener.ws.$name.access -## -## Value: ACL Rule -listener.ws.external.access.1 = "allow all" + ## The access control for the MQTT/WebSocket listener. + ## + ## See: $name.access + ## + ## Value: ACL Rule + access.1 = "allow all" -## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. -## Set to false for WeChat MiniApp. -## -## Value: true | false -## listener.ws.external.fail_if_no_subprotocol = true + ## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. + ## Set to false for WeChat MiniApp. + ## + ## Value: true | false + ## fail_if_no_subprotocol = true -## Supported subprotocols -## -## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 -## listener.ws.external.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + ## Supported subprotocols + ## + ## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 + ## supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" -## Specify which HTTP header for real source IP if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-For -## listener.ws.external.proxy_address_header = X-Forwarded-For + ## Specify which HTTP header for real source IP if the EMQ X cluster is + ## deployed behind NGINX or HAProxy. + ## + ## Default: X-Forwarded-For + ## proxy_address_header = X-Forwarded-For -## Specify which HTTP header for real source port if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-Port -## listener.ws.external.proxy_port_header = X-Forwarded-Port + ## Specify which HTTP header for real source port if the EMQ X cluster is + ## deployed behind NGINX or HAProxy. + ## + ## Default: X-Forwarded-Port + ## proxy_port_header = X-Forwarded-Port -## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind -## HAProxy or Nginx. -## -## See: listener.ws.$name.proxy_protocol -## -## Value: on | off -## listener.ws.external.proxy_protocol = on + ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind + ## HAProxy or Nginx. + ## + ## See: $name.proxy_protocol + ## + ## Value: on | off + ## proxy_protocol = on -## Sets the timeout for proxy protocol. -## -## See: listener.ws.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.ws.external.proxy_protocol_timeout = 3s + ## Sets the timeout for proxy protocol. + ## + ## See: $name.proxy_protocol_timeout + ## + ## Value: Duration + ## proxy_protocol_timeout = 3s -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT username. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.ws.external.peer_cert_as_username = cn + ## Enable the option for X.509 certificate based authentication. + ## EMQX will use the common name of certificate as MQTT username. + ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info + ## + ## Value: cn + ## peer_cert_as_username = cn -## Enable the option for X.509 certificate based authentication. -## EMQX will use the common name of certificate as MQTT clientid. -## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info -## -## Value: cn -## listener.ws.external.peer_cert_as_clientid = cn + ## Enable the option for X.509 certificate based authentication. + ## EMQX will use the common name of certificate as MQTT clientid. + ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info + ## + ## Value: cn + ## peer_cert_as_clientid = cn -## The TCP backlog of external MQTT/WebSocket Listener. -## -## See: listener.ws.$name.backlog -## -## Value: Number >= 0 -listener.ws.external.backlog = 1024 + ## The TCP backlog of external MQTT/WebSocket Listener. + ## + ## See: $name.backlog + ## + ## Value: Number >= 0 + backlog = 1024 -## The TCP send timeout for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.send_timeout -## -## Value: Duration -listener.ws.external.send_timeout = 15s + ## The TCP send timeout for external MQTT/WebSocket connections. + ## + ## See: $name.send_timeout + ## + ## Value: Duration + send_timeout = 15s -## Close the MQTT/WebSocket connection if send timeout. -## -## See: listener.ws.$name.send_timeout_close -## -## Value: on | off -listener.ws.external.send_timeout_close = on + ## Close the MQTT/WebSocket connection if send timeout. + ## + ## See: $name.send_timeout_close + ## + ## Value: on | off + send_timeout_close = on -## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.recbuf -## -## Value: Bytes -## listener.ws.external.recbuf = 2KB + ## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. + ## + ## See: $name.recbuf + ## + ## Value: Bytes + ## recbuf = 2KB -## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.sndbuf -## -## Value: Bytes -## listener.ws.external.sndbuf = 2KB + ## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. + ## + ## See: $name.sndbuf + ## + ## Value: Bytes + ## sndbuf = 2KB -## The size of the user-level software buffer used by the driver. -## -## See: listener.ws.$name.buffer -## -## Value: Bytes -## listener.ws.external.buffer = 2KB + ## The size of the user-level software buffer used by the driver. + ## + ## See: $name.buffer + ## + ## Value: Bytes + ## buffer = 2KB -## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. -## -## See: listener.ws.$name.tune_buffer -## -## Value: on | off -## listener.ws.external.tune_buffer = off + ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## + ## See: $name.tune_buffer + ## + ## Value: on | off + ## tune_buffer = off -## The TCP_NODELAY flag for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.nodelay -## -## Value: true | false -listener.ws.external.nodelay = true + ## The TCP_NODELAY flag for external MQTT/WebSocket connections. + ## + ## See: $name.nodelay + ## + ## Value: true | false + nodelay = true -## The compress flag for external MQTT/WebSocket connections. -## -## If this Value is set true,the websocket message would be compressed -## -## Value: true | false -## listener.ws.external.compress = true + ## The compress flag for external MQTT/WebSocket connections. + ## + ## If this Value is set true,the websocket message would be compressed + ## + ## Value: true | false + ## compress = true -## The level of deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.level -## -## Value: none | default | best_compression | best_speed -## listener.ws.external.deflate_opts.level = default + ## The level of deflate options for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.level + ## + ## Value: none | default | best_compression | best_speed + ## deflate_opts.level = default -## The mem_level of deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.mem_level -## -## Valid range is 1-9 -## listener.ws.external.deflate_opts.mem_level = 8 + ## The mem_level of deflate options for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.mem_level + ## + ## Valid range is 1-9 + ## deflate_opts.mem_level = 8 -## The strategy of deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.strategy -## -## Value: default | filtered | huffman_only | rle -## listener.ws.external.deflate_opts.strategy = default + ## The strategy of deflate options for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.strategy + ## + ## Value: default | filtered | huffman_only | rle + ## deflate_opts.strategy = default -## The deflate option for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.server_context_takeover -## -## Value: takeover | no_takeover -## listener.ws.external.deflate_opts.server_context_takeover = takeover + ## The deflate option for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.server_context_takeover + ## + ## Value: takeover | no_takeover + ## deflate_opts.server_context_takeover = takeover -## The deflate option for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.client_context_takeover -## -## Value: takeover | no_takeover -## listener.ws.external.deflate_opts.client_context_takeover = takeover + ## The deflate option for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.client_context_takeover + ## + ## Value: takeover | no_takeover + ## deflate_opts.client_context_takeover = takeover -## The deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.server_max_window_bits -## -## Valid range is 8-15 -## listener.ws.external.deflate_opts.server_max_window_bits = 15 + ## The deflate options for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.server_max_window_bits + ## + ## Valid range is 8-15 + ## deflate_opts.server_max_window_bits = 15 -## The deflate options for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.deflate_opts.client_max_window_bits -## -## Valid range is 8-15 -## listener.ws.external.deflate_opts.client_max_window_bits = 15 + ## The deflate options for external MQTT/WebSocket connections. + ## + ## See: $name.deflate_opts.client_max_window_bits + ## + ## Valid range is 8-15 + ## deflate_opts.client_max_window_bits = 15 -## The idle timeout for external MQTT/WebSocket connections. -## -## See: listener.ws.$name.idle_timeout -## -## Value: Duration -## listener.ws.external.idle_timeout = 60s + ## The idle timeout for external MQTT/WebSocket connections. + ## + ## See: $name.idle_timeout + ## + ## Value: Duration + ## idle_timeout = 60s -## The max frame size for external MQTT/WebSocket connections. -## -## -## Value: Number -## listener.ws.external.max_frame_size = 0 + ## The max frame size for external MQTT/WebSocket connections. + ## + ## + ## Value: Number + ## max_frame_size = 0 -## Whether a WebSocket message is allowed to contain multiple MQTT packets -## -## Value: single | multiple -listener.ws.external.mqtt_piggyback = multiple + ## Whether a WebSocket message is allowed to contain multiple MQTT packets + ## + ## Value: single | multiple + mqtt_piggyback = multiple -## By default, EMQX web socket connection does not restrict connections to specific origins. -## It also, by default, does not enforce the presence of origin in request headers for WebSocket connections. -## Because of this, a malicious user could potentially hijack an existing web-socket connection to EMQX. + ## By default, EMQX web socket connection does not restrict connections to specific origins. + ## It also, by default, does not enforce the presence of origin in request headers for WebSocket connections. + ## Because of this, a malicious user could potentially hijack an existing web-socket connection to EMQX. -## To prevent this, users can set allowed origin headers in their ws connection to EMQX. -## WS configs are set in listener.ws.external.* -## WSS configs are set in listener.wss.external.* + ## To prevent this, users can set allowed origin headers in their ws connection to EMQX. + ## Example for WS connection + ## To enables origin check in header for websocket connnection, + ## set `check_origin_enable = true`. By default it is false, + ## When it is set to true and no origin is present in the header of a ws connection request, the request fails. -## Example for WS connection -## To enables origin check in header for websocket connnection, -## set `listener.ws.external.check_origin_enable = true`. By default it is false, -## When it is set to true and no origin is present in the header of a ws connection request, the request fails. + ## To allow origins to be absent in header in the websocket connection when check_origin_enable is true, + ## set `allow_origin_absence = true` -## To allow origins to be absent in header in the websocket connection when check_origin_enable is true, -## set `listener.ws.external.allow_origin_absence = true` + ## Enabling origin check implies there are specific valid origins allowed for ws connection. + ## To set the list of allowed origins in header for websocket connection + ## check_origins = http://localhost:18083(localhost dashboard url), http://yourapp.com` + ## check_origins config allows a comma separated list of origins so you can specify as many origins are you want. + ## With these configs, you can allow only connections from only authorized origins to your broker -## Enabling origin check implies there are specific valid origins allowed for ws connection. -## To set the list of allowed origins in header for websocket connection -## listener.ws.external.check_origins = http://localhost:18083(localhost dashboard url), http://yourapp.com` -## check_origins config allows a comma separated list of origins so you can specify as many origins are you want. -## With these configs, you can allow only connections from only authorized origins to your broker + ## Enable origin check in header for websocket connection + ## + ## Value: true | false (default false) + check_origin_enable = false -## Enable origin check in header for websocket connection -## -## Value: true | false (default false) -listener.ws.external.check_origin_enable = false + ## Allow origin to be absent in header in websocket connection when check_origin_enable is true + ## + ## Value: true | false (default true) + allow_origin_absence = true -## Allow origin to be absent in header in websocket connection when check_origin_enable is true -## -## Value: true | false (default true) -listener.ws.external.allow_origin_absence = true - -## Comma separated list of allowed origin in header for websocket connection -## -## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 -listener.ws.external.check_origins = "http://localhost:18083, http://127.0.0.1:18083" + ## Comma separated list of allowed origin in header for websocket connection + ## + ## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 + check_origins = "http://localhost:18083, http://127.0.0.1:18083" +} ##-------------------------------------------------------------------- ## External WebSocket/SSL listener for MQTT Protocol +listener.wss.external { + ## listener.wss.$name.endpoint is the IP address and port that the MQTT/WebSocket/SSL + ## listener will bind. + ## + ## Value: IP:Port | Port + ## + ## Examples: 8084, "127.0.0.1:8084", "::1:8084" + endpoint = 8084 -## listener.wss.$name.endpoint is the IP address and port that the MQTT/WebSocket/SSL -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 8084, "127.0.0.1:8084", "::1:8084" -listener.wss.external.endpoint = 8084 + ## The path of WebSocket MQTT endpoint + ## + ## Value: URL Path + mqtt_path = "/mqtt" -## The path of WebSocket MQTT endpoint -## -## Value: URL Path -listener.wss.external.mqtt_path = "/mqtt" + ## The acceptor pool for external MQTT/WebSocket/SSL listener. + ## + ## Value: Number + acceptors = 4 -## The acceptor pool for external MQTT/WebSocket/SSL listener. -## -## Value: Number -listener.wss.external.acceptors = 4 + ## Maximum number of concurrent MQTT/Webwocket/SSL connections. + ## + ## Value: Number + max_connections = 16 -## Maximum number of concurrent MQTT/Webwocket/SSL connections. -## -## Value: Number -listener.wss.external.max_connections = 16 + ## Maximum MQTT/WebSocket/SSL connections per second. + ## + ## See: listener.tcp.$name.max_conn_rate + ## + ## Value: Number + max_conn_rate = 1000 -## Maximum MQTT/WebSocket/SSL connections per second. -## -## See: listener.tcp.$name.max_conn_rate -## -## Value: Number -listener.wss.external.max_conn_rate = 1000 + ## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. + ## + ## Value: Number + active_n = 100 -## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. -## -## Value: Number -listener.wss.external.active_n = 100 + ## Zone of the external MQTT/WebSocket/SSL listener belonged to. + ## + ## Value: String + zone = external -## Zone of the external MQTT/WebSocket/SSL listener belonged to. -## -## Value: String -listener.wss.external.zone = external + ## The access control rules for the MQTT/WebSocket/SSL listener. + ## + ## See: listener.tcp.$name.access. + ## + ## Value: ACL Rule + access.1 = "allow all" -## The access control rules for the MQTT/WebSocket/SSL listener. -## -## See: listener.tcp.$name.access. -## -## Value: ACL Rule -listener.wss.external.access.1 = "allow all" + ## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. + ## Set to false for WeChat MiniApp. + ## + ## Value: true | false + ## fail_if_no_subprotocol = true -## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. -## Set to false for WeChat MiniApp. -## -## Value: true | false -## listener.wss.external.fail_if_no_subprotocol = true + ## Supported subprotocols + ## + ## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 + ## supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" -## Supported subprotocols -## -## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 -## listener.wss.external.supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + ## Specify which HTTP header for real source IP if the EMQ X cluster is + ## deployed behind NGINX or HAProxy. + ## + ## Default: X-Forwarded-For + ## proxy_address_header = X-Forwarded-For -## Specify which HTTP header for real source IP if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-For -## listener.wss.external.proxy_address_header = X-Forwarded-For + ## Specify which HTTP header for real source port if the EMQ X cluster is + ## deployed behind NGINX or HAProxy. + ## + ## Default: X-Forwarded-Port + ## proxy_port_header = X-Forwarded-Port -## Specify which HTTP header for real source port if the EMQ X cluster is -## deployed behind NGINX or HAProxy. -## -## Default: X-Forwarded-Port -## listener.wss.external.proxy_port_header = X-Forwarded-Port + ## Enable the Proxy Protocol V1/2 support. + ## + ## See: listener.tcp.$name.proxy_protocol + ## + ## Value: on | off + ## proxy_protocol = on -## Enable the Proxy Protocol V1/2 support. -## -## See: listener.tcp.$name.proxy_protocol -## -## Value: on | off -## listener.wss.external.proxy_protocol = on + ## Sets the timeout for proxy protocol. + ## + ## See: listener.tcp.$name.proxy_protocol_timeout + ## + ## Value: Duration + ## proxy_protocol_timeout = 3s -## Sets the timeout for proxy protocol. -## -## See: listener.tcp.$name.proxy_protocol_timeout -## -## Value: Duration -## listener.wss.external.proxy_protocol_timeout = 3s + ## TLS versions only to protect from POODLE attack. + ## + ## See: listener.ssl.$name.tls_versions + ## + ## Value: String, seperated by ',' + ## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier + ## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" -## TLS versions only to protect from POODLE attack. -## -## See: listener.ssl.$name.tls_versions -## -## Value: String, seperated by ',' -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## listener.wss.external.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" + ## Path to the file containing the user's private PEM-encoded key. + ## + ## See: listener.ssl.$name.keyfile + ## + ## Value: File + keyfile = "{{ platform_etc_dir }}/certs/key.pem" -## Path to the file containing the user's private PEM-encoded key. -## -## See: listener.ssl.$name.keyfile -## -## Value: File -listener.wss.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ## Path to a file containing the user certificate. + ## + ## See: listener.ssl.$name.certfile + ## + ## Value: File + certfile = "{{ platform_etc_dir }}/certs/cert.pem" -## Path to a file containing the user certificate. -## -## See: listener.ssl.$name.certfile -## -## Value: File -listener.wss.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ## Path to the file containing PEM-encoded CA certificates. + ## + ## See: listener.ssl.$name.cacert + ## + ## Value: File + ## cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" -## Path to the file containing PEM-encoded CA certificates. -## -## See: listener.ssl.$name.cacert -## -## Value: File -## listener.wss.external.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + ## Maximum number of non-self-issued intermediate certificates that + ## can follow the peer certificate in a valid certification path. + ## + ## See: listener.ssl.external.depth + ## + ## Value: Number + ## depth = 10 -## Maximum number of non-self-issued intermediate certificates that -## can follow the peer certificate in a valid certification path. -## -## See: listener.ssl.external.depth -## -## Value: Number -## listener.wss.external.depth = 10 + ## String containing the user's password. Only used if the private keyfile + ## is password-protected. + ## + ## See: listener.ssl.$name.key_password + ## + ## Value: String + ## key_password = yourpass -## String containing the user's password. Only used if the private keyfile -## is password-protected. -## -## See: listener.ssl.$name.key_password -## -## Value: String -## listener.wss.external.key_password = yourpass + ## See: listener.ssl.$name.dhfile + ## + ## Value: File + ## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" -## See: listener.ssl.$name.dhfile -## -## Value: File -## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" + ## See: listener.ssl.$name.verify + ## + ## Value: verify_peer | verify_none + ## verify = verify_peer -## See: listener.ssl.$name.verify -## -## Value: verify_peer | verify_none -## listener.wss.external.verify = verify_peer + ## See: listener.ssl.$name.fail_if_no_peer_cert + ## + ## Value: false | true + ## fail_if_no_peer_cert = true -## See: listener.ssl.$name.fail_if_no_peer_cert -## -## Value: false | true -## listener.wss.external.fail_if_no_peer_cert = true + ## See: listener.ssl.$name.ciphers + ## + ## Value: Ciphers + ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## See: listener.ssl.$name.ciphers -## -## Value: Ciphers -listener.wss.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" + ## Ciphers for TLS PSK. + ## Note that 'ciphers' and 'psk_ciphers' cannot + ## be configured at the same time. + ## See 'https://tools.ietf.org/html/rfc4279#section-2'. + ## psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" -## Ciphers for TLS PSK. -## Note that 'listener.wss.external.ciphers' and 'listener.wss.external.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -## listener.wss.external.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" + ## See: listener.ssl.$name.secure_renegotiate + ## + ## Value: on | off + ## secure_renegotiate = off -## See: listener.ssl.$name.secure_renegotiate -## -## Value: on | off -## listener.wss.external.secure_renegotiate = off + ## See: listener.ssl.$name.reuse_sessions + ## + ## Value: on | off + ## reuse_sessions = on -## See: listener.ssl.$name.reuse_sessions -## -## Value: on | off -## listener.wss.external.reuse_sessions = on + ## See: listener.ssl.$name.honor_cipher_order + ## + ## Value: on | off + ## honor_cipher_order = on -## See: listener.ssl.$name.honor_cipher_order -## -## Value: on | off -## listener.wss.external.honor_cipher_order = on + ## See: listener.ssl.$name.peer_cert_as_username + ## + ## Value: cn | dn | crt | pem | md5 + ## peer_cert_as_username = cn -## See: listener.ssl.$name.peer_cert_as_username -## -## Value: cn | dn | crt | pem | md5 -## listener.wss.external.peer_cert_as_username = cn + ## See: listener.ssl.$name.peer_cert_as_clientid + ## + ## Value: cn | dn | crt | pem | md5 + ## peer_cert_as_clientid = cn -## See: listener.ssl.$name.peer_cert_as_clientid -## -## Value: cn | dn | crt | pem | md5 -## listener.wss.external.peer_cert_as_clientid = cn + ## TCP backlog for the WebSocket/SSL connection. + ## + ## See: listener.tcp.$name.backlog + ## + ## Value: Number >= 0 + backlog = 1024 -## TCP backlog for the WebSocket/SSL connection. -## -## See: listener.tcp.$name.backlog -## -## Value: Number >= 0 -listener.wss.external.backlog = 1024 + ## The TCP send timeout for the WebSocket/SSL connection. + ## + ## See: listener.tcp.$name.send_timeout + ## + ## Value: Duration + send_timeout = 15s -## The TCP send timeout for the WebSocket/SSL connection. -## -## See: listener.tcp.$name.send_timeout -## -## Value: Duration -listener.wss.external.send_timeout = 15s + ## Close the WebSocket/SSL connection if send timeout. + ## + ## See: listener.tcp.$name.send_timeout_close + ## + ## Value: on | off + send_timeout_close = on -## Close the WebSocket/SSL connection if send timeout. -## -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -listener.wss.external.send_timeout_close = on + ## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. + ## + ## See: listener.tcp.$name.recbuf + ## + ## Value: Bytes + ## recbuf = 4KB -## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. -## -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -## listener.wss.external.recbuf = 4KB + ## The TCP send buffer(os kernel) for the WebSocket/SSL connections. + ## + ## See: listener.tcp.$name.sndbuf + ## + ## Value: Bytes + ## sndbuf = 4KB -## The TCP send buffer(os kernel) for the WebSocket/SSL connections. -## -## See: listener.tcp.$name.sndbuf -## -## Value: Bytes -## listener.wss.external.sndbuf = 4KB + ## The size of the user-level software buffer used by the driver. + ## + ## See: listener.tcp.$name.buffer + ## + ## Value: Bytes + ## buffer = 4KB -## The size of the user-level software buffer used by the driver. -## -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.wss.external.buffer = 4KB + ## The TCP_NODELAY flag for WebSocket/SSL connections. + ## + ## See: listener.tcp.$name.nodelay + ## + ## Value: true | false + ## nodelay = true -## The TCP_NODELAY flag for WebSocket/SSL connections. -## -## See: listener.tcp.$name.nodelay -## -## Value: true | false -## listener.wss.external.nodelay = true + ## The compress flag for external WebSocket/SSL connections. + ## + ## If this Value is set true,the websocket message would be compressed + ## + ## Value: true | false + ## compress = true -## The compress flag for external WebSocket/SSL connections. -## -## If this Value is set true,the websocket message would be compressed -## -## Value: true | false -## listener.wss.external.compress = true + ## The level of deflate options for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.level + ## + ## Value: none | default | best_compression | best_speed + ## deflate_opts.level = default -## The level of deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.level -## -## Value: none | default | best_compression | best_speed -## listener.wss.external.deflate_opts.level = default + ## The mem_level of deflate options for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.mem_level + ## + ## Valid range is 1-9 + ## deflate_opts.mem_level = 8 -## The mem_level of deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.mem_level -## -## Valid range is 1-9 -## listener.wss.external.deflate_opts.mem_level = 8 + ## The strategy of deflate options for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.strategy + ## + ## Value: default | filtered | huffman_only | rle + ## deflate_opts.strategy = default -## The strategy of deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.strategy -## -## Value: default | filtered | huffman_only | rle -## listener.wss.external.deflate_opts.strategy = default + ## The deflate option for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.server_context_takeover + ## + ## Value: takeover | no_takeover + ## deflate_opts.server_context_takeover = takeover -## The deflate option for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.server_context_takeover -## -## Value: takeover | no_takeover -## listener.wss.external.deflate_opts.server_context_takeover = takeover + ## The deflate option for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.client_context_takeover + ## + ## Value: takeover | no_takeover + ## deflate_opts.client_context_takeover = takeover -## The deflate option for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.client_context_takeover -## -## Value: takeover | no_takeover -## listener.wss.external.deflate_opts.client_context_takeover = takeover + ## The deflate options for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.server_max_window_bits + ## + ## Valid range is 8-15 + ## deflate_opts.server_max_window_bits = 15 -## The deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.server_max_window_bits -## -## Valid range is 8-15 -## listener.wss.external.deflate_opts.server_max_window_bits = 15 + ## The deflate options for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.deflate_opts.client_max_window_bits + ## + ## Valid range is 8-15 + ## deflate_opts.client_max_window_bits = 15 -## The deflate options for external WebSocket/SSL connections. -## -## See: listener.wss.$name.deflate_opts.client_max_window_bits -## -## Valid range is 8-15 -## listener.wss.external.deflate_opts.client_max_window_bits = 15 + ## The idle timeout for external WebSocket/SSL connections. + ## + ## See: listener.wss.$name.idle_timeout + ## + ## Value: Duration + ## idle_timeout = 60s -## The idle timeout for external WebSocket/SSL connections. -## -## See: listener.wss.$name.idle_timeout -## -## Value: Duration -## listener.wss.external.idle_timeout = 60s - -## The max frame size for external WebSocket/SSL connections. -## -## Value: Number -## listener.wss.external.max_frame_size = 0 - -## Whether a WebSocket message is allowed to contain multiple MQTT packets -## -## Value: single | multiple -listener.wss.external.mqtt_piggyback = multiple -## Enable origin check in header for secure websocket connection -## -## Value: true | false (default false) -listener.wss.external.check_origin_enable = false -## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true -## -## Value: true | false (default true) -listener.wss.external.allow_origin_absence = true -## Comma separated list of allowed origin in header for secure websocket connection -## -## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 -listener.wss.external.check_origins = "https://localhost:8084, https://127.0.0.1:8084" + ## The max frame size for external WebSocket/SSL connections. + ## + ## Value: Number + ## max_frame_size = 0 + ## Whether a WebSocket message is allowed to contain multiple MQTT packets + ## + ## Value: single | multiple + mqtt_piggyback = multiple + ## Enable origin check in header for secure websocket connection + ## + ## Value: true | false (default false) + check_origin_enable = false + ## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true + ## + ## Value: true | false (default true) + allow_origin_absence = true + ## Comma separated list of allowed origin in header for secure websocket connection + ## + ## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 + check_origins = "https://localhost:8084, https://127.0.0.1:8084" +} ##-------------------------------------------------------------------- ## External QUIC listener for MQTT Protocol -## listener.quic.$name.endpoint is the IP address and port that the MQTT/QUIC -## listener will bind. -## -## Value: IP:Port | Port -## -## Examples: 14567, 127.0.0.1:14567, ::1:14567 -listener.quic.external.endpoint = 14567 +listener.quic.external { + ## listener.quic.$name.endpoint is the IP address and port that the MQTT/QUIC + ## listener will bind. + ## + ## Value: IP:Port | Port + ## + ## Examples: 14567, 127.0.0.1:14567, ::1:14567 + endpoint = 14567 -## The acceptor pool for external MQTT/QUIC listener. -## -## Value: Number -listener.quic.external.acceptors = 4 + ## The acceptor pool for external MQTT/QUIC listener. + ## + ## Value: Number + acceptors = 4 -## Maximum number of concurrent MQTT/Webwocket/SSL connections. -## -## Value: Number -listener.quic.external.max_connections = 16 + ## Maximum number of concurrent MQTT/Webwocket/SSL connections. + ## + ## Value: Number + max_connections = 16 -## Maximum MQTT/QUIC connections per second. -## -## See: listener.tcp.$name.max_conn_rate -## -## Value: Number -listener.quic.external.max_conn_rate = 1000 + ## Maximum MQTT/QUIC connections per second. + ## + ## See: listener.tcp.$name.max_conn_rate + ## + ## Value: Number + max_conn_rate = 1000 -## Simulate the {active, N} option for the MQTT/QUIC connections. -## @todo -## Value: Number -## listener.quic.external.active_n = 100 + ## Simulate the {active, N} option for the MQTT/QUIC connections. + ## @todo + ## Value: Number + ## active_n = 100 -## Zone of the external MQTT/QUIC listener belonged to. -## -## Value: String -listener.quic.external.zone = external + ## Zone of the external MQTT/QUIC listener belonged to. + ## + ## Value: String + zone = external -## Path to the file containing the user's private PEM-encoded key. -## -## See: listener.ssl.$name.keyfile -## -## Value: File -listener.quic.external.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ## Path to the file containing the user's private PEM-encoded key. + ## + ## See: listener.ssl.$name.keyfile + ## + ## Value: File + keyfile = "{{ platform_etc_dir }}/certs/key.pem" -## Path to a file containing the user certificate. -## -## See: listener.ssl.$name.certfile -## -## Value: File -listener.quic.external.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ## Path to a file containing the user certificate. + ## + ## See: listener.ssl.$name.certfile + ## + ## Value: File + certfile = "{{ platform_etc_dir }}/certs/cert.pem" -## Path to the file containing PEM-encoded CA certificates. -## @todo -## See: listener.ssl.$name.cacert -## -## Value: File -## listener.quic.external.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem + ## Path to the file containing PEM-encoded CA certificates. + ## @todo + ## See: listener.ssl.$name.cacert + ## + ## Value: File + ## cacertfile = {{ platform_etc_dir }}/certs/cacert.pem -## String containing the user's password. Only used if the private keyfile -## is password-protected. -## @todo -## See: listener.ssl.$name.key_password -## -## Value: String -## listener.quic.external.key_password = yourpass + ## String containing the user's password. Only used if the private keyfile + ## is password-protected. + ## @todo + ## See: listener.ssl.$name.key_password + ## + ## Value: String + ## key_password = yourpass -## See: listener.ssl.$name.verify -## @todo -## Value: verify_peer | verify_none -## listener.quic.external.verify = verify_peer + ## See: listener.ssl.$name.verify + ## @todo + ## Value: verify_peer | verify_none + ## verify = verify_peer -## See: listener.ssl.$name.fail_if_no_peer_cert -## @todo -## Value: false | true -## listener.quic.external.fail_if_no_peer_cert = true + ## See: listener.ssl.$name.fail_if_no_peer_cert + ## @todo + ## Value: false | true + ## fail_if_no_peer_cert = true -## See: listener.ssl.$name.ciphers -## @todo -## Value: Ciphers -listener.quic.external.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256" + ## See: listener.ssl.$name.ciphers + ## @todo + ## Value: Ciphers + ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256" -## Ciphers for TLS PSK. -## @todo -## Note that 'listener.quic.external.ciphers' and 'listener.quic.external.psk_ciphers' cannot -## be configured at the same time. -## See 'https://tools.ietf.org/html/rfc4279#section-2'. -## listener.quic.external.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA + ## Ciphers for TLS PSK. + ## @todo + ## Note that 'ciphers' and 'psk_ciphers' cannot + ## be configured at the same time. + ## See 'https://tools.ietf.org/html/rfc4279#section-2'. + ## psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA -## See: listener.ssl.$name.honor_cipher_order -## @todo -## Value: on | off -## listener.quic.external.honor_cipher_order = on + ## See: listener.ssl.$name.honor_cipher_order + ## @todo + ## Value: on | off + ## honor_cipher_order = on -## The send timeout for the QUIC stream. -## @todo -## -## Value: Duration -# listener.quic.external.send_timeout = 15s + ## The send timeout for the QUIC stream. + ## @todo + ## + ## Value: Duration + # send_timeout = 15s -## Close the QUIC connection if send timeout. -## @todo -## See: listener.tcp.$name.send_timeout_close -## -## Value: on | off -## listener.quic.external.send_timeout_close = on + ## Close the QUIC connection if send timeout. + ## @todo + ## See: listener.tcp.$name.send_timeout_close + ## + ## Value: on | off + ## send_timeout_close = on -## The receive buffer for the QUIC connections. -## @todo -## See: listener.tcp.$name.recbuf -## -## Value: Bytes -## listener.quic.external.recbuf = 4KB + ## The receive buffer for the QUIC connections. + ## @todo + ## See: listener.tcp.$name.recbuf + ## + ## Value: Bytes + ## recbuf = 4KB -## The TCP send buffer(os kernel) for the QUIC connections. -## @todo -## See: listener.tcp.$name.sndbuf -## -## Value: Bytes -## listener.quic.external.sndbuf = 4KB + ## The TCP send buffer(os kernel) for the QUIC connections. + ## @todo + ## See: listener.tcp.$name.sndbuf + ## + ## Value: Bytes + ## sndbuf = 4KB -## The size of the user-level software buffer used by the driver. -## @todo -## See: listener.tcp.$name.buffer -## -## Value: Bytes -## listener.quic.external.buffer = 4KB + ## The size of the user-level software buffer used by the driver. + ## @todo + ## See: listener.tcp.$name.buffer + ## + ## Value: Bytes + ## buffer = 4KB -## The idle timeout for external QUIC connections. -## @todo -## See: listener.quic.$name.idle_timeout -## -## Value: Duration -## listener.quic.external.idle_timeout = 60s + ## The idle timeout for external QUIC connections. + ## @todo + ## See: listener.quic.$name.idle_timeout + ## + ## Value: Duration + ## idle_timeout = 60s -## The max frame size for external QUIC connections. -## @todo -## Value: Number -## listener.quic.external.max_frame_size = 0 - -## CONFIG_SECTION_END=listeners ================================================ + ## The max frame size for external QUIC connections. + ## @todo + ## Value: Number + ## max_frame_size = 0 +} ## CONFIG_SECTION_BGN=modules ================================================== From c7cbe819ed51c8ddcd56fd41f0c63bf14eac92e1 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 2 Jul 2021 13:54:37 +0800 Subject: [PATCH 064/379] feat(hocon): convert config format of rpc and log to hocon --- apps/emqx/etc/emqx.conf | 503 ++++++++++++++++++++-------------------- 1 file changed, 253 insertions(+), 250 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 54b4b1078..86b9566ba 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -316,293 +316,296 @@ node: { backtrace_depth: 16 } +##-------------------------------------------------------------------- +## RPC +##-------------------------------------------------------------------- -## CONFIG_SECTION_BGN=rpc ====================================================== +rpc: { + ## RPC Mode. + ## + ## Value: sync | async + mode: async -## RPC Mode. -## -## Value: sync | async -rpc.mode = async + ## Max batch size of async RPC requests. + ## + ## Value: Integer + ## Zero or negative value disables rpc batching. + ## + ## NOTE: RPC batch won't work when rpc.mode = sync + async_batch_size: 256 -## Max batch size of async RPC requests. -## -## Value: Integer -## Zero or negative value disables rpc batching. -## -## NOTE: RPC batch won't work when rpc.mode = sync -rpc.async_batch_size = 256 + ## RPC port discovery + ## + ## The strategy for discovering the RPC listening port of other nodes. + ## + ## Value: Enum + ## - manual: discover ports by `tcp_server_port` and `tcp_client_port`. + ## - stateless: discover ports in a stateless manner. + ## If node name is `emqx@127.0.0.1`, where the `` is an integer, + ## then the listening port will be `5370 + ` + ## + ## Defaults to `stateless`. + port_discovery: stateless -## RPC port discovery -## -## The strategy for discovering the RPC listening port of other nodes. -## -## Value: Enum -## - manual: discover ports by `tcp_server_port` and `tcp_client_port`. -## - stateless: discover ports in a stateless manner. -## If node name is `emqx@127.0.0.1`, where the `` is an integer, -## then the listening port will be `5370 + ` -## -## Defaults to `stateless`. -rpc.port_discovery = stateless + ## TCP port number for RPC server to listen on. + ## + ## Only takes effect when `rpc.port_discovery` = `manual`. + ## + ## NOTE: All nodes in the cluster should agree to this same config. + ## + ## Value: Port [1024-65535] + ## tcp_server_port: 5369 -## TCP port number for RPC server to listen on. -## -## Only takes effect when `rpc.port_discovery` = `manual`. -## -## NOTE: All nodes in the cluster should agree to this same config. -## -## Value: Port [1024-65535] -#rpc.tcp_server_port = 5369 + ## Number of outgoing RPC connections. + ## + ## Value: Interger [0-256] + ## Default: 1 + ## tcp_client_num: 1 -## Number of outgoing RPC connections. -## -## Value: Interger [0-256] -## Default = 1 -#rpc.tcp_client_num = 1 + ## RCP Client connect timeout. + ## + ## Value: Seconds + connect_timeout: 5s -## RCP Client connect timeout. -## -## Value: Seconds -rpc.connect_timeout = 5s + ## TCP send timeout of RPC client and server. + ## + ## Value: Seconds + send_timeout: 5s -## TCP send timeout of RPC client and server. -## -## Value: Seconds -rpc.send_timeout = 5s + ## Authentication timeout + ## + ## Value: Seconds + authentication_timeout: 5s -## Authentication timeout -## -## Value: Seconds -rpc.authentication_timeout = 5s + ## Default receive timeout for call() functions + ## + ## Value: Seconds + call_receive_timeout: 15s -## Default receive timeout for call() functions -## -## Value: Seconds -rpc.call_receive_timeout = 15s + ## Socket idle keepalive. + ## + ## Value: Seconds + socket_keepalive_idle: 900s -## Socket idle keepalive. -## -## Value: Seconds -rpc.socket_keepalive_idle = 900s + ## TCP Keepalive probes interval. + ## + ## Value: Seconds + socket_keepalive_interval: 75s -## TCP Keepalive probes interval. -## -## Value: Seconds -rpc.socket_keepalive_interval = 75s + ## Probes lost to close the connection + ## + ## Value: Integer + socket_keepalive_count: 9 -## Probes lost to close the connection -## -## Value: Integer -rpc.socket_keepalive_count = 9 + ## Size of TCP send buffer. + ## + ## Value: Bytes + socket_sndbuf: 1MB -## Size of TCP send buffer. -## -## Value: Bytes -rpc.socket_sndbuf = 1MB + ## Size of TCP receive buffer. + ## + ## Value: Seconds + socket_recbuf: 1MB -## Size of TCP receive buffer. -## -## Value: Seconds -rpc.socket_recbuf = 1MB + ## Size of user-level software socket buffer. + ## + ## Value: Seconds + socket_buffer: 1MB +} -## Size of user-level software socket buffer. -## -## Value: Seconds -rpc.socket_buffer = 1MB +##-------------------------------------------------------------------- +## Log +##-------------------------------------------------------------------- -## CONFIG_SECTION_END=rpc ====================================================== +log: { + ## Where to emit the logs. + ## Enable the console (standard output) logs. + ## + ## Value: file | console | both + ## - file: write logs only to file + ## - console: write logs only to standard I/O + ## - both: write logs both to file and standard I/O + to: file -## CONFIG_SECTION_BGN=logger =================================================== + ## The log severity level. + ## + ## Value: debug | info | notice | warning | error | critical | alert | emergency + ## + ## Note: Only the messages with severity level higher than or equal to + ## this level will be logged. + ## + ## Default: warning + level: warning -## Where to emit the logs. -## Enable the console (standard output) logs. -## -## Value: file | console | both -## - file: write logs only to file -## - console: write logs only to standard I/O -## - both: write logs both to file and standard I/O -log.to = file + ## Timezone offset to display in logs + ## Value: + ## - "system" use system zone + ## - "utc" for Universal Coordinated Time (UTC) + ## - "+hh:mm" or "-hh:mm" for a specified offset + time_offset: system -## The log severity level. -## -## Value: debug | info | notice | warning | error | critical | alert | emergency -## -## Note: Only the messages with severity level higher than or equal to -## this level will be logged. -## -## Default: warning -log.level = warning + ## The dir for log files. + ## + ## Value: Folder + dir: "{{ platform_log_dir }}" -## Timezone offset to display in logs -## Value: -## - "system" use system zone -## - "utc" for Universal Coordinated Time (UTC) -## - "+hh:mm" or "-hh:mm" for a specified offset -log.time_offset = system + ## The log filename for logs of level specified in "log.level". + ## + ## If `log.rotation` is enabled, this is the base name of the + ## files. Each file in a rotated log is named .N, where N is an integer. + ## + ## Value: String + ## Default: emqx.log + file: emqx.log -## The dir for log files. -## -## Value: Folder -log.dir = "{{ platform_log_dir }}" + ## Limits the total number of characters printed for each log event. + ## + ## Value: Integer + ## Default: No Limit + ## chars_limit: 8192 -## The log filename for logs of level specified in "log.level". -## -## If `log.rotation` is enabled, this is the base name of the -## files. Each file in a rotated log is named .N, where N is an integer. -## -## Value: String -## Default: emqx.log -log.file = emqx.log + ## Maximum depth for Erlang term log formatting + ## and Erlang process message queue inspection. + ## + ## Value: Integer or 'unlimited' (without quotes) + ## Default: 80 + ## max_depth: 80 -## Limits the total number of characters printed for each log event. -## -## Value: Integer -## Default: No Limit -#log.chars_limit = 8192 + ## Log formatter + ## Value: text | json + ## formatter: text -## Maximum depth for Erlang term log formatting -## and Erlang process message queue inspection. -## -## Value: Integer or 'unlimited' (without quotes) -## Default: 80 -#log.max_depth = 80 + ## Log to single line + ## Value: Boolean + ## single_line: true -## Log formatter -## Value: text | json -#log.formatter = text + ## Enables the log rotation. + ## With this enabled, new log files will be created when the current + ## log file is full, max to `rotation.size` files will be created. + ## + ## Value: on | off + ## Default: on + rotation.enable: on -## Log to single line -## Value: Boolean -#log.single_line = true + ## Maximum size of each log file. + ## + ## Value: Number + ## Default: 10M + ## Supported Unit: KB | MB | GB + rotation.size: 10MB -## Enables the log rotation. -## With this enabled, new log files will be created when the current -## log file is full, max to `log.rotation.size` files will be created. -## -## Value: on | off -## Default: on -log.rotation.enable = on + ## Maximum rotation count of log files. + ## + ## Value: Number + ## Default: 5 + rotation.count: 5 -## Maximum size of each log file. -## -## Value: Number -## Default: 10M -## Supported Unit: KB | MB | GB -log.rotation.size = 10MB + ## To create additional log files for specific log levels. + ## + ## Value: File Name + ## Format: log.$level.file = $filename, + ## where "$level" can be one of: debug, info, notice, warning, + ## error, critical, alert, emergency + ## Note: Log files for a specific log level will only contain all the logs + ## that higher than or equal to that level + ## + ## info.file: info.log + ## error.file: error.log -## Maximum rotation count of log files. -## -## Value: Number -## Default: 5 -log.rotation.count = 5 + ## The max allowed queue length before switching to sync mode. + ## + ## Log overload protection parameter. If the message queue grows + ## larger than this value the handler switches from anync to sync mode. + ## + ## Default: 100 + ## + ## sync_mode_qlen: 100 -## To create additional log files for specific log levels. -## -## Value: File Name -## Format: log.$level.file = $filename, -## where "$level" can be one of: debug, info, notice, warning, -## error, critical, alert, emergency -## Note: Log files for a specific log level will only contain all the logs -## that higher than or equal to that level -## -#log.info.file = info.log -#log.error.file = error.log + ## The max allowed queue length before switching to drop mode. + ## + ## Log overload protection parameter. When the message queue grows + ## larger than this threshold, the handler switches to a mode in which + ## it drops all new events that senders want to log. + ## + ## Default: 3000 + ## + ## drop_mode_qlen: 3000 -## The max allowed queue length before switching to sync mode. -## -## Log overload protection parameter. If the message queue grows -## larger than this value the handler switches from anync to sync mode. -## -## Default: 100 -## -#log.sync_mode_qlen = 100 + ## The max allowed queue length before switching to flush mode. + ## + ## Log overload protection parameter. If the length of the message queue + ## grows larger than this threshold, a flush (delete) operation takes place. + ## To flush events, the handler discards the messages in the message queue + ## by receiving them in a loop without logging. + ## + ## Default: 8000 + ## + ## flush_qlen: 8000 -## The max allowed queue length before switching to drop mode. -## -## Log overload protection parameter. When the message queue grows -## larger than this threshold, the handler switches to a mode in which -## it drops all new events that senders want to log. -## -## Default: 3000 -## -#log.drop_mode_qlen = 3000 + ## Kill the log handler when it gets overloaded. + ## + ## Log overload protection parameter. It is possible that a handler, + ## even if it can successfully manage peaks of high load without crashing, + ## can build up a large message queue, or use a large amount of memory. + ## We could kill the log handler in these cases and restart it after a + ## few seconds. + ## + ## Default: on + ## + ## overload_kill: on -## The max allowed queue length before switching to flush mode. -## -## Log overload protection parameter. If the length of the message queue -## grows larger than this threshold, a flush (delete) operation takes place. -## To flush events, the handler discards the messages in the message queue -## by receiving them in a loop without logging. -## -## Default: 8000 -## -#log.flush_qlen = 8000 + ## The max allowed queue length before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum allowed queue + ## length. If the message queue grows larger than this, the handler + ## process is terminated. + ## + ## Default: 20000 + ## + ## overload_kill_qlen: 20000 -## Kill the log handler when it gets overloaded. -## -## Log overload protection parameter. It is possible that a handler, -## even if it can successfully manage peaks of high load without crashing, -## can build up a large message queue, or use a large amount of memory. -## We could kill the log handler in these cases and restart it after a -## few seconds. -## -## Default: on -## -#log.overload_kill = on + ## The max allowed memory size before killing the log hanlder. + ## + ## Log overload protection parameter. This is the maximum memory size + ## that the handler process is allowed to use. If the handler grows + ## larger than this, the process is terminated. + ## + ## Default: 30MB + ## + ## overload_kill_mem_size: 30MB -## The max allowed queue length before killing the log hanlder. -## -## Log overload protection parameter. This is the maximum allowed queue -## length. If the message queue grows larger than this, the handler -## process is terminated. -## -## Default: 20000 -## -#log.overload_kill_qlen = 20000 + ## Restart the log hanlder after some seconds. + ## + ## Log overload protection parameter. If the handler is terminated, + ## it restarts automatically after a delay specified in seconds. + ## The value "infinity" prevents restarts. + ## + ## Default: 5s + ## + ## overload_kill_restart_after: 5s -## The max allowed memory size before killing the log hanlder. -## -## Log overload protection parameter. This is the maximum memory size -## that the handler process is allowed to use. If the handler grows -## larger than this, the process is terminated. -## -## Default: 30MB -## -#log.overload_kill_mem_size = 30MB - -## Restart the log hanlder after some seconds. -## -## Log overload protection parameter. If the handler is terminated, -## it restarts automatically after a delay specified in seconds. -## The value "infinity" prevents restarts. -## -## Default: 5s -## -#log.overload_kill_restart_after = 5s - -## Max burst count and time window for burst control. -## -## Log overload protection parameter. Large bursts of log events - many -## events received by the handler under a short period of time - can -## potentially cause problems. By specifying the maximum number of events -## to be handled within a certain time frame, the handler can avoid -## choking the log with massive amounts of printouts. -## -## This config controls the maximum number of events to handle within -## a time frame. After the limit is reached, successive events are -## dropped until the end of the time frame. -## -## Note that there would be no warning if any messages were -## dropped because of burst control. -## -## Comment this config out to disable the burst control feature. -## -## Value: MaxBurstCount,TimeWindow -## Default: disabled -## -#log.burst_limit = "20000, 1s" - -## CONFIG_SECTION_END=logger =================================================== + ## Max burst count and time window for burst control. + ## + ## Log overload protection parameter. Large bursts of log events - many + ## events received by the handler under a short period of time - can + ## potentially cause problems. By specifying the maximum number of events + ## to be handled within a certain time frame, the handler can avoid + ## choking the log with massive amounts of printouts. + ## + ## This config controls the maximum number of events to handle within + ## a time frame. After the limit is reached, successive events are + ## dropped until the end of the time frame. + ## + ## Note that there would be no warning if any messages were + ## dropped because of burst control. + ## + ## Comment this config out to disable the burst control feature. + ## + ## Value: MaxBurstCount,TimeWindow + ## Default: disabled + ## + ## burst_limit: "20000, 1s" +} ##-------------------------------------------------------------------- ## Authentication/Access Control From 4b87594839cb0d3acc5cc0a3a85ddd7a70280e6a Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 14:05:38 +0800 Subject: [PATCH 065/379] =?UTF-8?q?feat(conf):=20sys=E3=80=81mon=E3=80=81a?= =?UTF-8?q?larm=E3=80=81plugins=E3=80=81broker=E3=80=81mqtt=20conf=20to=20?= =?UTF-8?q?hocon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/emqx/etc/emqx.conf | 1174 ++++++++++++++++----------------- apps/emqx/src/emqx_schema.erl | 47 +- 2 files changed, 571 insertions(+), 650 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 86b9566ba..f40ecd8e5 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -64,12 +64,12 @@ cluster: { ## IP Multicast Address. ## ## Value: IP Address - ## addr = "239.192.0.1" + ## addr: "239.192.0.1" ## Multicast Ports. ## ## Value: Port List - ## ports = "4369,4370" + ## ports: "4369,4370" ## Multicast Iface. ## @@ -291,7 +291,7 @@ node: { ## Value: File ## ## vm.args: -ssl_dist_optfile - ## node.ssl_dist_optfile = "{{ platform_etc_dir }}/ssl_dist.conf" + ## node.ssl_dist_optfile: "{{ platform_etc_dir }}/ssl_dist.conf" ## Sets the net_kernel tick time. TickTime is specified in seconds. ## Notice that all communicating nodes are to have the same TickTime @@ -331,7 +331,7 @@ rpc: { ## Value: Integer ## Zero or negative value disables rpc batching. ## - ## NOTE: RPC batch won't work when rpc.mode = sync + ## NOTE: RPC batch won't work when rpc.mode: sync async_batch_size: 256 ## RPC port discovery @@ -349,7 +349,7 @@ rpc: { ## TCP port number for RPC server to listen on. ## - ## Only takes effect when `rpc.port_discovery` = `manual`. + ## Only takes effect when `rpc.port_discovery`: `manual`. ## ## NOTE: All nodes in the cluster should agree to this same config. ## @@ -503,7 +503,7 @@ log: { ## To create additional log files for specific log levels. ## ## Value: File Name - ## Format: log.$level.file = $filename, + ## Format: log.$level.file: $filename, ## where "$level" can be one of: debug, info, notice, warning, ## error, critical, alert, emergency ## Note: Log files for a specific log level will only contain all the logs @@ -610,116 +610,119 @@ log: { ##-------------------------------------------------------------------- ## Authentication/Access Control ##-------------------------------------------------------------------- +acl: { + ## Allow anonymous authentication by default if no auth plugins loaded. + ## Notice: Disable the option in production deployment! + ## + ## Value: true | false + allow_anonymous: true -## Allow anonymous authentication by default if no auth plugins loaded. -## Notice: Disable the option in production deployment! -## -## Value: true | false -acl.allow_anonymous = true + ## Allow or deny if no ACL rules matched. + ## + ## Value: allow | deny + acl_nomatch: allow -## Allow or deny if no ACL rules matched. -## -## Value: allow | deny -acl.acl_nomatch = allow + ## Default ACL File. + ## + ## Value: File Name + acl_file: "{{ platform_etc_dir }}/acl.conf" -## Default ACL File. -## -## Value: File Name -acl.acl_file = "{{ platform_etc_dir }}/acl.conf" + ## Whether to enable ACL cache. + ## + ## If enabled, ACLs roles for each client will be cached in the memory + ## + ## Value: on | off + enable_acl_cache: on -## Whether to enable ACL cache. -## -## If enabled, ACLs roles for each client will be cached in the memory -## -## Value: on | off -acl.enable_acl_cache = on + ## The maximum count of ACL entries can be cached for a client. + ## + ## Value: Integer greater than 0 + ## Default: 32 + acl_cache_max_size: 32 -## The maximum count of ACL entries can be cached for a client. -## -## Value: Integer greater than 0 -## Default: 32 -acl.acl_cache_max_size = 32 + ## The time after which an ACL cache entry will be deleted + ## + ## Value: Duration + ## Default: 1 minute + acl_cache_ttl: 1m -## The time after which an ACL cache entry will be deleted -## -## Value: Duration -## Default: 1 minute -acl.acl_cache_ttl = 1m + ## The action when acl check reject current operation + ## + ## Value: ignore | disconnect + ## Default: ignore + acl_deny_action: ignore -## The action when acl check reject current operation -## -## Value: ignore | disconnect -## Default: ignore -acl.acl_deny_action = ignore + ## Specify the global flapping detect policy. + ## The value is a string composed of flapping threshold, duration and banned interval. + ## 1. threshold: an integer to specfify the disconnected times of a MQTT Client; + ## 2. duration: the time window for flapping detect; + ## 3. banned interval: the banned interval if a flapping is detected. + ## + ## Value: Integer,Duration,Duration + flapping_detect_policy: "30, 1m, 5m" -## Specify the global flapping detect policy. -## The value is a string composed of flapping threshold, duration and banned interval. -## 1. threshold: an integer to specfify the disconnected times of a MQTT Client; -## 2. duration: the time window for flapping detect; -## 3. banned interval: the banned interval if a flapping is detected. -## -## Value: Integer,Duration,Duration -acl.flapping_detect_policy = "30, 1m, 5m" +} ##-------------------------------------------------------------------- ## MQTT Protocol ##-------------------------------------------------------------------- +mqtt: { + ## Maximum MQTT packet size allowed. + ## + ## Value: Bytes + ## Default: 1MB + max_packet_size: "1MB" -## Maximum MQTT packet size allowed. -## -## Value: Bytes -## Default: 1MB -mqtt.max_packet_size = 1MB + ## Maximum length of MQTT clientId allowed. + ## + ## Value: Number [23-65535] + max_clientid_len: 65535 -## Maximum length of MQTT clientId allowed. -## -## Value: Number [23-65535] -mqtt.max_clientid_len = 65535 + ## Maximum topic levels allowed. 0 means no limit. + ## + ## Value: Number + max_topic_levels: 0 -## Maximum topic levels allowed. 0 means no limit. -## -## Value: Number -mqtt.max_topic_levels = 0 + ## Maximum QoS allowed. + ## + ## Value: 0 | 1 | 2 + max_qos_allowed: 2 -## Maximum QoS allowed. -## -## Value: 0 | 1 | 2 -mqtt.max_qos_allowed = 2 + ## Maximum Topic Alias, 0 means no topic alias supported. + ## + ## Value: 0-65535 + max_topic_alias: 65535 -## Maximum Topic Alias, 0 means no topic alias supported. -## -## Value: 0-65535 -mqtt.max_topic_alias = 65535 + ## Whether the Server supports MQTT retained messages. + ## + ## Value: boolean + retain_available: true -## Whether the Server supports MQTT retained messages. -## -## Value: boolean -mqtt.retain_available = true + ## Whether the Server supports MQTT Wildcard Subscriptions + ## + ## Value: boolean + wildcard_subscription: true -## Whether the Server supports MQTT Wildcard Subscriptions -## -## Value: boolean -mqtt.wildcard_subscription = true + ## Whether the Server supports MQTT Shared Subscriptions. + ## + ## Value: boolean + shared_subscription: true -## Whether the Server supports MQTT Shared Subscriptions. -## -## Value: boolean -mqtt.shared_subscription = true + ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) + ## + ## Value: true | false + ignore_loop_deliver: false -## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) -## -## Value: true | false -mqtt.ignore_loop_deliver = false + ## Whether to parse the MQTT frame in strict mode + ## + ## Value: true | false + strict_mode: false -## Whether to parse the MQTT frame in strict mode -## -## Value: true | false -mqtt.strict_mode = false - -## Specify the response information returned to the client -## -## Value: String -## mqtt.response_information = example + ## Specify the response information returned to the client + ## + ## Value: String + ## response_information: example +} ##-------------------------------------------------------------------- ## External Zone @@ -727,34 +730,34 @@ zone.external { ## Idle timeout of the external MQTT connections. ## ## Value: duration - idle_timeout = 15s + idle_timeout: 15s ## Enable ACL check. ## ## Value: Flag - enable_acl = on + enable_acl: on ## Enable ban check. ## ## Value: Flag - enable_ban = on + enable_ban: on ## Enable per connection statistics. ## ## Value: on | off - enable_stats = on + enable_stats: on ## The action when acl check reject current operation ## ## Value: ignore | disconnect ## Default: ignore - acl_deny_action = ignore + acl_deny_action: ignore ## Force the MQTT connection process GC after this number of ## messages | bytes passed through. ## ## Numbers delimited by `|'. Zero or negative is to disable. - force_gc_policy = "16000|16MB" + force_gc_policy: "16000|16MB" ## Max message queue length and total heap size to force shutdown ## connection/session process. @@ -766,89 +769,89 @@ zone.external { ## Default: ## - "10000|64MB" on ARCH_64 system ## - "1000|32MB" on ARCH_32 sytem - #force_shutdown_policy = "10000|64MB" + #force_shutdown_policy: "10000|64MB" ## Maximum MQTT packet size allowed. ## ## Value: Bytes ## Default: 1MB - ## max_packet_size = 64KB + ## max_packet_size: 64KB ## Maximum length of MQTT clientId allowed. ## ## Value: Number [23-65535] - ## max_clientid_len = 1024 + ## max_clientid_len: 1024 ## Maximum topic levels allowed. 0 means no limit. ## ## Value: Number - ## max_topic_levels = 7 + ## max_topic_levels: 7 ## Maximum QoS allowed. ## ## Value: 0 | 1 | 2 - ## max_qos_allowed = 2 + ## max_qos_allowed: 2 ## Maximum Topic Alias, 0 means no limit. ## ## Value: 0-65535 - ## max_topic_alias = 65535 + ## max_topic_alias: 65535 ## Whether the Server supports retained messages. ## ## Value: boolean - ## retain_available = true + ## retain_available: true ## Whether the Server supports Wildcard Subscriptions ## ## Value: boolean - ## wildcard_subscription = false + ## wildcard_subscription: false ## Whether the Server supports Shared Subscriptions ## ## Value: boolean - ## shared_subscription = false + ## shared_subscription: false ## Server Keep Alive ## ## Value: Number - ## server_keepalive = 0 + ## server_keepalive: 0 ## The backoff for MQTT keepalive timeout. The broker will kick a connection out ## until 'Keepalive * backoff * 2' timeout. ## ## Value: Float > 0.5 - keepalive_backoff = 0.75 + keepalive_backoff: 0.75 ## Maximum number of subscriptions allowed, 0 means no limit. ## ## Value: Number - max_subscriptions = 0 + max_subscriptions: 0 ## Force to upgrade QoS according to subscription. ## ## Value: on | off - upgrade_qos = off + upgrade_qos: off ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. ## ## Value: Number - max_inflight = 32 + max_inflight: 32 ## Retry interval for QoS1/2 message delivering. ## ## Value: Duration - retry_interval = 30s + retry_interval: 30s ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. ## ## Value: Number - max_awaiting_rel = 100 + max_awaiting_rel: 100 ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. ## ## Value: Duration - await_rel_timeout = 300s + await_rel_timeout: 300s ## Default session expiry interval for MQTT V3.1.1 connections. ## @@ -859,13 +862,13 @@ zone.external { ## -s: second ## ## Default: 2h, 2 hours - session_expiry_interval = 2h + session_expiry_interval: 2h ## Maximum queue length. Enqueued messages when persistent client disconnected, ## or inflight window is full. 0 means no limit. ## ## Value: Number >= 0 - max_mqueue_len = 1000 + max_mqueue_len: 1000 ## Topic priorities. ## 'none' to indicate no priority table (by default), hence all messages @@ -878,34 +881,34 @@ zone.external { ## either highest or lowest priority depending on the configured ## value for mqueue_default_priority ## - mqueue_priorities = none + mqueue_priorities: none ## Default to highest priority for topics not matching priority table ## ## Value: highest | lowest - mqueue_default_priority = highest + mqueue_default_priority: highest ## Whether to enqueue QoS0 messages. ## ## Value: false | true - mqueue_store_qos0 = true + mqueue_store_qos0: true ## Whether to turn on flapping detect ## ## Value: on | off - enable_flapping_detect = off + enable_flapping_detect: off ## Message limit for the a external MQTT connection. ## ## Value: Number,Duration ## Example: 100 messages per 10 seconds. - #rate_limit.conn_messages_in = "100,10s" + #rate_limit.conn_messages_in: "100,10s" ## Bytes limit for a external MQTT connections. ## ## Value: Number,Duration ## Example: 100KB incoming per 10 seconds. - #rate_limit.conn_bytes_in = "100KB,10s" + #rate_limit.conn_bytes_in: "100KB,10s" ## Whether to alarm the congested connections. ## @@ -921,7 +924,7 @@ zone.external { ## Where the is the client-id of the congested MQTT connection. ## And the is the username or "unknown_user" of not provided by the client. ## Default: off - #conn_congestion.alarm = off + #conn_congestion.alarm: off ## Won't clear the congested alarm in how long time. ## The alarm is cleared only when there're no pending bytes in the queue, and also it has been @@ -929,7 +932,7 @@ zone.external { ## ## This is to avoid clearing and sending the alarm again too often. ## Default: 1m - #conn_congestion.min_alarm_sustain_duration = 1m + #conn_congestion.min_alarm_sustain_duration: 1m ## Messages quota for the each of external MQTT connection. ## This value consumed by the number of recipient on a message. @@ -937,7 +940,7 @@ zone.external { ## Value: Number, Duration ## ## Example: 100 messages per 1s - #quota.conn_messages_routing = "100,1s" + #quota.conn_messages_routing: "100,1s" ## Messages quota for the all of external MQTT connections. ## This value consumed by the number of recipient on a message. @@ -945,7 +948,7 @@ zone.external { ## Value: Number, Duration ## ## Example: 200000 messages per 1s - #quota.overall_messages_routing = "200000,1s" + #quota.overall_messages_routing: "200000,1s" ## All the topics will be prefixed with the mountpoint path if this option is enabled. ## @@ -954,28 +957,28 @@ zone.external { ## - %u: username ## ## Value: String - ## mountpoint = "devicebound/" + ## mountpoint: "devicebound/" ## Whether use username replace client id ## ## Value: boolean ## Default: false - use_username_as_clientid = false + use_username_as_clientid: false ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) ## ## Value: true | false - ignore_loop_deliver = false + ignore_loop_deliver: false ## Whether to parse the MQTT frame in strict mode ## ## Value: true | false - strict_mode = false + strict_mode: false ## Specify the response information returned to the client ## ## Value: String - #response_information = example + #response_information: example } ##-------------------------------------------------------------------- @@ -985,73 +988,73 @@ zone.internal { ## Notice: Disable the option in production deployment! ## ## Value: true | false - allow_anonymous = true + allow_anonymous: true ## Enable per connection stats. ## ## Value: Flag - enable_stats = on + enable_stats: on ## Enable ACL check. ## ## Value: Flag - enable_acl = off + enable_acl: off ## The action when acl check reject current operation ## ## Value: ignore | disconnect ## Default: ignore - acl_deny_action = ignore + acl_deny_action: ignore ## See zone.$name.force_gc_policy - ## force_gc_policy = "128000|128MB" + ## force_gc_policy: "128000|128MB" ## See zone.$name.wildcard_subscription. ## ## Value: boolean - ## wildcard_subscription = true + ## wildcard_subscription: true ## See zone.$name.shared_subscription. ## ## Value: boolean - ## shared_subscription = true + ## shared_subscription: true ## See zone.$name.max_subscriptions. ## ## Value: Integer - max_subscriptions = 0 + max_subscriptions: 0 ## See zone.$name.max_inflight ## ## Value: Number - max_inflight = 128 + max_inflight: 128 ## See zone.$name.max_awaiting_rel ## ## Value: Number - max_awaiting_rel = 1000 + max_awaiting_rel: 1000 ## See zone.$name.max_mqueue_len ## ## Value: Number >= 0 - max_mqueue_len = 10000 + max_mqueue_len: 10000 ## Whether to enqueue Qos0 messages. ## ## Value: false | true - mqueue_store_qos0 = true + mqueue_store_qos0: true ## Whether to turn on flapping detect ## ## Value: on | off - enable_flapping_detect = off + enable_flapping_detect: off ## See zone.$name.force_shutdown_policy ## ## Default: ## - "10000|64MB" on ARCH_64 system ## - "1000|32MB" on ARCH_32 sytem - #force_shutdown_policy = 10000|64MB + #force_shutdown_policy: 10000|64MB ## All the topics will be prefixed with the mountpoint path if this option is enabled. ## @@ -1060,27 +1063,27 @@ zone.internal { ## - %u: username ## ## Value: String - ## mountpoint = "cloudbound/" + ## mountpoint: "cloudbound/" ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) ## ## Value: true | false - ignore_loop_deliver = false + ignore_loop_deliver: false ## Whether to parse the MQTT frame in strict mode ## ## Value: true | false - strict_mode = false + strict_mode: false ## Specify the response information returned to the client ## ## Value: String - ## response_information = example + ## response_information: example ## Allow the zone's clients to bypass authentication step ## ## Value: true | false - bypass_auth_plugins = true + bypass_auth_plugins: true } ##-------------------------------------------------------------------- @@ -1092,34 +1095,34 @@ listener.tcp.external { ## Value: IP:Port | Port ## ## Examples: 1883, "127.0.0.1:1883", "::1:1883" - endpoint = "0.0.0.0:1883" + endpoint: "0.0.0.0:1883" ## The acceptor pool for external MQTT/TCP listener. ## ## Value: Number - acceptors = 8 + acceptors: 8 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number - max_connections = 1024000 + max_connections: 1024000 ## Maximum external connections per second. ## ## Value: Number - max_conn_rate = 1000 + max_conn_rate: 1000 ## Specify the {active, N} option for the external MQTT/TCP Socket. ## ## Value: Number - active_n = 100 + active_n: 100 ## Zone of the external MQTT/TCP listener belonged to. ## ## See: zone.$name.* ## ## Value: String - zone = external + zone: external ## The access control rules for the MQTT/TCP listener. ## @@ -1128,7 +1131,7 @@ listener.tcp.external { ## Value: ACL Rule ## ## Example: "allow 192.168.0.0/24" - access.1 = "allow all" + access.1: "allow all" ## Enable the Proxy Protocol V1/2 if the EMQ X cluster is deployed ## behind HAProxy or Nginx. @@ -1136,57 +1139,57 @@ listener.tcp.external { ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ ## ## Value: on | off - ## proxy_protocol = on + ## proxy_protocol: on ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## ## Value: Duration - ## proxy_protocol_timeout = 3s + ## proxy_protocol_timeout: 3s ## Enable the option for X.509 certificate based authentication. ## EMQX will use the common name of certificate as MQTT username. ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info ## ## Value: cn - ## peer_cert_as_username = cn + ## peer_cert_as_username: cn ## Enable the option for X.509 certificate based authentication. ## EMQX will use the common name of certificate as MQTT clientid. ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info ## ## Value: cn - ## peer_cert_as_clientid = cn + ## peer_cert_as_clientid: cn ## The TCP backlog defines the maximum length that the queue of pending ## connections can grow to. ## ## Value: Number >= 0 - backlog = 1024 + backlog: 1024 ## The TCP send timeout for external MQTT connections. ## ## Value: Duration - send_timeout = 15s + send_timeout: 15s ## Close the TCP connection if send timeout. ## ## Value: on | off - send_timeout_close = on + send_timeout_close: on ## The TCP receive buffer(os kernel) for MQTT connections. ## ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes - ## recbuf = 2KB + ## recbuf: 2KB ## The TCP send buffer(os kernel) for MQTT connections. ## ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes - ## sndbuf = 2KB + ## sndbuf: 2KB ## The size of the user-level software buffer used by the driver. ## Not to be confused with options sndbuf and recbuf, which correspond @@ -1198,30 +1201,30 @@ listener.tcp.external { ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes - ## buffer = 2KB + ## buffer: 2KB - ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## Sets the 'buffer: max(sndbuf, recbuf)' if this option is enabled. ## ## Value: on | off - ## tune_buffer = off + ## tune_buffer: off ## The socket is set to a busy state when the amount of data queued internally ## by the ERTS socket implementation reaches this limit. ## ## Value: on | off ## Defaults to 1MB - ## high_watermark = 1MB + ## high_watermark: 1MB ## The TCP_NODELAY flag for MQTT connections. Small amounts of data are ## sent immediately if the option is enabled. ## ## Value: true | false - nodelay = true + nodelay: true ## The SO_REUSEADDR flag for TCP listener. ## ## Value: true | false - reuseaddr = true + reuseaddr: true } ##-------------------------------------------------------------------- @@ -1234,93 +1237,93 @@ listener.tcp.internal { ## Value: IP:Port, Port ## ## Examples: 11883, "127.0.0.1:11883", "::1:11883" - endpoint = "127.0.0.1:11883" + endpoint: "127.0.0.1:11883" ## The acceptor pool for internal MQTT/TCP listener. ## ## Value: Number - acceptors = 4 + acceptors: 4 ## Maximum number of concurrent MQTT/TCP connections. ## ## Value: Number - max_connections = 1024000 + max_connections: 1024000 ## Maximum internal connections per second. ## ## Value: Number - max_conn_rate = 1000 + max_conn_rate: 1000 ## Specify the {active, N} option for the internal MQTT/TCP Socket. ## ## Value: Number - active_n = 1000 + active_n: 1000 ## Zone of the internal MQTT/TCP listener belonged to. ## ## Value: String - zone = internal + zone: internal ## The TCP backlog of internal MQTT/TCP Listener. ## ## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 - backlog = 512 + backlog: 512 ## The TCP send timeout for internal MQTT connections. ## ## See: listener.tcp.$name.send_timeout ## ## Value: Duration - send_timeout = 5s + send_timeout: 5s ## Close the MQTT/TCP connection if send timeout. ## ## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off - send_timeout_close = on + send_timeout_close: on ## The TCP receive buffer(os kernel) for internal MQTT connections. ## ## See: listener.tcp.$name.recbuf ## ## Value: Bytes - recbuf = 64KB + recbuf: 64KB ## The TCP send buffer(os kernel) for internal MQTT connections. ## ## See: http://erlang.org/doc/man/inet.html ## ## Value: Bytes - sndbuf = 64KB + sndbuf: 64KB ## The size of the user-level software buffer used by the driver. ## ## See: listener.tcp.$name.buffer ## ## Value: Bytes - ## buffer = 16KB + ## buffer: 16KB - ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## Sets the 'buffer: max(sndbuf, recbuf)' if this option is enabled. ## ## See: listener.tcp.$name.tune_buffer ## ## Value: on | off - ## tune_buffer = off + ## tune_buffer: off ## The TCP_NODELAY flag for internal MQTT connections. ## ## See: listener.tcp.$name.nodelay ## ## Value: true | false - nodelay = false + nodelay: false ## The SO_REUSEADDR flag for MQTT/TCP Listener. ## ## Value: true | false - reuseaddr = true + reuseaddr: true } ##-------------------------------------------------------------------- @@ -1332,39 +1335,39 @@ listener.ssl.external { ## Value: IP:Port | Port ## ## Examples: 8883, "127.0.0.1:8883", "::1:8883" - endpoint = 8883 + endpoint: 8883 ## The acceptor pool for external MQTT/SSL listener. ## ## Value: Number - acceptors = 16 + acceptors: 16 ## Maximum number of concurrent MQTT/SSL connections. ## ## Value: Number - max_connections = 102400 + max_connections: 102400 ## Maximum MQTT/SSL connections per second. ## ## Value: Number - max_conn_rate = 500 + max_conn_rate: 500 ## Specify the {active, N} option for the internal MQTT/SSL Socket. ## ## Value: Number - active_n = 100 + active_n: 100 ## Zone of the external MQTT/SSL listener belonged to. ## ## Value: String - zone = external + zone: external ## The access control rules for the MQTT/SSL listener. ## ## See: listener.tcp.$name.access ## ## Value: ACL Rule - access.1 = "allow all" + access.1: "allow all" ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind ## HAProxy or Nginx. @@ -1372,14 +1375,14 @@ listener.ssl.external { ## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off - ## proxy_protocol = on + ## proxy_protocol: on ## Sets the timeout for proxy protocol. ## ## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration - ## proxy_protocol_timeout = 3s + ## proxy_protocol_timeout: 3s ## TLS versions only to protect from POODLE attack. ## @@ -1387,44 +1390,44 @@ listener.ssl.external { ## ## Value: String, seperated by ',' ## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier - ## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" + ## tls_versions: "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" ## TLS Handshake timeout. ## ## Value: Duration - handshake_timeout = 15s + handshake_timeout: 15s ## Maximum number of non-self-issued intermediate certificates that ## can follow the peer certificate in a valid certification path. ## ## Value: Number - ## depth = 10 + ## depth: 10 ## String containing the user's password. Only used if the private keyfile ## is password-protected. ## ## Value: String - ## key_password = yourpass + ## key_password: yourpass ## Path to the file containing the user's private PEM-encoded key. ## ## See: http://erlang.org/doc/man/ssl.html ## ## Value: File - keyfile = "{{ platform_etc_dir }}/certs/key.pem" + keyfile: "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. ## ## See: http://erlang.org/doc/man/ssl.html ## ## Value: File - certfile = "{{ platform_etc_dir }}/certs/cert.pem" + certfile: "{{ platform_etc_dir }}/certs/cert.pem" ## Path to the file containing PEM-encoded CA certificates. The CA certificates ## are used during server authentication and when building the client certificate chain. ## ## Value: File - ## cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + ## cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" ## The Ephemeral Diffie-Helman key exchange is a very effective way of ## ensuring Forward Secrecy by exchanging a set of keys that never hit @@ -1441,7 +1444,7 @@ listener.ssl.external { ## openssl dhparam -out dh-params.pem 2048 ## ## Value: File - ## dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" + ## dhfile: "{{ platform_etc_dir }}/certs/dh-params.pem" ## A server only does x509-path validation in mode verify_peer, ## as it then sends a certificate request to the client (this @@ -1450,14 +1453,14 @@ listener.ssl.external { ## More information at: http://erlang.org/doc/man/ssl.html ## ## Value: verify_peer | verify_none - ## verify = verify_peer + ## verify: verify_peer ## Used together with {verify, verify_peer} by an SSL server. If set to true, ## the server fails if the client does not have a certificate to send, that is, ## sends an empty certificate. ## ## Value: true | false - ## fail_if_no_peer_cert = true + ## fail_if_no_peer_cert: true ## This is the single most important configuration option of an Erlang SSL ## application. Ciphers (and their ordering) define the way the client and @@ -1476,13 +1479,13 @@ listener.ssl.external { ## Most of it was copied from Mozilla’s Server Side TLS article ## ## Value: Ciphers - ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" + ciphers: "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" ## Ciphers for TLS PSK. ## Note that 'ciphers' and 'psk_ciphers' cannot ## be configured at the same time. ## See 'https://tools.ietf.org/html/rfc4279#section-2'. - #psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" + #psk_ciphers: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" ## SSL parameter renegotiation is a feature that allows a client and a server ## to renegotiate the parameters of the SSL connection on the fly. @@ -1490,7 +1493,7 @@ listener.ssl.external { ## you drop support for the insecure renegotiation, prone to MitM attacks. ## ## Value: on | off - ## secure_renegotiate = off + ## secure_renegotiate: off ## A performance optimization setting, it allows clients to reuse ## pre-existing sessions, instead of initializing new ones. @@ -1499,7 +1502,7 @@ listener.ssl.external { ## See: http://erlang.org/doc/man/ssl.html ## ## Value: on | off - ## reuse_sessions = on + ## reuse_sessions: on ## An important security setting, it forces the cipher to be set based ## on the server-specified order instead of the client-specified order, @@ -1507,82 +1510,82 @@ listener.ssl.external { ## ordering of the server administrator. ## ## Value: on | off - ## honor_cipher_order = on + ## honor_cipher_order: on ## Use the CN, DN or CRT field from the client certificate as a username. ## Notice that 'verify' should be set as 'verify_peer'. ## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. ## ## Value: cn | dn | crt | pem | md5 - ## peer_cert_as_username = cn + ## peer_cert_as_username: cn ## Use the CN, DN or CRT field from the client certificate as a username. ## Notice that 'verify' should be set as 'verify_peer'. ## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. ## ## Value: cn | dn | crt | pem | md5 - ## peer_cert_as_clientid = cn + ## peer_cert_as_clientid: cn ## TCP backlog for the SSL connection. ## ## See listener.tcp.$name.backlog ## ## Value: Number >= 0 - ## backlog = 1024 + ## backlog: 1024 ## The TCP send timeout for the SSL connection. ## ## See listener.tcp.$name.send_timeout ## ## Value: Duration - ## send_timeout = 15s + ## send_timeout: 15s ## Close the SSL connection if send timeout. ## ## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off - ## send_timeout_close = on + ## send_timeout_close: on ## The TCP receive buffer(os kernel) for the SSL connections. ## ## See: listener.tcp.$name.recbuf ## ## Value: Bytes - ## recbuf = 4KB + ## recbuf: 4KB ## The TCP send buffer(os kernel) for internal MQTT connections. ## ## See: listener.tcp.$name.sndbuf ## ## Value: Bytes - ## sndbuf = 4KB + ## sndbuf: 4KB ## The size of the user-level software buffer used by the driver. ## ## See: listener.tcp.$name.buffer ## ## Value: Bytes - ## buffer = 4KB + ## buffer: 4KB - ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## Sets the 'buffer: max(sndbuf, recbuf)' if this option is enabled. ## ## See: listener.tcp.$name.tune_buffer ## ## Value: on | off - ## tune_buffer = off + ## tune_buffer: off ## The TCP_NODELAY flag for SSL connections. ## ## See: listener.tcp.$name.nodelay ## ## Value: true | false - ## nodelay = true + ## nodelay: true ## The SO_REUSEADDR flag for MQTT/SSL Listener. ## ## Value: true | false - reuseaddr = true + reuseaddr: true } ##-------------------------------------------------------------------- @@ -1595,67 +1598,67 @@ listener.ws.external { ## Value: IP:Port | Port ## ## Examples: 8083, "127.0.0.1:8083", "::1:8083" - endpoint = 8083 + endpoint: 8083 ## The path of WebSocket MQTT endpoint ## ## Value: URL Path - mqtt_path = "/mqtt" + mqtt_path: "/mqtt" ## The acceptor pool for external MQTT/WebSocket listener. ## ## Value: Number - acceptors = 4 + acceptors: 4 ## Maximum number of concurrent MQTT/WebSocket connections. ## ## Value: Number - max_connections = 102400 + max_connections: 102400 ## Maximum MQTT/WebSocket connections per second. ## ## Value: Number - max_conn_rate = 1000 + max_conn_rate: 1000 ## Simulate the {active, N} option for the MQTT/WebSocket connections. ## ## Value: Number - active_n = 100 + active_n: 100 ## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String - zone = external + zone: external ## The access control for the MQTT/WebSocket listener. ## ## See: $name.access ## ## Value: ACL Rule - access.1 = "allow all" + access.1: "allow all" ## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. ## Set to false for WeChat MiniApp. ## ## Value: true | false - ## fail_if_no_subprotocol = true + ## fail_if_no_subprotocol: true ## Supported subprotocols ## ## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 - ## supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + ## supported_subprotocols: "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" ## Specify which HTTP header for real source IP if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Default: X-Forwarded-For - ## proxy_address_header = X-Forwarded-For + ## proxy_address_header: X-Forwarded-For ## Specify which HTTP header for real source port if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Default: X-Forwarded-Port - ## proxy_port_header = X-Forwarded-Port + ## proxy_port_header: X-Forwarded-Port ## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind ## HAProxy or Nginx. @@ -1663,158 +1666,158 @@ listener.ws.external { ## See: $name.proxy_protocol ## ## Value: on | off - ## proxy_protocol = on + ## proxy_protocol: on ## Sets the timeout for proxy protocol. ## ## See: $name.proxy_protocol_timeout ## ## Value: Duration - ## proxy_protocol_timeout = 3s + ## proxy_protocol_timeout: 3s ## Enable the option for X.509 certificate based authentication. ## EMQX will use the common name of certificate as MQTT username. ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info ## ## Value: cn - ## peer_cert_as_username = cn + ## peer_cert_as_username: cn ## Enable the option for X.509 certificate based authentication. ## EMQX will use the common name of certificate as MQTT clientid. ## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info ## ## Value: cn - ## peer_cert_as_clientid = cn + ## peer_cert_as_clientid: cn ## The TCP backlog of external MQTT/WebSocket Listener. ## ## See: $name.backlog ## ## Value: Number >= 0 - backlog = 1024 + backlog: 1024 ## The TCP send timeout for external MQTT/WebSocket connections. ## ## See: $name.send_timeout ## ## Value: Duration - send_timeout = 15s + send_timeout: 15s ## Close the MQTT/WebSocket connection if send timeout. ## ## See: $name.send_timeout_close ## ## Value: on | off - send_timeout_close = on + send_timeout_close: on ## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. ## ## See: $name.recbuf ## ## Value: Bytes - ## recbuf = 2KB + ## recbuf: 2KB ## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. ## ## See: $name.sndbuf ## ## Value: Bytes - ## sndbuf = 2KB + ## sndbuf: 2KB ## The size of the user-level software buffer used by the driver. ## ## See: $name.buffer ## ## Value: Bytes - ## buffer = 2KB + ## buffer: 2KB - ## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. + ## Sets the 'buffer: max(sndbuf, recbuf)' if this option is enabled. ## ## See: $name.tune_buffer ## ## Value: on | off - ## tune_buffer = off + ## tune_buffer: off ## The TCP_NODELAY flag for external MQTT/WebSocket connections. ## ## See: $name.nodelay ## ## Value: true | false - nodelay = true + nodelay: true ## The compress flag for external MQTT/WebSocket connections. ## ## If this Value is set true,the websocket message would be compressed ## ## Value: true | false - ## compress = true + ## compress: true ## The level of deflate options for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.level ## ## Value: none | default | best_compression | best_speed - ## deflate_opts.level = default + ## deflate_opts.level: default ## The mem_level of deflate options for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.mem_level ## ## Valid range is 1-9 - ## deflate_opts.mem_level = 8 + ## deflate_opts.mem_level: 8 ## The strategy of deflate options for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.strategy ## ## Value: default | filtered | huffman_only | rle - ## deflate_opts.strategy = default + ## deflate_opts.strategy: default ## The deflate option for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.server_context_takeover ## ## Value: takeover | no_takeover - ## deflate_opts.server_context_takeover = takeover + ## deflate_opts.server_context_takeover: takeover ## The deflate option for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.client_context_takeover ## ## Value: takeover | no_takeover - ## deflate_opts.client_context_takeover = takeover + ## deflate_opts.client_context_takeover: takeover ## The deflate options for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.server_max_window_bits ## ## Valid range is 8-15 - ## deflate_opts.server_max_window_bits = 15 + ## deflate_opts.server_max_window_bits: 15 ## The deflate options for external MQTT/WebSocket connections. ## ## See: $name.deflate_opts.client_max_window_bits ## ## Valid range is 8-15 - ## deflate_opts.client_max_window_bits = 15 + ## deflate_opts.client_max_window_bits: 15 ## The idle timeout for external MQTT/WebSocket connections. ## ## See: $name.idle_timeout ## ## Value: Duration - ## idle_timeout = 60s + ## idle_timeout: 60s ## The max frame size for external MQTT/WebSocket connections. ## ## ## Value: Number - ## max_frame_size = 0 + ## max_frame_size: 0 ## Whether a WebSocket message is allowed to contain multiple MQTT packets ## ## Value: single | multiple - mqtt_piggyback = multiple + mqtt_piggyback: multiple ## By default, EMQX web socket connection does not restrict connections to specific origins. ## It also, by default, does not enforce the presence of origin in request headers for WebSocket connections. @@ -1823,32 +1826,32 @@ listener.ws.external { ## To prevent this, users can set allowed origin headers in their ws connection to EMQX. ## Example for WS connection ## To enables origin check in header for websocket connnection, - ## set `check_origin_enable = true`. By default it is false, + ## set `check_origin_enable: true`. By default it is false, ## When it is set to true and no origin is present in the header of a ws connection request, the request fails. ## To allow origins to be absent in header in the websocket connection when check_origin_enable is true, - ## set `allow_origin_absence = true` + ## set `allow_origin_absence: true` ## Enabling origin check implies there are specific valid origins allowed for ws connection. ## To set the list of allowed origins in header for websocket connection - ## check_origins = http://localhost:18083(localhost dashboard url), http://yourapp.com` + ## check_origins: http://localhost:18083(localhost dashboard url), http://yourapp.com` ## check_origins config allows a comma separated list of origins so you can specify as many origins are you want. ## With these configs, you can allow only connections from only authorized origins to your broker ## Enable origin check in header for websocket connection ## ## Value: true | false (default false) - check_origin_enable = false + check_origin_enable: false ## Allow origin to be absent in header in websocket connection when check_origin_enable is true ## ## Value: true | false (default true) - allow_origin_absence = true + allow_origin_absence: true ## Comma separated list of allowed origin in header for websocket connection ## ## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 - check_origins = "http://localhost:18083, http://127.0.0.1:18083" + check_origins: "http://localhost:18083, http://127.0.0.1:18083" } ##-------------------------------------------------------------------- @@ -1860,83 +1863,83 @@ listener.wss.external { ## Value: IP:Port | Port ## ## Examples: 8084, "127.0.0.1:8084", "::1:8084" - endpoint = 8084 + endpoint: 8084 ## The path of WebSocket MQTT endpoint ## ## Value: URL Path - mqtt_path = "/mqtt" + mqtt_path: "/mqtt" ## The acceptor pool for external MQTT/WebSocket/SSL listener. ## ## Value: Number - acceptors = 4 + acceptors: 4 ## Maximum number of concurrent MQTT/Webwocket/SSL connections. ## ## Value: Number - max_connections = 16 + max_connections: 16 ## Maximum MQTT/WebSocket/SSL connections per second. ## ## See: listener.tcp.$name.max_conn_rate ## ## Value: Number - max_conn_rate = 1000 + max_conn_rate: 1000 ## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. ## ## Value: Number - active_n = 100 + active_n: 100 ## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String - zone = external + zone: external ## The access control rules for the MQTT/WebSocket/SSL listener. ## ## See: listener.tcp.$name.access. ## ## Value: ACL Rule - access.1 = "allow all" + access.1: "allow all" ## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. ## Set to false for WeChat MiniApp. ## ## Value: true | false - ## fail_if_no_subprotocol = true + ## fail_if_no_subprotocol: true ## Supported subprotocols ## ## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 - ## supported_subprotocols = "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + ## supported_subprotocols: "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" ## Specify which HTTP header for real source IP if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Default: X-Forwarded-For - ## proxy_address_header = X-Forwarded-For + ## proxy_address_header: X-Forwarded-For ## Specify which HTTP header for real source port if the EMQ X cluster is ## deployed behind NGINX or HAProxy. ## ## Default: X-Forwarded-Port - ## proxy_port_header = X-Forwarded-Port + ## proxy_port_header: X-Forwarded-Port ## Enable the Proxy Protocol V1/2 support. ## ## See: listener.tcp.$name.proxy_protocol ## ## Value: on | off - ## proxy_protocol = on + ## proxy_protocol: on ## Sets the timeout for proxy protocol. ## ## See: listener.tcp.$name.proxy_protocol_timeout ## ## Value: Duration - ## proxy_protocol_timeout = 3s + ## proxy_protocol_timeout: 3s ## TLS versions only to protect from POODLE attack. ## @@ -1944,28 +1947,28 @@ listener.wss.external { ## ## Value: String, seperated by ',' ## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier - ## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" + ## tls_versions: "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" ## Path to the file containing the user's private PEM-encoded key. ## ## See: listener.ssl.$name.keyfile ## ## Value: File - keyfile = "{{ platform_etc_dir }}/certs/key.pem" + keyfile: "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. ## ## See: listener.ssl.$name.certfile ## ## Value: File - certfile = "{{ platform_etc_dir }}/certs/cert.pem" + certfile: "{{ platform_etc_dir }}/certs/cert.pem" ## Path to the file containing PEM-encoded CA certificates. ## ## See: listener.ssl.$name.cacert ## ## Value: File - ## cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + ## cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" ## Maximum number of non-self-issued intermediate certificates that ## can follow the peer certificate in a valid certification path. @@ -1973,7 +1976,7 @@ listener.wss.external { ## See: listener.ssl.external.depth ## ## Value: Number - ## depth = 10 + ## depth: 10 ## String containing the user's password. Only used if the private keyfile ## is password-protected. @@ -1981,192 +1984,192 @@ listener.wss.external { ## See: listener.ssl.$name.key_password ## ## Value: String - ## key_password = yourpass + ## key_password: yourpass ## See: listener.ssl.$name.dhfile ## ## Value: File - ## listener.ssl.external.dhfile = "{{ platform_etc_dir }}/certs/dh-params.pem" + ## listener.ssl.external.dhfile: "{{ platform_etc_dir }}/certs/dh-params.pem" ## See: listener.ssl.$name.verify ## ## Value: verify_peer | verify_none - ## verify = verify_peer + ## verify: verify_peer ## See: listener.ssl.$name.fail_if_no_peer_cert ## ## Value: false | true - ## fail_if_no_peer_cert = true + ## fail_if_no_peer_cert: true ## See: listener.ssl.$name.ciphers ## ## Value: Ciphers - ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" + ciphers: "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" ## Ciphers for TLS PSK. ## Note that 'ciphers' and 'psk_ciphers' cannot ## be configured at the same time. ## See 'https://tools.ietf.org/html/rfc4279#section-2'. - ## psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" + ## psk_ciphers: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" ## See: listener.ssl.$name.secure_renegotiate ## ## Value: on | off - ## secure_renegotiate = off + ## secure_renegotiate: off ## See: listener.ssl.$name.reuse_sessions ## ## Value: on | off - ## reuse_sessions = on + ## reuse_sessions: on ## See: listener.ssl.$name.honor_cipher_order ## ## Value: on | off - ## honor_cipher_order = on + ## honor_cipher_order: on ## See: listener.ssl.$name.peer_cert_as_username ## ## Value: cn | dn | crt | pem | md5 - ## peer_cert_as_username = cn + ## peer_cert_as_username: cn ## See: listener.ssl.$name.peer_cert_as_clientid ## ## Value: cn | dn | crt | pem | md5 - ## peer_cert_as_clientid = cn + ## peer_cert_as_clientid: cn ## TCP backlog for the WebSocket/SSL connection. ## ## See: listener.tcp.$name.backlog ## ## Value: Number >= 0 - backlog = 1024 + backlog: 1024 ## The TCP send timeout for the WebSocket/SSL connection. ## ## See: listener.tcp.$name.send_timeout ## ## Value: Duration - send_timeout = 15s + send_timeout: 15s ## Close the WebSocket/SSL connection if send timeout. ## ## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off - send_timeout_close = on + send_timeout_close: on ## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. ## ## See: listener.tcp.$name.recbuf ## ## Value: Bytes - ## recbuf = 4KB + ## recbuf: 4KB ## The TCP send buffer(os kernel) for the WebSocket/SSL connections. ## ## See: listener.tcp.$name.sndbuf ## ## Value: Bytes - ## sndbuf = 4KB + ## sndbuf: 4KB ## The size of the user-level software buffer used by the driver. ## ## See: listener.tcp.$name.buffer ## ## Value: Bytes - ## buffer = 4KB + ## buffer: 4KB ## The TCP_NODELAY flag for WebSocket/SSL connections. ## ## See: listener.tcp.$name.nodelay ## ## Value: true | false - ## nodelay = true + ## nodelay: true ## The compress flag for external WebSocket/SSL connections. ## ## If this Value is set true,the websocket message would be compressed ## ## Value: true | false - ## compress = true + ## compress: true ## The level of deflate options for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.level ## ## Value: none | default | best_compression | best_speed - ## deflate_opts.level = default + ## deflate_opts.level: default ## The mem_level of deflate options for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.mem_level ## ## Valid range is 1-9 - ## deflate_opts.mem_level = 8 + ## deflate_opts.mem_level: 8 ## The strategy of deflate options for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.strategy ## ## Value: default | filtered | huffman_only | rle - ## deflate_opts.strategy = default + ## deflate_opts.strategy: default ## The deflate option for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.server_context_takeover ## ## Value: takeover | no_takeover - ## deflate_opts.server_context_takeover = takeover + ## deflate_opts.server_context_takeover: takeover ## The deflate option for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.client_context_takeover ## ## Value: takeover | no_takeover - ## deflate_opts.client_context_takeover = takeover + ## deflate_opts.client_context_takeover: takeover ## The deflate options for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.server_max_window_bits ## ## Valid range is 8-15 - ## deflate_opts.server_max_window_bits = 15 + ## deflate_opts.server_max_window_bits: 15 ## The deflate options for external WebSocket/SSL connections. ## ## See: listener.wss.$name.deflate_opts.client_max_window_bits ## ## Valid range is 8-15 - ## deflate_opts.client_max_window_bits = 15 + ## deflate_opts.client_max_window_bits: 15 ## The idle timeout for external WebSocket/SSL connections. ## ## See: listener.wss.$name.idle_timeout ## ## Value: Duration - ## idle_timeout = 60s + ## idle_timeout: 60s ## The max frame size for external WebSocket/SSL connections. ## ## Value: Number - ## max_frame_size = 0 + ## max_frame_size: 0 ## Whether a WebSocket message is allowed to contain multiple MQTT packets ## ## Value: single | multiple - mqtt_piggyback = multiple + mqtt_piggyback: multiple ## Enable origin check in header for secure websocket connection ## ## Value: true | false (default false) - check_origin_enable = false + check_origin_enable: false ## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true ## ## Value: true | false (default true) - allow_origin_absence = true + allow_origin_absence: true ## Comma separated list of allowed origin in header for secure websocket connection ## ## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 - check_origins = "https://localhost:8084, https://127.0.0.1:8084" + check_origins: "https://localhost:8084, https://127.0.0.1:8084" } ##-------------------------------------------------------------------- @@ -2179,55 +2182,55 @@ listener.quic.external { ## Value: IP:Port | Port ## ## Examples: 14567, 127.0.0.1:14567, ::1:14567 - endpoint = 14567 + endpoint: 14567 ## The acceptor pool for external MQTT/QUIC listener. ## ## Value: Number - acceptors = 4 + acceptors: 4 ## Maximum number of concurrent MQTT/Webwocket/SSL connections. ## ## Value: Number - max_connections = 16 + max_connections: 16 ## Maximum MQTT/QUIC connections per second. ## ## See: listener.tcp.$name.max_conn_rate ## ## Value: Number - max_conn_rate = 1000 + max_conn_rate: 1000 ## Simulate the {active, N} option for the MQTT/QUIC connections. ## @todo ## Value: Number - ## active_n = 100 + ## active_n: 100 ## Zone of the external MQTT/QUIC listener belonged to. ## ## Value: String - zone = external + zone: external ## Path to the file containing the user's private PEM-encoded key. ## ## See: listener.ssl.$name.keyfile ## ## Value: File - keyfile = "{{ platform_etc_dir }}/certs/key.pem" + keyfile: "{{ platform_etc_dir }}/certs/key.pem" ## Path to a file containing the user certificate. ## ## See: listener.ssl.$name.certfile ## ## Value: File - certfile = "{{ platform_etc_dir }}/certs/cert.pem" + certfile: "{{ platform_etc_dir }}/certs/cert.pem" ## Path to the file containing PEM-encoded CA certificates. ## @todo ## See: listener.ssl.$name.cacert ## ## Value: File - ## cacertfile = {{ platform_etc_dir }}/certs/cacert.pem + ## cacertfile: {{ platform_etc_dir }}/certs/cacert.pem ## String containing the user's password. Only used if the private keyfile ## is password-protected. @@ -2235,382 +2238,333 @@ listener.quic.external { ## See: listener.ssl.$name.key_password ## ## Value: String - ## key_password = yourpass + ## key_password: yourpass ## See: listener.ssl.$name.verify ## @todo ## Value: verify_peer | verify_none - ## verify = verify_peer + ## verify: verify_peer ## See: listener.ssl.$name.fail_if_no_peer_cert ## @todo ## Value: false | true - ## fail_if_no_peer_cert = true + ## fail_if_no_peer_cert: true ## See: listener.ssl.$name.ciphers ## @todo ## Value: Ciphers - ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256" + ciphers: "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256" ## Ciphers for TLS PSK. ## @todo ## Note that 'ciphers' and 'psk_ciphers' cannot ## be configured at the same time. ## See 'https://tools.ietf.org/html/rfc4279#section-2'. - ## psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA + ## psk_ciphers: PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA ## See: listener.ssl.$name.honor_cipher_order ## @todo ## Value: on | off - ## honor_cipher_order = on + ## honor_cipher_order: on ## The send timeout for the QUIC stream. ## @todo ## ## Value: Duration - # send_timeout = 15s + # send_timeout: 15s ## Close the QUIC connection if send timeout. ## @todo ## See: listener.tcp.$name.send_timeout_close ## ## Value: on | off - ## send_timeout_close = on + ## send_timeout_close: on ## The receive buffer for the QUIC connections. ## @todo ## See: listener.tcp.$name.recbuf ## ## Value: Bytes - ## recbuf = 4KB + ## recbuf: 4KB ## The TCP send buffer(os kernel) for the QUIC connections. ## @todo ## See: listener.tcp.$name.sndbuf ## ## Value: Bytes - ## sndbuf = 4KB + ## sndbuf: 4KB ## The size of the user-level software buffer used by the driver. ## @todo ## See: listener.tcp.$name.buffer ## ## Value: Bytes - ## buffer = 4KB + ## buffer: 4KB ## The idle timeout for external QUIC connections. ## @todo ## See: listener.quic.$name.idle_timeout ## ## Value: Duration - ## idle_timeout = 60s + ## idle_timeout: 60s ## The max frame size for external QUIC connections. ## @todo ## Value: Number - ## max_frame_size = 0 + ## max_frame_size: 0 } -## CONFIG_SECTION_BGN=modules ================================================== - -## The file to store loaded module names. -## -## Value: File -module.loaded_file = "{{ platform_data_dir }}/loaded_modules" - -##-------------------------------------------------------------------- -## Presence Module - -## Sets the QoS for presence MQTT message. -## -## Value: 0 | 1 | 2 -module.presence.qos = 1 - -##-------------------------------------------------------------------- -## Subscription Module - -## Subscribe the Topics automatically when client connected. -## -## Value: String -## module.subscription.1.topic = "connected/%c/%u" - -## Qos of the proxy subscription. -## -## Value: 0 | 1 | 2 -## Default: 0 -## module.subscription.1.qos = 0 - -## No Local of the proxy subscription options. -## This configuration only takes effect in the MQTT V5 protocol. -## -## Value: 0 | 1 -## Default: 0 -## module.subscription.1.nl = 0 - -## Retain As Published of the proxy subscription options. -## This configuration only takes effect in the MQTT V5 protocol. -## -## Value: 0 | 1 -## Default: 0 -## module.subscription.1.rap = 0 - -## Retain Handling of the proxy subscription options. -## This configuration only takes effect in the MQTT V5 protocol. -## -## Value: 0 | 1 | 2 -## Default: 0 -## module.subscription.1.rh = 0 - -##-------------------------------------------------------------------- -## Rewrite Module - -## {rewrite, Topic, Re, Dest} -## module.rewrite.pub_rule.1 = "x/# ^x/y/(.+)$ z/y/$1" -## module.rewrite.sub_rule.1 = "y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2" - -## CONFIG_SECTION_END=modules ================================================== - ##------------------------------------------------------------------- ## Plugins ##------------------------------------------------------------------- +plugins: { + ## The etc dir for plugins' config. + ## + ## Value: Folder + etc_dir: "{{ platform_etc_dir }}/plugins/" -## The etc dir for plugins' config. -## -## Value: Folder -plugins.etc_dir = "{{ platform_etc_dir }}/plugins/" + ## The file to store loaded plugin names. + ## + ## Value: File + loaded_file: "{{ platform_data_dir }}/loaded_plugins" -## The file to store loaded plugin names. -## -## Value: File -plugins.loaded_file = "{{ platform_data_dir }}/loaded_plugins" + ## The directory of extension plugins. + ## + ## Value: File + expand_plugins_dir: "{{ platform_plugins_dir }}/" -## The directory of extension plugins. -## -## Value: File -plugins.expand_plugins_dir = "{{ platform_plugins_dir }}/" +} ##-------------------------------------------------------------------- ## Broker ##-------------------------------------------------------------------- +broker: { + ## System interval of publishing $SYS messages. + ## + ## Value: Duration + ## Default: 1m, 1 minute + sys_interval: "1m" -## System interval of publishing $SYS messages. -## -## Value: Duration -## Default: 1m, 1 minute -broker.sys_interval = 1m + ## System heartbeat interval of publishing following heart beat message: + ## - "$SYS/brokers//uptime" + ## - "$SYS/brokers//datetime" + ## + ## Value: Duration + ## Default: 30s + sys_heartbeat: "30s" -## System heartbeat interval of publishing following heart beat message: -## - "$SYS/brokers//uptime" -## - "$SYS/brokers//datetime" -## -## Value: Duration -## Default: 30s -broker.sys_heartbeat = 30s + ## Session locking strategy in a cluster. + ## + ## Value: Enum + ## - local + ## - leader + ## - quorum + ## - all + session_locking_strategy: quorum -## Session locking strategy in a cluster. -## -## Value: Enum -## - local -## - leader -## - quorum -## - all -broker.session_locking_strategy = quorum + ## Dispatch strategy for shared subscription + ## + ## Value: Enum + ## - random + ## - round_robin + ## - sticky + ## - hash # same as hash_clientid + ## - hash_clientid + ## - hash_topic + shared_subscription_strategy: random -## Dispatch strategy for shared subscription -## -## Value: Enum -## - random -## - round_robin -## - sticky -## - hash # same as hash_clientid -## - hash_clientid -## - hash_topic -broker.shared_subscription_strategy = random + ## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages + ## This should allow messages to be dispatched to a different subscriber in + ## the group in case the picked (based on shared_subscription_strategy) one # is offline + ## + ## Value: Enum + ## - true + ## - false + shared_dispatch_ack_enabled: false -## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages -## This should allow messages to be dispatched to a different subscriber in -## the group in case the picked (based on shared_subscription_strategy) one # is offline -## -## Value: Enum -## - true -## - false -broker.shared_dispatch_ack_enabled = false + ## Enable batch clean for deleted routes. + ## + ## Value: Flag + route_batch_clean: off -## Enable batch clean for deleted routes. -## -## Value: Flag -broker.route_batch_clean = off + perf: { + ## Performance toggle for subscribe/unsubscribe wildcard topic. + ## Change this toggle only when there are many wildcard topics. + ## Value: Enum + ## - key: mnesia translational updates with per-key locks. recommended for single node setup. + ## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. + ## - global: global lock protected updates. recommended for larger cluster. + ## NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster + ## to be stopped before the change. + # route_lock_type: key -## Performance toggle for subscribe/unsubscribe wildcard topic. -## Change this toggle only when there are many wildcard topics. -## Value: Enum -## - key: mnesia translational updates with per-key locks. recommended for single node setup. -## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. -## - global: global lock protected updates. recommended for larger cluster. -## NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster -## to be stopped before the change. -# broker.perf.route_lock_type = key + ## Enable trie path compaction. + ## Enabling it significantly improves wildcard topic subscribe rate, + ## if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', + ## where ID is unique per subscriber. + ## + ## Topic match performance (when publishing) may degrade if messages + ## are mostly published to topics with large number of levels. + ## + ## NOTE: This is a cluster-wide configuration. + ## It rquires all nodes to be stopped before changing it. + ## + ## Value: Enum + ## - true: enable trie path compaction + ## - false: disable trie path compaction + # trie_compaction: true + } +} -## Enable trie path compaction. -## Enabling it significantly improves wildcard topic subscribe rate, -## if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', -## where ID is unique per subscriber. -## -## Topic match performance (when publishing) may degrade if messages -## are mostly published to topics with large number of levels. -## -## NOTE: This is a cluster-wide configuration. -## It rquires all nodes to be stopped before changing it. -## -## Value: Enum -## - true: enable trie path compaction -## - false: disable trie path compaction -# broker.perf.trie_compaction = true +sysmon: { + ## Enable Long GC monitoring. Disable if the value is 0. + ## Notice: don't enable the monitor in production for: + ## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 + ## + ## Value: Duration + ## - h: hour + ## - m: minute + ## - s: second + ## - ms: milliseconds + ## + ## Examples: + ## - 2h: 2 hours + ## - 30m: 30 minutes + ## - 0.1s: 0.1 seconds + ## - 100ms : 100 milliseconds + ## + ## Default: 0ms + long_gc: 0 -## CONFIG_SECTION_BGN=sys_mon ================================================== + ## Enable Long Schedule(ms) monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## Value: Duration + ## - h: hour + ## - m: minute + ## - s: second + ## - ms: milliseconds + ## + ## Examples: + ## - 2h: 2 hours + ## - 30m: 30 minutes + ## - 100ms: 100 milliseconds + ## + ## Default: 0ms + long_schedule: "240ms" -## Enable Long GC monitoring. Disable if the value is 0. -## Notice: don't enable the monitor in production for: -## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## - ms: milliseconds -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 0.1s: 0.1 seconds -## - 100ms : 100 milliseconds -## -## Default: 0ms -sysmon.long_gc = 0 + ## Enable Large Heap monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## Value: bytes + ## + ## Default: 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. + large_heap: "8MB" -## Enable Long Schedule(ms) monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## - ms: milliseconds -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 100ms: 100 milliseconds -## -## Default: 0ms -sysmon.long_schedule = 240ms + ## Enable Busy Port monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## Value: true | false + busy_port: false -## Enable Large Heap monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: bytes -## -## Default: 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. -sysmon.large_heap = 8MB + ## Enable Busy Dist Port monitoring. + ## + ## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 + ## + ## Value: true | false + busy_dist_port: true +} -## Enable Busy Port monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: true | false -sysmon.busy_port = false +os_mon: { + ## The time interval for the periodic cpu check + ## + ## Value: Duration + ## -h: hour, e.g. '2h' for 2 hours + ## -m: minute, e.g. '5m' for 5 minutes + ## -s: second, e.g. '30s' for 30 seconds + ## + ## Default: 60s + cpu_check_interval: "60s" -## Enable Busy Dist Port monitoring. -## -## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 -## -## Value: true | false -sysmon.busy_dist_port = true + ## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. + ## + ## Default: 80% + cpu_high_watermark: "80%" -## The time interval for the periodic cpu check -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 60s -os_mon.cpu_check_interval = 60s + ## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. + ## + ## Default: 60% + cpu_low_watermark: "60%" -## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. -## -## Default: 80% -os_mon.cpu_high_watermark = 80% + ## The time interval for the periodic memory check + ## + ## Value: Duration + ## -h: hour, e.g. '2h' for 2 hours + ## -m: minute, e.g. '5m' for 5 minutes + ## -s: second, e.g. '30s' for 30 seconds + ## + ## Default: 60s + mem_check_interval: "60s" -## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. -## -## Default: 60% -os_mon.cpu_low_watermark = 60% + ## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. + ## + ## Default: 70% + sysmem_high_watermark: "70%" -## The time interval for the periodic memory check -## -## Value: Duration -## -h: hour, e.g. '2h' for 2 hours -## -m: minute, e.g. '5m' for 5 minutes -## -s: second, e.g. '30s' for 30 seconds -## -## Default: 60s -os_mon.mem_check_interval = 60s + ## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. + ## + ## Default: 5% + procmem_high_watermark: "5%" -## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. -## -## Default: 70% -os_mon.sysmem_high_watermark = 70% +} -## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. -## -## Default: 5% -os_mon.procmem_high_watermark = 5% +vm_mon: { + ## The time interval for the periodic process limit check + ## + ## Value: Duration + ## + ## Default: 30s + check_interval: "30s" -## The time interval for the periodic process limit check -## -## Value: Duration -## -## Default: 30s -vm_mon.check_interval = 30s + ## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. + ## + ## Default: 80% + process_high_watermark: "80%" -## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. -## -## Default: 80% -vm_mon.process_high_watermark = 80% + ## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. + ## + ## Default: 60% + process_low_watermark: "60%" +} -## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. -## -## Default: 60% -vm_mon.process_low_watermark = 60% +alarm: { + ## Specifies the actions to take when an alarm is activated + ## + ## Value: String + ## - log + ## - publish + ## + ## Default: "log,publish" + actions: "log,publish" -## Specifies the actions to take when an alarm is activated -## -## Value: String -## - log -## - publish -## -## Default: "log,publish" -alarm.actions = "log,publish" + ## The maximum number of deactivated alarms + ## + ## Value: Integer + ## + ## Default: 1000 + size_limit: 1000 -## The maximum number of deactivated alarms -## -## Value: Integer -## -## Default: 1000 -alarm.size_limit = 1000 - -## Validity Period of deactivated alarms -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## - ms: milliseconds -## -## Default: 24h -alarm.validity_period = 24h - -## CONFIG_SECTION_END=sys_mon ================================================== + ## Validity Period of deactivated alarms + ## + ## Value: Duration + ## - h: hour + ## - m: minute + ## - s: second + ## - ms: milliseconds + ## + ## Default: 24h + validity_period: "24h" +} diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index cdc20762e..440dd8117 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -54,7 +54,7 @@ -export([includes/0]). structs() -> ["cluster", "node", "rpc", "log", "lager", - "acl", "mqtt", "zone", "listener", "module", "broker", + "acl", "mqtt", "zone", "listener", "broker", "plugins", "sysmon", "os_mon", "vm_mon", "alarm"] ++ includes(). @@ -65,6 +65,12 @@ includes() -> [ "emqx_data_bridge" , "emqx_telemetry" , "emqx_retainer" + , "emqx_statsd" + , "emqx_authn" + , "emqx_authz" + , "emqx_bridge_mqtt" + , "emqx_modules" + , "emqx_management" ]. -endif. @@ -424,12 +430,6 @@ fields("deflate_opts") -> , {"client_max_window_bits", t(integer())} ]; -fields("module") -> - [ {"loaded_file", t(string(), "emqx.modules_loaded_file", undefined)} - , {"presence", ref("presence")} - , {"subscription", ref("subscription")} - , {"rewrite", ref("rewrite")} - ]; fields("presence") -> [ {"qos", t(range(0, 2), undefined, 1)}]; @@ -533,7 +533,6 @@ translation("emqx") -> [ {"flapping_detect_policy", fun tr_flapping_detect_policy/1} , {"zones", fun tr_zones/1} , {"listeners", fun tr_listeners/1} - , {"modules", fun tr_modules/1} , {"sysmon", fun tr_sysmon/1} , {"os_mon", fun tr_os_mon/1} , {"vm_mon", fun tr_vm_mon/1} @@ -827,38 +826,6 @@ tr_listeners(Conf) -> ++ [SslListeners("quic", Name) || Name <- keys("listener.quic", Conf)] ). -tr_modules(Conf) -> - Subscriptions = fun() -> - List = keys("module.subscription", Conf), - TopicList = [{N, conf_get(["module", "subscription", N, "topic"], Conf)}|| N <- List], - [{list_to_binary(T), #{ qos => conf_get("module.subscription." ++ N ++ ".qos", Conf, 0), - nl => conf_get("module.subscription." ++ N ++ ".nl", Conf, 0), - rap => conf_get("module.subscription." ++ N ++ ".rap", Conf, 0), - rh => conf_get("module.subscription." ++ N ++ ".rh", Conf, 0) - }} || {N, T} <- TopicList] - end, - Rewrites = fun() -> - Rules = keys("module.rewrite.rule", Conf), - PubRules = keys("module.rewrite.pub_rule", Conf), - SubRules = keys("module.rewrite.sub_rule", Conf), - TotalRules = - [ {["module", "rewrite", "pub", "rule", R], conf_get(["module.rewrite.rule", R], Conf)} || R <- Rules] ++ - [ {["module", "rewrite", "pub", "rule", R], conf_get(["module.rewrite.pub_rule", R], Conf)} || R <- PubRules] ++ - [ {["module", "rewrite", "sub", "rule", R], conf_get(["module.rewrite.rule", R], Conf)} || R <- Rules] ++ - [ {["module", "rewrite", "sub", "rule", R], conf_get(["module.rewrite.sub_rule", R], Conf)} || R <- SubRules], - lists:map(fun({[_, "rewrite", PubOrSub, "rule", _], Rule}) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, list_to_atom(PubOrSub), list_to_binary(Topic), list_to_binary(Re), list_to_binary(Dest)} - end, TotalRules) - end, - lists:append([ - [{emqx_mod_presence, [{qos, conf_get("module.presence.qos", Conf, 1)}]}], - [{emqx_mod_subscription, Subscriptions()}], - [{emqx_mod_rewrite, Rewrites()}], - [{emqx_mod_topic_metrics, []}], - [{emqx_mod_delayed, []}] - ]). - tr_sysmon(Conf) -> Keys = maps:to_list(conf_get("sysmon", Conf, #{})), [{binary_to_atom(K, utf8), maps:get(value, V)} || {K, V} <- Keys]. From fb2f2741a4a55f714918ee794d506e8cb6bd0f6c Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 14:18:09 +0800 Subject: [PATCH 066/379] fix: fix test cases fail --- apps/emqx/src/emqx_schema.erl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 440dd8117..bb8b41cf9 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -65,12 +65,6 @@ includes() -> [ "emqx_data_bridge" , "emqx_telemetry" , "emqx_retainer" - , "emqx_statsd" - , "emqx_authn" - , "emqx_authz" - , "emqx_bridge_mqtt" - , "emqx_modules" - , "emqx_management" ]. -endif. From 918a26e921bfb07de8f95060abaf7f15b6b4b36f Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 14:12:48 +0800 Subject: [PATCH 067/379] feat(conf): merge all conf to emqx.conf --- .ci/build_packages/tests.sh | 4 +- .../docker-compose-emqx-cluster.yaml | 4 +- .github/workflows/build_packages.yaml | 2 +- .github/workflows/build_slim_packages.yaml | 2 +- .github/workflows/run_fvt_tests.yaml | 14 +- .gitignore | 2 +- Makefile | 8 +- apps/emqx/etc/acl.conf | 26 -- apps/emqx/etc/acl.conf.paho | 14 - apps/emqx/include/emqx.hrl | 3 +- apps/emqx/src/emqx.appup.src | 111 -------- apps/emqx/src/emqx.erl | 1 - apps/emqx/src/emqx_app.erl | 2 +- apps/emqx/src/emqx_plugins.erl | 247 ++---------------- apps/emqx/src/emqx_schema.erl | 6 + apps/emqx/test/emqx_plugins_SUITE.erl | 62 +---- apps/emqx_authn/etc/emqx_authn.conf | 2 +- apps/emqx_authn/src/emqx_authn_app.erl | 7 +- apps/emqx_authn/src/emqx_authn_schema.erl | 16 +- apps/emqx_authz/src/emqx_authz.erl | 6 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 6 +- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 17 +- .../test/emqx_authz_mysql_SUITE.erl | 22 +- .../test/emqx_authz_pgsql_SUITE.erl | 22 +- .../test/emqx_authz_redis_SUITE.erl | 28 +- .../etc/emqx_bridge_mqtt.conf | 102 ++++---- .../src/emqx_bridge_mqtt_schema.erl | 6 +- .../test/emqx_dashboard_SUITE.erl | 33 +-- .../etc/emqx_data_bridge.conf | 247 +++++++++--------- .../src/emqx_data_bridge_schema.erl | 2 +- .../src/emqx_management.app.src | 2 +- .../src/emqx_management.appup.src | 13 - .../src/emqx_management_schema.erl | 2 +- apps/emqx_management/src/emqx_mgmt.erl | 2 +- .../src/emqx_mgmt_api_plugins.erl | 6 +- apps/emqx_management/src/emqx_mgmt_app.erl | 5 - apps/emqx_management/src/emqx_mgmt_auth.erl | 11 +- apps/emqx_management/src/emqx_mgmt_http.erl | 2 +- apps/emqx_management/test/emqx_mgmt_SUITE.erl | 39 +-- .../test/emqx_mgmt_api_SUITE.erl | 11 +- apps/emqx_modules/src/emqx_modules.erl | 2 + apps/emqx_modules/test/emqx_modules_SUITE.erl | 12 +- apps/emqx_sn/vars | 2 +- data/loaded_plugins.tmpl | 3 - rebar.config.erl | 83 ++---- scripts/find-apps.sh | 2 +- scripts/merge-config.escript | 36 +++ 47 files changed, 375 insertions(+), 882 deletions(-) delete mode 100644 apps/emqx/etc/acl.conf delete mode 100644 apps/emqx/etc/acl.conf.paho delete mode 100644 apps/emqx/src/emqx.appup.src delete mode 100644 apps/emqx_management/src/emqx_management.appup.src delete mode 100644 data/loaded_plugins.tmpl create mode 100755 scripts/merge-config.escript diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index d14974679..47994c9ad 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -39,7 +39,7 @@ emqx_test(){ unzip -q "${PACKAGE_PATH}/${packagename}" export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ EMQX_MQTT__MAX_TOPIC_ALIAS=10 - sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins + # sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins echo "running ${packagename} start" if ! "${PACKAGE_PATH}"/emqx/bin/emqx start; then @@ -115,7 +115,7 @@ emqx_test(){ running_test(){ export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ EMQX_MQTT__MAX_TOPIC_ALIAS=10 - sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins + # sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins if ! emqx start; then cat /var/log/emqx/erlang.log.1 || true diff --git a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml index 6bc8e67e2..81d48aba7 100644 --- a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml +++ b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml @@ -38,7 +38,7 @@ services: - -c - | sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf - sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins + # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins /opt/emqx/bin/emqx foreground healthcheck: test: ["CMD", "/opt/emqx/bin/emqx_ctl", "status"] @@ -62,7 +62,7 @@ services: - -c - | sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf - sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins + # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins /opt/emqx/bin/emqx foreground healthcheck: test: ["CMD", "/opt/emqx/bin/emqx", "ping"] diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index b983eaa67..6d1a757af 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -179,7 +179,7 @@ jobs: cd source pkg_name=$(basename _packages/${{ matrix.profile }}/${{ matrix.profile }}-*.zip) unzip -q _packages/${{ matrix.profile }}/$pkg_name - gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins + # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index bf85578c5..1f79b9ab2 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -108,7 +108,7 @@ jobs: run: | pkg_name=$(basename _packages/${EMQX_NAME}/emqx-*.zip) unzip -q _packages/${EMQX_NAME}/$pkg_name - gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins + # gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 035c0d0e3..e414537b9 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -48,13 +48,13 @@ jobs: echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:waiting emqx"; sleep 5; done - - name: verify EMQX_LOADED_PLUGINS override working - run: | - expected="{emqx_sn, true}." - output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1) - if [ "$expected" != "$output" ]; then - exit 1 - fi + # - name: verify EMQX_LOADED_PLUGINS override working + # run: | + # expected="{emqx_sn, true}." + # output=$(docker exec -i node1.emqx.io bash -c "cat data/loaded_plugins" | tail -n1) + # if [ "$expected" != "$output" ]; then + # exit 1 + # fi - name: make paho tests run: | if ! docker exec -i python /scripts/pytest.sh; then diff --git a/.gitignore b/.gitignore index dd8e9b82e..e7b31b394 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,6 @@ emqx_dialyzer_*_plt */emqx_dashboard/priv/www dist.zip scripts/git-token -etc/*.seg +apps/*/etc/*.all _upgrade_base/ TAGS diff --git a/Makefile b/Makefile index cc8cdb0db..c81b9d5a3 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,8 @@ coveralls: $(REBAR) @ENABLE_COVER_COMPILE=1 $(REBAR) as test coveralls send .PHONY: $(REL_PROFILES) -$(REL_PROFILES:%=%): $(REBAR) get-dashboard + +$(REL_PROFILES:%=%): $(REBAR) get-dashboard conf-segs @$(REBAR) as $(@) do compile,release ## Not calling rebar3 clean because @@ -111,7 +112,7 @@ xref: $(REBAR) dialyzer: $(REBAR) @$(REBAR) as check dialyzer -COMMON_DEPS := $(REBAR) get-dashboard $(CONF_SEGS) +COMMON_DEPS := $(REBAR) get-dashboard conf-segs ## rel target is to create release package without relup .PHONY: $(REL_PROFILES:%=%-rel) $(PKG_PROFILES:%=%-rel) @@ -152,3 +153,6 @@ quickrun: ./_build/$(PROFILE)/rel/emqx/bin/emqx console include docker.mk + +conf-segs: + @scripts/merge-config.escript diff --git a/apps/emqx/etc/acl.conf b/apps/emqx/etc/acl.conf deleted file mode 100644 index af2fb0dd1..000000000 --- a/apps/emqx/etc/acl.conf +++ /dev/null @@ -1,26 +0,0 @@ -%%-------------------------------------------------------------------- -%% [ACL](https://docs.emqx.io/broker/v3/en/config.html) -%% -%% -type(who() :: all | binary() | -%% {ipaddr, esockd_access:cidr()} | -%% {client, binary()} | -%% {user, binary()}). -%% -%% -type(access() :: subscribe | publish | pubsub). -%% -%% -type(topic() :: binary()). -%% -%% -type(rule() :: {allow, all} | -%% {allow, who(), access(), list(topic())} | -%% {deny, all} | -%% {deny, who(), access(), list(topic())}). -%%-------------------------------------------------------------------- - -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. - diff --git a/apps/emqx/etc/acl.conf.paho b/apps/emqx/etc/acl.conf.paho deleted file mode 100644 index 5beec4347..000000000 --- a/apps/emqx/etc/acl.conf.paho +++ /dev/null @@ -1,14 +0,0 @@ -%%-------------------------------------------------------------------- -%% For paho interoperability test cases -%%-------------------------------------------------------------------- - -{deny, {client, "myclientid"}, subscribe, ["test/nosubscribe"]}. - -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. - diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index d148e01a3..a11c30cb4 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -108,8 +108,7 @@ descr :: string(), vendor :: string() | undefined, active = false :: boolean(), - info = #{} :: map(), - type :: atom() + info = #{} :: map() }). %%-------------------------------------------------------------------- diff --git a/apps/emqx/src/emqx.appup.src b/apps/emqx/src/emqx.appup.src deleted file mode 100644 index 4f9f00673..000000000 --- a/apps/emqx/src/emqx.appup.src +++ /dev/null @@ -1,111 +0,0 @@ -%% -*- mode: erlang -*- -{VSN, - [ - {"4.3.4", - [{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]} - ]}, - {"4.3.3", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]} - ]}, - {"4.3.2", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]} - ]}, - {"4.3.1", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {"4.3.0", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_trie,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_metrics,brutal_purge,soft_purge,[]}, - {apply,{emqx_metrics,upgrade_retained_delayed_counter_type,[]}}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {<<".*">>,[]}], - [ - {"4.3.4", - [{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]} - ]}, - {"4.3.3", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]} - ]}, - {"4.3.2", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]} - ]}, - {"4.3.1", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {"4.3.0", - [{load_module,emqx_packet,brutal_purge,soft_purge,[]}, - {load_module,emqx_shared_sub,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_connection,brutal_purge,soft_purge,[]}, - {load_module,emqx_congestion,brutal_purge,soft_purge,[]}, - {load_module,emqx_frame,brutal_purge,soft_purge,[]}, - {load_module,emqx_trie,brutal_purge,soft_purge,[]}, - {load_module,emqx_cm,brutal_purge,soft_purge,[]}, - {load_module,emqx_node_dump,brutal_purge,soft_purge,[]}, - {load_module,emqx_channel,brutal_purge,soft_purge,[]}, - {load_module,emqx_app,brutal_purge,soft_purge,[]}, - {load_module,emqx_plugins,brutal_purge,soft_purge,[]}, - {load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]}, - {load_module,emqx_metrics,brutal_purge,soft_purge,[]}, - {load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]}, - {<<".*">>,[]}]}. diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index c700239ca..0a290ff4a 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -227,7 +227,6 @@ shutdown() -> shutdown(Reason) -> ?LOG(critical, "emqx shutdown for ~s", [Reason]), _ = emqx_alarm_handler:unload(), - _ = emqx_plugins:unload(), lists:foreach(fun application:stop/1 , lists:reverse(default_started_applications()) ). diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 60f0fc40d..666a704f3 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -51,7 +51,7 @@ start(_Type, _Args) -> ok = ekka_rlog:wait_for_shards(?EMQX_SHARDS, infinity), {ok, Sup} = emqx_sup:start_link(), ok = start_autocluster(), - ok = emqx_plugins:init(), + % ok = emqx_plugins:init(), _ = emqx_plugins:load(), _ = start_ce_modules(), emqx_boot:is_enabled(listeners) andalso (ok = emqx_listeners:start()), diff --git a/apps/emqx/src/emqx_plugins.erl b/apps/emqx/src/emqx_plugins.erl index 8abc2b21f..ae324c71d 100644 --- a/apps/emqx/src/emqx_plugins.erl +++ b/apps/emqx/src/emqx_plugins.erl @@ -21,8 +21,6 @@ -logger_header("[Plugins]"). --export([init/0]). - -export([ load/0 , load/1 , unload/0 @@ -30,8 +28,6 @@ , reload/1 , list/0 , find_plugin/1 - , generate_configs/1 - , apply_configs/1 ]). -export([funlog/2]). @@ -41,35 +37,14 @@ -compile(nowarn_export_all). -endif. --dialyzer({no_match, [ plugin_loaded/2 - , plugin_unloaded/2 - ]}). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -%% @doc Init plugins' config --spec(init() -> ok). -init() -> - case emqx:get_env(plugins_etc_dir) of - undefined -> ok; - PluginsEtc -> - CfgFiles = [filename:join(PluginsEtc, File) || - File <- filelib:wildcard("*.config", PluginsEtc)], - lists:foreach(fun init_config/1, CfgFiles) - end. - %% @doc Load all plugins when the broker started. -spec(load() -> ok | ignore | {error, term()}). load() -> - ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)), - case emqx:get_env(plugins_loaded_file) of - undefined -> ignore; %% No plugins available - File -> - _ = ensure_file(File), - with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end) - end. + ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)). %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, term()}). @@ -82,17 +57,13 @@ load(PluginName) when is_atom(PluginName) -> ?LOG(notice, "Plugin ~s is already started", [PluginName]), {error, already_started}; {_, false} -> - load_plugin(PluginName, true) + load_plugin(PluginName) end. %% @doc Unload all plugins before broker stopped. --spec(unload() -> list() | {error, term()}). +-spec(unload() -> ok). unload() -> - case emqx:get_env(plugins_loaded_file) of - undefined -> ignore; - File -> - with_loaded_file(File, fun stop_plugins/1) - end. + stop_plugins(list()). %% @doc UnLoad a Plugin -spec(unload(atom()) -> ok | {error, term()}). @@ -105,7 +76,7 @@ unload(PluginName) when is_atom(PluginName) -> ?LOG(error, "Plugin ~s is not started", [PluginName]), {error, not_started}; {_, _} -> - unload_plugin(PluginName, true) + unload_plugin(PluginName) end. reload(PluginName) when is_atom(PluginName)-> @@ -126,8 +97,8 @@ reload(PluginName) when is_atom(PluginName)-> -spec(list() -> [emqx_types:plugin()]). list() -> StartedApps = names(started_app), - lists:map(fun({Name, _, [Type| _]}) -> - Plugin = plugin(Name, Type), + lists:map(fun({Name, _, _}) -> + Plugin = plugin(Name), case lists:member(Name, StartedApps) of true -> Plugin#plugin{active = true}; false -> Plugin @@ -144,12 +115,6 @@ find_plugin(Name, Plugins) -> %% Internal functions %%-------------------------------------------------------------------- -init_config(CfgFile) -> - {ok, [AppsEnv]} = file:consult(CfgFile), - lists:foreach(fun({App, Envs}) -> - [application:set_env(App, Par, Val) || {Par, Val} <- Envs] - end, AppsEnv). - %% load external plugins which are placed in etc/plugins dir load_ext_plugins(undefined) -> ok; load_ext_plugins(Dir) -> @@ -173,15 +138,15 @@ load_ext_plugin(PluginDir) -> ?LOG(alert, "plugin_app_file_not_found: ~s", [AppFile]), error({plugin_app_file_not_found, AppFile}) end, - ok = load_plugin_app(AppName, Ebin), - try - ok = generate_configs(AppName, PluginDir) - catch - throw : {conf_file_not_found, ConfFile} -> - %% this is maybe a dependency of an external plugin - ?LOG(debug, "config_load_error_ignored for app=~p, path=~s", [AppName, ConfFile]), - ok - end. + ok = load_plugin_app(AppName, Ebin). + % try + % ok = generate_configs(AppName, PluginDir) + % catch + % throw : {conf_file_not_found, ConfFile} -> + % %% this is maybe a dependency of an external plugin + % ?LOG(debug, "config_load_error_ignored for app=~p, path=~s", [AppName, ConfFile]), + % ok + % end. load_plugin_app(AppName, Ebin) -> _ = code:add_patha(Ebin), @@ -199,57 +164,24 @@ load_plugin_app(AppName, Ebin) -> {error, {already_loaded, _}} -> ok end. -ensure_file(File) -> - case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. - -with_loaded_file(File, SuccFun) -> - case read_loaded(File) of - {ok, Names0} -> - Names = filter_plugins(Names0), - SuccFun(Names); - {error, Error} -> - ?LOG(alert, "Failed to read: ~p, error: ~p", [File, Error]), - {error, Error} - end. - -filter_plugins(Names) -> - lists:filtermap(fun(Name1) when is_atom(Name1) -> {true, Name1}; - ({Name1, true}) -> {true, Name1}; - ({_Name1, false}) -> false - end, Names). - -load_plugins(Names, Persistent) -> - Plugins = list(), - NotFound = Names -- names(Plugins), - case NotFound of - [] -> ok; - NotFound -> ?LOG(alert, "cannot_find_plugins: ~p", [NotFound]) - end, - NeedToLoad = Names -- NotFound -- names(started_app), - lists:foreach(fun(Name) -> - Plugin = find_plugin(Name, Plugins), - load_plugin(Plugin#plugin.name, Persistent) - end, NeedToLoad). - %% Stop plugins -stop_plugins(Names) -> - _ = [stop_app(App) || App <- Names], +stop_plugins(Plugins) -> + _ = [stop_app(Plugin#plugin.name) || Plugin <- Plugins], ok. -plugin(AppName, Type) -> +plugin(AppName) -> case application:get_all_key(AppName) of {ok, Attrs} -> Descr = proplists:get_value(description, Attrs, ""), - #plugin{name = AppName, descr = Descr, type = plugin_type(Type)}; + #plugin{name = AppName, descr = Descr}; undefined -> error({plugin_not_found, AppName}) end. -load_plugin(Name, Persistent) -> +load_plugin(Name) -> try - ok = ?MODULE:generate_configs(Name), case load_app(Name) of ok -> - start_app(Name, fun(App) -> plugin_loaded(App, Persistent) end); + start_app(Name); {error, Error0} -> {error, Error0} end @@ -268,22 +200,21 @@ load_app(App) -> {error, Error} end. -start_app(App, SuccFun) -> +start_app(App) -> case application:ensure_all_started(App) of {ok, Started} -> ?LOG(info, "Started plugins: ~p", [Started]), ?LOG(info, "Load plugin ~s successfully", [App]), - _ = SuccFun(App), ok; {error, {ErrApp, Reason}} -> ?LOG(error, "Load plugin ~s failed, cannot start plugin ~s for ~0p", [App, ErrApp, Reason]), {error, {ErrApp, Reason}} end. -unload_plugin(App, Persistent) -> +unload_plugin(App) -> case stop_app(App) of ok -> - _ = plugin_unloaded(App, Persistent), ok; + ok; {error, Reason} -> {error, Reason} end. @@ -307,133 +238,5 @@ names(started_app) -> names(Plugins) -> [Name || #plugin{name = Name} <- Plugins]. -plugin_loaded(_Name, false) -> - ok; -plugin_loaded(Name, true) -> - case read_loaded() of - {ok, Names} -> - case lists:member(Name, Names) of - false -> - %% write file if plugin is loaded - write_loaded(lists:append(Names, [{Name, true}])); - true -> - ignore - end; - {error, Error} -> - ?LOG(error, "Cannot read loaded plugins: ~p", [Error]) - end. - -plugin_unloaded(_Name, false) -> - ok; -plugin_unloaded(Name, true) -> - case read_loaded() of - {ok, Names0} -> - Names = filter_plugins(Names0), - case lists:member(Name, Names) of - true -> - write_loaded(lists:delete(Name, Names)); - false -> - ?LOG(error, "Cannot find ~s in loaded_file", [Name]) - end; - {error, Error} -> - ?LOG(error, "Cannot read loaded_plugins: ~p", [Error]) - end. - -read_loaded() -> - case emqx:get_env(plugins_loaded_file) of - undefined -> {error, not_found}; - File -> read_loaded(File) - end. - -read_loaded(File) -> file:consult(File). - -write_loaded(AppNames) -> - FilePath = emqx:get_env(plugins_loaded_file), - case file:write_file(FilePath, [io_lib:format("~p.~n", [Name]) || Name <- AppNames]) of - ok -> ok; - {error, Error} -> - ?LOG(error, "Write File ~p Error: ~p", [FilePath, Error]), - {error, Error} - end. - -plugin_type(auth) -> auth; -plugin_type(protocol) -> protocol; -plugin_type(backend) -> backend; -plugin_type(bridge) -> bridge; -plugin_type(_) -> feature. - - funlog(Key, Value) -> ?LOG(info, "~s = ~p", [string:join(Key, "."), Value]). - -generate_configs(App) -> - PluginConfDir = emqx:get_env(plugins_etc_dir), - PluginSchemaDir = code:priv_dir(App), - generate_configs(App, PluginConfDir, PluginSchemaDir). - -generate_configs(App, PluginDir) -> - PluginConfDir = filename:join([PluginDir, "etc"]), - PluginSchemaDir = filename:join([PluginDir, "priv"]), - generate_configs(App, PluginConfDir, PluginSchemaDir). - -generate_configs(App, PluginConfDir, PluginSchemaDir) -> - ConfigFile = filename:join([PluginConfDir, App]) ++ ".config", - case filelib:is_file(ConfigFile) of - true -> - {ok, [Configs]} = file:consult(ConfigFile), - apply_configs(Configs); - false -> - SchemaFile = filename:join([PluginSchemaDir, App]) ++ ".schema", - case filelib:is_file(SchemaFile) of - true -> - AppsEnv = do_generate_configs(App), - apply_configs(AppsEnv); - false -> - SchemaMod = lists:concat([App, "_schema"]), - ConfName = filename:join([PluginConfDir, App]) ++ ".conf", - SchemaFile1 = filename:join([code:lib_dir(App), "ebin", SchemaMod]) ++ ".beam", - do_generate_hocon_configs(App, ConfName, SchemaFile1) - end - end. - -do_generate_configs(App) -> - Name1 = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".conf", - Name2 = filename:join([code:lib_dir(App), "etc", App]) ++ ".conf", - ConfFile = case {filelib:is_file(Name1), filelib:is_file(Name2)} of - {true, _} -> Name1; - {false, true} -> Name2; - {false, false} -> error({config_not_found, [Name1, Name2]}) - end, - SchemaFile = filename:join([code:priv_dir(App), App]) ++ ".schema", - case filelib:is_file(SchemaFile) of - true -> - Schema = cuttlefish_schema:files([SchemaFile]), - Conf = cuttlefish_conf:file(ConfFile), - cuttlefish_generator:map(Schema, Conf, undefined, fun ?MODULE:funlog/2); - false -> - error({schema_not_found, SchemaFile}) - end. - -do_generate_hocon_configs(App, ConfName, SchemaFile) -> - SchemaMod = lists:concat([App, "_schema"]), - case {filelib:is_file(ConfName), filelib:is_file(SchemaFile)} of - {true, true} -> - {ok, RawConfig} = hocon:load(ConfName, #{format => richmap}), - _ = hocon_schema:check(list_to_atom(SchemaMod), RawConfig, #{atom_key => true, - return_plain => true}), - ok; - % emqx_config:update_config([App], Config); - {true, false} -> - error({schema_not_found, [SchemaFile]}); - {false, true} -> - error({config_not_found, [ConfName]}); - {false, false} -> - error({conf_and_schema_not_found, [ConfName, SchemaFile]}) - end. - -apply_configs([]) -> - ok; -apply_configs([{App, Config} | More]) -> - lists:foreach(fun({Key, _}) -> application:unset_env(App, Key) end, application:get_all_env(App)), - lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Config), - apply_configs(More). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index bb8b41cf9..440dd8117 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -65,6 +65,12 @@ includes() -> [ "emqx_data_bridge" , "emqx_telemetry" , "emqx_retainer" + , "emqx_statsd" + , "emqx_authn" + , "emqx_authz" + , "emqx_bridge_mqtt" + , "emqx_modules" + , "emqx_management" ]. -endif. diff --git a/apps/emqx/test/emqx_plugins_SUITE.erl b/apps/emqx/test/emqx_plugins_SUITE.erl index 6a76cb9d2..a7dd91602 100644 --- a/apps/emqx/test/emqx_plugins_SUITE.erl +++ b/apps/emqx/test/emqx_plugins_SUITE.erl @@ -63,64 +63,28 @@ t_load(_) -> ?assertEqual({error, not_started}, emqx_plugins:unload(emqx_hocon_plugin)), application:set_env(emqx, expand_plugins_dir, undefined), - application:set_env(emqx, plugins_loaded_file, undefined), - ?assertEqual(ignore, emqx_plugins:load()), - ?assertEqual(ignore, emqx_plugins:unload()). - - -t_init_config(_) -> - ConfFile = "emqx_mini_plugin.config", - Data = "[{emqx_mini_plugin,[{mininame ,test}]}].", - file:write_file(ConfFile, list_to_binary(Data)), - ?assertEqual(ok, emqx_plugins:init_config(ConfFile)), - file:delete(ConfFile), - ?assertEqual({ok,test}, application:get_env(emqx_mini_plugin, mininame)). + application:set_env(emqx, plugins_loaded_file, undefined). t_load_ext_plugin(_) -> ?assertError({plugin_app_file_not_found, _}, emqx_plugins:load_ext_plugin("./not_existed_path/")). t_list(_) -> - ?assertMatch([{plugin, _, _, _, _, _, _, _} | _ ], emqx_plugins:list()). + ?assertMatch([{plugin, _, _, _, _, _, _} | _ ], emqx_plugins:list()). t_find_plugin(_) -> - ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_mini_plugin)), - ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_hocon_plugin)). - -t_plugin_type(_) -> - ?assertEqual(auth, emqx_plugins:plugin_type(auth)), - ?assertEqual(protocol, emqx_plugins:plugin_type(protocol)), - ?assertEqual(backend, emqx_plugins:plugin_type(backend)), - ?assertEqual(bridge, emqx_plugins:plugin_type(bridge)), - ?assertEqual(feature, emqx_plugins:plugin_type(undefined)). - -t_with_loaded_file(_) -> - ?assertMatch({error, _}, emqx_plugins:with_loaded_file("./not_existed_path/", fun(_) -> ok end)). - -t_plugin_loaded(_) -> - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_mini_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_mini_plugin, true)), - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_hocon_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_loaded(emqx_hocon_plugin, true)). - -t_plugin_unloaded(_) -> - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_mini_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_mini_plugin, true)), - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_hocon_plugin, false)), - ?assertEqual(ok, emqx_plugins:plugin_unloaded(emqx_hocon_plugin, true)). + ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_mini_plugin)), + ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _}, emqx_plugins:find_plugin(emqx_hocon_plugin)). t_plugin(_) -> try - emqx_plugins:plugin(not_existed_plugin, undefined) + emqx_plugins:plugin(not_existed_plugin) catch _Error:Reason:_Stacktrace -> ?assertEqual({plugin_not_found,not_existed_plugin}, Reason) end, - ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _, _}, emqx_plugins:plugin(emqx_mini_plugin, undefined)), - ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _, _}, emqx_plugins:plugin(emqx_hocon_plugin, undefined)). - -t_filter_plugins(_) -> - ?assertEqual([name1, name2], emqx_plugins:filter_plugins([name1, {name2,true}, {name3, false}])). + ?assertMatch({plugin, emqx_mini_plugin, _, _, _, _, _}, emqx_plugins:plugin(emqx_mini_plugin)), + ?assertMatch({plugin, emqx_hocon_plugin, _, _, _, _, _}, emqx_plugins:plugin(emqx_hocon_plugin)). t_load_plugin(_) -> ok = meck:new(application, [unstick, non_strict, passthrough, no_history]), @@ -133,9 +97,9 @@ t_load_plugin(_) -> ok = meck:new(emqx_plugins, [unstick, non_strict, passthrough, no_history]), ok = meck:expect(emqx_plugins, generate_configs, fun(_) -> ok end), ok = meck:expect(emqx_plugins, apply_configs, fun(_) -> ok end), - ?assertMatch({error, _}, emqx_plugins:load_plugin(already_loaded_app, true)), - ?assertMatch(ok, emqx_plugins:load_plugin(normal, true)), - ?assertMatch({error,_}, emqx_plugins:load_plugin(error_app, true)), + ?assertMatch({error, _}, emqx_plugins:load_plugin(already_loaded_app)), + ?assertMatch(ok, emqx_plugins:load_plugin(normal)), + ?assertMatch({error,_}, emqx_plugins:load_plugin(error_app)), ok = meck:unload(emqx_plugins), ok = meck:unload(application). @@ -146,8 +110,8 @@ t_unload_plugin(_) -> (error_app) -> {error, error}; (_) -> ok end), - ?assertEqual(ok, emqx_plugins:unload_plugin(not_started_app, true)), - ?assertEqual(ok, emqx_plugins:unload_plugin(normal, true)), - ?assertEqual({error,error}, emqx_plugins:unload_plugin(error_app, true)), + ?assertEqual(ok, emqx_plugins:unload_plugin(not_started_app)), + ?assertEqual(ok, emqx_plugins:unload_plugin(normal)), + ?assertEqual({error,error}, emqx_plugins:unload_plugin(error_app)), ok = meck:unload(application). diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 092bd5404..ecd49d5a5 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,4 +1,4 @@ -authn: { +emqx_authn: { chains: [ # { # id: "chain1" diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 9e60e5dda..033c760af 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -38,11 +38,8 @@ stop(_State) -> ok. initialize() -> - ConfFile = filename:join([emqx:get_env(plugins_etc_dir), ?APP]) ++ ".conf", - {ok, RawConfig} = hocon:load(ConfFile), - #{authn := #{chains := Chains, - bindings := Bindings}} - = hocon_schema:check_plain(emqx_authn_schema, RawConfig, #{atom_key => true, nullable => true}), + #{chains := Chains, + bindings := Bindings} = emqx_config:get([authn], #{chains => [], bindings => []}), initialize_chains(Chains), initialize_bindings(Bindings). diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 0464350dd..d9bf72910 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -27,9 +27,9 @@ , authenticator_name/0 ]). -structs() -> [authn]. +structs() -> ["emqx_authn"]. -fields(authn) -> +fields("emqx_authn") -> [ {chains, fun chains/1} , {bindings, fun bindings/1}]; @@ -80,7 +80,7 @@ fields(pgsql) -> , {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))} ]. -chains(type) -> hoconsc:array({union, [hoconsc:ref('simple-chain')]}); +chains(type) -> hoconsc:array({union, [hoconsc:ref(?MODULE, 'simple-chain')]}); chains(default) -> []; chains(_) -> undefined. @@ -89,10 +89,10 @@ chain_id(nullable) -> false; chain_id(_) -> undefined. simple_authenticators(type) -> - hoconsc:array({union, [ hoconsc:ref('built-in-database') - , hoconsc:ref(jwt) - , hoconsc:ref(mysql) - , hoconsc:ref(pgsql)]}); + hoconsc:array({union, [ hoconsc:ref(?MODULE, 'built-in-database') + , hoconsc:ref(?MODULE, jwt) + , hoconsc:ref(?MODULE, mysql) + , hoconsc:ref(?MODULE, pgsql)]}); simple_authenticators(default) -> []; simple_authenticators(_) -> undefined. @@ -105,7 +105,7 @@ authenticator_name(type) -> authenticator_name(); authenticator_name(nullable) -> false; authenticator_name(_) -> undefined. -bindings(type) -> hoconsc:array(hoconsc:ref(binding)); +bindings(type) -> hoconsc:array(hoconsc:ref(?MODULE, binding)); bindings(default) -> []; bindings(_) -> undefined. diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 9be9fdce8..6710e4f69 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -36,11 +36,7 @@ register_metrics() -> init() -> ok = register_metrics(), - Conf = filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), - {ok, RawConf} = hocon:load(Conf), - #{emqx_authz := #{rules := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf, #{atom_key => true}), - emqx_config:put([emqx_authz], #{rules => Rules}), - % Rules = emqx_config:get([emqx_authz, rules], []), + Rules = emqx_config:get([emqx_authz, rules], []), NRules = [compile(Rule) || Rule <- Rules], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 944053f4e..66b2e62de 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -41,11 +41,7 @@ set_special_configs(emqx) -> application:set_env(emqx, enable_acl_cache, false), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"emqx_authz">> => #{<<"rules">> => []}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - % emqx_config:put([emqx_authz], #{rules => []}), + emqx_config:put([emqx_authz], #{rules => []}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index b813ad1f7..24683cd5b 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -55,22 +55,13 @@ set_special_configs(emqx) -> application:set_env(emqx, enable_acl_cache, false), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"emqx_authz">> => #{<<"rules">> => []}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - % emqx_config:put([emqx_authz], #{rules => []}), + emqx_config:put([emqx_authz], #{rules => []}), ok; set_special_configs(emqx_management) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_management, "test")), - Conf = #{<<"emqx_management">> => #{ - <<"listeners">> => [#{ - <<"protocol">> => <<"http">> - }]} - }, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), ok; set_special_configs(_App) -> diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 039d50383..d84f0722a 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -47,22 +47,12 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"emqx_authz">> => - #{<<"rules">> => - [#{<<"config">> =>#{<<"meck">> => <<"fake">>}, - <<"principal">> => all, - <<"sql">> => <<"fake sql">>, - <<"type">> => mysql} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - % Rules = [#{config =>#{<<"meck">> => <<"fake">>}, - % principal => all, - % sql => <<"fake sql">>, - % type => mysql} - % ], - % emqx_config:put([emqx_authz], #{rules => Rules}), + Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + principal => all, + sql => <<"fake sql">>, + type => mysql} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 9dcdd7c18..78112b5bc 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -47,22 +47,12 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"emqx_authz">> => - #{<<"rules">> => - [#{<<"config">> =>#{<<"meck">> => <<"fake">>}, - <<"principal">> => all, - <<"sql">> => <<"fake sql">>, - <<"type">> => pgsql} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - % Rules = [#{config =>#{<<"meck">> => <<"fake">>}, - % principal => all, - % sql => <<"fake sql">>, - % type => pgsql} - % ], - % emqx_config:put([emqx_authz], #{rules => Rules}), + Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + principal => all, + sql => <<"fake sql">>, + type => pgsql} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 8cb7a51b9..edff8a2a9 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -47,28 +47,12 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"emqx_authz">> => - #{<<"rules">> => - [#{<<"config">> =>#{ - <<"server">> => <<"127.0.0.1:6379">>, - <<"password">> => <<"public">>, - <<"pool_size">> => 1, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"principal">> => all, - <<"cmd">> => <<"fake cmd">>, - <<"type">> => redis} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - % Rules = [#{config =>#{<<"meck">> => <<"fake">>}, - % principal => all, - % cmd => <<"fake cmd">>, - % type => redis} - % ], - % emqx_config:put([emqx_authz], #{rules => Rules}), + Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + principal => all, + cmd => <<"fake cmd">>, + type => redis} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf index 4593e04f0..c34567ee4 100644 --- a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf +++ b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf @@ -3,54 +3,56 @@ ##==================================================================== emqx_bridge_mqtt:{ - bridges:[{ - name: "mqtt1" - start_type: auto - forwards: ["test/#"], - forward_mountpoint: "" - reconnect_interval: "30s" - batch_size: 100 - queue:{ - replayq_dir: false - # replayq_seg_bytes: "100MB" - # replayq_offload_mode: false - # replayq_max_total_bytes: "1GB" - }, - config:{ - conn_type: mqtt - address: "127.0.0.1:1883" - proto_ver: v4 - bridge_mode: true - clientid: "client1" - clean_start: true - username: "username1" - password: "" - keepalive: 300 - subscriptions: [{ - topic: "t/#" - qos: 1 - }] - receive_mountpoint: "" - retry_interval: "30s" - max_inflight: 32 - } - }, - { - name: "rpc1" - start_type: auto - forwards: ["test/#"], - forward_mountpoint: "" - reconnect_interval: "30s" - batch_size: 100 - queue:{ - replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" - replayq_seg_bytes: "100MB" - replayq_offload_mode: false - replayq_max_total_bytes: "1GB" - }, - config:{ - conn_type: rpc - node: "emqx@127.0.0.1" - } - }] + bridges:[ + # { + # name: "mqtt1" + # start_type: auto + # forwards: ["test/#"], + # forward_mountpoint: "" + # reconnect_interval: "30s" + # batch_size: 100 + # queue:{ + # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" + # replayq_seg_bytes: "100MB" + # replayq_offload_mode: false + # replayq_max_total_bytes: "1GB" + # }, + # config:{ + # conn_type: mqtt + # address: "127.0.0.1:1883" + # proto_ver: v4 + # bridge_mode: true + # clientid: "client1" + # clean_start: true + # username: "username1" + # password: "" + # keepalive: 300 + # subscriptions: [{ + # topic: "t/#" + # qos: 1 + # }] + # receive_mountpoint: "" + # retry_interval: "30s" + # max_inflight: 32 + # } + # }, + # { + # name: "rpc1" + # start_type: auto + # forwards: ["test/#"], + # forward_mountpoint: "" + # reconnect_interval: "30s" + # batch_size: 100 + # queue:{ + # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" + # replayq_seg_bytes: "100MB" + # replayq_offload_mode: false + # replayq_max_total_bytes: "1GB" + # }, + # config:{ + # conn_type: rpc + # node: "emqx@127.0.0.1" + # } + # } + ] } diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl index fcd2f2c1d..8cc87ef64 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -26,7 +26,7 @@ structs() -> ["emqx_bridge_mqtt"]. fields("emqx_bridge_mqtt") -> - [ {bridges, hoconsc:array("bridges")} + [ {bridges, hoconsc:array(hoconsc:ref(?MODULE, "bridges"))} ]; fields("bridges") -> @@ -36,8 +36,8 @@ fields("bridges") -> , {forward_mountpoint, emqx_schema:t(string())} , {reconnect_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} , {batch_size, emqx_schema:t(integer(), undefined, 100)} - , {queue, emqx_schema:t(hoconsc:ref("queue"))} - , {config, hoconsc:union([hoconsc:ref("mqtt"), hoconsc:ref("rpc")])} + , {queue, emqx_schema:t(hoconsc:ref(?MODULE, "queue"))} + , {config, hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), hoconsc:ref(?MODULE, "rpc")])} ]; fields("mqtt") -> diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index afec49419..3ea8ab743 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -40,18 +40,7 @@ -define(OVERVIEWS, ['alarms/activated', 'alarms/deactivated', banned, brokers, stats, metrics, listeners, clients, subscriptions, routes, plugins]). all() -> - [{group, overview}, - {group, admins}, - {group, rest}, - {group, cli} - ]. - -groups() -> - [{overview, [sequence], [t_overview]}, - {admins, [sequence], [t_admins_add_delete]}, - {rest, [sequence], [t_rest_api]}, - {cli, [sequence], [t_cli]} - ]. + emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([emqx_management, emqx_dashboard],fun set_special_configs/1), @@ -62,29 +51,27 @@ end_per_suite(_Config) -> ekka_mnesia:ensure_stopped(). set_special_configs(emqx_management) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_management, "test")), - Conf = #{<<"emqx_management">> => #{ - <<"listeners">> => [#{ - <<"protocol">> => <<"http">> - }]} - }, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), ok; set_special_configs(_) -> ok. t_overview(_) -> + mnesia:clear_table(mqtt_admin), + emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"tag">>), [?assert(request_dashboard(get, api_path(erlang:atom_to_list(Overview)), auth_header_()))|| Overview <- ?OVERVIEWS]. t_admins_add_delete(_) -> + mnesia:clear_table(mqtt_admin), ok = emqx_dashboard_admin:add_user(<<"username">>, <<"password">>, <<"tag">>), ok = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, <<"tag1">>), Admins = emqx_dashboard_admin:all_users(), - ?assertEqual(3, length(Admins)), + ?assertEqual(2, length(Admins)), ok = emqx_dashboard_admin:remove_user(<<"username1">>), Users = emqx_dashboard_admin:all_users(), - ?assertEqual(2, length(Users)), + ?assertEqual(1, length(Users)), ok = emqx_dashboard_admin:change_password(<<"username">>, <<"password">>, <<"pwd">>), timer:sleep(10), ?assert(request_dashboard(get, api_path("brokers"), auth_header_("username", "pwd"))), @@ -93,6 +80,8 @@ t_admins_add_delete(_) -> ?assertNotEqual(true, request_dashboard(get, api_path("brokers"), auth_header_("username", "pwd"))). t_rest_api(_Config) -> + mnesia:clear_table(mqtt_admin), + emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"administrator">>), {ok, Res0} = http_get("users"), ?assertEqual([#{<<"username">> => <<"admin">>, diff --git a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf index 696cadf05..c299b97a1 100644 --- a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf +++ b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf @@ -2,125 +2,128 @@ ## EMQ X Bridge Plugin ##-------------------------------------------------------------------- -emqx_data_bridge.bridges: [ -# {name: "mysql_bridge_1" -# type: mysql -# config: { -# server: "192.168.0.172:3306" -# database: mqtt -# pool_size: 1 -# username: root -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# , {name: "pgsql_bridge_1" -# type: pgsql -# config: { -# server: "192.168.0.172:5432" -# database: mqtt -# pool_size: 1 -# username: root -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# , {name: "mongodb_bridge_single" -# type: mongo -# config: { -# servers: "192.168.0.172:27017" -# mongo_type: single -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# } -# } -# ,{name: "mongodb_bridge_rs" -# type: mongo -# config: { -# servers: "127.0.0.1:27017" -# mongo_type: rs -# rs_set_name: rs_name -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# } -# } -# ,{name: "mongodb_bridge_shared" -# type: mongo -# config: { -# servers: "127.0.0.1:27017" -# mongo_type: shared -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# max_overflow: 1 -# overflow_ttl: -# overflow_check_period: 10s -# local_threshold_ms: 10s -# connect_timeout_ms: 10s -# socket_timeout_ms: 10s -# server_selection_timeout_ms: 10s -# wait_queue_timeout_ms: 10s -# heartbeat_frequency_ms: 10s -# min_heartbeat_frequency_ms: 10s -# } -# } -# , {name: "redis_bridge_single" -# type: redis -# config: { -# servers: "192.168.0.172:6379" -# redis_type: single -# pool_size: 1 -# database: 0 -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# ,{name: "redis_bridge_sentinel" -# type: redis -# config: { -# servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" -# redis_type: sentinel -# sentinel_name: mymaster -# pool_size: 1 -# database: 0 -# ssl: false -# } -# } -# ,{name: "redis_bridge_cluster" -# type: redis -# config: { -# servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" -# redis_type: cluster -# pool_size: 1 -# database: 0 -# password: "public" -# ssl: false -# } -# } -# , {name: "ldap_bridge_1" -# type: ldap -# config: { -# servers: "192.168.0.172" -# port: 389 -# bind_dn: "cn=root,dc=emqx,dc=io" -# bind_password: "public" -# timeout: 30s -# pool_size: 1 -# ssl: false -# } -# } -] +emqx_data_bridge:{ + bridges:[ + # {name: "mysql_bridge_1" + # type: mysql + # config: { + # server: "192.168.0.172:3306" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: false + # } + # } + # , {name: "pgsql_bridge_1" + # type: pgsql + # config: { + # server: "192.168.0.172:5432" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: false + # } + # } + # , {name: "mongodb_bridge_single" + # type: mongo + # config: { + # servers: "192.168.0.172:27017" + # mongo_type: single + # pool_size: 1 + # login: root + # password: public + # auth_source: mqtt + # database: mqtt + # ssl: false + # } + # } + # ,{name: "mongodb_bridge_rs" + # type: mongo + # config: { + # servers: "127.0.0.1:27017" + # mongo_type: rs + # rs_set_name: rs_name + # pool_size: 1 + # login: root + # password: public + # auth_source: mqtt + # database: mqtt + # ssl: false + # } + # } + # ,{name: "mongodb_bridge_shared" + # type: mongo + # config: { + # servers: "127.0.0.1:27017" + # mongo_type: shared + # pool_size: 1 + # login: root + # password: public + # auth_source: mqtt + # database: mqtt + # ssl: false + # max_overflow: 1 + # overflow_ttl: + # overflow_check_period: 10s + # local_threshold_ms: 10s + # connect_timeout_ms: 10s + # socket_timeout_ms: 10s + # server_selection_timeout_ms: 10s + # wait_queue_timeout_ms: 10s + # heartbeat_frequency_ms: 10s + # min_heartbeat_frequency_ms: 10s + # } + # } + # , {name: "redis_bridge_single" + # type: redis + # config: { + # servers: "192.168.0.172:6379" + # redis_type: single + # pool_size: 1 + # database: 0 + # password: public + # auto_reconnect: true + # ssl: false + # } + # } + # ,{name: "redis_bridge_sentinel" + # type: redis + # config: { + # servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" + # redis_type: sentinel + # sentinel_name: mymaster + # pool_size: 1 + # database: 0 + # ssl: false + # } + # } + # ,{name: "redis_bridge_cluster" + # type: redis + # config: { + # servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" + # redis_type: cluster + # pool_size: 1 + # database: 0 + # password: "public" + # ssl: false + # } + # } + # , {name: "ldap_bridge_1" + # type: ldap + # config: { + # servers: "192.168.0.172" + # port: 389 + # bind_dn: "cn=root,dc=emqx,dc=io" + # bind_password: "public" + # timeout: 30s + # pool_size: 1 + # ssl: false + # } + # } + + ] +} diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl index 1ba7f2fc5..b4749af0e 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -23,4 +23,4 @@ fields(mysql) -> ?BRIDGE_FIELDS(mysql); fields(pgsql) -> ?BRIDGE_FIELDS(pgsql); fields(mongo) -> ?BRIDGE_FIELDS(mongo); fields(redis) -> ?BRIDGE_FIELDS(redis); -fields(ldap) -> ?BRIDGE_FIELDS(ldap). \ No newline at end of file +fields(ldap) -> ?BRIDGE_FIELDS(ldap). diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index fe68fef44..6cd38d928 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -1,6 +1,6 @@ {application, emqx_management, [{description, "EMQ X Management API and CLI"}, - {vsn, "4.4.0"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel,stdlib,minirest]}, diff --git a/apps/emqx_management/src/emqx_management.appup.src b/apps/emqx_management/src/emqx_management.appup.src deleted file mode 100644 index 06945afad..000000000 --- a/apps/emqx_management/src/emqx_management.appup.src +++ /dev/null @@ -1,13 +0,0 @@ -%% -*- mode: erlang -*- -{VSN, - [ {<<"4.3.[0-2]">>, - [ {restart_application, emqx_management} - ]}, - {<<".*">>, []} - ], - [ {<<"4.3.[0-2]">>, - [ {restart_application, emqx_management} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl index 02f3e2f1a..0b99d1046 100644 --- a/apps/emqx_management/src/emqx_management_schema.erl +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -28,7 +28,7 @@ fields("emqx_management") -> [ {default_application_id, fun default_application_id/1} , {default_application_secret, fun default_application_secret/1} , {max_row_limit, fun max_row_limit/1} - , {listeners, hoconsc:array(hoconsc:union([hoconsc:ref("http"), hoconsc:ref("https")]))} + , {listeners, hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"), hoconsc:ref(?MODULE, "https")]))} ]; fields("http") -> diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index ce83b9b71..1aea90459 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -525,7 +525,7 @@ check_row_limit([Tab|Tables], Limit) -> end. max_row_limit() -> - application:get_env(?APP, max_row_limit, ?MAX_ROW_LIMIT). + emqx_config:get([?APP, max_row_limit], ?MAX_ROW_LIMIT). table_size(Tab) -> ets:info(Tab, size). diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl index e4908aeff..369ad6782 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl @@ -106,10 +106,8 @@ format({Node, Plugins}) -> format(#plugin{name = Name, descr = Descr, - active = Active, - type = Type}) -> + active = Active}) -> #{name => Name, description => iolist_to_binary(Descr), - active => Active, - type => Type}. + active => Active}. diff --git a/apps/emqx_management/src/emqx_mgmt_app.erl b/apps/emqx_management/src/emqx_mgmt_app.erl index 7161435f6..48d57f232 100644 --- a/apps/emqx_management/src/emqx_mgmt_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_app.erl @@ -29,11 +29,6 @@ -include("emqx_mgmt.hrl"). start(_Type, _Args) -> - Conf = filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), - {ok, RawConf} = hocon:load(Conf), - #{emqx_management := Config} = - hocon_schema:check_plain(emqx_management_schema, RawConf, #{atom_key => true}), - [application:set_env(?APP, Key, maps:get(Key, Config)) || Key <- maps:keys(Config)], {ok, Sup} = emqx_mgmt_sup:start_link(), ok = ekka_rlog:wait_for_shards([?MANAGEMENT_SHARD], infinity), _ = emqx_mgmt_auth:add_default_app(), diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 297de0196..fbba0b2a4 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -68,14 +68,14 @@ mnesia(copy) -> %%-------------------------------------------------------------------- -spec(add_default_app() -> ok | {ok, appsecret()} | {error, term()}). add_default_app() -> - AppId = application:get_env(?APP, default_application_id, undefined), - AppSecret = application:get_env(?APP, default_application_secret, undefined), + AppId = emqx_config:get([?APP, default_application_id], undefined), + AppSecret = emqx_config:get([?APP, default_application_secret], undefined), case {AppId, AppSecret} of {undefined, _} -> ok; {_, undefined} -> ok; {_, _} -> - AppId1 = erlang:list_to_binary(AppId), - AppSecret1 = erlang:list_to_binary(AppSecret), + AppId1 = to_binary(AppId), + AppSecret1 = to_binary(AppSecret), add_app(AppId1, <<"Default">>, AppSecret1, <<"Application user">>, true, undefined) end. @@ -210,3 +210,6 @@ is_authorized(AppId, AppSecret) -> is_expired(undefined) -> true; is_expired(Expired) -> Expired >= erlang:system_time(second). + +to_binary(L) when is_list(L) -> list_to_binary(L); +to_binary(B) when is_binary(B) -> B. diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index c23219e5d..ecf204128 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -80,7 +80,7 @@ stop_listener({Proto, Port, _}) -> listeners() -> [{list_to_atom(Protocol), Port, maps:to_list(maps:without([protocol, port], Map))} || Map = #{protocol := Protocol,port := Port} - <- application:get_env(?APP, listeners, [])]. + <- emqx_config:get([emqx_management, listeners], [])]. listener_name(Proto) -> list_to_atom(atom_to_list(Proto) ++ ":management"). diff --git a/apps/emqx_management/test/emqx_mgmt_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_SUITE.erl index f14cc0fef..415bfca2e 100644 --- a/apps/emqx_management/test/emqx_mgmt_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_SUITE.erl @@ -29,50 +29,19 @@ -define(LOG_HANDLER_ID, [file, default]). all() -> - [{group, manage_apps}, - {group, check_cli}]. - -groups() -> - [{manage_apps, [sequence], - [t_app - ]}, - {check_cli, [sequence], - [t_cli, - t_log_cmd, - t_mgmt_cmd, - t_status_cmd, - t_clients_cmd, - t_vm_cmd, - t_plugins_cmd, - t_trace_cmd, - t_broker_cmd, - t_router_cmd, - t_subscriptions_cmd, - t_listeners_cmd_old, - t_listeners_cmd_new - ]}]. - -apps() -> - [emqx_management, emqx_retainer]. + emqx_ct:all(?MODULE). init_per_suite(Config) -> ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), - emqx_ct_helpers:start_apps(apps(), fun set_special_configs/1), + emqx_ct_helpers:start_apps([emqx_retainer, emqx_management], fun set_special_configs/1), Config. end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps(apps()). + emqx_ct_helpers:stop_apps([emqx_management, emqx_retainer]). set_special_configs(emqx_management) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_management, "test")), - Conf = #{<<"emqx_management">> => #{ - <<"listeners">> => [#{ - <<"protocol">> => <<"http">> - }]} - }, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}]}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl index 0785b9563..66d9328a6 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl @@ -51,14 +51,9 @@ end_per_testcase(_, Config) -> Config. set_special_configs(emqx_management) -> - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_management, "test")), - Conf = #{<<"emqx_management">> => #{ - <<"listeners">> => [#{ - <<"protocol">> => <<"http">> - }]} - }, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_modules/src/emqx_modules.erl b/apps/emqx_modules/src/emqx_modules.erl index 6610a4716..11b4cdcbe 100644 --- a/apps/emqx_modules/src/emqx_modules.erl +++ b/apps/emqx_modules/src/emqx_modules.erl @@ -143,6 +143,8 @@ cli(_) -> {"modules reload ", "Reload module"} ]). +name(Name) when is_binary(Name) -> + name(binary_to_atom(Name, utf8)); name(delayed) -> emqx_mod_delayed; name(presence) -> emqx_mod_presence; name(recon) -> emqx_mod_recon; diff --git a/apps/emqx_modules/test/emqx_modules_SUITE.erl b/apps/emqx_modules/test/emqx_modules_SUITE.erl index db85cbd0e..897c73d50 100644 --- a/apps/emqx_modules/test/emqx_modules_SUITE.erl +++ b/apps/emqx_modules/test/emqx_modules_SUITE.erl @@ -37,15 +37,9 @@ init_per_suite(Config) -> Config. set_special_configs(emqx_management) -> - application:set_env(emqx, modules_loaded_file, emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_modules")), - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_management, "test")), - Conf = #{<<"emqx_management">> => #{ - <<"listeners">> => [#{ - <<"protocol">> => <<"http">> - }]} - }, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_management.conf'), jsx:encode(Conf)), + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), ok; set_special_configs(_) -> ok. diff --git a/apps/emqx_sn/vars b/apps/emqx_sn/vars index a170916f3..e39aa2801 100644 --- a/apps/emqx_sn/vars +++ b/apps/emqx_sn/vars @@ -5,4 +5,4 @@ {platform_etc_dir, "etc"}. {platform_lib_dir, "lib"}. {platform_log_dir, "log"}. -{platform_plugins_dir, "plugins"}. +{platform_plugins_dir, "data/plugins"}. diff --git a/data/loaded_plugins.tmpl b/data/loaded_plugins.tmpl deleted file mode 100644 index dc23386dc..000000000 --- a/data/loaded_plugins.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{emqx_management, true}. -{emqx_dashboard, true}. -{emqx_retainer, {{enable_plugin_emqx_retainer}}}. diff --git a/rebar.config.erl b/rebar.config.erl index 13bbb1c15..01abf254f 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -127,8 +127,9 @@ prod_compile_opts() -> prod_overrides() -> [{add, [ {erl_opts, [deterministic]}]}]. -relup_deps(Profile) -> - {post_hooks, [{"(linux|darwin|solaris|freebsd|netbsd|openbsd)", compile, "scripts/inject-deps.escript " ++ atom_to_list(Profile)}]}. +relup_deps(_Profile) -> + % {post_hooks, [{"(linux|darwin|solaris|freebsd|netbsd|openbsd)", compile, "scripts/inject-deps.escript " ++ atom_to_list(Profile)}]}. + {post_hooks, []}. profiles() -> Vsn = get_vsn(), @@ -192,9 +193,8 @@ overlay_vars_rel(RelType) -> cloud -> "vm.args"; edge -> "vm.args.edge" end, - [ - {enable_plugin_emqx_retainer, true} - , {vm_args_file, VmArgs} + + [ {vm_args_file, VmArgs} ]. %% vars per packaging type, bin(zip/tar.gz/docker) or pkg(rpm/deb) @@ -204,7 +204,7 @@ overlay_vars_pkg(bin) -> , {platform_etc_dir, "etc"} , {platform_lib_dir, "lib"} , {platform_log_dir, "log"} - , {platform_plugins_dir, "etc/plugins"} + , {platform_plugins_dir, "plugins"} , {runner_root_dir, "$(cd $(dirname $(readlink $0 || echo $0))/..; pwd -P)"} , {runner_bin_dir, "$RUNNER_ROOT_DIR/bin"} , {runner_etc_dir, "$RUNNER_ROOT_DIR/etc"} @@ -219,7 +219,7 @@ overlay_vars_pkg(pkg) -> , {platform_etc_dir, "/etc/emqx"} , {platform_lib_dir, ""} , {platform_log_dir, "/var/log/emqx"} - , {platform_plugins_dir, "/var/lib/emqx/plugins"} + , {platform_plugins_dir, "/var/lib/enqx/plugins"} , {runner_root_dir, "/usr/lib/emqx"} , {runner_bin_dir, "/usr/bin"} , {runner_etc_dir, "/etc/emqx"} @@ -255,8 +255,12 @@ relx_apps(ReleaseType) -> , emqx_connector , emqx_data_bridge , emqx_rule_engine + , emqx_rule_actions , emqx_bridge_mqtt , emqx_modules + , emqx_management + , emqx_retainer + , emqx_statsd ] ++ [emqx_telemetry || not is_enterprise()] ++ [emqx_license || is_enterprise()] @@ -279,25 +283,13 @@ is_app(Name) -> end. relx_plugin_apps(ReleaseType) -> - [ emqx_retainer - , emqx_management - , emqx_dashboard - , emqx_sn - , emqx_coap - , emqx_stomp - , emqx_statsd - , emqx_rule_actions - ] + [] ++ relx_plugin_apps_per_rel(ReleaseType) ++ relx_plugin_apps_enterprise(is_enterprise()) ++ relx_plugin_apps_extra(). relx_plugin_apps_per_rel(cloud) -> - [ emqx_lwm2m - , emqx_exhook - , emqx_exproto - , emqx_prometheus - ]; + []; relx_plugin_apps_per_rel(edge) -> []. @@ -313,11 +305,11 @@ relx_plugin_apps_extra() -> relx_overlay(ReleaseType) -> [ {mkdir, "log/"} , {mkdir, "data/"} + , {mkdir, "plugins"} , {mkdir, "data/mnesia"} , {mkdir, "data/configs"} , {mkdir, "data/patches"} , {mkdir, "data/scripts"} - , {template, "data/loaded_plugins.tmpl", "data/loaded_plugins"} , {template, "data/emqx_vars", "releases/emqx_vars"} , {template, "data/BUILT_ON", "releases/{{release_version}}/BUILT_ON"} , {copy, "bin/emqx", "bin/emqx"} @@ -337,12 +329,8 @@ relx_overlay(ReleaseType) -> end. etc_overlay(ReleaseType) -> - PluginApps = relx_plugin_apps(ReleaseType), - Templates = emqx_etc_overlay(ReleaseType) ++ - lists:append([plugin_etc_overlays(App) || App <- PluginApps]) ++ - [community_plugin_etc_overlays(App) || App <- relx_plugin_apps_extra()], + Templates = emqx_etc_overlay(ReleaseType), [ {mkdir, "etc/"} - , {mkdir, "etc/plugins"} , {copy, "{{base_dir}}/lib/emqx/etc/certs","etc/"} ] ++ lists:map( @@ -352,7 +340,7 @@ etc_overlay(ReleaseType) -> ++ extra_overlay(ReleaseType). extra_overlay(cloud) -> - [ {copy,"{{base_dir}}/lib/emqx_lwm2m/lwm2m_xml","etc/"} + [ ]; extra_overlay(edge) -> []. @@ -366,42 +354,9 @@ emqx_etc_overlay(edge) -> ]. emqx_etc_overlay_common() -> - [{"{{base_dir}}/lib/emqx/etc/acl.conf", "etc/acl.conf"}, - {"{{base_dir}}/lib/emqx/etc/emqx.conf", "etc/emqx.conf"}, - {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}, - {"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"}, - {"{{base_dir}}/lib/emqx_telemetry/etc/emqx_telemetry.conf", "etc/plugins/emqx_telemetry.conf"}, - {"{{base_dir}}/lib/emqx_authn/etc/emqx_authn.conf", "etc/plugins/emqx_authn.conf"}, - {"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"}, - {"{{base_dir}}/lib/emqx_rule_engine/etc/emqx_rule_engine.conf", "etc/plugins/emqx_rule_engine.conf"}, - {"{{base_dir}}/lib/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf", "etc/plugins/emqx_bridge_mqtt.conf"}, - {"{{base_dir}}/lib/emqx_modules/etc/emqx_modules.conf", "etc/plugins/emqx_modules.conf"}, - %% TODO: check why it has to end with .paho - %% and why it is put to etc/plugins dir - {"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}]. - -plugin_etc_overlays(App0) -> - App = atom_to_list(App0), - ConfFiles = find_conf_files(App), - %% NOTE: not filename:join here since relx translates it for windows - [{"{{base_dir}}/lib/"++ App ++"/etc/" ++ F, "etc/plugins/" ++ F} - || F <- ConfFiles]. - -community_plugin_etc_overlays(App0) -> - App = atom_to_list(App0), - {"{{base_dir}}/lib/"++ App ++"/etc/" ++ App ++ ".conf", "etc/plugins/" ++ App ++ ".conf"}. - -%% NOTE: for apps fetched as rebar dependency (there is so far no such an app) -%% the overlay should be hand-coded but not to rely on build-time wildcards. -find_conf_files(App) -> - Dir1 = filename:join(["apps", App, "etc"]), - filelib:wildcard("*.conf", Dir1) ++ - case is_enterprise() of - true -> - Dir2 = filename:join(["lib-ee", App, "etc"]), - filelib:wildcard("*.conf", Dir2); - false -> [] - end. + [ {"{{base_dir}}/lib/emqx/etc/emqx.conf.all", "etc/emqx.conf"} + , {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"} + ]. get_vsn() -> PkgVsn = os:cmd("./pkg-vsn.sh"), diff --git a/scripts/find-apps.sh b/scripts/find-apps.sh index 54467b62b..e437c49c5 100755 --- a/scripts/find-apps.sh +++ b/scripts/find-apps.sh @@ -7,7 +7,7 @@ cd -P -- "$(dirname -- "$0")/.." find_app() { local appdir="$1" - find "${appdir}" -mindepth 1 -maxdepth 1 -type d | grep -vE "emqx_exhook|emqx_exproto|emqx_lwm2m|emqx_sn|emqx_coap|emqx_stomp" + find "${appdir}" -mindepth 1 -maxdepth 1 -type d | grep -vE "emqx_exhook|emqx_exproto|emqx_lwm2m|emqx_sn|emqx_coap|emqx_stomp|emqx_dashboard" } find_app 'apps' diff --git a/scripts/merge-config.escript b/scripts/merge-config.escript new file mode 100755 index 000000000..a0fbdb4b3 --- /dev/null +++ b/scripts/merge-config.escript @@ -0,0 +1,36 @@ +#!/usr/bin/env escript + +%% This script reads up emqx.conf and split the sections +%% and dump sections to separate files. +%% Sections are grouped between CONFIG_SECTION_BGN and +%% CONFIG_SECTION_END pairs +%% +%% NOTE: this feature is so far not used in opensource +%% edition due to backward-compatibility reasons. + +-mode(compile). + +main(_) -> + BaseConf = "apps/emqx/etc/emqx.conf", + {ok, Bin} = file:read_file(BaseConf), + Apps = filelib:wildcard("emqx_*", "apps/"), + Conf = lists:foldl(fun(App, Acc) -> + case lists:member(App, ["emqx_exhook", + "emqx_exproto", + "emqx_lwm2m", + "emqx_sn", + "emqx_coap", + "emqx_stomp", + "emqx_dashboard"]) of + true -> Acc; + false -> + Filename = filename:join([apps, App, "etc", App]) ++ ".conf", + case filelib:is_regular(Filename) of + true -> + {ok, Bin1} = file:read_file(Filename), + <>; + false -> Acc + end + end + end, Bin, Apps), + ok = file:write_file("apps/emqx/etc/emqx.conf.all", Conf). From 390b28a25d65917a49851f8779cc263dd97dc374 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 14:40:19 +0800 Subject: [PATCH 068/379] fix: join cluster fail --- apps/emqx/src/emqx.erl | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 0a290ff4a..bd46ddc03 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -239,7 +239,7 @@ default_started_applications() -> [gproc, esockd, ranch, cowboy, ekka, quicer, emqx]. -else. default_started_applications() -> - [gproc, esockd, ranch, cowboy, ekka, quicer, emqx, emqx_modules]. + [gproc, esockd, ranch, cowboy, ekka, quicer, emqx] ++ emqx_feature(). -endif. %%-------------------------------------------------------------------- @@ -252,3 +252,19 @@ reload_config(ConfFile) -> [application:set_env(App, Par, Val) || {Par, Val} <- Vals] end, Conf). + +emqx_feature() -> + [ emqx_authn + , emqx_authz + , observer_cli + , emqx_http_lib + , emqx_resource + , emqx_connector + , emqx_data_bridge + , emqx_rule_engine + , emqx_rule_actions + , emqx_bridge_mqtt + , emqx_modules + , emqx_management + , emqx_retainer + , emqx_statsd]. From b93ec0a83aed11cbd9af146455b91e4660814896 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Fri, 2 Jul 2021 15:03:05 +0800 Subject: [PATCH 069/379] chore(CI): fix github workflows error (#1) --- .github/workflows/build_slim_packages.yaml | 2 +- .github/workflows/run_fvt_tests.yaml | 20 ++++++++++++++++++-- deploy/docker/docker-entrypoint.sh | 22 ---------------------- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 1f79b9ab2..30768e023 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -112,7 +112,7 @@ jobs: ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:18083 > /dev/null; then + if curl -fs 127.0.0.1:8081/status > /dev/null; then ready='yes' break fi diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index e414537b9..280173bf7 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -131,11 +131,27 @@ jobs: echo "waiting emqx started"; sleep 10; done - - name: get pods log + - name: get emqx-0 pods log if: failure() env: KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: kubectl describe pods emqx-0 + run: | + kubectl describe pods emqx-0 + kubectl logs emqx-0 + - name: get emqx-1 pods log + if: failure() + env: + KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" + run: | + kubectl describe pods emqx-1 + kubectl logs emqx-1 + - name: get emqx-2 pods log + if: failure() + env: + KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" + run: | + kubectl describe pods emqx-2 + kubectl logs emqx-2 - uses: actions/checkout@v2 with: repository: emqx/paho.mqtt.testing diff --git a/deploy/docker/docker-entrypoint.sh b/deploy/docker/docker-entrypoint.sh index 16b6cb077..24498dc10 100755 --- a/deploy/docker/docker-entrypoint.sh +++ b/deploy/docker/docker-entrypoint.sh @@ -20,8 +20,6 @@ LOCAL_IP=$(hostname -i | grep -oE '((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.){3}( # Base settings in /opt/emqx/etc/emqx.conf # Plugin settings in /opt/emqx/etc/plugins -_EMQX_HOME='/opt/emqx' - if [[ -z "$EMQX_NAME" ]]; then EMQX_NAME="$(hostname)" export EMQX_NAME @@ -109,26 +107,6 @@ fill_tuples() { done } -## EMQX Plugin load settings -# Plugins loaded by default -LOADED_PLUGINS="$_EMQX_HOME/data/loaded_plugins" -if [[ -n "$EMQX_LOADED_PLUGINS" ]]; then - EMQX_LOADED_PLUGINS=$(echo "$EMQX_LOADED_PLUGINS" | tr -d '[:cntrl:]' | sed -r -e 's/^[^A-Za-z0-9_]+//g' -e 's/[^A-Za-z0-9_]+$//g' -e 's/[^A-Za-z0-9_]+/ /g') - echo "EMQX_LOADED_PLUGINS=$EMQX_LOADED_PLUGINS" - # Parse module names and place `{module_name, true}.` tuples in `loaded_plugins`. - fill_tuples "$LOADED_PLUGINS" "$EMQX_LOADED_PLUGINS" -fi - -## EMQX Modules load settings -# Modules loaded by default -LOADED_MODULES="$_EMQX_HOME/data/loaded_modules" -if [[ -n "$EMQX_LOADED_MODULES" ]]; then - EMQX_LOADED_MODULES=$(echo "$EMQX_LOADED_MODULES" | tr -d '[:cntrl:]' | sed -r -e 's/^[^A-Za-z0-9_]+//g' -e 's/[^A-Za-z0-9_]+$//g' -e 's/[^A-Za-z0-9_]+/ /g') - echo "EMQX_LOADED_MODULES=$EMQX_LOADED_MODULES" - # Parse module names and place `{module_name, true}.` tuples in `loaded_modules`. - fill_tuples "$LOADED_MODULES" "$EMQX_LOADED_MODULES" -fi - # The default rpc port discovery 'stateless' is mostly for clusters # having static node names. So it's troulbe-free for multiple emqx nodes # running on the same host. From 41870a00b31f757187794354d0048cec2d4bc56e Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 16:16:17 +0800 Subject: [PATCH 070/379] chore: fix emqx_retainer test cases --- apps/emqx_retainer/test/emqx_retainer_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index 1a0a00a1c..4f549a385 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl @@ -31,6 +31,7 @@ all() -> emqx_ct:all(?MODULE). %%-------------------------------------------------------------------- init_per_suite(Config) -> + application:stop(emqx_retainer), emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), Config. @@ -172,7 +173,6 @@ t_clean(_) -> emqtt:publish(C1, <<"retained/0">>, <<"this is a retained message 0">>, [{qos, 0}, {retain, true}]), emqtt:publish(C1, <<"retained/1">>, <<"this is a retained message 1">>, [{qos, 0}, {retain, true}]), emqtt:publish(C1, <<"retained/test/0">>, <<"this is a retained message 2">>, [{qos, 0}, {retain, true}]), - {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/#">>, [{qos, 0}, {rh, 0}]), ?assertEqual(3, length(receive_messages(3))), From 82d0f2b016c23c1e4786a5aac9d9cf03b85b97b0 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 29 Jun 2021 16:20:58 +0200 Subject: [PATCH 071/379] feat(quic): Add tests and support more listener option - support option `active_n` - add quic group in emqx_mqtt_protocol_v5_SUITE - fix rate limit --- apps/emqx/src/emqx_connection.erl | 8 +- apps/emqx/src/emqx_listeners.erl | 1 + apps/emqx/src/emqx_quic_stream.erl | 5 +- apps/emqx/src/emqx_schema.erl | 3 +- .../emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 470 ++++++++++-------- 5 files changed, 276 insertions(+), 211 deletions(-) diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 8e3ee400b..7300281d7 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -447,7 +447,7 @@ handle_msg({Closed, _Sock}, State) handle_info({sock_closed, Closed}, close_socket(State)); handle_msg({Passive, _Sock}, State) - when Passive == tcp_passive; Passive == ssl_passive -> + when Passive == tcp_passive; Passive == ssl_passive; Passive =:= quic_passive -> %% In Stats Pubs = emqx_pd:reset_counter(incoming_pubs), Bytes = emqx_pd:reset_counter(incoming_bytes), @@ -738,9 +738,15 @@ handle_info({sock_error, Reason}, State) -> end, handle_info({sock_closed, Reason}, close_socket(State)); +handle_info({quic, peer_send_shutdown, _Stream}, State) -> + handle_info({sock_closed, force}, close_socket(State)); + handle_info({quic, closed, _Channel, ReasonFlag}, State) -> handle_info({sock_closed, ReasonFlag}, State); +handle_info({quic, closed, _Stream}, State) -> + handle_info({sock_closed, force}, State); + handle_info(Info, State) -> with_channel(handle_info, [Info], State). diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index c7d42e2e4..48e99926d 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -157,6 +157,7 @@ start_listener(quic, ListenOn, Options) -> ConnectionOpts = [ {conn_callback, emqx_quic_connection} , {peer_unidi_stream_count, 1} , {peer_bidi_stream_count, 10} + | Options ], StreamOpts = [], quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index e5cd4c3fc..64e851758 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -52,7 +52,8 @@ getstat(Socket, Stats) -> Res -> Res end. -setopts(_Socket, _Opts) -> +setopts(Socket, Opts) -> + [ quicer:setopt(Socket, Opt, V) || {Opt, V} <- Opts ], ok. getopts(_Socket, _Opts) -> @@ -64,7 +65,7 @@ getopts(_Socket, _Opts) -> {buffer,80000}]}. fast_close(Stream) -> - quicer:close_stream(Stream), + quicer:async_close_stream(Stream), %% Stream might be closed already. ok. diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 440dd8117..d32073ee5 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -392,8 +392,7 @@ fields("wss_listener_settings") -> lists:keydelete("high_watermark", 1, Settings); fields("quic_listener_settings") -> - Unsupported = [ "active_n" - , "access" + Unsupported = [ "access" , "proxy_protocol" , "proxy_protocol_timeout" , "backlog" diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index ab4d96eea..2f3048277 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -23,6 +23,7 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-include_lib("common_test/include/ct.hrl"). -import(lists, [nth/2]). @@ -32,18 +33,37 @@ -define(WILD_TOPICS, [<<"TopicA/+">>, <<"+/C">>, <<"#">>, <<"/#">>, <<"/+">>, <<"+/+">>, <<"TopicA/#">>]). -all() -> emqx_ct:all(?MODULE). +all() -> + [ {group, tcp} + , {group, quic} + ]. + +groups() -> + TCs = emqx_ct:all(?MODULE), + [ {tcp, [], TCs} + , {quic, [], TCs} + ]. + +init_per_group(tcp, Config) -> + emqx_ct_helpers:start_apps([]), + [ {port, 1883}, {conn_fun, connect} | Config]; +init_per_group(quic, Config) -> + emqx_ct_helpers:start_apps([]), + [ {port, 14567}, {conn_fun, quic_connect} | Config]; +init_per_group(_, Config) -> + emqx_ct_helpers:stop_apps([]), + Config. + +end_per_group(_Group, _Config) -> + ok. init_per_suite(Config) -> - %% Meck emqtt - ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), %% Start Apps emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), Config. end_per_suite(_Config) -> - ok = meck:unload(emqtt), emqx_ct_helpers:stop_apps([]). init_per_testcase(TestCase, Config) -> @@ -97,9 +117,10 @@ waiting_client_process_exit(C) -> 1000 -> error({waiting_timeout, C}) end. -clean_retained(Topic) -> - {ok, Clean} = emqtt:start_link([{clean_start, true}]), - {ok, _} = emqtt:connect(Clean), +clean_retained(Topic, Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Clean} = emqtt:start_link([{clean_start, true} | Config]), + {ok, _} = emqtt:ConnFun(Clean), {ok, _} = emqtt:publish(Clean, Topic, #{}, <<"">>, [{qos, ?QOS_1}, {retain, true}]), ok = emqtt:disconnect(Clean). @@ -107,11 +128,12 @@ clean_retained(Topic) -> %% Test Cases %%-------------------------------------------------------------------- -t_basic_test(_) -> +t_basic_test(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), ct:print("Basic test starting"), - {ok, C} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(C), + {ok, C} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(C), {ok, _, [1]} = emqtt:subscribe(C, Topic, qos1), {ok, _, [2]} = emqtt:subscribe(C, Topic, qos2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), @@ -124,16 +146,17 @@ t_basic_test(_) -> %% Connection %%-------------------------------------------------------------------- -t_connect_clean_start(_) -> +t_connect_clean_start(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), {ok, Client1} = emqtt:start_link([{clientid, <<"t_connect_clean_start">>}, - {proto_ver, v5},{clean_start, true}]), - {ok, _} = emqtt:connect(Client1), + {proto_ver, v5},{clean_start, true} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ?assertEqual(0, client_info(session_present, Client1)), %% [MQTT-3.1.2-4] ok = emqtt:pause(Client1), {ok, Client2} = emqtt:start_link([{clientid, <<"t_connect_clean_start">>}, - {proto_ver, v5},{clean_start, false}]), - {ok, _} = emqtt:connect(Client2), + {proto_ver, v5},{clean_start, false} | Config]), + {ok, _} = emqtt:ConnFun(Client2), ?assertEqual(1, client_info(session_present, Client2)), %% [MQTT-3.1.2-5] ?assertEqual(142, receive_disconnect_reasoncode()), waiting_client_process_exit(Client1), @@ -142,32 +165,32 @@ t_connect_clean_start(_) -> waiting_client_process_exit(Client2), {ok, Client3} = emqtt:start_link([{clientid, <<"new_client">>}, - {proto_ver, v5},{clean_start, false}]), - {ok, _} = emqtt:connect(Client3), + {proto_ver, v5},{clean_start, false} | Config]), + {ok, _} = emqtt:ConnFun(Client3), ?assertEqual(0, client_info(session_present, Client3)), %% [MQTT-3.1.2-6] ok = emqtt:disconnect(Client3), waiting_client_process_exit(Client3), process_flag(trap_exit, false). -t_connect_will_message(_) -> +t_connect_will_message(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Payload = "will message", - {ok, Client1} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload} - ]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client1)), Info = emqx_connection:info(sys:get_state(ClientPid)), ?assertNotEqual(undefined, maps:find(will_msg, Info)), %% [MQTT-3.1.2-7] - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, Topic, qos2), ok = emqtt:disconnect(Client1, 4), %% [MQTT-3.14.2-1] @@ -178,27 +201,32 @@ t_connect_will_message(_) -> ?assertEqual({ok, 0}, maps:find(qos, Msg)), ok = emqtt:disconnect(Client2), - {ok, Client3} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload} - ]), - {ok, _} = emqtt:connect(Client3), + {ok, Client3} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload} | Config + ]), + {ok, _} = emqtt:ConnFun(Client3), - {ok, Client4} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client4), + {ok, Client4} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client4), {ok, _, [2]} = emqtt:subscribe(Client4, Topic, qos2), ok = emqtt:disconnect(Client3), ?assertEqual(0, length(receive_messages(1))), %% [MQTT-3.1.2-10] ok = emqtt:disconnect(Client4). -t_batch_subscribe(_) -> - {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>}]), - {ok, _} = emqtt:connect(Client), +t_batch_subscribe(init, Config) -> ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end), + Config; +t_batch_subscribe('end', _Config) -> + meck:unload(emqx_access_control). + +t_batch_subscribe(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"batch_test">>} | Config]), + {ok, _} = emqtt:ConnFun(Client), {ok, _, [?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED, ?RC_NOT_AUTHORIZED]} = emqtt:subscribe(Client, [{<<"t1">>, qos1}, @@ -209,25 +237,25 @@ t_batch_subscribe(_) -> ?RC_NO_SUBSCRIPTION_EXISTED]} = emqtt:unsubscribe(Client, [<<"t1">>, <<"t2">>, <<"t3">>]), - meck:unload(emqx_access_control), emqtt:disconnect(Client). -t_connect_will_retain(_) -> +t_connect_will_retain(Config) -> + ConnFun = ?config(conn_fun, Config), + process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), Payload = "will message", - {ok, Client1} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload}, - {will_retain, false} - ]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload}, + {will_retain, false} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, #{}, [{Topic, [{rap, true}, {qos, 2}]}]), ok = emqtt:disconnect(Client1, 4), @@ -235,27 +263,26 @@ t_connect_will_retain(_) -> ?assertEqual({ok, false}, maps:find(retain, Msg1)), %% [MQTT-3.1.2-14] ok = emqtt:disconnect(Client2), - {ok, Client3} = emqtt:start_link([ - {proto_ver, v5}, - {clean_start, true}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, Payload}, - {will_retain, true} - ]), - {ok, _} = emqtt:connect(Client3), + {ok, Client3} = emqtt:start_link([ {proto_ver, v5}, + {clean_start, true}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, Payload}, + {will_retain, true} | Config + ]), + {ok, _} = emqtt:ConnFun(Client3), - {ok, Client4} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client4), + {ok, Client4} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client4), {ok, _, [2]} = emqtt:subscribe(Client4, #{}, [{Topic, [{rap, true}, {qos, 2}]}]), ok = emqtt:disconnect(Client3, 4), [Msg2 | _ ] = receive_messages(1), ?assertEqual({ok, true}, maps:find(retain, Msg2)), %% [MQTT-3.1.2-15] ok = emqtt:disconnect(Client4), - clean_retained(Topic). + clean_retained(Topic, Config). -t_connect_idle_timeout(_) -> +t_connect_idle_timeout(_Config) -> IdleTimeout = 2000, emqx_zone:set_env(external, idle_timeout, IdleTimeout), @@ -263,25 +290,30 @@ t_connect_idle_timeout(_) -> timer:sleep(IdleTimeout), ?assertMatch({error, closed}, emqtt_sock:recv(Sock,1024)). -t_connect_limit_timeout(_) -> +t_connect_limit_timeout(init, Config) -> ok = meck:new(proplists, [non_strict, passthrough, no_history, no_link, unstick]), meck:expect(proplists, get_value, fun(active_n, _Options, _Default) -> 1; (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) end), + Config; +t_connect_limit_timeout('end', _Config) -> + catch meck:unload(proplists). +t_connect_limit_timeout(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), emqx_zone:set_env(external, publish_limit, {3, 5}), - {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60}]), - {ok, _} = emqtt:connect(Client), + {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60} | Config]), + {ok, _} = emqtt:ConnFun(Client), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client)), ?assertEqual(undefined, emqx_connection:info(limit_timer, sys:get_state(ClientPid))), Payload = <<"t_shared_subscriptions_client_terminates_when_qos_eq_2">>, - ok = emqtt:publish(Client, Topic, Payload, 0), - ok = emqtt:publish(Client, Topic, Payload, 0), - ok = emqtt:publish(Client, Topic, Payload, 0), - timer:sleep(200), + {ok, 2} = emqtt:publish(Client, Topic, Payload, 1), + {ok, 3} = emqtt:publish(Client, Topic, Payload, 1), + {ok, 4} = emqtt:publish(Client, Topic, Payload, 1), + timer:sleep(250), ?assert(is_reference(emqx_connection:info(limit_timer, sys:get_state(ClientPid)))), ok = emqtt:disconnect(Client), @@ -301,9 +333,10 @@ t_connect_emit_stats_timeout('end', Config) -> ok. t_connect_emit_stats_timeout(Config) -> + ConnFun = ?config(conn_fun, Config), {_, IdleTimeout} = lists:keyfind(idle_timeout, 1, Config), - {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60}]), - {ok, _} = emqtt:connect(Client), + {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60} | Config]), + {ok, _} = emqtt:ConnFun(Client), [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client)), ?assert(is_reference(emqx_connection:info(stats_timer, sys:get_state(ClientPid)))), ?block_until(#{?snk_kind := cancel_stats_timer}, IdleTimeout * 2, _BackInTime = 0), @@ -311,15 +344,16 @@ t_connect_emit_stats_timeout(Config) -> ok = emqtt:disconnect(Client). %% [MQTT-3.1.2-22] -t_connect_keepalive_timeout(_) -> +t_connect_keepalive_timeout(Config) -> + ConnFun = ?config(conn_fun, Config), %% Prevent the emqtt client bringing us down on the disconnect. process_flag(trap_exit, true), Keepalive = 2, {ok, Client} = emqtt:start_link([{proto_ver, v5}, - {keepalive, Keepalive}]), - {ok, _} = emqtt:connect(Client), + {keepalive, Keepalive} | Config]), + {ok, _} = emqtt:ConnFun(Client), emqtt:pause(Client), receive {disconnected, ReasonCode, _Channel} -> ?assertEqual(141, ReasonCode) @@ -328,30 +362,30 @@ t_connect_keepalive_timeout(_) -> end. %% [MQTT-3.1.2-23] -t_connect_session_expiry_interval(_) -> +t_connect_session_expiry_interval(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Payload = "test message", - {ok, Client1} = emqtt:start_link([ - {clientid, <<"t_connect_session_expiry_interval">>}, - {proto_ver, v5}, - {properties, #{'Session-Expiry-Interval' => 7200}} + {ok, Client1} = emqtt:start_link([ {clientid, <<"t_connect_session_expiry_interval">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 7200}} + | Config ]), - {ok, _} = emqtt:connect(Client1), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), ok = emqtt:disconnect(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, 2} = emqtt:publish(Client2, Topic, Payload, 2), ok = emqtt:disconnect(Client2), - {ok, Client3} = emqtt:start_link([ - {clientid, <<"t_connect_session_expiry_interval">>}, - {proto_ver, v5}, - {clean_start, false} + {ok, Client3} = emqtt:start_link([ {clientid, <<"t_connect_session_expiry_interval">>}, + {proto_ver, v5}, + {clean_start, false} | Config ]), - {ok, _} = emqtt:connect(Client3), + {ok, _} = emqtt:ConnFun(Client3), [Msg | _ ] = receive_messages(1), ?assertEqual({ok, iolist_to_binary(Topic)}, maps:find(topic, Msg)), ?assertEqual({ok, iolist_to_binary(Payload)}, maps:find(payload, Msg)), @@ -360,13 +394,13 @@ t_connect_session_expiry_interval(_) -> %% [MQTT-3.1.3-9] %% !!!REFACTOR NEED: -%t_connect_will_delay_interval(_) -> +%t_connect_will_delay_interval(Config) -> % process_flag(trap_exit, true), % Topic = nth(1, ?TOPICS), % Payload = "will message", % -% {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), -% {ok, _} = emqtt:connect(Client1), +% {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), +% {ok, _} = emqtt:ConnFun(Client1), % {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), % % {ok, Client2} = emqtt:start_link([ @@ -379,9 +413,9 @@ t_connect_session_expiry_interval(_) -> % {will_payload, Payload}, % {will_props, #{'Will-Delay-Interval' => 3}}, % {properties, #{'Session-Expiry-Interval' => 7200}}, -% {keepalive, 2} +% {keepalive, 2} | Config % ]), -% {ok, _} = emqtt:connect(Client2), +% {ok, _} = emqtt:ConnFun(Client2), % timer:sleep(50), % erlang:exit(Client2, kill), % timer:sleep(2000), @@ -399,9 +433,9 @@ t_connect_session_expiry_interval(_) -> % {will_payload, Payload}, % {will_props, #{'Will-Delay-Interval' => 7200}}, % {properties, #{'Session-Expiry-Interval' => 3}}, -% {keepalive, 2} +% {keepalive, 2} | Config % ]), -% {ok, _} = emqtt:connect(Client3), +% {ok, _} = emqtt:ConnFun(Client3), % timer:sleep(50), % erlang:exit(Client3, kill), % @@ -418,18 +452,17 @@ t_connect_session_expiry_interval(_) -> % process_flag(trap_exit, false). %% [MQTT-3.1.4-3] -t_connect_duplicate_clientid(_) -> +t_connect_duplicate_clientid(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), - {ok, Client1} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5} - ]), - {ok, _} = emqtt:connect(Client1), - {ok, Client2} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5} - ]), - {ok, _} = emqtt:connect(Client2), + {ok, Client1} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), + {ok, Client2} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5} | Config + ]), + {ok, _} = emqtt:ConnFun(Client2), ?assertEqual(142, receive_disconnect_reasoncode()), waiting_client_process_exit(Client1), @@ -441,28 +474,33 @@ t_connect_duplicate_clientid(_) -> %% Connack %%-------------------------------------------------------------------- -t_connack_session_present(_) -> - {ok, Client1} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5}, - {properties, #{'Session-Expiry-Interval' => 7200}}, - {clean_start, true} - ]), - {ok, _} = emqtt:connect(Client1), +t_connack_session_present(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client1} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 7200}}, + {clean_start, true} | Config + ]), + {ok, _} = emqtt:ConnFun(Client1), ?assertEqual(0, client_info(session_present, Client1)), %% [MQTT-3.2.2-2] ok = emqtt:disconnect(Client1), - {ok, Client2} = emqtt:start_link([ - {clientid, <<"t_connect_duplicate_clientid">>}, - {proto_ver, v5}, - {properties, #{'Session-Expiry-Interval' => 7200}}, - {clean_start, false} - ]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([ {clientid, <<"t_connect_duplicate_clientid">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 7200}}, + {clean_start, false} | Config + ]), + {ok, _} = emqtt:ConnFun(Client2), ?assertEqual(1, client_info(session_present, Client2)), %% [[MQTT-3.2.2-3]] ok = emqtt:disconnect(Client2). -t_connack_max_qos_allowed(_) -> +t_connack_max_qos_allowed(init, Config) -> + Config; +t_connack_max_qos_allowed('end', _Config) -> + emqx_zone:set_env(external, max_qos_allowed, 2), + ok. +t_connack_max_qos_allowed(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), @@ -471,8 +509,8 @@ t_connack_max_qos_allowed(_) -> persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, Connack1} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, Connack1} = emqtt:ConnFun(Client1), ?assertEqual(0, maps:get('Maximum-QoS', Connack1)), %% [MQTT-3.2.2-9] {ok, _, [0]} = emqtt:subscribe(Client1, Topic, 0), %% [MQTT-3.2.2-10] @@ -483,14 +521,13 @@ t_connack_max_qos_allowed(_) -> ?assertEqual(155, receive_disconnect_reasoncode()), %% [MQTT-3.2.2-11] waiting_client_process_exit(Client1), - {ok, Client2} = emqtt:start_link([ - {proto_ver, v5}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, <<"Unsupported Qos">>}, - {will_qos, 2} - ]), - {error, Connack2} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([ {proto_ver, v5}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, <<"Unsupported Qos">>}, + {will_qos, 2} | Config + ]), + {error, Connack2} = emqtt:ConnFun(Client2), ?assertMatch({qos_not_supported, _}, Connack2), %% [MQTT-3.2.2-12] waiting_client_process_exit(Client2), @@ -499,8 +536,8 @@ t_connack_max_qos_allowed(_) -> persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), - {ok, Client3} = emqtt:start_link([{proto_ver, v5}]), - {ok, Connack3} = emqtt:connect(Client3), + {ok, Client3} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, Connack3} = emqtt:ConnFun(Client3), ?assertEqual(1, maps:get('Maximum-QoS', Connack3)), %% [MQTT-3.2.2-9] {ok, _, [0]} = emqtt:subscribe(Client3, Topic, 0), %% [MQTT-3.2.2-10] @@ -511,14 +548,13 @@ t_connack_max_qos_allowed(_) -> ?assertEqual(155, receive_disconnect_reasoncode()), %% [MQTT-3.2.2-11] waiting_client_process_exit(Client3), - {ok, Client4} = emqtt:start_link([ - {proto_ver, v5}, - {will_flag, true}, - {will_topic, Topic}, - {will_payload, <<"Unsupported Qos">>}, - {will_qos, 2} - ]), - {error, Connack4} = emqtt:connect(Client4), + {ok, Client4} = emqtt:start_link([ {proto_ver, v5}, + {will_flag, true}, + {will_topic, Topic}, + {will_payload, <<"Unsupported Qos">>}, + {will_qos, 2} | Config + ]), + {error, Connack4} = emqtt:ConnFun(Client4), ?assertMatch({qos_not_supported, _}, Connack4), %% [MQTT-3.2.2-12] waiting_client_process_exit(Client4), @@ -527,17 +563,18 @@ t_connack_max_qos_allowed(_) -> persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), - {ok, Client5} = emqtt:start_link([{proto_ver, v5}]), - {ok, Connack5} = emqtt:connect(Client5), + {ok, Client5} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, Connack5} = emqtt:ConnFun(Client5), ?assertEqual(undefined, maps:get('Maximum-QoS', Connack5, undefined)), %% [MQTT-3.2.2-9] ok = emqtt:disconnect(Client5), waiting_client_process_exit(Client5), process_flag(trap_exit, false). -t_connack_assigned_clienid(_) -> - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), +t_connack_assigned_clienid(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ?assert(is_binary(client_info(clientid, Client1))), %% [MQTT-3.2.2-16] ok = emqtt:disconnect(Client1). @@ -545,11 +582,12 @@ t_connack_assigned_clienid(_) -> %% Publish %%-------------------------------------------------------------------- -t_publish_rap(_) -> +t_publish_rap(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{Topic, [{rap, true}, {qos, 2}]}]), {ok, _} = emqtt:publish(Client1, Topic, #{}, <<"retained message">>, [{qos, ?QOS_1}, {retain, true}]), @@ -557,8 +595,8 @@ t_publish_rap(_) -> ?assertEqual(true, maps:get(retain, Msg1)), %% [MQTT-3.3.1-12] ok = emqtt:disconnect(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, #{}, [{Topic, [{rap, false}, {qos, 2}]}]), {ok, _} = emqtt:publish(Client2, Topic, #{}, <<"retained message">>, [{qos, ?QOS_1}, {retain, true}]), @@ -566,44 +604,47 @@ t_publish_rap(_) -> ?assertEqual(false, maps:get(retain, Msg2)), %% [MQTT-3.3.1-13] ok = emqtt:disconnect(Client2), - clean_retained(Topic). + clean_retained(Topic, Config). -t_publish_wildtopic(_) -> +t_publish_wildtopic(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?WILD_TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ok = emqtt:publish(Client1, Topic, <<"error topic">>), ?assertEqual(144, receive_disconnect_reasoncode()), waiting_client_process_exit(Client1), process_flag(trap_exit, false). -t_publish_payload_format_indicator(_) -> +t_publish_payload_format_indicator(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{'Payload-Format-Indicator' => 233}, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), ok = emqtt:publish(Client1, Topic, Properties, <<"Payload Format Indicator">>, [{qos, ?QOS_0}]), [Msg1 | _] = receive_messages(1), ?assertEqual(Properties, maps:get(properties, Msg1)), %% [MQTT-3.3.2-6] ok = emqtt:disconnect(Client1). -t_publish_topic_alias(_) -> +t_publish_topic_alias(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ok = emqtt:publish(Client1, Topic, #{'Topic-Alias' => 0}, <<"Topic-Alias">>, [{qos, ?QOS_0}]), ?assertEqual(148, receive_disconnect_reasoncode()), %% [MQTT-3.3.2-8] waiting_client_process_exit(Client1), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, Topic, qos2), ok = emqtt:publish(Client2, Topic, #{'Topic-Alias' => 233}, <<"Topic-Alias">>, [{qos, ?QOS_0}]), @@ -615,12 +656,13 @@ t_publish_topic_alias(_) -> process_flag(trap_exit, false). -t_publish_response_topic(_) -> +t_publish_response_topic(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), ok = emqtt:publish(Client1, Topic, #{'Response-Topic' => nth(1, ?WILD_TOPICS)}, <<"Response-Topic">>, [{qos, ?QOS_0}]), ?assertEqual(130, receive_disconnect_reasoncode()), %% [MQTT-3.3.2-14] @@ -628,7 +670,8 @@ t_publish_response_topic(_) -> process_flag(trap_exit, false). -t_publish_properties(_) -> +t_publish_properties(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{ 'Response-Topic' => Topic, %% [MQTT-3.3.2-15] @@ -637,20 +680,21 @@ t_publish_properties(_) -> 'Content-Type' => <<"2333">> %% [MQTT-3.3.2-20] }, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, qos2), ok = emqtt:publish(Client1, Topic, Properties, <<"Publish Properties">>, [{qos, ?QOS_0}]), [Msg1 | _] = receive_messages(1), ?assertEqual(Properties, maps:get(properties, Msg1)), %% [MQTT-3.3.2-16] ok = emqtt:disconnect(Client1). -t_publish_overlapping_subscriptions(_) -> +t_publish_overlapping_subscriptions(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{'Subscription-Identifier' => 2333}, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [1]} = emqtt:subscribe(Client1, Properties, nth(1, ?WILD_TOPICS), qos1), {ok, _, [0]} = emqtt:subscribe(Client1, Properties, nth(3, ?WILD_TOPICS), qos0), {ok, _} = emqtt:publish(Client1, Topic, #{}, @@ -665,13 +709,15 @@ t_publish_overlapping_subscriptions(_) -> %% Subsctibe %%-------------------------------------------------------------------- -t_subscribe_topic_alias(_) -> +t_subscribe_topic_alias(Config) -> + ConnFun = ?config(conn_fun, Config), Topic1 = nth(1, ?TOPICS), Topic2 = nth(2, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}, - {properties, #{'Topic-Alias-Maximum' => 1}} + {ok, Client1} = emqtt:start_link([ {proto_ver, v5}, + {properties, #{'Topic-Alias-Maximum' => 1}} + | Config ]), - {ok, _} = emqtt:connect(Client1), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic1, qos2), {ok, _, [2]} = emqtt:subscribe(Client1, Topic2, qos2), @@ -692,27 +738,29 @@ t_subscribe_topic_alias(_) -> ok = emqtt:disconnect(Client1). -t_subscribe_no_local(_) -> +t_subscribe_no_local(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{Topic, [{nl, true}, {qos, 2}]}]), - {ok, Client2} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client2), + {ok, Client2} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client2), {ok, _, [2]} = emqtt:subscribe(Client2, #{}, [{Topic, [{nl, true}, {qos, 2}]}]), ok = emqtt:publish(Client1, Topic, <<"t_subscribe_no_local">>, 0), ?assertEqual(1, length(receive_messages(2))), %% [MQTT-3.8.3-3] ok = emqtt:disconnect(Client1). -t_subscribe_actions(_) -> +t_subscribe_actions(Config) -> + ConnFun = ?config(conn_fun, Config), Topic = nth(1, ?TOPICS), Properties = #{'Subscription-Identifier' => 2333}, - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Properties, Topic, qos2), {ok, _, [1]} = emqtt:subscribe(Client1, Properties, Topic, qos1), {ok, _} = emqtt:publish(Client1, Topic, <<"t_subscribe_actions">>, 2), @@ -726,12 +774,13 @@ t_subscribe_actions(_) -> %% Unsubsctibe Unsuback %%-------------------------------------------------------------------- -t_unscbsctibe(_) -> +t_unscbsctibe(Config) -> + ConnFun = ?config(conn_fun, Config), Topic1 = nth(1, ?TOPICS), Topic2 = nth(2, ?TOPICS), - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), {ok, _, [2]} = emqtt:subscribe(Client1, Topic1, qos2), {ok, _, [0]} = emqtt:unsubscribe(Client1, Topic1), %% [MQTT-3.10.4-4] {ok, _, [17]} = emqtt:unsubscribe(Client1, <<"noExistTopic">>), %% [MQTT-3.10.4-5] @@ -745,9 +794,10 @@ t_unscbsctibe(_) -> %% Pingreq %%-------------------------------------------------------------------- -t_pingreq(_) -> - {ok, Client1} = emqtt:start_link([{proto_ver, v5}]), - {ok, _} = emqtt:connect(Client1), +t_pingreq(Config) -> + ConnFun = ?config(conn_fun, Config), + {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), + {ok, _} = emqtt:ConnFun(Client1), pong = emqtt:ping(Client1), %% [MQTT-3.12.4-1] ok = emqtt:disconnect(Client1). @@ -755,7 +805,14 @@ t_pingreq(_) -> %% Shared Subscriptions %%-------------------------------------------------------------------- -t_shared_subscriptions_client_terminates_when_qos_eq_2(_) -> +t_shared_subscriptions_client_terminates_when_qos_eq_2(init, Config) -> + ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), + Config; +t_shared_subscriptions_client_terminates_when_qos_eq_2('end', _Config) -> + catch meck:unload(emqtt). + +t_shared_subscriptions_client_terminates_when_qos_eq_2(Config) -> + ConnFun = ?config(conn_fun, Config), process_flag(trap_exit, true), application:set_env(emqx, shared_dispatch_ack_enabled, true), @@ -766,32 +823,33 @@ t_shared_subscriptions_client_terminates_when_qos_eq_2(_) -> meck:expect(emqtt, connected, fun(cast, ?PUBLISH_PACKET(?QOS_2, _PacketId), _State) -> ok = counters:add(CRef, 1, 1), - {stop, {shutdown, for_testiong}}; + {stop, {shutdown, for_testing}}; (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) end), - {ok, Sub1} = emqtt:start_link([{proto_ver, v5}, + {ok, Sub1} = emqtt:start_link([ {proto_ver, v5}, {clientid, <<"sub_client_1">>}, - {keepalive, 5}]), - {ok, _} = emqtt:connect(Sub1), + {keepalive, 5} | Config + ]), + {ok, _} = emqtt:ConnFun(Sub1), {ok, _, [2]} = emqtt:subscribe(Sub1, SharedTopic, qos2), {ok, Sub2} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"sub_client_2">>}, - {keepalive, 5}]), - {ok, _} = emqtt:connect(Sub2), + {keepalive, 5} | Config]), + {ok, _} = emqtt:ConnFun(Sub2), {ok, _, [2]} = emqtt:subscribe(Sub2, SharedTopic, qos2), - {ok, Pub} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"pub_client">>}]), - {ok, _} = emqtt:connect(Pub), + {ok, Pub} = emqtt:start_link([{proto_ver, v5}, {clientid, <<"pub_client">>} | Config]), + {ok, _} = emqtt:ConnFun(Pub), {ok, _} = emqtt:publish(Pub, Topic, <<"t_shared_subscriptions_client_terminates_when_qos_eq_2">>, 2), receive - {'EXIT', _,{shutdown, for_testiong}} -> + {'EXIT', _,{shutdown, for_testing}} -> ok after 1000 -> - error("disconnected timeout") + ct:fail("disconnected timeout") end, ?assertEqual(1, counters:get(CRef, 1)), From 72117f3f1df3efa677ac48437de7dce9f493960d Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 29 Jun 2021 16:25:25 +0200 Subject: [PATCH 072/379] feat(quic): bump emqtt and quicer version emqtt: 1.4.1 quicer: 0.0.5 --- apps/emqx/rebar.config | 4 ++-- rebar.config | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 83129bc1a..fbd980056 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -20,7 +20,7 @@ , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.3"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} ]}. {plugins, [rebar3_proper]}. @@ -31,7 +31,7 @@ [ meck , {bbmustache,"1.10.0"} , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.1"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} ]} diff --git a/rebar.config b/rebar.config index e5f46125a..fc863f7c4 100644 --- a/rebar.config +++ b/rebar.config @@ -55,7 +55,7 @@ , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, {git, "https://github.com/emqx/replayq", {tag, "0.3.2"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.1"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.2"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 @@ -63,7 +63,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.3"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} ]}. {xref_ignores, From 5ae8cd2fa3448bb538ca70be5eb53e98b61dfd76 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 30 Jun 2021 11:21:07 +0200 Subject: [PATCH 073/379] fix(quic): dialyzer errors --- apps/emqx/src/emqx_quic_stream.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 64e851758..236c11ad3 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -53,7 +53,11 @@ getstat(Socket, Stats) -> end. setopts(Socket, Opts) -> - [ quicer:setopt(Socket, Opt, V) || {Opt, V} <- Opts ], + lists:foreach(fun({Opt, V}) when is_atom(Opt) -> + quicer:setopt(Socket, Opt, V); + (Opt) when is_atom(Opt) -> + quicer:setopt(Socket, Opt, true) + end, Opts), ok. getopts(_Socket, _Opts) -> @@ -65,8 +69,8 @@ getopts(_Socket, _Opts) -> {buffer,80000}]}. fast_close(Stream) -> - quicer:async_close_stream(Stream), %% Stream might be closed already. + _ = quicer:async_close_stream(Stream), ok. -spec(ensure_ok_or_exit(atom(), list(term())) -> term()). From 24292f4f4ec21654af2550fbccb955b159d856a7 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 2 Jul 2021 17:45:18 +0800 Subject: [PATCH 074/379] chore: delete plugins related configuration --- apps/emqx/etc/emqx.conf | 12 +----------- apps/emqx/src/emqx.erl | 8 ++------ apps/emqx/src/emqx_schema.erl | 4 +--- rebar.config.erl | 4 ++-- scripts/merge-config.escript | 2 +- 5 files changed, 7 insertions(+), 23 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index f40ecd8e5..d07443012 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1,4 +1,4 @@ -## EMQ X Configuration 4.3 +## EMQ X Configuration 5.0 ##-------------------------------------------------------------------- ## Cluster @@ -2318,16 +2318,6 @@ listener.quic.external { ## Plugins ##------------------------------------------------------------------- plugins: { - ## The etc dir for plugins' config. - ## - ## Value: Folder - etc_dir: "{{ platform_etc_dir }}/plugins/" - - ## The file to store loaded plugin names. - ## - ## Value: File - loaded_file: "{{ platform_data_dir }}/loaded_plugins" - ## The directory of extension plugins. ## ## Value: File diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index bd46ddc03..e5855b786 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -254,15 +254,11 @@ reload_config(ConfFile) -> emqx_feature() -> - [ emqx_authn + [ emqx_resource + , emqx_authn , emqx_authz - , observer_cli - , emqx_http_lib - , emqx_resource - , emqx_connector , emqx_data_bridge , emqx_rule_engine - , emqx_rule_actions , emqx_bridge_mqtt , emqx_modules , emqx_management diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index d32073ee5..91aab2dcd 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -455,9 +455,7 @@ fields("rule") -> [ {"$id", t(string())}]; fields("plugins") -> - [ {"etc_dir", t(string(), "emqx.plugins_etc_dir", undefined)} - , {"loaded_file", t(string(), "emqx.plugins_loaded_file", undefined)} - , {"expand_plugins_dir", t(string(), "emqx.expand_plugins_dir", undefined)} + [ {"expand_plugins_dir", t(string(), "emqx.expand_plugins_dir", undefined)} ]; fields("broker") -> diff --git a/rebar.config.erl b/rebar.config.erl index 01abf254f..56599dd54 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -247,12 +247,12 @@ relx_apps(ReleaseType) -> , {mnesia, load} , {ekka, load} , {emqx_plugin_libs, load} - , emqx_authn - , emqx_authz , observer_cli , emqx_http_lib , emqx_resource , emqx_connector + , emqx_authn + , emqx_authz , emqx_data_bridge , emqx_rule_engine , emqx_rule_actions diff --git a/scripts/merge-config.escript b/scripts/merge-config.escript index a0fbdb4b3..503e6faa8 100755 --- a/scripts/merge-config.escript +++ b/scripts/merge-config.escript @@ -28,7 +28,7 @@ main(_) -> case filelib:is_regular(Filename) of true -> {ok, Bin1} = file:read_file(Filename), - <>; + [Acc, io_lib:nl(), Bin1]; false -> Acc end end From 4313e3799422d52640ed59a26e6565a32f2a49dc Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 2 Jul 2021 19:01:22 +0800 Subject: [PATCH 075/379] chore(release): update emqx release version --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index e69b07558..6bacf7052 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -29,7 +29,7 @@ -ifndef(EMQX_ENTERPRISE). --define(EMQX_RELEASE, {opensource, "5.0-pre"}). +-define(EMQX_RELEASE, {opensource, "5.0-alpha.1"}). -else. From 56cdd469ff8e68341e6f75862f58b05405482450 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 2 Jul 2021 20:17:40 +0800 Subject: [PATCH 076/379] feat(gateway): The prototype for emqx-gateway application * feat(gateway): add gateway application * chore(gateway): add normalize confs function * refactor: move emqx-stomp to emqx-gateway subdir * chore(gateway): fix some bad function defination * chore(gateway): rename type to gwid * chore(gw-stomp): upgrade the implementation to suppport gateway instance * feat(gw-stomp): add reconnect mechanism * refactor(stomp): upgrade connection&channel module to latest * refactor(stomp): more details for handle_in/out * refactor(stomp): get it up and running * chore(gw): load some modules by default * refactor: upgrade the emqx-gateway schema module * test(stomp): fix testcases for stomp gateway * chore(gw): remove needless lines * chore(gateway): correct a lot of specs * chore(gw): add a draft for metrics * chore(gw): add metrics process * fix(gw): fix cm process monitor * test(gw): add test cases for gateway-regitry * feat(gw): add metrics/cli for gateway * fix(gw): fix xref errors * chore(gw): pretty gateway metrics print format * chore(gw-stomp): generate clientid by default * chore(gw): more reliable * chore(gw): rename gwid -> type * chore(gw): impl the update logic * chore(gw): some format improvement * chore(gw): adapts the hocon configs * fix(gw): fix xref errors * test(gw): update configurations for tests * chore(gw): ignore diaylzer warnings * fix(gw): fix bad function call * chore(gw): remove needless comments --- apps/emqx/src/emqx_schema.erl | 1 + apps/emqx_exhook/src/emqx_exhook_app.erl | 2 +- apps/emqx_gateway/.gitignore | 20 + apps/emqx_gateway/LICENSE | 191 ++++ apps/emqx_gateway/Makefile | 28 + apps/emqx_gateway/README.md | 309 ++++++ apps/emqx_gateway/etc/emqx_gateway.conf | 30 + apps/emqx_gateway/include/emqx_gateway.hrl | 35 + apps/emqx_gateway/rebar.config | 7 + .../src/bhvrs/emqx_gateway_conn.erl | 22 + .../src/bhvrs/emqx_gateway_impl.erl | 49 + apps/emqx_gateway/src/emqx_gateway.app.src | 11 + apps/emqx_gateway/src/emqx_gateway.erl | 84 ++ apps/emqx_gateway/src/emqx_gateway_app.erl | 93 ++ apps/emqx_gateway/src/emqx_gateway_cli.erl | 201 ++++ apps/emqx_gateway/src/emqx_gateway_cm.erl | 447 ++++++++ .../src/emqx_gateway_cm_registry.erl | 141 +++ apps/emqx_gateway/src/emqx_gateway_ctx.erl | 148 +++ apps/emqx_gateway/src/emqx_gateway_gw_sup.erl | 135 +++ .../src/emqx_gateway_insta_sup.erl | 312 ++++++ .../emqx_gateway/src/emqx_gateway_metrics.erl | 101 ++ .../src/emqx_gateway_registry.erl | 163 +++ apps/emqx_gateway/src/emqx_gateway_schema.erl | 178 ++++ apps/emqx_gateway/src/emqx_gateway_sup.erl | 194 ++++ apps/emqx_gateway/src/emqx_gateway_utils.erl | 163 +++ .../src/stomp}/README.md | 16 +- .../src/stomp/emqx_stomp_channel.erl | 978 ++++++++++++++++++ .../src/stomp/emqx_stomp_connection.erl | 908 ++++++++++++++++ .../src/stomp}/emqx_stomp_frame.erl | 109 +- .../src/stomp}/emqx_stomp_heartbeat.erl | 13 +- .../src/stomp/emqx_stomp_impl.erl | 153 +++ .../src/stomp/include/emqx_stomp.hrl | 92 ++ .../test/emqx_gateway_registry_SUITE.erl | 87 ++ .../test/emqx_stomp_SUITE.erl | 118 ++- .../test/emqx_stomp_heartbeat_SUITE.erl | 0 apps/emqx_stomp/.gitignore | 25 - apps/emqx_stomp/etc/emqx_stomp.conf | 124 --- apps/emqx_stomp/include/emqx_stomp.hrl | 48 - apps/emqx_stomp/priv/emqx_stomp.schema | 149 --- apps/emqx_stomp/rebar.config | 16 - apps/emqx_stomp/src/emqx_stomp.app.src | 14 - apps/emqx_stomp/src/emqx_stomp.erl | 142 --- apps/emqx_stomp/src/emqx_stomp_connection.erl | 274 ----- apps/emqx_stomp/src/emqx_stomp_protocol.erl | 468 --------- apps/emqx_stomp/test/client.py | 19 - rebar.config.erl | 1 + 46 files changed, 5451 insertions(+), 1368 deletions(-) create mode 100644 apps/emqx_gateway/.gitignore create mode 100644 apps/emqx_gateway/LICENSE create mode 100644 apps/emqx_gateway/Makefile create mode 100644 apps/emqx_gateway/README.md create mode 100644 apps/emqx_gateway/etc/emqx_gateway.conf create mode 100644 apps/emqx_gateway/include/emqx_gateway.hrl create mode 100644 apps/emqx_gateway/rebar.config create mode 100644 apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl create mode 100644 apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway.app.src create mode 100644 apps/emqx_gateway/src/emqx_gateway.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_app.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_cli.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_cm.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_cm_registry.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_ctx.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_gw_sup.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_insta_sup.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_metrics.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_registry.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_schema.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_sup.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_utils.erl rename apps/{emqx_stomp => emqx_gateway/src/stomp}/README.md (90%) create mode 100644 apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl create mode 100644 apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl rename apps/{emqx_stomp/src => emqx_gateway/src/stomp}/emqx_stomp_frame.erl (70%) rename apps/{emqx_stomp/src => emqx_gateway/src/stomp}/emqx_stomp_heartbeat.erl (89%) create mode 100644 apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl create mode 100644 apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl create mode 100644 apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl rename apps/{emqx_stomp => emqx_gateway}/test/emqx_stomp_SUITE.erl (82%) rename apps/{emqx_stomp => emqx_gateway}/test/emqx_stomp_heartbeat_SUITE.erl (100%) delete mode 100644 apps/emqx_stomp/.gitignore delete mode 100644 apps/emqx_stomp/etc/emqx_stomp.conf delete mode 100644 apps/emqx_stomp/include/emqx_stomp.hrl delete mode 100644 apps/emqx_stomp/priv/emqx_stomp.schema delete mode 100644 apps/emqx_stomp/rebar.config delete mode 100644 apps/emqx_stomp/src/emqx_stomp.app.src delete mode 100644 apps/emqx_stomp/src/emqx_stomp.erl delete mode 100644 apps/emqx_stomp/src/emqx_stomp_connection.erl delete mode 100644 apps/emqx_stomp/src/emqx_stomp_protocol.erl delete mode 100644 apps/emqx_stomp/test/client.py diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 91aab2dcd..73a5812e8 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -71,6 +71,7 @@ includes() -> , "emqx_bridge_mqtt" , "emqx_modules" , "emqx_management" + , "emqx_gateway" ]. -endif. diff --git a/apps/emqx_exhook/src/emqx_exhook_app.erl b/apps/emqx_exhook/src/emqx_exhook_app.erl index 4e00340d8..2988be6d2 100644 --- a/apps/emqx_exhook/src/emqx_exhook_app.erl +++ b/apps/emqx_exhook/src/emqx_exhook_app.erl @@ -88,7 +88,7 @@ init_hooks_cnter() -> try _ = ets:new(?CNTER, [named_table, public]), ok catch - exit:badarg:_ -> + error:badarg:_ -> ok end. diff --git a/apps/emqx_gateway/.gitignore b/apps/emqx_gateway/.gitignore new file mode 100644 index 000000000..71ab0135c --- /dev/null +++ b/apps/emqx_gateway/.gitignore @@ -0,0 +1,20 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +*.iml +rebar3.crashdump +*~ +rebar.lock diff --git a/apps/emqx_gateway/LICENSE b/apps/emqx_gateway/LICENSE new file mode 100644 index 000000000..1f15def74 --- /dev/null +++ b/apps/emqx_gateway/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021, JianBo He . + + 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. + diff --git a/apps/emqx_gateway/Makefile b/apps/emqx_gateway/Makefile new file mode 100644 index 000000000..b2a54f7dd --- /dev/null +++ b/apps/emqx_gateway/Makefile @@ -0,0 +1,28 @@ +## shallow clone for speed + +REBAR_GIT_CLONE_OPTIONS += --depth 1 +export REBAR_GIT_CLONE_OPTIONS + +REBAR = rebar3 +all: compile + +compile: + $(REBAR) compile + +clean: distclean + +ct: + $(REBAR) as test ct -v + +eunit: + $(REBAR) as test eunit + +xref: + $(REBAR) xref + +cover: + $(REBAR) cover + +distclean: + @rm -rf _build + @rm -f data/app.*.config data/vm.*.args rebar.lock diff --git a/apps/emqx_gateway/README.md b/apps/emqx_gateway/README.md new file mode 100644 index 000000000..0f95f286a --- /dev/null +++ b/apps/emqx_gateway/README.md @@ -0,0 +1,309 @@ +# emqx_gateway + +***This is a very early prototype application*** for Gateway in EMQ X Broker 5.0 + +## Concept + + EMQ X Gateway Managment + - Gateway-Registry (or Gateway Type) + - *Load + - *UnLoad + - *List + + - Gateway + - *Create + - *Delete + - *Update + - *Stop-And-Start + - *Hot-Upgrade + - *Satrt/Enable + - *Stop/Disable + - Listener + +## ROADMAP + +Gateway v0.1: Management support + +Gateway v0.2: Conn/Frame/Protocol Template + +### Compatible with EMQ X + +> Why we need to compatible + +1. Authentication +2. Hooks/Event system +3. Messages Mode & Rule Engine +4. Cluster registration +5. Metrics & Statistic + +> How to do it + +> + +### User Interface + +#### Configurations + +```hocon +gateway { + + ## ... some confs for top scope + .. + ## End. + + ## Gateway Instances + + lwm2m[.name] { + + ## variable support + mountpoint: lwm2m/%e/ + + lifetime_min: 1s + lifetime_max: 86400s + #qmode_time_window: 22 + #auto_observe: off + + #update_msg_publish_condition: contains_object_list + + xml_dir: {{ platform_etc_dir }}/lwm2m_xml + + clientinfo_override: { + username: ${register.opts.uname} + password: ${register.opts.passwd} + clientid: ${epn} + } + + #authenticator: allow_anonymous + authenticator: [ + { + type: auth-http + method: post + //?? how to generate clientinfo ?? + params: $client.credential + } + ] + + translator: { + downlink: "dn/#" + uplink: { + notify: "up/notify" + response: "up/resp" + register: "up/resp" + update: "up/reps" + } + } + + %% ?? listener.$type.name ?? + listener.udp[.name] { + listen_on: 0.0.0.0:5683 + max_connections: 1024000 + max_conn_rate: 1000 + ## ?? udp keepalive in socket level ??? + #keepalive: + ## ?? udp proxy-protocol in socket level ??? + #proxy_protocol: on + #proxy_timeout: 30s + recbuf: 2KB + sndbuf: 2KB + buffer: 2KB + tune_buffer: off + #access: allow all + read_packets: 20 + } + + listener.dtls[.name] { + listen_on: 0.0.0.0:5684 + ... + } + } + + ## The CoAP Gateway + coap[.name] { + + #enable_stats: on + + authenticator: [ + ... + ] + + listener.udp[.name] { + ... + } + + listener.dtls[.name] { + ... + } +} + + ## The Stomp Gateway + stomp[.name] { + + allow_anonymous: true + + default_user.login: guest + default_user.passcode: guest + + frame.max_headers: 10 + frame.max_header_length: 1024 + frame.max_body_length: 8192 + + listener.tcp[.name] { + ... + } + + listener.ssl[.name] { + ... + } + } + + exproto[.name] { + + proto_name: DL-648 + + authenticators: [...] + + adapter: { + type: grpc + options: { + listen_on: 9100 + } + } + + handler: { + type: grpc + options: { + url: + } + } + + listener.tcp[.name] { + ... + } + } + + ## ============================ Enterpise gateways + + ## The JT/T 808 Gateway + jtt808[.name] { + + idle_timeout: 30s + enable_stats: on + max_packet_size: 8192 + + clientinfo_override: { + clientid: $phone + username: xxx + password: xxx + } + + authenticator: [ + { + type: auth-http + method: post + params: $clientinfo.credential + } + ] + + translator: { + subscribe: [jt808/%c/dn] + publish: [jt808/%c/up] + } + + listener.tcp[.name] { + ... + } + + listener.ssl[.name] { + ... + } + } + + gbt32960[.name] { + + frame.max_length: 8192 + retx_interval: 8s + retx_max_times: 3 + message_queue_len: 10 + + authenticators: [...] + + translator: { + ## upstream + login: gbt32960/${vin}/upstream/vlogin + logout: gbt32960/${vin}/upstream/vlogout + informing: gbt32960/${vin}/upstream/info + reinforming: gbt32960/${vin}/upstream/reinfo + ## downstream + downstream: gbt32960/${vin}/dnstream + response: gbt32960/${vin}/upstream/response + } + + listener.tcp[.name] { + ... + } + + listener.ssl[.name] { + ... + } + } + + privtcp[.name] { + + max_packet_size: 65535 + idle_timeout: 15s + + enable_stats: on + + force_gc_policy: 1000|1MB + force_shutdown_policy: 8000|800MB + + translator: { + up_topic: tcp/%c/up + dn_topic: tcp/%c/dn + } + + listener.tcp[.name]: { + ... + } + } +} +``` + +#### CLI + +##### Gateway + +```bash +## List all started gateway and gateway-instance +emqx_ctl gateway list +emqx_ctl gateway lookup +emqx_ctl gateway stop +emqx_ctl gateway start + +emqx_ctl gateway-registry re-searching +emqx_ctl gateway-registry list + +emqx_ctl gateway-clients list +emqx_ctl gateway-clients show +emqx_ctl gateway-clients kick + +## Banned ?? +emqx_ctl gateway-banned + +## Metrics +emqx_ctl gateway-metrics [] +``` + +#### Mangement by HTTP-API/Dashboard/ + +#### How to integrate a protocol to your platform + +### Develop your protocol gateway + +There are 3 way to create your protocol gateway for EMQ X 5.0: + +1. Use Erlang to create a new emqx plugin to handle all of protocol packets (same as v5.0 before) + +2. Based on the emqx-gateway-impl-bhvr and emqx-gateway + +3. Use the gRPC Gateway diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf new file mode 100644 index 000000000..ab5b52143 --- /dev/null +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -0,0 +1,30 @@ +##-------------------------------------------------------------------- +## EMQ X Gateway configurations +##-------------------------------------------------------------------- + +## TODO: + +emqx_gateway: { + stomp.1: { + frame: { + max_headers: 10 + max_headers_length: 1024 + max_body_length: 8192 + } + + clientinfo_override: { + username: "${Packet.headers.login}" + password: "${Packet.headers.passcode}" + } + + authenticator: allow_anonymous + + listener.tcp.1: { + bind: 61613 + acceptors: 16 + max_connections: 1024000 + max_conn_rate: 1000 + active_n: 100 + } + } +} diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl new file mode 100644 index 000000000..35fad7f23 --- /dev/null +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -0,0 +1,35 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 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. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_GATEWAY_HRL). +-define(EMQX_GATEWAY_HRL, 1). + +-type instance_id() :: atom(). +-type gateway_type() :: atom(). + +%% @doc The Gateway Instace defination +-type instance() :: + #{ id := instance_id() + , type := gateway_type() + , name := binary() + , descr => binary() | undefined + %% Appears only in creating or detailed info + , rawconf => map() + %% Appears only in getting instance status/info + , status => stopped | running + }. + +-endif. diff --git a/apps/emqx_gateway/rebar.config b/apps/emqx_gateway/rebar.config new file mode 100644 index 000000000..71fc61330 --- /dev/null +++ b/apps/emqx_gateway/rebar.config @@ -0,0 +1,7 @@ +{erl_opts, [debug_info]}. +{deps, []}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [emqx_gateway]} +]}. diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl new file mode 100644 index 000000000..7392f0b20 --- /dev/null +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -0,0 +1,22 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The behavior abstrat for TCP based gateway conn +%% +-module(emqx_gateway_conn). + +%% TODO: Gateway v0.2 + diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl new file mode 100644 index 000000000..9726dad02 --- /dev/null +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl @@ -0,0 +1,49 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_impl). + +-include("include/emqx_gateway.hrl"). + +-type state() :: map(). +-type reason() :: any(). + +%% @doc +-callback init(Options :: list()) -> {error, reason()} | {ok, GwState :: state()}. + +%% @doc +-callback on_insta_create(Insta :: instance(), + Ctx :: emqx_gateway_ctx:context(), + GwState :: state() + ) + -> {error, reason()} + | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}. + +%% @doc +-callback on_insta_update(NewInsta :: instance(), + OldInsta :: instance(), + GwInstaState :: state(), + GwState :: state()) + -> ok + | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()} + | {error, reason()}. + +%% @doc +-callback on_insta_destroy(Insta :: instance(), + GwInstaState :: state(), + GwState :: state()) -> ok. diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src new file mode 100644 index 000000000..287d710eb --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -0,0 +1,11 @@ +{application, emqx_gateway, + [{description, "The Gateway management application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {emqx_gateway_app, []}}, + {applications, [kernel, stdlib]}, + {env, []}, + {modules, []}, + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl new file mode 100644 index 000000000..f6c12ad53 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -0,0 +1,84 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway). + +-include("include/emqx_gateway.hrl"). + +%% APIs +-export([ registered_gateway/0 + , create/4 + , remove/1 + , lookup/1 + , update/1 + , start/1 + , stop/1 + , list/0 + ]). + +-spec registered_gateway() -> + [{gateway_type(), emqx_gateway_registry:descriptor()}]. +registered_gateway() -> + emqx_gateway_registry:list(). + +%%-------------------------------------------------------------------- +%% Gateway Instace APIs + +-spec list() -> [instance()]. +list() -> + lists:append(lists:map( + fun({_, Insta}) -> Insta end, + emqx_gateway_sup:list_gateway_insta() + )). + +-spec create(gateway_type(), binary(), binary(), map()) + -> {ok, pid()} + | {error, any()}. +create(Type, Name, Descr, RawConf) -> + Insta = #{ id => clacu_insta_id(Type, Name) + , type => Type + , name => Name + , descr => Descr + , rawconf => RawConf + }, + emqx_gateway_sup:create_gateway_insta(Insta). + +-spec remove(instance_id()) -> ok | {error, any()}. +remove(InstaId) -> + emqx_gateway_sup:remove_gateway_insta(InstaId). + +-spec lookup(instance_id()) -> instance() | undefined. +lookup(InstaId) -> + emqx_gateway_sup:lookup_gateway_insta(InstaId). + +-spec update(instance()) -> ok | {error, any()}. +update(NewInsta) -> + emqx_gateway_sup:update_gateway_insta(NewInsta). + +-spec start(instance_id()) -> ok | {error, any()}. +start(InstaId) -> + emqx_gateway_sup:start_gateway_insta(InstaId). + +-spec stop(instance_id()) -> ok | {error, any()}. +stop(InstaId) -> + emqx_gateway_sup:stop_gateway_insta(InstaId). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +clacu_insta_id(Type, Name) when is_binary(Name) -> + list_to_atom(lists:concat([Type, "#", binary_to_list(Name)])). diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl new file mode 100644 index 000000000..230776b73 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -0,0 +1,93 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_app). + +-behaviour(application). + +-include_lib("emqx/include/logger.hrl"). + +-emqx_plugin(?MODULE). + +-logger_header("[Gateway]"). + +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_gateway_sup:start_link(), + emqx_gateway_cli:load(), + load_default_gateway_applications(), + create_gateway_by_default(), + {ok, Sup}. + +stop(_State) -> + emqx_gateway_cli:unload(), + ok. + +%%-------------------------------------------------------------------- +%% Internal funcs + +load_default_gateway_applications() -> + Apps = gateway_type_searching(), + ?LOG(info, "Starting the default gateway types: ~p", [Apps]), + lists:foreach(fun load/1, Apps). + +gateway_type_searching() -> + %% FIXME: Hardcoded apps + [emqx_stomp_impl]. + +load(Mod) -> + try + Mod:load(), + ?LOG(info, "Load ~s gateway application successfully!", [Mod]) + catch + Class : Reason -> + ?LOG(error, "Load ~s gateway application failed: {~p, ~p}", + [Mod, Class, Reason]) + end. + +create_gateway_by_default() -> + create_gateway_by_default(zipped_confs()). + +create_gateway_by_default([]) -> + ok; +create_gateway_by_default([{Type, Name, Confs}|More]) -> + case emqx_gateway_registry:lookup(Type) of + undefined -> + ?LOG(error, "Skip to start ~p#~p: not_registred_type", + [Type, Name]); + _ -> + case emqx_gateway:create(Type, + atom_to_binary(Name, utf8), + <<>>, + Confs) of + {ok, _} -> + ?LOG(debug, "Start ~p#~p successfully!", [Type, Name]); + {error, Reason} -> + ?LOG(error, "Start ~p#~p failed: ~0p", + [Type, Name, Reason]) + end + end, + create_gateway_by_default(More). + +zipped_confs() -> + All = maps:to_list(emqx_config:get([emqx_gateway])), + lists:append(lists:foldr( + fun({Type, Gws}, Acc) -> + {Names, Confs} = lists:unzip(maps:to_list(Gws)), + Types = [ Type || _ <- lists:seq(1, length(Names))], + [lists:zip3(Types, Names, Confs) | Acc] + end, [], All)). diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl new file mode 100644 index 000000000..beb3e5eae --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -0,0 +1,201 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The Command-Line-Interface module for Gateway Application +-module(emqx_gateway_cli). + +-export([ load/0 + , unload/0 + ]). + +-export([ gateway/1 + , 'gateway-registry'/1 + , 'gateway-clients'/1 + , 'gateway-metrics'/1 + %, 'gateway-banned'/1 + ]). + +-spec load() -> ok. +load() -> + Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)], + lists:foreach(fun(Cmd) -> emqx_ctl:register_command(Cmd, {?MODULE, Cmd}, []) end, Cmds). + +-spec unload() -> ok. +unload() -> + Cmds = [Fun || {Fun, _} <- ?MODULE:module_info(exports), is_cmd(Fun)], + lists:foreach(fun(Cmd) -> emqx_ctl:unregister_command(Cmd) end, Cmds). + +is_cmd(Fun) -> + not lists:member(Fun, [init, load, module_info]). + + +%%-------------------------------------------------------------------- +%% Cmds + +gateway(["list"]) -> + lists:foreach(fun(#{id := InstaId, name := Name, type := Type}) -> + %% FIXME: Get the real running status + emqx_ctl:print("Gateway(~s, name=~s, type=~s, status=running~n", + [InstaId, Name, Type]) + end, emqx_gateway:list()); + +gateway(["lookup", GatewayInstaId]) -> + case emqx_gateway:lookup(GatewayInstaId) of + undefined -> + emqx_ctl:print("undefined"); + Info -> + emqx_ctl:print("~p~n", [Info]) + end; + +gateway(["stop", GatewayInstaId]) -> + case emqx_gateway:stop(GatewayInstaId) of + ok -> + emqx_ctl:print("ok"); + {error, Reason} -> + emqx_ctl:print("Error: ~p~n", [Reason]) + end; + +gateway(["start", GatewayInstaId]) -> + case emqx_gateway:start(GatewayInstaId) of + ok -> + emqx_ctl:print("ok"); + {error, Reason} -> + emqx_ctl:print("Error: ~p~n", [Reason]) + end; + +gateway(_) -> + %% TODO: create/remove APIs + emqx_ctl:usage([ {"gateway list", + "List all created gateway instances"} + , {"gateway lookup ", + "Looup a gateway detailed informations"} + , {"gateway stop ", + "Stop a gateway instance and release all resources"} + , {"gateway start ", + "Start a gateway instance"} + ]). + +'gateway-registry'(["list"]) -> + lists:foreach( + fun({GwType, #{cbkmod := CbMod}}) -> + emqx_ctl:print("Registered Type: ~s, Callback Module: ~s~n", [GwType, CbMod]) + end, + emqx_gateway_registry:list()); + +'gateway-registry'(_) -> + emqx_ctl:usage([ {"gateway-registry list", + "List all registered gateway types"} + ]). + +'gateway-clients'(["list", Type]) -> + InfoTab = emqx_gateway_cm:tabname(info, Type), + dump(InfoTab, client); + +'gateway-clients'(["lookup", Type, ClientId]) -> + ChanTab = emqx_gateway_cm:tabname(chan, Type), + case ets:lookup(ChanTab, bin(ClientId)) of + [] -> emqx_ctl:print("Not Found.~n"); + [Chann] -> + InfoTab = emqx_gateway_cm:tabname(info, Type), + [ChannInfo] = ets:lookup(InfoTab, Chann), + print({client, ChannInfo}) + end; + +'gateway-clients'(["kick", Type, ClientId]) -> + case emqx_gateway_cm:kick_session(Type, bin(ClientId)) of + ok -> emqx_ctl:print("ok~n"); + _ -> emqx_ctl:print("Not Found.~n") + end; + +'gateway-clients'(_) -> + emqx_ctl:usage([ {"gateway-clients list ", + "List all clients for a type of gateway"} + , {"gateway-clients lookup ", + "Lookup the Client Info for specified client"} + , {"gateway-clients kick ", + "Kick out a client"} + ]). + +'gateway-metrics'([GatewayType]) -> + Tab = emqx_gateway_metrics:tabname(GatewayType), + case ets:info(Tab) of + undefined -> + emqx_ctl:print("Bad Gateway Tyep.~n"); + _ -> + lists:foreach( + fun({K, V}) -> + emqx_ctl:print("~-30s: ~w~n", [K, V]) + end, lists:sort(ets:tab2list(Tab))) + end; + +'gateway-metrics'(_) -> + emqx_ctl:usage([ {"gateway-metrics ", + "List all metrics for a type of gateway"} + ]). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +bin(S) -> iolist_to_binary(S). + +dump(Table, Tag) -> + dump(Table, Tag, ets:first(Table), []). + +dump(_Table, _, '$end_of_table', Result) -> + lists:reverse(Result); + +dump(Table, Tag, Key, Result) -> + PrintValue = [print({Tag, Record}) || Record <- ets:lookup(Table, Key)], + dump(Table, Tag, ets:next(Table, Key), [PrintValue | Result]). + +print({client, {_, Infos, Stats}}) -> + ClientInfo = maps:get(clientinfo, Infos, #{}), + ConnInfo = maps:get(conninfo, Infos, #{}), + _Session = maps:get(session, Infos, #{}), + SafeGet = fun(K, M) -> maps:get(K, M, undefined) end, + StatsGet = fun(K) -> proplists:get_value(K, Stats, 0) end, + + ConnectedAt = SafeGet(connected_at, ConnInfo), + InfoKeys = [clientid, username, peername, clean_start, keepalive, + subscriptions_cnt, send_msg, connected, created_at, connected_at], + Info = #{ clientid => SafeGet(clientid, ClientInfo), + username => SafeGet(username, ClientInfo), + peername => SafeGet(peername, ConnInfo), + clean_start => SafeGet(clean_start, ConnInfo), + keepalive => SafeGet(keepalive, ConnInfo), + subscriptions_cnt => StatsGet(subscriptions_cnt), + send_msg => StatsGet(send_msg), + connected => SafeGet(conn_state, ClientInfo) == connected, + created_at => ConnectedAt, + connected_at => ConnectedAt + }, + + emqx_ctl:print("Client(~s, username=~s, peername=~s, " + "clean_start=~s, keepalive=~w, " + "subscriptions=~w, delivered_msgs=~w, " + "connected=~s, created_at=~w, connected_at=~w)~n", + [format(K, maps:get(K, Info)) || K <- InfoKeys]). + +format(_, undefined) -> + undefined; + +format(peername, {IPAddr, Port}) -> + IPStr = emqx_mgmt_util:ntoa(IPAddr), + io_lib:format("~s:~p", [IPStr, Port]); + +format(_, Val) -> + Val. diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl new file mode 100644 index 000000000..f8ca18c1a --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -0,0 +1,447 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway connection management +-module(emqx_gateway_cm). + +-behaviour(gen_server). + +-include("include/emqx_gateway.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-logger_header("[PGW-CM]"). + +%% APIs +-export([start_link/1]). + +-export([ open_session/5 + , kick_session/2 + , kick_session/3 + , register_channel/4 + , unregister_channel/2 + , insert_channel_info/4 + , set_chan_info/3 + , set_chan_info/4 + , get_chan_info/2 + , get_chan_info/3 + , set_chan_stats/3 + , set_chan_stats/4 + , get_chan_stats/2 + , get_chan_stats/3 + , connection_closed/2 + ]). + +%% Internal funcs for getting tabname by GatewayId +-export([cmtabs/1, tabname/2]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + type :: atom(), %% Gateway Id + locker :: pid(), %% ClientId Locker for CM + registry :: pid(), %% ClientId Registry server + chan_pmon :: emqx_pmon:pmon() + }). + +-define(T_TAKEOVER, 15000). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +%% XXX: Options for cm process +start_link(Options) -> + Type = proplists:get_value(type, Options), + gen_server:start_link({local, procname(Type)}, ?MODULE, Options, []). + +procname(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_cm'])). + +-spec cmtabs(Type :: atom()) -> {ChanTab :: atom(), + ConnTab :: atom(), + ChannInfoTab :: atom()}. +cmtabs(Type) -> + { tabname(chan, Type) %% Client Tabname; Record: {ClientId, Pid} + , tabname(conn, Type) %% Client ConnMod; Recrod: {{ClientId, Pid}, ConnMod} + , tabname(info, Type) %% ClientInfo Tabname; Record: {{ClientId, Pid}, ClientInfo, ClientStats} + }. + +tabname(chan, Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel'])); +tabname(conn, Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_conn'])); +tabname(info, Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_info'])). + +lockername(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_locker'])). + +-spec register_channel(atom(), binary(), pid(), emqx_types:conninfo()) -> ok. +register_channel(Type, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) -> + Chan = {ClientId, ChanPid}, + true = ets:insert(tabname(chan, Type), Chan), + true = ets:insert(tabname(conn, Type), {Chan, ConnMod}), + ok = emqx_gateway_cm_registry:register_channel(Type, Chan), + cast(procname(Type), {registered, Chan}). + +%% @doc Unregister a channel. +-spec unregister_channel(atom(), emqx_types:clientid()) -> ok. +unregister_channel(Type, ClientId) when is_binary(ClientId) -> + true = do_unregister_channel(Type, {ClientId, self()}, cmtabs(Type)), + ok. + +%% @doc Insert/Update the channel info and stats +-spec insert_channel_info(atom(), + emqx_types:clientid(), + emqx_types:infos(), + emqx_types:stats()) -> ok. +insert_channel_info(Type, ClientId, Info, Stats) -> + Chan = {ClientId, self()}, + true = ets:insert(tabname(info, Type), {Chan, Info, Stats}), + %%?tp(debug, insert_channel_info, #{client_id => ClientId}), + ok. + +%% @doc Get info of a channel. +-spec get_chan_info(gateway_type(), emqx_types:clientid()) + -> emqx_types:infos() | undefined. +get_chan_info(Type, ClientId) -> + with_channel(Type, ClientId, + fun(ChanPid) -> + get_chan_info(Type, ClientId, ChanPid) + end). + +-spec get_chan_info(gateway_type(), emqx_types:clientid(), pid()) + -> emqx_types:infos() | undefined. +get_chan_info(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(tabname(info, Type), Chan, 2) + catch + error:badarg -> undefined + end; +get_chan_info(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_info, [Type, ClientId, ChanPid]). + +%% @doc Update infos of the channel. +-spec set_chan_info(gateway_type(), + emqx_types:clientid(), + emqx_types:infos()) -> boolean(). +set_chan_info(Type, ClientId, Infos) -> + set_chan_info(Type, ClientId, self(), Infos). + +-spec set_chan_info(gateway_type(), + emqx_types:clientid(), + pid(), + emqx_types:infos()) -> boolean(). +set_chan_info(Type, ClientId, ChanPid, Infos) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:update_element(tabname(info, Type), Chan, {2, Infos}) + catch + error:badarg -> false + end; +set_chan_info(Type, ClientId, ChanPid, Infos) -> + rpc_call(node(ChanPid), set_chan_info, [Type, ClientId, ChanPid, Infos]). + +%% @doc Get channel's stats. +-spec get_chan_stats(gateway_type(), emqx_types:clientid()) + -> emqx_types:stats() | undefined. +get_chan_stats(Type, ClientId) -> + with_channel(Type, ClientId, + fun(ChanPid) -> + get_chan_stats(Type, ClientId, ChanPid) + end). + +-spec get_chan_stats(gateway_type(), emqx_types:clientid(), pid()) + -> emqx_types:stats() | undefined. +get_chan_stats(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(tabname(info, Type), Chan, 3) + catch + error:badarg -> undefined + end; +get_chan_stats(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_stats, [Type, ClientId, ChanPid]). + +-spec set_chan_stats(gateway_type(), + emqx_types:clientid(), + emqx_types:stats()) -> boolean(). +set_chan_stats(Type, ClientId, Stats) -> + set_chan_stats(Type, ClientId, self(), Stats). + +-spec set_chan_stats(gateway_type(), + emqx_types:clientid(), + pid(), + emqx_types:stats()) -> boolean(). +set_chan_stats(Type, ClientId, ChanPid, Stats) when node(ChanPid) == node() -> + Chan = {ClientId, self()}, + try ets:update_element(tabname(info, Type), Chan, {3, Stats}) + catch + error:badarg -> false + end; +set_chan_stats(Type, ClientId, ChanPid, Stats) -> + rpc_call(node(ChanPid), set_chan_stats, [Type, ClientId, ChanPid, Stats]). + +-spec connection_closed(gateway_type(), emqx_types:clientid()) -> true. +connection_closed(Type, ClientId) -> + %% XXX: Why we need to delete conn_mod tab ??? + Chan = {ClientId, self()}, + ets:delete_object(tabname(conn, Type), Chan). + +-spec open_session(Type :: atom(), CleanStart :: boolean(), + ClientInfo :: emqx_types:clientinfo(), + ConnInfo :: emqx_types:conninfo(), + CreateSessionFun :: function()) + -> {ok, #{session := map(), + present := boolean(), + pendings => list() + }} + | {error, any()}. + +open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + Self = self(), + ClientId = maps:get(clientid, ClientInfo), + Fun = fun(_) -> + ok = discard_session(Type, ClientId), + Session = create_session(Type, + ClientInfo, + ConnInfo, + CreateSessionFun + ), + register_channel(Type, ClientId, Self, ConnInfo), + {ok, #{session => Session, present => false}} + end, + locker_trans(Type, ClientId, Fun); + +open_session(_Type, false = _CleanStart, + _ClientInfo, _ConnInfo, _CreateSessionFun) -> + {error, not_supported_now}. + +%% @private +create_session(_Type, ClientInfo, ConnInfo, CreateSessionFun) -> + try + Session = emqx_gateway_utils:apply( + CreateSessionFun, + [ClientInfo, ConnInfo] + ), + %% TODO: v0.2 session metrics & hooks + %ok = emqx_metrics:inc('session.created'), + %ok = emqx_hooks:run('session.created', [ClientInfo, emqx_session:info(Session)]), + Session + catch + Class : Reason : Stk -> + ?LOG(error, "Failed to create a session: ~p, ~p " + "Stacktrace:~0p", [Class, Reason, Stk]), + throw(Reason) + end. + +%% @doc Discard all the sessions identified by the ClientId. +-spec discard_session(Type :: atom(), binary()) -> ok. +discard_session(Type, ClientId) when is_binary(ClientId) -> + case lookup_channels(Type, ClientId) of + [] -> ok; + ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(Type, ClientId, Pid) end, ChanPids) + end. + +%% @private +do_discard_session(Type, ClientId, Pid) -> + try + discard_session(Type, ClientId, Pid) + catch + _ : noproc -> % emqx_ws_connection: call + %?tp(debug, "session_already_gone", #{pid => Pid}), + ok; + _ : {noproc, _} -> % emqx_connection: gen_server:call + %?tp(debug, "session_already_gone", #{pid => Pid}), + ok; + _ : {{shutdown, _}, _} -> + %?tp(debug, "session_already_shutdown", #{pid => Pid}), + ok; + _ : _Error : _St -> + %?tp(error, "failed_to_discard_session", + % #{pid => Pid, reason => Error, stacktrace=>St}) + ok + end. + +%% @private +discard_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chann_conn_mod(Type, ClientId, ChanPid) of + undefined -> ok; + ConnMod when is_atom(ConnMod) -> + ConnMod:call(ChanPid, discard, ?T_TAKEOVER) + end; + +%% @private +discard_session(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), discard_session, [Type, ClientId, ChanPid]). + +-spec kick_session(gateway_type(), emqx_types:clientid()) + -> {error, any()} + | ok. +kick_session(Type, ClientId) -> + case lookup_channels(Type, ClientId) of + [] -> {error, not_found}; + [ChanPid] -> + kick_session(Type, ClientId, ChanPid); + ChanPids -> + [ChanPid|StalePids] = lists:reverse(ChanPids), + ?LOG(error, "More than one channel found: ~p", [ChanPids]), + lists:foreach(fun(StalePid) -> + catch discard_session(Type, ClientId, StalePid) + end, StalePids), + kick_session(Type, ClientId, ChanPid) + end. + +kick_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chan_info(Type, ClientId, ChanPid) of + #{conninfo := #{conn_mod := ConnMod}} -> + ConnMod:call(ChanPid, kick, ?T_TAKEOVER); + undefined -> + {error, not_found} + end; + +kick_session(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), kick_session, [Type, ClientId, ChanPid]). + +with_channel(Type, ClientId, Fun) -> + case lookup_channels(Type, ClientId) of + [] -> undefined; + [Pid] -> Fun(Pid); + Pids -> Fun(lists:last(Pids)) + end. + +%% @doc Lookup channels. +-spec(lookup_channels(atom(), emqx_types:clientid()) -> list(pid())). +lookup_channels(Type, ClientId) -> + emqx_gateway_cm_registry:lookup_channels(Type, ClientId). + +get_chann_conn_mod(Type, ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try [ConnMod] = ets:lookup_element(tabname(conn, Type), Chan, 2), ConnMod + catch + error:badarg -> undefined + end; +get_chann_conn_mod(Type, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chann_conn_mod, [Type, ClientId, ChanPid]). + +%% Locker + +locker_trans(_Type, undefined, Fun) -> + Fun([]); +locker_trans(Type, ClientId, Fun) -> + Locker = lockername(Type), + case locker_lock(Locker, ClientId) of + {true, Nodes} -> + try Fun(Nodes) after locker_unlock(Locker, ClientId) end; + {false, _Nodes} -> + {error, client_id_unavailable} + end. + +locker_lock(Locker, ClientId) -> + ekka_locker:acquire(Locker, ClientId, quorum). + +locker_unlock(Locker, ClientId) -> + ekka_locker:release(Locker, ClientId, quorum). + +%% @private +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> error(Reason); + Res -> Res + end. + +cast(Name, Msg) -> + gen_server:cast(Name, Msg). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init(Options) -> + Type = proplists:get_value(type, Options), + + TabOpts = [public, {write_concurrency, true}], + + {ChanTab, ConnTab, InfoTab} = cmtabs(Type), + ok = emqx_tables:new(ChanTab, [bag, {read_concurrency, true}|TabOpts]), + ok = emqx_tables:new(ConnTab, [bag | TabOpts]), + ok = emqx_tables:new(InfoTab, [set, compressed | TabOpts]), + + %% Start link cm-registry process + %% XXX: Should I hang it under a higher level supervisor? + {ok, Registry} = emqx_gateway_cm_registry:start_link(Type), + + %% Start locker process + {ok, Locker} = ekka_locker:start_link(lockername(Type)), + + %% Interval update stats + %% TODO: v0.2 + %ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), + + {ok, #state{type = Type, + locker = Locker, + registry = Registry, + chan_pmon = emqx_pmon:new()}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast({registered, {ClientId, ChanPid}}, State = #state{chan_pmon = PMon}) -> + PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon), + {noreply, State#state{chan_pmon = PMon1}}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', _MRef, process, Pid, _Reason}, + State = #state{type = Type, chan_pmon = PMon}) -> + ChanPids = [Pid | emqx_misc:drain_down(10000)], %% XXX: Fixed BATCH_SIZE + {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), + + CmTabs = cmtabs(Type), + ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, Type, CmTabs]), + {noreply, State#state{chan_pmon = PMon1}}; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +do_unregister_channel_task(Items, Type, CmTabs) -> + lists:foreach( + fun({ChanPid, ClientId}) -> + do_unregister_channel(Type, {ClientId, ChanPid}, CmTabs) + end, Items). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +do_unregister_channel(Type, Chan, {ChanTab, ConnTab, InfoTab}) -> + ok = emqx_gateway_cm_registry:unregister_channel(Type, Chan), + true = ets:delete(ConnTab, Chan), + true = ets:delete(InfoTab, Chan), + ets:delete_object(ChanTab, Chan). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl new file mode 100644 index 000000000..4275fdf3e --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl @@ -0,0 +1,141 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway connection registry +-module(emqx_gateway_cm_registry). + +-behaviour(gen_server). + +-logger_header("[PGW-CM-Registy]"). + +-export([start_link/1]). + +%% XXX: needless +%-export([is_enabled/0]). + +-export([ register_channel/2 + , unregister_channel/2 + ]). + +-export([lookup_channels/2]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-define(LOCK, {?MODULE, cleanup_down}). + +-record(channel, {chid, pid}). + +%% @doc Start the global channel registry. +-spec(start_link(atom()) -> gen_server:startlink_ret()). +start_link(Type) -> + gen_server:start_link(?MODULE, [Type], []). + +-spec tabname(atom()) -> atom(). +tabname(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_channel_registry'])). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +%% @doc Register a global channel. +-spec register_channel(atom(), binary() | {binary(), pid()}) -> ok. +register_channel(Type, ClientId) when is_binary(ClientId) -> + register_channel(Type, {ClientId, self()}); + +register_channel(Type, {ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> + ekka_mnesia:dirty_write(tabname(Type), record(ClientId, ChanPid)). + +%% @doc Unregister a global channel. +-spec unregister_channel(atom(), binary() | {binary(), pid()}) -> ok. +unregister_channel(Type, ClientId) when is_binary(ClientId) -> + unregister_channel(Type, {ClientId, self()}); + +unregister_channel(Type, {ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> + ekka_mnesia:dirty_delete_object(tabname(Type), record(ClientId, ChanPid)). + +%% @doc Lookup the global channels. +-spec lookup_channels(atom(), binary()) -> list(pid()). +lookup_channels(Type, ClientId) -> + [ChanPid || #channel{pid = ChanPid} <- mnesia:dirty_read(tabname(Type), ClientId)]. + +record(ClientId, ChanPid) -> + #channel{chid = ClientId, pid = ChanPid}. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Type]) -> + Tab = tabname(Type), + ok = ekka_mnesia:create_table(Tab, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, channel}, + {attributes, record_info(fields, channel)}, + {storage_properties, [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}]}]), + ok = ekka_mnesia:copy_table(Tab, ram_copies), + %%ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), + ok = ekka:monitor(membership), + {ok, #{type => Type}}. + +handle_call(Req, _From, State) -> + logger:error("Unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + logger:error("Unexpected cast: ~p", [Msg]), + {noreply, State}. + +handle_info({membership, {mnesia, down, Node}}, State = #{type := Type}) -> + Tab = tabname(Type), + global:trans({?LOCK, self()}, + fun() -> + %% FIXME: The shard name should be fixed later + ekka_mnesia:transaction(?MODULE, fun cleanup_channels/2, [Node, Tab]) + end), + {noreply, State}; + +handle_info({membership, _Event}, State) -> + {noreply, State}; + +handle_info(Info, State) -> + logger:error("Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +cleanup_channels(Node, Tab) -> + Pat = [{#channel{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], + lists:foreach(fun(Chan) -> + mnesia:delete_object(Tab, Chan, write) + end, mnesia:select(Tab, Pat, write)). diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl new file mode 100644 index 000000000..5dadf2aca --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -0,0 +1,148 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway instance context +-module(emqx_gateway_ctx). + +-include("include/emqx_gateway.hrl"). + +-logger_header(["PGW-Ctx"]). + +%% @doc The running context for a Connection/Channel process. +%% +%% The `Context` encapsulates a complex structure of contextual information. +%% It is convenient to use it directly in Channel/Connection to read +%% configuration, register devices and other common operations. +%% +-type context() :: + #{ %% Gateway Instance ID + instid := instance_id() + %% Gateway ID + , type := gateway_type() + %% Autenticator + , auth := allow_anonymous | emqx_authentication:chain_id() + %% The ConnectionManager PID + , cm := pid() + }. + +%% Authentication circle +-export([ authenticate/2 + , open_session/5 + , insert_channel_info/4 + , set_chan_info/3 + , set_chan_stats/3 + , connection_closed/2 + ]). + +%% Message circle +-export([ authorize/4 + % Needless for pub/sub + %, publish/3 + %, subscribe/4 + ]). + +%% Metrics & Stats +-export([ metrics_inc/2 + , metrics_inc/3 + ]). + +%%-------------------------------------------------------------------- +%% Authentication circle + +%% @doc Authenticate whether the client has access to the Broker. +-spec authenticate(context(), emqx_types:clientinfo()) + -> {ok, emqx_types:clientinfo()} + | {error, any()}. +authenticate(_Ctx = #{auth := allow_anonymous}, ClientInfo) -> + {ok, ClientInfo#{anonymous => true}}; +authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> + ClientInfo = ClientInfo0#{ + zone => undefined, + chain_id => ChainId + }, + case emqx_access_control:authenticate(ClientInfo) of + {ok, AuthResult} -> + {ok, mountpoint(maps:merge(ClientInfo, AuthResult))}; + {error, Reason} -> + {error, Reason} + end. + +%% @doc Register the session to the cluster. +%% +%% This function should be called after the client has authenticated +%% successfully so that the client can be managed in the cluster. +-spec open_session(context(), boolean(), emqx_types:clientinfo(), + emqx_types:conninfo(), function()) + -> {ok, #{session := any(), + present := boolean(), + pendings => list() + }} + | {error, any()}. +open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun) -> + logger:warning("clean_start=false is not supported now, " + "fallback to clean_start mode"), + open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun); + +open_session(_Ctx = #{type := Type}, + CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + emqx_gateway_cm:open_session(Type, CleanStart, + ClientInfo, ConnInfo, CreateSessionFun). + +-spec insert_channel_info(context(), + emqx_types:clientid(), + emqx_types:infos(), + emqx_types:stats()) -> ok. +insert_channel_info(_Ctx = #{type := Type}, ClientId, Infos, Stats) -> + emqx_gateway_cm:insert_channel_info(Type, ClientId, Infos, Stats). + +%% @doc Set the Channel Info to the ConnectionManager for this client +-spec set_chan_info(context(), + emqx_types:clientid(), + emqx_types:infos()) -> boolean(). +set_chan_info(_Ctx = #{type := Type}, ClientId, Infos) -> + emqx_gateway_cm:set_chan_info(Type, ClientId, Infos). + +-spec set_chan_stats(context(), + emqx_types:clientid(), + emqx_types:stats()) -> boolean(). +set_chan_stats(_Ctx = #{type := Type}, ClientId, Stats) -> + emqx_gateway_cm:set_chan_stats(Type, ClientId, Stats). + +-spec connection_closed(context(), emqx_types:clientid()) -> boolean(). +connection_closed(_Ctx = #{type := Type}, ClientId) -> + emqx_gateway_cm:connection_closed(Type, ClientId). + +-spec authorize(context(), emqx_types:clientinfo(), + emqx_types:pubsub(), emqx_types:topic()) + -> allow | deny. +authorize(_Ctx, ClientInfo, PubSub, Topic) -> + emqx_access_control:authorize(ClientInfo, PubSub, Topic). + +metrics_inc(_Ctx = #{type := Type}, Name) -> + emqx_gateway_metrics:inc(Type, Name). + +metrics_inc(_Ctx = #{type := Type}, Name, Oct) -> + emqx_gateway_metrics:inc(Type, Name, Oct). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +mountpoint(ClientInfo = #{mountpoint := undefined}) -> + ClientInfo; +mountpoint(ClientInfo = #{mountpoint := MountPoint}) -> + MountPoint1 = emqx_mountpoint:replvar(MountPoint, ClientInfo), + ClientInfo#{mountpoint := MountPoint1}. diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl new file mode 100644 index 000000000..21ad30c0d --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -0,0 +1,135 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The Gateway Top supervisor. +-module(emqx_gateway_gw_sup). + +-behaviour(supervisor). + +-include("include/emqx_gateway.hrl"). + +-export([start_link/1]). + +-export([ create_insta/3 + , remove_insta/2 + , update_insta/2 + , start_insta/2 + , stop_insta/2 + , list_insta/1 + ]). + +%% Supervisor callbacks +-export([init/1]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Type) -> + supervisor:start_link({local, Type}, ?MODULE, [Type]). + +-spec create_insta(pid(), instance(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}. +create_insta(Sup, Insta = #{id := InstaId}, GwDscrptr) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + {ok, _GwInstaPid} -> {error, alredy_existed}; + false -> + %% XXX: More instances options to it? + %% + Ctx = ctx(Sup, InstaId), + %% + ChildSpec = emqx_gateway_utils:childspec( + InstaId, + worker, + emqx_gateway_insta_sup, + [Insta, Ctx, GwDscrptr] + ), + emqx_gateway_utils:supervisor_ret( + supervisor:start_child(Sup, ChildSpec) + ) + end. + +-spec remove_insta(pid(), InstaId :: atom()) -> ok | {error, any()}. +remove_insta(Sup, InstaId) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> ok; + {ok, _GwInstaPid} -> + ok = supervisor:terminate_child(Sup, InstaId), + ok = supervisor:delete_child(Sup, InstaId) + end. + +-spec update_insta(pid(), NewInsta :: instance()) -> ok | {error, any()}. +update_insta(Sup, NewInsta = #{id := InstaId}) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> {error, not_found}; + {ok, GwInstaPid} -> + emqx_gateway_insta_sup:update(GwInstaPid, NewInsta) + end. + +-spec start_insta(pid(), atom()) -> ok | {error, any()}. +start_insta(Sup, InstaId) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> {error, not_found}; + {ok, GwInstaPid} -> + emqx_gateway_insta_sup:enable(GwInstaPid) + end. + +-spec stop_insta(pid(), atom()) -> ok | {error, any()}. +stop_insta(Sup, InstaId) -> + case emqx_gateway_utils:find_sup_child(Sup, InstaId) of + false -> {error, not_found}; + {ok, GwInstaPid} -> + emqx_gateway_insta_sup:disable(GwInstaPid) + end. + +-spec list_insta(pid()) -> [instance()]. +list_insta(Sup) -> + lists:filtermap( + fun({InstaId, GwInstaPid, _Type, _Mods}) -> + is_gateway_insta_id(InstaId) + andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)} + end, supervisor:which_children(Sup)). + +%% Supervisor callback + +%% @doc Initialize Top Supervisor for a Protocol +init([Type]) -> + SupFlags = #{ strategy => one_for_one + , intensity => 10 + , period => 60 + }, + CmOpts = [{type, Type}], + CM = emqx_gateway_utils:childspec(worker, emqx_gateway_cm, [CmOpts]), + Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [Type]), + {ok, {SupFlags, [CM, Metrics]}}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +ctx(Sup, InstaId) -> + {_, Type} = erlang:process_info(Sup, registered_name), + {ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm), + #{ instid => InstaId + , type => Type + , cm => CM + }. + +is_gateway_insta_id(emqx_gateway_cm) -> + false; +is_gateway_insta_id(emqx_gateway_metrics) -> + false; +is_gateway_insta_id(_Id) -> + true. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl new file mode 100644 index 000000000..9f21f0e05 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -0,0 +1,312 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The gateway instance management +-module(emqx_gateway_insta_sup). + +-behaviour(gen_server). + +-include("include/emqx_gateway.hrl"). + +-logger_header("[PGW-Insta-Sup]"). + +%% APIs +-export([ start_link/3 + , info/1 + , disable/1 + , enable/1 + , update/2 + ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + insta :: instance(), + ctx :: emqx_gateway_ctx:context(), + status :: stopped | running, + child_pids :: [pid()], + insta_state :: emqx_gateway_impl:state() | undefined + }). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Insta, Ctx, GwDscrptr) -> + gen_server:start_link( + {local, ?MODULE}, + ?MODULE, + [Insta, Ctx, GwDscrptr], + [] + ). + +-spec info(pid()) -> instance(). +info(Pid) -> + gen_server:call(Pid, info). + +%% @doc Stop instance +-spec disable(pid()) -> ok | {error, any()}. +disable(Pid) -> + call(Pid, disable). + +%% @doc Start instance +-spec enable(pid()) -> ok | {error, any()}. +enable(Pid) -> + call(Pid, enable). + +%% @doc Update the gateway instance configurations +-spec update(pid(), instance()) -> ok | {error, any()}. +update(Pid, NewInsta) -> + call(Pid, {update, NewInsta}). + +call(Pid, Req) -> + gen_server:call(Pid, Req, 5000). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Insta, Ctx0, _GwDscrptr]) -> + process_flag(trap_exit, true), + #{rawconf := RawConf} = Insta, + Ctx = do_init_context(RawConf, Ctx0), + State = #state{ + insta = Insta, + ctx = Ctx, + child_pids = [], + status = stopped + }, + case cb_insta_create(State) of + {error, _Reason} -> + do_deinit_context(Ctx), + %% XXX: Return Reason?? + {stop, create_gateway_instance_failed}; + {ok, NState} -> + {ok, NState} + end. + +do_init_context(RawConf, Ctx) -> + Auth = case maps:get(authenticator, RawConf, allow_anonymous) of + allow_anonymous -> allow_anonymous; + Funcs when is_list(Funcs) -> + create_authenticator_for_gateway_insta(Funcs) + end, + Ctx#{auth => Auth}. + +do_deinit_context(Ctx) -> + cleanup_authenticator_for_gateway_insta(maps:get(auth, Ctx)), + ok. + +handle_call(info, _From, State = #state{insta = Insta}) -> + {reply, Insta, State}; + +handle_call(disable, _From, State = #state{status = Status}) -> + case Status of + running -> + case cb_insta_destroy(State) of + {ok, NState} -> + {reply, ok, NState}; + {error, Reason} -> + {reply, {error, Reason}, State} + end; + _ -> + {reply, {error, already_stopped}, State} + end; + +handle_call(enable, _From, State = #state{status = Status}) -> + case Status of + stopped -> + case cb_insta_create(State) of + {error, Reason} -> + {reply, {error, Reason}, State}; + {ok, NState} -> + {reply, ok, NState} + end; + _ -> + {reply, {error, already_started}, State} + end; + +%% Stopped -> update +handle_call({update, NewInsta}, _From, State = #state{insta = Insta, + status = stopped}) -> + case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of + true -> + {reply, ok, State#state{insta = NewInsta}}; + false -> + {reply, {error, bad_instan_id}, State} + end; + +%% Running -> update +handle_call({update, NewInsta}, _From, State = #state{insta = Insta, + status = running}) -> + case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of + true -> + case cb_insta_update(NewInsta, State) of + {ok, NState} -> + {reply, ok, NState}; + {error, Reason} -> + {reply, {error, Reason}, State} + end; + false -> + {reply, {error, bad_instan_id}, State} + end; + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'EXIT', Pid, Reason}, State = #state{child_pids = Pids}) -> + case lists:member(Pid, Pids) of + true -> + logger:error("Child process ~p exited: ~0p.", [Pid, Reason]), + case Pids -- [Pid]of + [] -> + logger:error("All child process exited!"), + {noreply, State#state{status = stopped, + child_pids = [], + insta_state = undefined}}; + RemainPids -> + {noreply, State#state{child_pids = RemainPids}} + end; + _ -> + logger:error("Unknown process exited ~p:~0p", [Pid, Reason]), + {noreply, State} + end; + +handle_info(Info, State) -> + logger:warning("Unexcepted info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) -> + %% Cleanup instances + %% Step1. Destory instance + Pids /= [] andalso (_ = cb_insta_destroy(State)), + %% Step2. Delete authenticator resources + _ = do_deinit_context(Ctx), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +create_authenticator_for_gateway_insta(_Funcs) -> + todo. + +cleanup_authenticator_for_gateway_insta(allow_anonymouse) -> + ok; +cleanup_authenticator_for_gateway_insta(_ChainId) -> + todo. + +cb_insta_destroy(State = #state{insta = Insta = #{type := Type}, + insta_state = InstaState}) -> + try + #{cbkmod := CbMod, + state := GwState} = emqx_gateway_registry:lookup(Type), + CbMod:on_insta_destroy(Insta, InstaState, GwState), + {ok, State#state{child_pids = [], + insta_state = undefined, + status = stopped}} + catch + Class : Reason : Stk -> + logger:error("Destroy instance (~0p, ~0p, _) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [Insta, InstaState, + Class, Reason, Stk]), + {error, {Class, Reason, Stk}} + end. + +cb_insta_create(State = #state{insta = Insta = #{type := Type}, + ctx = Ctx}) -> + try + #{cbkmod := CbMod, + state := GwState} = emqx_gateway_registry:lookup(Type), + case CbMod:on_insta_create(Insta, Ctx, GwState) of + {error, Reason} -> throw({callback_return_error, Reason}); + {ok, InstaPidOrSpecs, InstaState} -> + ChildPids = start_child_process(InstaPidOrSpecs), + {ok, State#state{ + status = running, + child_pids = ChildPids, + insta_state = InstaState + }} + end + catch + Class : Reason1 : Stk -> + logger:error("Create instance (~0p, ~0p, _) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [Insta, Ctx, + Class, Reason1, Stk]), + {error, {Class, Reason1, Stk}} + end. + +cb_insta_update(NewInsta, + State = #state{insta = Insta = #{type := Type}, + ctx = Ctx, + insta_state = GwInstaState}) -> + try + #{cbkmod := CbMod, + state := GwState} = emqx_gateway_registry:lookup(Type), + case CbMod:on_insta_update(NewInsta, Insta, GwInstaState, GwState) of + {error, Reason} -> throw({callback_return_error, Reason}); + {ok, InstaPidOrSpecs, InstaState} -> + %% XXX: Hot-upgrade ??? + ChildPids = start_child_process(InstaPidOrSpecs), + {ok, State#state{ + status = running, + child_pids = ChildPids, + insta_state = InstaState + }} + end + catch + Class : Reason1 : Stk -> + logger:error("Update instance (~0p, ~0p, ~0p, _) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [NewInsta, Insta, Ctx, + Class, Reason1, Stk]), + {error, {Class, Reason1, Stk}} + end. + +start_child_process([Indictor|_] = InstaPidOrSpecs) -> + case erlang:is_pid(Indictor) of + true -> + InstaPidOrSpecs; + _ -> + do_start_child_process(InstaPidOrSpecs) + end. + +do_start_child_process(ChildSpecs) when is_list(ChildSpecs) -> + lists:map(fun do_start_child_process/1, ChildSpecs); + +do_start_child_process(_ChildSpec = #{start := {M, F, A}}) -> + case erlang:apply(M, F, A) of + {ok, Pid} -> + Pid; + {error, Reason} -> + throw({start_child_process, Reason}) + end. diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl new file mode 100644 index 000000000..04b711d0a --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -0,0 +1,101 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_metrics). + +-behaviour(gen_server). + +-include("include/emqx_gateway.hrl"). + +-logger_header("[PGW-Metrics]"). + +%% APIs +-export([start_link/1]). + +-export([ inc/2 + , inc/3 + , dec/2 + , dec/3 + ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-export([tabname/1]). + +-record(state, {}). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Type) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [Type], []). + +-spec inc(gateway_type(), atom()) -> ok. +inc(Type, Name) -> + inc(Type, Name, 1). + +-spec inc(gateway_type(), atom(), integer()) -> ok. +inc(Type, Name, Oct) -> + ets:update_counter(tabname(Type), Name, {2, Oct}, {Name, 0}), + ok. + +-spec dec(gateway_type(), atom()) -> ok. +dec(Type, Name) -> + inc(Type, Name, -1). + +-spec dec(gateway_type(), atom(), non_neg_integer()) -> ok. +dec(Type, Name, Oct) -> + inc(Type, Name, -Oct). + +tabname(Type) -> + list_to_atom(lists:concat([emqx_gateway_, Type, '_metrics'])). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Type]) -> + TabOpts = [public, {write_concurrency, true}], + ok = emqx_tables:new(tabname(Type), [set|TabOpts]), + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl new file mode 100644 index 000000000..a100636cf --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -0,0 +1,163 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The Registry Centre of Gateway Type +-module(emqx_gateway_registry). + +-include("include/emqx_gateway.hrl"). + +-logger_header("[PGW-Registry]"). + +-behavior(gen_server). + +%% APIs for Impl. +-export([ load/3 + , unload/1 + ]). + +-export([ list/0 + , lookup/1 + ]). + +%% APIs +-export([start_link/0]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + loaded = #{} :: #{ gateway_type() => descriptor() } + }). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% Mgmt +%%-------------------------------------------------------------------- + +-type registry_options() :: [registry_option()]. + +-type registry_option() :: {cbkmod, atom()}. + +-type gateway_options() :: list(). + +-type descriptor() :: #{ cbkmod := atom() + , rgopts := registry_options() + , gwopts := gateway_options() + , state => any() + }. + +-spec load(gateway_type(), registry_options(), gateway_options()) -> ok | {error, any()}. +load(Type, RgOpts, GwOpts) -> + CbMod = proplists:get_value(cbkmod, RgOpts, Type), + Dscrptr = #{ cbkmod => CbMod + , rgopts => RgOpts + , gwopts => GwOpts + }, + call({load, Type, Dscrptr}). + +-spec unload(gateway_type()) -> ok | {error, any()}. +unload(Type) -> + %% TODO: Checking ALL INSTACE HAS STOPPED + call({unload, Type}). + +%% TODO: +%unload(Type, Force) -> +% call({unload, Type, Froce}). + +%% @doc Return all registered protocol gateway implementation +-spec list() -> [{gateway_type(), descriptor()}]. +list() -> + call(all). + +-spec lookup(gateway_type()) -> undefined | descriptor(). +lookup(Type) -> + call({lookup, Type}). + +call(Req) -> + gen_server:call(?MODULE, Req, 5000). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([]) -> + %% TODO: Metrics ??? + process_flag(trap_exit, true), + {ok, #state{loaded = #{}}}. + +handle_call({load, Type, Dscrptr}, _From, State = #state{loaded = Gateways}) -> + case maps:get(Type, Gateways, notfound) of + notfound -> + try + GwOpts = maps:get(gwopts, Dscrptr), + CbMod = maps:get(cbkmod, Dscrptr), + {ok, GwState} = CbMod:init(GwOpts), + NDscrptr = maps:put(state, GwState, Dscrptr), + NGateways = maps:put(Type, NDscrptr, Gateways), + {reply, ok, State#state{loaded = NGateways}} + catch + Class : Reason : Stk -> + logger:error("Load ~s crashed {~p, ~p}; stacktrace: ~0p", + [Type, Class, Reason, Stk]), + {reply, {error, {Class, Reason}}, State} + end; + _ -> + {reply, {error, already_existed}, State} + end; + +handle_call({unload, Type}, _From, State = #state{loaded = Gateways}) -> + case maps:get(Type, Gateways, undefined) of + undefined -> + {reply, ok, State}; + _ -> + emqx_gateway_sup:stop_all_suptree(Type), + {reply, ok, State#state{loaded = maps:remove(Type, Gateways)}} + end; + +handle_call(all, _From, State = #state{loaded = Gateways}) -> + {reply, maps:to_list(Gateways), State}; + +handle_call({lookup, Type}, _From, State = #state{loaded = Gateways}) -> + Reply = maps:get(Type, Gateways, undefined), + {reply, Reply, State}; + +handle_call(Req, _From, State) -> + logger:error("Unexpected call: ~0p", [Req]), + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl new file mode 100644 index 000000000..8f05582c7 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -0,0 +1,178 @@ +-module(emqx_gateway_schema). + +-dialyzer(no_return). +-dialyzer(no_match). +-dialyzer(no_contracts). +-dialyzer(no_unused). +-dialyzer(no_fail_call). + +-include_lib("typerefl/include/types.hrl"). + +-type flag() :: true | false. +-type duration() :: integer(). +-type bytesize() :: integer(). +-type comma_separated_list() :: list(). +-type ip_port() :: tuple(). + +-typerefl_from_string({flag/0, emqx_schema, to_flag}). +-typerefl_from_string({duration/0, emqx_schema, to_duration}). +-typerefl_from_string({bytesize/0, emqx_schema, to_bytesize}). +-typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}). +-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). + +-behaviour(hocon_schema). + +-reflect_type([ flag/0 + , duration/0 + , bytesize/0 + , comma_separated_list/0 + , ip_port/0 + ]). + +-export([structs/0 , fields/1]). +-export([t/1, t/3, t/4, ref/1]). + +structs() -> ["emqx_gateway"]. + +fields("emqx_gateway") -> + [{stomp, t(ref(stomp))}]; + +fields(stomp) -> + [{"$id", t(ref(stomp_structs))}]; + +fields(stomp_structs) -> + [ {frame, t(ref(stomp_frame))} + , {clientinfo_override, t(ref(clientinfo_override))} + , {authenticator, t(union([allow_anonymous]))} + , {listener, t(ref(listener))} + ]; + +fields(stomp_frame) -> + [ {max_headers, t(integer(), undefined, 10)} + , {max_headers_length, t(integer(), undefined, 1024)} + , {max_body_length, t(integer(), undefined, 8192)} + ]; + +fields(clientinfo_override) -> + [ {username, t(string())} + , {password, t(string())} + , {clientid, t(string())} + ]; + +fields(listener) -> + [ {tcp, t(ref(tcp_listener))} + , {ssl, t(ref(ssl_listener))} + ]; + +fields(tcp_listener) -> + [ {"$name", t(ref(tcp_listener_settings))}]; + +fields(ssl_listener) -> + [ {"$name", t(ref(ssl_listener_settings))}]; + +fields(listener_settings) -> + %[ {"bind", t(union(ip_port(), integer()))} + [ {bind, t(integer())} + , {acceptors, t(integer(), undefined, 8)} + , {max_connections, t(integer(), undefined, 1024)} + , {max_conn_rate, t(integer())} + , {active_n, t(integer(), undefined, 100)} + %, {zone, t(string())} + %, {rate_limit, t(comma_separated_list())} + , {access, t(ref(access))} + , {proxy_protocol, t(flag())} + , {proxy_protocol_timeout, t(duration())} + , {backlog, t(integer(), undefined, 1024)} + , {send_timeout, t(duration(), undefined, "15s")} + , {send_timeout_close, t(flag(), undefined, true)} + , {recbuf, t(bytesize())} + , {sndbuf, t(bytesize())} + , {buffer, t(bytesize())} + , {high_watermark, t(bytesize(), undefined, "1MB")} + , {tune_buffer, t(flag())} + , {nodelay, t(boolean())} + , {reuseaddr, t(boolean())} + ]; + +fields(tcp_listener_settings) -> + [ + %% some special confs for tcp listener + ] ++ fields(listener_settings); + +fields(ssl_listener_settings) -> + [ + %% some special confs for ssl listener + ] ++ + ssl(undefined, #{handshake_timeout => "15s" + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); + +fields(access) -> + [ {"$id", #{type => string(), + nullable => true}}]; + +fields(ExtraField) -> + Mod = list_to_atom(ExtraField++"_schema"), + Mod:fields(ExtraField). + +%translations() -> []. +% +%translations(_) -> []. + +%%-------------------------------------------------------------------- +%% Helpers + +%% types + +t(Type) -> #{type => Type}. + +t(Type, Mapping, Default) -> + hoconsc:t(Type, #{mapping => Mapping, default => Default}). + +t(Type, Mapping, Default, OverrideEnv) -> + hoconsc:t(Type, #{ mapping => Mapping + , default => Default + , override_env => OverrideEnv + }). + +ref(Field) -> + hoconsc:ref(?MODULE, Field). + +%% utils + +%% generate a ssl field. +%% ssl("emqx", #{"verify" => verify_peer}) will return +%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)} +%% , {"certfile", t(string(), "emqx.certfile", undefined)} +%% , {"keyfile", t(string(), "emqx.keyfile", undefined)} +%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)} +%% , {"server_name_indication", "emqx.server_name_indication", undefined)} +%% ... +ssl(Mapping, Defaults) -> + M = fun (Field) -> + case (Mapping) of + undefined -> undefined; + _ -> Mapping ++ "." ++ Field + end end, + D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, + [ {"enable", t(flag(), M("enable"), D("enable"))} + , {"cacertfile", t(string(), M("cacertfile"), D("cacertfile"))} + , {"certfile", t(string(), M("certfile"), D("certfile"))} + , {"keyfile", t(string(), M("keyfile"), D("keyfile"))} + , {"verify", t(union(verify_peer, verify_none), M("verify"), D("verify"))} + , {"fail_if_no_peer_cert", t(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} + , {"secure_renegotiate", t(flag(), M("secure_renegotiate"), D("secure_renegotiate"))} + , {"reuse_sessions", t(flag(), M("reuse_sessions"), D("reuse_sessions"))} + , {"honor_cipher_order", t(flag(), M("honor_cipher_order"), D("honor_cipher_order"))} + , {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))} + , {"depth", t(integer(), M("depth"), D("depth"))} + , {"password", hoconsc:t(string(), #{mapping => M("key_password"), + default => D("key_password"), + sensitive => true + })} + , {"dhfile", t(string(), M("dhfile"), D("dhfile"))} + , {"server_name_indication", t(union(disable, string()), M("server_name_indication"), + D("server_name_indication"))} + , {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))} + , {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))} + , {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}]. diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl new file mode 100644 index 000000000..d56b27e52 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -0,0 +1,194 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_sup). + +-behaviour(supervisor). + +-include("include/emqx_gateway.hrl"). + +-export([start_link/0]). + +%% Gateway Instance APIs +-export([ create_gateway_insta/1 + , remove_gateway_insta/1 + , lookup_gateway_insta/1 + , update_gateway_insta/1 + , start_gateway_insta/1 + , stop_gateway_insta/1 + , list_gateway_insta/1 + , list_gateway_insta/0 + ]). + +%% Gateway APs +-export([ list_started_gateway/0 + , stop_all_suptree/1 + ]). + +%% supervisor callbacks +-export([init/1]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +-spec create_gateway_insta(instance()) -> {ok, pid()} | {error, any()}. +create_gateway_insta(Insta = #{type := Type}) -> + case emqx_gateway_registry:lookup(Type) of + undefined -> {error, {unknown_gateway_id, Type}}; + GwDscrptr -> + {ok, GwSup} = ensure_gateway_suptree_ready(gatewayid(Type)), + emqx_gateway_gw_sup:create_insta(GwSup, Insta, GwDscrptr) + end. + +-spec remove_gateway_insta(instance_id()) -> ok | {error, any()}. +remove_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {GwSup, _}} -> + emqx_gateway_gw_sup:remove_insta(GwSup, InstaId); + _ -> + ok + end. + +-spec lookup_gateway_insta(instance_id()) -> instance() | undefined. +lookup_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {_, GwInstaPid}} -> + emqx_gateway_insta_sup:info(GwInstaPid); + _ -> + undefined + end. + +-spec update_gateway_insta(instance()) + -> ok + | {error, any()}. +update_gateway_insta(NewInsta = #{type := Type}) -> + case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of + {ok, GwSup} -> + emqx_gateway_gw_sup:update_insta(GwSup, NewInsta); + _ -> {error, not_found} + end. + +start_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {GwSup, _}} -> + emqx_gateway_gw_sup:start_insta(GwSup, InstaId); + _ -> {error, not_found} + end. + +-spec stop_gateway_insta(instance_id()) -> ok | {error, any()}. +stop_gateway_insta(InstaId) -> + case search_gateway_insta_proc(InstaId) of + {ok, {GwSup, _}} -> + emqx_gateway_gw_sup:stop_insta(GwSup, InstaId); + _ -> {error, not_found} + end. + +-spec list_gateway_insta(gateway_type()) -> {ok, [instance()]} | {error, any()}. +list_gateway_insta(Type) -> + case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of + {ok, GwSup} -> + {ok, emqx_gateway_gw_sup:list_insta(GwSup)}; + _ -> {error, not_found} + end. + +-spec list_gateway_insta() -> [{gateway_type(), instance()}]. +list_gateway_insta() -> + lists:map( + fun(SupId) -> + Instas = emqx_gateway_gw_sup:list_insta(SupId), + {SupId, Instas} + end, list_started_gateway()). + +-spec list_started_gateway() -> [gateway_type()]. +list_started_gateway() -> + started_gateway_type(). + +-spec stop_all_suptree(atom()) -> ok. +stop_all_suptree(Type) -> + case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of + false -> ok; + _ -> + _ = supervisor:terminate_child(?MODULE, Type), + _ = supervisor:delete_child(?MODULE, Type), + ok + end. + +%% Supervisor callback + +init([]) -> + SupFlags = #{ strategy => one_for_one + , intensity => 10 + , period => 60 + }, + ChildSpecs = [ emqx_gateway_utils:childspec(worker, emqx_gateway_registry) + ], + {ok, {SupFlags, ChildSpecs}}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +gatewayid(Type) -> + list_to_atom(lists:concat([Type])). + +ensure_gateway_suptree_ready(Type) -> + case lists:keyfind(Type, 1, supervisor:which_children(?MODULE)) of + false -> + ChildSpec = emqx_gateway_utils:childspec( + Type, + supervisor, + emqx_gateway_gw_sup, + [Type] + ), + emqx_gateway_utils:supervisor_ret( + supervisor:start_child(?MODULE, ChildSpec) + ); + {_Id, Pid, _Type, _Mods} -> + {ok, Pid} + end. + +search_gateway_insta_proc(InstaId) -> + search_gateway_insta_proc(InstaId, started_gateway_pid()). + +search_gateway_insta_proc(_InstaId, []) -> + {error, not_found}; +search_gateway_insta_proc(InstaId, [SupPid|More]) -> + case emqx_gateway_utils:find_sup_child(SupPid, InstaId) of + {ok, InstaPid} -> {ok, {SupPid, InstaPid}}; + _ -> + search_gateway_insta_proc(InstaId, More) + end. + +started_gateway_type() -> + lists:filtermap( + fun({Id, _, _, _}) -> + is_a_gateway_id(Id) andalso {true, Id} + end, supervisor:which_children(?MODULE)). + +started_gateway_pid() -> + lists:filtermap( + fun({Id, Pid, _, _}) -> + is_a_gateway_id(Id) andalso {true, Pid} + end, supervisor:which_children(?MODULE)). + +is_a_gateway_id(Id) -> + Id /= emqx_gateway_registry. + + diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl new file mode 100644 index 000000000..184c3ff87 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -0,0 +1,163 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 Utils funcs for emqx-gateway +-module(emqx_gateway_utils). + +-export([ childspec/2 + , childspec/3 + , childspec/4 + , supervisor_ret/1 + , find_sup_child/2 + ]). + +-export([ apply/2 + ]). + +-export([ normalize_rawconf/1 + ]). + +%% Common Envs +-export([ active_n/1 + , ratelimit/1 + , frame_options/1 + , init_gc_state/1 + , stats_timer/1 + , idle_timeout/1 + , oom_policy/1 + ]). + +-define(ACTIVE_N, 100). +-define(DEFAULT_IDLE_TIMEOUT, 30000). + +-spec childspec(supervisor:worker(), Mod :: atom()) + -> supervisor:child_spec(). +childspec(Type, Mod) -> + childspec(Mod, Type, Mod, []). + +-spec childspec(supervisor:worker(), Mod :: atom(), Args :: list()) + -> supervisor:child_spec(). +childspec(Type, Mod, Args) -> + childspec(Mod, Type, Mod, Args). + +-spec childspec(atom(), supervisor:worker(), Mod :: atom(), Args :: list()) + -> supervisor:child_spec(). +childspec(Id, Type, Mod, Args) -> + #{ id => Id + , start => {Mod, start_link, Args} + , type => Type + }. + +-spec supervisor_ret(supervisor:startchild_ret()) + -> {ok, pid()} + | {error, supervisor:startchild_err()}. +supervisor_ret({ok, Pid, _Info}) -> {ok, Pid}; +supervisor_ret(Ret) -> Ret. + +-spec find_sup_child(Sup :: pid() | atom(), ChildId :: supervisor:child_id()) + -> false + | {ok, pid()}. +find_sup_child(Sup, ChildId) -> + case lists:keyfind(ChildId, 1, supervisor:which_children(Sup)) of + false -> false; + {_Id, Pid, _Type, _Mods} -> {ok, Pid} + end. + +apply({M, F, A}, A2) when is_atom(M), + is_atom(M), + is_list(A), + is_list(A2) -> + erlang:apply(M, F, A ++ A2); +apply({F, A}, A2) when is_function(F), + is_list(A), + is_list(A2) -> + erlang:apply(F, A ++ A2); +apply(F, A2) when is_function(F), + is_list(A2) -> + erlang:apply(F, A2). + +-type listener() :: #{}. + +-type rawconf() :: + #{ clientinfo_override => #{} + , authenticators := #{} + , listeners => listener() + , atom() => any() + }. + +-spec normalize_rawconf(rawconf()) + -> list({ Type :: udp | tcp | ssl | dtls + , ListenOn :: esockd:listen_on() + , SocketOpts :: esockd:option() + , Cfg :: map() + }). +normalize_rawconf(RawConf = #{listener := LisMap}) -> + Cfg0 = maps:without([listener], RawConf), + lists:append(maps:fold(fun(Type, Liss, AccIn1) -> + Listeners = + maps:fold(fun(_Name, Confs, AccIn2) -> + ListenOn = maps:get(bind, Confs), + SocketOpts = esockd:parse_opt(maps:to_list(Confs)), + RemainCfgs = maps:without( + [bind] ++ proplists:get_keys(SocketOpts), + Confs), + Cfg = maps:merge(Cfg0, RemainCfgs), + [{Type, ListenOn, SocketOpts, Cfg}|AccIn2] + end, [], Liss), + [Listeners|AccIn1] + end, [], LisMap)). + +%%-------------------------------------------------------------------- +%% Envs + +active_n(Options) -> + maps:get( + active_n, + maps:get(listener, Options, #{active_n => ?ACTIVE_N}), + ?ACTIVE_N + ). + +-spec idle_timeout(map()) -> pos_integer(). +idle_timeout(Options) -> + maps:get(idle_timeout, Options, ?DEFAULT_IDLE_TIMEOUT). + +-spec ratelimit(map()) -> esockd_rate_limit:config() | undefined. +ratelimit(Options) -> + maps:get(ratelimit, Options, undefined). + +-spec frame_options(map()) -> map(). +frame_options(Options) -> + maps:get(frame, Options, #{}). + +-spec init_gc_state(map()) -> emqx_gc:gc_state() | undefined. +init_gc_state(Options) -> + emqx_misc:maybe_apply(fun emqx_gc:init/1, force_gc_policy(Options)). + +-spec force_gc_policy(map()) -> emqx_gc:opts() | undefined. +force_gc_policy(Options) -> + maps:get(force_gc_policy, Options, undefined). + +-spec oom_policy(map()) -> emqx_types:oom_policy(). +oom_policy(Options) -> + maps:get(force_shutdown_policy, Options). + +-spec stats_timer(map()) -> undefined | disabled. +stats_timer(Options) -> + case enable_stats(Options) of true -> undefined; false -> disabled end. + +-spec enable_stats(map()) -> boolean(). +enable_stats(Options) -> + maps:get(enable_stats, Options, true). diff --git a/apps/emqx_stomp/README.md b/apps/emqx_gateway/src/stomp/README.md similarity index 90% rename from apps/emqx_stomp/README.md rename to apps/emqx_gateway/src/stomp/README.md index ec841b1e6..c5736a755 100644 --- a/apps/emqx_stomp/README.md +++ b/apps/emqx_gateway/src/stomp/README.md @@ -1,13 +1,12 @@ -emqx-stomp -========== +# emqx-stomp + The plugin adds STOMP 1.0/1.1/1.2 protocol supports to the EMQ X broker. The STOMP clients could PubSub to the MQTT clients. -Configuration -------------- +## Configuration etc/emqx_stomp.conf @@ -58,20 +57,17 @@ stomp.frame.max_header_length = 1024 stomp.frame.max_body_length = 8192 ``` -Load the Plugin ---------------- +## Load the Plugin ``` ./bin/emqx_ctl plugins load emqx_stomp ``` -License -------- +## License Apache License Version 2.0 -Author ------- +## Author EMQ X Team. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl new file mode 100644 index 000000000..322baa120 --- /dev/null +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -0,0 +1,978 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_stomp_channel). + +-include("src/stomp/include/emqx_stomp.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-logger_header("[Stomp-Proto]"). + +-import(proplists, [get_value/2, get_value/3]). + +%% API +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ init/2 + , handle_in/2 + , handle_out/3 + , handle_deliver/2 + , handle_timeout/3 + , terminate/2 + , set_conn_state/2 + ]). + +-export([ handle_call/2 + , handle_info/2 + ]). + +%% for trans callback +-export([ handle_recv_send_frame/2 + , handle_recv_ack_frame/2 + , handle_recv_nack_frame/2 + ]). + +-record(channel, { + %% Context + ctx :: emqx_gateway_ctx:context(), + %% Stomp Connection Info + conninfo :: emqx_types:conninfo(), + %% Stomp Client Info + clientinfo :: emqx_types:clientinfo(), + %% ClientInfo override specs + clientinfo_override :: map(), + %% Connection Channel + conn_state :: conn_state(), + %% Heartbeat + heartbeat :: emqx_stomp_heartbeat:heartbeat(), + %% Subscriptions + subscriptions = [], + %% Timer + timers :: #{atom() => disable | undefined | reference()}, + %% Transaction + transaction :: #{binary() => list()} + }). + +-type(channel() :: #channel{}). + +-type(conn_state() :: idle | connecting | connected | disconnected). + +-type(reply() :: {outgoing, stomp_frame()} + | {outgoing, [stomp_frame()]} + | {event, conn_state()|updated} + | {close, Reason :: atom()}). + +-type(replies() :: emqx_stomp_frame:packet() | reply() | [reply()]). + +-define(TIMER_TABLE, #{ + incoming_timer => incoming, + outgoing_timer => outgoing, + clean_trans_timer => clean_trans + }). + +-define(TRANS_TIMEOUT, 60000). + +-define(DEFAULT_OVERRIDE, + #{ clientid => <<"">> %% Generate clientid by default + , username => <<"${Packet.headers.login}">> + , password => <<"${Packet.headers.passcode}">> + }). + +-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]). + +-dialyzer({nowarn_function, [init/2,enrich_conninfo/2,ensure_connected/1, + process_connect/1,handle_in/2,handle_info/2, + ensure_disconnected/2,reverse_heartbeats/1, + negotiate_version/2]}). + +%%-------------------------------------------------------------------- +%% Init the channel +%%-------------------------------------------------------------------- + +%% @doc Init protocol +init(ConnInfo = #{peername := {PeerHost, _}, + sockname := {_, SockPort}}, Option) -> + Peercert = maps:get(peercert, ConnInfo, undefined), + Mountpoint = maps:get(mountpoint, Option, undefined), + ClientInfo = setting_peercert_infos( + Peercert, + #{ zone => undefined + , protocol => stomp + , peerhost => PeerHost + , sockport => SockPort + , clientid => undefined + , username => undefined + , is_bridge => false + , is_superuser => false + , mountpoint => Mountpoint + } + ), + + Ctx = maps:get(ctx, Option), + Override = maps:merge(?DEFAULT_OVERRIDE, + maps:get(clientinfo_override, Option, #{}) + ), + #channel{ ctx = Ctx + , conninfo = ConnInfo + , clientinfo = ClientInfo + , clientinfo_override = Override + , timers = #{} + , transaction = #{} + }. + +setting_peercert_infos(NoSSL, ClientInfo) + when NoSSL =:= nossl; + NoSSL =:= undefined -> + ClientInfo; +setting_peercert_infos(Peercert, ClientInfo) -> + {DN, CN} = {esockd_peercert:subject(Peercert), + esockd_peercert:common_name(Peercert)}, + ClientInfo#{dn => DN, cn => CN}. + +-spec info(channel()) -> emqx_types:infos(). +info(Channel) -> + maps:from_list(info(?INFO_KEYS, Channel)). + +-spec(info(list(atom())|atom(), channel()) -> term()). +info(Keys, Channel) when is_list(Keys) -> + [{Key, info(Key, Channel)} || Key <- Keys]; + +info(conninfo, #channel{conninfo = ConnInfo}) -> + ConnInfo; +info(conn_state, #channel{conn_state = ConnState}) -> + ConnState; +info(clientinfo, #channel{clientinfo = ClientInfo}) -> + ClientInfo; +info(session, _) -> + #{}; +info(will_msg, _) -> + undefined; +info(clientid, #channel{clientinfo = #{clientid := ClientId}}) -> + ClientId; +info(ctx, #channel{ctx = Ctx}) -> + Ctx. + +stats(_Channel) -> + []. + +set_conn_state(ConnState, Channel) -> + Channel#channel{conn_state = ConnState}. + +enrich_conninfo(_Packet, + Channel = #channel{conninfo = ConnInfo}) -> + %% XXX: How enrich more infos? + NConnInfo = ConnInfo#{ proto_name => <<"STOMP">> + , proto_ver => undefined + , clean_start => true + , keepalive => 0 + , expiry_interval => 0 + }, + {ok, Channel#channel{conninfo = NConnInfo}}. + +run_conn_hooks(Packet, Channel = #channel{ctx = Ctx, + conninfo = ConnInfo}) -> + %% XXX: Assign headers of Packet to ConnProps + ConnProps = #{}, + case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of + Error = {error, _Reason} -> Error; + _NConnProps -> + {ok, Packet, Channel} + end. + +negotiate_version(#stomp_frame{headers = Headers}, + Channel = #channel{conninfo = ConnInfo}) -> + %% XXX: + case do_negotiate_version(header(<<"accept-version">>, Headers)) of + {ok, Version} -> + {ok, Channel#channel{conninfo = ConnInfo#{proto_ver => Version}}}; + {error, Reason}-> + {error, Reason} + end. + +enrich_clientinfo(Packet, + Channel = #channel{ + conninfo = ConnInfo, + clientinfo = ClientInfo0, + clientinfo_override = Override}) -> + ClientInfo = write_clientinfo( + feedvar(Override, Packet, ConnInfo, ClientInfo0), + ClientInfo0 + ), + {ok, NPacket, NClientInfo} = emqx_misc:pipeline( + [ fun maybe_assign_clientid/2 + , fun parse_heartbeat/2 + %% FIXME: CALL After authentication successfully + , fun fix_mountpoint/2 + ], Packet, ClientInfo + ), + {ok, NPacket, Channel#channel{clientinfo = NClientInfo}}. + +feedvar(Override, Packet, ConnInfo, ClientInfo) -> + Envs = #{ 'ConnInfo' => ConnInfo + , 'ClientInfo' => ClientInfo + , 'Packet' => connect_packet_to_map(Packet) + }, + maps:map(fun(_K, V) -> + Tokens = emqx_rule_utils:preproc_tmpl(V), + emqx_rule_utils:proc_tmpl(Tokens, Envs) + end, Override). + +connect_packet_to_map(#stomp_frame{headers = Headers}) -> + #{headers => maps:from_list(Headers)}. + +write_clientinfo(Override, ClientInfo) -> + Override1 = maps:with([username, password, clientid], Override), + maps:merge(ClientInfo, Override1). + +maybe_assign_clientid(_Packet, ClientInfo = #{clientid := ClientId}) + when ClientId == undefined; + ClientId == <<>> -> + {ok, ClientInfo#{clientid => emqx_guid:to_base62(emqx_guid:gen())}}; + +maybe_assign_clientid(_Packet, ClientInfo) -> + {ok, ClientInfo}. + +parse_heartbeat(#stomp_frame{headers = Headers}, ClientInfo) -> + Heartbeat0 = header(<<"heart-beat">>, Headers, <<"0,0">>), + CxCy = re:split(Heartbeat0, <<",">>, [{return, list}]), + Heartbeat = list_to_tuple([list_to_integer(S) || S <- CxCy]), + {ok, ClientInfo#{heartbeat => Heartbeat}}. + +fix_mountpoint(_Packet, #{mountpoint := undefined}) -> ok; +fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> + %% TODO: Enrich the varibale replacement???? + %% i.e: ${ClientInfo.auth_result.productKey} + Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), + {ok, ClientInfo#{mountpoint := Mountpoint1}}. + +set_log_meta(_Packet, #channel{clientinfo = #{clientid := ClientId}}) -> + emqx_logger:set_metadata_clientid(ClientId), + ok. + +auth_connect(_Packet, Channel = #channel{ctx = Ctx, + clientinfo = ClientInfo}) -> + #{clientid := ClientId, + username := Username} = ClientInfo, + case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of + {ok, NClientInfo} -> + {ok, Channel#channel{clientinfo = NClientInfo}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, Reason} + end. + +ensure_connected(Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)}, + ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), + Channel#channel{conninfo = NConnInfo, + conn_state = connected + }. + +process_connect(Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo + }) -> + SessFun = fun(_,_) -> #{} end, + case emqx_gateway_ctx:open_session( + Ctx, + true, + ClientInfo, + ConnInfo, + SessFun + ) of + {ok, _Sess} -> %% The stomp protocol doesn't have session + #{proto_ver := Version} = ConnInfo, + #{heartbeat := Heartbeat} = ClientInfo, + Headers = [{<<"version">>, Version}, + {<<"heart-beat">>, reverse_heartbeats(Heartbeat)}], + handle_out(connected, Headers, Channel); + {error, Reason} -> + ?LOG(error, "Failed to open session du to ~p", [Reason]), + Headers = [{<<"version">>, <<"1.0,1.1,1.2">>}, + {<<"content-type">>, <<"text/plain">>}], + handle_out(connerr, {Headers, undefined, <<"Not Authenticated">>}, Channel) + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- + +-spec handle_in(emqx_types:packet(), channel()) + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}. + +handle_in(Frame = ?PACKET(?CMD_STOMP), Channel) -> + handle_in(Frame#stomp_frame{command = <<"CONNECT">>}, Channel); + +handle_in(?PACKET(?CMD_CONNECT), + Channel = #channel{conn_state = connected}) -> + {error, unexpected_connect, Channel}; + +handle_in(Packet = ?PACKET(?CMD_CONNECT), Channel) -> + case emqx_misc:pipeline( + [ fun enrich_conninfo/2 + , fun run_conn_hooks/2 + , fun negotiate_version/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + %% TODO: How to implement the banned in the gateway instance? + %, fun check_banned/2 + , fun auth_connect/2 + ], Packet, Channel#channel{conn_state = connecting}) of + {ok, _NPacket, NChannel} -> + process_connect(ensure_connected(NChannel)); + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + handle_out(connerr, {[], undefined, ErrMsg}, NChannel) + end; + +handle_in(Frame = ?PACKET(?CMD_SEND, Headers), + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo + }) -> + Topic = header(<<"destination">>, Headers), + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of + deny -> + handle_out(error, {receipt_id(Headers), "ACL Deny"}, Channel); + allow -> + case header(<<"transaction">>, Headers) of + undefined -> + handle_recv_send_frame(Frame, Channel); + TxId -> + add_action(TxId, {fun ?MODULE:handle_recv_send_frame/2, [Frame]}, receipt_id(Headers), Channel) + end + end; + +handle_in(?PACKET(?CMD_SUBSCRIBE, Headers), + Channel = #channel{ + ctx = Ctx, + subscriptions = Subs, + clientinfo = ClientInfo = #{mountpoint := Mountpoint} + }) -> + SubId = header(<<"id">>, Headers), + Topic = header(<<"destination">>, Headers), + Ack = header(<<"ack">>, Headers, <<"auto">>), + + MountedTopic = emqx_mountpoint:mount(Mountpoint, Topic), + + case lists:keyfind(SubId, 1, Subs) of + {SubId, MountedTopic, Ack} -> + maybe_outgoing_receipt(receipt_id(Headers), Channel); + {SubId, _OtherTopic, _OtherAck} -> + %% FIXME: + ?LOG(error, "Conflicts with subscribed topics ~s, id: ~s", + [_OtherTopic, SubId]), + ErrMsg = "Conflict subscribe id ", + handle_out(error, {receipt_id(Headers), ErrMsg}, Channel); + false -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of + deny -> + handle_out(error, {receipt_id(Headers), "ACL Deny"}, Channel); + allow -> + _ = emqx_broker:subscribe(MountedTopic), + maybe_outgoing_receipt( + receipt_id(Headers), + Channel#channel{subscriptions = [{SubId, MountedTopic, Ack} | Subs]} + ) + end + end; + +handle_in(?PACKET(?CMD_UNSUBSCRIBE, Headers), + Channel = #channel{subscriptions = Subs}) -> + SubId = header(<<"id">>, Headers), + {ok, NChannel} = case lists:keyfind(SubId, 1, Subs) of + {SubId, Topic, _Ack} -> + ok = emqx_broker:unsubscribe(Topic), + {ok, Channel#channel{subscriptions = lists:keydelete(SubId, 1, Subs)}}; + false -> + {ok, Channel} + end, + handle_out(receipt, receipt_id(Headers), NChannel); + +%% XXX: How to ack a frame ??? +handle_in(Frame = ?PACKET(?CMD_ACK, Headers), Channel) -> + case header(<<"transaction">>, Headers) of + undefined -> handle_recv_ack_frame(Frame, Channel); + TxId -> add_action(TxId, {fun ?MODULE:handle_recv_ack_frame/2, [Frame]}, receipt_id(Headers), Channel) + end; + +%% NACK +%% id:12345 +%% transaction:tx1 +%% +%% ^@ +handle_in(Frame = ?PACKET(?CMD_NACK, Headers), Channel) -> + case header(<<"transaction">>, Headers) of + undefined -> handle_recv_nack_frame(Frame, Channel); + TxId -> add_action(TxId, {fun ?MODULE:handle_recv_nack_frame/2, [Frame]}, receipt_id(Headers), Channel) + end; + +%% The transaction header is REQUIRED, and the transaction identifier +%% will be used for SEND, COMMIT, ABORT, ACK, and NACK frames to bind +%% them to the named transaction. +%% +%% BEGIN +%% transaction:tx1 +%% +%% ^@ +handle_in(?PACKET(?CMD_BEGIN, Headers), + Channel = #channel{transaction = Trans}) -> + TxId = header(<<"transaction">>, Headers), + case maps:get(TxId, Trans, undefined) of + undefined -> + StartedAt = erlang:system_time(millisecond), + NChannel = ensure_clean_trans_timer( + Channel#channel{ + transaction = Trans#{TxId => {StartedAt, []}}} + ), + handle_out(receipt, receipt_id(Headers), NChannel); + _ -> + ErrMsg = ["Transaction ", TxId, " already started"], + handle_out(error, {receipt_id(Headers), ErrMsg}, Channel) + end; + +%% COMMIT +%% transaction:tx1 +%% +%% ^@ +handle_in(?PACKET(?CMD_COMMIT, Headers), Channel) -> + with_transaction(Headers, Channel, fun(TxId, Actions) -> + Chann0 = remove_trans(TxId, Channel), + case trans_pipeline(lists:reverse(Actions), [], Chann0) of + {ok, Outgoings, Chann1} -> + maybe_outgoing_receipt(receipt_id(Headers), Outgoings, Chann1); + {error, Reason} -> + %% FIXME: atomic for transaction ?? + ErrMsg = io_lib:format("Execute transaction ~s falied: ~0p", + [TxId, Reason] + ), + handle_out(error, {receipt_id(Headers), ErrMsg}, Chann0) + end + end); + +%% ABORT +%% transaction:tx1 +%% +%% ^@ +handle_in(?PACKET(?CMD_ABORT, Headers), + Channel = #channel{transaction = Trans}) -> + with_transaction(Headers, Channel, fun(Id, _Actions) -> + NChannel = Channel#channel{transaction = maps:remove(Id, Trans)}, + handle_out(receipt, receipt_id(Headers), NChannel) + end); + +handle_in(?PACKET(?CMD_DISCONNECT, Headers), Channel) -> + shutdown_with_recepit(normal, receipt_id(Headers), Channel); + +handle_in({frame_error, Reason}, Channel = #channel{conn_state = _ConnState}) -> + ?LOG(error, "Unexpected frame error: ~p", [Reason]), + shutdown(Reason, Channel). + +with_transaction(Headers, Channel = #channel{transaction = Trans}, Fun) -> + Id = header(<<"transaction">>, Headers), + ReceiptId = receipt_id(Headers), + case maps:get(Id, Trans, undefined) of + {_, Actions} -> + Fun(Id, Actions); + _ -> + ErrMsg = ["Transaction ", Id, " not found"], + handle_out(error, {ReceiptId, ErrMsg}, Channel) + end. + +remove_trans(Id, Channel = #channel{transaction = Trans}) -> + Channel#channel{transaction = maps:remove(Id, Trans)}. + +trans_pipeline([], Outgoings, Channel) -> + {ok, Outgoings, Channel}; + +trans_pipeline([{Func, Args}|More], Outgoings, Channel) -> + case erlang:apply(Func, Args ++ [Channel]) of + {ok, NChannel} -> + trans_pipeline(More, Outgoings, NChannel); + {ok, Outgoings1, NChannel} -> + trans_pipeline(More, Outgoings ++ Outgoings1, NChannel); + {error, Reason} -> + {error, Reason, Channel} + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packet +%%-------------------------------------------------------------------- + +-spec(handle_out(atom(), term(), channel()) + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}). + +handle_out(connerr, {Headers, ReceiptId, ErrMsg}, Channel) -> + Frame = error_frame(Headers, ReceiptId, ErrMsg), + shutdown(ErrMsg, Frame, Channel); + +handle_out(error, {ReceiptId, ErrMsg}, Channel) -> + Frame = error_frame(ReceiptId, ErrMsg), + {ok, Frame, Channel}; + +handle_out(connected, Headers, Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo + }) -> + %% XXX: connection_accepted is not defined by stomp protocol + _ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]), + Replies = [{outgoing, connected_frame(Headers)}, + {event, connected} + ], + {ok, Replies, ensure_heartbeart_timer(Channel)}; + +handle_out(receipt, undefined, Channel) -> + {ok, Channel}; +handle_out(receipt, ReceiptId, Channel) -> + Frame = receipt_frame(ReceiptId), + {ok, Frame, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- + +-spec(handle_call(Req :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}). +handle_call(kick, Channel) -> + NChannel = ensure_disconnected(kicked, Channel), + shutdown_and_reply(kicked, ok, NChannel); + +handle_call(discard, Channel) -> + shutdown_and_reply(discarded, ok, Channel); + +%% XXX: No Session Takeover +%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +% reply(Session, Channel#channel{takeover = true}); +% +%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +% pendings = Pendings}) -> +% ok = emqx_session:takeover(Session), +% %% TODO: Should not drain deliver here (side effect) +% Delivers = emqx_misc:drain_deliver(), +% AllPendings = lists:append(Delivers, Pendings), +% shutdown_and_reply(takeovered, AllPendings, Channel); + +handle_call(list_acl_cache, Channel) -> + {reply, emqx_acl_cache:list_acl_cache(), Channel}; + +%% XXX: No Quota Now +% handle_call({quota, Policy}, Channel) -> +% Zone = info(zone, Channel), +% Quota = emqx_limiter:init(Zone, Policy), +% reply(ok, Channel#channel{quota = Quota}); + +handle_call(Req, Channel) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + reply(ignored, Channel). + +%%-------------------------------------------------------------------- +%% Handle Info +%%-------------------------------------------------------------------- + +-spec(handle_info(Info :: term(), channel()) + -> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}). + +%% XXX: Received from the emqx-management ??? +%handle_info({subscribe, TopicFilters}, Channel ) -> +% {_, NChannel} = lists:foldl( +% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) -> +% do_subscribe(TopicFilter, SubOpts, ChannelAcc) +% end, {[], Channel}, parse_topic_filters(TopicFilters)), +% {ok, NChannel}; +% +%handle_info({unsubscribe, TopicFilters}, Channel) -> +% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel), +% {ok, NChannel}; + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = idle}) -> + shutdown(Reason, Channel); + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = connecting}) -> + shutdown(Reason, Channel); + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = connected, + clientinfo = _ClientInfo}) -> + %% XXX: Flapping detect ??? + %% How to get the flapping detect policy ??? + %emqx_zone:enable_flapping_detect(Zone) + % andalso emqx_flapping:detect(ClientInfo), + NChannel = ensure_disconnected(Reason, Channel), + %% XXX: Session keepper detect here + shutdown(Reason, NChannel); + +handle_info({sock_closed, Reason}, + Channel = #channel{conn_state = disconnected}) -> + ?LOG(error, "Unexpected sock_closed: ~p", [Reason]), + {ok, Channel}; + +handle_info(clean_acl_cache, Channel) -> + ok = emqx_acl_cache:empty_acl_cache(), + {ok, Channel}; + +handle_info(Info, Channel) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Ensure disconnected + +ensure_disconnected(Reason, Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)}, + ok = run_hooks(Ctx, 'client.disconnected', + [ClientInfo, Reason, NConnInfo]), + Channel#channel{conninfo = NConnInfo, conn_state = disconnected}. + +%%-------------------------------------------------------------------- +%% Handle Delivers from broker to client +%%-------------------------------------------------------------------- + +-spec(handle_deliver(list(emqx_types:deliver()), channel()) + -> {ok, channel()} + | {ok, replies(), channel()}). + +handle_deliver(Delivers, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo, + subscriptions = Subs + }) -> + + %% TODO: Re-deliver ??? + %% Shared-subscription support ??? + + Frames0 = lists:foldl(fun({_, _, Message}, Acc) -> + Topic0 = emqx_message:topic(Message), + case lists:keyfind(Topic0, 2, Subs) of + {Id, Topic, Ack} -> + %% XXX: refactor later + metrics_inc('messages.delivered', Channel), + NMessage = run_hooks_without_metrics( + Ctx, + 'message.delivered', + [ClientInfo], + Message + ), + Topic = emqx_message:topic(NMessage), + Headers = emqx_message:get_headers(NMessage), + Payload = emqx_message:payload(NMessage), + Headers0 = [{<<"subscription">>, Id}, + {<<"message-id">>, next_msgid()}, + {<<"destination">>, Topic}, + {<<"content-type">>, <<"text/plain">>}], + Headers1 = case Ack of + _ when Ack =:= <<"client">>; + Ack =:= <<"client-individual">> -> + Headers0 ++ [{<<"ack">>, next_ackid()}]; + _ -> + Headers0 + end, + Frame = #stomp_frame{command = <<"MESSAGE">>, + headers = Headers1 ++ maps:get(stomp_headers, Headers, []), + body = Payload + }, + [Frame|Acc]; + false -> + ?LOG(error, "Dropped message ~0p due to not found " + "subscription id for ~s", + [Message, emqx_message:topic(Message)]), + metrics_inc('delivery.dropped', Channel), + metrics_inc('delivery.dropped.no_subid', Channel), + Acc + end + end, [], Delivers), + {ok, [{outgoing, lists:reverse(Frames0)}], Channel}. + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- + +-spec(handle_timeout(reference(), Msg :: term(), channel()) + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()}). + +handle_timeout(_TRef, {incoming, NewVal}, + Channel = #channel{heartbeat = HrtBt}) -> + case emqx_stomp_heartbeat:check(incoming, NewVal, HrtBt) of + {error, timeout} -> + shutdown(heartbeat_timeout, Channel); + {ok, NHrtBt} -> + {ok, reset_timer(incoming_timer, + Channel#channel{heartbeat = NHrtBt} + )} + end; + +handle_timeout(_TRef, {outgoing, NewVal}, + Channel = #channel{heartbeat = HrtBt}) -> + case emqx_stomp_heartbeat:check(outgoing, NewVal, HrtBt) of + {error, timeout} -> + NHrtBt = emqx_stomp_heartbeat:reset(outgoing, NewVal, HrtBt), + NChannel = Channel#channel{heartbeat = NHrtBt}, + {ok, emqx_stomp_frame:make(heartbeat), + reset_timer(outgoing_timer, NChannel)}; + {ok, NHrtBt} -> + {ok, reset_timer(outgoing_timer, + Channel#channel{heartbeat = NHrtBt} + )} + end; + +handle_timeout(_TRef, clean_trans, Channel = #channel{transaction = Trans}) -> + Now = erlang:system_time(millisecond), + NTrans = maps:filter(fun(_, {StartedAt, _}) -> + StartedAt + ?TRANS_TIMEOUT < Now + end, Trans), + {ok, ensure_clean_trans_timer(Channel#channel{transaction = NTrans})}. + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- + +terminate(_Reason, _Channel) -> + ok. + +reply(Reply, Channel) -> + {reply, Reply, Channel}. + +shutdown(Reason, Channel) -> + {shutdown, Reason, Channel}. + +shutdown_with_recepit(Reason, ReceiptId, Channel) -> + case ReceiptId of + undefined -> + {shutdown, Reason, Channel}; + _ -> + {shutdown, Reason, receipt_frame(ReceiptId), Channel} + end. + +shutdown(Reason, AckFrame, Channel) -> + {shutdown, Reason, AckFrame, Channel}. + +shutdown_and_reply(Reason, Reply, Channel) -> + {shutdown, Reason, Reply, Channel}. + +do_negotiate_version(undefined) -> + {ok, <<"1.0">>}; + +do_negotiate_version(Accepts) -> + do_negotiate_version( + ?STOMP_VER, + lists:reverse(lists:sort(binary:split(Accepts, <<",">>, [global]))) + ). + +do_negotiate_version(Ver, []) -> + {error, <<"Supported protocol versions < ", Ver/binary>>}; +do_negotiate_version(Ver, [AcceptVer|_]) when Ver >= AcceptVer -> + {ok, AcceptVer}; +do_negotiate_version(Ver, [_|T]) -> + do_negotiate_version(Ver, T). + +header(Name, Headers) -> + get_value(Name, Headers). +header(Name, Headers, Val) -> + get_value(Name, Headers, Val). + +connected_frame(Headers) -> + emqx_stomp_frame:make(<<"CONNECTED">>, Headers). + +receipt_frame(ReceiptId) -> + emqx_stomp_frame:make(<<"RECEIPT">>, [{<<"receipt-id">>, ReceiptId}]). + +error_frame(ReceiptId, Msg) -> + error_frame([{<<"content-type">>, <<"text/plain">>}], ReceiptId, Msg). + +error_frame(Headers, undefined, Msg) -> + emqx_stomp_frame:make(<<"ERROR">>, Headers, Msg); +error_frame(Headers, ReceiptId, Msg) -> + emqx_stomp_frame:make(<<"ERROR">>, [{<<"receipt-id">>, ReceiptId} | Headers], Msg). + +next_msgid() -> + MsgId = case get(msgid) of + undefined -> 1; + I -> I + end, + put(msgid, MsgId + 1), + MsgId. + +next_ackid() -> + AckId = case get(ackid) of + undefined -> 1; + I -> I + end, + put(ackid, AckId + 1), + AckId. + +frame2message(?PACKET(?CMD_SEND, Headers, Body), + #channel{ + conninfo = #{proto_ver := ProtoVer}, + clientinfo = #{ + protocol := Protocol, + clientid := ClientId, + username := Username, + peerhost := PeerHost, + mountpoint := Mountpoint + }}) -> + Topic = header(<<"destination">>, Headers), + Msg = emqx_message:make(ClientId, Topic, Body), + StompHeaders = lists:foldl( + fun(Key, Headers0) -> + proplists:delete(Key, Headers0) + end, Headers, + [<<"destination">>, + <<"content-length">>, + <<"content-type">>, + <<"transaction">>, + <<"receipt">> + ]), + %% Pass-through of custom headers on the sending side + NMsg = emqx_message:set_headers(#{proto_ver => ProtoVer, + protocol => Protocol, + username => Username, + peerhost => PeerHost, + stomp_headers => StompHeaders + }, Msg), + emqx_mountpoint:mount(Mountpoint, NMsg). + +receipt_id(Headers) -> + header(<<"receipt">>, Headers). + +%%-------------------------------------------------------------------- +%% Trans + +add_action(TxId, Action, ReceiptId, Channel = #channel{transaction = Trans}) -> + case maps:get(TxId, Trans, undefined) of + {_StartedAt, Actions} -> + NTrans = Trans#{TxId => {_StartedAt, [Action|Actions]}}, + {ok, Channel#channel{transaction = NTrans}}; + _ -> + {ok, error_frame(ReceiptId, ["Transaction ", TxId, " not found"]), Channel} + end. + +%%-------------------------------------------------------------------- +%% Transaction Handle + +handle_recv_send_frame(Frame = ?PACKET(?CMD_SEND, Headers), Channel) -> + Msg = frame2message(Frame, Channel), + _ = emqx_broker:publish(Msg), + maybe_outgoing_receipt(receipt_id(Headers), Channel). + +handle_recv_ack_frame(?PACKET(?CMD_ACK, Headers), Channel) -> + maybe_outgoing_receipt(receipt_id(Headers), Channel). + +handle_recv_nack_frame(?PACKET(?CMD_NACK, Headers), Channel) -> + maybe_outgoing_receipt(receipt_id(Headers), Channel). + +maybe_outgoing_receipt(undefined, Channel) -> + {ok, [], Channel}; +maybe_outgoing_receipt(ReceiptId, Channel) -> + {ok, [{outgoing, receipt_frame(ReceiptId)}], Channel}. + +maybe_outgoing_receipt(undefined, Outgoings, Channel) -> + {ok, Outgoings, Channel}; +maybe_outgoing_receipt(ReceiptId, Outgoings, Channel) -> + {ok, lists:reverse([receipt_frame(ReceiptId)|Outgoings]), Channel}. + +ensure_clean_trans_timer(Channel = #channel{transaction = Trans}) -> + case maps:size(Trans) of + 0 -> Channel; + _ -> ensure_timer(clean_trans_timer, Channel) + end. + +%%-------------------------------------------------------------------- +%% Heartbeat + +reverse_heartbeats({Cx, Cy}) -> + iolist_to_binary(io_lib:format("~w,~w", [Cy, Cx])). + +ensure_heartbeart_timer(Channel = #channel{clientinfo = ClientInfo}) -> + Heartbeat = maps:get(heartbeat, ClientInfo), + ensure_timer( + [incoming_timer, outgoing_timer], + Channel#channel{heartbeat = emqx_stomp_heartbeat:init(Heartbeat)}). + +%%-------------------------------------------------------------------- +%% Timer + +ensure_timer([Name], Channel) -> + ensure_timer(Name, Channel); +ensure_timer([Name | Rest], Channel) -> + ensure_timer(Rest, ensure_timer(Name, Channel)); + +ensure_timer(Name, Channel = #channel{timers = Timers}) -> + TRef = maps:get(Name, Timers, undefined), + Time = interval(Name, Channel), + case TRef == undefined andalso is_integer(Time) andalso Time > 0 of + true -> ensure_timer(Name, Time, Channel); + false -> Channel %% Timer disabled or exists + end. + +ensure_timer(Name, Time, Channel = #channel{timers = Timers}) -> + Msg = maps:get(Name, ?TIMER_TABLE), + TRef = emqx_misc:start_timer(Time, Msg), + Channel#channel{timers = Timers#{Name => TRef}}. + +reset_timer(Name, Channel) -> + ensure_timer(Name, clean_timer(Name, Channel)). + +clean_timer(Name, Channel = #channel{timers = Timers}) -> + Channel#channel{timers = maps:remove(Name, Timers)}. + +interval(incoming_timer, #channel{heartbeat = HrtBt}) -> + emqx_stomp_heartbeat:interval(incoming, HrtBt); +interval(outgoing_timer, #channel{heartbeat = HrtBt}) -> + emqx_stomp_heartbeat:interval(outgoing, HrtBt); +interval(clean_trans_timer, _) -> + ?TRANS_TIMEOUT. + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +run_hooks(Ctx, Name, Args) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run(Name, Args). + +run_hooks(Ctx, Name, Args, Acc) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run_fold(Name, Args, Acc). + +run_hooks_without_metrics(_Ctx, Name, Args, Acc) -> + emqx_hooks:run_fold(Name, Args, Acc). + +metrics_inc(Name, #channel{ctx = Ctx}) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl new file mode 100644 index 000000000..a4b87fcd4 --- /dev/null +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_connection.erl @@ -0,0 +1,908 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_stomp_connection). + +-include("src/stomp/include/emqx_stomp.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +-logger_header("[Stomp-Conn]"). + +%% API +-export([ start_link/3 + , stop/1 + ]). + +-export([ info/1 + , stats/1 + ]). + +-export([ async_set_keepalive/3 + , async_set_keepalive/4 + , async_set_socket_options/2 + ]). + +-export([ call/2 + , call/3 + , cast/2 + ]). + +%% Callback +-export([init/4]). + +%% Sys callbacks +-export([ system_continue/3 + , system_terminate/4 + , system_code_change/4 + , system_get_state/1 + ]). + +%% Internal callback +-export([wakeup_from_hib/2, recvloop/2, get_state/1]). + +%% Export for CT +-export([set_field/3]). + +-import(emqx_misc, + [ maybe_apply/2 + ]). + +-record(state, { + %% TCP/TLS Transport + transport :: esockd:transport(), + %% TCP/TLS Socket + socket :: esockd:socket(), + %% Peername of the connection + peername :: emqx_types:peername(), + %% Sockname of the connection + sockname :: emqx_types:peername(), + %% Sock State + sockstate :: emqx_types:sockstate(), + %% The {active, N} option + active_n :: pos_integer(), + %% Limiter + limiter :: emqx_limiter:limiter() | undefined, + %% Limit Timer + limit_timer :: reference() | undefined, + %% Parse State + parse_state :: emqx_stomp_frame:parse_state(), + %% Serialize options + serialize :: emqx_stomp_frame:serialize_opts(), + %% Channel State + channel :: emqx_stomp_channel:channel(), + %% GC State + gc_state :: emqx_gc:gc_state() | undefined, + %% Stats Timer + stats_timer :: disabled | reference(), + %% Idle Timeout + idle_timeout :: integer(), + %% Idle Timer + idle_timer :: reference() | undefined + }). + +-type(state() :: #state{}). + +-define(ACTIVE_N, 100). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). + +-define(ENABLED(X), (X =/= undefined)). + +%-define(ALARM_TCP_CONGEST(Channel), +% list_to_binary(io_lib:format("mqtt_conn/congested/~s/~s", +% [emqx_stomp_channel:info(clientid, Channel), +% emqx_stomp_channel:info(username, Channel)]))). +%-define(ALARM_CONN_INFO_KEYS, [ +% socktype, sockname, peername, +% clientid, username, proto_name, proto_ver, connected_at +%]). +%-define(ALARM_SOCK_STATS_KEYS, [send_pend, recv_cnt, recv_oct, send_cnt, send_oct]). +%-define(ALARM_SOCK_OPTS_KEYS, [high_watermark, high_msgq_watermark, sndbuf, recbuf, buffer]). + +-dialyzer({no_match, [info/2]}). +-dialyzer({nowarn_function, [ init/4 + , init_state/3 + , run_loop/2 + , system_terminate/4 + , system_code_change/4 + ]}). + +-dialyzer({nowarn_function, [ensure_stats_timer/2,cancel_stats_timer/1, + terminate/2,handle_call/3,handle_timeout/3, + parse_incoming/3,serialize_and_inc_stats_fun/1, + check_oom/1,inc_incoming_stats/1, + inc_outgoing_stats/1]}). + +-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) + -> {ok, pid()}). +start_link(Transport, Socket, Options) -> + Args = [self(), Transport, Socket, Options], + CPid = proc_lib:spawn_link(?MODULE, init, Args), + {ok, CPid}. + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc Get infos of the connection/channel. +-spec(info(pid()|state()) -> emqx_types:infos()). +info(CPid) when is_pid(CPid) -> + call(CPid, info); +info(State = #state{channel = Channel}) -> + ChanInfo = emqx_stomp_channel:info(Channel), + SockInfo = maps:from_list( + info(?INFO_KEYS, State)), + ChanInfo#{sockinfo => SockInfo}. + +info(Keys, State) when is_list(Keys) -> + [{Key, info(Key, State)} || Key <- Keys]; +info(socktype, #state{transport = Transport, socket = Socket}) -> + Transport:type(Socket); +info(peername, #state{peername = Peername}) -> + Peername; +info(sockname, #state{sockname = Sockname}) -> + Sockname; +info(sockstate, #state{sockstate = SockSt}) -> + SockSt; +info(active_n, #state{active_n = ActiveN}) -> + ActiveN; +info(stats_timer, #state{stats_timer = StatsTimer}) -> + StatsTimer; +info(limit_timer, #state{limit_timer = LimitTimer}) -> + LimitTimer; +info(limiter, #state{limiter = Limiter}) -> + maybe_apply(fun emqx_limiter:info/1, Limiter). + +%% @doc Get stats of the connection/channel. +-spec(stats(pid()|state()) -> emqx_types:stats()). +stats(CPid) when is_pid(CPid) -> + call(CPid, stats); +stats(#state{transport = Transport, + socket = Socket, + channel = Channel}) -> + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + ConnStats = emqx_pd:get_counters(?CONN_STATS), + ChanStats = emqx_stomp_channel:stats(Channel), + ProcStats = emqx_misc:proc_stats(), + lists:append([SockStats, ConnStats, ChanStats, ProcStats]). + +%% @doc Set TCP keepalive socket options to override system defaults. +%% Idle: The number of seconds a connection needs to be idle before +%% TCP begins sending out keep-alive probes (Linux default 7200). +%% Interval: The number of seconds between TCP keep-alive probes +%% (Linux default 75). +%% Probes: The maximum number of TCP keep-alive probes to send before +%% giving up and killing the connection if no response is +%% obtained from the other end (Linux default 9). +%% +%% NOTE: This API sets TCP socket options, which has nothing to do with +%% the MQTT layer's keepalive (PINGREQ and PINGRESP). +async_set_keepalive(Idle, Interval, Probes) -> + async_set_keepalive(self(), Idle, Interval, Probes). + +async_set_keepalive(Pid, Idle, Interval, Probes) -> + Options = [ {keepalive, true} + , {raw, 6, 4, <>} + , {raw, 6, 5, <>} + , {raw, 6, 6, <>} + ], + async_set_socket_options(Pid, Options). + +%% @doc Set custom socket options. +%% This API is made async because the call might be originated from +%% a hookpoint callback (otherwise deadlock). +%% If failed to set, the error message is logged. +async_set_socket_options(Pid, Options) -> + cast(Pid, {async_set_socket_options, Options}). + +cast(Pid, Req) -> + gen_server:cast(Pid, Req). + +call(Pid, Req) -> + call(Pid, Req, infinity). +call(Pid, Req, Timeout) -> + gen_server:call(Pid, Req, Timeout). + +stop(Pid) -> + gen_server:stop(Pid). + +%%-------------------------------------------------------------------- +%% callbacks +%%-------------------------------------------------------------------- + +init(Parent, Transport, RawSocket, Options) -> + case Transport:wait(RawSocket) of + {ok, Socket} -> + run_loop(Parent, init_state(Transport, Socket, Options)); + {error, Reason} -> + ok = Transport:fast_close(RawSocket), + exit_on_sock_error(Reason) + end. + +init_state(Transport, Socket, Options) -> + {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), + {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), + Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), + ConnInfo = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, + peercert => Peercert, + conn_mod => ?MODULE + }, + ActiveN = emqx_gateway_utils:active_n(Options), + %% TODO: RateLimit ? How ? + Limiter = undefined, + %RateLimit = emqx_gateway_utils:ratelimit(Options), + %%Limiter = emqx_limiter:init(Zone, RateLimit), + FrameOpts = emqx_gateway_utils:frame_options(Options), + ParseState = emqx_stomp_frame:initial_parse_state(FrameOpts), + Serialize = emqx_stomp_frame:serialize_opts(), + Channel = emqx_stomp_channel:init(ConnInfo, Options), + GcState = emqx_gateway_utils:init_gc_state(Options), + StatsTimer = emqx_gateway_utils:stats_timer(Options), + IdleTimeout = emqx_gateway_utils:idle_timeout(Options), + IdleTimer = emqx_misc:start_timer(IdleTimeout, idle_timeout), + #state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + sockstate = idle, + active_n = ActiveN, + limiter = Limiter, + parse_state = ParseState, + serialize = Serialize, + channel = Channel, + gc_state = GcState, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + idle_timer = IdleTimer + }. + +run_loop(Parent, State = #state{transport = Transport, + socket = Socket, + peername = Peername, + channel = _Channel}) -> + emqx_logger:set_metadata_peername(esockd:format(Peername)), + % TODO: How yo get oom_policy ??? + %emqx_misc:tune_heap_size(emqx_gateway_utils:oom_policy( + % emqx_stomp_channel:info(zone, Channel))), + case activate_socket(State) of + {ok, NState} -> hibernate(Parent, NState); + {error, Reason} -> + ok = Transport:fast_close(Socket), + exit_on_sock_error(Reason) + end. + +-spec exit_on_sock_error(any()) -> no_return(). +exit_on_sock_error(Reason) when Reason =:= einval; + Reason =:= enotconn; + Reason =:= closed -> + erlang:exit(normal); +exit_on_sock_error(timeout) -> + erlang:exit({shutdown, ssl_upgrade_timeout}); +exit_on_sock_error(Reason) -> + erlang:exit({shutdown, Reason}). + +%%-------------------------------------------------------------------- +%% Recv Loop + +recvloop(Parent, State = #state{idle_timeout = IdleTimeout}) -> + receive + Msg -> + handle_recv(Msg, Parent, State) + after + IdleTimeout + 100 -> + hibernate(Parent, cancel_stats_timer(State)) + end. + +handle_recv({system, From, Request}, Parent, State) -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State); +handle_recv({'EXIT', Parent, Reason}, Parent, State) -> + %% FIXME: it's not trapping exit, should never receive an EXIT + terminate(Reason, State); +handle_recv(Msg, Parent, State = #state{idle_timeout = IdleTimeout}) -> + case process_msg([Msg], ensure_stats_timer(IdleTimeout, State)) of + {ok, NewState} -> + ?MODULE:recvloop(Parent, NewState); + {stop, Reason, NewSate} -> + terminate(Reason, NewSate) + end. + +hibernate(Parent, State) -> + proc_lib:hibernate(?MODULE, wakeup_from_hib, [Parent, State]). + +%% Maybe do something here later. +wakeup_from_hib(Parent, State) -> + ?MODULE:recvloop(Parent, State). + +%%-------------------------------------------------------------------- +%% Ensure/cancel stats timer + +ensure_stats_timer(Timeout, State = #state{stats_timer = undefined}) -> + State#state{stats_timer = emqx_misc:start_timer(Timeout, emit_stats)}; +ensure_stats_timer(_Timeout, State) -> State. + +cancel_stats_timer(State = #state{stats_timer = TRef}) + when is_reference(TRef) -> + ?tp(debug, cancel_stats_timer, #{}), + ok = emqx_misc:cancel_timer(TRef), + State#state{stats_timer = undefined}; +cancel_stats_timer(State) -> State. + +%%-------------------------------------------------------------------- +%% Process next Msg + +process_msg([], State) -> + {ok, State}; +process_msg([Msg|More], State) -> + try + case handle_msg(Msg, State) of + ok -> + process_msg(More, State); + {ok, NState} -> + process_msg(More, NState); + {ok, Msgs, NState} -> + process_msg(append_msg(More, Msgs), NState); + {stop, Reason, NState} -> + {stop, Reason, NState} + end + catch + exit : normal -> + {stop, normal, State}; + exit : shutdown -> + {stop, shutdown, State}; + exit : {shutdown, _} = Shutdown -> + {stop, Shutdown, State}; + Exception : Context : Stack -> + {stop, #{exception => Exception, + context => Context, + stacktrace => Stack}, State} + end. + +append_msg([], Msgs) when is_list(Msgs) -> + Msgs; +append_msg([], Msg) -> [Msg]; +append_msg(Q, Msgs) when is_list(Msgs) -> + lists:append(Q, Msgs); +append_msg(Q, Msg) -> + lists:append(Q, [Msg]). + +%%-------------------------------------------------------------------- +%% Handle a Msg + +handle_msg({'$gen_call', From, Req}, State) -> + case handle_call(From, Req, State) of + {reply, Reply, NState} -> + gen_server:reply(From, Reply), + {ok, NState}; + {stop, Reason, Reply, NState} -> + gen_server:reply(From, Reply), + stop(Reason, NState) + end; +handle_msg({'$gen_cast', Req}, State) -> + NewState = handle_cast(Req, State), + {ok, NewState}; + +handle_msg({Inet, _Sock, Data}, State = #state{channel = Channel}) + when Inet == tcp; + Inet == ssl -> + ?LOG(debug, "RECV ~0p", [Data]), + Oct = iolist_size(Data), + inc_counter(incoming_bytes, Oct), + Ctx = emqx_stomp_channel:info(ctx, Channel), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.received', Oct), + parse_incoming(Data, State); + +handle_msg({incoming, Packet}, State = #state{idle_timer = undefined}) -> + handle_incoming(Packet, State); + +handle_msg({incoming, Packet}, + State = #state{idle_timer = IdleTimer}) -> + ok = emqx_misc:cancel_timer(IdleTimer), + %% XXX: Serialize with inpunt packets + %%Serialize = emqx_stomp_frame:serialize_opts(), + NState = State#state{idle_timer = undefined}, + handle_incoming(Packet, NState); + +handle_msg({outgoing, Packets}, State) -> + handle_outgoing(Packets, State); + +handle_msg({Error, _Sock, Reason}, State) + when Error == tcp_error; Error == ssl_error -> + handle_info({sock_error, Reason}, State); + +handle_msg({Closed, _Sock}, State) + when Closed == tcp_closed; Closed == ssl_closed -> + handle_info({sock_closed, Closed}, close_socket(State)); + +handle_msg({Passive, _Sock}, State) + when Passive == tcp_passive; Passive == ssl_passive -> + %% In Stats + Pubs = emqx_pd:reset_counter(incoming_pubs), + Bytes = emqx_pd:reset_counter(incoming_bytes), + InStats = #{cnt => Pubs, oct => Bytes}, + %% Ensure Rate Limit + NState = ensure_rate_limit(InStats, State), + %% Run GC and Check OOM + NState1 = check_oom(run_gc(InStats, NState)), + handle_info(activate_socket, NState1); + +handle_msg(Deliver = {deliver, _Topic, _Msg}, + #state{active_n = ActiveN} = State) -> + Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], + with_channel(handle_deliver, [Delivers], State); + +%% Something sent +handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> + case emqx_pd:get_counter(outgoing_pubs) > ActiveN of + true -> + Pubs = emqx_pd:reset_counter(outgoing_pubs), + Bytes = emqx_pd:reset_counter(outgoing_bytes), + OutStats = #{cnt => Pubs, oct => Bytes}, + {ok, run_gc(OutStats, State)}; + %% FIXME: check oom ??? + %%{ok, check_oom(run_gc(OutStats, State))}; + false -> ok + end; + +handle_msg({inet_reply, _Sock, {error, Reason}}, State) -> + handle_info({sock_error, Reason}, State); + +handle_msg({connack, ConnAck}, State) -> + handle_outgoing(ConnAck, State); + +handle_msg({close, Reason}, State) -> + ?LOG(debug, "Force to close the socket due to ~p", [Reason]), + handle_info({sock_closed, Reason}, close_socket(State)); + +handle_msg({event, connected}, State = #state{channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:insert_channel_info( + Ctx, + ClientId, + info(State), + stats(State) + ); + +handle_msg({event, disconnected}, State = #state{channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)), + emqx_gateway_ctx:connection_closed(Ctx, ClientId), + {ok, State}; + +handle_msg({event, _Other}, State = #state{channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:set_chan_info(Ctx, ClientId, info(State)), + emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)), + {ok, State}; + +handle_msg({timeout, TRef, TMsg}, State) -> + handle_timeout(TRef, TMsg, State); + +handle_msg(Shutdown = {shutdown, _Reason}, State) -> + stop(Shutdown, State); + +handle_msg(Msg, State) -> + handle_info(Msg, State). + +%%-------------------------------------------------------------------- +%% Terminate + +-spec terminate(any(), state()) -> no_return(). +terminate(Reason, State = #state{channel = Channel, transport = _Transport, + socket = _Socket}) -> + try + Channel1 = emqx_stomp_channel:set_conn_state(disconnected, Channel), + %emqx_congestion:cancel_alarms(Socket, Transport, Channel1), + emqx_stomp_channel:terminate(Reason, Channel1), + close_socket_ok(State) + catch + E : C : S -> + ?tp(warning, unclean_terminate, #{exception => E, context => C, stacktrace => S}) + end, + ?tp(info, terminate, #{reason => Reason}), + maybe_raise_excption(Reason). + +%% close socket, discard new state, always return ok. +close_socket_ok(State) -> + _ = close_socket(State), + ok. + +%% tell truth about the original exception +maybe_raise_excption(#{exception := Exception, + context := Context, + stacktrace := Stacktrace + }) -> + erlang:raise(Exception, Context, Stacktrace); +maybe_raise_excption(Reason) -> + exit(Reason). + +%%-------------------------------------------------------------------- +%% Sys callbacks + +system_continue(Parent, _Debug, State) -> + ?MODULE:recvloop(Parent, State). + +system_terminate(Reason, _Parent, _Debug, State) -> + terminate(Reason, State). + +system_code_change(State, _Mod, _OldVsn, _Extra) -> + {ok, State}. + +system_get_state(State) -> {ok, State}. + +%%-------------------------------------------------------------------- +%% Handle call + +handle_call(_From, info, State) -> + {reply, info(State), State}; + +handle_call(_From, stats, State) -> + {reply, stats(State), State}; + +%% TODO: How to set ratelimit ??? +%%handle_call(_From, {ratelimit, Policy}, State = #state{channel = Channel}) -> +%% Zone = emqx_stomp_channel:info(zone, Channel), +%% Limiter = emqx_limiter:init(Zone, Policy), +%% {reply, ok, State#state{limiter = Limiter}}; + +handle_call(_From, Req, State = #state{channel = Channel}) -> + case emqx_stomp_channel:handle_call(Req, Channel) of + {reply, Reply, NChannel} -> + {reply, Reply, State#state{channel = NChannel}}; + {shutdown, Reason, Reply, NChannel} -> + shutdown(Reason, Reply, State#state{channel = NChannel}); + {shutdown, Reason, Reply, OutPacket, NChannel} -> + NState = State#state{channel = NChannel}, + ok = handle_outgoing(OutPacket, NState), + shutdown(Reason, Reply, NState) + end. + +%%-------------------------------------------------------------------- +%% Handle timeout + +handle_timeout(_TRef, idle_timeout, State) -> + shutdown(idle_timeout, State); + +handle_timeout(_TRef, limit_timeout, State) -> + NState = State#state{sockstate = idle, + limit_timer = undefined + }, + handle_info(activate_socket, NState); + +handle_timeout(_TRef, emit_stats, State = #state{channel = Channel, + transport = _Transport, + socket = _Socket}) -> + %emqx_congestion:maybe_alarm_conn_congestion(Socket, Transport, Channel), + Ctx = emqx_stomp_channel:info(ctx, Channel), + ClientId = emqx_stomp_channel:info(clientid, Channel), + emqx_gateway_ctx:set_chan_stats(Ctx, ClientId, stats(State)), + {ok, State#state{stats_timer = undefined}}; + +%% Abstraction ??? +%handle_timeout(TRef, keepalive, State = #state{transport = Transport, +% socket = Socket, +% channel = Channel})-> +% case emqx_stomp_channel:info(conn_state, Channel) of +% disconnected -> {ok, State}; +% _ -> +% case Transport:getstat(Socket, [recv_oct]) of +% {ok, [{recv_oct, RecvOct}]} -> +% handle_timeout(TRef, {keepalive, RecvOct}, State); +% {error, Reason} -> +% handle_info({sock_error, Reason}, State) +% end +% end; + +handle_timeout(TRef, TMsg, State = #state{transport = Transport, + socket = Socket, + channel = Channel + }) + when TMsg =:= incoming; + TMsg =:= outgoing -> + Stat = case TMsg of incoming -> recv_oct; _ -> send_oct end, + case emqx_stomp_channel:info(conn_state, Channel) of + disconnected -> {ok, State}; + _ -> + case Transport:getstat(Socket, [Stat]) of + {ok, [{recv_oct, RecvOct}]} -> + handle_timeout(TRef, {incoming, RecvOct}, State); + {ok, [{send_oct, SendOct}]} -> + handle_timeout(TRef, {outgoing, SendOct}, State); + {error, Reason} -> + handle_info({sock_error, Reason}, State) + end + end; + +handle_timeout(TRef, Msg, State) -> + with_channel(handle_timeout, [TRef, Msg], State). + +%%-------------------------------------------------------------------- +%% Parse incoming data + +parse_incoming(Data, State) -> + {Packets, NState} = parse_incoming(Data, [], State), + {ok, next_incoming_msgs(Packets), NState}. + +parse_incoming(<<>>, Packets, State) -> + {Packets, State}; + +parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> + try emqx_stomp_frame:parse(Data, ParseState) of + {more, NParseState} -> + {Packets, State#state{parse_state = NParseState}}; + {ok, Packet, Rest, NParseState} -> + NState = State#state{parse_state = NParseState}, + parse_incoming(Rest, [Packet|Packets], NState) + catch + error:Reason:Stk -> + ?LOG(error, "~nParse failed for ~0p~n~0p~nFrame data:~0p", + [Reason, Stk, Data]), + {[{frame_error, Reason}|Packets], State} + end. + +next_incoming_msgs([Packet]) -> + {incoming, Packet}; +next_incoming_msgs(Packets) -> + [{incoming, Packet} || Packet <- lists:reverse(Packets)]. + +%%-------------------------------------------------------------------- +%% Handle incoming packet + +handle_incoming(Packet, State) when is_record(Packet, stomp_frame) -> + ok = inc_incoming_stats(Packet), + ?LOG(debug, "RECV ~s", [emqx_stomp_frame:format(Packet)]), + with_channel(handle_in, [Packet], State); + +handle_incoming(FrameError, State) -> + with_channel(handle_in, [FrameError], State). + +%%-------------------------------------------------------------------- +%% With Channel + +with_channel(Fun, Args, State = #state{channel = Channel}) -> + case erlang:apply(emqx_stomp_channel, Fun, Args ++ [Channel]) of + ok -> {ok, State}; + {ok, NChannel} -> + {ok, State#state{channel = NChannel}}; + {ok, Replies, NChannel} -> + {ok, next_msgs(Replies), State#state{channel = NChannel}}; + {shutdown, Reason, NChannel} -> + shutdown(Reason, State#state{channel = NChannel}); + {shutdown, Reason, Packet, NChannel} -> + NState = State#state{channel = NChannel}, + ok = handle_outgoing(Packet, NState), + shutdown(Reason, NState) + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packets + +handle_outgoing(Packets, State) when is_list(Packets) -> + send(lists:map(serialize_and_inc_stats_fun(State), Packets), State); + +handle_outgoing(Packet, State) -> + send((serialize_and_inc_stats_fun(State))(Packet), State). + +serialize_and_inc_stats_fun(#state{serialize = Serialize, channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + fun(Packet) -> + case emqx_stomp_frame:serialize_pkt(Packet, Serialize) of + <<>> -> ?LOG(warning, "~s is discarded due to the frame is too large!", + [emqx_stomp_frame:format(Packet)]), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped.too_large'), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'delivery.dropped'), + <<>>; + Data -> ?LOG(debug, "SEND ~s", [emqx_stomp_frame:format(Packet)]), + ok = inc_outgoing_stats(Packet), + Data + end + end. + +%%-------------------------------------------------------------------- +%% Send data + +-spec(send(iodata(), state()) -> ok). +send(IoData, #state{transport = Transport, socket = Socket, channel = Channel}) -> + Ctx = emqx_stomp_channel:info(ctx, Channel), + Oct = iolist_size(IoData), + ok = emqx_gateway_ctx:metrics_inc(Ctx, 'bytes.sent', Oct), + inc_counter(outgoing_bytes, Oct), + %emqx_congestion:maybe_alarm_conn_congestion(Socket, Transport, Channel), + case Transport:async_send(Socket, IoData, [nosuspend]) of + ok -> ok; + Error = {error, _Reason} -> + %% Send an inet_reply to postpone handling the error + self() ! {inet_reply, Socket, Error}, + ok + end. + +%%-------------------------------------------------------------------- +%% Handle Info + +handle_info(activate_socket, State = #state{sockstate = OldSst}) -> + case activate_socket(State) of + {ok, NState = #state{sockstate = NewSst}} -> + case OldSst =/= NewSst of + true -> {ok, {event, NewSst}, NState}; + false -> {ok, NState} + end; + {error, Reason} -> + handle_info({sock_error, Reason}, State) + end; + +handle_info({sock_error, Reason}, State) -> + case Reason =/= closed andalso Reason =/= einval of + true -> ?LOG(warning, "socket_error: ~p", [Reason]); + false -> ok + end, + handle_info({sock_closed, Reason}, close_socket(State)); + +handle_info(Info, State) -> + with_channel(handle_info, [Info], State). + +%%-------------------------------------------------------------------- +%% Handle Info + +handle_cast({async_set_socket_options, Opts}, + State = #state{transport = Transport, + socket = Socket + }) -> + case Transport:setopts(Socket, Opts) of + ok -> ?tp(info, "custom_socket_options_successfully", #{opts => Opts}); + Err -> ?tp(error, "failed_to_set_custom_socket_optionn", #{reason => Err}) + end, + State; +handle_cast(Req, State) -> + ?tp(error, "received_unknown_cast", #{cast => Req}), + State. + +%%-------------------------------------------------------------------- +%% Ensure rate limit + +ensure_rate_limit(Stats, State = #state{limiter = Limiter}) -> + case ?ENABLED(Limiter) andalso emqx_limiter:check(Stats, Limiter) of + false -> State; + {ok, Limiter1} -> + State#state{limiter = Limiter1}; + {pause, Time, Limiter1} -> + ?LOG(warning, "Pause ~pms due to rate limit", [Time]), + TRef = emqx_misc:start_timer(Time, limit_timeout), + State#state{sockstate = blocked, + limiter = Limiter1, + limit_timer = TRef + } + end. + +%%-------------------------------------------------------------------- +%% Run GC and Check OOM + +run_gc(Stats, State = #state{gc_state = GcSt}) -> + case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of + false -> State; + {_IsGC, GcSt1} -> + State#state{gc_state = GcSt1} + end. + +check_oom(State = #state{channel = Channel}) -> + Zone = emqx_stomp_channel:info(zone, Channel), + OomPolicy = emqx_gateway_utils:oom_policy(Zone), + ?tp(debug, check_oom, #{policy => OomPolicy}), + case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of + {shutdown, Reason} -> + %% triggers terminate/2 callback immediately + erlang:exit({shutdown, Reason}); + _Other -> + ok + end, + State. + +%%-------------------------------------------------------------------- +%% Activate Socket + +-compile({inline, [activate_socket/1]}). +activate_socket(State = #state{sockstate = closed}) -> + {ok, State}; +activate_socket(State = #state{sockstate = blocked}) -> + {ok, State}; +activate_socket(State = #state{transport = Transport, + socket = Socket, + active_n = N}) -> + case Transport:setopts(Socket, [{active, N}]) of + ok -> {ok, State#state{sockstate = running}}; + Error -> Error + end. + +%%-------------------------------------------------------------------- +%% Close Socket + +close_socket(State = #state{sockstate = closed}) -> State; +close_socket(State = #state{transport = Transport, socket = Socket}) -> + ok = Transport:fast_close(Socket), + State#state{sockstate = closed}. + +%%-------------------------------------------------------------------- +%% Inc incoming/outgoing stats + +%% XXX: Other packet type? +inc_incoming_stats(Packet = ?PACKET(Type)) -> + inc_counter(recv_pkt, 1), + case Type =:= ?CMD_SEND of + true -> + inc_counter(recv_msg, 1), + inc_counter(incoming_pubs, 1); + false -> + ok + end, + emqx_metrics:inc_recv(Packet). + +inc_outgoing_stats(Packet = ?PACKET(Type)) -> + inc_counter(send_pkt, 1), + case Type =:= ?CMD_MESSAGE of + true -> + inc_counter(send_msg, 1), + inc_counter(outgoing_pubs, 1); + false -> + ok + end, + emqx_metrics:inc_sent(Packet). + +%%-------------------------------------------------------------------- +%% Helper functions + +next_msgs(Packet) when is_record(Packet, stomp_frame) -> + {outgoing, Packet}; +next_msgs(Event) when is_tuple(Event) -> + Event; +next_msgs(More) when is_list(More) -> + More. + +shutdown(Reason, State) -> + stop({shutdown, Reason}, State). + +shutdown(Reason, Reply, State) -> + stop({shutdown, Reason}, Reply, State). + +stop(Reason, State) -> + {stop, Reason, State}. + +stop(Reason, Reply, State) -> + {stop, Reason, Reply, State}. + +inc_counter(Key, Inc) -> + _ = emqx_pd:inc_counter(Key, Inc), + ok. + +%%-------------------------------------------------------------------- +%% For CT tests +%%-------------------------------------------------------------------- + +set_field(Name, Value, State) -> + Pos = emqx_misc:index_of(Name, record_info(fields, state)), + setelement(Pos+1, State, Value). + +get_state(Pid) -> + State = sys:get_state(Pid), + maps:from_list(lists:zip(record_info(fields, state), + tl(tuple_to_list(State)))). diff --git a/apps/emqx_stomp/src/emqx_stomp_frame.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_frame.erl similarity index 70% rename from apps/emqx_stomp/src/emqx_stomp_frame.erl rename to apps/emqx_gateway/src/stomp/emqx_stomp_frame.erl index fa9cb63a8..4db8a1f5f 100644 --- a/apps/emqx_stomp/src/emqx_stomp_frame.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_frame.erl @@ -68,14 +68,16 @@ -module(emqx_stomp_frame). --include("emqx_stomp.hrl"). +-include("src/stomp/include/emqx_stomp.hrl"). --export([ init_parer_state/1 +-export([ initial_parse_state/1 , parse/2 - , serialize/1 + , serialize_opts/0 + , serialize_pkt/2 ]). --export([ make/2 +-export([ make/1 + , make/2 , make/3 , format/1 ]). @@ -96,28 +98,33 @@ -record(frame_limit, {max_header_num, max_header_length, max_body_length}). --type(result() :: {ok, stomp_frame(), binary()} - | {more, parser()} - | {error, any()}). +-type(parse_result() :: {ok, stomp_frame(), binary()} + | {more, parse_state()}). --type(parser() :: #{phase := none | command | headers | hdname | hdvalue | body, - pre => binary(), - state := #parser_state{}}). +-type(parse_state() :: + #{phase := none | command | headers | hdname | hdvalue | body, + pre => binary(), + state := #parser_state{} + }). + +-dialyzer({nowarn_function, [serialize_pkt/2,make/1]}). %% @doc Initialize a parser --spec init_parer_state([proplists:property()]) -> parser(). -init_parer_state(Opts) -> +-spec initial_parse_state(map()) -> parse_state(). +initial_parse_state(Opts) -> #{phase => none, state => #parser_state{limit = limit(Opts)}}. limit(Opts) -> - #frame_limit{max_header_num = g(max_header_num, Opts, ?MAX_HEADER_NUM), - max_header_length = g(max_header_length, Opts, ?MAX_BODY_LENGTH), - max_body_length = g(max_body_length, Opts, ?MAX_BODY_LENGTH)}. + #frame_limit{ + max_header_num = g(max_header_num, Opts, ?MAX_HEADER_NUM), + max_header_length = g(max_header_length, Opts, ?MAX_BODY_LENGTH), + max_body_length = g(max_body_length, Opts, ?MAX_BODY_LENGTH) + }. g(Key, Opts, Val) -> - proplists:get_value(Key, Opts, Val). + maps:get(Key, Opts, Val). --spec parse(binary(), parser()) -> result(). +-spec parse(binary(), parse_state()) -> parse_result(). parse(<<>>, Parser) -> {more, Parser}; @@ -131,11 +138,14 @@ parse(<>, #{phase := Phase, state := State}) -> parse(<>, Parser) -> {more, Parser#{pre => <>}}; parse(<>, _Parser) -> - {error, linefeed_expected}; + error(linefeed_expected); -parse(<>, Parser = #{phase := Phase}) when Phase =:= hdname; Phase =:= hdvalue -> +parse(<>, Parser = #{phase := Phase}) when Phase =:= hdname; + Phase =:= hdvalue -> {more, Parser#{pre => <>}}; -parse(<>, #{phase := Phase, state := State}) when Phase =:= hdname; Phase =:= hdvalue -> +parse(<>, + #{phase := Phase, state := State}) when Phase =:= hdname; + Phase =:= hdvalue -> parse(Phase, Rest, acc(unescape(Ch), State)); parse(Bytes, #{phase := none, state := State}) -> @@ -153,14 +163,19 @@ parse(headers, Bin, State) -> parse(hdname, Bin, State); parse(hdname, <>, _State) -> - {error, unexpected_linefeed}; + error(unexpected_linefeed); parse(hdname, <>, State = #parser_state{acc = Acc}) -> parse(hdvalue, Rest, State#parser_state{hdname = Acc, acc = <<>>}); parse(hdname, <>, State) -> parse(hdname, Rest, acc(Ch, State)); -parse(hdvalue, <>, State = #parser_state{headers = Headers, hdname = Name, acc = Acc}) -> - parse(headers, Rest, State#parser_state{headers = add_header(Name, Acc, Headers), hdname = undefined, acc = <<>>}); +parse(hdvalue, <>, + State = #parser_state{headers = Headers, hdname = Name, acc = Acc}) -> + NState = State#parser_state{headers = add_header(Name, Acc, Headers), + hdname = undefined, + acc = <<>> + }, + parse(headers, Rest, NState); parse(hdvalue, <>, State) -> parse(hdvalue, Rest, acc(Ch, State)). @@ -170,15 +185,19 @@ parse(body, <<>>, State, Length) -> parse(body, Bin, State, none) -> case binary:split(Bin, <>) of [Chunk, Rest] -> - {ok, new_frame(acc(Chunk, State)), Rest}; + {ok, new_frame(acc(Chunk, State)), Rest, new_state(State)}; [Chunk] -> - {more, #{phase => body, length => none, state => acc(Chunk, State)}} + {more, #{phase => body, + length => none, + state => acc(Chunk, State)}} end; parse(body, Bin, State, Len) when byte_size(Bin) >= (Len+1) -> <> = Bin, - {ok, new_frame(acc(Chunk, State)), Rest}; + {ok, new_frame(acc(Chunk, State)), Rest, new_state(State)}; parse(body, Bin, State, Len) -> - {more, #{phase => body, length => Len - byte_size(Bin), state => acc(Bin, State)}}. + {more, #{phase => body, + length => Len - byte_size(Bin), + state => acc(Bin, State)}}. add_header(Name, Value, Headers) -> case lists:keyfind(Name, 1, Headers) of @@ -208,20 +227,33 @@ unescape($r) -> ?CR; unescape($n) -> ?LF; unescape($c) -> ?COLON; unescape($\\) -> ?BSL; -unescape(_Ch) -> {error, cannnot_unescape}. +unescape(_Ch) -> error(cannnot_unescape). -serialize(#stomp_frame{command = Cmd, headers = Headers, body = Body}) -> +%%-------------------------------------------------------------------- +%% Serialize funcs +%%-------------------------------------------------------------------- + +serialize_opts() -> + #{}. + +serialize_pkt(#stomp_frame{command = heartbeat}, _SerializeOpts) -> + <<$\n>>; + +serialize_pkt(#stomp_frame{command = Cmd, headers = Headers, body = Body}, + _SerializeOpts) -> Headers1 = lists:keydelete(<<"content-length">>, 1, Headers), Headers2 = case iolist_size(Body) of 0 -> Headers1; Len -> Headers1 ++ [{<<"content-length">>, Len}] end, - [Cmd, ?LF, [serialize(header, Header) || Header <- Headers2], ?LF, Body, 0]. + [Cmd, + ?LF, [serialize_pkt(header, Header) || Header <- Headers2], + ?LF, Body, 0]; -serialize(header, {Name, Val}) when is_integer(Val) -> +serialize_pkt(header, {Name, Val}) when is_integer(Val) -> [escape(Name), ?COLON, integer_to_list(Val), ?LF]; -serialize(header, {Name, Val}) -> +serialize_pkt(header, {Name, Val}) -> [escape(Name), ?COLON, escape(Val), ?LF]. escape(Bin) when is_binary(Bin) -> @@ -232,8 +264,18 @@ escape(?BSL) -> <>; escape(?COLON) -> <>; escape(Ch) -> <>. +new_state(#parser_state{limit = Limit}) -> + #{phase => none, state => #parser_state{limit = Limit}}. + +%%-------------------------------------------------------------------- +%% ??? +%%-------------------------------------------------------------------- %% @doc Make a frame + +make(heartbeat) -> + #stomp_frame{command = heartbeat}. + make(<<"CONNECTED">>, Headers) -> #stomp_frame{command = <<"CONNECTED">>, headers = [{<<"server">>, ?STOMP_SERVER} | Headers]}; @@ -245,5 +287,4 @@ make(Command, Headers, Body) -> #stomp_frame{command = Command, headers = Headers, body = Body}. %% @doc Format a frame -format(Frame) -> serialize(Frame). - +format(Frame) -> serialize_pkt(Frame, #{}). diff --git a/apps/emqx_stomp/src/emqx_stomp_heartbeat.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_heartbeat.erl similarity index 89% rename from apps/emqx_stomp/src/emqx_stomp_heartbeat.erl rename to apps/emqx_gateway/src/stomp/emqx_stomp_heartbeat.erl index 145359e53..99a1508e1 100644 --- a/apps/emqx_stomp/src/emqx_stomp_heartbeat.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_heartbeat.erl @@ -17,10 +17,11 @@ %% @doc Stomp heartbeat. -module(emqx_stomp_heartbeat). --include("emqx_stomp.hrl"). +-include("src/stomp/include/emqx_stomp.hrl"). -export([ init/1 , check/3 + , reset/3 , info/1 , interval/2 ]). @@ -33,7 +34,6 @@ outgoing => #heartbeater{} }. - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -77,6 +77,15 @@ check(NewVal, HrtBter = #heartbeater{statval = OldVal, true -> {error, timeout} end. +-spec reset(name(), pos_integer(), heartbeat()) + -> heartbeat(). +reset(Name, NewVal, HrtBt) -> + HrtBter = maps:get(Name, HrtBt), + HrtBt#{Name => reset(NewVal, HrtBter)}. + +reset(NewVal, HrtBter) -> + HrtBter#heartbeater{statval = NewVal, repeat = 1}. + -spec info(heartbeat()) -> map(). info(HrtBt) -> maps:map(fun(_, #heartbeater{interval = Intv, diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl new file mode 100644 index 000000000..e6e62565a --- /dev/null +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -0,0 +1,153 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_stomp_impl). + +-include_lib("emqx_gateway/include/emqx_gateway.hrl"). + +-behavior(emqx_gateway_impl). + +%% APIs +-export([ load/0 + , unload/0 + ]). + +-export([ init/1 + , on_insta_create/3 + , on_insta_update/4 + , on_insta_destroy/3 + ]). + +-define(TCP_OPTS, [binary, {packet, raw}, {reuseaddr, true}, {nodelay, true}]). + +-dialyzer({nowarn_function, [load/0]}). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +load() -> + RegistryOptions = [ {cbkmod, ?MODULE} + , {schema, emqx_stomp_schema} + ], + + YourOptions = [param1, param2], + emqx_gateway_registry:load(stomp, RegistryOptions, YourOptions). + +unload() -> + emqx_gateway_registry:unload(stomp). + +init([param1, param2]) -> + GwState = #{}, + {ok, GwState}. + +%%-------------------------------------------------------------------- +%% emqx_gateway_registry callbacks +%%-------------------------------------------------------------------- + +on_insta_create(_Insta = #{ id := InstaId, + rawconf := RawConf + }, Ctx, _GwState) -> + %% Step1. Fold the rawconfs to listeners + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + %% Step2. Start listeners or escokd:specs + ListenerPids = lists:map(fun(Lis) -> + start_listener(InstaId, Ctx, Lis) + end, Listeners), + %% FIXME: How to throw an exception to interrupt the restart logic ? + %% FIXME: Assign ctx to InstaState + {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + +%% @private +on_insta_update(NewInsta, OldInstace, GwInstaState = #{ctx := Ctx}, GwState) -> + InstaId = maps:get(id, NewInsta), + try + %% XXX: 1. How hot-upgrade the changes ??? + %% XXX: 2. Check the New confs first before destroy old instance ??? + on_insta_destroy(OldInstace, GwInstaState, GwState), + on_insta_create(NewInsta, Ctx, GwState) + catch + Class : Reason : Stk -> + logger:error("Failed to update stomp instance ~s; " + "reason: {~0p, ~0p} stacktrace: ~0p", + [InstaId, Class, Reason, Stk]), + {error, {Class, Reason}} + end. + +on_insta_destroy(_Insta = #{ id := InstaId, + rawconf := RawConf + }, _GwInstaState, _GwState) -> + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + lists:foreach(fun(Lis) -> + stop_listener(InstaId, Lis) + end, Listeners). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> + case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + {ok, Pid} -> + io:format("Start stomp ~s:~s listener on ~s successfully.~n", + [InstaId, Type, format(ListenOn)]), + Pid; + {error, Reason} -> + io:format(standard_error, + "Failed to start stomp ~s:~s listener on ~s: ~0p~n", + [InstaId, Type, format(ListenOn), Reason]), + throw({badconf, Reason}) + end. + +start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(InstaId, Type), + esockd:open(Name, ListenOn, merge_default(SocketOpts), + {emqx_stomp_connection, start_link, [Cfg#{ctx => Ctx}]}). + +name(InstaId, Type) -> + list_to_atom(lists:concat([InstaId, ":", Type])). + +merge_default(Options) -> + case lists:keytake(tcp_options, 1, Options) of + {value, {tcp_options, TcpOpts}, Options1} -> + [{tcp_options, emqx_misc:merge_opts(?TCP_OPTS, TcpOpts)} | Options1]; + false -> + [{tcp_options, ?TCP_OPTS} | Options] + end. + +format(Port) when is_integer(Port) -> + io_lib:format("0.0.0.0:~w", [Port]); +format({Addr, Port}) when is_list(Addr) -> + io_lib:format("~s:~w", [Addr, Port]); +format({Addr, Port}) when is_tuple(Addr) -> + io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). + +stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), + case StopRet of + ok -> io:format("Stop stomp ~s:~s listener on ~s successfully.~n", + [InstaId, Type, format(ListenOn)]); + {error, Reason} -> + io:format(standard_error, + "Failed to stop stomp ~s:~s listener on ~s: ~0p~n", + [InstaId, Type, format(ListenOn), Reason] + ) + end, + StopRet. + +stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(InstaId, Type), + esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl b/apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl new file mode 100644 index 000000000..cffcb1bdf --- /dev/null +++ b/apps/emqx_gateway/src/stomp/include/emqx_stomp.hrl @@ -0,0 +1,92 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 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. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_STOMP_HRL). +-define(EMQX_STOMP_HRL, true). + +-define(STOMP_VER, <<"1.2">>). + +-define(STOMP_SERVER, <<"emqx-stomp/1.2">>). + +%%-------------------------------------------------------------------- +%% STOMP Frame +%%-------------------------------------------------------------------- + +%% client command +-define(CMD_STOMP, <<"STOMP">>). +-define(CMD_CONNECT, <<"CONNECT">>). +-define(CMD_SEND, <<"SEND">>). +-define(CMD_SUBSCRIBE, <<"SUBSCRIBE">>). +-define(CMD_UNSUBSCRIBE, <<"UNSUBSCRIBE">>). +-define(CMD_BEGIN, <<"BEGIN">>). +-define(CMD_COMMIT, <<"COMMIT">>). +-define(CMD_ABORT, <<"ABORT">>). +-define(CMD_ACK, <<"ACK">>). +-define(CMD_NACK, <<"NACK">>). +-define(CMD_DISCONNECT, <<"DISCONNECT">>). + +%% server command +-define(CMD_CONNECTED, <<"CONNECTED">>). +-define(CMD_MESSAGE, <<"MESSAGE">>). +-define(CMD_RECEIPT, <<"RECEIPT">>). +-define(CMD_ERROR, <<"ERROR">>). + +-type client_command() :: binary(). +%-type client_command() :: ?CMD_SEND | ?CMD_SUBSCRIBE | ?CMD_UNSUBSCRIBE +% | ?CMD_BEGIN | ?CMD_COMMIT | ?CMD_ABORT | ?CMD_ACK +% | ?CMD_NACK | ?CMD_DISCONNECT | ?CMD_CONNECT +% | ?CMD_STOMP. +% +-type server_command() :: binary(). +%-type server_command() :: ?CMD_CONNECTED | ?CMD_MESSAGE | ?CMD_RECEIPT +% | ?CMD_ERROR. + +-record(stomp_frame, { + command :: client_command() | server_command(), + headers = [], + body = <<>> :: iodata()} + ). + +-type stomp_frame() :: #stomp_frame{}. + +-define(PACKET(CMD), #stomp_frame{command = CMD}). + +-define(PACKET(CMD, Headers), #stomp_frame{command = CMD, headers = Headers}). + +-define(PACKET(CMD, Headers, Body), #stomp_frame{command = CMD, + headers = Headers, + body = Body + }). + +%%-------------------------------------------------------------------- +%% Frame Size Limits +%% +%% To prevent malicious clients from exploiting memory allocation in a server, +%% servers MAY place maximum limits on: +%% +%% the number of frame headers allowed in a single frame +%% the maximum length of header lines +%% the maximum size of a frame body +%% +%% If these limits are exceeded the server SHOULD send the client an ERROR frame +%% and then close the connection. +%%-------------------------------------------------------------------- + +-define(MAX_HEADER_NUM, 10). +-define(MAX_HEADER_LENGTH, 1024). +-define(MAX_BODY_LENGTH, 65536). + +-endif. diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl new file mode 100644 index 000000000..cfc9399bb --- /dev/null +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -0,0 +1,87 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_gateway_registry_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). +-compile(nowarn_export_all). + +all() -> emqx_ct:all(?MODULE). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +init_per_suite(Cfg) -> + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_configs/1), + Cfg. + +end_per_suite(_Cfg) -> + emqx_ct_helpers:stop_apps([emqx_gateway]), + ok. + +set_special_configs(emqx_gateway) -> + emqx_config:put( + [emqx_gateway], + #{stomp => + #{'1' => + #{authenticator => allow_anonymous, + clientinfo_override => + #{password => "${Packet.headers.passcode}", + username => "${Packet.headers.login}"}, + frame => + #{max_body_length => 8192, + max_headers => 10, + max_headers_length => 1024}, + listener => + #{tcp => + #{'1' => + #{acceptors => 16,active_n => 100,backlog => 1024, + bind => 61613,high_watermark => 1048576, + max_conn_rate => 1000,max_connections => 1024000, + send_timeout => 15000,send_timeout_close => true}}}}}}), + ok; +set_special_configs(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_load_unload(_) -> + OldCnt = length(emqx_gateway_registry:list()), + RgOpts = [{cbkmod, ?MODULE}], + GwOpts = [paramsin], + ok = emqx_gateway_registry:load(test, RgOpts, GwOpts), + ?assertEqual(OldCnt+1, length(emqx_gateway_registry:list())), + + #{cbkmod := ?MODULE, + rgopts := RgOpts, + gwopts := GwOpts, + state := #{gwstate := 1}} = emqx_gateway_registry:lookup(test), + + {error, already_existed} = emqx_gateway_registry:load(test, [{cbkmod, ?MODULE}], GwOpts), + + ok = emqx_gateway_registry:unload(test), + undefined = emqx_gateway_registry:lookup(test), + OldCnt = length(emqx_gateway_registry:list()), + ok. + +init([paramsin]) -> + {ok, _GwState = #{gwstate => 1}}. + diff --git a/apps/emqx_stomp/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl similarity index 82% rename from apps/emqx_stomp/test/emqx_stomp_SUITE.erl rename to apps/emqx_gateway/test/emqx_stomp_SUITE.erl index 9a5d9698e..cc2b0db54 100644 --- a/apps/emqx_stomp/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -16,7 +16,7 @@ -module(emqx_stomp_SUITE). --include_lib("emqx_stomp/include/emqx_stomp.hrl"). +-include_lib("emqx_gateway/src/stomp/include/emqx_stomp.hrl"). -compile(export_all). -compile(nowarn_export_all). @@ -29,12 +29,37 @@ all() -> emqx_ct:all(?MODULE). %% Setups %%-------------------------------------------------------------------- -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_stomp]), - Config. +init_per_suite(Cfg) -> + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_configs/1), + Cfg. -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_stomp]). +end_per_suite(_Cfg) -> + emqx_ct_helpers:stop_apps([emqx_gateway]), + ok. + +set_special_configs(emqx_gateway) -> + emqx_config:put( + [emqx_gateway], + #{stomp => + #{'1' => + #{authenticator => allow_anonymous, + clientinfo_override => + #{password => "${Packet.headers.passcode}", + username => "${Packet.headers.login}"}, + frame => + #{max_body_length => 8192, + max_headers => 10, + max_headers_length => 1024}, + listener => + #{tcp => + #{'1' => + #{acceptors => 16,active_n => 100,backlog => 1024, + bind => 61613,high_watermark => 1048576, + max_conn_rate => 1000,max_connections => 1024000, + send_timeout => 15000,send_timeout_close => true}}}}}}), + ok; +set_special_configs(_) -> + ok. %%-------------------------------------------------------------------- %% Test Cases @@ -52,7 +77,7 @@ t_connect(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, Frame = #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), <<"2000,1000">> = proplists:get_value(<<"heart-beat">>, Frame#stomp_frame.headers), gen_tcp:send(Sock, serialize(<<"DISCONNECT">>, @@ -61,22 +86,23 @@ t_connect(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data1) + body = _}, _, _} = parse(Data1) end), %% Connect will be failed, because of bad login or passcode - with_connection(fun(Sock) -> - gen_tcp:send(Sock, serialize(<<"CONNECT">>, - [{<<"accept-version">>, ?STOMP_VER}, - {<<"host">>, <<"127.0.0.1:61613">>}, - {<<"login">>, <<"admin">>}, - {<<"passcode">>, <<"admin">>}, - {<<"heart-beat">>, <<"1000,2000">>}])), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, #stomp_frame{command = <<"ERROR">>, - headers = _, - body = <<"Login or passcode error!">>}, _} = parse(Data) - end), + %% FIXME: Waiting for authentication works + %with_connection(fun(Sock) -> + % gen_tcp:send(Sock, serialize(<<"CONNECT">>, + % [{<<"accept-version">>, ?STOMP_VER}, + % {<<"host">>, <<"127.0.0.1:61613">>}, + % {<<"login">>, <<"admin">>}, + % {<<"passcode">>, <<"admin">>}, + % {<<"heart-beat">>, <<"1000,2000">>}])), + % {ok, Data} = gen_tcp:recv(Sock, 0), + % {ok, #stomp_frame{command = <<"ERROR">>, + % headers = _, + % body = <<"Login or passcode error!">>}, _, _} = parse(Data) + % end), %% Connect will be failed, because of bad version with_connection(fun(Sock) -> @@ -89,7 +115,7 @@ t_connect(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"ERROR">>, headers = _, - body = <<"Supported protocol versions < 1.2">>}, _} = parse(Data) + body = <<"Login Failed: Supported protocol versions < 1.2">>}, _, _} = parse(Data) end). t_heartbeat(_) -> @@ -104,7 +130,7 @@ t_heartbeat(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), {ok, ?HEARTBEAT} = gen_tcp:recv(Sock, 0), %% Server will close the connection because never receive the heart beat from client @@ -122,7 +148,7 @@ t_subscribe(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), %% Subscribe gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>, @@ -139,7 +165,7 @@ t_subscribe(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0, 1000), {ok, Frame = #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"hello">>}, _} = parse(Data1), + body = <<"hello">>}, _, _} = parse(Data1), lists:foreach(fun({Key, Val}) -> Val = proplists:get_value(Key, Frame#stomp_frame.headers) end, [{<<"destination">>, <<"/queue/foo">>}, @@ -155,7 +181,7 @@ t_subscribe(_) -> {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data2), + body = _}, _, _} = parse(Data2), gen_tcp:send(Sock, serialize(<<"SEND">>, [{<<"destination">>, <<"/queue/foo">>}], @@ -175,7 +201,7 @@ t_transaction(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), %% Subscribe gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>, @@ -208,12 +234,12 @@ t_transaction(_) -> {ok, #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"hello">>}, Rest1} = parse(Data1), + body = <<"hello">>}, Rest1, _} = parse(Data1), %{ok, Data2} = gen_tcp:recv(Sock, 0, 500), {ok, #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"hello again">>}, _Rest2} = parse(Rest1), + body = <<"hello again">>}, _Rest2, _} = parse(Rest1), %% Transaction: tx2 gen_tcp:send(Sock, serialize(<<"BEGIN">>, @@ -236,7 +262,7 @@ t_transaction(_) -> {ok, Data3} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data3) + body = _}, _, _} = parse(Data3) end). t_receipt_in_error(_) -> @@ -250,7 +276,7 @@ t_receipt_in_error(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), gen_tcp:send(Sock, serialize(<<"ABORT">>, [{<<"transaction">>, <<"tx1">>}, @@ -259,7 +285,7 @@ t_receipt_in_error(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0), {ok, Frame = #stomp_frame{command = <<"ERROR">>, headers = _, - body = <<"Transaction tx1 not found">>}, _} = parse(Data1), + body = <<"Transaction tx1 not found">>}, _, _} = parse(Data1), <<"12345">> = proplists:get_value(<<"receipt-id">>, Frame#stomp_frame.headers) end). @@ -275,7 +301,7 @@ t_ack(_) -> {ok, Data} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"CONNECTED">>, headers = _, - body = _}, _} = parse(Data), + body = _}, _, _} = parse(Data), %% Subscribe gen_tcp:send(Sock, serialize(<<"SUBSCRIBE">>, @@ -290,7 +316,7 @@ t_ack(_) -> {ok, Data1} = gen_tcp:recv(Sock, 0), {ok, Frame = #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"ack test">>}, _} = parse(Data1), + body = <<"ack test">>}, _, _} = parse(Data1), AckId = proplists:get_value(<<"ack">>, Frame#stomp_frame.headers), @@ -301,7 +327,7 @@ t_ack(_) -> {ok, Data2} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data2), + body = _}, _, _} = parse(Data2), gen_tcp:send(Sock, serialize(<<"SEND">>, [{<<"destination">>, <<"/queue/foo">>}], @@ -310,7 +336,7 @@ t_ack(_) -> {ok, Data3} = gen_tcp:recv(Sock, 0), {ok, Frame1 = #stomp_frame{command = <<"MESSAGE">>, headers = _, - body = <<"nack test">>}, _} = parse(Data3), + body = <<"nack test">>}, _, _} = parse(Data3), AckId1 = proplists:get_value(<<"ack">>, Frame1#stomp_frame.headers), @@ -321,9 +347,16 @@ t_ack(_) -> {ok, Data4} = gen_tcp:recv(Sock, 0), {ok, #stomp_frame{command = <<"RECEIPT">>, headers = [{<<"receipt-id">>, <<"12345">>}], - body = _}, _} = parse(Data4) + body = _}, _, _} = parse(Data4) end). +%% TODO: Mountpoint, AuthChain, ACL + Mountpoint, ClientInfoOverride, +%% Listeners, Metrics, Stats, ClientInfo +%% +%% TODO: Start/Stop, List Instace +%% +%% TODO: RateLimit, OOM, + with_connection(DoFun) -> {ok, Sock} = gen_tcp:connect({127, 0, 0, 1}, 61613, @@ -336,14 +369,15 @@ with_connection(DoFun) -> end. serialize(Command, Headers) -> - emqx_stomp_frame:serialize(emqx_stomp_frame:make(Command, Headers)). + emqx_stomp_frame:serialize_pkt(emqx_stomp_frame:make(Command, Headers), #{}). serialize(Command, Headers, Body) -> - emqx_stomp_frame:serialize(emqx_stomp_frame:make(Command, Headers, Body)). + emqx_stomp_frame:serialize_pkt(emqx_stomp_frame:make(Command, Headers, Body), #{}). parse(Data) -> - ProtoEnv = [{max_headers, 10}, - {max_header_length, 1024}, - {max_body_length, 8192}], - Parser = emqx_stomp_frame:init_parer_state(ProtoEnv), + ProtoEnv = #{max_headers => 10, + max_header_length => 1024, + max_body_length => 8192 + }, + Parser = emqx_stomp_frame:initial_parse_state(ProtoEnv), emqx_stomp_frame:parse(Data, Parser). diff --git a/apps/emqx_stomp/test/emqx_stomp_heartbeat_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_heartbeat_SUITE.erl similarity index 100% rename from apps/emqx_stomp/test/emqx_stomp_heartbeat_SUITE.erl rename to apps/emqx_gateway/test/emqx_stomp_heartbeat_SUITE.erl diff --git a/apps/emqx_stomp/.gitignore b/apps/emqx_stomp/.gitignore deleted file mode 100644 index 95654d437..000000000 --- a/apps/emqx_stomp/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -.erlang.mk/ -emq_stomp.d -ct.coverdata -logs/ -test/ct.cover.spec -data/ -.DS_Store -emqx_stomp.d -_build/ -rebar.lock -erlang.mk -rebar3.crashdump -etc/emqx_stomp.conf.rendered -.rebar3/ -*.swp diff --git a/apps/emqx_stomp/etc/emqx_stomp.conf b/apps/emqx_stomp/etc/emqx_stomp.conf deleted file mode 100644 index 832b50655..000000000 --- a/apps/emqx_stomp/etc/emqx_stomp.conf +++ /dev/null @@ -1,124 +0,0 @@ -##-------------------------------------------------------------------- -## Stomp Plugin -##-------------------------------------------------------------------- - -##-------------------------------------------------------------------- -## Stomp listener - -## The Port that stomp listener will bind. -## -## Value: Port -stomp.listener.port = 61613 - -## The acceptor pool for stomp listener. -## -## Value: Number -stomp.listener.acceptors = 4 - -## Maximum number of concurrent stomp connections. -## -## Value: Number -stomp.listener.max_connections = 512 - -## Whether to enable SSL. -## -## Value: on | off -## stomp.listener.ssl = off - -## Path to the file containing the user's private PEM-encoded key. -## -## Value: File -## stomp.listener.keyfile = "etc/certs/key.pem" - -## Path to a file containing the user certificate. -## -## Value: File -## stomp.listener.certfile = "etc/certs/cert.pem" - -## Path to the file containing PEM-encoded CA certificates. -## -## Value: File -## stomp.listener.cacertfile = "etc/certs/cacert.pem" - -## See: 'listener.ssl..dhfile' in emq.conf -## -## Value: File -## stomp.listener.dhfile = "etc/certs/dh-params.pem" - -## See: 'listener.ssl..verify' in emq.conf -## -## Value: verify_peer | verify_none -## stomp.listener.verify = verify_peer - -## See: 'listener.ssl..fail_if_no_peer_cert' in emq.conf -## -## Value: false | true -## stomp.listener.fail_if_no_peer_cert = true - -## TLS versions only to protect from POODLE attack. -## -## Value: String, seperated by ',' -## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier -## stomp.listener.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" - -## SSL Handshake timeout. -## -## Value: Duration -## stomp.listener.handshake_timeout = 15s - -## See: 'listener.ssl..ciphers' in emq.conf -## -## Value: Ciphers -## stomp.listener.ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" - -## See: 'listener.ssl..secure_renegotiate' in emq.conf -## -## Value: on | off -## stomp.listener.secure_renegotiate = off - -## See: 'listener.ssl..reuse_sessions' in emq.conf -## -## Value: on | off -## stomp.listener.reuse_sessions = on - -## See: 'listener.ssl..honor_cipher_order' in emq.conf -## -## Value: on | off -## stomp.listener.honor_cipher_order = on - -##-------------------------------------------------------------------- -## Stomp login user and password - -## Default login user -## -## Value: String -stomp.default_user.login = guest - -## Default login password -## -## Value: String -stomp.default_user.passcode = guest - -## Allow anonymous authentication. -## -## Value: true | false -stomp.allow_anonymous = true - -##-------------------------------------------------------------------- -## Stomp frame - -## Maximum numbers of frame headers. -## -## Value: Number -stomp.frame.max_headers = 10 - -## Maximum length of frame header. -## -## Value: Number -stomp.frame.max_header_length = 1024 - -## Maximum body length of frame. -## -## Value: Number -stomp.frame.max_body_length = 8192 - diff --git a/apps/emqx_stomp/include/emqx_stomp.hrl b/apps/emqx_stomp/include/emqx_stomp.hrl deleted file mode 100644 index a9cf2cf48..000000000 --- a/apps/emqx_stomp/include/emqx_stomp.hrl +++ /dev/null @@ -1,48 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 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 Stomp Frame Header. - --define(STOMP_VER, <<"1.2">>). - --define(STOMP_SERVER, <<"emqx-stomp/1.2">>). - -%%-------------------------------------------------------------------- -%% STOMP Frame -%%-------------------------------------------------------------------- - --record(stomp_frame, {command, headers = [], body = <<>> :: iodata()}). - --type(stomp_frame() :: #stomp_frame{}). - -%%-------------------------------------------------------------------- -%% Frame Size Limits -%% -%% To prevent malicious clients from exploiting memory allocation in a server, -%% servers MAY place maximum limits on: -%% -%% the number of frame headers allowed in a single frame -%% the maximum length of header lines -%% the maximum size of a frame body -%% -%% If these limits are exceeded the server SHOULD send the client an ERROR frame -%% and then close the connection. -%%-------------------------------------------------------------------- - --define(MAX_HEADER_NUM, 10). --define(MAX_HEADER_LENGTH, 1024). --define(MAX_BODY_LENGTH, 65536). - diff --git a/apps/emqx_stomp/priv/emqx_stomp.schema b/apps/emqx_stomp/priv/emqx_stomp.schema deleted file mode 100644 index 32a3c272b..000000000 --- a/apps/emqx_stomp/priv/emqx_stomp.schema +++ /dev/null @@ -1,149 +0,0 @@ -%%-*- mode: erlang -*- -%% emqx_stomp config mapping - -{mapping, "stomp.listener.port", "emqx_stomp.listener", [ - {default, 61613}, - {datatype, [integer, ip]} -]}. - -{mapping, "stomp.listener.acceptors", "emqx_stomp.listener", [ - {default, 4}, - {datatype, integer} -]}. - -{mapping, "stomp.listener.max_connections", "emqx_stomp.listener", [ - {default, 512}, - {datatype, integer} -]}. - -{mapping, "stomp.listener.ssl", "emqx_stomp.listener", [ - {datatype, flag}, - {default, off} -]}. - -{mapping, "stomp.listener.tls_versions", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.handshake_timeout", "emqx_stomp.listener", [ - {default, "15s"}, - {datatype, {duration, ms}} -]}. - -{mapping, "stomp.listener.dhfile", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.keyfile", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.certfile", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.cacertfile", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.verify", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.fail_if_no_peer_cert", "emqx_stomp.listener", [ - {datatype, {enum, [true, false]}} -]}. - -{mapping, "stomp.listener.ciphers", "emqx_stomp.listener", [ - {datatype, string} -]}. - -{mapping, "stomp.listener.secure_renegotiate", "emqx_stomp.listener", [ - {datatype, flag} -]}. - -{mapping, "stomp.listener.reuse_sessions", "emqx_stomp.listener", [ - {default, on}, - {datatype, flag} -]}. - -{mapping, "stomp.listener.honor_cipher_order", "emqx_stomp.listener", [ - {datatype, flag} -]}. - -{translation, "emqx_stomp.listener", fun(Conf) -> - Port = cuttlefish:conf_get("stomp.listener.port", Conf), - Acceptors = cuttlefish:conf_get("stomp.listener.acceptors", Conf), - MaxConnections = cuttlefish:conf_get("stomp.listener.max_connections", Conf), - Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, - SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end, - SslOpts = fun(Prefix) -> - Versions = case SplitFun(cuttlefish:conf_get(Prefix ++ ".tls_versions", Conf, undefined)) of - undefined -> undefined; - L -> [list_to_atom(V) || V <- L] - end, - Filter([{versions, Versions}, - {ciphers, SplitFun(cuttlefish:conf_get(Prefix ++ ".ciphers", Conf, undefined))}, - {handshake_timeout, cuttlefish:conf_get(Prefix ++ ".handshake_timeout", Conf, undefined)}, - {dhfile, cuttlefish:conf_get(Prefix ++ ".dhfile", Conf, undefined)}, - {keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)}, - {certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)}, - {cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}, - {verify, cuttlefish:conf_get(Prefix ++ ".verify", Conf, undefined)}, - {fail_if_no_peer_cert, cuttlefish:conf_get(Prefix ++ ".fail_if_no_peer_cert", Conf, undefined)}, - {secure_renegotiate, cuttlefish:conf_get(Prefix ++ ".secure_renegotiate", Conf, undefined)}, - {reuse_sessions, cuttlefish:conf_get(Prefix ++ ".reuse_sessions", Conf, undefined)}, - {honor_cipher_order, cuttlefish:conf_get(Prefix ++ ".honor_cipher_order", Conf, undefined)}]) - end, - Opts = [{acceptors, Acceptors}, {max_connections, MaxConnections}], - {Port, case cuttlefish:conf_get("stomp.listener.ssl", Conf) of - true -> - [{sslopts, SslOpts("stomp.listener")} | Opts]; - false -> - Opts - end} -end}. - -{mapping, "stomp.default_user.login", "emqx_stomp.default_user", [ - {default, "guest"}, - {datatype, string} -]}. - -{mapping, "stomp.default_user.passcode", "emqx_stomp.default_user", [ - {default, "guest"}, - {datatype, string} -]}. - -{translation, "emqx_stomp.default_user", fun(Conf) -> - Login = cuttlefish:conf_get("stomp.default_user.login", Conf), - Passcode = cuttlefish:conf_get("stomp.default_user.passcode", Conf), - [{login, Login}, {passcode, Passcode}] -end}. - -{mapping, "stomp.allow_anonymous", "emqx_stomp.allow_anonymous", [ - {default, true}, - {datatype, {enum, [true, false]}} -]}. - -{mapping, "stomp.frame.max_headers", "emqx_stomp.frame", [ - {default, 10}, - {datatype, integer} -]}. - -{mapping, "stomp.frame.max_header_length", "emqx_stomp.frame", [ - {default, 1024}, - {datatype, integer} -]}. - -{mapping, "stomp.frame.max_body_length", "emqx_stomp.frame", [ - {default, 8192}, - {datatype, integer} -]}. - -{translation, "emqx_stomp.frame", fun(Conf) -> - MaxHeaders = cuttlefish:conf_get("stomp.frame.max_headers", Conf), - MaxHeaderLength = cuttlefish:conf_get("stomp.frame.max_header_length", Conf), - MaxBodyLength = cuttlefish:conf_get("stomp.frame.max_body_length", Conf), - [{max_headers, MaxHeaders}, {max_header_length, MaxHeaderLength}, {max_body_length, MaxBodyLength}] -end}. - diff --git a/apps/emqx_stomp/rebar.config b/apps/emqx_stomp/rebar.config deleted file mode 100644 index 7ac3b98c8..000000000 --- a/apps/emqx_stomp/rebar.config +++ /dev/null @@ -1,16 +0,0 @@ -{deps, []}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. diff --git a/apps/emqx_stomp/src/emqx_stomp.app.src b/apps/emqx_stomp/src/emqx_stomp.app.src deleted file mode 100644 index 2e66734ec..000000000 --- a/apps/emqx_stomp/src/emqx_stomp.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_stomp, - [{description, "EMQ X Stomp Protocol Plugin"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_stomp_sup]}, - {applications, [kernel,stdlib]}, - {mod, {emqx_stomp,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-stomp"} - ]} - ]}. diff --git a/apps/emqx_stomp/src/emqx_stomp.erl b/apps/emqx_stomp/src/emqx_stomp.erl deleted file mode 100644 index 9eafe3cf7..000000000 --- a/apps/emqx_stomp/src/emqx_stomp.erl +++ /dev/null @@ -1,142 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_stomp). - --behaviour(application). --behaviour(supervisor). - --emqx_plugin(protocol). - --export([ start/2 - , stop/1 - ]). - --export([ start_listeners/0 - , start_listener/1 - , start_listener/3 - , stop_listeners/0 - , stop_listener/1 - , stop_listener/3 - ]). - --export([init/1]). - --define(APP, ?MODULE). --define(TCP_OPTS, [binary, {packet, raw}, {reuseaddr, true}, {nodelay, true}]). - --type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). - -%%-------------------------------------------------------------------- -%% Application callbacks -%%-------------------------------------------------------------------- - -start(_StartType, _StartArgs) -> - {ok, Sup} = supervisor:start_link({local, emqx_stomp_sup}, ?MODULE, []), - start_listeners(), - {ok, Sup}. - -stop(_State) -> - stop_listeners(). - -%%-------------------------------------------------------------------- -%% Supervisor callbacks -%%-------------------------------------------------------------------- - -init([]) -> - {ok, {{one_for_all, 10, 100}, []}}. - -%%-------------------------------------------------------------------- -%% Start/Stop listeners -%%-------------------------------------------------------------------- - --spec(start_listeners() -> ok). -start_listeners() -> - lists:foreach(fun start_listener/1, listeners_confs()). - --spec(start_listener(listener()) -> ok). -start_listener({Proto, ListenOn, Options}) -> - case start_listener(Proto, ListenOn, Options) of - {ok, _} -> io:format("Start stomp:~s listener on ~s successfully.~n", - [Proto, format(ListenOn)]); - {error, Reason} -> - io:format(standard_error, "Failed to start stomp:~s listener on ~s: ~0p~n", - [Proto, format(ListenOn), Reason]), - error(Reason) - end. - --spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) - -> {ok, pid()} | {error, term()}). -start_listener(tcp, ListenOn, Options) -> - start_stomp_listener('stomp:tcp', ListenOn, Options); -start_listener(ssl, ListenOn, Options) -> - start_stomp_listener('stomp:ssl', ListenOn, Options). - -%% @private -start_stomp_listener(Name, ListenOn, Options) -> - SockOpts = esockd:parse_opt(Options), - esockd:open(Name, ListenOn, merge_default(SockOpts), - {emqx_stomp_connection, start_link, [Options -- SockOpts]}). - --spec(stop_listeners() -> ok). -stop_listeners() -> - lists:foreach(fun stop_listener/1, listeners_confs()). - --spec(stop_listener(listener()) -> ok | {error, term()}). -stop_listener({Proto, ListenOn, Opts}) -> - StopRet = stop_listener(Proto, ListenOn, Opts), - case StopRet of - ok -> io:format("Stop stomp:~s listener on ~s successfully.~n", - [Proto, format(ListenOn)]); - {error, Reason} -> - io:format(standard_error, "Failed to stop stomp:~s listener on ~s: ~0p~n", - [Proto, format(ListenOn), Reason]) - end, - StopRet. - --spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) - -> ok | {error, term()}). -stop_listener(tcp, ListenOn, _Opts) -> - esockd:close('stomp:tcp', ListenOn); -stop_listener(ssl, ListenOn, _Opts) -> - esockd:close('stomp:ssl', ListenOn). - -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- - -listeners_confs() -> - {ok, {Port, Opts}} = application:get_env(?APP, listener), - Options = application:get_env(?APP, frame, []), - Anonymous = application:get_env(emqx_stomp, allow_anonymous, false), - {ok, DefaultUser} = application:get_env(emqx_stomp, default_user), - [{tcp, Port, [{allow_anonymous, Anonymous}, - {default_user, DefaultUser} | Options ++ Opts]}]. - -merge_default(Options) -> - case lists:keytake(tcp_options, 1, Options) of - {value, {tcp_options, TcpOpts}, Options1} -> - [{tcp_options, emqx_misc:merge_opts(?TCP_OPTS, TcpOpts)} | Options1]; - false -> - [{tcp_options, ?TCP_OPTS} | Options] - end. - -format(Port) when is_integer(Port) -> - io_lib:format("0.0.0.0:~w", [Port]); -format({Addr, Port}) when is_list(Addr) -> - io_lib:format("~s:~w", [Addr, Port]); -format({Addr, Port}) when is_tuple(Addr) -> - io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). diff --git a/apps/emqx_stomp/src/emqx_stomp_connection.erl b/apps/emqx_stomp/src/emqx_stomp_connection.erl deleted file mode 100644 index d4e7f6475..000000000 --- a/apps/emqx_stomp/src/emqx_stomp_connection.erl +++ /dev/null @@ -1,274 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_stomp_connection). - --behaviour(gen_server). - --include("emqx_stomp.hrl"). --include_lib("emqx/include/logger.hrl"). - --logger_header("[Stomp-Conn]"). - --export([ start_link/3 - , info/1 - ]). - -%% gen_server Function Exports --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , code_change/3 - , terminate/2 - ]). - -%% for protocol --export([send/4, heartbeat/2]). - --record(state, {transport, socket, peername, conn_name, conn_state, - await_recv, rate_limit, parser, pstate, - proto_env, heartbeat}). - --define(INFO_KEYS, [peername, await_recv, conn_state]). --define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). - -start_link(Transport, Sock, ProtoEnv) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, ProtoEnv]])}. - -info(CPid) -> - gen_server:call(CPid, info, infinity). - -init([Transport, Sock, ProtoEnv]) -> - process_flag(trap_exit, true), - case Transport:wait(Sock) of - {ok, NewSock} -> - {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - ConnName = esockd:format(Peername), - SendFun = {fun ?MODULE:send/4, [Transport, Sock, self()]}, - HrtBtFun = {fun ?MODULE:heartbeat/2, [Transport, Sock]}, - Parser = emqx_stomp_frame:init_parer_state(ProtoEnv), - PState = emqx_stomp_protocol:init(#{peername => Peername, - sendfun => SendFun, - heartfun => HrtBtFun}, ProtoEnv), - RateLimit = init_rate_limit(proplists:get_value(rate_limit, ProtoEnv)), - State = run_socket(#state{transport = Transport, - socket = NewSock, - peername = Peername, - conn_name = ConnName, - conn_state = running, - await_recv = false, - rate_limit = RateLimit, - parser = Parser, - proto_env = ProtoEnv, - pstate = PState}), - emqx_logger:set_metadata_peername(esockd:format(Peername)), - gen_server:enter_loop(?MODULE, [{hibernate_after, 5000}], State, 20000); - {error, Reason} -> - {stop, Reason} - end. - -init_rate_limit(undefined) -> - undefined; -init_rate_limit({Rate, Burst}) -> - esockd_rate_limit:new(Rate, Burst). - -send(Data, Transport, Sock, ConnPid) -> - try Transport:async_send(Sock, Data) of - ok -> ok; - {error, Reason} -> ConnPid ! {shutdown, Reason} - catch - error:Error -> ConnPid ! {shutdown, Error} - end. - -heartbeat(Transport, Sock) -> - Transport:send(Sock, <<$\n>>). - -handle_call(info, _From, State = #state{transport = Transport, - socket = Sock, - peername = Peername, - await_recv = AwaitRecv, - conn_state = ConnState, - pstate = PState}) -> - ClientInfo = [{peername, Peername}, {await_recv, AwaitRecv}, - {conn_state, ConnState}], - ProtoInfo = emqx_stomp_protocol:info(PState), - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, SockStats} -> - {reply, lists:append([ClientInfo, ProtoInfo, SockStats]), State}; - {error, Reason} -> - {stop, Reason, lists:append([ClientInfo, ProtoInfo]), State} - end; - -handle_call(Req, _From, State) -> - ?LOG(error, "unexpected request: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - ?LOG(error, "unexpected msg: ~p", [Msg]), - noreply(State). - -handle_info(timeout, State) -> - shutdown(idle_timeout, State); - -handle_info({shutdown, Reason}, State) -> - shutdown(Reason, State); - -handle_info({timeout, TRef, TMsg}, State) when TMsg =:= incoming; - TMsg =:= outgoing -> - - Stat = case TMsg of - incoming -> recv_oct; - _ -> send_oct - end, - case getstat(Stat, State) of - {ok, Val} -> - with_proto(timeout, [TRef, {TMsg, Val}], State); - {error, Reason} -> - shutdown({sock_error, Reason}, State) - end; - -handle_info({timeout, TRef, TMsg}, State) -> - with_proto(timeout, [TRef, TMsg], State); - -handle_info({'EXIT', HbProc, Error}, State = #state{heartbeat = HbProc}) -> - stop(Error, State); - -handle_info(activate_sock, State) -> - noreply(run_socket(State#state{conn_state = running})); - -handle_info({inet_async, _Sock, _Ref, {ok, Bytes}}, State) -> - ?LOG(debug, "RECV ~p", [Bytes]), - received(Bytes, rate_limit(size(Bytes), State#state{await_recv = false})); - -handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> - shutdown(Reason, State); - -handle_info({inet_reply, _Ref, ok}, State) -> - noreply(State); - -handle_info({inet_reply, _Sock, {error, Reason}}, State) -> - shutdown(Reason, State); - -handle_info({deliver, _Topic, Msg}, State = #state{pstate = PState}) -> - noreply(State#state{pstate = case emqx_stomp_protocol:send(Msg, PState) of - {ok, PState1} -> - PState1; - {error, dropped, PState1} -> - PState1 - end}); - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - noreply(State). - -terminate(Reason, #state{transport = Transport, - socket = Sock, - pstate = PState}) -> - ?LOG(info, "terminated for ~p", [Reason]), - Transport:fast_close(Sock), - case {PState, Reason} of - {undefined, _} -> ok; - {_, {shutdown, Error}} -> - emqx_stomp_protocol:shutdown(Error, PState); - {_, Reason} -> - emqx_stomp_protocol:shutdown(Reason, PState) - end. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Receive and Parse data -%%-------------------------------------------------------------------- - -with_proto(Fun, Args, State = #state{pstate = PState}) -> - case erlang:apply(emqx_stomp_protocol, Fun, Args ++ [PState]) of - {ok, NPState} -> - noreply(State#state{pstate = NPState}); - {F, Reason, NPState} when F == stop; - F == error; - F == shutdown -> - shutdown(Reason, State#state{pstate = NPState}) - end. - -received(<<>>, State) -> - noreply(State); - -received(Bytes, State = #state{parser = Parser, - pstate = PState}) -> - try emqx_stomp_frame:parse(Bytes, Parser) of - {more, NewParser} -> - noreply(State#state{parser = NewParser}); - {ok, Frame, Rest} -> - ?LOG(info, "RECV Frame: ~s", [emqx_stomp_frame:format(Frame)]), - case emqx_stomp_protocol:received(Frame, PState) of - {ok, PState1} -> - received(Rest, reset_parser(State#state{pstate = PState1})); - {error, Error, PState1} -> - shutdown(Error, State#state{pstate = PState1}); - {stop, Reason, PState1} -> - stop(Reason, State#state{pstate = PState1}) - end; - {error, Error} -> - ?LOG(error, "Framing error - ~s", [Error]), - ?LOG(error, "Bytes: ~p", [Bytes]), - shutdown(frame_error, State) - catch - _Error:Reason -> - ?LOG(error, "Parser failed for ~p", [Reason]), - ?LOG(error, "Error data: ~p", [Bytes]), - shutdown(parse_error, State) - end. - -reset_parser(State = #state{proto_env = ProtoEnv}) -> - State#state{parser = emqx_stomp_frame:init_parer_state(ProtoEnv)}. - -rate_limit(_Size, State = #state{rate_limit = undefined}) -> - run_socket(State); -rate_limit(Size, State = #state{rate_limit = Rl}) -> - case esockd_rate_limit:check(Size, Rl) of - {0, Rl1} -> - run_socket(State#state{conn_state = running, rate_limit = Rl1}); - {Pause, Rl1} -> - ?LOG(error, "Rate limiter pause for ~p", [Pause]), - erlang:send_after(Pause, self(), activate_sock), - State#state{conn_state = blocked, rate_limit = Rl1} - end. - -run_socket(State = #state{conn_state = blocked}) -> - State; -run_socket(State = #state{await_recv = true}) -> - State; -run_socket(State = #state{transport = Transport, socket = Sock}) -> - Transport:async_recv(Sock, 0, infinity), - State#state{await_recv = true}. - -getstat(Stat, #state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, [Stat]) of - {ok, [{Stat, Val}]} -> {ok, Val}; - {error, Error} -> {error, Error} - end. - -noreply(State) -> - {noreply, State}. - -stop(Reason, State) -> - {stop, Reason, State}. - -shutdown(Reason, State) -> - stop({shutdown, Reason}, State). - diff --git a/apps/emqx_stomp/src/emqx_stomp_protocol.erl b/apps/emqx_stomp/src/emqx_stomp_protocol.erl deleted file mode 100644 index cc5c28ce9..000000000 --- a/apps/emqx_stomp/src/emqx_stomp_protocol.erl +++ /dev/null @@ -1,468 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 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 Stomp Protocol Processor. --module(emqx_stomp_protocol). - --include("emqx_stomp.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - --logger_header("[Stomp-Proto]"). - --import(proplists, [get_value/2, get_value/3]). - -%% API --export([ init/2 - , info/1 - ]). - --export([ received/2 - , send/2 - , shutdown/2 - , timeout/3 - ]). - -%% for trans callback --export([ handle_recv_send_frame/2 - , handle_recv_ack_frame/2 - , handle_recv_nack_frame/2 - ]). - --record(pstate, { - peername, - heartfun, - sendfun, - connected = false, - proto_ver, - proto_name, - heart_beats, - login, - allow_anonymous, - default_user, - subscriptions = [], - timers :: #{atom() => disable | undefined | reference()}, - transaction :: #{binary() => list()} - }). - --define(TIMER_TABLE, #{ - incoming_timer => incoming, - outgoing_timer => outgoing, - clean_trans_timer => clean_trans - }). - --define(TRANS_TIMEOUT, 60000). - --type(pstate() :: #pstate{}). - -%% @doc Init protocol -init(#{peername := Peername, - sendfun := SendFun, - heartfun := HeartFun}, Env) -> - AllowAnonymous = get_value(allow_anonymous, Env, false), - DefaultUser = get_value(default_user, Env), - #pstate{peername = Peername, - heartfun = HeartFun, - sendfun = SendFun, - timers = #{}, - transaction = #{}, - allow_anonymous = AllowAnonymous, - default_user = DefaultUser}. - -info(#pstate{connected = Connected, - proto_ver = ProtoVer, - proto_name = ProtoName, - heart_beats = Heartbeats, - login = Login, - subscriptions = Subscriptions}) -> - [{connected, Connected}, - {proto_ver, ProtoVer}, - {proto_name, ProtoName}, - {heart_beats, Heartbeats}, - {login, Login}, - {subscriptions, Subscriptions}]. - --spec(received(stomp_frame(), pstate()) - -> {ok, pstate()} - | {error, any(), pstate()} - | {stop, any(), pstate()}). -received(Frame = #stomp_frame{command = <<"STOMP">>}, State) -> - received(Frame#stomp_frame{command = <<"CONNECT">>}, State); - -received(#stomp_frame{command = <<"CONNECT">>, headers = Headers}, - State = #pstate{connected = false, allow_anonymous = AllowAnonymous, default_user = DefaultUser}) -> - case negotiate_version(header(<<"accept-version">>, Headers)) of - {ok, Version} -> - Login = header(<<"login">>, Headers), - Passc = header(<<"passcode">>, Headers), - case check_login(Login, Passc, AllowAnonymous, DefaultUser) of - true -> - emqx_logger:set_metadata_clientid(Login), - - Heartbeats = parse_heartbeats(header(<<"heart-beat">>, Headers, <<"0,0">>)), - NState = start_heartbeart_timer(Heartbeats, State#pstate{connected = true, - proto_ver = Version, login = Login}), - send(connected_frame([{<<"version">>, Version}, - {<<"heart-beat">>, reverse_heartbeats(Heartbeats)}]), NState); - false -> - _ = send(error_frame(undefined, <<"Login or passcode error!">>), State), - {error, login_or_passcode_error, State} - end; - {error, Msg} -> - _ = send(error_frame([{<<"version">>, <<"1.0,1.1,1.2">>}, - {<<"content-type">>, <<"text/plain">>}], undefined, Msg), State), - {error, unsupported_version, State} - end; - -received(#stomp_frame{command = <<"CONNECT">>}, State = #pstate{connected = true}) -> - {error, unexpected_connect, State}; - -received(Frame = #stomp_frame{command = <<"SEND">>, headers = Headers}, State) -> - case header(<<"transaction">>, Headers) of - undefined -> {ok, handle_recv_send_frame(Frame, State)}; - TransactionId -> add_action(TransactionId, {fun ?MODULE:handle_recv_send_frame/2, [Frame]}, receipt_id(Headers), State) - end; - -received(#stomp_frame{command = <<"SUBSCRIBE">>, headers = Headers}, - State = #pstate{subscriptions = Subscriptions}) -> - Id = header(<<"id">>, Headers), - Topic = header(<<"destination">>, Headers), - Ack = header(<<"ack">>, Headers, <<"auto">>), - {ok, State1} = case lists:keyfind(Id, 1, Subscriptions) of - {Id, Topic, Ack} -> - {ok, State}; - false -> - emqx_broker:subscribe(Topic), - {ok, State#pstate{subscriptions = [{Id, Topic, Ack}|Subscriptions]}} - end, - maybe_send_receipt(receipt_id(Headers), State1); - -received(#stomp_frame{command = <<"UNSUBSCRIBE">>, headers = Headers}, - State = #pstate{subscriptions = Subscriptions}) -> - Id = header(<<"id">>, Headers), - - {ok, State1} = case lists:keyfind(Id, 1, Subscriptions) of - {Id, Topic, _Ack} -> - ok = emqx_broker:unsubscribe(Topic), - {ok, State#pstate{subscriptions = lists:keydelete(Id, 1, Subscriptions)}}; - false -> - {ok, State} - end, - maybe_send_receipt(receipt_id(Headers), State1); - -%% ACK -%% id:12345 -%% transaction:tx1 -%% -%% ^@ -received(Frame = #stomp_frame{command = <<"ACK">>, headers = Headers}, State) -> - case header(<<"transaction">>, Headers) of - undefined -> {ok, handle_recv_ack_frame(Frame, State)}; - TransactionId -> add_action(TransactionId, {fun ?MODULE:handle_recv_ack_frame/2, [Frame]}, receipt_id(Headers), State) - end; - -%% NACK -%% id:12345 -%% transaction:tx1 -%% -%% ^@ -received(Frame = #stomp_frame{command = <<"NACK">>, headers = Headers}, State) -> - case header(<<"transaction">>, Headers) of - undefined -> {ok, handle_recv_nack_frame(Frame, State)}; - TransactionId -> add_action(TransactionId, {fun ?MODULE:handle_recv_nack_frame/2, [Frame]}, receipt_id(Headers), State) - end; - -%% BEGIN -%% transaction:tx1 -%% -%% ^@ -received(#stomp_frame{command = <<"BEGIN">>, headers = Headers}, - State = #pstate{transaction = Trans}) -> - Id = header(<<"transaction">>, Headers), - case maps:get(Id, Trans, undefined) of - undefined -> - Ts = erlang:system_time(millisecond), - NState = ensure_clean_trans_timer(State#pstate{transaction = Trans#{Id => {Ts, []}}}), - maybe_send_receipt(receipt_id(Headers), NState); - _ -> - send(error_frame(receipt_id(Headers), ["Transaction ", Id, " already started"]), State) - end; - -%% COMMIT -%% transaction:tx1 -%% -%% ^@ -received(#stomp_frame{command = <<"COMMIT">>, headers = Headers}, - State = #pstate{transaction = Trans}) -> - Id = header(<<"transaction">>, Headers), - case maps:get(Id, Trans, undefined) of - {_, Actions} -> - NState = lists:foldr(fun({Func, Args}, S) -> - erlang:apply(Func, Args ++ [S]) - end, State#pstate{transaction = maps:remove(Id, Trans)}, Actions), - maybe_send_receipt(receipt_id(Headers), NState); - _ -> - send(error_frame(receipt_id(Headers), ["Transaction ", Id, " not found"]), State) - end; - -%% ABORT -%% transaction:tx1 -%% -%% ^@ -received(#stomp_frame{command = <<"ABORT">>, headers = Headers}, - State = #pstate{transaction = Trans}) -> - Id = header(<<"transaction">>, Headers), - case maps:get(Id, Trans, undefined) of - {_, _Actions} -> - NState = State#pstate{transaction = maps:remove(Id, Trans)}, - maybe_send_receipt(receipt_id(Headers), NState); - _ -> - send(error_frame(receipt_id(Headers), ["Transaction ", Id, " not found"]), State) - end; - -received(#stomp_frame{command = <<"DISCONNECT">>, headers = Headers}, State) -> - _ = maybe_send_receipt(receipt_id(Headers), State), - {stop, normal, State}. - -send(Msg = #message{topic = Topic, headers = Headers, payload = Payload}, - State = #pstate{subscriptions = Subscriptions}) -> - case lists:keyfind(Topic, 2, Subscriptions) of - {Id, Topic, Ack} -> - Headers0 = [{<<"subscription">>, Id}, - {<<"message-id">>, next_msgid()}, - {<<"destination">>, Topic}, - {<<"content-type">>, <<"text/plain">>}], - Headers1 = case Ack of - _ when Ack =:= <<"client">> orelse Ack =:= <<"client-individual">> -> - Headers0 ++ [{<<"ack">>, next_ackid()}]; - _ -> - Headers0 - end, - Frame = #stomp_frame{command = <<"MESSAGE">>, - headers = Headers1 ++ maps:get(stomp_headers, Headers, []), - body = Payload}, - send(Frame, State); - false -> - ?LOG(error, "Stomp dropped: ~p", [Msg]), - {error, dropped, State} - end; - -send(Frame, State = #pstate{sendfun = {Fun, Args}}) -> - ?LOG(info, "SEND Frame: ~s", [emqx_stomp_frame:format(Frame)]), - Data = emqx_stomp_frame:serialize(Frame), - ?LOG(debug, "SEND ~p", [Data]), - erlang:apply(Fun, [Data] ++ Args), - {ok, State}. - -shutdown(_Reason, _State) -> - ok. - -timeout(_TRef, {incoming, NewVal}, - State = #pstate{heart_beats = HrtBt}) -> - case emqx_stomp_heartbeat:check(incoming, NewVal, HrtBt) of - {error, timeout} -> - {shutdown, heartbeat_timeout, State}; - {ok, NHrtBt} -> - {ok, reset_timer(incoming_timer, State#pstate{heart_beats = NHrtBt})} - end; - -timeout(_TRef, {outgoing, NewVal}, - State = #pstate{heart_beats = HrtBt, - heartfun = {Fun, Args}}) -> - case emqx_stomp_heartbeat:check(outgoing, NewVal, HrtBt) of - {error, timeout} -> - _ = erlang:apply(Fun, Args), - {ok, State}; - {ok, NHrtBt} -> - {ok, reset_timer(outgoing_timer, State#pstate{heart_beats = NHrtBt})} - end; - -timeout(_TRef, clean_trans, State = #pstate{transaction = Trans}) -> - Now = erlang:system_time(millisecond), - NTrans = maps:filter(fun(_, {Ts, _}) -> Ts + ?TRANS_TIMEOUT < Now end, Trans), - {ok, ensure_clean_trans_timer(State#pstate{transaction = NTrans})}. - -negotiate_version(undefined) -> - {ok, <<"1.0">>}; -negotiate_version(Accepts) -> - negotiate_version(?STOMP_VER, - lists:reverse( - lists:sort( - binary:split(Accepts, <<",">>, [global])))). - -negotiate_version(Ver, []) -> - {error, <<"Supported protocol versions < ", Ver/binary>>}; -negotiate_version(Ver, [AcceptVer|_]) when Ver >= AcceptVer -> - {ok, AcceptVer}; -negotiate_version(Ver, [_|T]) -> - negotiate_version(Ver, T). - -check_login(undefined, _, AllowAnonymous, _) -> - AllowAnonymous; -check_login(_, _, _, undefined) -> - false; -check_login(Login, Passcode, _, DefaultUser) -> - case {list_to_binary(get_value(login, DefaultUser)), - list_to_binary(get_value(passcode, DefaultUser))} of - {Login, Passcode} -> true; - {_, _ } -> false - end. - -add_action(Id, Action, ReceiptId, State = #pstate{transaction = Trans}) -> - case maps:get(Id, Trans, undefined) of - {Ts, Actions} -> - NTrans = Trans#{Id => {Ts, [Action|Actions]}}, - {ok, State#pstate{transaction = NTrans}}; - _ -> - send(error_frame(ReceiptId, ["Transaction ", Id, " not found"]), State) - end. - -maybe_send_receipt(undefined, State) -> - {ok, State}; -maybe_send_receipt(ReceiptId, State) -> - send(receipt_frame(ReceiptId), State). - -ack(_Id, State) -> - State. - -nack(_Id, State) -> State. - -header(Name, Headers) -> - get_value(Name, Headers). -header(Name, Headers, Val) -> - get_value(Name, Headers, Val). - -connected_frame(Headers) -> - emqx_stomp_frame:make(<<"CONNECTED">>, Headers). - -receipt_frame(ReceiptId) -> - emqx_stomp_frame:make(<<"RECEIPT">>, [{<<"receipt-id">>, ReceiptId}]). - -error_frame(ReceiptId, Msg) -> - error_frame([{<<"content-type">>, <<"text/plain">>}], ReceiptId, Msg). - -error_frame(Headers, undefined, Msg) -> - emqx_stomp_frame:make(<<"ERROR">>, Headers, Msg); -error_frame(Headers, ReceiptId, Msg) -> - emqx_stomp_frame:make(<<"ERROR">>, [{<<"receipt-id">>, ReceiptId} | Headers], Msg). - -next_msgid() -> - MsgId = case get(msgid) of - undefined -> 1; - I -> I - end, - put(msgid, MsgId + 1), - MsgId. - -next_ackid() -> - AckId = case get(ackid) of - undefined -> 1; - I -> I - end, - put(ackid, AckId + 1), - AckId. - -make_mqtt_message(Topic, Headers, Body) -> - Msg = emqx_message:make(stomp, Topic, Body), - Headers1 = lists:foldl(fun(Key, Headers0) -> - proplists:delete(Key, Headers0) - end, Headers, [<<"destination">>, - <<"content-length">>, - <<"content-type">>, - <<"transaction">>, - <<"receipt">>]), - emqx_message:set_headers(#{stomp_headers => Headers1}, Msg). - -receipt_id(Headers) -> - header(<<"receipt">>, Headers). - -%%-------------------------------------------------------------------- -%% Transaction Handle - -handle_recv_send_frame(#stomp_frame{command = <<"SEND">>, headers = Headers, body = Body}, State) -> - Topic = header(<<"destination">>, Headers), - _ = maybe_send_receipt(receipt_id(Headers), State), - _ = emqx_broker:publish( - make_mqtt_message(Topic, Headers, iolist_to_binary(Body)) - ), - State. - -handle_recv_ack_frame(#stomp_frame{command = <<"ACK">>, headers = Headers}, State) -> - Id = header(<<"id">>, Headers), - _ = maybe_send_receipt(receipt_id(Headers), State), - ack(Id, State). - -handle_recv_nack_frame(#stomp_frame{command = <<"NACK">>, headers = Headers}, State) -> - Id = header(<<"id">>, Headers), - _ = maybe_send_receipt(receipt_id(Headers), State), - nack(Id, State). - -ensure_clean_trans_timer(State = #pstate{transaction = Trans}) -> - case maps:size(Trans) of - 0 -> State; - _ -> ensure_timer(clean_trans_timer, State) - end. - -%%-------------------------------------------------------------------- -%% Heartbeat - -parse_heartbeats(Heartbeats) -> - CxCy = re:split(Heartbeats, <<",">>, [{return, list}]), - list_to_tuple([list_to_integer(S) || S <- CxCy]). - -reverse_heartbeats({Cx, Cy}) -> - iolist_to_binary(io_lib:format("~w,~w", [Cy, Cx])). - -start_heartbeart_timer(Heartbeats, State) -> - ensure_timer( - [incoming_timer, outgoing_timer], - State#pstate{heart_beats = emqx_stomp_heartbeat:init(Heartbeats)}). - -%%-------------------------------------------------------------------- -%% Timer - -ensure_timer([Name], State) -> - ensure_timer(Name, State); -ensure_timer([Name | Rest], State) -> - ensure_timer(Rest, ensure_timer(Name, State)); - -ensure_timer(Name, State = #pstate{timers = Timers}) -> - TRef = maps:get(Name, Timers, undefined), - Time = interval(Name, State), - case TRef == undefined andalso is_integer(Time) andalso Time > 0 of - true -> ensure_timer(Name, Time, State); - false -> State %% Timer disabled or exists - end. - -ensure_timer(Name, Time, State = #pstate{timers = Timers}) -> - Msg = maps:get(Name, ?TIMER_TABLE), - TRef = emqx_misc:start_timer(Time, Msg), - State#pstate{timers = Timers#{Name => TRef}}. - -reset_timer(Name, State) -> - ensure_timer(Name, clean_timer(Name, State)). - -clean_timer(Name, State = #pstate{timers = Timers}) -> - State#pstate{timers = maps:remove(Name, Timers)}. - -interval(incoming_timer, #pstate{heart_beats = HrtBt}) -> - emqx_stomp_heartbeat:interval(incoming, HrtBt); -interval(outgoing_timer, #pstate{heart_beats = HrtBt}) -> - emqx_stomp_heartbeat:interval(outgoing, HrtBt); -interval(clean_trans_timer, _) -> - ?TRANS_TIMEOUT. diff --git a/apps/emqx_stomp/test/client.py b/apps/emqx_stomp/test/client.py deleted file mode 100644 index f9f9e6577..000000000 --- a/apps/emqx_stomp/test/client.py +++ /dev/null @@ -1,19 +0,0 @@ -from stompest.config import StompConfig -from stompest.protocol import StompSpec -from stompest.sync import Stomp - -CONFIG = StompConfig('tcp://localhost:61613', version=StompSpec.VERSION_1_1) -QUEUE = '/queue/test' - -if __name__ == '__main__': - client = Stomp(CONFIG) - client.connect(heartBeats=(0, 10000)) - client.subscribe(QUEUE, {StompSpec.ID_HEADER: 1, StompSpec.ACK_HEADER: StompSpec.ACK_CLIENT_INDIVIDUAL}) - client.send(QUEUE, 'test message 1') - client.send(QUEUE, 'test message 2') - while True: - frame = client.receiveFrame() - print 'Got %s' % frame.info() - client.ack(frame) - client.disconnect() - diff --git a/rebar.config.erl b/rebar.config.erl index 56599dd54..de42601af 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -253,6 +253,7 @@ relx_apps(ReleaseType) -> , emqx_connector , emqx_authn , emqx_authz + , emqx_gateway , emqx_data_bridge , emqx_rule_engine , emqx_rule_actions From 8ee86f2fbe29b172a2bbb59d0986b82587433d3f Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 2 Jul 2021 20:23:14 +0800 Subject: [PATCH 077/379] fix(gw): start emqx-gateway after emqx restarted --- apps/emqx/src/emqx.erl | 1 + apps/emqx_gateway/src/emqx_gateway_app.erl | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index e5855b786..82688017a 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -257,6 +257,7 @@ emqx_feature() -> [ emqx_resource , emqx_authn , emqx_authz + , emqx_gateway , emqx_data_bridge , emqx_rule_engine , emqx_bridge_mqtt diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index 230776b73..c99228f17 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -20,8 +20,6 @@ -include_lib("emqx/include/logger.hrl"). --emqx_plugin(?MODULE). - -logger_header("[Gateway]"). -export([start/2, stop/1]). From 8b3fcde380f60139d4ef422b7d64c602a74fdd8e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 2 Jul 2021 22:17:22 +0800 Subject: [PATCH 078/379] feat(config): make the listeners up --- apps/emqx/etc/emqx.conf | 233 +++++++------- apps/emqx/etc/emqx.conf.old | 10 +- apps/emqx/src/emqx_connection.erl | 16 +- apps/emqx/src/emqx_listeners.erl | 291 +++++++----------- apps/emqx/src/emqx_schema.erl | 101 ++---- apps/emqx/src/emqx_tls_lib.erl | 23 +- apps/emqx/src/emqx_ws_connection.erl | 18 +- apps/emqx/test/emqx_connection_SUITE.erl | 8 +- .../emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 2 +- apps/emqx/test/emqx_ws_connection_SUITE.erl | 2 +- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 2 +- apps/emqx_exproto/etc/emqx_exproto.conf | 2 +- apps/emqx_exproto/priv/emqx_exproto.schema | 4 +- apps/emqx_exproto/src/emqx_exproto_conn.erl | 16 +- apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl | 2 +- apps/emqx_sn/src/emqx_sn_gateway.erl | 2 +- 16 files changed, 305 insertions(+), 427 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 3ea51ec3f..0dcb7285e 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -817,25 +817,25 @@ broker { ## - `conn_congestion.*` ## - `overall_max_connections` ## -## Syntax: zone. {} -zone.default { +## Syntax: zones. {} +zones.default { ## Enable authentication ## - ## @doc zone..auth.enable + ## @doc zones..auth.enable ## ValueType: Boolean ## Default: false auth.enable: false ## Enable per connection statistics. ## - ## @doc zone..stats.enable + ## @doc zones..stats.enable ## ValueType: Boolean ## Default: true stats.enable: true ## Maximum number of concurrent connections. ## - ## @doc zone..listeners..overall_max_connections + ## @doc zones..overall_max_connections ## ValueType: Number | infinity ## Default: infinity overall_max_connections: infinity @@ -846,7 +846,7 @@ zone.default { ## is delivered to the subscriber. The mountpoint is a way that users can use ## to implement isolation of message routing between different listeners. ## - ## For example if a clientA subscribes to "t" with `zone..mqtt.mountpoint` + ## For example if a clientA subscribes to "t" with `zones..mqtt.mountpoint` ## set to "some_tenant", then the client accually subscribes to the topic ## "some_tenant/t". Similarly if another clientB (connected to the same listener ## with the clientA) send a message to topic "t", the message is accually route @@ -859,7 +859,7 @@ zone.default { ## - %c: clientid ## - %u: username ## - ## @doc zone..listeners..mountpoint + ## @doc zones..listeners..mountpoint ## ValueType: String ## Default: "" mountpoint: "" @@ -868,21 +868,21 @@ zone.default { ## TCP connection is established but MQTT CONNECT has not been ## received. ## - ## @doc zone..mqtt.idle_timeout + ## @doc zones..mqtt.idle_timeout ## ValueType: Duration | infinity ## Default: 15s idle_timeout: 15s ## Maximum MQTT packet size allowed. ## - ## @doc zone..mqtt.max_packet_size + ## @doc zones..mqtt.max_packet_size ## ValueType: Bytes | infinity ## Default: 1MB max_packet_size: 1MB ## Maximum length of MQTT clientId allowed. ## - ## @doc zone..mqtt.max_clientid_len + ## @doc zones..mqtt.max_clientid_len ## ValueType: Integer ## Range: [23, 65535] ## Default: 65535 @@ -890,7 +890,7 @@ zone.default { ## Maximum topic levels allowed. ## - ## @doc zone..mqtt.max_topic_levels + ## @doc zones..mqtt.max_topic_levels ## ValueType: Integer ## Range: [1, 65535] ## Default: 65535 @@ -898,14 +898,14 @@ zone.default { ## Maximum QoS allowed. ## - ## @doc zone..mqtt.max_qos_allowed + ## @doc zones..mqtt.max_qos_allowed ## ValueType: 0 | 1 | 2 ## Default: 2 max_qos_allowed: 2 ## Maximum Topic Alias, 0 means no topic alias supported. ## - ## @doc zone..mqtt.max_topic_alias + ## @doc zones..mqtt.max_topic_alias ## ValueType: Integer ## Range: [0, 65535] ## Default: 65535 @@ -913,35 +913,35 @@ zone.default { ## Whether the Server supports MQTT retained messages. ## - ## @doc zone..mqtt.retain_available + ## @doc zones..mqtt.retain_available ## ValueType: Boolean ## Default: true retain_available: true ## Whether the Server supports MQTT Wildcard Subscriptions ## - ## @doc zone..mqtt.wildcard_subscription + ## @doc zones..mqtt.wildcard_subscription ## ValueType: Boolean ## Default: true wildcard_subscription: true ## Whether the Server supports MQTT Shared Subscriptions. ## - ## @doc zone..mqtt.shared_subscription + ## @doc zones..mqtt.shared_subscription ## ValueType: Boolean ## Default: true shared_subscription: true ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) ## - ## @doc zone..mqtt.ignore_loop_deliver + ## @doc zones..mqtt.ignore_loop_deliver ## ValueType: Boolean ## Default: false ignore_loop_deliver: false ## Whether to parse the MQTT frame in strict mode ## - ## @doc zone..mqtt.strict_mode + ## @doc zones..mqtt.strict_mode ## ValueType: Boolean ## Default: false strict_mode: false @@ -950,14 +950,14 @@ zone.default { ## ## This feature is disabled if not set ## - ## @doc zone..mqtt.response_information + ## @doc zones..mqtt.response_information ## ValueType: String ## Default: "" response_information: "" ## Server Keep Alive of MQTT 5.0 ## - ## @doc zone..mqtt.server_keepalive + ## @doc zones..mqtt.server_keepalive ## ValueType: Number | disabled ## Default: disabled server_keepalive: disabled @@ -965,7 +965,7 @@ zone.default { ## The backoff for MQTT keepalive timeout. The broker will kick a connection out ## until 'Keepalive * backoff * 2' timeout. ## - ## @doc zone..mqtt.keepalive_backoff + ## @doc zones..mqtt.keepalive_backoff ## ValueType: Float ## Range: (0.5, 1] ## Default: 0.75 @@ -973,7 +973,7 @@ zone.default { ## Maximum number of subscriptions allowed. ## - ## @doc zone..mqtt.max_subscriptions + ## @doc zones..mqtt.max_subscriptions ## ValueType: Integer | infinity ## Range: [1, ) ## Default: infinity @@ -981,14 +981,14 @@ zone.default { ## Force to upgrade QoS according to subscription. ## - ## @doc zone..mqtt.upgrade_qos + ## @doc zones..mqtt.upgrade_qos ## ValueType: Boolean ## Default: false upgrade_qos: false ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. ## - ## @doc zone..mqtt.max_inflight + ## @doc zones..mqtt.max_inflight ## ValueType: Integer ## Range: [1, 65535] ## Default: 32 @@ -996,14 +996,14 @@ zone.default { ## Retry interval for QoS1/2 message delivering. ## - ## @doc zone..mqtt.retry_interval + ## @doc zones..mqtt.retry_interval ## ValueType: Duration ## Default: 30s retry_interval: 30s ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. ## - ## @doc zone..mqtt.max_awaiting_rel + ## @doc zones..mqtt.max_awaiting_rel ## ValueType: Integer | infinity ## Range: [1, ) ## Default: 100 @@ -1011,14 +1011,14 @@ zone.default { ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. ## - ## @doc zone..mqtt.await_rel_timeout + ## @doc zones..mqtt.await_rel_timeout ## ValueType: Duration ## Default: 300s await_rel_timeout: 300s ## Default session expiry interval for MQTT V3.1.1 connections. ## - ## @doc zone..mqtt.session_expiry_interval + ## @doc zones..mqtt.session_expiry_interval ## ValueType: Duration ## Default: 2h session_expiry_interval: 2h @@ -1026,7 +1026,7 @@ zone.default { ## Maximum queue length. Enqueued messages when persistent client disconnected, ## or inflight window is full. ## - ## @doc zone..mqtt.max_mqueue_len + ## @doc zones..mqtt.max_mqueue_len ## ValueType: Integer | infinity ## Range: [0, ) ## Default: 1000 @@ -1042,7 +1042,7 @@ zone.default { ## either highest or lowest priority depending on the configured ## value for mqtt.mqueue_default_priority ## - ## @doc zone..mqtt.mqueue_priorities + ## @doc zones..mqtt.mqueue_priorities ## ValueType: Array ## Examples: ## To configure "t/1" > "t/2" > "t/3": @@ -1052,21 +1052,21 @@ zone.default { ## Default to highest priority for topics not matching priority table ## - ## @doc zone..mqtt.mqueue_default_priority + ## @doc zones..mqtt.mqueue_default_priority ## ValueType: highest | lowest ## Default: highest mqueue_default_priority: highest ## Whether to enqueue QoS0 messages. ## - ## @doc zone..mqtt.mqueue_store_qos0 + ## @doc zones..mqtt.mqueue_store_qos0 ## ValueType: Boolean ## Default: true mqueue_store_qos0: true ## Whether use username replace client id ## - ## @doc zone..mqtt.use_username_as_clientid + ## @doc zones..mqtt.use_username_as_clientid ## ValueType: Boolean ## Default: false use_username_as_clientid: false @@ -1074,7 +1074,7 @@ zone.default { ## Use the CN, DN or CRT field from the client certificate as a username. ## Only works for SSL connection. ## - ## @doc zone..mqtt.peer_cert_as_username + ## @doc zones..mqtt.peer_cert_as_username ## ValueType: cn | dn | crt | disabled ## Default: disabled peer_cert_as_username: disabled @@ -1082,7 +1082,7 @@ zone.default { ## Use the CN, DN or CRT field from the client certificate as a clientid. ## Only works for SSL connection. ## - ## @doc zone..mqtt.peer_cert_as_clientid + ## @doc zones..mqtt.peer_cert_as_clientid ## ValueType: cn | dn | crt | disabled ## Default: disabled peer_cert_as_clientid: disabled @@ -1093,14 +1093,14 @@ zone.default { ## Enable ACL check. ## - ## @doc zone..acl.enable + ## @doc zones..acl.enable ## ValueType: Boolean ## Default: false enable: false ## The action when acl check reject current operation ## - ## @doc zone..acl.deny_action + ## @doc zones..acl.deny_action ## ValueType: ignore | disconnect ## Default: ignore deny_action: ignore @@ -1109,14 +1109,14 @@ zone.default { ## ## If enabled, ACLs roles for each client will be cached in the memory ## - ## @doc zone..acl.cache.enable + ## @doc zones..acl.cache.enable ## ValueType: Boolean ## Default: true cache.enable: true ## The maximum count of ACL entries can be cached for a client. ## - ## @doc zone..acl.cache.max_size + ## @doc zones..acl.cache.max_size ## ValueType: Integer ## Range: [0, 1048576] ## Default: 32 @@ -1124,7 +1124,7 @@ zone.default { ## The time after which an ACL cache entry will be deleted ## - ## @doc zone..acl.cache.ttl + ## @doc zones..acl.cache.ttl ## ValueType: Duration ## Default: 1m cache.ttl: 1m @@ -1138,28 +1138,28 @@ zone.default { ## After the limit is reached, successive CONNECT requests are forbidden ## (banned) until the end of the time period defined by `ban_time`. ## - ## @doc zone..flapping_detect.enable + ## @doc zones..flapping_detect.enable ## ValueType: Boolean ## Default: true enable: true ## The max disconnect allowed of a MQTT Client in `window_time` ## - ## @doc zone..flapping_detect.max_count + ## @doc zones..flapping_detect.max_count ## ValueType: Integer ## Default: 15 max_count: 15 ## The time window for flapping detect ## - ## @doc zone..flapping_detect.window_time + ## @doc zones..flapping_detect.window_time ## ValueType: Duration ## Default: 1m window_time: 1m ## How long the clientid will be banned ## - ## @doc zone..flapping_detect.ban_time + ## @doc zones..flapping_detect.ban_time ## ValueType: Duration ## Default: 5m ban_time: 5m @@ -1169,13 +1169,13 @@ zone.default { force_shutdown: { ## Enable force_shutdown ## - ## @doc zone..force_shutdown.enable + ## @doc zones..force_shutdown.enable ## ValueType: Boolean ## Default: true enable: true ## Max message queue length - ## @doc zone..force_shutdown.max_message_queue_len + ## @doc zones..force_shutdown.max_message_queue_len ## ValueType: Integer ## Range: (0, ) ## Default: 1000 @@ -1183,7 +1183,7 @@ zone.default { ## Total heap size ## - ## @doc zone..force_shutdown.max_heap_size + ## @doc zones..force_shutdown.max_heap_size ## ValueType: Size ## Default: 32MB max_heap_size: 32MB @@ -1193,13 +1193,13 @@ zone.default { ## Force the MQTT connection process GC after this number of ## messages or bytes passed through. ## - ## @doc zone..force_gc.enable + ## @doc zones..force_gc.enable ## ValueType: Boolean ## Default: true enable: true ## GC the process after how many messages received - ## @doc zone..force_gc.max_message_queue_len + ## @doc zones..force_gc.max_message_queue_len ## ValueType: Integer ## Range: (0, ) ## Default: 16000 @@ -1207,7 +1207,7 @@ zone.default { ## GC the process after how much bytes passed through ## - ## @doc zone..force_gc.bytes + ## @doc zones..force_gc.bytes ## ValueType: Size ## Default: 16MB bytes: 16MB @@ -1231,7 +1231,7 @@ zone.default { ## Where the is the client-id of the congested MQTT connection. ## And the is the username or "unknown_user" of not provided by the client. ## - ## @doc zone..conn_congestion.enable_alarm + ## @doc zones..conn_congestion.enable_alarm ## ValueType: Boolean ## Default: true enable_alarm: true @@ -1243,7 +1243,7 @@ zone.default { ## ## This is to avoid clearing and sending the alarm again too often. ## - ## @doc zone..conn_congestion.min_alarm_sustain_duration + ## @doc zones..conn_congestion.min_alarm_sustain_duration ## ValueType: Duration ## Default: 1m min_alarm_sustain_duration: 1m @@ -1256,7 +1256,7 @@ zone.default { ## The type of the listener. ## - ## @doc zone..listeners..type + ## @doc zones..listeners..type ## ValueType: tcp | ws ## - tcp: MQTT over TCP ## - ws: MQTT over Websocket @@ -1265,7 +1265,7 @@ zone.default { ## The IP address and port that the listener will bind. ## - ## @doc zone..listeners..bind + ## @doc zones..listeners..bind ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 1883, 127.0.0.1:1883, ::1:1883 @@ -1273,14 +1273,14 @@ zone.default { ## The size of the acceptor pool for this listener. ## - ## @doc zone..listeners..acceptors + ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 acceptors: 16 ## Maximum number of concurrent connections. ## - ## @doc zone..listeners..max_connections + ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity max_connections: 1024000 @@ -1289,7 +1289,7 @@ zone.default { ## ## See: https://github.com/emqtt/esockd#allowdeny ## - ## @doc zone..listeners..access_rules + ## @doc zones..listeners..access_rules ## ValueType: Array ## Default: [] ## Examples: @@ -1306,7 +1306,7 @@ zone.default { ## ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ ## - ## @doc zone..listeners..proxy_protocol + ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: false proxy_protocol: false @@ -1314,7 +1314,7 @@ zone.default { ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## - ## @doc zone..listeners..proxy_protocol_timeout + ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s proxy_protocol_timeout: 3s @@ -1322,7 +1322,7 @@ zone.default { rate_limit { ## Maximum connections per second. ## - ## @doc zone..max_conn_rate + ## @doc zones..max_conn_rate ## ValueType: Number | infinity ## Default: 1000 ## Examples: @@ -1331,7 +1331,7 @@ zone.default { ## Message limit for the a external MQTT connection. ## - ## @doc zone..rate_limit.conn_messages_in + ## @doc zones..rate_limit.conn_messages_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messages per 10 seconds. @@ -1344,7 +1344,7 @@ zone.default { ## The connection won't accept more messages if the messages come ## faster than the limit. ## - ## @doc zone..rate_limit.conn_bytes_in + ## @doc zones..rate_limit.conn_bytes_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100KB incoming per 10 seconds. @@ -1355,7 +1355,7 @@ zone.default { ## Messages quota for the each of external MQTT connection. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.conn_messages_routing + ## @doc zones..rate_limit.quota.conn_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messaegs per 1s: @@ -1365,7 +1365,7 @@ zone.default { ## Messages quota for the all of external MQTT connections. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.overall_messages_routing + ## @doc zones..rate_limit.quota.overall_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 200000 messages per 1s: @@ -1387,7 +1387,7 @@ zone.default { ## The type of the listener. ## - ## @doc zone..listeners..type + ## @doc zones..listeners..type ## ValueType: tcp | ws ## - tcp: MQTT over TCP ## - ws: MQTT over Websocket @@ -1396,7 +1396,7 @@ zone.default { ## The IP address and port that the listener will bind. ## - ## @doc zone..listeners..bind + ## @doc zones..listeners..bind ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 8883, 127.0.0.1:8883, ::1:8883 @@ -1404,14 +1404,14 @@ zone.default { ## The size of the acceptor pool for this listener. ## - ## @doc zone..listeners..acceptors + ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 acceptors: 16 ## Maximum number of concurrent connections. ## - ## @doc zone..listeners..max_connections + ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity max_connections: 512000 @@ -1420,7 +1420,7 @@ zone.default { ## ## See: https://github.com/emqtt/esockd#allowdeny ## - ## @doc zone..listeners..access_rules + ## @doc zones..listeners..access_rules ## ValueType: Array ## Default: [] ## Examples: @@ -1437,15 +1437,15 @@ zone.default { ## ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ ## - ## @doc zone..listeners..proxy_protocol + ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: true - proxy_protocol: true + proxy_protocol: false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## - ## @doc zone..listeners..proxy_protocol_timeout + ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s proxy_protocol_timeout: 3s @@ -1453,7 +1453,7 @@ zone.default { rate_limit { ## Maximum connections per second. ## - ## @doc zone..max_conn_rate + ## @doc zones..max_conn_rate ## ValueType: Number | infinity ## Default: 1000 ## Examples: @@ -1462,7 +1462,7 @@ zone.default { ## Message limit for the a external MQTT connection. ## - ## @doc zone..rate_limit.conn_messages_in + ## @doc zones..rate_limit.conn_messages_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messages per 10 seconds. @@ -1475,7 +1475,7 @@ zone.default { ## The connection won't accept more messages if the messages come ## faster than the limit. ## - ## @doc zone..rate_limit.conn_bytes_in + ## @doc zones..rate_limit.conn_bytes_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100KB incoming per 10 seconds. @@ -1486,7 +1486,7 @@ zone.default { ## Messages quota for the each of external MQTT connection. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.conn_messages_routing + ## @doc zones..rate_limit.quota.conn_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messaegs per 1s: @@ -1496,7 +1496,7 @@ zone.default { ## Messages quota for the all of external MQTT connections. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.overall_messages_routing + ## @doc zones..rate_limit.quota.overall_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 200000 messages per 1s: @@ -1508,6 +1508,7 @@ zone.default { ## SSL options ## See ${example_common_ssl_options} for more information ssl.enable: true + ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" @@ -1524,7 +1525,7 @@ zone.default { ## The type of the listener. ## - ## @doc zone..listeners..type + ## @doc zones..listeners..type ## ValueType: tcp | ws ## - tcp: MQTT over TCP ## - ws: MQTT over Websocket @@ -1533,7 +1534,7 @@ zone.default { ## The IP address and port that the listener will bind. ## - ## @doc zone..listeners..bind + ## @doc zones..listeners..bind ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 @@ -1541,14 +1542,14 @@ zone.default { ## The size of the acceptor pool for this listener. ## - ## @doc zone..listeners..acceptors + ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 acceptors: 16 ## Maximum number of concurrent connections. ## - ## @doc zone..listeners..max_connections + ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity max_connections: 1024000 @@ -1557,7 +1558,7 @@ zone.default { ## ## See: https://github.com/emqtt/esockd#allowdeny ## - ## @doc zone..listeners..access_rules + ## @doc zones..listeners..access_rules ## ValueType: Array ## Default: [] ## Examples: @@ -1574,15 +1575,15 @@ zone.default { ## ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ ## - ## @doc zone..listeners..proxy_protocol + ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: true - proxy_protocol: true + proxy_protocol: false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## - ## @doc zone..listeners..proxy_protocol_timeout + ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s proxy_protocol_timeout: 3s @@ -1590,7 +1591,7 @@ zone.default { rate_limit { ## Maximum connections per second. ## - ## @doc zone..max_conn_rate + ## @doc zones..max_conn_rate ## ValueType: Number | infinity ## Default: 1000 ## Examples: @@ -1599,7 +1600,7 @@ zone.default { ## Message limit for the a external MQTT connection. ## - ## @doc zone..rate_limit.conn_messages_in + ## @doc zones..rate_limit.conn_messages_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messages per 10 seconds. @@ -1612,7 +1613,7 @@ zone.default { ## The connection won't accept more messages if the messages come ## faster than the limit. ## - ## @doc zone..rate_limit.conn_bytes_in + ## @doc zones..rate_limit.conn_bytes_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100KB incoming per 10 seconds. @@ -1623,7 +1624,7 @@ zone.default { ## Messages quota for the each of external MQTT connection. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.conn_messages_routing + ## @doc zones..rate_limit.quota.conn_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messaegs per 1s: @@ -1633,7 +1634,7 @@ zone.default { ## Messages quota for the all of external MQTT connections. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.overall_messages_routing + ## @doc zones..rate_limit.quota.overall_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 200000 messages per 1s: @@ -1658,7 +1659,7 @@ zone.default { ## The type of the listener. ## - ## @doc zone..listeners..type + ## @doc zones..listeners..type ## ValueType: tcp | ws ## - tcp: MQTT over TCP ## - ws: MQTT over Websocket @@ -1667,7 +1668,7 @@ zone.default { ## The IP address and port that the listener will bind. ## - ## @doc zone..listeners..bind + ## @doc zones..listeners..bind ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 @@ -1675,14 +1676,14 @@ zone.default { ## The size of the acceptor pool for this listener. ## - ## @doc zone..listeners..acceptors + ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 acceptors: 16 ## Maximum number of concurrent connections. ## - ## @doc zone..listeners..max_connections + ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity max_connections: 512000 @@ -1691,7 +1692,7 @@ zone.default { ## ## See: https://github.com/emqtt/esockd#allowdeny ## - ## @doc zone..listeners..access_rules + ## @doc zones..listeners..access_rules ## ValueType: Array ## Default: [] ## Examples: @@ -1708,15 +1709,15 @@ zone.default { ## ## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ ## - ## @doc zone..listeners..proxy_protocol + ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: true - proxy_protocol: true + proxy_protocol: false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection ## if no proxy protocol packet recevied within the timeout. ## - ## @doc zone..listeners..proxy_protocol_timeout + ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s proxy_protocol_timeout: 3s @@ -1724,7 +1725,7 @@ zone.default { rate_limit { ## Maximum connections per second. ## - ## @doc zone..max_conn_rate + ## @doc zones..max_conn_rate ## ValueType: Number | infinity ## Default: 1000 ## Examples: @@ -1733,7 +1734,7 @@ zone.default { ## Message limit for the a external MQTT connection. ## - ## @doc zone..rate_limit.conn_messages_in + ## @doc zones..rate_limit.conn_messages_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messages per 10 seconds. @@ -1746,7 +1747,7 @@ zone.default { ## The connection won't accept more messages if the messages come ## faster than the limit. ## - ## @doc zone..rate_limit.conn_bytes_in + ## @doc zones..rate_limit.conn_bytes_in ## ValueType: String | infinity ## Default: infinity ## Examples: 100KB incoming per 10 seconds. @@ -1757,7 +1758,7 @@ zone.default { ## Messages quota for the each of external MQTT connection. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.conn_messages_routing + ## @doc zones..rate_limit.quota.conn_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 100 messaegs per 1s: @@ -1767,7 +1768,7 @@ zone.default { ## Messages quota for the all of external MQTT connections. ## This value consumed by the number of recipient on a message. ## - ## @doc zone..rate_limit.quota.overall_messages_routing + ## @doc zones..rate_limit.quota.overall_messages_routing ## ValueType: String | infinity ## Default: infinity ## Examples: 200000 messages per 1s: @@ -1797,7 +1798,7 @@ zone.default { #This is an example zone which has less "strict" settings. #It's useful to clients connecting the broker from trusted networks. -zone.internal { +zones.internal { acl.enable: false auth.enable: false listeners.mqtt_internal: { @@ -1805,7 +1806,7 @@ zone.internal { bind: "127.0.0.1:11883" acceptors: 4 max_connections: 1024000 - tcp.active_n: 1000 + tcp.active: 1000 tcp.backlog: 512 } } @@ -1958,10 +1959,10 @@ example_common_tcp_options { ## ## See: https://erlang.org/doc/man/inet.html#setopts-2 ## - ## @doc listeners..tcp.active_n + ## @doc listeners..tcp.active ## ValueType: Number ## Default: 100 - tcp.active_n: 100 + tcp.active: 100 ## TCP backlog defines the maximum length that the queue of ## pending connections can grow to. @@ -2072,10 +2073,10 @@ example_common_ssl_options { ## TLS versions only to protect from POODLE attack. ## - ## @doc listeners..ssl.tls_versions + ## @doc listeners..ssl.versions ## ValueType: Array - ## Default: [tlsv1.2,tlsv1.1,tlsv1] - ssl.tls_versions: [tlsv1.2,tlsv1.1,tlsv1] + ## Default: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] ## TLS Handshake timeout. ## @@ -2189,17 +2190,9 @@ example_common_ssl_options { ## ## @doc listeners..ssl.ciphers ## ValueType: Array - ## Default: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA] - ssl.ciphers: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA] + ## Default: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA,PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] + ssl.ciphers: [ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA,PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] - ## Ciphers for TLS PSK. See 'https://tools.ietf.org/html/rfc4279#section-2'. - ## - ## Note that 'ciphers' and 'psk_ciphers' cannot be configured at the same time. - ## - ## @doc listeners..ssl.psk_ciphers - ## ValueType: Array - ## Default: [PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] - ssl.psk_ciphers: [PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] } ## Socket options for websocket connections diff --git a/apps/emqx/etc/emqx.conf.old b/apps/emqx/etc/emqx.conf.old index d2b5fd11d..862f9d78f 100644 --- a/apps/emqx/etc/emqx.conf.old +++ b/apps/emqx/etc/emqx.conf.old @@ -1107,7 +1107,7 @@ listener.tcp.external.max_conn_rate = 1000 ## Specify the {active, N} option for the external MQTT/TCP Socket. ## ## Value: Number -listener.tcp.external.active_n = 100 +listener.tcp.external.active = 100 ## Zone of the external MQTT/TCP listener belonged to. ## @@ -1247,7 +1247,7 @@ listener.tcp.internal.max_conn_rate = 1000 ## Specify the {active, N} option for the internal MQTT/TCP Socket. ## ## Value: Number -listener.tcp.internal.active_n = 1000 +listener.tcp.internal.active = 1000 ## Zone of the internal MQTT/TCP listener belonged to. ## @@ -1344,7 +1344,7 @@ listener.ssl.external.max_conn_rate = 500 ## Specify the {active, N} option for the internal MQTT/SSL Socket. ## ## Value: Number -listener.ssl.external.active_n = 100 +listener.ssl.external.active = 100 ## Zone of the external MQTT/SSL listener belonged to. ## @@ -1610,7 +1610,7 @@ listener.ws.external.max_conn_rate = 1000 ## Simulate the {active, N} option for the MQTT/WebSocket connections. ## ## Value: Number -listener.ws.external.active_n = 100 +listener.ws.external.active = 100 ## Zone of the external MQTT/WebSocket listener belonged to. ## @@ -1879,7 +1879,7 @@ listener.wss.external.max_conn_rate = 1000 ## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. ## ## Value: Number -listener.wss.external.active_n = 100 +listener.wss.external.active = 100 ## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index ab91c02b4..3363b013e 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -84,7 +84,7 @@ %% Sock State sockstate :: emqx_types:sockstate(), %% The {active, N} option - active_n :: pos_integer(), + active :: pos_integer(), %% Limiter limiter :: maybe(emqx_limiter:limiter()), %% Limit Timer @@ -108,7 +108,7 @@ -type(state() :: #state{}). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -165,7 +165,7 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active_n, #state{active_n = ActiveN}) -> +info(active, #state{active = ActiveN}) -> ActiveN; info(stats_timer, #state{stats_timer = StatsTimer}) -> StatsTimer; @@ -254,7 +254,7 @@ init_state(Transport, Socket, Options) -> conn_mod => ?MODULE }, Zone = proplists:get_value(zone, Options), - ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), + ActiveN = proplists:get_value(active, Options, ?ACTIVE_N), PubLimit = emqx_zone:publish_limit(Zone), BytesIn = proplists:get_value(rate_limit, Options), RateLimit = emqx_zone:ratelimit(Zone), @@ -272,7 +272,7 @@ init_state(Transport, Socket, Options) -> peername = Peername, sockname = Sockname, sockstate = idle, - active_n = ActiveN, + active = ActiveN, limiter = Limiter, parse_state = ParseState, serialize = Serialize, @@ -452,12 +452,12 @@ handle_msg({Passive, _Sock}, State) handle_info(activate_socket, NState1); handle_msg(Deliver = {deliver, _Topic, _Msg}, - #state{active_n = ActiveN} = State) -> + #state{active = ActiveN} = State) -> Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent -handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> +handle_msg({inet_reply, _Sock, ok}, State = #state{active = ActiveN}) -> case emqx_pd:get_counter(outgoing_pubs) > ActiveN of true -> Pubs = emqx_pd:reset_counter(outgoing_pubs), @@ -800,7 +800,7 @@ activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{transport = Transport, socket = Socket, - active_n = N}) -> + active = N}) -> case Transport:setopts(Socket, [{active, N}]) of ok -> {ok, State#state{sockstate = running}}; Error -> Error diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 1f3d1776b..e3c6a5e22 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -21,7 +21,6 @@ %% APIs -export([ start/0 - , ensure_all_started/0 , restart/0 , stop/0 ]). @@ -29,88 +28,35 @@ -export([ start_listener/1 , start_listener/3 , stop_listener/1 + , stop_listener/3 , restart_listener/1 , restart_listener/3 ]). --export([ find_id_by_listen_on/1 - , find_by_listen_on/1 - , find_by_id/1 - , identifier/1 - , format_listen_on/1 - ]). - --type(listener() :: #{ name := binary() - , proto := esockd:proto() - , listen_on := esockd:listen_on() - , opts := [esockd:option()] - }). - -%% @doc Find listener identifier by listen-on. -%% Return empty string (binary) if listener is not found in config. --spec(find_id_by_listen_on(esockd:listen_on()) -> binary() | false). -find_id_by_listen_on(ListenOn) -> - case find_by_listen_on(ListenOn) of - false -> false; - L -> identifier(L) - end. - -%% @doc Find listener by listen-on. -%% Return 'false' if not found. --spec(find_by_listen_on(esockd:listen_on()) -> listener() | false). -find_by_listen_on(ListenOn) -> - find_by_listen_on(ListenOn, emqx:get_env(listeners, [])). - -%% @doc Find listener by identifier. -%% Return 'false' if not found. --spec(find_by_id(string() | binary()) -> listener() | false). -find_by_id(Id) -> - find_by_id(iolist_to_binary(Id), emqx:get_env(listeners, [])). - -%% @doc Return the ID of the given listener. --spec identifier(listener()) -> binary(). -identifier(#{proto := Proto, name := Name}) -> - identifier(Proto, Name). - %% @doc Start all listeners. -spec(start() -> ok). start() -> - lists:foreach(fun start_listener/1, emqx:get_env(listeners, [])). + lists:foreach(fun({ZoneName, ZoneConf}) -> + lists:foreach(fun({LName, LConf}) -> + start_listener(ZoneName, LName, LConf) + end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) + end, maps:to_list(emqx_config:get([zones], #{}))). -%% @doc Ensure all configured listeners are started. -%% Raise exception if any of them failed to start. --spec(ensure_all_started() -> ok). -ensure_all_started() -> - ensure_all_started(emqx:get_env(listeners, []), []). +-spec(start_listener(atom()) -> ok). +start_listener(Id) -> + {ZoneName, ListenerName} = decode_listener_id(Id), + start_listener(ZoneName, ListenerName, + emqx_config:get([zones, ZoneName, listeners, ListenerName])). -ensure_all_started([], []) -> ok; -ensure_all_started([], Failed) -> error(Failed); -ensure_all_started([L | Rest], Results) -> - #{proto := Proto, listen_on := ListenOn, opts := Options} = L, - NewResults = - case start_listener(Proto, ListenOn, Options) of - {ok, _Pid} -> - Results; - {error, {already_started, _Pid}} -> - Results; - {error, Reason} -> - [{identifier(L), Reason} | Results] - end, - ensure_all_started(Rest, NewResults). - -%% @doc Format address:port for logging. --spec(format_listen_on(esockd:listen_on()) -> [char()]). -format_listen_on(ListenOn) -> format(ListenOn). - --spec(start_listener(listener()) -> ok). -start_listener(#{proto := Proto, name := Name, listen_on := ListenOn, opts := Options}) -> - ID = identifier(Proto, Name), - case start_listener(Proto, ListenOn, Options) of +-spec(start_listener(atom(), atom(), map()) -> ok). +start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) -> + case do_start_listener(ZoneName, ListenerName, Conf) of {ok, _} -> - console_print("Start ~s listener on ~s successfully.~n", [ID, format(ListenOn)]); + console_print("Start ~s listener ~s on ~s successfully.~n", + [Type, listener_id(ZoneName, ListenerName), format(Bind)]); {error, Reason} -> - io:format(standard_error, "Failed to start mqtt listener ~s on ~s: ~0p~n", - [ID, format(ListenOn), Reason]), + io:format(standard_error, "Failed to start ~s listener ~s on ~s: ~0p~n", + [Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]), error(Reason) end. @@ -122,124 +68,105 @@ console_print(_Fmt, _Args) -> ok. -endif. %% Start MQTT/TCP listener --spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) +-spec(do_start_listener(atom(), atom(), map()) -> {ok, pid()} | {error, term()}). -start_listener(tcp, ListenOn, Options) -> - start_mqtt_listener('mqtt:tcp', ListenOn, Options); - -%% Start MQTT/TLS listener -start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> - start_mqtt_listener('mqtt:ssl', ListenOn, Options); +do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opts) -> + esockd:open(listener_id(ZoneName, ListenerName), ListenOn, merge_default(esockd_opts(Opts)), + {emqx_connection, start_link, [ZoneName, ListenerName]}); %% Start MQTT/WS listener -start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, - ranch_opts(Options), ws_opts(Options)); - -%% Start MQTT/WSS listener -start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, - ranch_opts(Options), ws_opts(Options)). - -replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. - -drop_tls13_for_old_otp(Options) -> - case proplists:get_value(ssl_options, Options) of - undefined -> Options; - SslOpts -> - SslOpts1 = emqx_tls_lib:drop_tls13_for_old_otp(SslOpts), - replace(Options, ssl_options, SslOpts1) +do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) -> + Id = listener_id(ZoneName, ListenerName), + RanchOpts = ranch_opts(Opts), + WsOpts = ws_opts(ZoneName, ListenerName, Opts), + case is_ssl(Opts) of + false -> + cowboy:start_clear(Id, with_port(ListenOn, RanchOpts), WsOpts); + true -> + cowboy:start_tls(Id, with_port(ListenOn, RanchOpts), WsOpts) end. -start_mqtt_listener(Name, ListenOn, Options0) -> - Options = drop_tls13_for_old_otp(Options0), - SockOpts = esockd:parse_opt(Options), - esockd:open(Name, ListenOn, merge_default(SockOpts), - {emqx_connection, start_link, [Options -- SockOpts]}). +esockd_opts(Opts0) -> + Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), + Opts2 = case emqx_config:deep_get([rate_limit, max_conn_rate], Opts0) of + infinity -> Opts1; + Rate -> Opts1#{max_conn_rate => Rate} + end, + Opts3 = Opts2#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))}, + maps:to_list(Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)}). -start_http_listener(Start, Name, ListenOn, RanchOpts, ProtoOpts) -> - Start(ws_name(Name, ListenOn), with_port(ListenOn, RanchOpts), ProtoOpts). - -mqtt_path(Options) -> - proplists:get_value(mqtt_path, Options, "/mqtt"). - -ws_opts(Options) -> - WsPaths = [{mqtt_path(Options), emqx_ws_connection, Options}], +ws_opts(ZoneName, ListenerName, Opts) -> + WsPaths = [{maps:get(mqtt_path, Opts, "/mqtt"), emqx_ws_connection, + #{zone => ZoneName, listener => ListenerName}}], Dispatch = cowboy_router:compile([{'_', WsPaths}]), - ProxyProto = proplists:get_value(proxy_protocol, Options, false), + ProxyProto = maps:get(proxy_protocol, Opts, false), #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. -ranch_opts(Options0) -> - Options = drop_tls13_for_old_otp(Options0), - NumAcceptors = proplists:get_value(acceptors, Options, 4), - MaxConnections = proplists:get_value(max_connections, Options, 1024), - TcpOptions = proplists:get_value(tcp_options, Options, []), - RanchOpts = #{num_acceptors => NumAcceptors, - max_connections => MaxConnections, - socket_opts => TcpOptions}, - case proplists:get_value(ssl_options, Options) of - undefined -> RanchOpts; - SslOptions -> RanchOpts#{socket_opts => TcpOptions ++ SslOptions} - end. +ranch_opts(Opts) -> + NumAcceptors = maps:get(acceptors, Opts, 4), + MaxConnections = maps:get(max_connections, Opts, 1024), + #{num_acceptors => NumAcceptors, + max_connections => MaxConnections, + handshake_timeout => maps:get(handshake_timeout, Opts, 15000), + socket_opts => case is_ssl(Opts) of + true -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); + false -> tcp_opts(Opts) + end}. with_port(Port, Opts = #{socket_opts := SocketOption}) when is_integer(Port) -> Opts#{socket_opts => [{port, Port}| SocketOption]}; with_port({Addr, Port}, Opts = #{socket_opts := SocketOption}) -> Opts#{socket_opts => [{ip, Addr}, {port, Port}| SocketOption]}. +esockd_access_rules(StrRules) -> + Access = fun(S) -> + [A, CIDR] = string:tokens(S, " "), + {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} + end, + [Access(R) || R <- StrRules]. + %% @doc Restart all listeners -spec(restart() -> ok). restart() -> - lists:foreach(fun restart_listener/1, emqx:get_env(listeners, [])). + lists:foreach(fun({ZoneName, ZoneConf}) -> + lists:foreach(fun({LName, LConf}) -> + restart_listener(ZoneName, LName, LConf) + end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) + end, maps:to_list(emqx_config:get([zones], #{}))). --spec(restart_listener(listener() | string() | binary()) -> ok | {error, any()}). -restart_listener(#{proto := Proto, listen_on := ListenOn, opts := Options}) -> - restart_listener(Proto, ListenOn, Options); -restart_listener(Identifier) -> - case emqx_listeners:find_by_id(Identifier) of - false -> {error, {no_such_listener, Identifier}}; - Listener -> restart_listener(Listener) +-spec(restart_listener(atom()) -> ok | {error, any()}). +restart_listener(ListenerID) -> + {ZoneName, ListenerName} = decode_listener_id(ListenerID), + restart_listener(ZoneName, ListenerName, + emqx_config:get([zones, ZoneName, listeners, ListenerName])). + +-spec(restart_listener(atom(), atom(), map()) -> ok | {error, any()}). +restart_listener(ZoneName, ListenerName, Conf) -> + case stop_listener(ZoneName, ListenerName, Conf) of + ok -> start_listener(ZoneName, ListenerName, Conf); + Error -> Error end. --spec(restart_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) -> - ok | {error, any()}). -restart_listener(tcp, ListenOn, _Options) -> - esockd:reopen('mqtt:tcp', ListenOn); -restart_listener(Proto, ListenOn, _Options) when Proto == ssl; Proto == tls -> - esockd:reopen('mqtt:ssl', ListenOn); -restart_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - _ = cowboy:stop_listener(ws_name('mqtt:ws', ListenOn)), - ok(start_listener(Proto, ListenOn, Options)); -restart_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - _ = cowboy:stop_listener(ws_name('mqtt:wss', ListenOn)), - ok(start_listener(Proto, ListenOn, Options)); -restart_listener(Proto, ListenOn, _Opts) -> - esockd:reopen(Proto, ListenOn). - -ok({ok, _}) -> ok; -ok(Other) -> Other. - %% @doc Stop all listeners. -spec(stop() -> ok). stop() -> - lists:foreach(fun stop_listener/1, emqx:get_env(listeners, [])). + lists:foreach(fun({ZoneName, ZoneConf}) -> + lists:foreach(fun({LName, LConf}) -> + stop_listener(ZoneName, LName, LConf) + end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) + end, maps:to_list(emqx_config:get([zones], #{}))). --spec(stop_listener(listener()) -> ok | {error, term()}). -stop_listener(#{proto := Proto, listen_on := ListenOn, opts := Opts}) -> - stop_listener(Proto, ListenOn, Opts). +-spec(stop_listener(atom()) -> ok | {error, term()}). +stop_listener(ListenerID) -> + {ZoneName, ListenerName} = decode_listener_id(ListenerID), + stop_listener(ZoneName, ListenerName, + emqx_config:get([zones, ZoneName, listeners, ListenerName])). --spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) - -> ok | {error, term()}). -stop_listener(tcp, ListenOn, _Opts) -> - esockd:close('mqtt:tcp', ListenOn); -stop_listener(Proto, ListenOn, _Opts) when Proto == ssl; Proto == tls -> - esockd:close('mqtt:ssl', ListenOn); -stop_listener(Proto, ListenOn, _Opts) when Proto == http; Proto == ws -> - cowboy:stop_listener(ws_name('mqtt:ws', ListenOn)); -stop_listener(Proto, ListenOn, _Opts) when Proto == https; Proto == wss -> - cowboy:stop_listener(ws_name('mqtt:wss', ListenOn)); -stop_listener(Proto, ListenOn, _Opts) -> - esockd:close(Proto, ListenOn). +-spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). +stop_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn}) -> + esockd:close(listener_id(ZoneName, ListenerName), ListenOn); +stop_listener(ZoneName, ListenerName, #{type := ws}) -> + cowboy:stop_listener(listener_id(ZoneName, ListenerName)). merge_default(Options) -> case lists:keytake(tcp_options, 1, Options) of @@ -256,23 +183,23 @@ format({Addr, Port}) when is_list(Addr) -> format({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). -ws_name(Name, {_Addr, Port}) -> - ws_name(Name, Port); -ws_name(Name, Port) -> - list_to_atom(lists:concat([Name, ":", Port])). +listener_id(ZoneName, ListenerName) -> + list_to_atom(lists:append([atom_to_list(ZoneName), ":", atom_to_list(ListenerName)])). -identifier(Proto, Name) when is_atom(Proto) -> - identifier(atom_to_list(Proto), Name); -identifier(Proto, Name) -> - iolist_to_binary(["mqtt", ":", Proto, ":", Name]). - -find_by_listen_on(_ListenOn, []) -> false; -find_by_listen_on(ListenOn, [#{listen_on := ListenOn} = L | _]) -> L; -find_by_listen_on(ListenOn, [_ | Rest]) -> find_by_listen_on(ListenOn, Rest). - -find_by_id(_Id, []) -> false; -find_by_id(Id, [L | Rest]) -> - case identifier(L) =:= Id of - true -> L; - false -> find_by_id(Id, Rest) +decode_listener_id(Id) -> + case string:split(atom_to_list(Id), ":", leading) of + [Zone, Listen] -> {list_to_atom(Zone), list_to_atom(Listen)}; + _ -> error({invalid_listener_id, Id}) end. + +ssl_opts(Opts) -> + maps:to_list( + emqx_tls_lib:drop_tls13_for_old_otp( + maps:without([enable], + maps:get(ssl, Opts, #{})))). + +tcp_opts(Opts) -> + maps:to_list(maps:get(tcp, Opts, #{})). + +is_ssl(Opts) -> + emqx_config:deep_get([ssl, enable], Opts, false). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index f4900d36b..5a6044307 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -20,6 +20,7 @@ -type comma_separated_atoms() :: [atom()]. -type bar_separated_list() :: list(). -type ip_port() :: tuple(). +-type cipher() :: map(). -typerefl_from_string({duration/0, emqx_schema, to_duration}). -typerefl_from_string({duration_s/0, emqx_schema, to_duration_s}). @@ -30,6 +31,7 @@ -typerefl_from_string({comma_separated_list/0, emqx_schema, to_comma_separated_list}). -typerefl_from_string({bar_separated_list/0, emqx_schema, to_bar_separated_list}). -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({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). % workaround: prevent being recognized as unused functions @@ -37,6 +39,7 @@ to_bytesize/1, to_wordsize/1, to_percent/1, to_comma_separated_list/1, to_bar_separated_list/1, to_ip_port/1, + to_erl_cipher_suite/1, to_comma_separated_atoms/1]). -behaviour(hocon_schema). @@ -44,18 +47,19 @@ -reflect_type([ log_level/0, duration/0, duration_s/0, duration_ms/0, bytesize/0, wordsize/0, percent/0, file/0, comma_separated_list/0, bar_separated_list/0, ip_port/0, + cipher/0, comma_separated_atoms/0]). -export([structs/0, fields/1, translations/0, translation/1]). -export([t/1, t/3, t/4, ref/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). --export([ssl/1, tr_ssl/2, tr_password_hash/2]). +-export([ssl/1]). %% will be used by emqx_ct_helper to find the dependent apps -export([includes/0]). structs() -> ["cluster", "node", "rpc", "log", "lager", - "acl", "mqtt", "zone", "listeners", "module", "broker", + "acl", "mqtt", "zones", "listeners", "module", "broker", "plugins", "sysmon", "alarm", "telemetry"] ++ includes(). @@ -271,7 +275,7 @@ fields("mqtt") -> , {"peer_cert_as_clientid", maybe_disabled(union([cn, dn, crt, pem, md5]))} ]; -fields("zone") -> +fields("zones") -> [ {"$name", ref("zone_settings")}]; fields("zone_settings") -> @@ -368,14 +372,14 @@ fields("ws_opts") -> "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} , {"check_origin_enable", t(boolean(), undefined, false)} , {"allow_origin_absence", t(boolean(), undefined, true)} - , {"check_origins", t(comma_separated_list())} + , {"check_origins", t(hoconsc:array(binary()), undefined, [])} , {"proxy_address_header", t(string(), undefined, "x-forwarded-for")} , {"proxy_port_header", t(string(), undefined, "x-forwarded-port")} , {"deflate_opts", ref("deflate_opts")} ]; fields("tcp_opts") -> - [ {"active_n", t(integer(), undefined, 100)} + [ {"active", t(integer(), undefined, 100)} , {"backlog", t(integer(), undefined, 1024)} , {"send_timeout", t(duration(), undefined, "15s")} , {"send_timeout_close", t(boolean(), undefined, true)} @@ -391,7 +395,9 @@ fields("tcp_opts") -> fields("ssl_opts") -> ssl(#{handshake_timeout => "15s" , depth => 10 - , reuse_sessions => true}); + , reuse_sessions => true + , versions => default_tls_vsns() + }); fields("deflate_opts") -> [ {"level", t(union([none, default, best_compression, best_speed]))} @@ -643,72 +649,19 @@ ssl(Defaults) -> , {"dhfile", t(string(), undefined, D("dhfile"))} , {"server_name_indication", t(union(disable, string()), undefined, D("server_name_indication"))} - , {"tls_versions", t(comma_separated_list(), undefined, D("tls_versions"))} - , {"ciphers", t(comma_separated_list(), undefined, D("ciphers"))} - , {"psk_ciphers", t(comma_separated_list(), undefined, D("ciphers"))}]. - -tr_ssl(Field, Conf) -> - Versions = case conf_get([Field, "tls_versions"], Conf) of - undefined -> undefined; - Vs -> [list_to_existing_atom(V) || V <- Vs] - end, - TLSCiphers = conf_get([Field, "ciphers"], Conf), - PSKCiphers = conf_get([Field, "psk_ciphers"], Conf), - Ciphers = ciphers(TLSCiphers, PSKCiphers, Field), - case emqx_schema:conf_get([Field, "enable"], Conf) of - X when X =:= true orelse X =:= undefined -> - filter([{versions, Versions}, - {ciphers, Ciphers}, - {user_lookup_fun, user_lookup_fun(PSKCiphers)}, - {handshake_timeout, conf_get([Field, "handshake_timeout"], Conf)}, - {depth, conf_get([Field, "depth"], Conf)}, - {password, conf_get([Field, "key_password"], Conf)}, - {dhfile, conf_get([Field, "dhfile"], Conf)}, - {keyfile, emqx_schema:conf_get([Field, "keyfile"], Conf)}, - {certfile, emqx_schema:conf_get([Field, "certfile"], Conf)}, - {cacertfile, emqx_schema:conf_get([Field, "cacertfile"], Conf)}, - {verify, emqx_schema:conf_get([Field, "verify"], Conf)}, - {fail_if_no_peer_cert, conf_get([Field, "fail_if_no_peer_cert"], Conf)}, - {secure_renegotiate, conf_get([Field, "secure_renegotiate"], Conf)}, - {reuse_sessions, conf_get([Field, "reuse_sessions"], Conf)}, - {honor_cipher_order, conf_get([Field, "honor_cipher_order"], Conf)}, - {server_name_indication, emqx_schema:conf_get([Field, "server_name_indication"], Conf)} - ]); - _ -> - [] - end. - -map_psk_ciphers(PSKCiphers) -> - lists:map( - fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha}; - ("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha}; - ("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha}; - ("PSK-RC4-SHA") -> {psk, rc4_128, sha} - end, PSKCiphers). - -ciphers(undefined, undefined, _) -> - undefined; -ciphers(TLSCiphers, undefined, _) -> - TLSCiphers; -ciphers(undefined, PSKCiphers, _) -> - map_psk_ciphers(PSKCiphers); -ciphers(_, _, Field) -> - error(Field ++ ".ciphers and " ++ Field ++ ".psk_ciphers cannot be configured at the same time"). - -user_lookup_fun(undefined) -> - undefined; -user_lookup_fun(_PSKCiphers) -> - {fun emqx_psk:lookup/3, <<>>}. - -tr_password_hash(Field, Conf) -> - case emqx_schema:conf_get([Field, "password_hash"], Conf) of - [Hash] -> list_to_atom(Hash); - [Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)}; - [Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), - list_to_integer(Iterations), list_to_integer(Dklen)}; - _ -> plain - end. + , {"versions", #{ type => list(atom()) + , default => maps:get(versions, Defaults, default_tls_vsns()) + , converter => fun (Vsns) -> [tls_vsn(V) || V <- Vsns] end + }} + , {"ciphers", t(hoconsc:array(string()), undefined, D("ciphers"))} + , {"user_lookup_fun", t(any(), undefined, {fun emqx_psk:lookup/3, <<>>})} + ]. +default_tls_vsns() -> [<<"tlsv1.3">>, <<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. +tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3'; +tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2'; +tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1'; +tls_vsn(<<"tlsv1">>) -> 'tlsv1'. %% @private return a list of keys in a parent field -spec(keys(string(), hocon:config()) -> [string()]). @@ -814,3 +767,9 @@ to_ip_port(Str) -> end; _ -> {error, Str} end. + +to_erl_cipher_suite(Str) -> + case ssl:str_to_suite(Str) of + {error, Reason} -> error({invalid_cipher, Reason}); + Cipher -> Cipher + end. diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index 72a955962..24a9a15cf 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -161,17 +161,16 @@ drop_tls13_for_old_otp(SslOpts) -> , "TLS_AES_128_CCM_8_SHA256" ]). drop_tls13(SslOpts0) -> - SslOpts1 = case proplists:get_value(versions, SslOpts0) of - undefined -> SslOpts0; - Vsns -> replace(SslOpts0, versions, Vsns -- ['tlsv1.3']) + SslOpts1 = case maps:find(versions, SslOpts0) of + error -> SslOpts0; + {ok, Vsns} -> SslOpts0#{versions => (Vsns -- ['tlsv1.3'])} end, - case proplists:get_value(ciphers, SslOpts1) of - undefined -> SslOpts1; - Ciphers -> replace(SslOpts1, ciphers, Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS) + case maps:find(ciphers, SslOpts1) of + error -> SslOpts1; + {ok, Ciphers} -> + SslOpts1#{ciphers => Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS} end. -replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. - -if(?OTP_RELEASE > 22). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -181,13 +180,13 @@ drop_tls13_test() -> ?assert(lists:member('tlsv1.3', Versions)), Ciphers = default_ciphers(), ?assert(has_tlsv13_cipher(Ciphers)), - Opts0 = [{versions, Versions}, {ciphers, Ciphers}, other, {bool, true}], + Opts0 = #{versions => Versions, ciphers => Ciphers, other => true}, Opts = drop_tls13(Opts0), - ?assertNot(lists:member('tlsv1.3', proplists:get_value(versions, Opts))), - ?assertNot(has_tlsv13_cipher(proplists:get_value(ciphers, Opts))). + ?assertNot(lists:member('tlsv1.3', maps:get(versions, Opts, undefined))), + ?assertNot(has_tlsv13_cipher(maps:get(ciphers, Opts, undefined))). drop_tls13_no_versions_cipers_test() -> - Opts0 = [other, {bool, true}], + Opts0 = #{other => 0, bool => true}, Opts = drop_tls13(Opts0), ?_assertEqual(Opts0, Opts). diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index 7bc68c271..8d7816acf 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -62,8 +62,8 @@ sockname :: emqx_types:peername(), %% Sock state sockstate :: emqx_types:sockstate(), - %% Simulate the active_n opt - active_n :: pos_integer(), + %% Simulate the active opt + active :: pos_integer(), %% MQTT Piggyback mqtt_piggyback :: single | multiple, %% Limiter @@ -93,7 +93,7 @@ -type(ws_cmd() :: {active, boolean()}|close). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). @@ -124,7 +124,7 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active_n, #state{active_n = ActiveN}) -> +info(active, #state{active = ActiveN}) -> ActiveN; info(limiter, #state{limiter = Limiter}) -> maybe_apply(fun emqx_limiter:info/1, Limiter); @@ -293,7 +293,7 @@ websocket_init([Req, Opts]) -> BytesIn = proplists:get_value(rate_limit, Opts), RateLimit = emqx_zone:ratelimit(Zone), Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit), - ActiveN = proplists:get_value(active_n, Opts, ?ACTIVE_N), + ActiveN = proplists:get_value(active, Opts, ?ACTIVE_N), MQTTPiggyback = proplists:get_value(mqtt_piggyback, Opts, multiple), FrameOpts = emqx_zone:mqtt_frame_options(Zone), ParseState = emqx_frame:initial_parse_state(FrameOpts), @@ -309,7 +309,7 @@ websocket_init([Req, Opts]) -> {ok, #state{peername = Peername, sockname = Sockname, sockstate = running, - active_n = ActiveN, + active = ActiveN, mqtt_piggyback = MQTTPiggyback, limiter = Limiter, parse_state = ParseState, @@ -372,7 +372,7 @@ websocket_info({check_gc, Stats}, State) -> return(check_oom(run_gc(Stats, State))); websocket_info(Deliver = {deliver, _Topic, _Msg}, - State = #state{active_n = ActiveN}) -> + State = #state{active = ActiveN}) -> Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); @@ -551,7 +551,7 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) -> %% Handle incoming packet %%-------------------------------------------------------------------- -handle_incoming(Packet, State = #state{active_n = ActiveN}) +handle_incoming(Packet, State = #state{active = ActiveN}) when is_record(Packet, mqtt_packet) -> ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ok = inc_incoming_stats(Packet), @@ -586,7 +586,7 @@ with_channel(Fun, Args, State = #state{channel = Channel}) -> %% Handle outgoing packets %%-------------------------------------------------------------------- -handle_outgoing(Packets, State = #state{active_n = ActiveN, mqtt_piggyback = MQTTPiggyback}) -> +handle_outgoing(Packets, State = #state{active = ActiveN, mqtt_piggyback = MQTTPiggyback}) -> IoData = lists:map(serialize_and_inc_stats_fun(State), Packets), Oct = iolist_size(IoData), ok = inc_sent_stats(length(Packets), Oct), diff --git a/apps/emqx/test/emqx_connection_SUITE.erl b/apps/emqx/test/emqx_connection_SUITE.erl index a6b2b614a..6bace4ccc 100644 --- a/apps/emqx/test/emqx_connection_SUITE.erl +++ b/apps/emqx/test/emqx_connection_SUITE.erl @@ -120,7 +120,7 @@ t_info(_) -> end end), #{sockinfo := SockInfo} = emqx_connection:info(CPid), - ?assertMatch(#{active_n := 100, + ?assertMatch(#{active := 100, peername := {{127,0,0,1},3456}, sockname := {{127,0,0,1},1883}, sockstate := idle, @@ -219,8 +219,8 @@ t_handle_msg_deliver(_) -> t_handle_msg_inet_reply(_) -> ok = meck:expect(emqx_pd, get_counter, fun(_) -> 10 end), - ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st(#{active_n => 0}))), - ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st(#{active_n => 100}))), + ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st(#{active => 0}))), + ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st(#{active => 100}))), ?assertMatch({stop, {shutdown, for_testing}, _St}, handle_msg({inet_reply, for_testing, {error, for_testing}}, st())). @@ -386,7 +386,7 @@ t_start_link_exit_on_activate(_) -> t_get_conn_info(_) -> with_conn(fun(CPid) -> #{sockinfo := SockInfo} = emqx_connection:info(CPid), - ?assertEqual(#{active_n => 100, + ?assertEqual(#{active => 100, peername => {{127,0,0,1},3456}, sockname => {{127,0,0,1},1883}, sockstate => running, diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index 8ce35b50c..e80643e96 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -265,7 +265,7 @@ t_connect_idle_timeout(_) -> t_connect_limit_timeout(_) -> ok = meck:new(proplists, [non_strict, passthrough, no_history, no_link, unstick]), - meck:expect(proplists, get_value, fun(active_n, _Options, _Default) -> 1; + meck:expect(proplists, get_value, fun(active, _Options, _Default) -> 1; (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) end), diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index 6db831972..a9a6b7792 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -118,7 +118,7 @@ t_info(_) -> end), #{sockinfo := SockInfo} = ?ws_conn:call(WsPid, info), #{socktype := ws, - active_n := 100, + active := 100, peername := {{127,0,0,1}, 3456}, sockname := {{127,0,0,1}, 18083}, sockstate := running diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index d465f9ca3..fa127cc8b 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -303,7 +303,7 @@ sockinfo(#state{peername = Peername}) -> peername => Peername, sockname => {{127, 0, 0, 1}, 5683}, %% FIXME: Sock? sockstate => running, - active_n => 1 + active => 1 }. %% copies from emqx_channel:info/1 diff --git a/apps/emqx_exproto/etc/emqx_exproto.conf b/apps/emqx_exproto/etc/emqx_exproto.conf index 7a7667271..687e97748 100644 --- a/apps/emqx_exproto/etc/emqx_exproto.conf +++ b/apps/emqx_exproto/etc/emqx_exproto.conf @@ -49,7 +49,7 @@ exproto.listener.protoname.max_conn_rate = 1000 ## Specify the {active, N} option for the external MQTT/TCP Socket. ## ## Value: Number -exproto.listener.protoname.active_n = 100 +exproto.listener.protoname.active = 100 ## Idle timeout ## diff --git a/apps/emqx_exproto/priv/emqx_exproto.schema b/apps/emqx_exproto/priv/emqx_exproto.schema index 4bd215847..6d0fb0fa8 100644 --- a/apps/emqx_exproto/priv/emqx_exproto.schema +++ b/apps/emqx_exproto/priv/emqx_exproto.schema @@ -78,7 +78,7 @@ end}. {datatype, integer} ]}. -{mapping, "exproto.listener.$proto.active_n", "emqx_exproto.listeners", [ +{mapping, "exproto.listener.$proto.active", "emqx_exproto.listeners", [ {default, 100}, {datatype, integer} ]}. @@ -250,7 +250,7 @@ end}. end, ConnOpts = fun(Prefix) -> - Filter([{active_n, cuttlefish:conf_get(Prefix ++ ".active_n", Conf, undefined)}, + Filter([{active, cuttlefish:conf_get(Prefix ++ ".active", Conf, undefined)}, {idle_timeout, cuttlefish:conf_get(Prefix ++ ".idle_timeout", Conf, undefined)}]) end, diff --git a/apps/emqx_exproto/src/emqx_exproto_conn.erl b/apps/emqx_exproto/src/emqx_exproto_conn.erl index da655bcb4..fb30e1e88 100644 --- a/apps/emqx_exproto/src/emqx_exproto_conn.erl +++ b/apps/emqx_exproto/src/emqx_exproto_conn.erl @@ -61,7 +61,7 @@ %% Sock State sockstate :: emqx_types:sockstate(), %% The {active, N} option - active_n :: pos_integer(), + active :: pos_integer(), %% BACKW: e4.2.0-e4.2.1 %% We should remove it sendfun :: function() | undefined, @@ -84,7 +84,7 @@ -type(state() :: #state{}). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -137,7 +137,7 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active_n, #state{active_n = ActiveN}) -> +info(active, #state{active = ActiveN}) -> ActiveN. -spec(stats(pid()|state()) -> emqx_types:stats()). @@ -240,7 +240,7 @@ init_state(WrappedSock, Peername, Options) -> conn_mod => ?MODULE }, - ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), + ActiveN = proplists:get_value(active, Options, ?ACTIVE_N), %% FIXME: %%Limiter = emqx_limiter:init(Options), @@ -255,7 +255,7 @@ init_state(WrappedSock, Peername, Options) -> peername = Peername, sockname = Sockname, sockstate = idle, - active_n = ActiveN, + active = ActiveN, sendfun = undefined, limiter = undefined, channel = Channel, @@ -403,13 +403,13 @@ handle_msg({Passive, _Sock}, State) handle_info(activate_socket, NState1); handle_msg(Deliver = {deliver, _Topic, _Msg}, - State = #state{active_n = ActiveN}) -> + State = #state{active = ActiveN}) -> Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent %% TODO: Who will deliver this message? -handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> +handle_msg({inet_reply, _Sock, ok}, State = #state{active = ActiveN}) -> case emqx_pd:get_counter(outgoing_pkt) > ActiveN of true -> Pubs = emqx_pd:reset_counter(outgoing_pkt), @@ -652,7 +652,7 @@ activate_socket(State = #state{sockstate = closed}) -> activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{socket = Socket, - active_n = N}) -> + active = N}) -> %% FIXME: Works on dtls/udp ??? %% How to hanlde buffer? case esockd_setopts(Socket, [{active, N}]) of diff --git a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl index 55f992da6..9a8c0229a 100644 --- a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl +++ b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl @@ -459,7 +459,7 @@ sockinfo(#lwm2m_state{peername = Peername}) -> peername => Peername, sockname => {{127,0,0,1}, 5683}, %% FIXME: Sock? sockstate => running, - active_n => 1 + active => 1 }. %% copies from emqx_channel:info/1 diff --git a/apps/emqx_sn/src/emqx_sn_gateway.erl b/apps/emqx_sn/src/emqx_sn_gateway.erl index 2339961cf..bc1c11075 100644 --- a/apps/emqx_sn/src/emqx_sn_gateway.erl +++ b/apps/emqx_sn/src/emqx_sn_gateway.erl @@ -97,7 +97,7 @@ pending_topic_ids = #{} :: pending_msgs() }). --define(INFO_KEYS, [socktype, peername, sockname, sockstate]). %, active_n]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate]). %, active]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). From fb809a5c0810151e011e5b8caddb247fb2538915 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 11:17:35 +0800 Subject: [PATCH 079/379] fix(active_n): revert the changes to active_n --- apps/emqx/etc/emqx.conf | 6 +++--- apps/emqx/src/emqx_connection.erl | 16 ++++++++-------- apps/emqx/src/emqx_listeners.erl | 4 +++- apps/emqx/src/emqx_schema.erl | 2 +- apps/emqx/src/emqx_ws_connection.erl | 18 +++++++++--------- apps/emqx/test/emqx_connection_SUITE.erl | 8 ++++---- apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 2 +- apps/emqx/test/emqx_ws_connection_SUITE.erl | 2 +- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 2 +- apps/emqx_exproto/etc/emqx_exproto.conf | 2 +- apps/emqx_exproto/priv/emqx_exproto.schema | 4 ++-- apps/emqx_exproto/src/emqx_exproto_conn.erl | 16 ++++++++-------- apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl | 2 +- apps/emqx_sn/src/emqx_sn_gateway.erl | 2 +- 14 files changed, 44 insertions(+), 42 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 0dcb7285e..58417b7cf 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1806,7 +1806,7 @@ zones.internal { bind: "127.0.0.1:11883" acceptors: 4 max_connections: 1024000 - tcp.active: 1000 + tcp.active_n: 1000 tcp.backlog: 512 } } @@ -1959,10 +1959,10 @@ example_common_tcp_options { ## ## See: https://erlang.org/doc/man/inet.html#setopts-2 ## - ## @doc listeners..tcp.active + ## @doc listeners..tcp.active_n ## ValueType: Number ## Default: 100 - tcp.active: 100 + tcp.active_n: 100 ## TCP backlog defines the maximum length that the queue of ## pending connections can grow to. diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 3363b013e..ab91c02b4 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -84,7 +84,7 @@ %% Sock State sockstate :: emqx_types:sockstate(), %% The {active, N} option - active :: pos_integer(), + active_n :: pos_integer(), %% Limiter limiter :: maybe(emqx_limiter:limiter()), %% Limit Timer @@ -108,7 +108,7 @@ -type(state() :: #state{}). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -165,7 +165,7 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active, #state{active = ActiveN}) -> +info(active_n, #state{active_n = ActiveN}) -> ActiveN; info(stats_timer, #state{stats_timer = StatsTimer}) -> StatsTimer; @@ -254,7 +254,7 @@ init_state(Transport, Socket, Options) -> conn_mod => ?MODULE }, Zone = proplists:get_value(zone, Options), - ActiveN = proplists:get_value(active, Options, ?ACTIVE_N), + ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), PubLimit = emqx_zone:publish_limit(Zone), BytesIn = proplists:get_value(rate_limit, Options), RateLimit = emqx_zone:ratelimit(Zone), @@ -272,7 +272,7 @@ init_state(Transport, Socket, Options) -> peername = Peername, sockname = Sockname, sockstate = idle, - active = ActiveN, + active_n = ActiveN, limiter = Limiter, parse_state = ParseState, serialize = Serialize, @@ -452,12 +452,12 @@ handle_msg({Passive, _Sock}, State) handle_info(activate_socket, NState1); handle_msg(Deliver = {deliver, _Topic, _Msg}, - #state{active = ActiveN} = State) -> + #state{active_n = ActiveN} = State) -> Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent -handle_msg({inet_reply, _Sock, ok}, State = #state{active = ActiveN}) -> +handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> case emqx_pd:get_counter(outgoing_pubs) > ActiveN of true -> Pubs = emqx_pd:reset_counter(outgoing_pubs), @@ -800,7 +800,7 @@ activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{transport = Transport, socket = Socket, - active = N}) -> + active_n = N}) -> case Transport:setopts(Socket, [{active, N}]) of ok -> {ok, State#state{sockstate = running}}; Error -> Error diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index e3c6a5e22..d8407ec9e 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -199,7 +199,9 @@ ssl_opts(Opts) -> maps:get(ssl, Opts, #{})))). tcp_opts(Opts) -> - maps:to_list(maps:get(tcp, Opts, #{})). + maps:to_list( + maps:without([active_n], + maps:get(tcp, Opts, #{}))). is_ssl(Opts) -> emqx_config:deep_get([ssl, enable], Opts, false). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 5a6044307..616f65bfd 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -379,7 +379,7 @@ fields("ws_opts") -> ]; fields("tcp_opts") -> - [ {"active", t(integer(), undefined, 100)} + [ {"active_n", t(integer(), undefined, 100)} , {"backlog", t(integer(), undefined, 1024)} , {"send_timeout", t(duration(), undefined, "15s")} , {"send_timeout_close", t(boolean(), undefined, true)} diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index 8d7816acf..7bc68c271 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -62,8 +62,8 @@ sockname :: emqx_types:peername(), %% Sock state sockstate :: emqx_types:sockstate(), - %% Simulate the active opt - active :: pos_integer(), + %% Simulate the active_n opt + active_n :: pos_integer(), %% MQTT Piggyback mqtt_piggyback :: single | multiple, %% Limiter @@ -93,7 +93,7 @@ -type(ws_cmd() :: {active, boolean()}|close). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). @@ -124,7 +124,7 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active, #state{active = ActiveN}) -> +info(active_n, #state{active_n = ActiveN}) -> ActiveN; info(limiter, #state{limiter = Limiter}) -> maybe_apply(fun emqx_limiter:info/1, Limiter); @@ -293,7 +293,7 @@ websocket_init([Req, Opts]) -> BytesIn = proplists:get_value(rate_limit, Opts), RateLimit = emqx_zone:ratelimit(Zone), Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit), - ActiveN = proplists:get_value(active, Opts, ?ACTIVE_N), + ActiveN = proplists:get_value(active_n, Opts, ?ACTIVE_N), MQTTPiggyback = proplists:get_value(mqtt_piggyback, Opts, multiple), FrameOpts = emqx_zone:mqtt_frame_options(Zone), ParseState = emqx_frame:initial_parse_state(FrameOpts), @@ -309,7 +309,7 @@ websocket_init([Req, Opts]) -> {ok, #state{peername = Peername, sockname = Sockname, sockstate = running, - active = ActiveN, + active_n = ActiveN, mqtt_piggyback = MQTTPiggyback, limiter = Limiter, parse_state = ParseState, @@ -372,7 +372,7 @@ websocket_info({check_gc, Stats}, State) -> return(check_oom(run_gc(Stats, State))); websocket_info(Deliver = {deliver, _Topic, _Msg}, - State = #state{active = ActiveN}) -> + State = #state{active_n = ActiveN}) -> Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); @@ -551,7 +551,7 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) -> %% Handle incoming packet %%-------------------------------------------------------------------- -handle_incoming(Packet, State = #state{active = ActiveN}) +handle_incoming(Packet, State = #state{active_n = ActiveN}) when is_record(Packet, mqtt_packet) -> ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ok = inc_incoming_stats(Packet), @@ -586,7 +586,7 @@ with_channel(Fun, Args, State = #state{channel = Channel}) -> %% Handle outgoing packets %%-------------------------------------------------------------------- -handle_outgoing(Packets, State = #state{active = ActiveN, mqtt_piggyback = MQTTPiggyback}) -> +handle_outgoing(Packets, State = #state{active_n = ActiveN, mqtt_piggyback = MQTTPiggyback}) -> IoData = lists:map(serialize_and_inc_stats_fun(State), Packets), Oct = iolist_size(IoData), ok = inc_sent_stats(length(Packets), Oct), diff --git a/apps/emqx/test/emqx_connection_SUITE.erl b/apps/emqx/test/emqx_connection_SUITE.erl index 6bace4ccc..a6b2b614a 100644 --- a/apps/emqx/test/emqx_connection_SUITE.erl +++ b/apps/emqx/test/emqx_connection_SUITE.erl @@ -120,7 +120,7 @@ t_info(_) -> end end), #{sockinfo := SockInfo} = emqx_connection:info(CPid), - ?assertMatch(#{active := 100, + ?assertMatch(#{active_n := 100, peername := {{127,0,0,1},3456}, sockname := {{127,0,0,1},1883}, sockstate := idle, @@ -219,8 +219,8 @@ t_handle_msg_deliver(_) -> t_handle_msg_inet_reply(_) -> ok = meck:expect(emqx_pd, get_counter, fun(_) -> 10 end), - ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st(#{active => 0}))), - ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st(#{active => 100}))), + ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st(#{active_n => 0}))), + ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st(#{active_n => 100}))), ?assertMatch({stop, {shutdown, for_testing}, _St}, handle_msg({inet_reply, for_testing, {error, for_testing}}, st())). @@ -386,7 +386,7 @@ t_start_link_exit_on_activate(_) -> t_get_conn_info(_) -> with_conn(fun(CPid) -> #{sockinfo := SockInfo} = emqx_connection:info(CPid), - ?assertEqual(#{active => 100, + ?assertEqual(#{active_n => 100, peername => {{127,0,0,1},3456}, sockname => {{127,0,0,1},1883}, sockstate => running, diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index e80643e96..8ce35b50c 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -265,7 +265,7 @@ t_connect_idle_timeout(_) -> t_connect_limit_timeout(_) -> ok = meck:new(proplists, [non_strict, passthrough, no_history, no_link, unstick]), - meck:expect(proplists, get_value, fun(active, _Options, _Default) -> 1; + meck:expect(proplists, get_value, fun(active_n, _Options, _Default) -> 1; (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) end), diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index a9a6b7792..6db831972 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -118,7 +118,7 @@ t_info(_) -> end), #{sockinfo := SockInfo} = ?ws_conn:call(WsPid, info), #{socktype := ws, - active := 100, + active_n := 100, peername := {{127,0,0,1}, 3456}, sockname := {{127,0,0,1}, 18083}, sockstate := running diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index fa127cc8b..d465f9ca3 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -303,7 +303,7 @@ sockinfo(#state{peername = Peername}) -> peername => Peername, sockname => {{127, 0, 0, 1}, 5683}, %% FIXME: Sock? sockstate => running, - active => 1 + active_n => 1 }. %% copies from emqx_channel:info/1 diff --git a/apps/emqx_exproto/etc/emqx_exproto.conf b/apps/emqx_exproto/etc/emqx_exproto.conf index 687e97748..7a7667271 100644 --- a/apps/emqx_exproto/etc/emqx_exproto.conf +++ b/apps/emqx_exproto/etc/emqx_exproto.conf @@ -49,7 +49,7 @@ exproto.listener.protoname.max_conn_rate = 1000 ## Specify the {active, N} option for the external MQTT/TCP Socket. ## ## Value: Number -exproto.listener.protoname.active = 100 +exproto.listener.protoname.active_n = 100 ## Idle timeout ## diff --git a/apps/emqx_exproto/priv/emqx_exproto.schema b/apps/emqx_exproto/priv/emqx_exproto.schema index 6d0fb0fa8..4bd215847 100644 --- a/apps/emqx_exproto/priv/emqx_exproto.schema +++ b/apps/emqx_exproto/priv/emqx_exproto.schema @@ -78,7 +78,7 @@ end}. {datatype, integer} ]}. -{mapping, "exproto.listener.$proto.active", "emqx_exproto.listeners", [ +{mapping, "exproto.listener.$proto.active_n", "emqx_exproto.listeners", [ {default, 100}, {datatype, integer} ]}. @@ -250,7 +250,7 @@ end}. end, ConnOpts = fun(Prefix) -> - Filter([{active, cuttlefish:conf_get(Prefix ++ ".active", Conf, undefined)}, + Filter([{active_n, cuttlefish:conf_get(Prefix ++ ".active_n", Conf, undefined)}, {idle_timeout, cuttlefish:conf_get(Prefix ++ ".idle_timeout", Conf, undefined)}]) end, diff --git a/apps/emqx_exproto/src/emqx_exproto_conn.erl b/apps/emqx_exproto/src/emqx_exproto_conn.erl index fb30e1e88..da655bcb4 100644 --- a/apps/emqx_exproto/src/emqx_exproto_conn.erl +++ b/apps/emqx_exproto/src/emqx_exproto_conn.erl @@ -61,7 +61,7 @@ %% Sock State sockstate :: emqx_types:sockstate(), %% The {active, N} option - active :: pos_integer(), + active_n :: pos_integer(), %% BACKW: e4.2.0-e4.2.1 %% We should remove it sendfun :: function() | undefined, @@ -84,7 +84,7 @@ -type(state() :: #state{}). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -137,7 +137,7 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active, #state{active = ActiveN}) -> +info(active_n, #state{active_n = ActiveN}) -> ActiveN. -spec(stats(pid()|state()) -> emqx_types:stats()). @@ -240,7 +240,7 @@ init_state(WrappedSock, Peername, Options) -> conn_mod => ?MODULE }, - ActiveN = proplists:get_value(active, Options, ?ACTIVE_N), + ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), %% FIXME: %%Limiter = emqx_limiter:init(Options), @@ -255,7 +255,7 @@ init_state(WrappedSock, Peername, Options) -> peername = Peername, sockname = Sockname, sockstate = idle, - active = ActiveN, + active_n = ActiveN, sendfun = undefined, limiter = undefined, channel = Channel, @@ -403,13 +403,13 @@ handle_msg({Passive, _Sock}, State) handle_info(activate_socket, NState1); handle_msg(Deliver = {deliver, _Topic, _Msg}, - State = #state{active = ActiveN}) -> + State = #state{active_n = ActiveN}) -> Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent %% TODO: Who will deliver this message? -handle_msg({inet_reply, _Sock, ok}, State = #state{active = ActiveN}) -> +handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> case emqx_pd:get_counter(outgoing_pkt) > ActiveN of true -> Pubs = emqx_pd:reset_counter(outgoing_pkt), @@ -652,7 +652,7 @@ activate_socket(State = #state{sockstate = closed}) -> activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{socket = Socket, - active = N}) -> + active_n = N}) -> %% FIXME: Works on dtls/udp ??? %% How to hanlde buffer? case esockd_setopts(Socket, [{active, N}]) of diff --git a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl index 9a8c0229a..55f992da6 100644 --- a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl +++ b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl @@ -459,7 +459,7 @@ sockinfo(#lwm2m_state{peername = Peername}) -> peername => Peername, sockname => {{127,0,0,1}, 5683}, %% FIXME: Sock? sockstate => running, - active => 1 + active_n => 1 }. %% copies from emqx_channel:info/1 diff --git a/apps/emqx_sn/src/emqx_sn_gateway.erl b/apps/emqx_sn/src/emqx_sn_gateway.erl index bc1c11075..2339961cf 100644 --- a/apps/emqx_sn/src/emqx_sn_gateway.erl +++ b/apps/emqx_sn/src/emqx_sn_gateway.erl @@ -97,7 +97,7 @@ pending_topic_ids = #{} :: pending_msgs() }). --define(INFO_KEYS, [socktype, peername, sockname, sockstate]). %, active]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate]). %, active_n]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). From 2f5ab6a9743f9d3e2d503bb4d4896b2167be3608 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 5 Jul 2021 09:49:17 +0800 Subject: [PATCH 080/379] chore(CI): fix macos build error --- .github/workflows/build_packages.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 6d1a757af..de4315d36 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -183,7 +183,7 @@ jobs: ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:18083 > /dev/null; then + if curl -fs 127.0.0.1:8081/status > /dev/null; then ready='yes' break fi From a884d215e17a594d8d79a98014ad8d915aa03f64 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 13:50:37 +0800 Subject: [PATCH 081/379] fix(connection): start connection failed --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_connection.erl | 3 ++- apps/emqx/src/emqx_listeners.erl | 2 +- rebar.config | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 22f31a345..7493e79ed 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -12,7 +12,7 @@ [ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} - , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}} + , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.1"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index ab91c02b4..54d3a4691 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -106,6 +106,7 @@ }). -type(state() :: #state{}). +-type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). -define(ACTIVE_N, 100). -define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). @@ -134,7 +135,7 @@ , system_code_change/4 ]}). --spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) +-spec(start_link(esockd:transport(), esockd:socket(), opts()) -> {ok, pid()}). start_link(Transport, Socket, Options) -> Args = [self(), Transport, Socket, Options], diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index d8407ec9e..f3af077e2 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -72,7 +72,7 @@ console_print(_Fmt, _Args) -> ok. -> {ok, pid()} | {error, term()}). do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opts) -> esockd:open(listener_id(ZoneName, ListenerName), ListenOn, merge_default(esockd_opts(Opts)), - {emqx_connection, start_link, [ZoneName, ListenerName]}); + {emqx_connection, start_link, [{ZoneName, ListenerName}]}); %% Start MQTT/WS listener do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) -> diff --git a/rebar.config b/rebar.config index 66d24fe91..36290848a 100644 --- a/rebar.config +++ b/rebar.config @@ -39,7 +39,7 @@ , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} - , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}} + , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.1"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} % TODO: delete when all apps moved to hocon From ce1e6ce89d452cf0b07cc96d1a970cc5c616f117 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 5 Jul 2021 14:12:05 +0800 Subject: [PATCH 082/379] fix: start jiffy by emqx --- apps/emqx/src/emqx.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index a6984370e..d9efbe82a 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -4,7 +4,7 @@ {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, - {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,quicer]}, + {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,quicer,jiffy]}, {mod, {emqx_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, From e1a33c373c4e6b61c93d320b6e84ecf0156703ed Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 5 Jul 2021 14:44:14 +0800 Subject: [PATCH 083/379] chore(CI): disable apps version check in unstable tags --- scripts/apps-version-check.sh | 82 ++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/scripts/apps-version-check.sh b/scripts/apps-version-check.sh index 4fc31ccc6..7c2ea5eb2 100755 --- a/scripts/apps-version-check.sh +++ b/scripts/apps-version-check.sh @@ -17,40 +17,52 @@ get_vsn() { fi } -while read -r app_path; do - app=$(basename "$app_path") - src_file="$app_path/src/$app.app.src" - old_app_version="$(get_vsn "$latest_release" "$src_file")" - ## TODO: delete it after new version is released with emqx app in apps dir - if [ "$app" = 'emqx' ] && [ "$old_app_version" = '' ]; then - old_app_version="$(get_vsn "$latest_release" 'src/emqx.app.src')" - fi - now_app_version="$(get_vsn 'HEAD' "$src_file")" - ## TODO: delete it after new version is released with emqx app in apps dir - if [ "$app" = 'emqx' ] && [ "$now_app_version" = '' ]; then - now_app_version="$(get_vsn 'HEAD' 'src/emqx.app.src')" - fi - if [ -z "$now_app_version" ]; then - echo "failed_to_get_new_app_vsn for $app" - exit 1 - fi - if [ -z "${old_app_version:-}" ]; then - echo "skiped checking new app ${app}" - elif [ "$old_app_version" = "$now_app_version" ]; then - lines="$(git diff --name-only "$latest_release"...HEAD \ - -- "$app_path/src" \ - -- "$app_path/priv" \ - -- "$app_path/c_src")" - if [ "$lines" != '' ]; then - echo "$src_file needs a vsn bump (old=$old_app_version)" - echo "changed: $lines" - bad_app_count=$(( bad_app_count + 1)) +check_apps() { + while read -r app_path; do + app=$(basename "$app_path") + src_file="$app_path/src/$app.app.src" + old_app_version="$(get_vsn "$latest_release" "$src_file")" + ## TODO: delete it after new version is released with emqx app in apps dir + if [ "$app" = 'emqx' ] && [ "$old_app_version" = '' ]; then + old_app_version="$(get_vsn "$latest_release" 'src/emqx.app.src')" fi - fi -done < <(./scripts/find-apps.sh) + now_app_version="$(get_vsn 'HEAD' "$src_file")" + ## TODO: delete it after new version is released with emqx app in apps dir + if [ "$app" = 'emqx' ] && [ "$now_app_version" = '' ]; then + now_app_version="$(get_vsn 'HEAD' 'src/emqx.app.src')" + fi + if [ -z "$now_app_version" ]; then + echo "failed_to_get_new_app_vsn for $app" + exit 1 + fi + if [ -z "${old_app_version:-}" ]; then + echo "skiped checking new app ${app}" + elif [ "$old_app_version" = "$now_app_version" ]; then + lines="$(git diff --name-only "$latest_release"...HEAD \ + -- "$app_path/src" \ + -- "$app_path/priv" \ + -- "$app_path/c_src")" + if [ "$lines" != '' ]; then + echo "$src_file needs a vsn bump (old=$old_app_version)" + echo "changed: $lines" + bad_app_count=$(( bad_app_count + 1)) + fi + fi + done < <(./scripts/find-apps.sh) -if [ $bad_app_count -gt 0 ]; then - exit 1 -else - echo "apps version check successfully" -fi + if [ $bad_app_count -gt 0 ]; then + exit 1 + else + echo "apps version check successfully" + fi +} + +_main() { + if echo "${latest_release}" |grep -oE '[0-9]+.[0-9]+.[0-9]+' > /dev/null 2>&1; then + check_apps + else + echo "skiped unstable tag: ${latest_release}" + fi +} + +_main From 30c2a76dae6231c0a2728e6ad82fa20d35919d9f Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 15:06:56 +0800 Subject: [PATCH 084/379] refactor(emqx_config): move helper funcs to emqx_map_lib --- apps/emqx/src/emqx_config.erl | 84 +++++---------------------- apps/emqx/src/emqx_config_handler.erl | 4 +- apps/emqx/src/emqx_listeners.erl | 4 +- apps/emqx/src/emqx_map_lib.erl | 75 ++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 apps/emqx/src/emqx_map_lib.erl diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 3d431f6a8..bc0289baf 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -35,43 +35,34 @@ , put_raw/2 ]). --export([ deep_get/2 - , deep_get/3 - , deep_put/3 - , safe_atom_key_map/1 - , unsafe_atom_key_map/1 - ]). - -define(CONF, ?MODULE). -define(RAW_CONF, {?MODULE, raw}). --export_type([update_request/0, raw_config/0, config_key/0, config_key_path/0]). +-export_type([update_request/0, raw_config/0]). -type update_request() :: term(). -type raw_config() :: hocon:config() | undefined. --type config_key() :: atom() | binary(). --type config_key_path() :: [config_key()]. -spec get() -> map(). get() -> persistent_term:get(?CONF, #{}). --spec get(config_key_path()) -> term(). +-spec get(emqx_map_lib:config_key_path()) -> term(). get(KeyPath) -> - deep_get(KeyPath, get()). + emqx_map_lib:deep_get(KeyPath, get()). --spec get(config_key_path(), term()) -> term(). +-spec get(emqx_map_lib:config_key_path(), term()) -> term(). get(KeyPath, Default) -> - deep_get(KeyPath, get(), Default). + emqx_map_lib:deep_get(KeyPath, get(), Default). -spec put(map()) -> ok. put(Config) -> persistent_term:put(?CONF, Config). --spec put(config_key_path(), term()) -> ok. +-spec put(emqx_map_lib:config_key_path(), term()) -> ok. put(KeyPath, Config) -> - put(deep_put(KeyPath, get(), Config)). + put(emqx_map_lib:deep_put(KeyPath, get(), Config)). --spec update_config(config_key_path(), update_request()) -> +-spec update_config(emqx_map_lib:config_key_path(), update_request()) -> ok | {error, term()}. update_config(ConfKeyPath, UpdateReq) -> emqx_config_handler:update_config(ConfKeyPath, UpdateReq, get_raw()). @@ -80,66 +71,19 @@ update_config(ConfKeyPath, UpdateReq) -> get_raw() -> persistent_term:get(?RAW_CONF, #{}). --spec get_raw(config_key_path()) -> term(). +-spec get_raw(emqx_map_lib:config_key_path()) -> term(). get_raw(KeyPath) -> - deep_get(KeyPath, get_raw()). + emqx_map_lib:deep_get(KeyPath, get_raw()). --spec get_raw(config_key_path(), term()) -> term(). +-spec get_raw(emqx_map_lib:config_key_path(), term()) -> term(). get_raw(KeyPath, Default) -> - deep_get(KeyPath, get_raw(), Default). + emqx_map_lib:deep_get(KeyPath, get_raw(), Default). -spec put_raw(map()) -> ok. put_raw(Config) -> persistent_term:put(?RAW_CONF, Config). --spec put_raw(config_key_path(), term()) -> ok. +-spec put_raw(emqx_map_lib:config_key_path(), term()) -> ok. put_raw(KeyPath, Config) -> - put_raw(deep_put(KeyPath, get_raw(), Config)). + put_raw(emqx_map_lib:deep_put(KeyPath, get_raw(), Config)). -%%----------------------------------------------------------------- --spec deep_get(config_key_path(), map()) -> term(). -deep_get(ConfKeyPath, Map) -> - do_deep_get(ConfKeyPath, Map, fun(KeyPath, Data) -> - error({not_found, KeyPath, Data}) end). - --spec deep_get(config_key_path(), map(), term()) -> term(). -deep_get(ConfKeyPath, Map, Default) -> - do_deep_get(ConfKeyPath, Map, fun(_, _) -> Default end). - --spec deep_put(config_key_path(), map(), term()) -> map(). -deep_put([], Map, Config) when is_map(Map) -> - Config; -deep_put([Key | KeyPath], Map, Config) -> - SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config), - Map#{Key => SubMap}. - -unsafe_atom_key_map(Map) -> - covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). - -safe_atom_key_map(Map) -> - covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). - -%%--------------------------------------------------------------------------- - --spec do_deep_get(config_key_path(), map(), fun((config_key(), term()) -> any())) -> term(). -do_deep_get([], Map, _) -> - Map; -do_deep_get([Key | KeyPath], Map, OnNotFound) when is_map(Map) -> - case maps:find(Key, Map) of - {ok, SubMap} -> do_deep_get(KeyPath, SubMap, OnNotFound); - error -> OnNotFound(Key, Map) - end; -do_deep_get([Key | _KeyPath], Data, OnNotFound) -> - OnNotFound(Key, Data). - -covert_keys_to_atom(BinKeyMap, Conv) when is_map(BinKeyMap) -> - maps:fold( - fun(K, V, Acc) when is_binary(K) -> - Acc#{Conv(K) => covert_keys_to_atom(V, Conv)}; - (K, V, Acc) when is_atom(K) -> - %% richmap keys - Acc#{K => covert_keys_to_atom(V, Conv)} - end, #{}, BinKeyMap); -covert_keys_to_atom(ListV, Conv) when is_list(ListV) -> - [covert_keys_to_atom(V, Conv) || V <- ListV]; -covert_keys_to_atom(Val, _) -> Val. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index 138521929..eba844829 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -79,7 +79,7 @@ init(_) -> handle_call({add_child, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) -> {reply, ok, State#{handlers => - emqx_config:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; + emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; handle_call({update_config, ConfKeyPath, UpdateReq, RawConf}, _From, #{handlers := Handlers} = State) -> @@ -161,7 +161,7 @@ save_configs(RootKeys, RawConf) -> save_config_to_emqx(Conf, RawConf) -> ?LOG(debug, "set config: ~p", [Conf]), - emqx_config:put(emqx_config:unsafe_atom_key_map(Conf)), + emqx_config:put(emqx_map_lib:unsafe_atom_key_map(Conf)), emqx_config:put_raw(RawConf). save_config_to_disk(RootKeys, Conf) -> diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index f3af077e2..aa4fbe7f1 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -88,7 +88,7 @@ do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts esockd_opts(Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), - Opts2 = case emqx_config:deep_get([rate_limit, max_conn_rate], Opts0) of + Opts2 = case emqx_map_lib:deep_get([rate_limit, max_conn_rate], Opts0) of infinity -> Opts1; Rate -> Opts1#{max_conn_rate => Rate} end, @@ -204,4 +204,4 @@ tcp_opts(Opts) -> maps:get(tcp, Opts, #{}))). is_ssl(Opts) -> - emqx_config:deep_get([ssl, enable], Opts, false). + emqx_map_lib:deep_get([ssl, enable], Opts, false). diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl new file mode 100644 index 000000000..b9d6ae03b --- /dev/null +++ b/apps/emqx/src/emqx_map_lib.erl @@ -0,0 +1,75 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_map_lib). + +-export([ deep_get/2 + , deep_get/3 + , deep_put/3 + , safe_atom_key_map/1 + , unsafe_atom_key_map/1 + ]). + +-export_type([config_key/0, config_key_path/0]). +-type config_key() :: atom() | binary(). +-type config_key_path() :: [config_key()]. + +%%----------------------------------------------------------------- +-spec deep_get(config_key_path(), map()) -> term(). +deep_get(ConfKeyPath, Map) -> + do_deep_get(ConfKeyPath, Map, fun(KeyPath, Data) -> + error({not_found, KeyPath, Data}) end). + +-spec deep_get(config_key_path(), map(), term()) -> term(). +deep_get(ConfKeyPath, Map, Default) -> + do_deep_get(ConfKeyPath, Map, fun(_, _) -> Default end). + +-spec deep_put(config_key_path(), map(), term()) -> map(). +deep_put([], Map, Config) when is_map(Map) -> + Config; +deep_put([Key | KeyPath], Map, Config) -> + SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config), + Map#{Key => SubMap}. + +unsafe_atom_key_map(Map) -> + covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). + +safe_atom_key_map(Map) -> + covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). + +%%--------------------------------------------------------------------------- + +-spec do_deep_get(config_key_path(), map(), fun((config_key(), term()) -> any())) -> term(). +do_deep_get([], Map, _) -> + Map; +do_deep_get([Key | KeyPath], Map, OnNotFound) when is_map(Map) -> + case maps:find(Key, Map) of + {ok, SubMap} -> do_deep_get(KeyPath, SubMap, OnNotFound); + error -> OnNotFound(Key, Map) + end; +do_deep_get([Key | _KeyPath], Data, OnNotFound) -> + OnNotFound(Key, Data). + +covert_keys_to_atom(BinKeyMap, Conv) when is_map(BinKeyMap) -> + maps:fold( + fun(K, V, Acc) when is_binary(K) -> + Acc#{Conv(K) => covert_keys_to_atom(V, Conv)}; + (K, V, Acc) when is_atom(K) -> + %% richmap keys + Acc#{K => covert_keys_to_atom(V, Conv)} + end, #{}, BinKeyMap); +covert_keys_to_atom(ListV, Conv) when is_list(ListV) -> + [covert_keys_to_atom(V, Conv) || V <- ListV]; +covert_keys_to_atom(Val, _) -> Val. From 0aec496886d04bb267f5dc231095d00889c8e503 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 16:02:03 +0800 Subject: [PATCH 085/379] feat(map_lib): support emqx_map_lib:deep_find/2 --- apps/emqx/src/emqx_map_lib.erl | 35 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index b9d6ae03b..8b3108a53 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -17,6 +17,7 @@ -export([ deep_get/2 , deep_get/3 + , deep_find/2 , deep_put/3 , safe_atom_key_map/1 , unsafe_atom_key_map/1 @@ -29,12 +30,28 @@ %%----------------------------------------------------------------- -spec deep_get(config_key_path(), map()) -> term(). deep_get(ConfKeyPath, Map) -> - do_deep_get(ConfKeyPath, Map, fun(KeyPath, Data) -> - error({not_found, KeyPath, Data}) end). + case deep_find(ConfKeyPath, Map) of + {not_found, KeyPath, Data} -> error({not_found, KeyPath, Data}); + {ok, Data} -> Data + end. -spec deep_get(config_key_path(), map(), term()) -> term(). deep_get(ConfKeyPath, Map, Default) -> - do_deep_get(ConfKeyPath, Map, fun(_, _) -> Default end). + case deep_find(ConfKeyPath, Map) of + {not_found, _KeyPath, _Data} -> Default; + {ok, Data} -> Data + end. + +-spec deep_find(config_key_path(), map()) -> {ok, term()} | {not_found, config_key(), term()}. +deep_find([], Map) -> + {ok, Map}; +deep_find([Key | KeyPath], Map) when is_map(Map) -> + case maps:find(Key, Map) of + {ok, SubMap} -> deep_find(KeyPath, SubMap); + error -> {not_found, Key, Map} + end; +deep_find([Key | _KeyPath], Data) -> + {not_found, Key, Data}. -spec deep_put(config_key_path(), map(), term()) -> map(). deep_put([], Map, Config) when is_map(Map) -> @@ -50,18 +67,6 @@ safe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). %%--------------------------------------------------------------------------- - --spec do_deep_get(config_key_path(), map(), fun((config_key(), term()) -> any())) -> term(). -do_deep_get([], Map, _) -> - Map; -do_deep_get([Key | KeyPath], Map, OnNotFound) when is_map(Map) -> - case maps:find(Key, Map) of - {ok, SubMap} -> do_deep_get(KeyPath, SubMap, OnNotFound); - error -> OnNotFound(Key, Map) - end; -do_deep_get([Key | _KeyPath], Data, OnNotFound) -> - OnNotFound(Key, Data). - covert_keys_to_atom(BinKeyMap, Conv) when is_map(BinKeyMap) -> maps:fold( fun(K, V, Acc) when is_binary(K) -> From 807bbb391c2cdcf357250443470b0b602b361a92 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 16:40:00 +0800 Subject: [PATCH 086/379] feat(map_lib): support emqx_config:get_listener_conf/3,4 --- apps/emqx/src/emqx_config.erl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index bc0289baf..91b115e9d 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -20,10 +20,16 @@ -export([ get/0 , get/1 , get/2 + , find/1 , put/1 , put/2 ]). +-export([ get_listener_conf/3 + , get_listener_conf/4 + , find_listener_conf/3 + ]). + -export([ update_config/2 ]). @@ -54,6 +60,34 @@ get(KeyPath) -> get(KeyPath, Default) -> emqx_map_lib:deep_get(KeyPath, get(), Default). +-spec find(emqx_map_lib:config_key_path()) -> + {ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}. +find(KeyPath) -> + emqx_map_lib:deep_find(KeyPath, get()). + +-spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> term(). +get_listener_conf(Zone, Listener, KeyPath) -> + case find_listener_conf(Zone, Listener, KeyPath) of + {not_found, SubKeyPath, Data} -> error({not_found, SubKeyPath, Data}); + {ok, Data} -> Data + end. + +-spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> term(). +get_listener_conf(Zone, Listener, KeyPath, Default) -> + case find_listener_conf(Zone, Listener, KeyPath) of + {not_found, _, _} -> Default; + {ok, Data} -> Data + end. + +-spec find_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> + {ok, term()} | {not_foud, emqx_map_lib:config_key_path(), term()}. +find_listener_conf(Zone, Listener, KeyPath) -> + %% the configs in listener is prior to the ones in the zone + case find([zones, Zone, listeners, Listener | KeyPath]) of + {not_found, _, _} -> find([zones, Zone | KeyPath]); + {ok, Data} -> {ok, Data} + end. + -spec put(map()) -> ok. put(Config) -> persistent_term:put(?CONF, Config). From 694f3bd67fde5485f38494881bc43bbadde06151 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Wed, 30 Jun 2021 19:09:23 +0800 Subject: [PATCH 087/379] feat(authz): support mongo single --- .../docker-compose-mongo-tcp.yaml | 2 + apps/emqx_authz/README.md | 13 +++ apps/emqx_authz/etc/emqx_authz.conf | 12 ++ apps/emqx_authz/src/emqx_authz.erl | 5 +- apps/emqx_authz/src/emqx_authz_mongo.erl | 106 ++++++++++++++++++ apps/emqx_authz/src/emqx_authz_schema.erl | 8 +- .../src/emqx_connector_mongo.erl | 34 +++--- .../src/emqx_connector_redis.erl | 2 +- .../src/emqx_connector_schema_lib.erl | 4 +- .../src/emqx_plugin_libs_pool.erl | 2 +- 10 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 apps/emqx_authz/src/emqx_authz_mongo.erl diff --git a/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml b/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml index dee2daff6..494b42ce4 100644 --- a/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml +++ b/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml @@ -9,6 +9,8 @@ services: MONGO_INITDB_DATABASE: mqtt networks: - emqx_bridge + ports: + - "27017:27017" command: --ipv6 --bind_ip_all diff --git a/apps/emqx_authz/README.md b/apps/emqx_authz/README.md index 699781b71..de0d695ee 100644 --- a/apps/emqx_authz/README.md +++ b/apps/emqx_authz/README.md @@ -133,3 +133,16 @@ HSET mqtt_acl:emqx '$SYS/#' subscribe A rule of Redis ACL defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists. +#### Mongo + +Create Example BSON documents +```sql +db.inventory.insertOne( + {username: "emqx", + clientid: "emqx", + ipaddress: "127.0.0.1", + permission: "allow", + action: "all", + topics: ["#"] + }) +``` diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 89515592f..e91a68a63 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -43,6 +43,18 @@ emqx_authz:{ # } # cmd: "HGETALL mqtt_acl:%u" # }, + # { + # type: mongo + # config: { + # mongo_type: single + # servers: "127.0.0.1:27017" + # pool_size: 1 + # database: mqtt + # ssl: {enable: false} + # } + # collection: mqtt_acl + # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } + # }, { permission: allow action: all diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 6710e4f69..f673b3979 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -93,8 +93,9 @@ compile(#{topics := Topics, }; compile(#{principal := Principal, - type := redis - } = Rule) -> + type := DB + } = Rule) when DB =:= redis; + DB =:= mongo -> NRule = create_resource(Rule), NRule#{principal => compile_principal(Principal)}; diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl new file mode 100644 index 000000000..a106dd0f5 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -0,0 +1,106 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_mongo). + +-include("emqx_authz.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). + +%% ACL Callbacks +-export([ authorize/4 + , description/0 + ]). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + +description() -> + "AuthZ with Mongo". + +authorize(Client, PubSub, Topic, + #{resource_id := ResourceID, + collection := Collection, + find := Find + }) -> + case emqx_resource:query(ResourceID, {find, Collection, replvar(Find, Client), #{}}) of + {error, Reason} -> + ?LOG(error, "[AuthZ] Query mongo error: ~p", [Reason]), + nomatch; + [] -> nomatch; + Rows -> + do_authorize(Client, PubSub, Topic, Rows) + end. + +do_authorize(_Client, _PubSub, _Topic, []) -> + nomatch; +do_authorize(Client, PubSub, Topic, [Rule | Tail]) -> + case match(Client, PubSub, Topic, Rule) + of + {matched, Permission} -> {matched, Permission}; + nomatch -> do_authorize(Client, PubSub, Topic, Tail) + end. + +match(Client, PubSub, Topic, + #{<<"topics">> := Topics, + <<"permission">> := Permission, + <<"action">> := Action + }) -> + Rule = #{<<"principal">> => all, + <<"permission">> => Permission, + <<"topics">> => Topics, + <<"action">> => Action + }, + #{simple_rule := + #{permission := NPermission} = NRule + } = hocon_schema:check_plain( + emqx_authz_schema, + #{<<"simple_rule">> => Rule}, + #{atom_key => true}, + [simple_rule]), + case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of + true -> {matched, NPermission}; + false -> nomatch + end. + +replvar(Find, #{clientid := Clientid, + username := Username, + peerhost := IpAddress + }) -> + Fun = fun + _Fun(K, V, AccIn) when is_map(V) -> maps:put(K, maps:fold(_Fun, AccIn, V), AccIn); + _Fun(K, V, AccIn) when is_list(V) -> + maps:put(K, [ begin + [{K1, V1}] = maps:to_list(M), + _Fun(K1, V1, AccIn) + end || M <- V], + AccIn); + _Fun(K, V, AccIn) when is_binary(V) -> + V1 = re:replace(V, "%c", bin(Clientid), [global, {return, binary}]), + V2 = re:replace(V1, "%u", bin(Username), [global, {return, binary}]), + V3 = re:replace(V2, "%a", inet_parse:ntoa(IpAddress), [global, {return, binary}]), + maps:put(K, V3, AccIn); + _Fun(K, V, AccIn) -> maps:put(K, V, AccIn) + end, + maps:fold(Fun, #{}, Find). + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(B) when is_binary(B) -> B; +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. + diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 5836eb12b..5c82d460e 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -16,6 +16,13 @@ structs() -> ["emqx_authz"]. fields("emqx_authz") -> [ {rules, rules()} ]; +fields(mongo_connector) -> + [ {principal, principal()} + , {type, #{type => hoconsc:enum([mongo])}} + , {config, #{type => map()}} + , {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; fields(redis_connector) -> [ {principal, principal()} , {type, #{type => hoconsc:enum([redis])}} @@ -27,7 +34,6 @@ fields(redis_connector) -> } , {cmd, query()} ]; - fields(sql_connector) -> [ {principal, principal() } , {type, #{type => hoconsc:enum([mysql, pgsql])}} diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 9b5609c2f..dda192252 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -38,7 +38,7 @@ structs() -> [""]. fields("") -> mongodb_fields() ++ mongodb_topology_fields() ++ - mongodb_rs_set_name_fields() ++ + % mongodb_rs_set_name_fields() ++ emqx_connector_schema_lib:ssl_fields(). on_jsonify(Config) -> @@ -71,7 +71,7 @@ on_start(InstId, #{servers := Servers, PoolName = emqx_plugin_libs_pool:pool_name(InstId), _ = emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts), - {ok, #{pool => PoolName, + {ok, #{poolname => PoolName, type => Type, test_conn => TestConn, test_opts => TestOpts}}. @@ -82,23 +82,27 @@ on_stop(InstId, #{poolname := PoolName}) -> on_query(InstId, {Action, Collection, Selector, Docs}, AfterQuery, #{poolname := PoolName} = State) -> logger:debug("mongodb connector ~p received request: ~p, at state: ~p", [InstId, {Action, Collection, Selector, Docs}, State]), - case Result = ecpool:pick_and_do(PoolName, {?MODULE, mongo_query, [Action, Collection, Selector, Docs]}, no_handover) of + case ecpool:pick_and_do(PoolName, {?MODULE, mongo_query, [Action, Collection, Selector, Docs]}, no_handover) of {error, Reason} -> logger:debug("mongodb connector ~p do sql query failed, request: ~p, reason: ~p", [InstId, {Action, Collection, Selector, Docs}, Reason]), - emqx_resource:query_failed(AfterQuery); - _ -> - emqx_resource:query_success(AfterQuery) - end, - Result. + emqx_resource:query_failed(AfterQuery), + {error, Reason}; + {ok, Cursor} when is_pid(Cursor) -> + emqx_resource:query_success(AfterQuery), + mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000); + Result -> + emqx_resource:query_success(AfterQuery), + Result + end. -dialyzer({nowarn_function, [on_health_check/2]}). -on_health_check(_InstId, #{test_opts := TestOpts}) -> +on_health_check(_InstId, #{test_opts := TestOpts} = State) -> case mc_worker_api:connect(TestOpts) of {ok, TestConn} -> mc_worker_api:disconnect(TestConn), - {ok, true}; + {ok, State}; {error, _} -> - {ok, false} + {error, health_check_failed, State} end. %% =================================================================== @@ -197,11 +201,12 @@ mongodb_topology_fields() -> , {min_heartbeat_frequency_ms, fun duration/1} ]. -mongodb_rs_set_name_fields() -> - [ {rs_set_name, fun emqx_connector_schema_lib:database/1} - ]. +% mongodb_rs_set_name_fields() -> +% [ {rs_set_name, fun emqx_connector_schema_lib:database/1} +% ]. auth_source(type) -> binary(); +auth_source(nullable) -> true; auth_source(_) -> undefined. servers(type) -> binary(); @@ -213,4 +218,5 @@ mongo_type(default) -> single; mongo_type(_) -> undefined. duration(type) -> emqx_schema:duration_ms(); +duration(nullable) -> true; duration(_) -> undefined. diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 4e1dc1773..6333cdb1a 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -140,7 +140,7 @@ on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) -> eredis_cluster_pool_worker:is_connected(Pid) =:= true end, Workers) of true -> {ok, State}; - false -> {error, test_query_failed, State} + false -> {error, health_check_failed, State} end; on_health_check(_InstId, #{poolname := PoolName} = State) -> emqx_plugin_libs_pool:health_check(PoolName, fun ?MODULE:do_health_check/1, State). diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 17069c7f0..743d37ae3 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -99,11 +99,11 @@ pool_size(validator) -> [?MIN(1), ?MAX(64)]; pool_size(_) -> undefined. username(type) -> binary(); -username(default) -> "root"; +username(nullable) -> true; username(_) -> undefined. password(type) -> binary(); -password(default) -> ""; +password(nullable) -> true; password(_) -> undefined. auto_reconnect(type) -> boolean(); diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl index 71119264d..03c5bdc8f 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_pool.erl @@ -54,5 +54,5 @@ health_check(PoolName, CheckFunc, State) when is_function(CheckFunc) -> end || {_WorkerName, Worker} <- ecpool:workers(PoolName)], case length(Status) > 0 andalso lists:all(fun(St) -> St =:= true end, Status) of true -> {ok, State}; - false -> {error, test_query_failed, State} + false -> {error, health_check_failed, State} end. From f92b8bb7fb23550044540ac813622af4787ab55e Mon Sep 17 00:00:00 2001 From: Rory Z Date: Mon, 5 Jul 2021 11:09:47 +0800 Subject: [PATCH 088/379] chore(authz): add test case --- .../test/emqx_authz_mongo_SUITE.erl | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl new file mode 100644 index 000000000..daf4d1722 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -0,0 +1,112 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_mongo_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authz.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ), + ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), + Config. + +end_per_suite(_Config) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource). + +set_special_configs(emqx) -> + application:set_env(emqx, allow_anonymous, true), + application:set_env(emqx, enable_acl_cache, false), + application:set_env(emqx, acl_nomatch, deny), + application:set_env(emqx, plugins_loaded_file, + emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), + ok; +set_special_configs(emqx_authz) -> + Rules = [#{config =>#{}, + principal => all, + collection => <<"fake">>, + find => #{<<"a">> => <<"b">>}, + type => mongo} + ], + emqx_config:put([emqx_authz], #{rules => Rules}), + ok; +set_special_configs(_App) -> + ok. + +-define(RULE1,[#{<<"topics">> => [<<"#">>], + <<"permission">> => <<"deny">>, + <<"action">> => <<"all">>}]). +-define(RULE2,[#{<<"topics">> => [<<"eq #">>], + <<"permission">> => <<"allow">>, + <<"action">> => <<"all">>}]). +-define(RULE3,[#{<<"topics">> => [<<"test/%c">>], + <<"permission">> => <<"allow">>, + <<"action">> => <<"subscribe">>}]). +-define(RULE4,[#{<<"topics">> => [<<"test/%u">>], + <<"permission">> => <<"allow">>, + <<"action">> => <<"publish">>}]). + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_authz(_) -> + ClientInfo1 = #{clientid => <<"test">>, + username => <<"test">>, + peerhost => {127,0,0,1} + }, + ClientInfo2 = #{clientid => <<"test_clientid">>, + username => <<"test_username">>, + peerhost => {192,168,0,10} + }, + ClientInfo3 = #{clientid => <<"test_clientid">>, + username => <<"fake_username">>, + peerhost => {127,0,0,1} + }, + + meck:expect(emqx_resource, query, fun(_, _) -> [] end), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"#">>)), % nomatch + + meck:expect(emqx_resource, query, fun(_, _) -> ?RULE1 ++ ?RULE2 end), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, publish, <<"+">>)), + + meck:expect(emqx_resource, query, fun(_, _) -> ?RULE2 ++ ?RULE1 end), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo1, subscribe, <<"#">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo1, subscribe, <<"+">>)), + + meck:expect(emqx_resource, query, fun(_, _) -> ?RULE3 ++ ?RULE4 end), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_clientid">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo2, subscribe, <<"test/test_username">>)), + ?assertEqual(allow, emqx_access_control:authorize(ClientInfo2, publish, <<"test/test_username">>)), + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, subscribe, <<"test">>)), % nomatch + ?assertEqual(deny, emqx_access_control:authorize(ClientInfo3, publish, <<"test">>)), % nomatch + ok. + From f733293a8b5edc4ff902a94ec543349ebeaae164 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Mon, 5 Jul 2021 15:07:03 +0800 Subject: [PATCH 089/379] chore(authz): update apps vsn --- apps/emqx_authz/src/emqx_authz.app.src | 2 +- apps/emqx_authz/src/emqx_authz_mongo.erl | 3 +-- apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl | 13 +++++-------- apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl | 13 +++++-------- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 7 +++---- apps/emqx_connector/src/emqx_connector.app.src | 2 +- 6 files changed, 16 insertions(+), 24 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index 10801eca1..f79e10f85 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,6 +1,6 @@ {application, emqx_authz, [{description, "An OTP application"}, - {vsn, "0.1.0"}, + {vsn, "0.1.1"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl index a106dd0f5..bffb3f07b 100644 --- a/apps/emqx_authz/src/emqx_authz_mongo.erl +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -50,8 +50,7 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [Rule | Tail]) -> - case match(Client, PubSub, Topic, Rule) - of + case match(Client, PubSub, Topic, Rule) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index d84f0722a..edc35ca45 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -47,9 +47,9 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + Rules = [#{config =>#{}, principal => all, - sql => <<"fake sql">>, + sql => <<"fake">>, type => mysql} ], emqx_config:put([emqx_authz], #{rules => Rules}), @@ -76,18 +76,15 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10}, - zone => zone + peerhost => {192,168,0,10} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 78112b5bc..d5f89bcad 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -47,9 +47,9 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + Rules = [#{config =>#{}, principal => all, - sql => <<"fake sql">>, + sql => <<"fake">>, type => pgsql} ], emqx_config:put([emqx_authz], #{rules => Rules}), @@ -76,18 +76,15 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10}, - zone => zone + peerhost => {192,168,0,10} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index edff8a2a9..0d7ffa9d8 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -47,9 +47,9 @@ set_special_configs(emqx) -> emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), ok; set_special_configs(emqx_authz) -> - Rules = [#{config =>#{<<"meck">> => <<"fake">>}, + Rules = [#{config =>#{}, principal => all, - cmd => <<"fake cmd">>, + cmd => <<"fake">>, type => redis} ], emqx_config:put([emqx_authz], #{rules => Rules}), @@ -68,8 +68,7 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo = #{clientid => <<"clientid">>, username => <<"username">>, - peerhost => {127,0,0,1}, - zone => zone + peerhost => {127,0,0,1} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 6eb22cfe5..0b8717a0f 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -1,6 +1,6 @@ {application, emqx_connector, [{description, "An OTP application"}, - {vsn, "0.1.0"}, + {vsn, "0.1.1"}, {registered, []}, {mod, {emqx_connector_app, []}}, {applications, From 45ee504dc5000ff62d071175320d39221b0b86b0 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Mon, 5 Jul 2021 15:11:58 +0800 Subject: [PATCH 090/379] chore(authz): rename ACL to AuthZ --- apps/emqx_authz/README.md | 2 +- apps/emqx_authz/include/emqx_authz.hrl | 12 ++++++------ apps/emqx_authz/src/emqx_authz.erl | 10 +++++----- apps/emqx_authz/src/emqx_authz_mongo.erl | 2 +- apps/emqx_authz/src/emqx_authz_mysql.erl | 2 +- apps/emqx_authz/src/emqx_authz_pgsql.erl | 2 +- apps/emqx_authz/src/emqx_authz_redis.erl | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/emqx_authz/README.md b/apps/emqx_authz/README.md index de0d695ee..a8b4ca170 100644 --- a/apps/emqx_authz/README.md +++ b/apps/emqx_authz/README.md @@ -131,7 +131,7 @@ Sample data in the default configuration: HSET mqtt_acl:emqx '$SYS/#' subscribe ``` -A rule of Redis ACL defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists. +A rule of Redis AuthZ defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists. #### Mongo diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index 9caf3d979..76aa20688 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -6,14 +6,14 @@ -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). -define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= all))). --record(acl_metrics, { - allow = 'client.acl.allow', - deny = 'client.acl.deny', - ignore = 'client.acl.ignore' +-record(authz_metrics, { + allow = 'client.authorize.allow', + deny = 'client.authorize.deny', + ignore = 'client.authorize.ignore' }). -define(METRICS(Type), tl(tuple_to_list(#Type{}))). -define(METRICS(Type, K), #Type{}#Type.K). --define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). +-define(AUTHZ_METRICS, ?METRICS(authz_metrics)). +-define(AUTHZ_METRICS(K), ?METRICS(authz_metrics, K)). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f673b3979..578f6d7cc 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -32,7 +32,7 @@ -spec(register_metrics() -> ok). register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). + lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS). init() -> ok = register_metrics(), @@ -147,10 +147,10 @@ b2l(B) when is_list(B) -> B; b2l(B) when is_binary(B) -> binary_to_list(B). %%-------------------------------------------------------------------- -%% ACL callbacks +%% AuthZ callbacks %%-------------------------------------------------------------------- -%% @doc Check ACL +%% @doc Check AuthZ -spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) -> {stop, allow} | {ok, deny}). authorize(#{username := Username, @@ -159,11 +159,11 @@ authorize(#{username := Username, case do_authorize(Client, PubSub, Topic, Rules) of {matched, allow} -> ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), - emqx_metrics:inc(?ACL_METRICS(allow)), + emqx_metrics:inc(?AUTHZ_METRICS(allow)), {stop, allow}; {matched, deny} -> ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]), - emqx_metrics:inc(?ACL_METRICS(deny)), + emqx_metrics:inc(?AUTHZ_METRICS(deny)), {stop, deny}; nomatch -> ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl index bffb3f07b..04af8f1ec 100644 --- a/apps/emqx_authz/src/emqx_authz_mongo.erl +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -20,7 +20,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks +%% AuthZ Callbacks -export([ authorize/4 , description/0 ]). diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index 672954841..4c769085d 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -20,7 +20,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks +%% AuthZ Callbacks -export([ description/0 , parse_query/1 , authorize/4 diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index fa24c5f5e..d74db36b2 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -20,7 +20,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks +%% AuthZ Callbacks -export([ description/0 , parse_query/1 , authorize/4 diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index dc80d959c..8d24b4534 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -20,7 +20,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -%% ACL Callbacks +%% AuthZ Callbacks -export([ authorize/4 , description/0 ]). From fd0a211629ea831b9837a8f8de068c105477e02a Mon Sep 17 00:00:00 2001 From: Rory Z Date: Mon, 5 Jul 2021 16:10:41 +0800 Subject: [PATCH 091/379] chore(authz): mongo connector support ssl --- .../docker-compose-mongo-tls.yaml | 15 ++++-- apps/emqx_authz/src/emqx_authz_schema.erl | 1 + .../src/emqx_connector_mongo.erl | 47 +++++++++---------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/.ci/docker-compose-file/docker-compose-mongo-tls.yaml b/.ci/docker-compose-file/docker-compose-mongo-tls.yaml index a09bc803d..c4f162783 100644 --- a/.ci/docker-compose-file/docker-compose-mongo-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-mongo-tls.yaml @@ -8,11 +8,16 @@ services: environment: MONGO_INITDB_DATABASE: mqtt volumes: - - ../../apps/emqx_auth_mongo/test/emqx_auth_mongo_SUITE_data/mongodb.pem/:/etc/certs/mongodb.pem + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem networks: - emqx_bridge + ports: + - "27017:27017" command: - --ipv6 - --bind_ip_all - --sslMode requireSSL - --sslPEMKeyFile /etc/certs/mongodb.pem + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --sslMode requireSSL --sslPEMKeyFile /etc/certs/mongodb.pem + diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 5c82d460e..0b6a1d107 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -90,6 +90,7 @@ rules() -> [ hoconsc:ref(?MODULE, simple_rule) , hoconsc:ref(?MODULE, sql_connector) , hoconsc:ref(?MODULE, redis_connector) + , hoconsc:ref(?MODULE, mongo_connector) ]) }. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index dda192252..25d9f36df 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -36,10 +36,28 @@ structs() -> [""]. fields("") -> - mongodb_fields() ++ - mongodb_topology_fields() ++ + [ {mongo_type, fun mongo_type/1} + , {servers, fun servers/1} + , {pool_size, fun emqx_connector_schema_lib:pool_size/1} + , {login, fun emqx_connector_schema_lib:username/1} + , {password, fun emqx_connector_schema_lib:password/1} + , {auth_source, fun auth_source/1} + , {database, fun emqx_connector_schema_lib:database/1} + ] ++ % mongodb_rs_set_name_fields() ++ - emqx_connector_schema_lib:ssl_fields(). + emqx_connector_schema_lib:ssl_fields(); +fields(topology) -> + [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} + , {overflow_ttl, fun duration/1} + , {overflow_check_period, fun duration/1} + , {local_threshold_ms, fun duration/1} + , {connect_timeout_ms, fun duration/1} + , {socket_timeout_ms, fun duration/1} + , {server_selection_timeout_ms, fun duration/1} + , {wait_queue_timeout_ms, fun duration/1} + , {heartbeat_frequency_ms, fun duration/1} + , {min_heartbeat_frequency_ms, fun duration/1} + ]. on_jsonify(Config) -> Config. @@ -178,29 +196,6 @@ host_port(HostPort) -> [{host, Host1}] end. -mongodb_fields() -> - [ {mongo_type, fun mongo_type/1} - , {servers, fun servers/1} - , {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {login, fun emqx_connector_schema_lib:username/1} - , {password, fun emqx_connector_schema_lib:password/1} - , {auth_source, fun auth_source/1} - , {database, fun emqx_connector_schema_lib:database/1} - ]. - -mongodb_topology_fields() -> - [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} - , {overflow_ttl, fun duration/1} - , {overflow_check_period, fun duration/1} - , {local_threshold_ms, fun duration/1} - , {connect_timeout_ms, fun duration/1} - , {socket_timeout_ms, fun duration/1} - , {server_selection_timeout_ms, fun duration/1} - , {wait_queue_timeout_ms, fun duration/1} - , {heartbeat_frequency_ms, fun duration/1} - , {min_heartbeat_frequency_ms, fun duration/1} - ]. - % mongodb_rs_set_name_fields() -> % [ {rs_set_name, fun emqx_connector_schema_lib:database/1} % ]. From 6d92d87ae78ea0f4f23626b1a6b52406ca447da8 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 19:11:05 +0800 Subject: [PATCH 092/379] feat(map_lib): support emqx_map_lib:deep_merge/2 --- apps/emqx/src/emqx_map_lib.erl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 8b3108a53..477891c51 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -19,6 +19,7 @@ , deep_get/3 , deep_find/2 , deep_put/3 + , deep_merge/2 , safe_atom_key_map/1 , unsafe_atom_key_map/1 ]). @@ -60,6 +61,23 @@ deep_put([Key | KeyPath], Map, Config) -> SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config), Map#{Key => SubMap}. +%% #{a => #{b => 3, c => 2}, d => 4} +%% = deep_merge(#{a => #{b => 1, c => 2}, d => 4}, #{a => #{b => 3}}). +-spec deep_merge(map(), map()) -> map(). +deep_merge(BaseMap, NewMap) -> + NewKeys = maps:keys(NewMap) -- maps:keys(BaseMap), + MergedBase = maps:fold(fun(K, V, Acc) -> + case maps:find(K, NewMap) of + error -> + Acc#{K => V}; + {ok, NewV} when is_map(V), is_map(NewV) -> + Acc#{K => deep_merge(V, NewV)}; + {ok, NewV} -> + Acc#{K => NewV} + end + end, #{}, BaseMap), + maps:merge(MergedBase, maps:with(NewKeys, NewMap)). + unsafe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). From 10b01a34ef2492fb8104924e78f411f84266f6cc Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 19:12:26 +0800 Subject: [PATCH 093/379] refactor(listeners): reformat the code for starting stopping listeners --- apps/emqx/src/emqx_listeners.erl | 39 +++++++++++++++++++------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index aa4fbe7f1..b9830578a 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -36,11 +36,7 @@ %% @doc Start all listeners. -spec(start() -> ok). start() -> - lists:foreach(fun({ZoneName, ZoneConf}) -> - lists:foreach(fun({LName, LConf}) -> - start_listener(ZoneName, LName, LConf) - end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) - end, maps:to_list(emqx_config:get([zones], #{}))). + foreach_listeners(fun start_listener/3). -spec(start_listener(atom()) -> ok). start_listener(Id) -> @@ -93,7 +89,12 @@ esockd_opts(Opts0) -> Rate -> Opts1#{max_conn_rate => Rate} end, Opts3 = Opts2#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))}, - maps:to_list(Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)}). + maps:to_list(case is_ssl(Opts0) of + false -> + Opts3#{tcp_options => tcp_opts(Opts0)}; + true -> + Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} + end). ws_opts(ZoneName, ListenerName, Opts) -> WsPaths = [{maps:get(mqtt_path, Opts, "/mqtt"), emqx_ws_connection, @@ -128,11 +129,7 @@ esockd_access_rules(StrRules) -> %% @doc Restart all listeners -spec(restart() -> ok). restart() -> - lists:foreach(fun({ZoneName, ZoneConf}) -> - lists:foreach(fun({LName, LConf}) -> - restart_listener(ZoneName, LName, LConf) - end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) - end, maps:to_list(emqx_config:get([zones], #{}))). + foreach_listeners(fun restart_listener/3). -spec(restart_listener(atom()) -> ok | {error, any()}). restart_listener(ListenerID) -> @@ -150,11 +147,7 @@ restart_listener(ZoneName, ListenerName, Conf) -> %% @doc Stop all listeners. -spec(stop() -> ok). stop() -> - lists:foreach(fun({ZoneName, ZoneConf}) -> - lists:foreach(fun({LName, LConf}) -> - stop_listener(ZoneName, LName, LConf) - end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) - end, maps:to_list(emqx_config:get([zones], #{}))). + foreach_listeners(fun stop_listener/3). -spec(stop_listener(atom()) -> ok | {error, term()}). stop_listener(ListenerID) -> @@ -205,3 +198,17 @@ tcp_opts(Opts) -> is_ssl(Opts) -> emqx_map_lib:deep_get([ssl, enable], Opts, false). + +foreach_listeners(Do) -> + lists:foreach(fun({ZoneName, ZoneConf}) -> + lists:foreach(fun({LName, LConf}) -> + Do(ZoneName, LName, merge_zone_and_listener_confs(ZoneConf, LConf)) + end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) + end, maps:to_list(emqx_config:get([zones], #{}))). + +%% merge the configs in zone and listeners in a manner that +%% all config entries in the listener are prior to the ones in the zone. +merge_zone_and_listener_confs(ZoneConf, ListenerConf) -> + ConfsInZonesOnly = [listeners, overall_max_connections], + BaseConf = maps:without(ConfsInZonesOnly, ZoneConf), + emqx_map_lib:deep_merge(BaseConf, ListenerConf). From 969e72c82bb49737db398e842abc8843c67c010e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 5 Jul 2021 19:13:55 +0800 Subject: [PATCH 094/379] refactor(connection): remove active_n from state --- apps/emqx/etc/emqx.conf | 5 ++-- apps/emqx/src/emqx_connection.erl | 40 +++++++++++++++------------- apps/emqx/src/emqx_ws_connection.erl | 28 ++++++++++--------- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 58417b7cf..2179fba90 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -815,7 +815,6 @@ broker { ## - `flapping_detect.*` ## - `force_shutdown.*` ## - `conn_congestion.*` -## - `overall_max_connections` ## ## Syntax: zones. {} zones.default { @@ -833,7 +832,9 @@ zones.default { ## Default: true stats.enable: true - ## Maximum number of concurrent connections. + ## Maximum number of concurrent connections in this zone. + ## This value must be larger than the sum of `max_connections` set + ## in the listeners under this zone. ## ## @doc zones..overall_max_connections ## ValueType: Number | infinity diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 54d3a4691..1cbc36e05 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -83,8 +83,6 @@ sockname :: emqx_types:peername(), %% Sock State sockstate :: emqx_types:sockstate(), - %% The {active, N} option - active_n :: pos_integer(), %% Limiter limiter :: maybe(emqx_limiter:limiter()), %% Limit Timer @@ -102,14 +100,18 @@ %% Idle Timeout idle_timeout :: integer(), %% Idle Timer - idle_timer :: maybe(reference()) + idle_timer :: maybe(reference()), + %% Zone name + zone :: atom(), + %% Listener Name + listener :: atom() }). -type(state() :: #state{}). -type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). @@ -166,8 +168,6 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active_n, #state{active_n = ActiveN}) -> - ActiveN; info(stats_timer, #state{stats_timer = StatsTimer}) -> StatsTimer; info(limit_timer, #state{limit_timer = LimitTimer}) -> @@ -254,8 +254,9 @@ init_state(Transport, Socket, Options) -> peercert => Peercert, conn_mod => ?MODULE }, - Zone = proplists:get_value(zone, Options), - ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), + Zone = maps:get(zone, Options), + Listener = maps:get(listener, Options), + PubLimit = emqx_zone:publish_limit(Zone), BytesIn = proplists:get_value(rate_limit, Options), RateLimit = emqx_zone:ratelimit(Zone), @@ -273,7 +274,6 @@ init_state(Transport, Socket, Options) -> peername = Peername, sockname = Sockname, sockstate = idle, - active_n = ActiveN, limiter = Limiter, parse_state = ParseState, serialize = Serialize, @@ -281,7 +281,9 @@ init_state(Transport, Socket, Options) -> gc_state = GcState, stats_timer = StatsTimer, idle_timeout = IdleTimeout, - idle_timer = IdleTimer + idle_timer = IdleTimer, + zone = Zone, + listener = Listener }. run_loop(Parent, State = #state{transport = Transport, @@ -452,14 +454,16 @@ handle_msg({Passive, _Sock}, State) NState1 = check_oom(run_gc(InStats, NState)), handle_info(activate_socket, NState1); -handle_msg(Deliver = {deliver, _Topic, _Msg}, - #state{active_n = ActiveN} = State) -> +handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{zone = Zone, + listener = Listener} = State) -> + ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent -handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) -> - case emqx_pd:get_counter(outgoing_pubs) > ActiveN of +handle_msg({inet_reply, _Sock, ok}, State = #state{zone = Zone, listener = Listener}) -> + case emqx_pd:get_counter(outgoing_pubs) > + emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of true -> Pubs = emqx_pd:reset_counter(outgoing_pubs), Bytes = emqx_pd:reset_counter(outgoing_bytes), @@ -799,10 +803,10 @@ activate_socket(State = #state{sockstate = closed}) -> {ok, State}; activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; -activate_socket(State = #state{transport = Transport, - socket = Socket, - active_n = N}) -> - case Transport:setopts(Socket, [{active, N}]) of +activate_socket(State = #state{transport = Transport, socket = Socket, + zone = Zone, listener = Listener}) -> + ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), + case Transport:setopts(Socket, [{active, ActiveN}]) of ok -> {ok, State#state{sockstate = running}}; Error -> Error end. diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index 7bc68c271..b50505bf8 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -62,8 +62,6 @@ sockname :: emqx_types:peername(), %% Sock state sockstate :: emqx_types:sockstate(), - %% Simulate the active_n opt - active_n :: pos_integer(), %% MQTT Piggyback mqtt_piggyback :: single | multiple, %% Limiter @@ -85,7 +83,11 @@ %% Idle Timeout idle_timeout :: timeout(), %% Idle Timer - idle_timer :: maybe(reference()) + idle_timer :: maybe(reference()), + %% Zone name + zone :: atom(), + %% Listener Name + listener :: atom() }). -type(state() :: #state{}). @@ -93,7 +95,7 @@ -type(ws_cmd() :: {active, boolean()}|close). -define(ACTIVE_N, 100). --define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]). +-define(INFO_KEYS, [socktype, peername, sockname, sockstate]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). @@ -124,8 +126,6 @@ info(sockname, #state{sockname = Sockname}) -> Sockname; info(sockstate, #state{sockstate = SockSt}) -> SockSt; -info(active_n, #state{active_n = ActiveN}) -> - ActiveN; info(limiter, #state{limiter = Limiter}) -> maybe_apply(fun emqx_limiter:info/1, Limiter); info(channel, #state{channel = Channel}) -> @@ -293,7 +293,6 @@ websocket_init([Req, Opts]) -> BytesIn = proplists:get_value(rate_limit, Opts), RateLimit = emqx_zone:ratelimit(Zone), Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit), - ActiveN = proplists:get_value(active_n, Opts, ?ACTIVE_N), MQTTPiggyback = proplists:get_value(mqtt_piggyback, Opts, multiple), FrameOpts = emqx_zone:mqtt_frame_options(Zone), ParseState = emqx_frame:initial_parse_state(FrameOpts), @@ -309,7 +308,6 @@ websocket_init([Req, Opts]) -> {ok, #state{peername = Peername, sockname = Sockname, sockstate = running, - active_n = ActiveN, mqtt_piggyback = MQTTPiggyback, limiter = Limiter, parse_state = ParseState, @@ -372,7 +370,8 @@ websocket_info({check_gc, Stats}, State) -> return(check_oom(run_gc(Stats, State))); websocket_info(Deliver = {deliver, _Topic, _Msg}, - State = #state{active_n = ActiveN}) -> + State = #state{zone = Zone, listener = Listener}) -> + ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); @@ -551,11 +550,12 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) -> %% Handle incoming packet %%-------------------------------------------------------------------- -handle_incoming(Packet, State = #state{active_n = ActiveN}) +handle_incoming(Packet, State = #state{zone = Zone, listener = Listener}) when is_record(Packet, mqtt_packet) -> ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ok = inc_incoming_stats(Packet), - NState = case emqx_pd:get_counter(incoming_pubs) > ActiveN of + NState = case emqx_pd:get_counter(incoming_pubs) > + emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of true -> postpone({cast, rate_limit}, State); false -> State end, @@ -586,11 +586,13 @@ with_channel(Fun, Args, State = #state{channel = Channel}) -> %% Handle outgoing packets %%-------------------------------------------------------------------- -handle_outgoing(Packets, State = #state{active_n = ActiveN, mqtt_piggyback = MQTTPiggyback}) -> +handle_outgoing(Packets, State = #state{mqtt_piggyback = MQTTPiggyback, + zone = Zone, listener = Listener}) -> IoData = lists:map(serialize_and_inc_stats_fun(State), Packets), Oct = iolist_size(IoData), ok = inc_sent_stats(length(Packets), Oct), - NState = case emqx_pd:get_counter(outgoing_pubs) > ActiveN of + NState = case emqx_pd:get_counter(outgoing_pubs) > + emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of true -> Stats = #{cnt => emqx_pd:reset_counter(outgoing_pubs), oct => emqx_pd:reset_counter(outgoing_bytes) From b2801299cbb47083dff385f65d83403dfee17452 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 30 Jun 2021 19:18:12 +0800 Subject: [PATCH 095/379] chore(CI): cancel build of 32-bit docker --- .github/workflows/build_packages.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index de4315d36..99ea45d29 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -342,13 +342,6 @@ jobs: - [amd64, x86_64] - [arm64v8, aarch64] - [arm32v7, arm] - - [i386, i386] - - [s390x, s390x] - exclude: - - profile: emqx-ee - arch: [i386, i386] - - profile: emqx-ee - arch: [s390x, s390x] steps: - uses: actions/download-artifact@v2 From 606e48560cf6893cb675c48991061ec068031232 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 2 Jul 2021 11:52:57 +0200 Subject: [PATCH 096/379] ci: build-package set fail-fast to false To get better overall vision --- .github/workflows/build_packages.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 99ea45d29..0f1f8ccac 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -63,6 +63,7 @@ jobs: if: endsWith(github.repository, 'emqx') strategy: + fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} exclude: @@ -131,6 +132,7 @@ jobs: needs: prepare strategy: + fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} erl_otp: @@ -210,6 +212,7 @@ jobs: needs: prepare strategy: + fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} arch: From 53df218e6a57d8faea9c5d37dbebc99af9f8b02e Mon Sep 17 00:00:00 2001 From: Rory Z Date: Tue, 6 Jul 2021 14:53:28 +0800 Subject: [PATCH 097/379] feat(connector): mongo support replica set --- .../docker-compose-mongo-replicaset-tcp.yaml | 81 ++++++++++ .../docker-compose-mongo-replicaset-tls.yaml | 98 ++++++++++++ ...l => docker-compose-mongo-single-tcp.yaml} | 0 ...l => docker-compose-mongo-single-tls.yaml} | 0 apps/emqx_authz/src/emqx_authz.erl | 1 + .../src/emqx_connector_mongo.erl | 143 +++++++++++------- .../src/emqx_connector_redis.erl | 6 +- 7 files changed, 275 insertions(+), 54 deletions(-) create mode 100644 .ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml create mode 100644 .ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml rename .ci/docker-compose-file/{docker-compose-mongo-tcp.yaml => docker-compose-mongo-single-tcp.yaml} (100%) rename .ci/docker-compose-file/{docker-compose-mongo-tls.yaml => docker-compose-mongo-single-tls.yaml} (100%) diff --git a/.ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml new file mode 100644 index 000000000..f83fe0932 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tcp.yaml @@ -0,0 +1,81 @@ +version: "3" + +services: + mongo1: + hostname: mongo1 + container_name: mongo1 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27011:27017 + restart: always + command: + --ipv6 + --bind_ip_all + --replSet rs0 + + mongo2: + hostname: mongo2 + container_name: mongo2 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27012:27017 + restart: always + command: + --ipv6 + --bind_ip_all + --replSet rs0 + + mongo3: + hostname: mongo3 + container_name: mongo3 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27013:27017 + restart: always + command: + --ipv6 + --bind_ip_all + --replSet rs0 + + mongo_client: + image: mongo:${MONGO_TAG} + container_name: mongo_client + networks: + - emqx_bridge + depends_on: + - mongo1 + - mongo2 + - mongo3 + command: + - /bin/bash + - -c + - | + while ! mongo --host mongo1 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo2 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo3 --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + mongo --host mongo1 --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongo1:27017' }, { _id : 1, host : 'mongo2:27017' }, { _id : 2, host : 'mongo3:27017' } ] })" --quiet + mongo --host mongo1 --eval "rs.status()" --quiet diff --git a/.ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml new file mode 100644 index 000000000..be8f7ea21 --- /dev/null +++ b/.ci/docker-compose-file/docker-compose-mongo-replicaset-tls.yaml @@ -0,0 +1,98 @@ +version: "3" + +services: + mongo1: + hostname: mongo1 + container_name: mongo1 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27011:27017 + restart: always + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0 + + mongo2: + hostname: mongo2 + container_name: mongo2 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27012:27017 + restart: always + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0 + + mongo3: + hostname: mongo3 + container_name: mongo3 + image: mongo:${MONGO_TAG} + environment: + MONGO_INITDB_DATABASE: mqtt + networks: + - emqx_bridge + expose: + - 27017 + ports: + - 27013:27017 + restart: always + volumes: + - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/cert.pem + - ../../apps/emqx/etc/certs/key.pem:/etc/certs/key.pem + command: + - /bin/bash + - -c + - | + cat /etc/certs/key.pem /etc/certs/cert.pem > /etc/certs/mongodb.pem + mongod --ipv6 --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /etc/certs/mongodb.pem --replSet rs0 + + mongo_client: + image: mongo:${MONGO_TAG} + container_name: mongo_client + networks: + - emqx_bridge + depends_on: + - mongo1 + - mongo2 + - mongo3 + volumes: + - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/cacert.pem + command: + - /bin/bash + - -c + - | + while ! mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo2 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + while ! mongo --host mongo3 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval 'db.runCommand("ping").ok' --quiet > /dev/null 2>&1; do + sleep 1 + done + mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongo1:27017' }, { _id : 1, host : 'mongo2:27017' }, { _id : 2, host : 'mongo3:27017' } ] })" --quiet + mongo --host mongo1 --tls --tlsCAFile /etc/certs/cacert.pem --tlsAllowInvalidHostnames --eval "rs.status()" --quiet diff --git a/.ci/docker-compose-file/docker-compose-mongo-tcp.yaml b/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml similarity index 100% rename from .ci/docker-compose-file/docker-compose-mongo-tcp.yaml rename to .ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml diff --git a/.ci/docker-compose-file/docker-compose-mongo-tls.yaml b/.ci/docker-compose-file/docker-compose-mongo-single-tls.yaml similarity index 100% rename from .ci/docker-compose-file/docker-compose-mongo-tls.yaml rename to .ci/docker-compose-file/docker-compose-mongo-single-tls.yaml diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 578f6d7cc..725d884e1 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -66,6 +66,7 @@ create_resource(#{type := DB, ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]), NConfig = case DB of redis -> #{config => Config }; + mongo -> #{config => Config }; _ -> Config end, case emqx_resource:check_and_create( diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 25d9f36df..bac97a41a 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -19,6 +19,9 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). +-type server() :: string(). +-reflect_type([server/0]). + %% callbacks of behaviour emqx_resource -export([ on_start/2 , on_stop/2 @@ -36,16 +39,28 @@ structs() -> [""]. fields("") -> - [ {mongo_type, fun mongo_type/1} + [ {config, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, single) + , hoconsc:ref(?MODULE, rs) + , hoconsc:ref(?MODULE, sharded) + ])}} + ]; +fields(single) -> + [ {mongo_type, #{type => single, + default => single}} + , {server, fun server/1} + ] ++ mongo_fields(); +fields(rs) -> + [ {mongo_type, #{type => rs, + default => rs}} , {servers, fun servers/1} - , {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {login, fun emqx_connector_schema_lib:username/1} - , {password, fun emqx_connector_schema_lib:password/1} - , {auth_source, fun auth_source/1} - , {database, fun emqx_connector_schema_lib:database/1} - ] ++ - % mongodb_rs_set_name_fields() ++ - emqx_connector_schema_lib:ssl_fields(); + , {replicaset_name, fun emqx_connector_schema_lib:database/1} + ] ++ mongo_fields(); +fields(sharded) -> + [ {mongo_type, #{type => sharded, + default => sharded}} + , {servers, fun servers/1} + ] ++ mongo_fields(); fields(topology) -> [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} , {overflow_ttl, fun duration/1} @@ -59,40 +74,42 @@ fields(topology) -> , {min_heartbeat_frequency_ms, fun duration/1} ]. +mongo_fields() -> + [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} + , {login, fun emqx_connector_schema_lib:username/1} + , {password, fun emqx_connector_schema_lib:password/1} + , {auth_source, fun auth_source/1} + , {database, fun emqx_connector_schema_lib:database/1} + ] ++ + emqx_connector_schema_lib:ssl_fields(). + on_jsonify(Config) -> Config. %% =================================================================== -on_start(InstId, #{servers := Servers, - mongo_type := Type, - database := Database, - pool_size := PoolSize, - ssl := SSL} = Config) -> +on_start(InstId, #{config := #{server := Server, + mongo_type := single} = Config}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), - SslOpts = case maps:get(enable, SSL) of - true -> - [{ssl, true}, - {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} - ]; - false -> [{ssl, false}] - end, - Hosts = [string:trim(H) || H <- string:tokens(binary_to_list(Servers), ",")], - Opts = [{type, init_type(Type, Config)}, - {hosts, Hosts}, - {pool_size, PoolSize}, - {options, init_topology_options(maps:to_list(Config), [])}, - {worker_options, init_worker_options(maps:to_list(Config), SslOpts)}], + Opts = [{type, single}, + {hosts, [Server]} + ], + do_start(InstId, Opts, Config); - %% test the connection - TestOpts = [{database, Database}] ++ host_port(hd(Hosts)), - {ok, TestConn} = mc_worker_api:connect(TestOpts), +on_start(InstId, #{config := #{servers := Servers, + mongo_type := rs, + replicaset_name := RsName} = Config}) -> + logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), + Opts = [{type, {rs, RsName}}, + {hosts, Servers}], + do_start(InstId, Opts, Config); - PoolName = emqx_plugin_libs_pool:pool_name(InstId), - _ = emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts), - {ok, #{poolname => PoolName, - type => Type, - test_conn => TestConn, - test_opts => TestOpts}}. +on_start(InstId, #{config := #{servers := Servers, + mongo_type := sharded} = Config}) -> + logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), + Opts = [{type, sharded}, + {hosts, Servers} + ], + do_start(InstId, Opts, Config). on_stop(InstId, #{poolname := PoolName}) -> logger:info("stopping mongodb connector: ~p", [InstId]), @@ -138,10 +155,38 @@ mongo_query(Conn, find, Collection, Selector, Docs) -> mongo_query(_Conn, _Action, _Collection, _Selector, _Docs) -> ok. -init_type(rs, #{rs_set_name := Name}) -> - {rs, Name}; -init_type(Type, _Opts) -> - Type. +do_start(InstId, Opts0, Config = #{mongo_type := Type, + database := Database, + pool_size := PoolSize, + ssl := SSL}) -> + SslOpts = case maps:get(enable, SSL) of + true -> + [{ssl, true}, + {ssl_opts, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} + ]; + false -> [{ssl, false}] + end, + Opts = Opts0 ++ + [{pool_size, PoolSize}, + {options, init_topology_options(maps:to_list(Config), [])}, + {worker_options, init_worker_options(maps:to_list(Config), SslOpts)}], + %% test the connection + TestOpts = case maps:is_key(server, Config) of + true -> + Server = maps:get(server, Config), + host_port(Server); + false -> + Servers = maps:get(servers, Config), + host_port(erlang:hd(Servers)) + end ++ [{database, Database}], + {ok, TestConn} = mc_worker_api:connect(TestOpts), + + PoolName = emqx_plugin_libs_pool:pool_name(InstId), + _ = emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts), + {ok, #{poolname => PoolName, + type => Type, + test_conn => TestConn, + test_opts => TestOpts}}. init_topology_options([{pool_size, Val}| R], Acc) -> init_topology_options(R, [{pool_size, Val}| Acc]); @@ -196,22 +241,18 @@ host_port(HostPort) -> [{host, Host1}] end. -% mongodb_rs_set_name_fields() -> -% [ {rs_set_name, fun emqx_connector_schema_lib:database/1} -% ]. +server(type) -> server(); +server(validator) -> [?REQUIRED("the field 'server' is required")]; +server(_) -> undefined. + +servers(type) -> hoconsc:array(server()); +servers(validator) -> [?REQUIRED("the field 'servers' is required")]; +servers(_) -> undefined. auth_source(type) -> binary(); auth_source(nullable) -> true; auth_source(_) -> undefined. -servers(type) -> binary(); -servers(validator) -> [?REQUIRED("the field 'servers' is required")]; -servers(_) -> undefined. - -mongo_type(type) -> hoconsc:enum([single, unknown, shared, rs]); -mongo_type(default) -> single; -mongo_type(_) -> undefined. - duration(type) -> emqx_schema:duration_ms(); duration(nullable) -> true; duration(_) -> undefined. diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 6333cdb1a..0df12185d 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -45,9 +45,9 @@ structs() -> [""]. fields("") -> [ {config, #{type => hoconsc:union( - [ hoconsc:ref(cluster) - , hoconsc:ref(single) - , hoconsc:ref(sentinel) + [ hoconsc:ref(?MODULE, cluster) + , hoconsc:ref(?MODULE, single) + , hoconsc:ref(?MODULE, sentinel) ])} } ]; From 72f9e60d63e6f464f18f8a592b716981dc5af759 Mon Sep 17 00:00:00 2001 From: lafirest Date: Mon, 5 Jul 2021 14:31:21 +0800 Subject: [PATCH 098/379] feat(emqx_retainer): add simple restapi for emqx_retainer --- apps/emqx_retainer/etc/emqx_retainer.conf | 2 + apps/emqx_retainer/src/emqx_retainer.app.src | 2 +- .../emqx_retainer/src/emqx_retainer.appup.src | 15 -- apps/emqx_retainer/src/emqx_retainer.erl | 37 +++- apps/emqx_retainer/src/emqx_retainer_api.erl | 61 +++++++ apps/emqx_retainer/src/emqx_retainer_app.erl | 1 - .../src/emqx_retainer_schema.erl | 3 +- .../test/emqx_retainer_SUITE.erl | 3 +- .../test/emqx_retainer_api_SUITE.erl | 158 ++++++++++++++++++ .../test/mqtt_protocol_v5_SUITE.erl | 12 +- 10 files changed, 271 insertions(+), 23 deletions(-) delete mode 100644 apps/emqx_retainer/src/emqx_retainer.appup.src create mode 100644 apps/emqx_retainer/src/emqx_retainer_api.erl create mode 100644 apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl diff --git a/apps/emqx_retainer/etc/emqx_retainer.conf b/apps/emqx_retainer/etc/emqx_retainer.conf index 08220a207..f91b3aa4f 100644 --- a/apps/emqx_retainer/etc/emqx_retainer.conf +++ b/apps/emqx_retainer/etc/emqx_retainer.conf @@ -6,6 +6,8 @@ ## ## Notice that all nodes in the same cluster have to be configured to emqx_retainer: { + ## enable/disable emqx_retainer + enable: true ## use the same storage_type. ## ## Value: ram | disc | disc_only diff --git a/apps/emqx_retainer/src/emqx_retainer.app.src b/apps/emqx_retainer/src/emqx_retainer.app.src index c5ca7599d..4bc3b962b 100644 --- a/apps/emqx_retainer/src/emqx_retainer.app.src +++ b/apps/emqx_retainer/src/emqx_retainer.app.src @@ -1,6 +1,6 @@ {application, emqx_retainer, [{description, "EMQ X Retainer"}, - {vsn, "4.3.2"}, % strict semver, bump manually! + {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_retainer_sup]}, {applications, [kernel,stdlib]}, diff --git a/apps/emqx_retainer/src/emqx_retainer.appup.src b/apps/emqx_retainer/src/emqx_retainer.appup.src deleted file mode 100644 index 759ec56bd..000000000 --- a/apps/emqx_retainer/src/emqx_retainer.appup.src +++ /dev/null @@ -1,15 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {<<"4.3.[0-1]">>, [ - {restart_application, emqx_retainer} - ]}, - {<<".*">>, []} - ], - [ - {<<"4.3.[0-1]">>, [ - {restart_application, emqx_retainer} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index affbc5ca3..961578870 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -27,15 +27,15 @@ -export([start_link/0]). --export([ load/0 - , unload/0 +-export([unload/0 ]). -export([ on_session_subscribed/3 , on_message_publish/1 ]). --export([clean/1]). +-export([ clean/1 + , update_config/1]). %% for emqx_pool task func -export([dispatch/2]). @@ -56,6 +56,7 @@ -define(DEF_MAX_RETAINED_MESSAGES, 0). -define(DEF_MAX_PAYLOAD_SIZE, (1024 * 1024)). -define(DEF_EXPIRY_INTERVAL, 0). +-define(DEF_ENABLE_VAL, false). %% convenient to generate stats_timer/expiry_timer -define(MAKE_TIMER(State, Timer, Interval, Msg), @@ -130,6 +131,15 @@ clean(Topic) when is_binary(Topic) -> {atomic, N} = ekka_mnesia:transaction(?RETAINER_SHARD, Fun), N end. +%%-------------------------------------------------------------------- +%% Update Config +%%-------------------------------------------------------------------- +-spec update_config(hocon:config()) -> ok. +update_config(Conf) -> + OldCfg = emqx_config:get([?APP]), + emqx_config:put([?APP], Conf), + check_enable_when_update(OldCfg). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- @@ -162,6 +172,7 @@ init([]) -> end, StatsFun = emqx_stats:statsfun('retained.count', 'retained.max'), State = ?MAKE_TIMER(#state{stats_fun = StatsFun}, stats_timer, ?STATS_INTERVAL, stats), + check_enable_when_init(), {ok, start_expire_timer(ExpiryInterval, State)}. start_expire_timer(0, State) -> @@ -321,3 +332,23 @@ condition(Ws) -> false -> Ws1; _ -> (Ws1 -- ['#']) ++ '_' end. + +-spec check_enable_when_init() -> ok. +check_enable_when_init() -> + case emqx_config:get([?APP, enable], ?DEF_ENABLE_VAL) of + true -> load(); + _ -> ok + end. + +-spec check_enable_when_update(hocon:config()) -> ok. +check_enable_when_update(OldCfg) -> + OldVal = maps:get(enable, OldCfg, undefined), + case emqx_config:get([?APP, enable], ?DEF_ENABLE_VAL) of + OldVal -> + ok; + true -> + load(); + _ -> + unload() + end. + diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl new file mode 100644 index 000000000..237a3b19c --- /dev/null +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -0,0 +1,61 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_retainer_api). + +-rest_api(#{name => lookup_config, + method => 'GET', + path => "/retainer", + func => lookup_config, + descr => "lookup retainer config" + }). + +-rest_api(#{name => update_config, + method => 'PUT', + path => "/retainer", + func => update_config, + descr => "update retainer config" + }). + +-export([ lookup_config/2 + , update_config/2 + ]). + +lookup_config(_Bindings, _Params) -> + Config = emqx_config:get([emqx_retainer]), + minirest:return({ok, Config}). + +update_config(_Bindings, Params) -> + try + ConfigList = proplists:get_value(<<"emqx_retainer">>, Params), + {ok, RawConf} = hocon:binary(jsx:encode(#{<<"emqx_retainer">> => ConfigList}), + #{format => richmap}), + RichConf = hocon_schema:check(emqx_retainer_schema, RawConf, #{atom_key => true}), + #{emqx_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), + Action = proplists:get_value(<<"action">>, Params, undefined), + do_update_config(Action, Conf), + minirest:return() + catch _:_:Reason -> + minirest:return({error, Reason}) + end. + +%%------------------------------------------------------------------------------ +%% Interval Funcs +%%------------------------------------------------------------------------------ +do_update_config(undefined, Config) -> + emqx_retainer:update_config(Config); +do_update_config(<<"test">>, _) -> + ok. diff --git a/apps/emqx_retainer/src/emqx_retainer_app.erl b/apps/emqx_retainer/src/emqx_retainer_app.erl index 3f42ddbd6..3626bbd00 100644 --- a/apps/emqx_retainer/src/emqx_retainer_app.erl +++ b/apps/emqx_retainer/src/emqx_retainer_app.erl @@ -26,7 +26,6 @@ start(_Type, _Args) -> {ok, Sup} = emqx_retainer_sup:start_link(), - emqx_retainer:load(), emqx_retainer_cli:load(), {ok, Sup}. diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl index ece873dae..14f643823 100644 --- a/apps/emqx_retainer/src/emqx_retainer_schema.erl +++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl @@ -11,7 +11,8 @@ structs() -> ["emqx_retainer"]. fields("emqx_retainer") -> - [ {storage_type, t(storage_type(), ram)} + [ {enable, t(boolean(), false)} + , {storage_type, t(storage_type(), ram)} , {max_retained_messages, t(integer(), 0, fun is_pos_integer/1)} , {max_payload_size, t(emqx_schema:bytesize(), "1MB")} , {expiry_interval, t(emqx_schema:duration_ms(), "0s")} diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index 4f549a385..cf74b1334 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl @@ -55,7 +55,8 @@ set_special_configs(_) -> init_emqx_retainer_conf(Expiry) -> emqx_config:put([emqx_retainer], - #{storage_type => ram, + #{enable => true, + storage_type => ram, max_retained_messages => 0, max_payload_size => 1024 * 1024, expiry_interval => Expiry}). diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl new file mode 100644 index 000000000..57196c7bd --- /dev/null +++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl @@ -0,0 +1,158 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_retainer_api_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_retainer.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-import(emqx_ct_http, [ request_api/3 + , request_api/5 + , get_http_data/1 + , create_default_app/0 + , delete_default_app/0 + , default_auth_header/0 + ]). + +-define(HOST, "http://127.0.0.1:8081/"). +-define(API_VERSION, "v4"). +-define(BASE_PATH, "api"). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + application:stop(emqx_retainer), + emqx_ct_helpers:start_apps([emqx_retainer, emqx_management], fun set_special_configs/1), + create_default_app(), + Config. + +end_per_suite(_Config) -> + delete_default_app(), + emqx_ct_helpers:stop_apps([emqx_retainer]). + +init_per_testcase(_, Config) -> + Config. + +set_special_configs(emqx_retainer) -> + init_emqx_retainer_conf(0); +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], + default_application_id => <<"admin">>, + default_application_secret => <<"public">>}), + ok; +set_special_configs(_) -> + ok. + +init_emqx_retainer_conf(Expiry) -> + emqx_config:put([emqx_retainer], + #{enable => true, + storage_type => ram, + max_retained_messages => 0, + max_payload_size => 1024 * 1024, + expiry_interval => Expiry}). +%%------------------------------------------------------------------------------ +%% Test Cases +%%------------------------------------------------------------------------------ + +t_config(_Config) -> + {ok, Return} = request_http_rest_lookup(["retainer"]), + NowCfg = get_http_data(Return), + NewCfg = NowCfg#{<<"expiry_interval">> => timer:seconds(60)}, + RetainerConf = #{<<"emqx_retainer">> => NewCfg}, + + {ok, _} = request_http_rest_update(["retainer?action=test"], RetainerConf), + {ok, TestReturn} = request_http_rest_lookup(["retainer"]), + ?assertEqual(NowCfg, get_http_data(TestReturn)), + + {ok, _} = request_http_rest_update(["retainer"], RetainerConf), + {ok, UpdateReturn} = request_http_rest_lookup(["retainer"]), + ?assertEqual(NewCfg, get_http_data(UpdateReturn)), + ok. + +t_enable_disable(_Config) -> + Conf = switch_emqx_retainer(undefined, true), + + {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), + {ok, _} = emqtt:connect(C1), + + emqtt:publish(C1, <<"retained">>, <<"this is a retained message">>, [{qos, 0}, {retain, true}]), + timer:sleep(100), + + {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]), + ?assertEqual(1, length(receive_messages(1))), + + _ = switch_emqx_retainer(Conf, false), + + {ok, #{}, [0]} = emqtt:unsubscribe(C1, <<"retained">>), + emqtt:publish(C1, <<"retained">>, <<"this is a retained message">>, [{qos, 0}, {retain, true}]), + timer:sleep(100), + {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]), + ?assertEqual(0, length(receive_messages(1))), + + ok = emqtt:disconnect(C1). + +%%-------------------------------------------------------------------- +%% HTTP Request +%%-------------------------------------------------------------------- +request_http_rest_lookup(Path) -> + request_api(get, uri([Path]), default_auth_header()). + +request_http_rest_update(Path, Params) -> + request_api(put, uri([Path]), [], default_auth_header(), Params). + +uri(Parts) when is_list(Parts) -> + NParts = [b2l(E) || E <- Parts], + ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). + +%% @private +b2l(B) when is_binary(B) -> + binary_to_list(B); +b2l(L) when is_list(L) -> + L. + +receive_messages(Count) -> + receive_messages(Count, []). +receive_messages(0, Msgs) -> + Msgs; +receive_messages(Count, Msgs) -> + receive + {publish, Msg} -> + ct:log("Msg: ~p ~n", [Msg]), + receive_messages(Count-1, [Msg|Msgs]); + Other -> + ct:log("Other Msg: ~p~n",[Other]), + receive_messages(Count, Msgs) + after 2000 -> + Msgs + end. + +switch_emqx_retainer(undefined, IsEnable) -> + {ok, Return} = request_http_rest_lookup(["retainer"]), + NowCfg = get_http_data(Return), + switch_emqx_retainer(NowCfg, IsEnable); + +switch_emqx_retainer(NowCfg, IsEnable) -> + NewCfg = NowCfg#{<<"enable">> => IsEnable}, + RetainerConf = #{<<"emqx_retainer">> => NewCfg}, + {ok, _} = request_http_rest_update(["retainer"], RetainerConf), + NewCfg. diff --git a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl b/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl index cec492c6a..19d404010 100644 --- a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl @@ -27,7 +27,7 @@ init_per_suite(Config) -> %% Meck emqtt ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), %% Start Apps - emqx_ct_helpers:start_apps([emqx_retainer]), + emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), Config. end_per_suite(_Config) -> @@ -37,6 +37,16 @@ end_per_suite(_Config) -> %%-------------------------------------------------------------------- %% Helpers %%-------------------------------------------------------------------- +set_special_configs(emqx_retainer) -> + emqx_config:put([emqx_retainer], + #{enable => true, + storage_type => ram, + max_retained_messages => 0, + max_payload_size => 1024 * 1024, + expiry_interval => 0}); + +set_special_configs(_) -> + ok. client_info(Key, Client) -> maps:get(Key, maps:from_list(emqtt:info(Client)), undefined). From 5efd5c8d3b17e54c5687050bba117c635471886b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 6 Jul 2021 18:26:54 +0800 Subject: [PATCH 099/379] refactor(emqx_connection): make the mqtt tcp connection work with new config --- apps/emqx/etc/emqx.conf | 2 +- apps/emqx/src/emqx_access_control.erl | 24 ++--- apps/emqx/src/emqx_channel.erl | 139 +++++++++++++++----------- apps/emqx/src/emqx_connection.erl | 54 ++++++---- apps/emqx/src/emqx_frame.erl | 11 +- apps/emqx/src/emqx_listeners.erl | 3 +- apps/emqx/src/emqx_schema.erl | 8 +- 7 files changed, 137 insertions(+), 104 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 2179fba90..b4b1393d6 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -870,7 +870,7 @@ zones.default { ## received. ## ## @doc zones..mqtt.idle_timeout - ## ValueType: Duration | infinity + ## ValueType: Duration ## Default: 15s idle_timeout: 15s diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 1ef885ed5..a2a11bd15 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -32,14 +32,9 @@ %%-------------------------------------------------------------------- -spec(authenticate(emqx_types:clientinfo()) -> {ok, result()} | {error, term()}). -authenticate(ClientInfo = #{zone := Zone}) -> - AuthResult = default_auth_result(Zone), - case emqx_zone:get_env(Zone, bypass_auth_plugins, false) of - true -> - return_auth_result(AuthResult); - false -> - return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult)) - end. +authenticate(ClientInfo = #{zone := Zone, listener := Listener}) -> + AuthResult = default_auth_result(Zone, Listener), + return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult)). %% @doc Check ACL -spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) @@ -59,17 +54,16 @@ check_acl_cache(ClientInfo, PubSub, Topic) -> AclResult -> AclResult end. -do_check_acl(ClientInfo = #{zone := Zone}, PubSub, Topic) -> - Default = emqx_zone:get_env(Zone, acl_nomatch, deny), - case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], Default) of +do_check_acl(ClientInfo, PubSub, Topic) -> + case run_hooks('client.check_acl', [ClientInfo, PubSub, Topic], allow) of allow -> allow; _Other -> deny end. -default_auth_result(Zone) -> - case emqx_zone:get_env(Zone, allow_anonymous, false) of - true -> #{auth_result => success, anonymous => true}; - false -> #{auth_result => not_authorized, anonymous => false} +default_auth_result(Zone, Listener) -> + case emqx_config:get_listener_conf(Zone, Listener, [auth, enable]) of + false -> #{auth_result => success, anonymous => true}; + true -> #{auth_result => not_authorized, anonymous => false} end. -compile({inline, [run_hooks/3]}). diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index e3cbff692..bbc48e709 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -31,6 +31,8 @@ -export([ info/1 , info/2 + , get_mqtt_conf/3 + , get_mqtt_conf/4 , set_conn_state/2 , get_session/1 , set_session/2 @@ -63,7 +65,7 @@ , maybe_apply/2 ]). --export_type([channel/0]). +-export_type([channel/0, opts/0]). -record(channel, { %% MQTT ConnInfo @@ -98,6 +100,8 @@ -type(channel() :: #channel{}). +-type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). + -type(conn_state() :: idle | connecting | connected | disconnected). -type(reply() :: {outgoing, emqx_types:packet()} @@ -151,7 +155,9 @@ info(connected_at, #channel{conninfo = ConnInfo}) -> info(clientinfo, #channel{clientinfo = ClientInfo}) -> ClientInfo; info(zone, #channel{clientinfo = ClientInfo}) -> - maps:get(zone, ClientInfo, undefined); + maps:get(zone, ClientInfo); +info(listener, #channel{clientinfo = ClientInfo}) -> + maps:get(listener, ClientInfo); info(clientid, #channel{clientinfo = ClientInfo}) -> maps:get(clientid, ClientInfo, undefined); info(username, #channel{clientinfo = ClientInfo}) -> @@ -195,17 +201,20 @@ caps(#channel{clientinfo = #{zone := Zone}}) -> %% Init the channel %%-------------------------------------------------------------------- --spec(init(emqx_types:conninfo(), proplists:proplist()) -> channel()). +-spec(init(emqx_types:conninfo(), opts()) -> channel()). init(ConnInfo = #{peername := {PeerHost, _Port}, - sockname := {_Host, SockPort}}, Options) -> - Zone = proplists:get_value(zone, Options), + sockname := {_Host, SockPort}}, #{zone := Zone, listener := Listener}) -> Peercert = maps:get(peercert, ConnInfo, undefined), Protocol = maps:get(protocol, ConnInfo, mqtt), - MountPoint = emqx_zone:mountpoint(Zone), - QuotaPolicy = emqx_zone:quota_policy(Zone), - ClientInfo = setting_peercert_infos( + MountPoint = case get_mqtt_conf(Zone, Listener, mountpoint) of + "" -> undefined; + MP -> MP + end, + QuotaPolicy = emqx_config:get_listener_conf(Zone, Listener, [rate_limit, quota]), + ClientInfo = set_peercert_infos( Peercert, #{zone => Zone, + listener => Listener, protocol => Protocol, peerhost => PeerHost, sockport => SockPort, @@ -214,7 +223,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, mountpoint => MountPoint, is_bridge => false, is_superuser => false - }, Options), + }, Zone, Listener), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), #channel{conninfo = NConnInfo, clientinfo = NClientInfo, @@ -222,7 +231,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, outbound => #{} }, auth_cache = #{}, - quota = emqx_limiter:init(Zone, QuotaPolicy), + quota = emqx_limiter:init(Zone, quota_policy(QuotaPolicy)), timers = #{}, conn_state = idle, takeover = false, @@ -230,30 +239,34 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, pendings = [] }. -setting_peercert_infos(NoSSL, ClientInfo, _Options) +quota_policy(RawPolicy) -> + [{Name, {list_to_integer(StrCount), + erlang:trunc(hocon_postprocess:duration(StrWind) / 1000)}} + || {Name, [StrCount, StrWind]} <- maps:to_list(RawPolicy)]. + +set_peercert_infos(NoSSL, ClientInfo, _, _) when NoSSL =:= nossl; NoSSL =:= undefined -> ClientInfo#{username => undefined}; -setting_peercert_infos(Peercert, ClientInfo, Options) -> +set_peercert_infos(Peercert, ClientInfo, Zone, Listener) -> {DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)}, - Username = peer_cert_as(peer_cert_as_username, Options, Peercert, DN, CN), - ClientId = peer_cert_as(peer_cert_as_clientid, Options, Peercert, DN, CN), - ClientInfo#{username => Username, clientid => ClientId, dn => DN, cn => CN}. - --dialyzer([{nowarn_function, [peer_cert_as/5]}]). -% esockd_peercert:peercert is opaque -% https://github.com/emqx/esockd/blob/master/src/esockd_peercert.erl -peer_cert_as(Key, Options, Peercert, DN, CN) -> - case proplists:get_value(Key, Options) of + PeercetAs = fun(Key) -> + % esockd_peercert:peercert is opaque + % https://github.com/emqx/esockd/blob/master/src/esockd_peercert.erl + case get_mqtt_conf(Zone, Listener, Key) of cn -> CN; dn -> DN; crt -> Peercert; pem -> base64:encode(Peercert); md5 -> emqx_passwd:hash(md5, Peercert); _ -> undefined - end. + end + end, + Username = PeercetAs(peer_cert_as_username), + ClientId = PeercetAs(peer_cert_as_clientid), + ClientInfo#{username => Username, clientid => ClientId, dn => DN, cn => CN}. take_ws_cookie(ClientInfo, ConnInfo) -> case maps:take(ws_cookie, ConnInfo) of @@ -403,16 +416,17 @@ handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = S end; handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), - Channel = #channel{clientinfo = ClientInfo = #{zone := Zone}}) -> + Channel = #channel{clientinfo = ClientInfo = #{zone := Zone, listener := Listener}}) -> case emqx_packet:check(Packet) of ok -> TopicFilters0 = parse_topic_filters(TopicFilters), TopicFilters1 = put_subid_in_subopts(Properties, TopicFilters0), TupleTopicFilters0 = check_sub_acls(TopicFilters1, Channel), - case emqx_zone:get_env(Zone, acl_deny_action, ignore) =:= disconnect andalso - lists:any(fun({_TopicFilter, ReasonCode}) -> - ReasonCode =:= ?RC_NOT_AUTHORIZED - end, TupleTopicFilters0) of + HasAclDeny = lists:any(fun({_TopicFilter, ReasonCode}) -> + ReasonCode =:= ?RC_NOT_AUTHORIZED + end, TupleTopicFilters0), + DenyAction = emqx_config:get_listener_conf(Zone, Listener, [acl, deny_action]), + case DenyAction =:= disconnect andalso HasAclDeny of true -> handle_out(disconnect, ?RC_NOT_AUTHORIZED, Channel); false -> Replace = fun @@ -512,7 +526,7 @@ process_connect(AckProps, Channel = #channel{conninfo = ConnInfo, %%-------------------------------------------------------------------- process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), - Channel = #channel{clientinfo = #{zone := Zone}}) -> + Channel = #channel{clientinfo = #{zone := Zone, listener := Listener}}) -> case pipeline([fun check_quota_exceeded/2, fun process_alias/2, fun check_pub_alias/2, @@ -525,7 +539,7 @@ process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), {error, Rc = ?RC_NOT_AUTHORIZED, NChannel} -> ?LOG(warning, "Cannot publish message to ~s due to ~s.", [Topic, emqx_reason_codes:text(Rc)]), - case emqx_zone:get_env(Zone, acl_deny_action, ignore) of + case emqx_config:get_listener_conf(Zone, Listener, [acl_deny_action]) of ignore -> case QoS of ?QOS_0 -> {ok, NChannel}; @@ -968,8 +982,8 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting}) handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connected, - clientinfo = ClientInfo = #{zone := Zone}}) -> - emqx_zone:enable_flapping_detect(Zone) + clientinfo = ClientInfo = #{zone := Zone, listener := Listener}}) -> + emqx_config:get_listener_conf(Zone, Listener, [flapping_detect, enable]) andalso emqx_flapping:detect(ClientInfo), Channel1 = ensure_disconnected(Reason, mabye_publish_will_msg(Channel)), case maybe_shutdown(Reason, Channel1) of @@ -1130,9 +1144,9 @@ enrich_conninfo(ConnPkt = #mqtt_packet_connect{ username = Username }, Channel = #channel{conninfo = ConnInfo, - clientinfo = #{zone := Zone} + clientinfo = #{zone := Zone, listener := Listener} }) -> - ExpiryInterval = expiry_interval(Zone, ConnPkt), + ExpiryInterval = expiry_interval(Zone, Listener, ConnPkt), NConnInfo = ConnInfo#{proto_name => ProtoName, proto_ver => ProtoVer, clean_start => CleanStart, @@ -1141,22 +1155,21 @@ enrich_conninfo(ConnPkt = #mqtt_packet_connect{ username => Username, conn_props => ConnProps, expiry_interval => ExpiryInterval, - receive_maximum => receive_maximum(Zone, ConnProps) + receive_maximum => receive_maximum(Zone, Listener, ConnProps) }, {ok, Channel#channel{conninfo = NConnInfo}}. %% If the Session Expiry Interval is absent the value 0 is used. --compile({inline, [expiry_interval/2]}). -expiry_interval(_Zone, #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5, +expiry_interval(_, _, #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5, properties = ConnProps}) -> emqx_mqtt_props:get('Session-Expiry-Interval', ConnProps, 0); -expiry_interval(Zone, #mqtt_packet_connect{clean_start = false}) -> - emqx_zone:session_expiry_interval(Zone); -expiry_interval(_Zone, #mqtt_packet_connect{clean_start = true}) -> +expiry_interval(Zone, Listener, #mqtt_packet_connect{clean_start = false}) -> + get_mqtt_conf(Zone, Listener, session_expiry_interval); +expiry_interval(_, _, #mqtt_packet_connect{clean_start = true}) -> 0. -receive_maximum(Zone, ConnProps) -> - MaxInflightConfig = case emqx_zone:max_inflight(Zone) of +receive_maximum(Zone, Listener, ConnProps) -> + MaxInflightConfig = case get_mqtt_conf(Zone, Listener, max_inflight) of 0 -> ?RECEIVE_MAXIMUM_LIMIT; N -> N end, @@ -1205,8 +1218,9 @@ set_bridge_mode(_ConnPkt, _ClientInfo) -> ok. maybe_username_as_clientid(_ConnPkt, ClientInfo = #{username := undefined}) -> {ok, ClientInfo}; -maybe_username_as_clientid(_ConnPkt, ClientInfo = #{zone := Zone, username := Username}) -> - case emqx_zone:use_username_as_clientid(Zone) of +maybe_username_as_clientid(_ConnPkt, ClientInfo = #{zone := Zone, listener := Listener, + username := Username}) -> + case get_mqtt_conf(Zone, Listener, use_username_as_clientid) of true -> {ok, ClientInfo#{clientid => Username}}; false -> ok end. @@ -1234,8 +1248,8 @@ set_log_meta(_ConnPkt, #channel{clientinfo = #{clientid := ClientId}}) -> %%-------------------------------------------------------------------- %% Check banned -check_banned(_ConnPkt, #channel{clientinfo = ClientInfo = #{zone := Zone}}) -> - case emqx_zone:enable_ban(Zone) andalso emqx_banned:check(ClientInfo) of +check_banned(_ConnPkt, #channel{clientinfo = ClientInfo}) -> + case emqx_banned:check(ClientInfo) of true -> {error, ?RC_BANNED}; false -> ok end. @@ -1463,8 +1477,9 @@ put_subid_in_subopts(_Properties, TopicFilters) -> TopicFilters. enrich_subopts(SubOpts, _Channel = ?IS_MQTT_V5) -> SubOpts; -enrich_subopts(SubOpts, #channel{clientinfo = #{zone := Zone, is_bridge := IsBridge}}) -> - NL = flag(emqx_zone:ignore_loop_deliver(Zone)), +enrich_subopts(SubOpts, #channel{clientinfo = #{zone := Zone, is_bridge := IsBridge, + listener := Listener}}) -> + NL = flag(get_mqtt_conf(Zone, Listener, ignore_loop_deliver)), SubOpts#{rap => flag(IsBridge), nl => NL}. %%-------------------------------------------------------------------- @@ -1499,8 +1514,8 @@ enrich_connack_caps(AckProps, _Channel) -> AckProps. %%-------------------------------------------------------------------- %% Enrich server keepalive -enrich_server_keepalive(AckProps, #channel{clientinfo = #{zone := Zone}}) -> - case emqx_zone:server_keepalive(Zone) of +enrich_server_keepalive(AckProps, #channel{clientinfo = #{zone := Zone, listener := Listener}}) -> + case get_mqtt_conf(Zone, Listener, server_keepalive) of undefined -> AckProps; Keepalive -> AckProps#{'Server-Keep-Alive' => Keepalive} end. @@ -1509,10 +1524,14 @@ enrich_server_keepalive(AckProps, #channel{clientinfo = #{zone := Zone}}) -> %% Enrich response information enrich_response_information(AckProps, #channel{conninfo = #{conn_props := ConnProps}, - clientinfo = #{zone := Zone}}) -> + clientinfo = #{zone := Zone, listener := Listener}}) -> case emqx_mqtt_props:get('Request-Response-Information', ConnProps, 0) of 0 -> AckProps; - 1 -> AckProps#{'Response-Information' => emqx_zone:response_information(Zone)} + 1 -> AckProps#{'Response-Information' => + case get_mqtt_conf(Zone, Listener, response_information, "") of + "" -> undefined; + RspInfo -> RspInfo + end} end. %%-------------------------------------------------------------------- @@ -1559,9 +1578,10 @@ ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel = #channel{conninfo ensure_keepalive(_AckProps, Channel = #channel{conninfo = ConnInfo}) -> ensure_keepalive_timer(maps:get(keepalive, ConnInfo), Channel). -ensure_keepalive_timer(0, Channel) -> Channel; -ensure_keepalive_timer(Interval, Channel = #channel{clientinfo = #{zone := Zone}}) -> - Backoff = emqx_zone:keepalive_backoff(Zone), +ensure_keepalive_timer(disabled, Channel) -> Channel; +ensure_keepalive_timer(Interval, Channel = #channel{clientinfo = #{zone := Zone, + listener := Listener}}) -> + Backoff = get_mqtt_conf(Zone, Listener, keepalive_backoff), Keepalive = emqx_keepalive:init(round(timer:seconds(Interval) * Backoff)), ensure_timer(alive_timer, Channel#channel{keepalive = Keepalive}). @@ -1604,8 +1624,8 @@ maybe_shutdown(Reason, Channel = #channel{conninfo = ConnInfo}) -> %% Is ACL enabled? -compile({inline, [is_acl_enabled/1]}). -is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> - (not IsSuperuser) andalso emqx_zone:enable_acl(Zone). +is_acl_enabled(#{zone := Zone, listener := Listener, is_superuser := IsSuperuser}) -> + (not IsSuperuser) andalso emqx_config:get_listener_conf(Zone, Listener, [acl, enable]). %%-------------------------------------------------------------------- %% Parse Topic Filters @@ -1715,6 +1735,12 @@ sp(false) -> 0. flag(true) -> 1; flag(false) -> 0. +get_mqtt_conf(Zone, Listener, Key) -> + emqx_config:get_listener_conf(Zone, Listener, [mqtt, Key]). + +get_mqtt_conf(Zone, Listener, Key, Default) -> + emqx_config:get_listener_conf(Zone, Listener, [mqtt, Key], Default). + %%-------------------------------------------------------------------- %% For CT tests %%-------------------------------------------------------------------- @@ -1722,4 +1748,3 @@ flag(false) -> 0. set_field(Name, Value, Channel) -> Pos = emqx_misc:index_of(Name, record_info(fields, channel)), setelement(Pos+1, Channel, Value). - diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 1cbc36e05..fb8fe241b 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -108,7 +108,6 @@ }). -type(state() :: #state{}). --type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). -define(ACTIVE_N, 100). -define(INFO_KEYS, [socktype, peername, sockname, sockstate]). @@ -137,7 +136,7 @@ , system_code_change/4 ]}). --spec(start_link(esockd:transport(), esockd:socket(), opts()) +-spec(start_link(esockd:transport(), esockd:socket(), emqx_channel:opts()) -> {ok, pid()}). start_link(Transport, Socket, Options) -> Args = [self(), Transport, Socket, Options], @@ -256,18 +255,23 @@ init_state(Transport, Socket, Options) -> }, Zone = maps:get(zone, Options), Listener = maps:get(listener, Options), - - PubLimit = emqx_zone:publish_limit(Zone), - BytesIn = proplists:get_value(rate_limit, Options), - RateLimit = emqx_zone:ratelimit(Zone), - Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit), - FrameOpts = emqx_zone:mqtt_frame_options(Zone), + Limiter = emqx_limiter:init(Zone, undefined, undefined, []), + FrameOpts = #{ + strict_mode => emqx_config:get_listener_conf(Zone, Listener, [mqtt, strict_mode]), + max_size => emqx_config:get_listener_conf(Zone, Listener, [mqtt, max_packet_size]) + }, ParseState = emqx_frame:initial_parse_state(FrameOpts), Serialize = emqx_frame:serialize_opts(), Channel = emqx_channel:init(ConnInfo, Options), - GcState = emqx_zone:init_gc_state(Zone), - StatsTimer = emqx_zone:stats_timer(Zone), - IdleTimeout = emqx_zone:idle_timeout(Zone), + GcState = case emqx_config:get_listener_conf(Zone, Listener, [force_gc]) of + #{enable := false} -> undefined; + GcPolicy -> emqx_gc:init(GcPolicy) + end, + StatsTimer = case emqx_config:get_listener_conf(Zone, Listener, [stats, enable]) of + true -> undefined; + false -> disabled + end, + IdleTimeout = emqx_channel:get_mqtt_conf(Zone, Listener, idle_timeout), IdleTimer = start_timer(IdleTimeout, idle_timeout), #state{transport = Transport, socket = Socket, @@ -291,8 +295,11 @@ run_loop(Parent, State = #state{transport = Transport, peername = Peername, channel = Channel}) -> emqx_logger:set_metadata_peername(esockd:format(Peername)), - emqx_misc:tune_heap_size(emqx_zone:oom_policy( - emqx_channel:info(zone, Channel))), + case emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), + emqx_channel:info(listener, Channel), [force_shutdown]) of + #{enable := false} -> ok; + ShutdownPolicy -> emqx_misc:tune_heap_size(ShutdownPolicy) + end, case activate_socket(State) of {ok, NState} -> hibernate(Parent, NState); {error, Reason} -> @@ -783,15 +790,18 @@ run_gc(Stats, State = #state{gc_state = GcSt}) -> end. check_oom(State = #state{channel = Channel}) -> - Zone = emqx_channel:info(zone, Channel), - OomPolicy = emqx_zone:oom_policy(Zone), - ?tp(debug, check_oom, #{policy => OomPolicy}), - case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of - {shutdown, Reason} -> - %% triggers terminate/2 callback immediately - erlang:exit({shutdown, Reason}); - _Other -> - ok + ShutdownPolicy = emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), + emqx_channel:info(listener, Channel), [force_shutdown]), + ?tp(debug, check_oom, #{policy => ShutdownPolicy}), + case ShutdownPolicy of + #{enable := false} -> ok; + ShutdownPolicy -> + case emqx_misc:check_oom(ShutdownPolicy) of + {shutdown, Reason} -> + %% triggers terminate/2 callback immediately + erlang:exit({shutdown, Reason}); + _ -> ok + end end, State. diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 37063c65f..1737e8791 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -34,6 +34,9 @@ , serialize/2 ]). +-export([ set_opts/2 + ]). + -export_type([ options/0 , parse_state/0 , parse_result/0 @@ -81,11 +84,11 @@ initial_parse_state() -> -spec(initial_parse_state(options()) -> {none, options()}). initial_parse_state(Options) when is_map(Options) -> - ?none(merge_opts(Options)). + ?none(maps:merge(?DEFAULT_OPTIONS, Options)). -%% @pivate -merge_opts(Options) -> - maps:merge(?DEFAULT_OPTIONS, Options). +-spec set_opts(parse_state(), options()) -> parse_state(). +set_opts({_, OldOpts}, Opts) -> + maps:merge(OldOpts, Opts). %%-------------------------------------------------------------------- %% Parse MQTT Frame diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index b9830578a..9daf6c79e 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -68,7 +68,8 @@ console_print(_Fmt, _Args) -> ok. -> {ok, pid()} | {error, term()}). do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opts) -> esockd:open(listener_id(ZoneName, ListenerName), ListenOn, merge_default(esockd_opts(Opts)), - {emqx_connection, start_link, [{ZoneName, ListenerName}]}); + {emqx_connection, start_link, + [#{zone => ZoneName, listener => ListenerName}]}); %% Start MQTT/WS listener do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 616f65bfd..98488edf8 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -59,7 +59,7 @@ -export([includes/0]). structs() -> ["cluster", "node", "rpc", "log", "lager", - "acl", "mqtt", "zones", "listeners", "module", "broker", + "zones", "listeners", "module", "broker", "plugins", "sysmon", "alarm", "telemetry"] ++ includes(). @@ -244,7 +244,7 @@ fields("acl_cache") -> ]; fields("mqtt") -> - [ {"mountpoint", t(binary(), undefined, <<"">>)} + [ {"mountpoint", t(binary(), undefined, <<>>)} , {"idle_timeout", maybe_infinity(duration(), "15s")} , {"max_packet_size", maybe_infinity(bytesize(), "1MB")} , {"max_clientid_len", t(integer(), undefined, 65535)} @@ -256,7 +256,7 @@ fields("mqtt") -> , {"shared_subscription", t(boolean(), undefined, true)} , {"ignore_loop_deliver", t(boolean())} , {"strict_mode", t(boolean(), undefined, false)} - , {"response_information", t(string(), undefined, undefined)} + , {"response_information", t(string(), undefined, "")} , {"server_keepalive", maybe_disabled(integer())} , {"keepalive_backoff", t(float(), undefined, 0.75)} , {"max_subscriptions", maybe_infinity(integer())} @@ -365,7 +365,7 @@ fields("ws_opts") -> [ {"mqtt_path", t(string(), undefined, "/mqtt")} , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} , {"compress", t(boolean())} - , {"idle_timeout", maybe_infinity(duration())} + , {"idle_timeout", t(duration(), undefined, "15s")} , {"max_frame_size", maybe_infinity(integer())} , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} , {"supported_subprotocols", t(string(), undefined, From 7c0fd642bb3573ed6e3eedd4b4aa9223db12c635 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 6 Jul 2021 18:36:40 +0800 Subject: [PATCH 100/379] feat(acl): make acl cache work with the new config --- apps/emqx/src/emqx_access_control.erl | 10 ++--- apps/emqx/src/emqx_acl_cache.erl | 64 +++++++++++++-------------- apps/emqx/src/emqx_channel.erl | 2 - 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index a2a11bd15..4aa8eb505 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -39,17 +39,17 @@ authenticate(ClientInfo = #{zone := Zone, listener := Listener}) -> %% @doc Check ACL -spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). -check_acl(ClientInfo, PubSub, Topic) -> - case emqx_acl_cache:is_enabled() of +check_acl(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> + case emqx_acl_cache:is_enabled(Zone, Listener) of true -> check_acl_cache(ClientInfo, PubSub, Topic); false -> do_check_acl(ClientInfo, PubSub, Topic) end. -check_acl_cache(ClientInfo, PubSub, Topic) -> - case emqx_acl_cache:get_acl_cache(PubSub, Topic) of +check_acl_cache(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> + case emqx_acl_cache:get_acl_cache(Zone, Listener, PubSub, Topic) of not_found -> AclResult = do_check_acl(ClientInfo, PubSub, Topic), - emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), + emqx_acl_cache:put_acl_cache(Zone, Listener, PubSub, Topic, AclResult), AclResult; AclResult -> AclResult end. diff --git a/apps/emqx/src/emqx_acl_cache.erl b/apps/emqx/src/emqx_acl_cache.erl index 4cbe6b06a..a49f42c83 100644 --- a/apps/emqx/src/emqx_acl_cache.erl +++ b/apps/emqx/src/emqx_acl_cache.erl @@ -19,14 +19,14 @@ -include("emqx.hrl"). -export([ list_acl_cache/0 - , get_acl_cache/2 - , put_acl_cache/3 - , cleanup_acl_cache/0 + , get_acl_cache/4 + , put_acl_cache/5 + , cleanup_acl_cache/2 , empty_acl_cache/0 , dump_acl_cache/0 - , get_cache_max_size/0 - , get_cache_ttl/0 - , is_enabled/0 + , get_cache_max_size/2 + , get_cache_ttl/2 + , is_enabled/2 , drain_cache/0 ]). @@ -50,43 +50,44 @@ cache_k(PubSub, Topic)-> {PubSub, Topic}. cache_v(AclResult)-> {AclResult, time_now()}. drain_k() -> {?MODULE, drain_timestamp}. --spec(is_enabled() -> boolean()). -is_enabled() -> - application:get_env(emqx, enable_acl_cache, true). +-spec(is_enabled(atom(), atom()) -> boolean()). +is_enabled(Zone, Listener) -> + emqx_config:get_listener_conf(Zone, Listener, [acl, cache, enable]). --spec(get_cache_max_size() -> integer()). -get_cache_max_size() -> - application:get_env(emqx, acl_cache_max_size, 32). +-spec(get_cache_max_size(atom(), atom()) -> integer()). +get_cache_max_size(Zone, Listener) -> + emqx_config:get_listener_conf(Zone, Listener, [acl, cache, max_size]). --spec(get_cache_ttl() -> integer()). -get_cache_ttl() -> - application:get_env(emqx, acl_cache_ttl, 60000). +-spec(get_cache_ttl(atom(), atom()) -> integer()). +get_cache_ttl(Zone, Listener) -> + emqx_config:get_listener_conf(Zone, Listener, [acl, cache, ttl]). -spec(list_acl_cache() -> [acl_cache_entry()]). list_acl_cache() -> - cleanup_acl_cache(), map_acl_cache(fun(Cache) -> Cache end). %% We'll cleanup the cache before replacing an expired acl. --spec(get_acl_cache(emqx_types:pubsub(), emqx_topic:topic()) -> (acl_result() | not_found)). -get_acl_cache(PubSub, Topic) -> +-spec get_acl_cache(atom(), atom(), emqx_types:pubsub(), emqx_topic:topic()) -> + acl_result() | not_found. +get_acl_cache(Zone, Listener, PubSub, Topic) -> case erlang:get(cache_k(PubSub, Topic)) of undefined -> not_found; {AclResult, CachedAt} -> - if_expired(CachedAt, + if_expired(get_cache_ttl(Zone, Listener), CachedAt, fun(false) -> AclResult; (true) -> - cleanup_acl_cache(), + cleanup_acl_cache(Zone, Listener), not_found end) end. %% If the cache get full, and also the latest one %% is expired, then delete all the cache entries --spec(put_acl_cache(emqx_types:pubsub(), emqx_topic:topic(), acl_result()) -> ok). -put_acl_cache(PubSub, Topic, AclResult) -> - MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), +-spec put_acl_cache(atom(), atom(), emqx_types:pubsub(), emqx_topic:topic(), acl_result()) + -> ok. +put_acl_cache(Zone, Listener, PubSub, Topic, AclResult) -> + MaxSize = get_cache_max_size(Zone, Listener), true = (MaxSize =/= 0), Size = get_cache_size(), case Size < MaxSize of true -> @@ -94,7 +95,7 @@ put_acl_cache(PubSub, Topic, AclResult) -> false -> NewestK = get_newest_key(), {_AclResult, CachedAt} = erlang:get(NewestK), - if_expired(CachedAt, + if_expired(get_cache_ttl(Zone, Listener), CachedAt, fun(true) -> % all cache expired, cleanup first empty_acl_cache(), @@ -121,10 +122,10 @@ evict_acl_cache() -> decr_cache_size(). %% cleanup all the expired cache entries --spec(cleanup_acl_cache() -> ok). -cleanup_acl_cache() -> +-spec(cleanup_acl_cache(atom(), atom()) -> ok). +cleanup_acl_cache(Zone, Listener) -> keys_queue_set( - cleanup_acl(keys_queue_get())). + cleanup_acl(get_cache_ttl(Zone, Listener), keys_queue_get())). get_oldest_key() -> keys_queue_pick(queue_front()). @@ -174,16 +175,16 @@ update_acl(K, V) -> erlang:put(K, V), keys_queue_update(K). -cleanup_acl(KeysQ) -> +cleanup_acl(TTL, KeysQ) -> case queue:out(KeysQ) of {{value, OldestK}, KeysQ2} -> {_AclResult, CachedAt} = erlang:get(OldestK), - if_expired(CachedAt, + if_expired(TTL, CachedAt, fun(false) -> KeysQ; (true) -> erlang:erase(OldestK), decr_cache_size(), - cleanup_acl(KeysQ2) + cleanup_acl(TTL, KeysQ2) end); {empty, KeysQ} -> KeysQ end. @@ -246,8 +247,7 @@ queue_rear() -> fun queue:get_r/1. time_now() -> erlang:system_time(millisecond). -if_expired(CachedAt, Fun) -> - TTL = get_cache_ttl(), +if_expired(TTL, CachedAt, Fun) -> Now = time_now(), CurrentEvictTimestamp = persistent_term:get(drain_k(), 0), case CachedAt =< CurrentEvictTimestamp orelse (CachedAt + TTL) =< Now of diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index bbc48e709..4c9ee7682 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1622,8 +1622,6 @@ maybe_shutdown(Reason, Channel = #channel{conninfo = ConnInfo}) -> %%-------------------------------------------------------------------- %% Is ACL enabled? - --compile({inline, [is_acl_enabled/1]}). is_acl_enabled(#{zone := Zone, listener := Listener, is_superuser := IsSuperuser}) -> (not IsSuperuser) andalso emqx_config:get_listener_conf(Zone, Listener, [acl, enable]). From 707851c36f41a8051f0579cb89fe62bb7958dcb1 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 6 Jul 2021 20:11:49 +0800 Subject: [PATCH 101/379] refactor(force_shutdown): force shutdown the connection if it don't kill itself --- apps/emqx/src/emqx_misc.erl | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/emqx/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl index 04af5f72c..640ed5704 100644 --- a/apps/emqx/src/emqx_misc.erl +++ b/apps/emqx/src/emqx_misc.erl @@ -197,6 +197,7 @@ check_oom(Policy) -> check_oom(self(), Policy). -spec(check_oom(pid(), emqx_types:oom_policy()) -> ok | {shutdown, term()}). +check_oom(_Pid, #{enable := false}) -> ok; check_oom(Pid, #{message_queue_len := MaxQLen, max_heap_size := MaxHeapSize}) -> case process_info(Pid, [message_queue_len, total_heap_size]) of @@ -214,13 +215,26 @@ do_check_oom([{Val, Max, Reason}|Rest]) -> false -> do_check_oom(Rest) end. -tune_heap_size(#{max_heap_size := MaxHeapSize}) -> - %% If set to zero, the limit is disabled. - erlang:process_flag(max_heap_size, #{size => MaxHeapSize, - kill => false, - error_logger => true - }); -tune_heap_size(undefined) -> ok. +tune_heap_size(#{enable := false}) -> + ok; +%% If the max_heap_size is set to zero, the limit is disabled. +tune_heap_size(#{max_heap_size := MaxHeapSize}) when MaxHeapSize > 0 -> + MaxSize = case erlang:system_info(wordsize) of + 8 -> % arch_64 + (1 bsl 59) - 1; + 4 -> % arch_32 + (1 bsl 27) - 1 + end, + OverflowedSize = case erlang:trunc(MaxHeapSize * 1.5) of + SZ when SZ > MaxSize -> MaxSize; + SZ -> SZ + end, + erlang:process_flag(max_heap_size, #{ + size => OverflowedSize, + kill => true, + error_logger => true + }). + -spec(proc_name(atom(), pos_integer()) -> atom()). proc_name(Mod, Id) -> From a95b91c005e6494be697a636f33180644006c3ff Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Tue, 6 Jul 2021 18:37:49 +0200 Subject: [PATCH 102/379] chore(coap): Fix debug logs --- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index b93d1c640..315cfbb5c 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -105,8 +105,8 @@ call(Pid, Msg, _) -> %%-------------------------------------------------------------------- init({ClientId, Username, Password, Channel}) -> - ?LOG(debug, "try to start adapter ClientId=~p, Username=~p, Password=~p, " - "Channel=~0p", [ClientId, Username, Password, Channel]), + ?LOG(debug, "try to start adapter ClientId=~p, Username=~p, " + "Channel=~0p", [ClientId, Username, Channel]), State0 = #state{peername = Channel, clientid = ClientId, username = Username, @@ -384,4 +384,3 @@ clientinfo(#state{peername = {PeerHost, _}, mountpoint => undefined, ws_cookie => undefined }. - From 612d25fdb358bf31ac833673c768eb8ea4e279f3 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Tue, 6 Jul 2021 18:19:22 +0800 Subject: [PATCH 103/379] chore(connector): rename mongo config key --- apps/emqx_connector/src/emqx_connector_mongo.erl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index bac97a41a..b8a3c0da0 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -76,9 +76,10 @@ fields(topology) -> mongo_fields() -> [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {login, fun emqx_connector_schema_lib:username/1} + , {username, fun emqx_connector_schema_lib:username/1} , {password, fun emqx_connector_schema_lib:password/1} - , {auth_source, fun auth_source/1} + , {authentication_database, #{type => binary(), + nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -217,9 +218,9 @@ init_topology_options([], Acc) -> init_worker_options([{database, V} | R], Acc) -> init_worker_options(R, [{database, V} | Acc]); -init_worker_options([{auth_source, V} | R], Acc) -> +init_worker_options([{authentication_database, V} | R], Acc) -> init_worker_options(R, [{auth_source, V} | Acc]); -init_worker_options([{login, V} | R], Acc) -> +init_worker_options([{username, V} | R], Acc) -> init_worker_options(R, [{login, V} | Acc]); init_worker_options([{password, V} | R], Acc) -> init_worker_options(R, [{password, V} | Acc]); @@ -249,10 +250,6 @@ servers(type) -> hoconsc:array(server()); servers(validator) -> [?REQUIRED("the field 'servers' is required")]; servers(_) -> undefined. -auth_source(type) -> binary(); -auth_source(nullable) -> true; -auth_source(_) -> undefined. - duration(type) -> emqx_schema:duration_ms(); duration(nullable) -> true; duration(_) -> undefined. From 630b54f6eec97bd89313288d2d83ea03c346ee2c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 7 Jul 2021 14:43:54 +0800 Subject: [PATCH 104/379] feat(acl): make mqtt over websocket work with the new config --- apps/emqx/etc/emqx.conf | 11 ++- apps/emqx/src/emqx_connection.erl | 28 +++----- apps/emqx/src/emqx_listeners.erl | 25 +++---- apps/emqx/src/emqx_map_lib.erl | 11 +-- apps/emqx/src/emqx_schema.erl | 10 +-- apps/emqx/src/emqx_ws_connection.erl | 104 +++++++++++++++------------ 6 files changed, 100 insertions(+), 89 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index b4b1393d6..4e3ff60c3 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -2218,8 +2218,8 @@ example_common_websocket_options { ## ## @doc listeners..websocket.compress ## ValueType: Boolean - ## Default: true - websocket.compress: true + ## Default: false + websocket.compress: false ## The idle timeout for external WebSocket connections. ## @@ -2244,6 +2244,13 @@ example_common_websocket_options { ## Default: true websocket.fail_if_no_subprotocol: true + ## Supported subprotocols + ## + ## @doc listeners..websocket.supported_subprotocols + ## ValueType: String + ## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 + websocket.supported_subprotocols: "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + ## Enable origin check in header for websocket connection ## ## @doc listeners..websocket.check_origin_enable diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index fb8fe241b..0df7cccf1 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -243,7 +243,7 @@ init(Parent, Transport, RawSocket, Options) -> exit_on_sock_error(Reason) end. -init_state(Transport, Socket, Options) -> +init_state(Transport, Socket, #{zone := Zone, listener := Listener} = Opts) -> {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]), @@ -253,8 +253,6 @@ init_state(Transport, Socket, Options) -> peercert => Peercert, conn_mod => ?MODULE }, - Zone = maps:get(zone, Options), - Listener = maps:get(listener, Options), Limiter = emqx_limiter:init(Zone, undefined, undefined, []), FrameOpts = #{ strict_mode => emqx_config:get_listener_conf(Zone, Listener, [mqtt, strict_mode]), @@ -262,7 +260,7 @@ init_state(Transport, Socket, Options) -> }, ParseState = emqx_frame:initial_parse_state(FrameOpts), Serialize = emqx_frame:serialize_opts(), - Channel = emqx_channel:init(ConnInfo, Options), + Channel = emqx_channel:init(ConnInfo, Opts), GcState = case emqx_config:get_listener_conf(Zone, Listener, [force_gc]) of #{enable := false} -> undefined; GcPolicy -> emqx_gc:init(GcPolicy) @@ -295,11 +293,9 @@ run_loop(Parent, State = #state{transport = Transport, peername = Peername, channel = Channel}) -> emqx_logger:set_metadata_peername(esockd:format(Peername)), - case emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), - emqx_channel:info(listener, Channel), [force_shutdown]) of - #{enable := false} -> ok; - ShutdownPolicy -> emqx_misc:tune_heap_size(ShutdownPolicy) - end, + ShutdownPolicy = emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), + emqx_channel:info(listener, Channel), [force_shutdown]), + emqx_misc:tune_heap_size(ShutdownPolicy), case activate_socket(State) of {ok, NState} -> hibernate(Parent, NState); {error, Reason} -> @@ -793,15 +789,11 @@ check_oom(State = #state{channel = Channel}) -> ShutdownPolicy = emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), emqx_channel:info(listener, Channel), [force_shutdown]), ?tp(debug, check_oom, #{policy => ShutdownPolicy}), - case ShutdownPolicy of - #{enable := false} -> ok; - ShutdownPolicy -> - case emqx_misc:check_oom(ShutdownPolicy) of - {shutdown, Reason} -> - %% triggers terminate/2 callback immediately - erlang:exit({shutdown, Reason}); - _ -> ok - end + case emqx_misc:check_oom(ShutdownPolicy) of + {shutdown, Reason} -> + %% triggers terminate/2 callback immediately + erlang:exit({shutdown, Reason}); + _ -> ok end, State. diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 9daf6c79e..6e3bc69be 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -74,13 +74,13 @@ do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opt %% Start MQTT/WS listener do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) -> Id = listener_id(ZoneName, ListenerName), - RanchOpts = ranch_opts(Opts), + RanchOpts = ranch_opts(ListenOn, Opts), WsOpts = ws_opts(ZoneName, ListenerName, Opts), case is_ssl(Opts) of false -> - cowboy:start_clear(Id, with_port(ListenOn, RanchOpts), WsOpts); + cowboy:start_clear(Id, RanchOpts, WsOpts); true -> - cowboy:start_tls(Id, with_port(ListenOn, RanchOpts), WsOpts) + cowboy:start_tls(Id, RanchOpts, WsOpts) end. esockd_opts(Opts0) -> @@ -104,21 +104,22 @@ ws_opts(ZoneName, ListenerName, Opts) -> ProxyProto = maps:get(proxy_protocol, Opts, false), #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. -ranch_opts(Opts) -> +ranch_opts(ListenOn, Opts) -> NumAcceptors = maps:get(acceptors, Opts, 4), MaxConnections = maps:get(max_connections, Opts, 1024), + SocketOpts = case is_ssl(Opts) of + true -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); + false -> tcp_opts(Opts) + end, #{num_acceptors => NumAcceptors, max_connections => MaxConnections, handshake_timeout => maps:get(handshake_timeout, Opts, 15000), - socket_opts => case is_ssl(Opts) of - true -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); - false -> tcp_opts(Opts) - end}. + socket_opts => ip_port(ListenOn) ++ SocketOpts}. -with_port(Port, Opts = #{socket_opts := SocketOption}) when is_integer(Port) -> - Opts#{socket_opts => [{port, Port}| SocketOption]}; -with_port({Addr, Port}, Opts = #{socket_opts := SocketOption}) -> - Opts#{socket_opts => [{ip, Addr}, {port, Port}| SocketOption]}. +ip_port(Port) when is_integer(Port) -> + [{port, Port}]; +ip_port({Addr, Port}) -> + [{ip, Addr}, {port, Port}]. esockd_access_rules(StrRules) -> Access = fun(S) -> diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 477891c51..154a3d24f 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -43,16 +43,17 @@ deep_get(ConfKeyPath, Map, Default) -> {ok, Data} -> Data end. --spec deep_find(config_key_path(), map()) -> {ok, term()} | {not_found, config_key(), term()}. +-spec deep_find(config_key_path(), map()) -> + {ok, term()} | {not_found, config_key_path(), term()}. deep_find([], Map) -> {ok, Map}; -deep_find([Key | KeyPath], Map) when is_map(Map) -> +deep_find([Key | KeyPath] = Path, Map) when is_map(Map) -> case maps:find(Key, Map) of {ok, SubMap} -> deep_find(KeyPath, SubMap); - error -> {not_found, Key, Map} + error -> {not_found, Path, Map} end; -deep_find([Key | _KeyPath], Data) -> - {not_found, Key, Data}. +deep_find(_KeyPath, Data) -> + {not_found, _KeyPath, Data}. -spec deep_put(config_key_path(), map(), term()) -> map(). deep_put([], Map, Config) when is_map(Map) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 98488edf8..0d6c8f3de 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -364,11 +364,11 @@ fields("mqtt_ws_listener") -> fields("ws_opts") -> [ {"mqtt_path", t(string(), undefined, "/mqtt")} , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} - , {"compress", t(boolean())} + , {"compress", t(boolean(), undefined, false)} , {"idle_timeout", t(duration(), undefined, "15s")} , {"max_frame_size", maybe_infinity(integer())} , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} - , {"supported_subprotocols", t(string(), undefined, + , {"supported_subprotocols", t(comma_separated_list(), undefined, "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} , {"check_origin_enable", t(boolean(), undefined, false)} , {"allow_origin_absence", t(boolean(), undefined, true)} @@ -401,12 +401,12 @@ fields("ssl_opts") -> fields("deflate_opts") -> [ {"level", t(union([none, default, best_compression, best_speed]))} - , {"mem_level", t(range(1, 9))} + , {"mem_level", t(range(1, 9), undefined, 8)} , {"strategy", t(union([default, filtered, huffman_only, rle]))} , {"server_context_takeover", t(union(takeover, no_takeover))} , {"client_context_takeover", t(union(takeover, no_takeover))} - , {"server_max_window_bits", t(integer())} - , {"client_max_window_bits", t(integer())} + , {"server_max_window_bits", t(range(8, 15), undefined, 15)} + , {"client_max_window_bits", t(range(8, 15), undefined, 15)} ]; fields("module") -> diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index b50505bf8..cd432b9fc 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -174,21 +174,13 @@ call(WsPid, Req, Timeout) when is_pid(WsPid) -> %% WebSocket callbacks %%-------------------------------------------------------------------- -init(Req, Opts) -> +init(Req, #{zone := Zone, listener := Listener} = Opts) -> %% WS Transport Idle Timeout - IdleTimeout = proplists:get_value(idle_timeout, Opts, 7200000), - DeflateOptions = maps:from_list(proplists:get_value(deflate_options, Opts, [])), - MaxFrameSize = case proplists:get_value(max_frame_size, Opts, 0) of - 0 -> infinity; - I -> I - end, - Compress = proplists:get_bool(compress, Opts), - WsOpts = #{compress => Compress, - deflate_opts => DeflateOptions, - max_frame_size => MaxFrameSize, - idle_timeout => IdleTimeout + WsOpts = #{compress => get_ws_opts(Zone, Listener, compress), + deflate_opts => get_ws_opts(Zone, Listener, deflate_opts), + max_frame_size => get_ws_opts(Zone, Listener, max_frame_size), + idle_timeout => get_ws_opts(Zone, Listener, idle_timeout) }, - case check_origin_header(Req, Opts) of {error, Message} -> ?LOG(error, "Invalid Origin Header ~p~n", [Message]), @@ -196,18 +188,17 @@ init(Req, Opts) -> ok -> parse_sec_websocket_protocol(Req, Opts, WsOpts) end. -parse_sec_websocket_protocol(Req, Opts, WsOpts) -> - FailIfNoSubprotocol = proplists:get_value(fail_if_no_subprotocol, Opts), +parse_sec_websocket_protocol(Req, #{zone := Zone, listener := Listener} = Opts, WsOpts) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> - case FailIfNoSubprotocol of + case get_ws_opts(Zone, Listener, fail_if_no_subprotocol) of true -> {ok, cowboy_req:reply(400, Req), WsOpts}; false -> {cowboy_websocket, Req, [Req, Opts], WsOpts} end; Subprotocols -> - SupportedSubprotocols = proplists:get_value(supported_subprotocols, Opts), + SupportedSubprotocols = get_ws_opts(Zone, Listener, supported_subprotocols), NSupportedSubprotocols = [list_to_binary(Subprotocol) || Subprotocol <- SupportedSubprotocols], case pick_subprotocol(Subprotocols, NSupportedSubprotocols) of @@ -231,31 +222,30 @@ pick_subprotocol([Subprotocol | Rest], SupportedSubprotocols) -> pick_subprotocol(Rest, SupportedSubprotocols) end. -parse_header_fun_origin(Req, Opts) -> +parse_header_fun_origin(Req, #{zone := Zone, listener := Listener}) -> case cowboy_req:header(<<"origin">>, Req) of undefined -> - case proplists:get_bool(allow_origin_absence, Opts) of + case get_ws_opts(Zone, Listener, allow_origin_absence) of true -> ok; false -> {error, origin_header_cannot_be_absent} end; Value -> - Origins = proplists:get_value(check_origins, Opts, []), - case lists:member(Value, Origins) of + case lists:member(Value, get_ws_opts(Zone, Listener, check_origins)) of true -> ok; false -> {origin_not_allowed, Value} end end. -check_origin_header(Req, Opts) -> - case proplists:get_bool(check_origin_enable, Opts) of +check_origin_header(Req, #{zone := Zone, listener := Listener} = Opts) -> + case get_ws_opts(Zone, Listener, check_origin_enable) of true -> parse_header_fun_origin(Req, Opts); false -> ok end. -websocket_init([Req, Opts]) -> +websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> {Peername, Peercert} = - case proplists:get_bool(proxy_protocol, Opts) - andalso maps:get(proxy_header, Req) of + case emqx_config:get_listener_conf(Zone, Listener, [proxy_protocol]) andalso + maps:get(proxy_header, Req) of #{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} -> SourceName = {SrcAddr, SrcPort}, %% Notice: Only CN is available in Proxy Protocol V2 additional info @@ -266,7 +256,7 @@ websocket_init([Req, Opts]) -> {SourceName, SourceSSL}; #{src_address := SrcAddr, src_port := SrcPort} -> SourceName = {SrcAddr, SrcPort}, - {SourceName , nossl}; + {SourceName, nossl}; _ -> {get_peer(Req, Opts), cowboy_req:cert(Req)} end, @@ -288,22 +278,31 @@ websocket_init([Req, Opts]) -> ws_cookie => WsCookie, conn_mod => ?MODULE }, - Zone = proplists:get_value(zone, Opts), - PubLimit = emqx_zone:publish_limit(Zone), - BytesIn = proplists:get_value(rate_limit, Opts), - RateLimit = emqx_zone:ratelimit(Zone), - Limiter = emqx_limiter:init(Zone, PubLimit, BytesIn, RateLimit), - MQTTPiggyback = proplists:get_value(mqtt_piggyback, Opts, multiple), - FrameOpts = emqx_zone:mqtt_frame_options(Zone), + Limiter = emqx_limiter:init(Zone, undefined, undefined, []), + MQTTPiggyback = get_ws_opts(Zone, Listener, mqtt_piggyback), + FrameOpts = #{ + strict_mode => emqx_config:get_listener_conf(Zone, Listener, [mqtt, strict_mode]), + max_size => emqx_config:get_listener_conf(Zone, Listener, [mqtt, max_packet_size]) + }, ParseState = emqx_frame:initial_parse_state(FrameOpts), Serialize = emqx_frame:serialize_opts(), Channel = emqx_channel:init(ConnInfo, Opts), - GcState = emqx_zone:init_gc_state(Zone), - StatsTimer = emqx_zone:stats_timer(Zone), + GcState = case emqx_config:get_listener_conf(Zone, Listener, [force_gc]) of + #{enable := false} -> undefined; + GcPolicy -> emqx_gc:init(GcPolicy) + end, + StatsTimer = case emqx_config:get_listener_conf(Zone, Listener, [stats, enable]) of + true -> undefined; + false -> disabled + end, %% MQTT Idle Timeout - IdleTimeout = emqx_zone:idle_timeout(Zone), + IdleTimeout = emqx_channel:get_mqtt_conf(Zone, Listener, idle_timeout), IdleTimer = start_timer(IdleTimeout, idle_timeout), - emqx_misc:tune_heap_size(emqx_zone:oom_policy(Zone)), + case emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), + emqx_channel:info(listener, Channel), [force_shutdown]) of + #{enable := false} -> ok; + ShutdownPolicy -> emqx_misc:tune_heap_size(ShutdownPolicy) + end, emqx_logger:set_metadata_peername(esockd:format(Peername)), {ok, #state{peername = Peername, sockname = Sockname, @@ -317,7 +316,9 @@ websocket_init([Req, Opts]) -> postponed = [], stats_timer = StatsTimer, idle_timeout = IdleTimeout, - idle_timer = IdleTimer + idle_timer = IdleTimer, + zone = Zone, + listener = Listener }, hibernate}. websocket_handle({binary, Data}, State) when is_list(Data) -> @@ -517,11 +518,16 @@ run_gc(Stats, State = #state{gc_state = GcSt}) -> end. check_oom(State = #state{channel = Channel}) -> - OomPolicy = emqx_zone:oom_policy(emqx_channel:info(zone, Channel)), - case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of - Shutdown = {shutdown, _Reason} -> - postpone(Shutdown, State); - _Other -> State + ShutdownPolicy = emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), + emqx_channel:info(listener, Channel), [force_shutdown]), + case ShutdownPolicy of + #{enable := false} -> ok; + #{enable := true} -> + case emqx_misc:check_oom(ShutdownPolicy) of + Shutdown = {shutdown, _Reason} -> + postpone(Shutdown, State); + _Other -> State + end end. %%-------------------------------------------------------------------- @@ -741,9 +747,10 @@ classify([Event|More], Packets, Cmds, Events) -> trigger(Event) -> erlang:send(self(), Event). -get_peer(Req, Opts) -> +get_peer(Req, #{zone := Zone, listener := Listener}) -> {PeerAddr, PeerPort} = cowboy_req:peer(Req), - AddrHeader = cowboy_req:header(proplists:get_value(proxy_address_header, Opts), Req, <<>>), + AddrHeader = cowboy_req:header( + get_ws_opts(Zone, Listener, proxy_address_header), Req, <<>>), ClientAddr = case string:tokens(binary_to_list(AddrHeader), ", ") of [] -> undefined; @@ -756,7 +763,8 @@ get_peer(Req, Opts) -> _ -> PeerAddr end, - PortHeader = cowboy_req:header(proplists:get_value(proxy_port_header, Opts), Req, <<>>), + PortHeader = cowboy_req:header( + get_ws_opts(Zone, Listener, proxy_port_header), Req, <<>>), ClientPort = case string:tokens(binary_to_list(PortHeader), ", ") of [] -> undefined; @@ -777,3 +785,5 @@ set_field(Name, Value, State) -> Pos = emqx_misc:index_of(Name, record_info(fields, state)), setelement(Pos+1, State, Value). +get_ws_opts(Zone, Listener, Key) -> + emqx_config:get_listener_conf(Zone, Listener, [websocket, Key]). From 4dd72e59fa11be25d75d4a2cb4dd6d0c23454296 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 7 Jul 2021 16:22:38 +0800 Subject: [PATCH 105/379] feat(listeners): make the APIs and CLIs work with the new listener --- apps/emqx/src/emqx_listeners.erl | 29 +++--- apps/emqx_management/src/emqx_mgmt.erl | 3 +- .../src/emqx_mgmt_api_listeners.erl | 9 +- apps/emqx_management/src/emqx_mgmt_cli.erl | 98 +++++-------------- .../src/emqx_mod_acl_internal.erl | 7 +- 5 files changed, 50 insertions(+), 96 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 6e3bc69be..1d2592fff 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -39,10 +39,8 @@ start() -> foreach_listeners(fun start_listener/3). -spec(start_listener(atom()) -> ok). -start_listener(Id) -> - {ZoneName, ListenerName} = decode_listener_id(Id), - start_listener(ZoneName, ListenerName, - emqx_config:get([zones, ZoneName, listeners, ListenerName])). +start_listener(ListenerId) -> + apply_on_listener(ListenerId, fun start_listener/3). -spec(start_listener(atom(), atom(), map()) -> ok). start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) -> @@ -133,13 +131,11 @@ esockd_access_rules(StrRules) -> restart() -> foreach_listeners(fun restart_listener/3). --spec(restart_listener(atom()) -> ok | {error, any()}). -restart_listener(ListenerID) -> - {ZoneName, ListenerName} = decode_listener_id(ListenerID), - restart_listener(ZoneName, ListenerName, - emqx_config:get([zones, ZoneName, listeners, ListenerName])). +-spec(restart_listener(atom()) -> ok | {error, term()}). +restart_listener(ListenerId) -> + apply_on_listener(ListenerId, fun restart_listener/3). --spec(restart_listener(atom(), atom(), map()) -> ok | {error, any()}). +-spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}). restart_listener(ZoneName, ListenerName, Conf) -> case stop_listener(ZoneName, ListenerName, Conf) of ok -> start_listener(ZoneName, ListenerName, Conf); @@ -152,10 +148,8 @@ stop() -> foreach_listeners(fun stop_listener/3). -spec(stop_listener(atom()) -> ok | {error, term()}). -stop_listener(ListenerID) -> - {ZoneName, ListenerName} = decode_listener_id(ListenerID), - stop_listener(ZoneName, ListenerName, - emqx_config:get([zones, ZoneName, listeners, ListenerName])). +stop_listener(ListenerId) -> + apply_on_listener(ListenerId, fun stop_listener/3). -spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). stop_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn}) -> @@ -214,3 +208,10 @@ merge_zone_and_listener_confs(ZoneConf, ListenerConf) -> ConfsInZonesOnly = [listeners, overall_max_connections], BaseConf = maps:without(ConfsInZonesOnly, ZoneConf), emqx_map_lib:deep_merge(BaseConf, ListenerConf). + +apply_on_listener(ListenerId, Do) -> + {ZoneName, ListenerName} = decode_listener_id(ListenerId), + case emqx_config:find([zones, ZoneName, listeners, ListenerName]) of + {not_found, _, _} -> error({not_found, ListenerId}); + {ok, Conf} -> Do(ZoneName, ListenerName, Conf) + end. diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index ce83b9b71..e04a7d9bc 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -417,7 +417,7 @@ list_listeners(Node) when Node =:= node() -> Tcp = lists:map(fun({{Protocol, ListenOn}, _Pid}) -> #{protocol => Protocol, listen_on => ListenOn, - identifier => emqx_listeners:find_id_by_listen_on(ListenOn), + identifier => Protocol, acceptors => esockd:get_acceptors({Protocol, ListenOn}), max_conns => esockd:get_max_connections({Protocol, ListenOn}), current_conns => esockd:get_current_connections({Protocol, ListenOn}), @@ -436,6 +436,7 @@ list_listeners(Node) when Node =:= node() -> list_listeners(Node) -> rpc_call(Node, list_listeners, [Node]). +-spec restart_listener(node(), atom()) -> ok | {error, term()}. restart_listener(Node, Identifier) when Node =:= node() -> emqx_listeners:restart_listener(Identifier); diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 7cccbd2ac..1b0c90033 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -30,13 +30,13 @@ -rest_api(#{name => restart_listener, method => 'PUT', - path => "/listeners/:bin:identifier/restart", + path => "/listeners/:atom:identifier/restart", func => restart, descr => "Restart a listener in the cluster"}). -rest_api(#{name => restart_node_listener, method => 'PUT', - path => "/nodes/:atom:node/listeners/:bin:identifier/restart", + path => "/nodes/:atom:node/listeners/:atom:identifier/restart", func => restart, descr => "Restart a listener on a node"}). @@ -57,10 +57,7 @@ restart(#{node := Node, identifier := Identifier}, _Params) -> ok -> minirest:return({ok, "Listener restarted."}); {error, Error} -> minirest:return({error, Error}) end; - -%% Restart listeners in the cluster. -restart(#{identifier := <<"http", _/binary>>}, _Params) -> - {403, <<"http_listener_restart_unsupported">>}; +%% Restart listeners on all nodes in the cluster. restart(#{identifier := Identifier}, _Params) -> Results = [{Node, emqx_mgmt:restart_listener(Node, Identifier)} || {Node, _Info} <- emqx_mgmt:list_nodes()], case lists:filter(fun({_, Result}) -> Result =/= ok end, Results) of diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 77fe96182..fb0f64ec9 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -464,86 +464,57 @@ trace_off(Who, Name) -> listeners([]) -> lists:foreach(fun({{Protocol, ListenOn}, _Pid}) -> - Info = [{listen_on, {string, emqx_listeners:format_listen_on(ListenOn)}}, + Info = [{listen_on, {string, format_listen_on(ListenOn)}}, {acceptors, esockd:get_acceptors({Protocol, ListenOn})}, {max_conns, esockd:get_max_connections({Protocol, ListenOn})}, {current_conn, esockd:get_current_connections({Protocol, ListenOn})}, {shutdown_count, esockd:get_shutdown_count({Protocol, ListenOn})} ], - emqx_ctl:print("~s~n", [listener_identifier(Protocol, ListenOn)]), + emqx_ctl:print("~s~n", [Protocol]), lists:foreach(fun indent_print/1, Info) end, esockd:listeners()), lists:foreach(fun({Protocol, Opts}) -> Port = proplists:get_value(port, Opts), - Info = [{listen_on, {string, emqx_listeners:format_listen_on(Port)}}, + Info = [{listen_on, {string, format_listen_on(Port)}}, {acceptors, maps:get(num_acceptors, proplists:get_value(transport_options, Opts, #{}), 0)}, {max_conns, proplists:get_value(max_connections, Opts)}, {current_conn, proplists:get_value(all_connections, Opts)}, {shutdown_count, []}], - emqx_ctl:print("~s~n", [listener_identifier(Protocol, Port)]), + emqx_ctl:print("~s~n", [Protocol]), lists:foreach(fun indent_print/1, Info) end, ranch:info()); -listeners(["stop", Name = "http" ++ _N | _MaybePort]) -> - %% _MaybePort is to be backward compatible, to stop http listener, there is no need for the port number - case minirest:stop_http(list_to_atom(Name)) of +listeners(["stop", ListenerId]) -> + case emqx_listeners:stop_listener(list_to_atom(ListenerId)) of ok -> - emqx_ctl:print("Stop ~s listener successfully.~n", [Name]); + emqx_ctl:print("Stop ~s listener successfully.~n", [ListenerId]); {error, Error} -> - emqx_ctl:print("Failed to stop ~s listener: ~0p~n", [Name, Error]) + emqx_ctl:print("Failed to stop ~s listener: ~0p~n", [ListenerId, Error]) end; -listeners(["stop", "mqtt:" ++ _ = Identifier]) -> - stop_listener(emqx_listeners:find_by_id(Identifier), Identifier); - -listeners(["stop", _Proto, ListenOn]) -> - %% this clause is kept to be backward compatible - ListenOn1 = case string:tokens(ListenOn, ":") of - [Port] -> list_to_integer(Port); - [IP, Port] -> {IP, list_to_integer(Port)} - end, - stop_listener(emqx_listeners:find_by_listen_on(ListenOn1), ListenOn1); - -listeners(["restart", "http:management"]) -> - restart_http_listener(http, emqx_management); - -listeners(["restart", "https:management"]) -> - restart_http_listener(https, emqx_management); - -listeners(["restart", "http:dashboard"]) -> - restart_http_listener(http, emqx_dashboard); - -listeners(["restart", "https:dashboard"]) -> - restart_http_listener(https, emqx_dashboard); - -listeners(["restart", Identifier]) -> - case emqx_listeners:restart_listener(Identifier) of +listeners(["start", ListenerId]) -> + case emqx_listeners:start_listener(list_to_atom(ListenerId)) of ok -> - emqx_ctl:print("Restarted ~s listener successfully.~n", [Identifier]); + emqx_ctl:print("Started ~s listener successfully.~n", [ListenerId]); {error, Error} -> - emqx_ctl:print("Failed to restart ~s listener: ~0p~n", [Identifier, Error]) + emqx_ctl:print("Failed to start ~s listener: ~0p~n", [ListenerId, Error]) + end; + +listeners(["restart", ListenerId]) -> + case emqx_listeners:restart_listener(list_to_atom(ListenerId)) of + ok -> + emqx_ctl:print("Restarted ~s listener successfully.~n", [ListenerId]); + {error, Error} -> + emqx_ctl:print("Failed to restart ~s listener: ~0p~n", [ListenerId, Error]) end; listeners(_) -> emqx_ctl:usage([{"listeners", "List listeners"}, {"listeners stop ", "Stop a listener"}, - {"listeners stop ", "Stop a listener"}, + {"listeners start ", "Start a listener"}, {"listeners restart ", "Restart a listener"} ]). -stop_listener(false, Input) -> - emqx_ctl:print("No such listener ~p~n", [Input]); -stop_listener(#{listen_on := ListenOn} = Listener, _Input) -> - ID = emqx_listeners:identifier(Listener), - ListenOnStr = emqx_listeners:format_listen_on(ListenOn), - case emqx_listeners:stop_listener(Listener) of - ok -> - emqx_ctl:print("Stop ~s listener on ~s successfully.~n", [ID, ListenOnStr]); - {error, Reason} -> - emqx_ctl:print("Failed to stop ~s listener on ~s: ~0p~n", - [ID, ListenOnStr, Reason]) - end. - %%-------------------------------------------------------------------- %% @doc data Command @@ -692,24 +663,9 @@ indent_print({Key, {string, Val}}) -> indent_print({Key, Val}) -> emqx_ctl:print(" ~-16s: ~w~n", [Key, Val]). -listener_identifier(Protocol, ListenOn) -> - case emqx_listeners:find_id_by_listen_on(ListenOn) of - false -> - atom_to_list(Protocol); - ID -> - ID - end. - -restart_http_listener(Scheme, AppName) -> - Listeners = application:get_env(AppName, listeners, []), - case lists:keyfind(Scheme, 1, Listeners) of - false -> - emqx_ctl:print("Listener ~s not exists!~n", [AppName]); - {Scheme, Port, Options} -> - ModName = http_mod_name(AppName), - ModName:stop_listener({Scheme, Port, Options}), - ModName:start_listener({Scheme, Port, Options}) - end. - -http_mod_name(emqx_management) -> emqx_mgmt_http; -http_mod_name(Name) -> Name. +format_listen_on(Port) when is_integer(Port) -> + io_lib:format("0.0.0.0:~w", [Port]); +format_listen_on({Addr, Port}) when is_list(Addr) -> + io_lib:format("~s:~w", [Addr, Port]); +format_listen_on({Addr, Port}) when is_tuple(Addr) -> + io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). \ No newline at end of file diff --git a/apps/emqx_modules/src/emqx_mod_acl_internal.erl b/apps/emqx_modules/src/emqx_mod_acl_internal.erl index 8956229ea..5fa459c5c 100644 --- a/apps/emqx_modules/src/emqx_mod_acl_internal.erl +++ b/apps/emqx_modules/src/emqx_mod_acl_internal.erl @@ -50,10 +50,9 @@ unload(_Env) -> emqx_hooks:del('client.check_acl', {?MODULE, check_acl}). reload(Env) -> - emqx_acl_cache:is_enabled() andalso ( - lists:foreach( - fun(Pid) -> erlang:send(Pid, clean_acl_cache) end, - emqx_cm:all_channels())), + lists:foreach( + fun(Pid) -> erlang:send(Pid, clean_acl_cache) end, + emqx_cm:all_channels()), unload(Env), load(Env). description() -> From dc98cff27bca0329bec2dbb994b4c5d9f3b9f1dc Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 7 Jul 2021 17:04:05 +0800 Subject: [PATCH 106/379] fix: mgmt conf & schema; prepare minirest (#5178) --- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 5 +- .../test/emqx_dashboard_SUITE.erl | 5 +- apps/emqx_management/etc/emqx_management.conf | 10 +++- .../src/emqx_management_schema.erl | 23 +++---- apps/emqx_management/src/emqx_mgmt.erl | 8 +++ .../emqx_management/src/emqx_mgmt_api_acl.erl | 8 +-- .../src/emqx_mgmt_api_alarms.erl | 8 +-- .../src/emqx_mgmt_api_apps.erl | 18 +++--- .../src/emqx_mgmt_api_banned.erl | 10 ++-- .../src/emqx_mgmt_api_brokers.erl | 6 +- .../src/emqx_mgmt_api_clients.erl | 60 +++++++++---------- .../src/emqx_mgmt_api_listeners.erl | 12 ++-- .../src/emqx_mgmt_api_metrics.erl | 6 +- .../src/emqx_mgmt_api_nodes.erl | 4 +- .../src/emqx_mgmt_api_plugins.erl | 18 +++--- .../src/emqx_mgmt_api_pubsub.erl | 18 +++--- .../src/emqx_mgmt_api_routes.erl | 4 +- .../src/emqx_mgmt_api_stats.erl | 6 +- .../src/emqx_mgmt_api_subscriptions.erl | 14 ++--- apps/emqx_management/src/emqx_mgmt_auth.erl | 24 ++++---- apps/emqx_management/src/emqx_mgmt_http.erl | 2 +- apps/emqx_management/test/emqx_mgmt_SUITE.erl | 2 +- .../test/emqx_mgmt_api_SUITE.erl | 5 +- .../test/etc/emqx_management.conf | 10 +++- apps/emqx_modules/test/emqx_modules_SUITE.erl | 5 +- .../test/emqx_retainer_api_SUITE.erl | 5 +- 26 files changed, 152 insertions(+), 144 deletions(-) diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 24683cd5b..f1b691587 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -59,9 +59,8 @@ set_special_configs(emqx_authz) -> ok; set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], - default_application_id => <<"admin">>, - default_application_secret => <<"public">>}), + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), ok; set_special_configs(_App) -> diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 3ea8ab743..360495dbc 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -51,9 +51,8 @@ end_per_suite(_Config) -> ekka_mnesia:ensure_stopped(). set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], - default_application_id => <<"admin">>, - default_application_secret => <<"public">>}), + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), ok; set_special_configs(_) -> ok. diff --git a/apps/emqx_management/etc/emqx_management.conf b/apps/emqx_management/etc/emqx_management.conf index 62f26474c..89cc0293b 100644 --- a/apps/emqx_management/etc/emqx_management.conf +++ b/apps/emqx_management/etc/emqx_management.conf @@ -1,12 +1,16 @@ emqx_management:{ - default_application_id: "admin" - default_application_secret: "public" + applications: [ + { + id: "admin", + secret: "public" + } + ] max_row_limit: 10000 listeners: [ { num_acceptors: 4 max_connections: 512 - protocol: "http" + protocol: http port: 8081 backlog: 512 send_timeout: 15s diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl index 0b99d1046..3bee25031 100644 --- a/apps/emqx_management/src/emqx_management_schema.erl +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -20,19 +20,24 @@ -behaviour(hocon_schema). -export([ structs/0 - , fields/1]). + , fields/1]). structs() -> ["emqx_management"]. fields("emqx_management") -> - [ {default_application_id, fun default_application_id/1} - , {default_application_secret, fun default_application_secret/1} + [ {applications, hoconsc:array(hoconsc:ref(?MODULE, "application"))} , {max_row_limit, fun max_row_limit/1} , {listeners, hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"), hoconsc:ref(?MODULE, "https")]))} ]; +fields("application") -> + [ {"id", emqx_schema:t(string(), undefined, "admin")} + , {"secret", emqx_schema:t(string(), undefined, "public")} + ]; + + fields("http") -> - [ {"protocol", emqx_schema:t(string(), undefined, "http")} + [ {"protocol", hoconsc:enum([http, https])} , {"port", emqx_schema:t(integer(), undefined, 8081)} , {"num_acceptors", emqx_schema:t(integer(), undefined, 4)} , {"max_connections", emqx_schema:t(integer(), undefined, 512)} @@ -46,16 +51,6 @@ fields("http") -> fields("https") -> emqx_schema:ssl(undefined, #{enable => true}) ++ fields("http"). -default_application_id(type) -> string(); -default_application_id(default) -> "admin"; -default_application_id(nullable) -> true; -default_application_id(_) -> undefined. - -default_application_secret(type) -> string(); -default_application_secret(default) -> "public"; -default_application_secret(nullable) -> true; -default_application_secret(_) -> undefined. - max_row_limit(type) -> integer(); max_row_limit(default) -> 1000; max_row_limit(nullable) -> false; diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 1aea90459..245031354 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -106,10 +106,18 @@ , max_row_limit/0 ]). +-export([ return/0 + , return/1]). + -define(MAX_ROW_LIMIT, 10000). -define(APP, emqx_management). +return() -> + minirest:return(). +return(Response) -> + minirest:return(Response). + %%-------------------------------------------------------------------- %% Node Info %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_acl.erl b/apps/emqx_management/src/emqx_mgmt_api_acl.erl index 039b4035a..025a2263b 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_acl.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_acl.erl @@ -36,12 +36,12 @@ clean_all(_Bindings, _Params) -> case emqx_mgmt:clean_acl_cache_all() of - ok -> minirest:return(); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end. clean_node(#{node := Node}, _Params) -> case emqx_mgmt:clean_acl_cache_all(Node) of - ok -> minirest:return(); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index d8a0f25dc..9b9e4a4a6 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -125,13 +125,13 @@ get_name(Params) -> binary_to_atom(proplists:get_value(<<"name">>, Params, undefined), utf8). do_deactivate(undefined, _) -> - minirest:return({error, missing_param}); + emqx_mgmt:return({error, missing_param}); do_deactivate(_, undefined) -> - minirest:return({error, missing_param}); + emqx_mgmt:return({error, missing_param}); do_deactivate(Node, Name) -> case emqx_mgmt:deactivate(Node, Name) of ok -> - minirest:return(); + emqx_mgmt:return(); {error, Reason} -> - minirest:return({error, Reason}) + emqx_mgmt:return({error, Reason}) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_apps.erl b/apps/emqx_management/src/emqx_mgmt_api_apps.erl index cca0b41f0..fa6bfeeb3 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_apps.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_apps.erl @@ -63,30 +63,30 @@ add_app(_Bindings, Params) -> Status = proplists:get_value(<<"status">>, Params), Expired = proplists:get_value(<<"expired">>, Params), case emqx_mgmt_auth:add_app(AppId, Name, Secret, Desc, Status, Expired) of - {ok, AppSecret} -> minirest:return({ok, #{secret => AppSecret}}); - {error, Reason} -> minirest:return({error, Reason}) + {ok, AppSecret} -> emqx_mgmt:return({ok, #{secret => AppSecret}}); + {error, Reason} -> emqx_mgmt:return({error, Reason}) end. del_app(#{appid := AppId}, _Params) -> case emqx_mgmt_auth:del_app(AppId) of - ok -> minirest:return(); - {error, Reason} -> minirest:return({error, Reason}) + ok -> emqx_mgmt:return(); + {error, Reason} -> emqx_mgmt:return({error, Reason}) end. list_apps(_Bindings, _Params) -> - minirest:return({ok, [format(Apps)|| Apps <- emqx_mgmt_auth:list_apps()]}). + emqx_mgmt:return({ok, [format(Apps)|| Apps <- emqx_mgmt_auth:list_apps()]}). lookup_app(#{appid := AppId}, _Params) -> case emqx_mgmt_auth:lookup_app(AppId) of {AppId, AppSecret, Name, Desc, Status, Expired} -> - minirest:return({ok, #{app_id => AppId, + emqx_mgmt:return({ok, #{app_id => AppId, secret => AppSecret, name => Name, desc => Desc, status => Status, expired => Expired}}); undefined -> - minirest:return({ok, #{}}) + emqx_mgmt:return({ok, #{}}) end. update_app(#{appid := AppId}, Params) -> @@ -95,8 +95,8 @@ update_app(#{appid := AppId}, Params) -> Status = proplists:get_value(<<"status">>, Params), Expired = proplists:get_value(<<"expired">>, Params), case emqx_mgmt_auth:update_app(AppId, Name, Desc, Status, Expired) of - ok -> minirest:return(); - {error, Reason} -> minirest:return({error, Reason}) + ok -> emqx_mgmt:return(); + {error, Reason} -> emqx_mgmt:return({error, Reason}) end. format({AppId, _AppSecret, Name, Desc, Status, Expired}) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl index b92875d9e..bdd43b35c 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl @@ -44,7 +44,7 @@ ]). list(_Bindings, Params) -> - minirest:return({ok, emqx_mgmt_api:paginate(emqx_banned, Params, fun format/1)}). + emqx_mgmt:return({ok, emqx_mgmt_api:paginate(emqx_banned, Params, fun format/1)}). create(_Bindings, Params) -> case pipeline([fun ensure_required/1, @@ -52,9 +52,9 @@ create(_Bindings, Params) -> {ok, NParams} -> {ok, Banned} = pack_banned(NParams), ok = emqx_mgmt:create_banned(Banned), - minirest:return({ok, maps:from_list(Params)}); + emqx_mgmt:return({ok, maps:from_list(Params)}); {error, Code, Message} -> - minirest:return({error, Code, Message}) + emqx_mgmt:return({error, Code, Message}) end. delete(#{as := As, who := Who}, _) -> @@ -64,9 +64,9 @@ delete(#{as := As, who := Who}, _) -> fun validate_params/1], Params) of {ok, NParams} -> do_delete(proplists:get_value(<<"as">>, NParams), proplists:get_value(<<"who">>, NParams)), - minirest:return(); + emqx_mgmt:return(); {error, Code, Message} -> - minirest:return({error, Code, Message}) + emqx_mgmt:return({error, Code, Message}) end. pipeline([], Params) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_brokers.erl b/apps/emqx_management/src/emqx_mgmt_api_brokers.erl index bd901a3fe..836f097cb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_brokers.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_brokers.erl @@ -35,13 +35,13 @@ ]). list(_Bindings, _Params) -> - minirest:return({ok, [Info || {_Node, Info} <- emqx_mgmt:list_brokers()]}). + emqx_mgmt:return({ok, [Info || {_Node, Info} <- emqx_mgmt:list_brokers()]}). get(#{node := Node}, _Params) -> case emqx_mgmt:lookup_broker(Node) of {error, Reason} -> - minirest:return({error, ?ERROR2, Reason}); + emqx_mgmt:return({error, ?ERROR2, Reason}); Info -> - minirest:return({ok, Info}) + emqx_mgmt:return({ok, Info}) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 2fe6a5ccb..523314ebb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -151,93 +151,93 @@ list(#{node := Node}, Params) when Node =:= node() -> list(Bindings = #{node := Node}, Params) -> case rpc:call(Node, ?MODULE, list, [Bindings, Params]) of - {badrpc, Reason} -> minirest:return({error, ?ERROR1, Reason}); + {badrpc, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}); Res -> Res end. %% @private fence(Func) -> try - minirest:return({ok, Func()}) + emqx_mgmt:return({ok, Func()}) catch throw : {bad_value_type, {_Key, Type, Value}} -> Reason = iolist_to_binary( io_lib:format("Can't convert ~p to ~p type", [Value, Type]) ), - minirest:return({error, ?ERROR8, Reason}) + emqx_mgmt:return({error, ?ERROR8, Reason}) end. lookup(#{node := Node, clientid := ClientId}, _Params) -> - minirest:return({ok, emqx_mgmt:lookup_client(Node, {clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); + emqx_mgmt:return({ok, emqx_mgmt:lookup_client(Node, {clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); lookup(#{clientid := ClientId}, _Params) -> - minirest:return({ok, emqx_mgmt:lookup_client({clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); + emqx_mgmt:return({ok, emqx_mgmt:lookup_client({clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); lookup(#{node := Node, username := Username}, _Params) -> - minirest:return({ok, emqx_mgmt:lookup_client(Node, {username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}); + emqx_mgmt:return({ok, emqx_mgmt:lookup_client(Node, {username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}); lookup(#{username := Username}, _Params) -> - minirest:return({ok, emqx_mgmt:lookup_client({username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}). + emqx_mgmt:return({ok, emqx_mgmt:lookup_client({username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}). kickout(#{clientid := ClientId}, _Params) -> case emqx_mgmt:kickout_client(emqx_mgmt_util:urldecode(ClientId)) of - ok -> minirest:return(); - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end. clean_acl_cache(#{clientid := ClientId}, _Params) -> case emqx_mgmt:clean_acl_cache(emqx_mgmt_util:urldecode(ClientId)) of - ok -> minirest:return(); - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end. list_acl_cache(#{clientid := ClientId}, _Params) -> case emqx_mgmt:list_acl_cache(emqx_mgmt_util:urldecode(ClientId)) of - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}); - Caches -> minirest:return({ok, [format_acl_cache(Cache) || Cache <- Caches]}) + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}); + Caches -> emqx_mgmt:return({ok, [format_acl_cache(Cache) || Cache <- Caches]}) end. set_ratelimit_policy(#{clientid := ClientId}, Params) -> P = [{conn_bytes_in, proplists:get_value(<<"conn_bytes_in">>, Params)}, {conn_messages_in, proplists:get_value(<<"conn_messages_in">>, Params)}], case [{K, parse_ratelimit_str(V)} || {K, V} <- P, V =/= undefined] of - [] -> minirest:return(); + [] -> emqx_mgmt:return(); Policy -> case emqx_mgmt:set_ratelimit_policy(emqx_mgmt_util:urldecode(ClientId), Policy) of - ok -> minirest:return(); - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end end. clean_ratelimit(#{clientid := ClientId}, _Params) -> case emqx_mgmt:set_ratelimit_policy(emqx_mgmt_util:urldecode(ClientId), []) of - ok -> minirest:return(); - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end. set_quota_policy(#{clientid := ClientId}, Params) -> P = [{conn_messages_routing, proplists:get_value(<<"conn_messages_routing">>, Params)}], case [{K, parse_ratelimit_str(V)} || {K, V} <- P, V =/= undefined] of - [] -> minirest:return(); + [] -> emqx_mgmt:return(); Policy -> case emqx_mgmt:set_quota_policy(emqx_mgmt_util:urldecode(ClientId), Policy) of - ok -> minirest:return(); - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end end. clean_quota(#{clientid := ClientId}, _Params) -> case emqx_mgmt:set_quota_policy(emqx_mgmt_util:urldecode(ClientId), []) of - ok -> minirest:return(); - {error, not_found} -> minirest:return({error, ?ERROR12, not_found}); - {error, Reason} -> minirest:return({error, ?ERROR1, Reason}) + ok -> emqx_mgmt:return(); + {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); + {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) end. %% @private diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 7cccbd2ac..fd2b575d8 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -44,18 +44,18 @@ %% List listeners on a node. list(#{node := Node}, _Params) -> - minirest:return({ok, format(emqx_mgmt:list_listeners(Node))}); + emqx_mgmt:return({ok, format(emqx_mgmt:list_listeners(Node))}); %% List listeners in the cluster. list(_Binding, _Params) -> - minirest:return({ok, [#{node => Node, listeners => format(Listeners)} + emqx_mgmt:return({ok, [#{node => Node, listeners => format(Listeners)} || {Node, Listeners} <- emqx_mgmt:list_listeners()]}). %% Restart listeners on a node. restart(#{node := Node, identifier := Identifier}, _Params) -> case emqx_mgmt:restart_listener(Node, Identifier) of - ok -> minirest:return({ok, "Listener restarted."}); - {error, Error} -> minirest:return({error, Error}) + ok -> emqx_mgmt:return({ok, "Listener restarted."}); + {error, Error} -> emqx_mgmt:return({error, Error}) end; %% Restart listeners in the cluster. @@ -64,8 +64,8 @@ restart(#{identifier := <<"http", _/binary>>}, _Params) -> restart(#{identifier := Identifier}, _Params) -> Results = [{Node, emqx_mgmt:restart_listener(Node, Identifier)} || {Node, _Info} <- emqx_mgmt:list_nodes()], case lists:filter(fun({_, Result}) -> Result =/= ok end, Results) of - [] -> minirest:return(ok); - Errors -> minirest:return({error, {restart, Errors}}) + [] -> emqx_mgmt:return(ok); + Errors -> emqx_mgmt:return({error, {restart, Errors}}) end. format(Listeners) when is_list(Listeners) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index b59aa0ac5..a4bf652a2 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -31,12 +31,12 @@ -export([list/2]). list(Bindings, _Params) when map_size(Bindings) == 0 -> - minirest:return({ok, [#{node => Node, metrics => maps:from_list(Metrics)} + emqx_mgmt:return({ok, [#{node => Node, metrics => maps:from_list(Metrics)} || {Node, Metrics} <- emqx_mgmt:get_metrics()]}); list(#{node := Node}, _Params) -> case emqx_mgmt:get_metrics(Node) of - {error, Reason} -> minirest:return({error, Reason}); - Metrics -> minirest:return({ok, maps:from_list(Metrics)}) + {error, Reason} -> emqx_mgmt:return({error, Reason}); + Metrics -> emqx_mgmt:return({ok, maps:from_list(Metrics)}) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index c24e46de9..42970cdfb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -33,10 +33,10 @@ ]). list(_Bindings, _Params) -> - minirest:return({ok, [format(Node, Info) || {Node, Info} <- emqx_mgmt:list_nodes()]}). + emqx_mgmt:return({ok, [format(Node, Info) || {Node, Info} <- emqx_mgmt:list_nodes()]}). get(#{node := Node}, _Params) -> - minirest:return({ok, emqx_mgmt:lookup_node(Node)}). + emqx_mgmt:return({ok, emqx_mgmt:lookup_node(Node)}). format(Node, {error, Reason}) -> #{node => Node, error => Reason}; diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl index 369ad6782..fda7151d7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl @@ -69,36 +69,36 @@ ]). list(#{node := Node}, _Params) -> - minirest:return({ok, [format(Plugin) || Plugin <- emqx_mgmt:list_plugins(Node)]}); + emqx_mgmt:return({ok, [format(Plugin) || Plugin <- emqx_mgmt:list_plugins(Node)]}); list(_Bindings, _Params) -> - minirest:return({ok, [format({Node, Plugins}) || {Node, Plugins} <- emqx_mgmt:list_plugins()]}). + emqx_mgmt:return({ok, [format({Node, Plugins}) || {Node, Plugins} <- emqx_mgmt:list_plugins()]}). load(#{node := Node, plugin := Plugin}, _Params) -> - minirest:return(emqx_mgmt:load_plugin(Node, Plugin)). + emqx_mgmt:return(emqx_mgmt:load_plugin(Node, Plugin)). unload(#{node := Node, plugin := Plugin}, _Params) -> - minirest:return(emqx_mgmt:unload_plugin(Node, Plugin)); + emqx_mgmt:return(emqx_mgmt:unload_plugin(Node, Plugin)); unload(#{plugin := Plugin}, _Params) -> Results = [emqx_mgmt:unload_plugin(Node, Plugin) || {Node, _Info} <- emqx_mgmt:list_nodes()], case lists:filter(fun(Item) -> Item =/= ok end, Results) of [] -> - minirest:return(ok); + emqx_mgmt:return(ok); Errors -> - minirest:return(lists:last(Errors)) + emqx_mgmt:return(lists:last(Errors)) end. reload(#{node := Node, plugin := Plugin}, _Params) -> - minirest:return(emqx_mgmt:reload_plugin(Node, Plugin)); + emqx_mgmt:return(emqx_mgmt:reload_plugin(Node, Plugin)); reload(#{plugin := Plugin}, _Params) -> Results = [emqx_mgmt:reload_plugin(Node, Plugin) || {Node, _Info} <- emqx_mgmt:list_nodes()], case lists:filter(fun(Item) -> Item =/= ok end, Results) of [] -> - minirest:return(ok); + emqx_mgmt:return(ok); Errors -> - minirest:return(lists:last(Errors)) + emqx_mgmt:return(lists:last(Errors)) end. format({Node, Plugins}) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl b/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl index e5a3e9d77..28e67c9f1 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl @@ -67,7 +67,7 @@ subscribe(_Bindings, Params) -> logger:debug("API subscribe Params:~p", [Params]), {ClientId, Topic, QoS} = parse_subscribe_params(Params), - minirest:return(do_subscribe(ClientId, Topic, QoS)). + emqx_mgmt:return(do_subscribe(ClientId, Topic, QoS)). publish(_Bindings, Params) -> logger:debug("API publish Params:~p", [Params]), @@ -75,33 +75,33 @@ publish(_Bindings, Params) -> case do_publish(ClientId, Topic, Qos, Retain, Payload) of {ok, MsgIds} -> case proplists:get_value(<<"return">>, Params, undefined) of - undefined -> minirest:return(ok); + undefined -> emqx_mgmt:return(ok); _Val -> case proplists:get_value(<<"topics">>, Params, undefined) of - undefined -> minirest:return({ok, #{msgid => lists:last(MsgIds)}}); - _ -> minirest:return({ok, #{msgids => MsgIds}}) + undefined -> emqx_mgmt:return({ok, #{msgid => lists:last(MsgIds)}}); + _ -> emqx_mgmt:return({ok, #{msgids => MsgIds}}) end end; Result -> - minirest:return(Result) + emqx_mgmt:return(Result) end. unsubscribe(_Bindings, Params) -> logger:debug("API unsubscribe Params:~p", [Params]), {ClientId, Topic} = parse_unsubscribe_params(Params), - minirest:return(do_unsubscribe(ClientId, Topic)). + emqx_mgmt:return(do_unsubscribe(ClientId, Topic)). subscribe_batch(_Bindings, Params) -> logger:debug("API subscribe batch Params:~p", [Params]), - minirest:return({ok, loop_subscribe(Params)}). + emqx_mgmt:return({ok, loop_subscribe(Params)}). publish_batch(_Bindings, Params) -> logger:debug("API publish batch Params:~p", [Params]), - minirest:return({ok, loop_publish(Params)}). + emqx_mgmt:return({ok, loop_publish(Params)}). unsubscribe_batch(_Bindings, Params) -> logger:debug("API unsubscribe batch Params:~p", [Params]), - minirest:return({ok, loop_unsubscribe(Params)}). + emqx_mgmt:return({ok, loop_unsubscribe(Params)}). loop_subscribe(Params) -> loop_subscribe(Params, []). diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 380c9f0f6..00cb7bb90 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -35,11 +35,11 @@ ]). list(Bindings, Params) when map_size(Bindings) == 0 -> - minirest:return({ok, emqx_mgmt_api:paginate(emqx_route, Params, fun format/1)}). + emqx_mgmt:return({ok, emqx_mgmt_api:paginate(emqx_route, Params, fun format/1)}). lookup(#{topic := Topic}, _Params) -> Topic1 = emqx_mgmt_util:urldecode(Topic), - minirest:return({ok, [format(R) || R <- emqx_mgmt:lookup_routes(Topic1)]}). + emqx_mgmt:return({ok, [format(R) || R <- emqx_mgmt:lookup_routes(Topic1)]}). format(#route{topic = Topic, dest = {_, Node}}) -> #{topic => Topic, node => Node}; format(#route{topic = Topic, dest = Node}) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_stats.erl b/apps/emqx_management/src/emqx_mgmt_api_stats.erl index 95d54b775..e57c3dc0e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_stats.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_stats.erl @@ -34,12 +34,12 @@ %% List stats of all nodes list(Bindings, _Params) when map_size(Bindings) == 0 -> - minirest:return({ok, [#{node => Node, stats => maps:from_list(Stats)} + emqx_mgmt:return({ok, [#{node => Node, stats => maps:from_list(Stats)} || {Node, Stats} <- emqx_mgmt:get_stats()]}). %% List stats of a node lookup(#{node := Node}, _Params) -> case emqx_mgmt:get_stats(Node) of - {error, Reason} -> minirest:return({error, Reason}); - Stats -> minirest:return({ok, maps:from_list(Stats)}) + {error, Reason} -> emqx_mgmt:return({error, Reason}); + Stats -> emqx_mgmt:return({ok, maps:from_list(Stats)}) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 4165ca51a..3f563427b 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -63,9 +63,9 @@ list(Bindings, Params) when map_size(Bindings) == 0 -> case proplists:get_value(<<"topic">>, Params) of undefined -> - minirest:return({ok, emqx_mgmt_api:cluster_query(Params, ?SUBS_QS_SCHEMA, ?query_fun)}); + emqx_mgmt:return({ok, emqx_mgmt_api:cluster_query(Params, ?SUBS_QS_SCHEMA, ?query_fun)}); Topic -> - minirest:return({ok, emqx_mgmt:list_subscriptions_via_topic(emqx_mgmt_util:urldecode(Topic), ?format_fun)}) + emqx_mgmt:return({ok, emqx_mgmt:list_subscriptions_via_topic(emqx_mgmt_util:urldecode(Topic), ?format_fun)}) end; list(#{node := Node} = Bindings, Params) -> @@ -73,22 +73,22 @@ list(#{node := Node} = Bindings, Params) -> undefined -> case Node =:= node() of true -> - minirest:return({ok, emqx_mgmt_api:node_query(Node, Params, ?SUBS_QS_SCHEMA, ?query_fun)}); + emqx_mgmt:return({ok, emqx_mgmt_api:node_query(Node, Params, ?SUBS_QS_SCHEMA, ?query_fun)}); false -> case rpc:call(Node, ?MODULE, list, [Bindings, Params]) of - {badrpc, Reason} -> minirest:return({error, Reason}); + {badrpc, Reason} -> emqx_mgmt:return({error, Reason}); Res -> Res end end; Topic -> - minirest:return({ok, emqx_mgmt:list_subscriptions_via_topic(Node, emqx_mgmt_util:urldecode(Topic), ?format_fun)}) + emqx_mgmt:return({ok, emqx_mgmt:list_subscriptions_via_topic(Node, emqx_mgmt_util:urldecode(Topic), ?format_fun)}) end. lookup(#{node := Node, clientid := ClientId}, _Params) -> - minirest:return({ok, format(emqx_mgmt:lookup_subscriptions(Node, emqx_mgmt_util:urldecode(ClientId)))}); + emqx_mgmt:return({ok, format(emqx_mgmt:lookup_subscriptions(Node, emqx_mgmt_util:urldecode(ClientId)))}); lookup(#{clientid := ClientId}, _Params) -> - minirest:return({ok, format(emqx_mgmt:lookup_subscriptions(emqx_mgmt_util:urldecode(ClientId)))}). + emqx_mgmt:return({ok, format(emqx_mgmt:lookup_subscriptions(emqx_mgmt_util:urldecode(ClientId)))}). format(Items) when is_list(Items) -> [format(Item) || Item <- Items]; diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index fbba0b2a4..d42b921b4 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -66,18 +66,20 @@ mnesia(copy) -> %%-------------------------------------------------------------------- %% Manage Apps %%-------------------------------------------------------------------- --spec(add_default_app() -> ok | {ok, appsecret()} | {error, term()}). +-spec(add_default_app() -> list()). add_default_app() -> - AppId = emqx_config:get([?APP, default_application_id], undefined), - AppSecret = emqx_config:get([?APP, default_application_secret], undefined), - case {AppId, AppSecret} of - {undefined, _} -> ok; - {_, undefined} -> ok; - {_, _} -> - AppId1 = to_binary(AppId), - AppSecret1 = to_binary(AppSecret), - add_app(AppId1, <<"Default">>, AppSecret1, <<"Application user">>, true, undefined) - end. + Apps = emqx_config:get([?APP, applications], []), + [ begin + case {AppId, AppSecret} of + {undefined, _} -> ok; + {_, undefined} -> ok; + {_, _} -> + AppId1 = to_binary(AppId), + AppSecret1 = to_binary(AppSecret), + add_app(AppId1, <<"Default">>, AppSecret1, <<"Application user">>, true, undefined) + end + end + || #{id := AppId, secret := AppSecret} <- Apps]. -spec(add_app(appid(), binary()) -> {ok, appsecret()} | {error, term()}). add_app(AppId, Name) when is_binary(AppId) -> diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index ecf204128..9890e3935 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -78,7 +78,7 @@ stop_listener({Proto, Port, _}) -> minirest:stop_http(listener_name(Proto)). listeners() -> - [{list_to_atom(Protocol), Port, maps:to_list(maps:without([protocol, port], Map))} + [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} || Map = #{protocol := Protocol,port := Port} <- emqx_config:get([emqx_management, listeners], [])]. diff --git a/apps/emqx_management/test/emqx_mgmt_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_SUITE.erl index 415bfca2e..1108dc37f 100644 --- a/apps/emqx_management/test/emqx_mgmt_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_SUITE.erl @@ -41,7 +41,7 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([emqx_management, emqx_retainer]). set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}]}), + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}]}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl index 66d9328a6..69be9af32 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl @@ -51,9 +51,8 @@ end_per_testcase(_, Config) -> Config. set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], - default_application_id => <<"admin">>, - default_application_secret => <<"public">>}), + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_management/test/etc/emqx_management.conf b/apps/emqx_management/test/etc/emqx_management.conf index e54164cbd..0fb2e4250 100644 --- a/apps/emqx_management/test/etc/emqx_management.conf +++ b/apps/emqx_management/test/etc/emqx_management.conf @@ -1,12 +1,16 @@ emqx_management:{ - default_application_id: "admin" - default_application_secret: "public" + applications: [ + { + id: "admin", + secret: "public" + } + ] max_row_limit: 10000 listeners: [ { num_acceptors: 4 max_connections: 512 - protocol: "http" + protocol: http port: 8080 backlog: 512 send_timeout: 15s diff --git a/apps/emqx_modules/test/emqx_modules_SUITE.erl b/apps/emqx_modules/test/emqx_modules_SUITE.erl index 897c73d50..ec717381c 100644 --- a/apps/emqx_modules/test/emqx_modules_SUITE.erl +++ b/apps/emqx_modules/test/emqx_modules_SUITE.erl @@ -37,9 +37,8 @@ init_per_suite(Config) -> Config. set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], - default_application_id => <<"admin">>, - default_application_secret => <<"public">>}), + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), ok; set_special_configs(_) -> ok. diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl index 57196c7bd..5fd1ff4e6 100644 --- a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl @@ -56,9 +56,8 @@ init_per_testcase(_, Config) -> set_special_configs(emqx_retainer) -> init_emqx_retainer_conf(0); set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => "http", port => 8081}], - default_application_id => <<"admin">>, - default_application_secret => <<"public">>}), + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), ok; set_special_configs(_) -> ok. From 6adb76cf27998dbbcb0815d6c6624f560b7c1973 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 2 Jul 2021 12:31:35 +0200 Subject: [PATCH 107/379] ci: try 32bit fix from quicer --- apps/emqx/rebar.config | 3 ++- rebar.config | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index fbd980056..a38f4af7a 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -31,7 +31,8 @@ [ meck , {bbmustache,"1.10.0"} , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} + %, {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {branch, "main"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} ]} diff --git a/rebar.config b/rebar.config index fc863f7c4..93fdcfba4 100644 --- a/rebar.config +++ b/rebar.config @@ -63,7 +63,8 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} + %, {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} + , {quicer, {git, "https://github.com/emqx/quic.git", {branch, "main"}}} ]}. {xref_ignores, From 1f20bae3920e6fda0caadcd5a2b1e2452d3e393a Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 2 Jul 2021 18:28:26 +0200 Subject: [PATCH 108/379] feat(quic): conditionally build/start quicer app --- apps/emqx/rebar.config | 1 - apps/emqx/rebar.config.script | 39 +++++++++++++++++++------- apps/emqx/src/emqx.app.src | 2 +- apps/emqx/src/emqx_app.erl | 2 ++ apps/emqx/src/emqx_listeners.erl | 48 +++++++++++++++++++------------- rebar.config | 2 -- rebar.config.erl | 30 ++++++++++++++++---- 7 files changed, 86 insertions(+), 38 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index a38f4af7a..7997f0540 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -20,7 +20,6 @@ , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} ]}. {plugins, [rebar3_proper]}. diff --git a/apps/emqx/rebar.config.script b/apps/emqx/rebar.config.script index eae18f106..2352e4a81 100644 --- a/apps/emqx/rebar.config.script +++ b/apps/emqx/rebar.config.script @@ -1,11 +1,30 @@ -Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}, -AddBcrypt = fun(C) -> - {deps, Deps0} = lists:keyfind(deps, 1, C), - Deps = [Bcrypt | Deps0], - lists:keystore(deps, 1, C, {deps, Deps}) -end, +IsCentos6 = fun() -> + case file:read_file("/etc/centos-release") of + {ok, <<"CentOS release 6", _/binary >>} -> + true; + _ -> + false + end + end, -case os:type() of - {win32, _} -> CONFIG; - _ -> AddBcrypt(CONFIG) -end. +IsWin32 = fun() -> + win32 =:= element(1, os:type()) + end, + +IsQuicSupp = fun() -> + not (IsCentos6() orelse IsWin32() orelse + false =/= os:getenv("EMQX_BUILD_WITHOUT_QUIC") + ) + end, + +Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}, +Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {branch, "main"}}}, + +ExtraDeps = fun(C) -> + {deps, Deps0} = lists:keyfind(deps, 1, C), + Deps = Deps0 ++ [Bcrypt || not IsWin32()] ++ + [ Quicer || IsQuicSupp()], + lists:keystore(deps, 1, C, {deps, Deps}) + end, + +ExtraDeps(CONFIG). diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index d9efbe82a..546b70f14 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -4,7 +4,7 @@ {vsn, "5.0.0"}, % strict semver, bump manually! {modules, []}, {registered, []}, - {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,quicer,jiffy]}, + {applications, [kernel,stdlib,gproc,gen_rpc,esockd,cowboy,sasl,os_mon,jiffy]}, {mod, {emqx_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 666a704f3..d2f5ba691 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -49,6 +49,8 @@ start(_Type, _Args) -> _ = load_ce_modules(), ekka:start(), ok = ekka_rlog:wait_for_shards(?EMQX_SHARDS, infinity), + false == os:getenv("EMQX_NO_QUIC") + andalso application:ensure_all_started(quicer), {ok, Sup} = emqx_sup:start_link(), ok = start_autocluster(), % ok = emqx_plugins:init(), diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 48e99926d..462ba9897 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -106,6 +106,8 @@ format_listen_on(ListenOn) -> format(ListenOn). start_listener(#{proto := Proto, name := Name, listen_on := ListenOn, opts := Options}) -> ID = identifier(Proto, Name), case start_listener(Proto, ListenOn, Options) of + {ok, skipped} -> + console_print("Start ~s listener on ~s skpped.~n", [ID, format(ListenOn)]); {ok, _} -> console_print("Start ~s listener on ~s successfully.~n", [ID, format(ListenOn)]); {error, Reason} -> @@ -123,7 +125,7 @@ console_print(_Fmt, _Args) -> ok. %% Start MQTT/TCP listener -spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) - -> {ok, pid()} | {error, term()}). + -> {ok, pid() | skipped} | {error, term()}). start_listener(tcp, ListenOn, Options) -> start_mqtt_listener('mqtt:tcp', ListenOn, Options); @@ -143,24 +145,32 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> %% Start MQTT/QUIC listener start_listener(quic, ListenOn, Options) -> - %% @fixme unsure why we need reopen lib and reopen config. - quicer_nif:open_lib(), - quicer_nif:reg_open(), - SSLOpts = proplists:get_value(ssl_options, Options), - DefAcceptors = erlang:system_info(schedulers_online) * 8, - ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} - , {key, proplists:get_value(keyfile, SSLOpts)} - , {alpn, ["mqtt"]} - , {conn_acceptors, proplists:get_value(acceptors, Options, DefAcceptors)} - , {idle_timeout_ms, proplists:get_value(idle_timeout, Options, 60000)} - ], - ConnectionOpts = [ {conn_callback, emqx_quic_connection} - , {peer_unidi_stream_count, 1} - , {peer_bidi_stream_count, 10} - | Options - ], - StreamOpts = [], - quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). + IsQuicEnabled = false == os:getenv("EMQX_NO_QUIC"), + case [ A || {quicer, _, _} = A<-application:which_applications() ] of + [_] when IsQuicEnabled -> + %% @fixme unsure why we need reopen lib and reopen config. + quicer_nif:open_lib(), + quicer_nif:reg_open(), + SSLOpts = proplists:get_value(ssl_options, Options), + DefAcceptors = erlang:system_info(schedulers_online) * 8, + ListenOpts = [ {cert, proplists:get_value(certfile, SSLOpts)} + , {key, proplists:get_value(keyfile, SSLOpts)} + , {alpn, ["mqtt"]} + , {conn_acceptors, proplists:get_value(acceptors, Options, DefAcceptors)} + , {idle_timeout_ms, proplists:get_value(idle_timeout, Options, 60000)} + ], + ConnectionOpts = [ {conn_callback, emqx_quic_connection} + , {peer_unidi_stream_count, 1} + , {peer_bidi_stream_count, 10} + | Options + ], + StreamOpts = [], + quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}); + [] -> + io:format(standard_error, "INFO: quicer application is unavailable/disabled~n", + []), + {ok, skipped} + end. replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)]. diff --git a/rebar.config b/rebar.config index 93fdcfba4..120029643 100644 --- a/rebar.config +++ b/rebar.config @@ -63,8 +63,6 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - %, {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.5"}}} - , {quicer, {git, "https://github.com/emqx/quic.git", {branch, "main"}}} ]}. {xref_ignores, diff --git a/rebar.config.erl b/rebar.config.erl index de42601af..258cd0bf3 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -15,12 +15,14 @@ do(Dir, CONFIG) -> bcrypt() -> {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}. +quicer() -> + %% @todo use tag + {quicer, {git, "https://github.com/emqx/quic.git", {branch, "main"}}}. + deps(Config) -> {deps, OldDeps} = lists:keyfind(deps, 1, Config), - MoreDeps = case provide_bcrypt_dep() of - true -> [bcrypt()]; - false -> [] - end, + MoreDeps = [bcrypt() || provide_bcrypt_dep()] ++ + [quicer() || is_quicer_supported()], {HasElixir, ExtraDeps} = extra_deps(), {HasElixir, lists:keystore(deps, 1, Config, {deps, OldDeps ++ MoreDeps ++ ExtraDeps})}. @@ -78,6 +80,24 @@ is_cover_enabled() -> is_enterprise() -> filelib:is_regular("EMQX_ENTERPRISE"). +is_quicer_supported() -> + not (false =/= os:getenv("BUILD_WITHOUT_QUIC") orelse + is_win32() orelse is_centos_6() + ). + +is_centos_6() -> + %% reason: + %% glibc is too old + case file:read_file("/etc/centos-release") of + {ok, <<"CentOS release 6", _/binary >>} -> + true; + _ -> + false + end. + +is_win32() -> + win32 =:= element(1, os:type()). + project_app_dirs() -> ["apps/*"] ++ case is_enterprise() of @@ -242,7 +262,6 @@ relx_apps(ReleaseType) -> , compiler , runtime_tools , cuttlefish - , quicer , emqx , {mnesia, load} , {ekka, load} @@ -263,6 +282,7 @@ relx_apps(ReleaseType) -> , emqx_retainer , emqx_statsd ] + ++ [quicer || is_quicer_supported()] ++ [emqx_telemetry || not is_enterprise()] ++ [emqx_license || is_enterprise()] ++ [bcrypt || provide_bcrypt_release(ReleaseType)] From 9d760ff5bda70290ce0b58f18f0bc0fedfbd19d9 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 2 Jul 2021 23:58:44 +0200 Subject: [PATCH 109/379] ci(build-packages): rm rebar.lock in prepare phase --- .github/workflows/build_packages.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 0f1f8ccac..1311cdc91 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -42,6 +42,7 @@ jobs: if: endsWith(github.repository, 'emqx') run: | make -C source deps-all + rm source/rebar.lock zip -ryq source.zip source/* source/.[^.]* - name: get_all_deps if: endsWith(github.repository, 'enterprise') From 7a3330856d193a9f764313bd225d85cdf99d3192 Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 5 Jul 2021 11:39:44 +0200 Subject: [PATCH 110/379] ci(build-packages): Don't start quic on ARM platform --- .ci/build_packages/tests.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 47994c9ad..d5517f3a3 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -12,13 +12,16 @@ case "$(uname -m)" in ARCH='amd64' ;; aarch64) + EMQX_NO_QUIC=0 ARCH='arm64' ;; arm*) + EMQX_NO_QUIC=0 ARCH=arm ;; esac export ARCH +export EMQX_NO_QUIC emqx_prepare(){ mkdir -p "${PACKAGE_PATH}" From 660d16e84b878b42eb9d984c0401c0bb5d4876fe Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 5 Jul 2021 23:43:35 +0200 Subject: [PATCH 111/379] feat(config): set the endpoint to "" to disable listener --- apps/emqx/src/emqx_listeners.erl | 3 +-- apps/emqx/src/emqx_schema.erl | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 462ba9897..3e688b91e 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -145,9 +145,8 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> %% Start MQTT/QUIC listener start_listener(quic, ListenOn, Options) -> - IsQuicEnabled = false == os:getenv("EMQX_NO_QUIC"), case [ A || {quicer, _, _} = A<-application:which_applications() ] of - [_] when IsQuicEnabled -> + [_] -> %% @fixme unsure why we need reopen lib and reopen config. quicer_nif:open_lib(), quicer_nif:reg_open(), diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 73a5812e8..2a58b34c3 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -333,7 +333,7 @@ fields("quic_listener") -> [ {"$name", ref("quic_listener_settings")}]; fields("listener_settings") -> - [ {"endpoint", t(union(ip_port(), integer()))} + [ {"endpoint", t(union([ip_port(), integer(), ""]))} , {"acceptors", t(integer(), undefined, 8)} , {"max_connections", t(integer(), undefined, 1024)} , {"max_conn_rate", t(integer())} @@ -785,6 +785,7 @@ tr_listeners(Conf) -> TcpListeners = fun(Type, Name) -> Prefix = string:join(["listener", Type, Name], "."), ListenOnN = case conf_get(Prefix ++ ".endpoint", Conf) of + "" -> []; undefined -> []; ListenOn -> ListenOn end, @@ -801,6 +802,8 @@ tr_listeners(Conf) -> SslListeners = fun(Type, Name) -> Prefix = string:join(["listener", Type, Name], "."), case conf_get(Prefix ++ ".endpoint", Conf) of + "" -> + []; undefined -> []; ListenOn -> From 16eb5da44028ff5babbc5971b81db9aa23b5c6a3 Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 5 Jul 2021 23:45:46 +0200 Subject: [PATCH 112/379] ci: disable quic listener on arm --- .ci/build_packages/tests.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index d5517f3a3..3b61d9b33 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -12,16 +12,13 @@ case "$(uname -m)" in ARCH='amd64' ;; aarch64) - EMQX_NO_QUIC=0 ARCH='arm64' ;; arm*) - EMQX_NO_QUIC=0 ARCH=arm ;; esac export ARCH -export EMQX_NO_QUIC emqx_prepare(){ mkdir -p "${PACKAGE_PATH}" @@ -120,6 +117,7 @@ running_test(){ EMQX_MQTT__MAX_TOPIC_ALIAS=10 # sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins + [[ "$ARCH" == arm* ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT="" if ! emqx start; then cat /var/log/emqx/erlang.log.1 || true cat /var/log/emqx/emqx.log.1 || true From 939f3855d509ea89b42db3bfdf1c772e5d1b675b Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 6 Jul 2021 10:14:00 +0200 Subject: [PATCH 113/379] ci(build-packages): docker fail fast: false --- .github/workflows/build_packages.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 1311cdc91..fcffded94 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -340,6 +340,7 @@ jobs: needs: prepare strategy: + fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} arch: From 79d169b3fff1b477ccbd901f9dfce69e82f5711a Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 6 Jul 2021 12:10:14 +0200 Subject: [PATCH 114/379] chore(quic): bump emqtt vsn to 1.4.2 --- apps/emqx/rebar.config | 3 +-- rebar.config | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 7997f0540..1dd82ceaa 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -30,8 +30,7 @@ [ meck , {bbmustache,"1.10.0"} , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}} - %, {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {branch, "main"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} ]} diff --git a/rebar.config b/rebar.config index 120029643..ae87d37bd 100644 --- a/rebar.config +++ b/rebar.config @@ -55,7 +55,7 @@ , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, {git, "https://github.com/emqx/replayq", {tag, "0.3.2"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.1"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.2"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 From 9516e5047a5818db9981a74b1198f88818df2331 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 6 Jul 2021 16:01:20 +0200 Subject: [PATCH 115/379] ci(build-packages): start emqx as user emqx --- .ci/build_packages/tests.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 3b61d9b33..dc4fbd35b 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -113,12 +113,11 @@ emqx_test(){ } running_test(){ - export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ - EMQX_MQTT__MAX_TOPIC_ALIAS=10 # sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins + start_cmd="export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 EMQX_MQTT__MAX_TOPIC_ALIAS=10; \ + [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT=''; emqx start" - [[ "$ARCH" == arm* ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT="" - if ! emqx start; then + if ! su - emqx -c "$start_cmd"; then cat /var/log/emqx/erlang.log.1 || true cat /var/log/emqx/emqx.log.1 || true exit 1 From 5fcdaa6afeddbf95eb914d77980b207074157641 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 6 Jul 2021 20:42:55 +0200 Subject: [PATCH 116/379] ci(build-packages): fix test zip --- .ci/build_packages/tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index dc4fbd35b..7a77bc60e 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -38,7 +38,8 @@ emqx_test(){ packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.zip) unzip -q "${PACKAGE_PATH}/${packagename}" export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ - EMQX_MQTT__MAX_TOPIC_ALIAS=10 + EMQX_MQTT__MAX_TOPIC_ALIAS=10 + [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT='' # sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins echo "running ${packagename} start" From 1b014b492f02fd3c825c948ad7411daa9eb391ad Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 7 Jul 2021 00:04:53 +0200 Subject: [PATCH 117/379] ci(build-package): disable quicer listener for deb test --- .ci/build_packages/tests.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 7a77bc60e..10daca56c 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -115,10 +115,10 @@ emqx_test(){ running_test(){ # sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins - start_cmd="export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 EMQX_MQTT__MAX_TOPIC_ALIAS=10; \ - [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT=''; emqx start" + start_env="export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 EMQX_MQTT__MAX_TOPIC_ALIAS=10; \ + [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT=''" - if ! su - emqx -c "$start_cmd"; then + if ! su - emqx -c "$start_env ; emqx start"; then cat /var/log/emqx/erlang.log.1 || true cat /var/log/emqx/emqx.log.1 || true exit 1 @@ -140,6 +140,7 @@ running_test(){ if [ "$(sed -n '/^ID=/p' /etc/os-release | sed -r 's/ID=(.*)/\1/g' | sed 's/"//g')" = ubuntu ] \ || [ "$(sed -n '/^ID=/p' /etc/os-release | sed -r 's/ID=(.*)/\1/g' | sed 's/"//g')" = debian ] ;then + echo "$start_env" >> /etc/default/emqx if ! service emqx start; then cat /var/log/emqx/erlang.log.1 || true cat /var/log/emqx/emqx.log.1 || true From 2d2be45c0d32d68f5ba9c46b430076ae863e61b6 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 7 Jul 2021 11:15:06 +0200 Subject: [PATCH 118/379] ci(build-package): fix env setting before start emqx --- .ci/build_packages/tests.sh | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 10daca56c..9ee444cd5 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -115,10 +115,24 @@ emqx_test(){ running_test(){ # sed -i '/emqx_telemetry/d' /var/lib/emqx/loaded_plugins - start_env="export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 EMQX_MQTT__MAX_TOPIC_ALIAS=10; \ - [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENER__QUIC__EXTERNAL__ENDPOINT=''" + emqx_env_vars=$(dirname "$(readlink "$(command -v emqx)")")/../releases/emqx_vars - if ! su - emqx -c "$start_env ; emqx start"; then + if [ -f "$emqx_env_vars" ]; + then + tee -a "$emqx_env_vars" <> /etc/default/emqx if ! service emqx start; then cat /var/log/emqx/erlang.log.1 || true cat /var/log/emqx/emqx.log.1 || true From 6d7424b445802250775871572c71628926f1c454 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Wed, 7 Jul 2021 14:02:58 +0800 Subject: [PATCH 119/379] fix(connector): fix mongo connector auth failed --- apps/emqx_connector/rebar.config | 2 +- apps/emqx_connector/src/emqx_connector_mongo.erl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index b12bd3edb..9bfbb9277 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -9,7 +9,7 @@ {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, {epgsql, {git, "https://github.com/epgsql/epgsql", {tag, "4.4.0"}}}, %% NOTE: mind poolboy version when updating mongodb-erlang version - {mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}}, + {mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.8"}}}, %% NOTE: mind poolboy version when updating eredis_cluster version {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.6.7"}}}, %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index b8a3c0da0..3397fabda 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -78,8 +78,8 @@ mongo_fields() -> [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} , {username, fun emqx_connector_schema_lib:username/1} , {password, fun emqx_connector_schema_lib:password/1} - , {authentication_database, #{type => binary(), - nullable => true}} + , {auth_source, #{type => binary(), + nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -218,7 +218,7 @@ init_topology_options([], Acc) -> init_worker_options([{database, V} | R], Acc) -> init_worker_options(R, [{database, V} | Acc]); -init_worker_options([{authentication_database, V} | R], Acc) -> +init_worker_options([{auth_source, V} | R], Acc) -> init_worker_options(R, [{auth_source, V} | Acc]); init_worker_options([{username, V} | R], Acc) -> init_worker_options(R, [{login, V} | Acc]); From 27d5c5b2d93c68de72da242c5741cafa9c8aca49 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 8 Jul 2021 15:05:07 +0800 Subject: [PATCH 120/379] feat(config): make quic listener start with the new config --- apps/emqx/etc/emqx.conf | 23 ++-- apps/emqx/src/emqx_listeners.erl | 10 +- apps/emqx/src/emqx_schema.erl | 5 + .../src/emqx_mod_acl_internal.erl | 121 ------------------ 4 files changed, 25 insertions(+), 134 deletions(-) delete mode 100644 apps/emqx_modules/src/emqx_mod_acl_internal.erl diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index f5495881d..d8db63b3a 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -841,6 +841,7 @@ zones.default { stats.enable: true ## Maximum number of concurrent connections in this zone. + ## ## This value must be larger than the sum of `max_connections` set ## in the listeners under this zone. ## @@ -1258,7 +1259,6 @@ zones.default { min_alarm_sustain_duration: 1m } - listeners.mqtt_tcp: #${example_common_tcp_options} # common options can be written in a separate config entry and reference it from here. { @@ -1531,7 +1531,6 @@ zones.default { } listeners.mqtt_quic: - #${example_common_ssl_options} # common options can be written in a separate config entry and reference it from here. { ## The type of the listener. @@ -1566,13 +1565,19 @@ zones.default { ## Default: infinity max_connections: 1024000 - ## SSL options - ## See ${example_common_ssl_options} for more information - ssl.enable: false - #ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] - #ssl.keyfile: "{{ platform_etc_dir }}/certs/key.pem" - #ssl.certfile: "{{ platform_etc_dir }}/certs/cert.pem" - #ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + ## Path to the file containing the user's private PEM-encoded key. + ## + ## @doc zones..listeners..keyfile + ## ValueType: String + ## Default: "{{ platform_etc_dir }}/certs/key.pem" + keyfile: "{{ platform_etc_dir }}/certs/key.pem" + + ## Path to a file containing the user certificate. + ## + ## @doc zones..listeners..certfile + ## ValueType: String + ## Default: "{{ platform_etc_dir }}/certs/cert.pem" + certfile: "{{ platform_etc_dir }}/certs/cert.pem" } listeners.mqtt_ws: diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 636acc1bc..b6ed8d615 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -86,10 +86,9 @@ do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Op %% @fixme unsure why we need reopen lib and reopen config. quicer_nif:open_lib(), quicer_nif:reg_open(), - SSLOpts = ssl_opts(Opts), DefAcceptors = erlang:system_info(schedulers_online) * 8, - ListenOpts = [ {cert, maps:get(certfile, SSLOpts, undefined)} - , {key, maps:get(keyfile, SSLOpts, undefined)} + ListenOpts = [ {cert, maps:get(certfile, Opts)} + , {key, maps:get(keyfile, Opts)} , {alpn, ["mqtt"]} , {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)} , {idle_timeout_ms, emqx_config:get_listener_conf(ZoneName, ListenerName, @@ -100,7 +99,7 @@ do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Op , peer_bidi_stream_count => 10 }, StreamOpts = [], - quicer:start_listener('mqtt:quic', ListenOn, {ListenOpts, ConnectionOpts, StreamOpts}). + quicer:start_listener('mqtt:quic', port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts}). esockd_opts(Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), @@ -140,6 +139,9 @@ ip_port(Port) when is_integer(Port) -> ip_port({Addr, Port}) -> [{ip, Addr}, {port, Port}]. +port(Port) when is_integer(Port) -> Port; +port({_Addr, Port}) when is_integer(Port) -> Port. + esockd_access_rules(StrRules) -> Access = fun(S) -> [A, CIDR] = string:tokens(S, " "), diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 8d65a8ebe..01082824f 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -372,6 +372,11 @@ fields("mqtt_ws_listener") -> fields("mqtt_quic_listener") -> [ {"type", t(quic)} + , {"certfile", t(string(), undefined, undefined)} + , {"keyfile", t(string(), undefined, undefined)} + , {"ciphers", t(comma_separated_list(), undefined, "TLS_AES_256_GCM_SHA384," + "TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256")} + , {"idle_timeout", t(duration(), undefined, 60000)} ] ++ base_listener(); fields("ws_opts") -> diff --git a/apps/emqx_modules/src/emqx_mod_acl_internal.erl b/apps/emqx_modules/src/emqx_mod_acl_internal.erl deleted file mode 100644 index 5fa459c5c..000000000 --- a/apps/emqx_modules/src/emqx_mod_acl_internal.erl +++ /dev/null @@ -1,121 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_mod_acl_internal). - --behaviour(emqx_gen_mod). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --logger_header("[ACL_INTERNAL]"). - -%% APIs --export([ check_acl/5 - , rules_from_file/1 - ]). - -%% emqx_gen_mod callbacks --export([ load/1 - , unload/1 - , reload/1 - , description/0 - ]). - --type(acl_rules() :: #{publish => [emqx_access_rule:rule()], - subscribe => [emqx_access_rule:rule()]}). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -load(Env) -> - Rules = rules_from_file(proplists:get_value(acl_file, Env)), - emqx_hooks:add('client.check_acl', {?MODULE, check_acl, [Rules]}, -1). - -unload(_Env) -> - emqx_hooks:del('client.check_acl', {?MODULE, check_acl}). - -reload(Env) -> - lists:foreach( - fun(Pid) -> erlang:send(Pid, clean_acl_cache) end, - emqx_cm:all_channels()), - unload(Env), load(Env). - -description() -> - "EMQ X Internal ACL Module". -%%-------------------------------------------------------------------- -%% ACL callbacks -%%-------------------------------------------------------------------- - -%% @doc Check ACL --spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_topic:topic(), - emqx_access_rule:acl_result(), acl_rules()) - -> {ok, allow} | {ok, deny} | ok). -check_acl(Client, PubSub, Topic, _AclResult, Rules) -> - case match(Client, Topic, lookup(PubSub, Rules)) of - {matched, allow} -> {ok, allow}; - {matched, deny} -> {ok, deny}; - nomatch -> ok - end. - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- -lookup(PubSub, Rules) -> - maps:get(PubSub, Rules, []). - -match(_Client, _Topic, []) -> - nomatch; -match(Client, Topic, [Rule|Rules]) -> - case emqx_access_rule:match(Client, Topic, Rule) of - nomatch -> - match(Client, Topic, Rules); - {matched, AllowDeny} -> - {matched, AllowDeny} - end. - --spec(rules_from_file(file:filename()) -> map()). -rules_from_file(AclFile) -> - case file:consult(AclFile) of - {ok, Terms} -> - Rules = [emqx_access_rule:compile(Term) || Term <- Terms], - #{publish => [Rule || Rule <- Rules, filter(publish, Rule)], - subscribe => [Rule || Rule <- Rules, filter(subscribe, Rule)]}; - {error, eacces} -> - ?LOG(alert, "Insufficient permissions to read the ~s file", [AclFile]), - #{}; - {error, enoent} -> - ?LOG(alert, "The ~s file does not exist", [AclFile]), - #{}; - {error, Reason} -> - ?LOG(alert, "Failed to read ~s: ~p", [AclFile, Reason]), - #{} - end. - -filter(_PubSub, {allow, all}) -> - true; -filter(_PubSub, {deny, all}) -> - true; -filter(publish, {_AllowDeny, _Who, publish, _Topics}) -> - true; -filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) -> - true; -filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) -> - true; -filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> - false. - From 0ac2492b365c1f10622c9d4ac912db51c8e511e6 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 8 Jul 2021 19:27:36 +0800 Subject: [PATCH 121/379] fix(config): read app env 'config_files' to get the path to emqx.conf --- apps/emqx/etc/emqx.conf | 7 ------- apps/emqx/src/emqx_config_handler.erl | 11 ++++++----- apps/emqx/src/emqx_listeners.erl | 7 +++++-- apps/emqx/src/emqx_schema.erl | 4 +++- bin/emqx | 1 + 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index d8db63b3a..ec2636c40 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -29,13 +29,6 @@ node { ## Default: "{{ platform_data_dir }}/" data_dir: "{{ platform_data_dir }}/" - ## The config file dir where the emqx.conf can be found - ## - ## @doc node.etc_dir - ## ValueType: Folder - ## Default: "{{ platform_etc_dir }}/" - etc_dir: "{{ platform_etc_dir }}/" - ## Dir of crash dump file. ## ## @doc node.crash_dump_dir diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index eba844829..6a4ac8703 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -72,7 +72,7 @@ add_handler(ConfKeyPath, HandlerName) -> -spec init(term()) -> {ok, state()}. init(_) -> - {ok, RawConf} = hocon:load(emqx_conf_name(), #{format => richmap}), + RawConf = load_config_file(), {_MappedEnvs, Conf} = hocon_schema:map_translate(emqx_schema, RawConf, #{}), ok = save_config_to_emqx(to_plainmap(Conf), to_plainmap(RawConf)), {ok, #{handlers => #{?MOD => ?MODULE}}}. @@ -192,14 +192,15 @@ read_old_config(FileName) -> _ -> #{} end. -emqx_conf_name() -> - filename:join([etc_dir(), "emqx.conf"]). +load_config_file() -> + lists:foldl(fun(ConfFile, Acc) -> + {ok, RawConf} = hocon:load(ConfFile, #{format => richmap}), + emqx_map_lib:deep_merge(Acc, RawConf) + end, #{}, emqx:get_env(config_files, [])). emqx_override_conf_name() -> filename:join([emqx:get_env(data_dir), "emqx_override.conf"]). -etc_dir() -> - emqx:get_env(etc_dir). to_richmap(Map) -> {ok, RichMap} = hocon:binary(jsx:encode(Map), #{format => richmap}), diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index b6ed8d615..bc0fc24b2 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -99,7 +99,8 @@ do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Op , peer_bidi_stream_count => 10 }, StreamOpts = [], - quicer:start_listener('mqtt:quic', port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts}). + quicer:start_listener(listener_id(ZoneName, ListenerName), + port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts}). esockd_opts(Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), @@ -178,7 +179,9 @@ stop_listener(ListenerId) -> stop_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn}) -> esockd:close(listener_id(ZoneName, ListenerName), ListenOn); stop_listener(ZoneName, ListenerName, #{type := ws}) -> - cowboy:stop_listener(listener_id(ZoneName, ListenerName)). + cowboy:stop_listener(listener_id(ZoneName, ListenerName)); +stop_listener(ZoneName, ListenerName, #{type := quic}) -> + quicer:stop_listener(listener_id(ZoneName, ListenerName)). merge_default(Options) -> case lists:keytake(tcp_options, 1, Options) of diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 01082824f..72fe3832b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -146,7 +146,9 @@ fields("node") -> override_env => "EMQX_NODE_COOKIE" })} , {"data_dir", t(string(), "emqx.data_dir", undefined)} - , {"etc_dir", t(string(), "emqx.etc_dir", undefined)} + , {"config_files", t(list(string()), "emqx.config_files", + [ filename:join([os:getenv("RUNNER_ETC_DIR"), "emqx.conf"]) + ])} , {"global_gc_interval", t(duration_s(), "emqx.global_gc_interval", undefined)} , {"crash_dump_dir", t(file(), "vm_args.-env ERL_CRASH_DUMP", undefined)} , {"dist_net_ticktime", t(integer(), "vm_args.-kernel net_ticktime", undefined)} diff --git a/bin/emqx b/bin/emqx index 22593ea82..9f4aa9bd4 100755 --- a/bin/emqx +++ b/bin/emqx @@ -194,6 +194,7 @@ relx_nodetool() { call_hocon() { export RUNNER_ROOT_DIR + export RUNNER_ETC_DIR export REL_VSN "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" hocon "$@" } From e990220e832718554c816e8d54ba6a900edbf74b Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 9 Jul 2021 07:34:04 +0200 Subject: [PATCH 122/379] fix(emqx.conf): default wss max_connection from 16 to 102400 --- apps/emqx/etc/emqx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index d07443012..7e1eedbab 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1878,7 +1878,7 @@ listener.wss.external { ## Maximum number of concurrent MQTT/Webwocket/SSL connections. ## ## Value: Number - max_connections: 16 + max_connections: 102400 ## Maximum MQTT/WebSocket/SSL connections per second. ## From c10d154dab2ea227e297fc12662fac919ff1a68e Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 9 Jul 2021 10:18:20 +0800 Subject: [PATCH 123/379] chore(connector): update schema file Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 9 +--- apps/emqx_authz/src/emqx_authz_schema.erl | 47 +++++++++---------- .../test/emqx_authz_mongo_SUITE.erl | 2 +- .../test/emqx_authz_mysql_SUITE.erl | 2 +- .../test/emqx_authz_pgsql_SUITE.erl | 2 +- .../test/emqx_authz_redis_SUITE.erl | 2 +- .../src/emqx_connector_mongo.erl | 14 +++--- .../src/emqx_connector_mysql.erl | 3 ++ .../src/emqx_connector_pgsql.erl | 3 ++ .../src/emqx_connector_redis.erl | 10 ++-- .../src/emqx_connector_schema_lib.erl | 2 + .../src/emqx_data_bridge_schema.erl | 20 ++++---- .../src/emqx_resource_validator.erl | 2 +- 13 files changed, 59 insertions(+), 59 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 725d884e1..78aa47d91 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -64,15 +64,10 @@ create_resource(#{type := DB, config := Config } = Rule) -> ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]), - NConfig = case DB of - redis -> #{config => Config }; - mongo -> #{config => Config }; - _ -> Config - end, - case emqx_resource:check_and_create( + case emqx_resource:create( ResourceID, list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - NConfig) + Config) of {ok, _} -> Rule#{resource_id => ResourceID}; diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 0b6a1d107..f1a79db25 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -16,30 +16,20 @@ structs() -> ["emqx_authz"]. fields("emqx_authz") -> [ {rules, rules()} ]; -fields(mongo_connector) -> - [ {principal, principal()} - , {type, #{type => hoconsc:enum([mongo])}} - , {config, #{type => map()}} - , {collection, #{type => atom()}} +fields(mongo) -> + connector_fields(mongo) ++ + [ {collection, #{type => atom()}} , {find, #{type => map()}} ]; -fields(redis_connector) -> - [ {principal, principal()} - , {type, #{type => hoconsc:enum([redis])}} - , {config, #{type => hoconsc:union( - [ hoconsc:ref(emqx_connector_redis, cluster) - , hoconsc:ref(emqx_connector_redis, sentinel) - , hoconsc:ref(emqx_connector_redis, single) - ])} - } - , {cmd, query()} - ]; -fields(sql_connector) -> - [ {principal, principal() } - , {type, #{type => hoconsc:enum([mysql, pgsql])}} - , {config, #{type => map()}} - , {sql, query()} - ]; +fields(redis) -> + connector_fields(redis) ++ + [ {cmd, query()} ]; +fields(mysql) -> + connector_fields(mysql) ++ + [ {sql, query()} ]; +fields(pgsql) -> + connector_fields(pgsql) ++ + [ {sql, query()} ]; fields(simple_rule) -> [ {permission, #{type => permission()}} , {action, #{type => action()}} @@ -88,9 +78,10 @@ union_array(Item) when is_list(Item) -> rules() -> #{type => union_array( [ hoconsc:ref(?MODULE, simple_rule) - , hoconsc:ref(?MODULE, sql_connector) - , hoconsc:ref(?MODULE, redis_connector) - , hoconsc:ref(?MODULE, mongo_connector) + , hoconsc:ref(?MODULE, mysql) + , hoconsc:ref(?MODULE, pgsql) + , hoconsc:ref(?MODULE, redis) + , hoconsc:ref(?MODULE, mongo) ]) }. @@ -115,3 +106,9 @@ query() -> end end }. + +connector_fields(DB) -> + Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), + [ {principal, principal()} + , {type, #{type => DB}} + ] ++ Mod:fields(""). diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index daf4d1722..d2792e388 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -30,7 +30,7 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ), + meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), Config. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index edc35ca45..a9acf5e36 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -30,7 +30,7 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ), + meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), Config. diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index d5f89bcad..03bec2415 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -30,7 +30,7 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ), + meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), Config. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 0d7ffa9d8..7530c3183 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -30,7 +30,7 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ), + meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), Config. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 3397fabda..6fac3cb26 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -88,24 +88,24 @@ on_jsonify(Config) -> Config. %% =================================================================== -on_start(InstId, #{config := #{server := Server, - mongo_type := single} = Config}) -> +on_start(InstId, Config = #{server := Server, + mongo_type := single}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, single}, {hosts, [Server]} ], do_start(InstId, Opts, Config); -on_start(InstId, #{config := #{servers := Servers, - mongo_type := rs, - replicaset_name := RsName} = Config}) -> +on_start(InstId, Config = #{servers := Servers, + mongo_type := rs, + replicaset_name := RsName}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, {rs, RsName}}, {hosts, Servers}], do_start(InstId, Opts, Config); -on_start(InstId, #{config := #{servers := Servers, - mongo_type := sharded} = Config}) -> +on_start(InstId, Config = #{servers := Servers, + mongo_type := sharded}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, sharded}, {hosts, Servers} diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index a606bb82d..6a5d93ca2 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -37,6 +37,9 @@ structs() -> [""]. fields("") -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]; + +fields(config) -> emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index ddcc2a7c7..e89ab7401 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -38,6 +38,9 @@ structs() -> [""]. fields("") -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]; + +fields(config) -> emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 0df12185d..1ea31ced8 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -78,11 +78,11 @@ on_jsonify(Config) -> Config. %% =================================================================== -on_start(InstId, #{config :=#{redis_type := Type, - database := Database, - pool_size := PoolSize, - auto_reconnect := AutoReconn, - ssl := SSL } = Config}) -> +on_start(InstId, #{redis_type := Type, + database := Database, + pool_size := PoolSize, + auto_reconnect := AutoReconn, + ssl := SSL } = Config) -> logger:info("starting redis connector: ~p, config: ~p", [InstId, Config]), Servers = case Type of single -> [{servers, [maps:get(server, Config)]}]; diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 743d37ae3..4f43a3bd4 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -86,10 +86,12 @@ relational_db_fields() -> ]. server(type) -> emqx_schema:ip_port(); +server(nullable) -> false; server(validator) -> [?REQUIRED("the field 'server' is required")]; server(_) -> undefined. database(type) -> binary(); +database(nullable) -> false; database(validator) -> [?REQUIRED("the field 'database' is required")]; database(_) -> undefined. diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl index b4749af0e..066d72096 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -5,11 +5,6 @@ %%====================================================================================== %% Hocon Schema Definitions --define(BRIDGE_FIELDS(T), - [{name, hoconsc:t(typerefl:binary())}, - {type, hoconsc:t(typerefl:atom(T))}, - {config, hoconsc:t(hoconsc:ref(list_to_atom("emqx_connector_"++atom_to_list(T)), ""))}]). - -define(TYPES, [mysql, pgsql, mongo, redis, ldap]). -define(BRIDGES, [hoconsc:ref(?MODULE, T) || T <- ?TYPES]). @@ -19,8 +14,13 @@ fields("emqx_data_bridge") -> [{bridges, #{type => hoconsc:array(hoconsc:union(?BRIDGES)), default => []}}]; -fields(mysql) -> ?BRIDGE_FIELDS(mysql); -fields(pgsql) -> ?BRIDGE_FIELDS(pgsql); -fields(mongo) -> ?BRIDGE_FIELDS(mongo); -fields(redis) -> ?BRIDGE_FIELDS(redis); -fields(ldap) -> ?BRIDGE_FIELDS(ldap). +fields(mysql) -> connector_fields(mysql); +fields(pgsql) -> connector_fields(pgsql); +fields(mongo) -> connector_fields(mongo); +fields(redis) -> connector_fields(redis); +fields(ldap) -> connector_fields(ldap). + +connector_fields(DB) -> + Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), + [{name, hoconsc:t(typerefl:binary())}, + {type, #{type => DB}}] ++ Mod:fields(""). diff --git a/apps/emqx_resource/src/emqx_resource_validator.erl b/apps/emqx_resource/src/emqx_resource_validator.erl index e9517f160..519ad4095 100644 --- a/apps/emqx_resource/src/emqx_resource_validator.erl +++ b/apps/emqx_resource/src/emqx_resource_validator.erl @@ -39,7 +39,7 @@ enum(Items) -> end. required(ErrMsg) -> - fun(undefined) -> {error, ErrMsg}; + fun(<<>>) -> {error, ErrMsg}; (_) -> ok end. From 178bafbabf5e0c6090a393110be5a716b2e57ee6 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 9 Jul 2021 13:49:28 +0800 Subject: [PATCH 124/379] chore: rename required function to not_empty Signed-off-by: zhanghongtong --- apps/emqx_connector/include/emqx_connector.hrl | 2 +- apps/emqx_connector/src/emqx_connector_mongo.erl | 4 ++-- apps/emqx_connector/src/emqx_connector_schema_lib.erl | 6 +++--- apps/emqx_resource/src/emqx_resource_validator.erl | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/emqx_connector/include/emqx_connector.hrl b/apps/emqx_connector/include/emqx_connector.hrl index 143816402..fb299b19b 100644 --- a/apps/emqx_connector/include/emqx_connector.hrl +++ b/apps/emqx_connector/include/emqx_connector.hrl @@ -1,4 +1,4 @@ -define(VALID, emqx_resource_validator). --define(REQUIRED(MSG), ?VALID:required(MSG)). +-define(NOT_EMPTY(MSG), ?VALID:not_empty(MSG)). -define(MAX(MAXV), ?VALID:max(number, MAXV)). -define(MIN(MINV), ?VALID:min(number, MINV)). diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 6fac3cb26..daddb7e13 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -243,11 +243,11 @@ host_port(HostPort) -> end. server(type) -> server(); -server(validator) -> [?REQUIRED("the field 'server' is required")]; +server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; server(_) -> undefined. servers(type) -> hoconsc:array(server()); -servers(validator) -> [?REQUIRED("the field 'servers' is required")]; +servers(validator) -> [?NOT_EMPTY("the value of the field 'servers' cannot be empty")]; servers(_) -> undefined. duration(type) -> emqx_schema:duration_ms(); diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 4f43a3bd4..7dcf24be5 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -87,12 +87,12 @@ relational_db_fields() -> server(type) -> emqx_schema:ip_port(); server(nullable) -> false; -server(validator) -> [?REQUIRED("the field 'server' is required")]; +server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; server(_) -> undefined. database(type) -> binary(); database(nullable) -> false; -database(validator) -> [?REQUIRED("the field 'database' is required")]; +database(validator) -> [?NOT_EMPTY("the value of the field 'database' cannot be empty")]; database(_) -> undefined. pool_size(type) -> integer(); @@ -129,7 +129,7 @@ verify(default) -> false; verify(_) -> undefined. servers(type) -> servers(); -servers(validator) -> [?REQUIRED("the field 'servers' is required")]; +servers(validator) -> [?NOT_EMPTY("the value of the field 'servers' cannot be empty")]; servers(_) -> undefined. to_ip_port(Str) -> diff --git a/apps/emqx_resource/src/emqx_resource_validator.erl b/apps/emqx_resource/src/emqx_resource_validator.erl index 519ad4095..ee8cb6067 100644 --- a/apps/emqx_resource/src/emqx_resource_validator.erl +++ b/apps/emqx_resource/src/emqx_resource_validator.erl @@ -20,7 +20,7 @@ , max/2 , equals/2 , enum/1 - , required/1 + , not_empty/1 ]). max(Type, Max) -> @@ -38,7 +38,7 @@ enum(Items) -> err_limit({enum, {is_member_of, Items}, {got, Value}})) end. -required(ErrMsg) -> +not_empty(ErrMsg) -> fun(<<>>) -> {error, ErrMsg}; (_) -> ok end. From fb78e510caea5009a083fcee9b339be188c15fa3 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 9 Jul 2021 15:04:54 +0800 Subject: [PATCH 125/379] fix(test): update test cases for emqx_access_control_SUITE --- apps/emqx/src/emqx_config.erl | 5 ++++ apps/emqx/src/emqx_map_lib.erl | 2 ++ apps/emqx/test/emqx_SUITE.erl | 1 + apps/emqx/test/emqx_access_control_SUITE.erl | 24 ++++++-------------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 9d5428fdd..18e0d7020 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -27,6 +27,7 @@ -export([ get_listener_conf/3 , get_listener_conf/4 + , put_listener_conf/4 , find_listener_conf/3 ]). @@ -79,6 +80,10 @@ get_listener_conf(Zone, Listener, KeyPath, Default) -> {ok, Data} -> Data end. +-spec put_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> ok. +put_listener_conf(Zone, Listener, KeyPath, Conf) -> + ?MODULE:put([zones, Zone, listeners, Listener | KeyPath], Conf). + -spec find_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> {ok, term()} | {not_foud, emqx_map_lib:config_key_path(), term()}. find_listener_conf(Zone, Listener, KeyPath) -> diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 154a3d24f..c0d922de6 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -58,6 +58,8 @@ deep_find(_KeyPath, Data) -> -spec deep_put(config_key_path(), map(), term()) -> map(). deep_put([], Map, Config) when is_map(Map) -> Config; +deep_put([], _Map, Config) -> %% not map, replace it + Config; deep_put([Key | KeyPath], Map, Config) -> SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Config), Map#{Key => SubMap}. diff --git a/apps/emqx/test/emqx_SUITE.erl b/apps/emqx/test/emqx_SUITE.erl index 4213a5aac..dca66eca9 100644 --- a/apps/emqx/test/emqx_SUITE.erl +++ b/apps/emqx/test/emqx_SUITE.erl @@ -27,6 +27,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), + ct:pal("------------config: ~p", [emqx_config:get()]), Config. end_per_suite(_Config) -> diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index b356402fb..054f839a3 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -33,36 +33,23 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_authenticate(_) -> - emqx_zone:set_env(zone, allow_anonymous, false), + toggle_auth(true), ?assertMatch({error, _}, emqx_access_control:authenticate(clientinfo())), - emqx_zone:set_env(zone, allow_anonymous, true), + toggle_auth(false), ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish, <<"t">>)). -t_bypass_auth_plugins(_) -> - ClientInfo = clientinfo(), - emqx_zone:set_env(bypass_zone, allow_anonymous, true), - emqx_zone:set_env(zone, allow_anonymous, false), - emqx_zone:set_env(bypass_zone, bypass_auth_plugins, true), - emqx:hook('client.authenticate',{?MODULE, auth_fun, []}), - ?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo#{zone => bypass_zone})), - ?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo)). - %%-------------------------------------------------------------------- %% Helper functions %%-------------------------------------------------------------------- -auth_fun(#{zone := bypass_zone}, AuthRes) -> - {stop, AuthRes#{auth_result => password_error}}; -auth_fun(#{zone := _}, AuthRes) -> - {stop, AuthRes#{auth_result => success}}. - clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> - maps:merge(#{zone => zone, + maps:merge(#{zone => default, + listener => mqtt_tcp, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -72,3 +59,6 @@ clientinfo(InitProps) -> peercert => undefined, mountpoint => undefined }, InitProps). + +toggle_auth(Bool) when is_boolean(Bool) -> + emqx_config:put_listener_conf(default, mqtt_tcp, [auth, enable], Bool). From 14af90d0c3ea4c319d920e16f2012af9e988658e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 9 Jul 2021 15:38:51 +0800 Subject: [PATCH 126/379] fix(test): update test cases for emqx_acl_cache_SUITE --- apps/emqx/src/emqx_acl_cache.erl | 7 +-- apps/emqx/src/emqx_channel.erl | 5 +- apps/emqx/test/emqx_acl_cache_SUITE.erl | 71 ++----------------------- 3 files changed, 10 insertions(+), 73 deletions(-) diff --git a/apps/emqx/src/emqx_acl_cache.erl b/apps/emqx/src/emqx_acl_cache.erl index a49f42c83..d4c7cfbdb 100644 --- a/apps/emqx/src/emqx_acl_cache.erl +++ b/apps/emqx/src/emqx_acl_cache.erl @@ -18,7 +18,7 @@ -include("emqx.hrl"). --export([ list_acl_cache/0 +-export([ list_acl_cache/2 , get_acl_cache/4 , put_acl_cache/5 , cleanup_acl_cache/2 @@ -62,8 +62,9 @@ get_cache_max_size(Zone, Listener) -> get_cache_ttl(Zone, Listener) -> emqx_config:get_listener_conf(Zone, Listener, [acl, cache, ttl]). --spec(list_acl_cache() -> [acl_cache_entry()]). -list_acl_cache() -> +-spec(list_acl_cache(atom(), atom()) -> [acl_cache_entry()]). +list_acl_cache(Zone, Listener) -> + cleanup_acl_cache(Zone, Listener), map_acl_cache(fun(Cache) -> Cache end). %% We'll cleanup the cache before replacing an expired acl. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 600e8bed0..9b43b3bf3 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -944,8 +944,9 @@ handle_call({takeover, 'end'}, Channel = #channel{session = Session, AllPendings = lists:append(Delivers, Pendings), disconnect_and_shutdown(takeovered, AllPendings, Channel); -handle_call(list_acl_cache, Channel) -> - {reply, emqx_acl_cache:list_acl_cache(), Channel}; +handle_call(list_acl_cache, #channel{clientinfo = #{zone := Zone, listener := Listener}} + = Channel) -> + {reply, emqx_acl_cache:list_acl_cache(Zone, Listener), Channel}; handle_call({quota, Policy}, Channel) -> Zone = info(zone, Channel), diff --git a/apps/emqx/test/emqx_acl_cache_SUITE.erl b/apps/emqx/test/emqx_acl_cache_SUITE.erl index be7c29055..f631f18cb 100644 --- a/apps/emqx/test/emqx_acl_cache_SUITE.erl +++ b/apps/emqx/test/emqx_acl_cache_SUITE.erl @@ -26,6 +26,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), + toggle_acl(true), Config. end_per_suite(_Config) -> @@ -55,7 +56,6 @@ t_clean_acl_cache(_) -> ?assertEqual(0, length(gen_server:call(ClientPid, list_acl_cache))), emqtt:stop(Client). - t_drain_acl_cache(_) -> {ok, Client} = emqtt:start_link([{clientid, <<"emqx_c">>}]), {ok, _} = emqtt:connect(Client), @@ -79,70 +79,5 @@ t_drain_acl_cache(_) -> ?assert(length(gen_server:call(ClientPid, list_acl_cache)) > 0), emqtt:stop(Client). -% optimize?? -t_reload_aclfile_and_cleanall(_Config) -> - - RasieMsg = fun() -> Self = self(), #{puback => fun(Msg) -> Self ! {puback, Msg} end, - disconnected => fun(_) -> ok end, - publish => fun(_) -> ok end } end, - - {ok, Client} = emqtt:start_link([{clientid, <<"emqx_c">>}, {proto_ver, v5}, - {msg_handler, RasieMsg()}]), - {ok, _} = emqtt:connect(Client), - - {ok, PktId} = emqtt:publish(Client, <<"t1">>, <<"{\"x\":1}">>, qos1), - - %% Success publish to broker - receive - {puback, #{packet_id := PktId, reason_code := Rc}} -> - ?assertEqual(16#10, Rc); - _ -> - ?assert(false) - end, - - %% Check acl cache list - [ClientPid] = emqx_cm:lookup_channels(<<"emqx_c">>), - ?assert(length(gen_server:call(ClientPid, list_acl_cache)) > 0), - emqtt:stop(Client). - -%% @private -testdir(DataPath) -> - Ls = filename:split(DataPath), - filename:join(lists:sublist(Ls, 1, length(Ls) - 1)). - -% t_cache_k(_) -> -% error('TODO'). - -% t_cache_v(_) -> -% error('TODO'). - -% t_cleanup_acl_cache(_) -> -% error('TODO'). - -% t_get_oldest_key(_) -> -% error('TODO'). - -% t_get_newest_key(_) -> -% error('TODO'). - -% t_get_cache_max_size(_) -> -% error('TODO'). - -% t_get_cache_size(_) -> -% error('TODO'). - -% t_dump_acl_cache(_) -> -% error('TODO'). - -% t_empty_acl_cache(_) -> -% error('TODO'). - -% t_put_acl_cache(_) -> -% error('TODO'). - -% t_get_acl_cache(_) -> -% error('TODO'). - -% t_is_enabled(_) -> -% error('TODO'). - +toggle_acl(Bool) when is_boolean(Bool) -> + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], Bool). From c11a8c6db6e9b6292f4ceb152c038aefc80ca465 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 7 Jul 2021 20:33:30 +0800 Subject: [PATCH 127/379] refactor: clients api; status api; adapter minirest v1 The serious influence: authn: api authz: api; api test suit dashboard: all closed lwm2m: api; modules: api(api_topic_metrics, modules_api); test suit(emqx_modules_SUITE) prometheus: api retainer: api; api test suit rule_engine: api: api test suit telemetry: api --- .ci/build_packages/tests.sh | 6 +- .github/workflows/build_packages.yaml | 2 +- .github/workflows/build_slim_packages.yaml | 2 +- apps/emqx_authn/src/emqx_authn_api.erl | 6 +- apps/emqx_authz/src/emqx_authz_api.erl | 12 +- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 4 +- apps/emqx_dashboard/src/emqx_dashboard.erl | 81 +- .../emqx_dashboard/src/emqx_dashboard_api.erl | 5 +- .../test/emqx_dashboard_SUITE.erl | 4 +- apps/emqx_lwm2m/src/emqx_lwm2m_api.erl | 6 +- apps/emqx_management/README.md | 3 + apps/emqx_management/include/emqx_mgmt.hrl | 26 + apps/emqx_management/src/emqx_mgmt.erl | 7 +- .../src/emqx_mgmt_api_clients.erl | 814 ++++++++++-------- .../src/emqx_mgmt_api_status.erl | 47 + apps/emqx_management/src/emqx_mgmt_cli.erl | 2 +- apps/emqx_management/src/emqx_mgmt_http.erl | 127 ++- apps/emqx_management/src/emqx_mgmt_util.erl | 33 + apps/emqx_management/test/emqx_mgmt_SUITE.erl | 340 -------- .../test/emqx_mgmt_api_SUITE.erl | 592 ------------- .../test/etc/emqx_management.conf | 43 - .../test/etc/emqx_reloader.conf | 24 - apps/emqx_management/test/rfc6455_client.erl | 252 ------ apps/emqx_management/test/test_utils.erl | 19 - .../src/emqx_mod_api_topic_metrics.erl | 6 +- apps/emqx_modules/src/emqx_modules_api.erl | 6 +- apps/emqx_modules/test/emqx_modules_SUITE.erl | 107 +-- apps/emqx_prometheus/src/emqx_prometheus.erl | 6 +- apps/emqx_retainer/src/emqx_retainer_api.erl | 12 +- .../test/emqx_retainer_api_SUITE.erl | 4 +- .../src/emqx_rule_engine_api.erl | 5 +- .../test/emqx_rule_engine_SUITE.erl | 17 +- .../emqx_telemetry/src/emqx_telemetry_api.erl | 5 +- deploy/charts/emqx/templates/StatefulSet.yaml | 2 +- rebar.config | 2 +- 35 files changed, 798 insertions(+), 1831 deletions(-) create mode 100644 apps/emqx_management/src/emqx_mgmt_api_status.erl delete mode 100644 apps/emqx_management/test/emqx_mgmt_SUITE.erl delete mode 100644 apps/emqx_management/test/emqx_mgmt_api_SUITE.erl delete mode 100644 apps/emqx_management/test/etc/emqx_management.conf delete mode 100644 apps/emqx_management/test/etc/emqx_reloader.conf delete mode 100644 apps/emqx_management/test/rfc6455_client.erl delete mode 100644 apps/emqx_management/test/test_utils.erl diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 47994c9ad..67fcad5f3 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -48,7 +48,7 @@ emqx_test(){ exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/status >/dev/null 2>&1; do + while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx running error" @@ -123,7 +123,7 @@ running_test(){ exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/status >/dev/null 2>&1; do + while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx running error" @@ -145,7 +145,7 @@ running_test(){ exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/status >/dev/null 2>&1; do + while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx service error" diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 99ea45d29..718266987 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -183,7 +183,7 @@ jobs: ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:8081/status > /dev/null; then + if curl -fs 127.0.0.1:8081/api/v5/status > /dev/null; then ready='yes' break fi diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 30768e023..162959040 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -112,7 +112,7 @@ jobs: ./emqx/bin/emqx start || cat emqx/log/erlang.log.1 ready='no' for i in {1..10}; do - if curl -fs 127.0.0.1:8081/status > /dev/null; then + if curl -fs 127.0.0.1:8081/api/v5/status > /dev/null; then ready='yes' break fi diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index ad9542958..c24b790a3 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -40,8 +40,6 @@ , list_users/2 ]). --import(minirest, [return/1]). - -rest_api(#{name => create_chain, method => 'POST', path => "/authentication/chains", @@ -542,3 +540,7 @@ get_missed_params(Actual, Expected) -> end end, [], Expected), lists:reverse(Keys). + +return(_) -> +%% TODO: V5 API + ok. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 08ff0a7d7..99ec2841c 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -53,21 +53,21 @@ ]). lookup_authz(_Bindings, _Params) -> - minirest:return({ok, emqx_authz:lookup()}). + return({ok, emqx_authz:lookup()}). update_authz(_Bindings, Params) -> Rules = get_rules(Params), - minirest:return(emqx_authz:update(Rules)). + return(emqx_authz:update(Rules)). append_authz(_Bindings, Params) -> Rules = get_rules(Params), NRules = lists:append(emqx_authz:lookup(), Rules), - minirest:return(emqx_authz:update(NRules)). + return(emqx_authz:update(NRules)). push_authz(_Bindings, Params) -> Rules = get_rules(Params), NRules = lists:append(Rules, emqx_authz:lookup()), - minirest:return(emqx_authz:update(NRules)). + return(emqx_authz:update(NRules)). %%------------------------------------------------------------------------------ %% Interval Funcs @@ -88,3 +88,7 @@ get_rules(Params) -> -endif. + +return(_) -> +%% TODO: V5 api + ok. \ No newline at end of file diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index f1b691587..789de9fcc 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -35,7 +35,9 @@ -define(BASE_PATH, "api"). all() -> - emqx_ct:all(?MODULE). +%% TODO: V5 API +%% emqx_ct:all(?MODULE). + []. groups() -> []. diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 0390339d3..8e81b979f 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -19,7 +19,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). --import(proplists, [get_value/3]). +%%-import(proplists, [get_value/3]). -export([ start_listeners/0 , stop_listeners/0 @@ -42,56 +42,61 @@ start_listeners() -> lists:foreach(fun(Listener) -> start_listener(Listener) end, listeners()). %% Start HTTP Listener -start_listener({Proto, Port, Options}) when Proto == http -> - Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, - {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, - {"/api/v4/[...]", minirest, http_handlers()}], - minirest:start_http(listener_name(Proto), ranch_opts(Port, Options), Dispatch); - -start_listener({Proto, Port, Options}) when Proto == https -> - Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, - {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, - {"/api/v4/[...]", minirest, http_handlers()}], - minirest:start_https(listener_name(Proto), ranch_opts(Port, Options), Dispatch). - -ranch_opts(Port, Options0) -> - NumAcceptors = get_value(num_acceptors, Options0, 4), - MaxConnections = get_value(max_connections, Options0, 512), - Options = lists:foldl(fun({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors -> - Acc; - ({inet6, true}, Acc) -> [inet6 | Acc]; - ({inet6, false}, Acc) -> Acc; - ({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc]; - ({ipv6_v6only, false}, Acc) -> Acc; - ({K, V}, Acc)-> - [{K, V} | Acc] - end, [], Options0), - #{num_acceptors => NumAcceptors, - max_connections => MaxConnections, - socket_opts => [{port, Port} | Options]}. +start_listener(_) -> ok. +%% TODO: V5 API +%%start_listener({Proto, Port, Options}) when Proto == http -> +%% Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, +%% {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, +%% {"/api/v4/[...]", minirest, http_handlers()}], +%% minirest:start_http(listener_name(Proto), ranch_opts(Port, Options), Dispatch); +%% +%%start_listener({Proto, Port, Options}) when Proto == https -> +%% Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, +%% {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, +%% {"/api/v4/[...]", minirest, http_handlers()}], +%% minirest:start_https(listener_name(Proto), ranch_opts(Port, Options), Dispatch). +%% +%%ranch_opts(Port, Options0) -> +%% NumAcceptors = get_value(num_acceptors, Options0, 4), +%% MaxConnections = get_value(max_connections, Options0, 512), +%% Options = lists:foldl(fun({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors -> +%% Acc; +%% ({inet6, true}, Acc) -> [inet6 | Acc]; +%% ({inet6, false}, Acc) -> Acc; +%% ({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc]; +%% ({ipv6_v6only, false}, Acc) -> Acc; +%% ({K, V}, Acc)-> +%% [{K, V} | Acc] +%% end, [], Options0), +%% #{num_acceptors => NumAcceptors, +%% max_connections => MaxConnections, +%% socket_opts => [{port, Port} | Options]}. stop_listeners() -> lists:foreach(fun(Listener) -> stop_listener(Listener) end, listeners()). -stop_listener({Proto, _Port, _}) -> - minirest:stop_http(listener_name(Proto)). +stop_listener(_) -> + ok. +%% TODO: V5 API +%%stop_listener({Proto, _Port, _}) -> +%% minirest:stop_http(listener_name(Proto)). listeners() -> application:get_env(?APP, listeners, []). -listener_name(Proto) -> - list_to_atom(atom_to_list(Proto) ++ ":dashboard"). +%%listener_name(Proto) -> +%% list_to_atom(atom_to_list(Proto) ++ ":dashboard"). %%-------------------------------------------------------------------- %% HTTP Handlers and Dispatcher %%-------------------------------------------------------------------- -http_handlers() -> - Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()), - [{"/api/v4/", - minirest:handler(#{apps => Plugins ++ [emqx_modules], - filter => fun ?MODULE:filter/1}), - [{authorization, fun ?MODULE:is_authorized/1}]}]. +%%http_handlers() -> +%% Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()), +%% [{"/api/v4/", +%% minirest:handler(#{apps => Plugins ++ [emqx_modules], +%% filter => fun ?MODULE:filter/1}), +%% [{authorization, fun ?MODULE:is_authorized/1}]}]. %%-------------------------------------------------------------------- %% Basic Authorization diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index e1c89efbb..653380ab6 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -18,8 +18,6 @@ -include("emqx_dashboard.hrl"). --import(minirest, [return/1]). - -rest_api(#{name => auth_user, method => 'POST', path => "/auth", @@ -107,3 +105,6 @@ delete(#{name := Username}, _Params) -> row(#mqtt_admin{username = Username, tags = Tags}) -> #{username => Username, tags => Tags}. +return(_) -> +%% TODO: V5 API + ok. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 360495dbc..1ffb6786e 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -40,7 +40,9 @@ -define(OVERVIEWS, ['alarms/activated', 'alarms/deactivated', banned, brokers, stats, metrics, listeners, clients, subscriptions, routes, plugins]). all() -> - emqx_ct:all(?MODULE). +%% TODO: V5 API +%% emqx_ct:all(?MODULE). + []. init_per_suite(Config) -> emqx_ct_helpers:start_apps([emqx_management, emqx_dashboard],fun set_special_configs/1), diff --git a/apps/emqx_lwm2m/src/emqx_lwm2m_api.erl b/apps/emqx_lwm2m/src/emqx_lwm2m_api.erl index 6018aa7c7..80449238c 100644 --- a/apps/emqx_lwm2m/src/emqx_lwm2m_api.erl +++ b/apps/emqx_lwm2m/src/emqx_lwm2m_api.erl @@ -16,8 +16,6 @@ -module(emqx_lwm2m_api). --import(minirest, [return/1]). - -rest_api(#{name => list, method => 'GET', path => "/lwm2m_channels/", @@ -160,3 +158,7 @@ path_list(Path) -> [ObjId, ObjInsId] -> [ObjId, ObjInsId]; [ObjId] -> [ObjId] end. + +return(_) -> +%% TODO: V5 API + ok. diff --git a/apps/emqx_management/README.md b/apps/emqx_management/README.md index c71a47628..52013c025 100644 --- a/apps/emqx_management/README.md +++ b/apps/emqx_management/README.md @@ -7,3 +7,6 @@ EMQ X Management API http://restful-api-design.readthedocs.io/en/latest/scope.html +default application see: +header: +authorization: Basic YWRtaW46cHVibGlj diff --git a/apps/emqx_management/include/emqx_mgmt.hrl b/apps/emqx_management/include/emqx_mgmt.hrl index b952332c5..40baec4e1 100644 --- a/apps/emqx_management/include/emqx_mgmt.hrl +++ b/apps/emqx_management/include/emqx_mgmt.hrl @@ -35,3 +35,29 @@ -define(VERSIONS, ["4.0", "4.1", "4.2", "4.3"]). -define(MANAGEMENT_SHARD, emqx_management_shard). + +-define(GENERATE_API_METADATA(MetaData), + maps:fold( + fun(Method, MethodDef0, NextMetaData) -> + Default = #{ + tags => [?MODULE], + security => [#{application => []}]}, + MethodDef = + lists:foldl( + fun(Key, NMethodDef) -> + case maps:is_key(Key, NMethodDef) of + true -> + NMethodDef; + false -> + maps:put(Key, maps:get(Key, Default), NMethodDef) + end + end, MethodDef0, maps:keys(Default)), + maps:put(Method, MethodDef, NextMetaData) + end, + #{}, MetaData)). + +-define(GENERATE_API(Path, MetaData, Function), + {Path, ?GENERATE_API_METADATA(MetaData), Function}). + +-define(GENERATE_APIS(Apis), + [?GENERATE_API(Path, MetaData, Function) || {Path, MetaData, Function} <- Apis]). diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 245031354..a3102ab66 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -113,10 +113,11 @@ -define(APP, emqx_management). +%% TODO: remove these function after all api use minirest version 1.X return() -> - minirest:return(). -return(Response) -> - minirest:return(Response). + ok. +return(_Response) -> + ok. %%-------------------------------------------------------------------- %% Node Info diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 523314ebb..1afd906f1 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -16,308 +16,469 @@ -module(emqx_mgmt_api_clients). --include("emqx_mgmt.hrl"). +-behavior(minirest_api). --include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx.hrl"). --define(CLIENT_QS_SCHEMA, {emqx_channel_info, - [{<<"clientid">>, binary}, - {<<"username">>, binary}, - {<<"zone">>, atom}, - {<<"ip_address">>, ip}, - {<<"conn_state">>, atom}, - {<<"clean_start">>, atom}, - {<<"proto_name">>, binary}, - {<<"proto_ver">>, integer}, - {<<"_like_clientid">>, binary}, - {<<"_like_username">>, binary}, - {<<"_gte_created_at">>, timestamp}, - {<<"_lte_created_at">>, timestamp}, - {<<"_gte_connected_at">>, timestamp}, - {<<"_lte_connected_at">>, timestamp}]}). +-include_lib("emqx/include/logger.hrl"). --rest_api(#{name => list_clients, - method => 'GET', - path => "/clients/", - func => list, - descr => "A list of clients on current node"}). +-include("emqx_mgmt.hrl"). --rest_api(#{name => list_node_clients, - method => 'GET', - path => "nodes/:atom:node/clients/", - func => list, - descr => "A list of clients on specified node"}). +%% API +-export([api_spec/0]). --rest_api(#{name => lookup_client, - method => 'GET', - path => "/clients/:bin:clientid", - func => lookup, - descr => "Lookup a client in the cluster"}). - --rest_api(#{name => lookup_node_client, - method => 'GET', - path => "nodes/:atom:node/clients/:bin:clientid", - func => lookup, - descr => "Lookup a client on the node"}). - --rest_api(#{name => lookup_client_via_username, - method => 'GET', - path => "/clients/username/:bin:username", - func => lookup, - descr => "Lookup a client via username in the cluster" - }). - --rest_api(#{name => lookup_node_client_via_username, - method => 'GET', - path => "/nodes/:atom:node/clients/username/:bin:username", - func => lookup, - descr => "Lookup a client via username on the node " - }). - --rest_api(#{name => kickout_client, - method => 'DELETE', - path => "/clients/:bin:clientid", - func => kickout, - descr => "Kick out the client in the cluster"}). - --rest_api(#{name => clean_acl_cache, - method => 'DELETE', - path => "/clients/:bin:clientid/acl_cache", - func => clean_acl_cache, - descr => "Clear the ACL cache of a specified client in the cluster"}). - --rest_api(#{name => list_acl_cache, - method => 'GET', - path => "/clients/:bin:clientid/acl_cache", - func => list_acl_cache, - descr => "List the ACL cache of a specified client in the cluster"}). - --rest_api(#{name => set_ratelimit_policy, - method => 'POST', - path => "/clients/:bin:clientid/ratelimit", - func => set_ratelimit_policy, - descr => "Set the client ratelimit policy"}). - --rest_api(#{name => clean_ratelimit, - method => 'DELETE', - path => "/clients/:bin:clientid/ratelimit", - func => clean_ratelimit, - descr => "Clear the ratelimit policy"}). - --rest_api(#{name => set_quota_policy, - method => 'POST', - path => "/clients/:bin:clientid/quota", - func => set_quota_policy, - descr => "Set the client quota policy"}). - --rest_api(#{name => clean_quota, - method => 'DELETE', - path => "/clients/:bin:clientid/quota", - func => clean_quota, - descr => "Clear the quota policy"}). - --import(emqx_mgmt_util, [ ntoa/1 - , strftime/1 - ]). - --export([ list/2 - , lookup/2 - , kickout/2 - , clean_acl_cache/2 - , list_acl_cache/2 - , set_ratelimit_policy/2 - , set_quota_policy/2 - , clean_ratelimit/2 - , clean_quota/2 - ]). +-export([ clients/2 + , client/2 + , acl_cache/2 + , subscribe/2 + , subscribe_batch/2]). -export([ query/3 - , format_channel_info/1 - ]). + , format_channel_info/1]). + +%% for batch operation +-export([do_subscribe/3]). + +-define(CLIENT_QS_SCHEMA, {emqx_channel_info, + [ {<<"clientid">>, binary} + , {<<"username">>, binary} + , {<<"zone">>, atom} + , {<<"ip_address">>, ip} + , {<<"conn_state">>, atom} + , {<<"clean_start">>, atom} + , {<<"proto_name">>, binary} + , {<<"proto_ver">>, integer} + , {<<"_like_clientid">>, binary} + , {<<"_like_username">>, binary} + , {<<"_gte_created_at">>, timestamp} + , {<<"_lte_created_at">>, timestamp} + , {<<"_gte_connected_at">>, timestamp} + , {<<"_lte_connected_at">>, timestamp}]}). -define(query_fun, {?MODULE, query}). -define(format_fun, {?MODULE, format_channel_info}). -list(Bindings, Params) when map_size(Bindings) == 0 -> - fence(fun() -> - emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun) - end); +-define(CLIENT_ID_NOT_FOUND, + <<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>). -list(#{node := Node}, Params) when Node =:= node() -> - fence(fun() -> - emqx_mgmt_api:node_query(Node, Params, ?CLIENT_QS_SCHEMA, ?query_fun) - end); +api_spec() -> + {apis(), schemas()}. -list(Bindings = #{node := Node}, Params) -> - case rpc:call(Node, ?MODULE, list, [Bindings, Params]) of - {badrpc, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}); - Res -> Res +apis() -> + [ clients_api() + , client_api() + , clients_acl_cache_api() + , subscribe_api()]. + +schemas() -> + ClientDef = #{ + <<"node">> => #{ + type => <<"string">>, + description => <<"Name of the node to which the client is connected">>}, + <<"clientid">> => #{ + type => <<"string">>, + description => <<"Client identifier">>}, + <<"username">> => #{ + type => <<"string">>, + description => <<"User name of client when connecting">>}, + <<"proto_name">> => #{ + type => <<"string">>, + description => <<"Client protocol name">>}, + <<"proto_ver">> => #{ + type => <<"integer">>, + description => <<"Protocol version used by the client">>}, + <<"ip_address">> => #{ + type => <<"string">>, + description => <<"Client's IP address">>}, + <<"is_bridge">> => #{ + type => <<"boolean">>, + description => <<"Indicates whether the client is connectedvia bridge">>}, + <<"connected_at">> => #{ + type => <<"string">>, + description => <<"Client connection time">>}, + <<"disconnected_at">> => #{ + type => <<"string">>, + description => <<"Client offline time, This field is only valid and returned when connected is false">>}, + <<"connected">> => #{ + type => <<"boolean">>, + description => <<"Whether the client is connected">>}, + <<"will_msg">> => #{ + type => <<"string">>, + description => <<"Client will message">>}, + <<"zone">> => #{ + type => <<"string">>, + description => <<"Indicate the configuration group used by the client">>}, + <<"keepalive">> => #{ + type => <<"integer">>, + description => <<"keepalive time, with the unit of second">>}, + <<"clean_start">> => #{ + type => <<"boolean">>, + description => <<"Indicate whether the client is using a brand new session">>}, + <<"expiry_interval">> => #{ + type => <<"integer">>, + description => <<"Session expiration interval, with the unit of second">>}, + <<"created_at">> => #{ + type => <<"string">>, + description => <<"Session creation time">>}, + <<"subscriptions_cnt">> => #{ + type => <<"integer">>, + description => <<"Number of subscriptions established by this client.">>}, + <<"subscriptions_max">> => #{ + type => <<"integer">>, + description => <<"v4 api name [max_subscriptions] Maximum number of subscriptions allowed by this client">>}, + <<"inflight_cnt">> => #{ + type => <<"integer">>, + description => <<"Current length of inflight">>}, + <<"inflight_max">> => #{ + type => <<"integer">>, + description => <<"v4 api name [max_inflight]. Maximum length of inflight">>}, + <<"mqueue_len">> => #{ + type => <<"integer">>, + description => <<"Current length of message queue">>}, + <<"mqueue_max">> => #{ + type => <<"integer">>, + description => <<"v4 api name [max_mqueue]. Maximum length of message queue">>}, + <<"mqueue_dropped">> => #{ + type => <<"integer">>, + description => <<"Number of messages dropped by the message queue due to exceeding the length">>}, + <<"awaiting_rel_cnt">> => #{ + type => <<"integer">>, + description => <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>}, + <<"awaiting_rel_max">> => #{ + type => <<"integer">>, + description => <<"v4 api name [max_awaiting_rel]. Maximum allowed number of awaiting PUBREC packet">>}, + <<"recv_oct">> => #{ + type => <<"integer">>, + description => <<"Number of bytes received by EMQ X Broker (the same below)">>}, + <<"recv_cnt">> => #{ + type => <<"integer">>, + description => <<"Number of TCP packets received">>}, + <<"recv_pkt">> => #{ + type => <<"integer">>, + description => <<"Number of MQTT packets received">>}, + <<"recv_msg">> => #{ + type => <<"integer">>, + description => <<"Number of PUBLISH packets received">>}, + <<"send_oct">> => #{ + type => <<"integer">>, + description => <<"Number of bytes sent">>}, + <<"send_cnt">> => #{ + type => <<"integer">>, + description => <<"Number of TCP packets sent">>}, + <<"send_pkt">> => #{ + type => <<"integer">>, + description => <<"Number of MQTT packets sent">>}, + <<"send_msg">> => #{ + type => <<"integer">>, + description => <<"Number of PUBLISH packets sent">>}, + <<"mailbox_len">> => #{ + type => <<"integer">>, + description => <<"Process mailbox size">>}, + <<"heap_size">> => #{ + type => <<"integer">>, + description => <<"Process heap size with the unit of byte">> + }, + <<"reductions">> => #{ + type => <<"integer">>, + description => <<"Erlang reduction">>}}, + ACLCacheDefinitionProperties = #{ + <<"topic">> => #{ + type => <<"string">>, + description => <<"Topic name">>}, + <<"access">> => #{ + type => <<"string">>, + enum => [<<"subscribe">>, <<"publish">>], + description => <<"Access type">>}, + <<"result">> => #{ + type => <<"string">>, + enum => [<<"allow">>, <<"deny">>], + default => <<"allow">>, + description => <<"Allow or deny">>}, + <<"updated_time">> => #{ + type => <<"integer">>, + description => <<"Update time">>}}, + [{<<"client">>, ClientDef}, {<<"acl_cache">>, ACLCacheDefinitionProperties}]. + +clients_api() -> + Metadata = #{ + get => #{ + description => "List clients", + responses => #{ + <<"200">> => #{ + description => <<"List clients 200 OK">>, + schema => #{ + type => array, + items => minirest:ref(<<"client">>)}}}}}, + {"/clients", Metadata, clients}. + +client_api() -> + Metadata = #{ + get => #{ + description => "Get clients info by client ID", + parameters => [#{ + name => clientid, + in => path, + type => string, + required => true, + default => 123456}], + responses => #{ + <<"404">> => emqx_mgmt_util:not_found_schema(<<"Client id not found">>), + <<"200">> => #{ + description => <<"Get clients 200 OK">>, + schema => minirest:ref(<<"client">>)}}}, + delete => #{ + description => "Kick out client by client ID", + parameters => [#{ + name => clientid, + in => path, + type => string, + required => true, + default => 123456}], + responses => #{ + <<"404">> => emqx_mgmt_util:not_found_schema(<<"Client id not found">>), + <<"200">> => #{description => <<"Kick out clients OK">>}}}}, + {"/clients/:clientid", Metadata, client}. + +clients_acl_cache_api() -> + Metadata = #{ + get => #{ + description => "Get client acl cache", + parameters => [#{ + name => clientid, + in => path, + type => string, + required => true, + default => 123456}], + responses => #{ + <<"404">> => emqx_mgmt_util:not_found_schema(<<"Client id not found">>), + <<"200">> => #{ + description => <<"List 200 OK">>, + schema => minirest:ref(<<"acl_cache">>)}}}, + delete => #{ + description => "Clean client acl cache", + parameters => [#{ + name => clientid, + in => path, + type => string, + required => true, + default => 123456}], + responses => #{ + <<"404">> => emqx_mgmt_util:not_found_schema(<<"client id not found">>), + <<"200">> => #{ + description => <<"Clean acl cache 200 OK">>}}}}, + {"/clients/:clientid/acl_cache", Metadata, acl_cache}. + +subscribe_api() -> + Path = "/clients/:clientid/subscribe", + Metadata = #{ + post => #{ + description => "subscribe", + parameters => [ + #{ + name => clientid, + in => path, + type => string, + required => true, + default => 123456 + }, + #{ + name => topics, + in => body, + schema => #{ + type => object, + properties => #{ + <<"topic">> => #{ + type => <<"string">>, + example => <<"topic_1">>, + description => <<"Topic">>}, + <<"qos">> => #{ + type => <<"integer">>, + enum => [0, 1, 2], + example => 0, + description => <<"QOS">>}}} + } + ], + responses => #{ + <<"404">> => emqx_mgmt_util:not_found_schema(<<"Client id not found">>), + <<"200">> => #{description => <<"publish ok">>}}}}, + {Path, Metadata, subscribe}. + +%%%============================================================================================== +%% parameters trans +clients(get, _Request) -> + list(#{}). + +client(get, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + lookup(#{clientid => ClientID}); + +client(delete, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + kickout(#{clientid => ClientID}). + +acl_cache(get, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + get_acl_cache(#{clientid => ClientID}); + +acl_cache(delete, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + clean_acl_cache(#{clientid => ClientID}). + +subscribe(post, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + TopicInfo = emqx_json:decode(Body, [return_maps]), + Topic = maps:get(<<"topic">>, TopicInfo), + Qos = maps:get(<<"qos">>, TopicInfo, 0), + subscribe(#{clientid => ClientID, topic => Topic, qos => Qos}). + +%% TODO: batch +subscribe_batch(post, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + TopicInfos = emqx_json:decode(Body, [return_maps]), + Topics = + [begin + Topic = maps:get(<<"topic">>, TopicInfo), + Qos = maps:get(<<"qos">>, TopicInfo, 0), + #{topic => Topic, qos => Qos} + end || TopicInfo <- TopicInfos], + subscribe_batch(#{clientid => ClientID, topics => Topics}). + +%%%============================================================================================== +%% api apply + +list(Params) -> + Data = emqx_mgmt_api:cluster_query(maps:to_list(Params), ?CLIENT_QS_SCHEMA, ?query_fun), + Body = emqx_json:encode(Data), + {200, Body}. + +lookup(#{clientid := ClientID}) -> + case emqx_mgmt:lookup_client({clientid, ClientID}, ?format_fun) of + [] -> + {404, ?CLIENT_ID_NOT_FOUND}; + ClientInfo -> + Response = emqx_json:encode(hd(ClientInfo)), + {ok, Response} end. -%% @private -fence(Func) -> - try - emqx_mgmt:return({ok, Func()}) - catch - throw : {bad_value_type, {_Key, Type, Value}} -> - Reason = iolist_to_binary( - io_lib:format("Can't convert ~p to ~p type", - [Value, Type]) - ), - emqx_mgmt:return({error, ?ERROR8, Reason}) +kickout(#{clientid := ClientID}) -> + emqx_mgmt:kickout_client(ClientID), + {200}. + +get_acl_cache(#{clientid := ClientID})-> + case emqx_mgmt:list_acl_cache(ClientID) of + {error, not_found} -> + {404, ?CLIENT_ID_NOT_FOUND}; + {error, Reason} -> + {500, #{code => <<"UNKNOW_ERROR">>, reason => io_lib:format("~p", [Reason])}}; + Caches -> + Response = emqx_json:encode([format_acl_cache(Cache) || Cache <- Caches]), + {200, Response} end. -lookup(#{node := Node, clientid := ClientId}, _Params) -> - emqx_mgmt:return({ok, emqx_mgmt:lookup_client(Node, {clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); - -lookup(#{clientid := ClientId}, _Params) -> - emqx_mgmt:return({ok, emqx_mgmt:lookup_client({clientid, emqx_mgmt_util:urldecode(ClientId)}, ?format_fun)}); - -lookup(#{node := Node, username := Username}, _Params) -> - emqx_mgmt:return({ok, emqx_mgmt:lookup_client(Node, {username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}); - -lookup(#{username := Username}, _Params) -> - emqx_mgmt:return({ok, emqx_mgmt:lookup_client({username, emqx_mgmt_util:urldecode(Username)}, ?format_fun)}). - -kickout(#{clientid := ClientId}, _Params) -> - case emqx_mgmt:kickout_client(emqx_mgmt_util:urldecode(ClientId)) of - ok -> emqx_mgmt:return(); - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) +clean_acl_cache(#{clientid := ClientID}) -> + case emqx_mgmt:clean_acl_cache(ClientID) of + ok -> + {200}; + {error, not_found} -> + {404, ?CLIENT_ID_NOT_FOUND}; + {error, Reason} -> + {500, #{code => <<"UNKNOW_ERROR">>, reason => io_lib:format("~p", [Reason])}} end. -clean_acl_cache(#{clientid := ClientId}, _Params) -> - case emqx_mgmt:clean_acl_cache(emqx_mgmt_util:urldecode(ClientId)) of - ok -> emqx_mgmt:return(); - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) +subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) -> + case do_subscribe(ClientID, Topic, Qos) of + {error, channel_not_found} -> + {404, ?CLIENT_ID_NOT_FOUND}; + {error, Reason} -> + Body = emqx_json:encode(#{code => <<"UNKNOW_ERROR">>, reason => io_lib:format("~p", [Reason])}), + {200, Body}; + ok -> + {200} end. -list_acl_cache(#{clientid := ClientId}, _Params) -> - case emqx_mgmt:list_acl_cache(emqx_mgmt_util:urldecode(ClientId)) of - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}); - Caches -> emqx_mgmt:return({ok, [format_acl_cache(Cache) || Cache <- Caches]}) - end. +subscribe_batch(#{clientid := ClientID, topics := Topics}) -> + ArgList = [[ClientID, Topic, Qos]|| #{topic := Topic, qos := Qos} <- Topics], + emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList). -set_ratelimit_policy(#{clientid := ClientId}, Params) -> - P = [{conn_bytes_in, proplists:get_value(<<"conn_bytes_in">>, Params)}, - {conn_messages_in, proplists:get_value(<<"conn_messages_in">>, Params)}], - case [{K, parse_ratelimit_str(V)} || {K, V} <- P, V =/= undefined] of - [] -> emqx_mgmt:return(); - Policy -> - case emqx_mgmt:set_ratelimit_policy(emqx_mgmt_util:urldecode(ClientId), Policy) of - ok -> emqx_mgmt:return(); - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) - end - end. +%%%============================================================================================== +%% internal function +format_channel_info({_, ClientInfo, ClientStats}) -> + Fun = + fun + (_Key, Value, Current) when is_map(Value) -> + maps:merge(Current, Value); + (Key, Value, Current) -> + maps:put(Key, Value, Current) + end, + StatsMap = maps:without([memory, next_pkt_id, total_heap_size], + maps:from_list(ClientStats)), + ClientInfoMap0 = maps:fold(Fun, #{}, ClientInfo), + IpAddress = peer_to_binary(maps:get(peername, ClientInfoMap0)), + Connected = maps:get(conn_state, ClientInfoMap0) =:= connected, + ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0), + ClientInfoMap2 = maps:put(node, node(), ClientInfoMap1), + ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2), + ClientInfoMap = maps:put(connected, Connected, ClientInfoMap3), + RemoveList = [ + auth_result + , peername + , sockname + , peerhost + , conn_state + , send_pend + , conn_props + , peercert + , sockstate + , receive_maximum + , protocol + , is_superuser + , sockport + , anonymous + , mountpoint + , socktype + , active_n + , await_rel_timeout + , conn_mod + , sockname + , retry_interval + , upgrade_qos + ], + maps:without(RemoveList, ClientInfoMap). -clean_ratelimit(#{clientid := ClientId}, _Params) -> - case emqx_mgmt:set_ratelimit_policy(emqx_mgmt_util:urldecode(ClientId), []) of - ok -> emqx_mgmt:return(); - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) - end. - -set_quota_policy(#{clientid := ClientId}, Params) -> - P = [{conn_messages_routing, proplists:get_value(<<"conn_messages_routing">>, Params)}], - case [{K, parse_ratelimit_str(V)} || {K, V} <- P, V =/= undefined] of - [] -> emqx_mgmt:return(); - Policy -> - case emqx_mgmt:set_quota_policy(emqx_mgmt_util:urldecode(ClientId), Policy) of - ok -> emqx_mgmt:return(); - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) - end - end. - -clean_quota(#{clientid := ClientId}, _Params) -> - case emqx_mgmt:set_quota_policy(emqx_mgmt_util:urldecode(ClientId), []) of - ok -> emqx_mgmt:return(); - {error, not_found} -> emqx_mgmt:return({error, ?ERROR12, not_found}); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) - end. - -%% @private -%% S = 100,1s -%% | 100KB, 1m -parse_ratelimit_str(S) when is_binary(S) -> - parse_ratelimit_str(binary_to_list(S)); -parse_ratelimit_str(S) -> - [L, D] = string:tokens(S, ", "), - Limit = case cuttlefish_bytesize:parse(L) of - Sz when is_integer(Sz) -> Sz; - {error, Reason1} -> error(Reason1) - end, - Duration = case cuttlefish_duration:parse(D, s) of - Secs when is_integer(Secs) -> Secs; - {error, Reason} -> error(Reason) - end, - {Limit, Duration}. - -%%-------------------------------------------------------------------- -%% Format - -format_channel_info({_Key, Info, Stats0}) -> - Stats = maps:from_list(Stats0), - ClientInfo = maps:get(clientinfo, Info, #{}), - ConnInfo = maps:get(conninfo, Info, #{}), - Session = case maps:get(session, Info, #{}) of - undefined -> #{}; - _Sess -> _Sess - end, - SessCreated = maps:get(created_at, Session, maps:get(connected_at, ConnInfo)), - Connected = case maps:get(conn_state, Info, connected) of - connected -> true; - _ -> false - end, - NStats = Stats#{max_subscriptions => maps:get(subscriptions_max, Stats, 0), - max_inflight => maps:get(inflight_max, Stats, 0), - max_awaiting_rel => maps:get(awaiting_rel_max, Stats, 0), - max_mqueue => maps:get(mqueue_max, Stats, 0), - inflight => maps:get(inflight_cnt, Stats, 0), - awaiting_rel => maps:get(awaiting_rel_cnt, Stats, 0)}, - format( - lists:foldl(fun(Items, Acc) -> - maps:merge(Items, Acc) - end, #{connected => Connected}, - [maps:with([ subscriptions_cnt, max_subscriptions, - inflight, max_inflight, awaiting_rel, - max_awaiting_rel, mqueue_len, mqueue_dropped, - max_mqueue, heap_size, reductions, mailbox_len, - recv_cnt, recv_msg, recv_oct, recv_pkt, send_cnt, - send_msg, send_oct, send_pkt], NStats), - maps:with([clientid, username, mountpoint, is_bridge, zone], ClientInfo), - maps:with([clean_start, keepalive, expiry_interval, proto_name, - proto_ver, peername, connected_at, disconnected_at], ConnInfo), - #{created_at => SessCreated}])). - -format(Data) when is_map(Data)-> - {IpAddr, Port} = maps:get(peername, Data), - ConnectedAt = maps:get(connected_at, Data), - CreatedAt = maps:get(created_at, Data), - Data1 = maps:without([peername], Data), - maps:merge(Data1#{node => node(), - ip_address => iolist_to_binary(ntoa(IpAddr)), - port => Port, - connected_at => iolist_to_binary(strftime(ConnectedAt div 1000)), - created_at => iolist_to_binary(strftime(CreatedAt div 1000))}, - case maps:get(disconnected_at, Data, undefined) of - undefined -> #{}; - DisconnectedAt -> #{disconnected_at => iolist_to_binary(strftime(DisconnectedAt div 1000))} - end). +peer_to_binary({Addr, Port}) -> + AddrBinary = list_to_binary(inet:ntoa(Addr)), + PortBinary = integer_to_binary(Port), + <>; +peer_to_binary(Addr) -> + list_to_binary(inet:ntoa(Addr)). format_acl_cache({{PubSub, Topic}, {AclResult, Timestamp}}) -> - #{access => PubSub, - topic => Topic, - result => AclResult, - updated_time => Timestamp}. + #{ + access => PubSub, + topic => Topic, + result => AclResult, + updated_time => Timestamp + }. -%%-------------------------------------------------------------------- +do_subscribe(ClientID, Topic0, Qos) -> + {Topic, Opts} = emqx_topic:parse(Topic0), + TopicTable = [{Topic, Opts#{qos => Qos}}], + emqx_mgmt:subscribe(ClientID, TopicTable), + case emqx_mgmt:subscribe(ClientID, TopicTable) of + {error, Reason} -> + {error, Reason}; + {subscribe, Subscriptions} -> + case proplists:is_defined(Topic, Subscriptions) of + true -> + ok; + false -> + {error, unknow_error} + end + end. +%%%============================================================================================== %% Query Functions -%%-------------------------------------------------------------------- query({Qs, []}, Start, Limit) -> Ms = qs2ms(Qs), @@ -328,37 +489,8 @@ query({Qs, Fuzzy}, Start, Limit) -> MatchFun = match_fun(Ms, Fuzzy), emqx_mgmt_api:traverse_table(emqx_channel_info, MatchFun, Start, Limit, fun format_channel_info/1). -%%-------------------------------------------------------------------- -%% Match funcs - -match_fun(Ms, Fuzzy) -> - MsC = ets:match_spec_compile(Ms), - REFuzzy = lists:map(fun({K, like, S}) -> - {ok, RE} = re:compile(S), - {K, like, RE} - end, Fuzzy), - fun(Rows) -> - case ets:match_spec_run(Rows, MsC) of - [] -> []; - Ls -> - lists:filter(fun(E) -> - run_fuzzy_match(E, REFuzzy) - end, Ls) - end - end. - -run_fuzzy_match(_, []) -> - true; -run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) -> - Val = case maps:get(Key, ClientInfo, "") of - undefined -> ""; - V -> V - end, - re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy). - -%%-------------------------------------------------------------------- +%%%============================================================================================== %% QueryString to Match Spec - -spec qs2ms(list()) -> ets:match_spec(). qs2ms(Qs) -> {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), @@ -380,7 +512,7 @@ put_conds({_, Op, V}, Holder, Conds) -> [{Op, Holder, V} | Conds]; put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) -> [{Op2, Holder, V2}, - {Op1, Holder, V1} | Conds]. + {Op1, Holder, V1} | Conds]. ms(clientid, X) -> #{clientinfo => #{clientid => X}}; @@ -403,51 +535,29 @@ ms(connected_at, X) -> ms(created_at, X) -> #{session => #{created_at => X}}. -%%-------------------------------------------------------------------- -%% EUnits -%%-------------------------------------------------------------------- +%%%============================================================================================== +%% Match funcs +match_fun(Ms, Fuzzy) -> + MsC = ets:match_spec_compile(Ms), + REFuzzy = lists:map(fun({K, like, S}) -> + {ok, RE} = re:compile(S), + {K, like, RE} + end, Fuzzy), + fun(Rows) -> + case ets:match_spec_run(Rows, MsC) of + [] -> []; + Ls -> + lists:filter(fun(E) -> + run_fuzzy_match(E, REFuzzy) + end, Ls) + end + end. --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - -params2qs_test() -> - QsSchema = element(2, ?CLIENT_QS_SCHEMA), - Params = [{<<"clientid">>, <<"abc">>}, - {<<"username">>, <<"def">>}, - {<<"zone">>, <<"external">>}, - {<<"ip_address">>, <<"127.0.0.1">>}, - {<<"conn_state">>, <<"connected">>}, - {<<"clean_start">>, true}, - {<<"proto_name">>, <<"MQTT">>}, - {<<"proto_ver">>, 4}, - {<<"_gte_created_at">>, 1}, - {<<"_lte_created_at">>, 5}, - {<<"_gte_connected_at">>, 1}, - {<<"_lte_connected_at">>, 5}, - {<<"_like_clientid">>, <<"a">>}, - {<<"_like_username">>, <<"e">>} - ], - ExpectedMtchHead = - #{clientinfo => #{clientid => <<"abc">>, - username => <<"def">>, - zone => external, - peerhost => {127,0,0,1} - }, - conn_state => connected, - conninfo => #{clean_start => true, - proto_name => <<"MQTT">>, - proto_ver => 4, - connected_at => '$3'}, - session => #{created_at => '$2'}}, - ExpectedCondi = [{'>=','$2', 1}, - {'=<','$2', 5}, - {'>=','$3', 1}, - {'=<','$3', 5}], - {10, {Qs1, []}} = emqx_mgmt_api:params2qs(Params, QsSchema), - [{{'$1', MtchHead, _}, Condi, _}] = qs2ms(Qs1), - ?assertEqual(ExpectedMtchHead, MtchHead), - ?assertEqual(ExpectedCondi, Condi), - - [{{'$1', #{}, '_'}, [], ['$_']}] = qs2ms([]). - --endif. +run_fuzzy_match(_, []) -> + true; +run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) -> + Val = case maps:get(Key, ClientInfo, "") of + undefined -> ""; + V -> V + end, + re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy). diff --git a/apps/emqx_management/src/emqx_mgmt_api_status.erl b/apps/emqx_management/src/emqx_mgmt_api_status.erl new file mode 100644 index 000000000..f7f013f20 --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_api_status.erl @@ -0,0 +1,47 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_api_status). +%% API +-behavior(minirest_api). + +-export([api_spec/0]). + +-export([running_status/2]). + +api_spec() -> + {[status_api()], []}. + +status_api() -> + Path = "/status", + Metadata = #{ + get => #{ + security => [], + responses => #{ + <<"200">> => #{description => <<"running">>}}}}, + {Path, Metadata, running_status}. + +running_status(get, _Request) -> + {InternalStatus, _ProvidedStatus} = init:get_status(), + AppStatus = + case lists:keysearch(emqx, 1, application:which_applications()) of + false -> not_running; + {value, _Val} -> running + end, + Status = io_lib:format("Node ~s is ~s~nemqx is ~s", [node(), InternalStatus, AppStatus]), + Body = list_to_binary(Status), + {200, #{<<"content-type">> => <<"text/plain">>}, Body}. + + diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index e70def3ee..9c5f02b62 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -485,7 +485,7 @@ listeners([]) -> listeners(["stop", Name = "http" ++ _N | _MaybePort]) -> %% _MaybePort is to be backward compatible, to stop http listener, there is no need for the port number - case minirest:stop_http(list_to_atom(Name)) of + case minirest:stop(list_to_atom(Name)) of ok -> emqx_ctl:print("Stop ~s listener successfully.~n", [Name]); {error, Error} -> diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index 9890e3935..178e4b04d 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -13,27 +13,21 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- - -module(emqx_mgmt_http). -export([ start_listeners/0 - , handle_request/2 , stop_listeners/0 , start_listener/1 - , stop_listener/1 - ]). + , stop_listener/1]). --export([init/2]). +%% Authorization +-export([authorize_appid/1]). -include_lib("emqx/include/emqx.hrl"). -define(APP, emqx_management). --define(EXCEPT_PLUGIN, [emqx_dashboard]). --ifdef(TEST). --define(EXCEPT, []). --else. --define(EXCEPT, [add_app, del_app, list_apps, lookup_app, update_app]). --endif. + +-define(BASE_PATH, "/api/v5"). %%-------------------------------------------------------------------- %% Start/Stop Listeners @@ -45,37 +39,54 @@ start_listeners() -> stop_listeners() -> lists:foreach(fun stop_listener/1, listeners()). -start_listener({Proto, Port, Options}) when Proto == http -> - Dispatch = [{"/status", emqx_mgmt_http, []}, - {"/api/v4/[...]", minirest, http_handlers()}], - minirest:start_http(listener_name(Proto), ranch_opts(Port, Options), Dispatch); +start_listener({Proto, Port, Options}) -> + {ok, _} = application:ensure_all_started(minirest), + Authorization = {?MODULE, authorize_appid}, + RanchOptions = ranch_opts(Port, Options), + GlobalSpec = #{ + swagger => "2.0", + info => #{title => "EMQ X API", version => "5.0.0"}, + basePath => ?BASE_PATH, + securityDefinitions => #{ + application => #{ + type => apiKey, + name => "authorization", + in => header}}}, + Minirest = #{ + protocol => Proto, + base_path => ?BASE_PATH, + apps => apps(), + authorization => Authorization, + security => [#{application => []}], + swagger_global_spec => GlobalSpec}, + MinirestOptions = maps:merge(Minirest, RanchOptions), + minirest:start(listener_name(Proto), MinirestOptions). -start_listener({Proto, Port, Options}) when Proto == https -> - Dispatch = [{"/status", emqx_mgmt_http, []}, - {"/api/v4/[...]", minirest, http_handlers()}], - minirest:start_https(listener_name(Proto), ranch_opts(Port, Options), Dispatch). +apps() -> + Apps = [App || {App, _, _} <- application:loaded_applications(), + case re:run(atom_to_list(App), "^emqx") of + {match,[{0,4}]} -> true; + _ -> false + end], + Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()), + Apps ++ Plugins. ranch_opts(Port, Options0) -> - NumAcceptors = proplists:get_value(num_acceptors, Options0, 4), - MaxConnections = proplists:get_value(max_connections, Options0, 512), - Options = lists:foldl(fun({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors -> - Acc; - ({inet6, true}, Acc) -> [inet6 | Acc]; - ({inet6, false}, Acc) -> Acc; - ({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc]; - ({ipv6_v6only, false}, Acc) -> Acc; - ({K, V}, Acc)-> - [{K, V} | Acc] - end, [], Options0), - - Res = #{num_acceptors => NumAcceptors, - max_connections => MaxConnections, - socket_opts => [{port, Port} | Options]}, - Res. + Options = lists:foldl( + fun + ({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors -> Acc; + ({inet6, true}, Acc) -> [inet6 | Acc]; + ({inet6, false}, Acc) -> Acc; + ({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc]; + ({ipv6_v6only, false}, Acc) -> Acc; + ({K, V}, Acc)-> + [{K, V} | Acc] + end, [], Options0), + maps:from_list([{port, Port} | Options]). stop_listener({Proto, Port, _}) -> io:format("Stop http:management listener on ~s successfully.~n",[format(Port)]), - minirest:stop_http(listener_name(Proto)). + minirest:stop(listener_name(Proto)). listeners() -> [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} @@ -85,45 +96,15 @@ listeners() -> listener_name(Proto) -> list_to_atom(atom_to_list(Proto) ++ ":management"). -http_handlers() -> - Apps = [ App || {App, _, _} <- application:loaded_applications(), - case re:run(atom_to_list(App), "^emqx") of - {match,[{0,4}]} -> true; - _ -> false - end], - Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()), - [{"/api/v4", minirest:handler(#{apps => Plugins ++ Apps -- ?EXCEPT_PLUGIN, - except => ?EXCEPT, - filter => fun(_) -> true end}), - [{authorization, fun authorize_appid/1}]}]. - -%%-------------------------------------------------------------------- -%% Handle 'status' request -%%-------------------------------------------------------------------- -init(Req, Opts) -> - Req1 = handle_request(cowboy_req:path(Req), Req), - {ok, Req1, Opts}. - -handle_request(Path, Req) -> - handle_request(cowboy_req:method(Req), Path, Req). - -handle_request(<<"GET">>, <<"/status">>, Req) -> - {InternalStatus, _ProvidedStatus} = init:get_status(), - AppStatus = case lists:keysearch(emqx, 1, application:which_applications()) of - false -> not_running; - {value, _Val} -> running - end, - Status = io_lib:format("Node ~s is ~s~nemqx is ~s", - [node(), InternalStatus, AppStatus]), - cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, Status, Req); - -handle_request(_Method, _Path, Req) -> - cowboy_req:reply(400, #{<<"content-type">> => <<"text/plain">>}, <<"Not found.">>, Req). - authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of - {basic, AppId, AppSecret} -> emqx_mgmt_auth:is_authorized(AppId, AppSecret); - _ -> false + {basic, AppId, AppSecret} -> + case emqx_mgmt_auth:is_authorized(AppId, AppSecret) of + true -> ok; + false -> {401} + end; + _ -> + {401} end. format(Port) when is_integer(Port) -> diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 132bbc83f..8cbe8bdf4 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -21,6 +21,8 @@ , kmg/1 , ntoa/1 , merge_maps/2 + , not_found_schema/1 + , batch_operation/3 ]). -export([urldecode/1]). @@ -77,3 +79,34 @@ merge_maps(Default, New) -> urldecode(S) -> emqx_http_lib:uri_decode(S). +not_found_schema(Description) -> + not_found_schema(Description, ["RESOURCE_NOT_FOUND"]). + +not_found_schema(Description, Enum) -> + #{ + description => Description, + schema => #{ + type => object, + properties => #{ + code => #{ + type => string, + enum => Enum}, + reason => #{ + type => string}}} + }. +batch_operation(Module, Function, ArgsList) -> + Failed = batch_operation(Module, Function, ArgsList, []), + Len = erlang:length(Failed), + Success = erlang:length(ArgsList) - Len, + Fun = fun({Args, Reason}, Detail) -> [#{data => Args, reason => io_lib:format("~p", [Reason])} | Detail] end, + #{success => Success, failed => Len, detail => lists:foldl(Fun, [], Failed)}. + +batch_operation(_Module, _Function, [], Failed) -> + lists:reverse(Failed); +batch_operation(Module, Function, [Args | ArgsList], Failed) -> + case erlang:apply(Module, Function, Args) of + ok -> + batch_operation(Module, Function, ArgsList, Failed); + {error ,Reason} -> + batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed]) + end. diff --git a/apps/emqx_management/test/emqx_mgmt_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_SUITE.erl deleted file mode 100644 index 1108dc37f..000000000 --- a/apps/emqx_management/test/emqx_mgmt_SUITE.erl +++ /dev/null @@ -1,340 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_mgmt_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). - --define(LOG_LEVELS, ["debug", "error", "info"]). --define(LOG_HANDLER_ID, [file, default]). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - ekka_mnesia:start(), - emqx_mgmt_auth:mnesia(boot), - emqx_ct_helpers:start_apps([emqx_retainer, emqx_management], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_management, emqx_retainer]). - -set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}]}), - ok; -set_special_configs(_App) -> - ok. - -t_app(_Config) -> - {ok, AppSecret} = emqx_mgmt_auth:add_app(<<"app_id">>, <<"app_name">>), - ?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret)), - ?assertEqual(AppSecret, emqx_mgmt_auth:get_appsecret(<<"app_id">>)), - ?assertEqual({<<"app_id">>, AppSecret, - <<"app_name">>, <<"Application user">>, - true, undefined}, - lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())), - emqx_mgmt_auth:del_app(<<"app_id">>), - application:set_env(emqx_management, application, []), - %% Specify the application secret - {ok, AppSecret2} = emqx_mgmt_auth:add_app( - <<"app_id">>, <<"app_name">>, <<"secret">>, - <<"app_desc">>, true, undefined), - ?assert(emqx_mgmt_auth:is_authorized(<<"app_id">>, AppSecret2)), - ?assertEqual(AppSecret2, emqx_mgmt_auth:get_appsecret(<<"app_id">>)), - ?assertEqual({<<"app_id">>, AppSecret2, <<"app_name">>, <<"app_desc">>, true, undefined}, - lists:keyfind(<<"app_id">>, 1, emqx_mgmt_auth:list_apps())), - emqx_mgmt_auth:del_app(<<"app_id">>), - ok. - -t_log_cmd(_) -> - mock_print(), - lists:foreach(fun(Level) -> - emqx_mgmt_cli:log(["primary-level", Level]), - ?assertEqual(Level ++ "\n", emqx_mgmt_cli:log(["primary-level"])) - end, ?LOG_LEVELS), - lists:foreach(fun(Level) -> - emqx_mgmt_cli:log(["set-level", Level]), - ?assertEqual(Level ++ "\n", emqx_mgmt_cli:log(["primary-level"])) - end, ?LOG_LEVELS), - [lists:foreach(fun(Level) -> - ?assertEqual(Level ++ "\n", emqx_mgmt_cli:log(["handlers", "set-level", - atom_to_list(Id), Level])) - end, ?LOG_LEVELS) - || #{id := Id} <- emqx_logger:get_log_handlers()], - meck:unload(). - -t_mgmt_cmd(_) -> - % ct:pal("start testing the mgmt command"), - mock_print(), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt( - ["lookup", "emqx_appid"]), "Not Found.")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt( - ["insert", "emqx_appid", "emqx_name"]), "AppSecret:")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt( - ["insert", "emqx_appid", "emqx_name"]), "Error:")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt( - ["lookup", "emqx_appid"]), "app_id:")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt( - ["update", "emqx_appid", "ts"]), "update successfully")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:mgmt( - ["delete", "emqx_appid"]), "ok")), - ok = emqx_mgmt_cli:mgmt(["list"]), - meck:unload(). - -t_status_cmd(_) -> - % ct:pal("start testing status command"), - mock_print(), - %% init internal status seem to be always 'starting' when running ct tests - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:status([]), "Node\s.*@.*\sis\sstart(ed|ing)")), - meck:unload(). - -t_broker_cmd(_) -> - % ct:pal("start testing the broker command"), - mock_print(), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker([]), "sysdescr")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker(["stats"]), "subscriptions.shared")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker(["metrics"]), "bytes.sent")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:broker([undefined]), "broker")), - meck:unload(). - -t_clients_cmd(_) -> - % ct:pal("start testing the client command"), - mock_print(), - process_flag(trap_exit, true), - {ok, T} = emqtt:start_link([{clientid, <<"client12">>}, - {username, <<"testuser1">>}, - {password, <<"pass1">>} - ]), - {ok, _} = emqtt:connect(T), - timer:sleep(300), - emqx_mgmt_cli:clients(["list"]), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client12"]), "client12")), - ?assertEqual((emqx_mgmt_cli:clients(["kick", "client12"])), "ok~n"), - timer:sleep(500), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client12"]), "Not Found")), - receive - {'EXIT', T, _} -> - ok - % ct:pal("Connection closed: ~p~n", [Reason]) - after - 500 -> - erlang:error("Client is not kick") - end, - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - Packet = raw_send_serialize(?CONNECT_PACKET(#mqtt_packet_connect{ - clientid = <<"client13">>})), - ok = rfc6455_client:send_binary(WS, Packet), - Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT), - {binary, Bin} = rfc6455_client:recv(WS), - {ok, Connack, <<>>, _} = raw_recv_pase(Bin), - timer:sleep(300), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client13"]), "client13")), - meck:unload(). - % emqx_mgmt_cli:clients(["kick", "client13"]), - % timer:sleep(500), - % ?assertMatch({match, _}, re:run(emqx_mgmt_cli:clients(["show", "client13"]), "Not Found")). - -raw_recv_pase(Packet) -> - emqx_frame:parse(Packet). - -raw_send_serialize(Packet) -> - emqx_frame:serialize(Packet). - -t_vm_cmd(_) -> - % ct:pal("start testing the vm command"), - mock_print(), - [[?assertMatch({match, _}, re:run(Result, Name)) - || Result <- emqx_mgmt_cli:vm([Name])] - || Name <- ["load", "memory", "process", "io", "ports"]], - [?assertMatch({match, _}, re:run(Result, "load")) - || Result <- emqx_mgmt_cli:vm(["load"])], - [?assertMatch({match, _}, re:run(Result, "memory")) - || Result <- emqx_mgmt_cli:vm(["memory"])], - [?assertMatch({match, _}, re:run(Result, "process")) - || Result <- emqx_mgmt_cli:vm(["process"])], - [?assertMatch({match, _}, re:run(Result, "io")) - || Result <- emqx_mgmt_cli:vm(["io"])], - [?assertMatch({match, _}, re:run(Result, "ports")) - || Result <- emqx_mgmt_cli:vm(["ports"])], - unmock_print(). - -t_trace_cmd(_) -> - % ct:pal("start testing the trace command"), - mock_print(), - logger:set_primary_config(level, debug), - {ok, T} = emqtt:start_link([{clientid, <<"client">>}, - {username, <<"testuser">>}, - {password, <<"pass">>} - ]), - emqtt:connect(T), - emqtt:subscribe(T, <<"a/b/c">>), - Trace1 = emqx_mgmt_cli:trace(["start", "client", "client", - "log/clientid_trace.log"]), - ?assertMatch({match, _}, re:run(Trace1, "successfully")), - Trace2 = emqx_mgmt_cli:trace(["stop", "client", "client"]), - ?assertMatch({match, _}, re:run(Trace2, "successfully")), - Trace3 = emqx_mgmt_cli:trace(["start", "client", "client", - "log/clientid_trace.log", - "error"]), - ?assertMatch({match, _}, re:run(Trace3, "successfully")), - Trace4 = emqx_mgmt_cli:trace(["stop", "client", "client"]), - ?assertMatch({match, _}, re:run(Trace4, "successfully")), - Trace5 = emqx_mgmt_cli:trace(["start", "topic", "a/b/c", - "log/clientid_trace.log"]), - ?assertMatch({match, _}, re:run(Trace5, "successfully")), - Trace6 = emqx_mgmt_cli:trace(["stop", "topic", "a/b/c"]), - ?assertMatch({match, _}, re:run(Trace6, "successfully")), - Trace7 = emqx_mgmt_cli:trace(["start", "topic", "a/b/c", - "log/clientid_trace.log", "error"]), - ?assertMatch({match, _}, re:run(Trace7, "successfully")), - logger:set_primary_config(level, error), - unmock_print(). - -t_router_cmd(_) -> - % ct:pal("start testing the router command"), - mock_print(), - {ok, T} = emqtt:start_link([{clientid, <<"client1">>}, - {username, <<"testuser1">>}, - {password, <<"pass1">>} - ]), - emqtt:connect(T), - emqtt:subscribe(T, <<"a/b/c">>), - {ok, T1} = emqtt:start_link([{clientid, <<"client2">>}, - {username, <<"testuser2">>}, - {password, <<"pass2">>} - ]), - - emqtt:connect(T1), - emqtt:subscribe(T1, <<"a/b/c/d">>), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:routes(["list"]), "a/b/c | a/b/c")), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:routes(["show", "a/b/c"]), "a/b/c")), - unmock_print(). - -t_subscriptions_cmd(_) -> - % ct:pal("Start testing the subscriptions command"), - mock_print(), - {ok, T3} = emqtt:start_link([{clientid, <<"client">>}, - {username, <<"testuser">>}, - {password, <<"pass">>} - ]), - {ok, _} = emqtt:connect(T3), - {ok, _, _} = emqtt:subscribe(T3, <<"b/b/c">>), - timer:sleep(300), - [?assertMatch({match, _} , re:run(Result, "b/b/c")) - || Result <- emqx_mgmt_cli:subscriptions(["show", <<"client">>])], - ?assertEqual(emqx_mgmt_cli:subscriptions(["add", "client", "b/b/c", "0"]), "ok~n"), - ?assertEqual(emqx_mgmt_cli:subscriptions(["del", "client", "b/b/c"]), "ok~n"), - unmock_print(). - -t_listeners_cmd_old(_) -> - ok = emqx_listeners:ensure_all_started(), - mock_print(), - ?assertEqual(emqx_mgmt_cli:listeners([]), ok), - ?assertEqual( - "Stop mqtt:wss:external listener on 0.0.0.0:8084 successfully.\n", - emqx_mgmt_cli:listeners(["stop", "wss", "8084"]) - ), - unmock_print(). - -t_listeners_cmd_new(_) -> - ok = emqx_listeners:ensure_all_started(), - mock_print(), - ?assertEqual(emqx_mgmt_cli:listeners([]), ok), - ?assertEqual( - "Stop mqtt:wss:external listener on 0.0.0.0:8084 successfully.\n", - emqx_mgmt_cli:listeners(["stop", "mqtt:wss:external"]) - ), - ?assertEqual( - emqx_mgmt_cli:listeners(["restart", "mqtt:tcp:external"]), - "Restarted mqtt:tcp:external listener successfully.\n" - ), - ?assertEqual( - emqx_mgmt_cli:listeners(["restart", "mqtt:ssl:external"]), - "Restarted mqtt:ssl:external listener successfully.\n" - ), - ?assertEqual( - emqx_mgmt_cli:listeners(["restart", "bad:listener:identifier"]), - "Failed to restart bad:listener:identifier listener: {no_such_listener,\"bad:listener:identifier\"}\n" - ), - unmock_print(). - -t_plugins_cmd(_) -> - mock_print(), - meck:new(emqx_plugins, [non_strict, passthrough]), - meck:expect(emqx_plugins, load, fun(_) -> ok end), - meck:expect(emqx_plugins, unload, fun(_) -> ok end), - meck:expect(emqx_plugins, reload, fun(_) -> ok end), - ?assertEqual(emqx_mgmt_cli:plugins(["list"]), ok), - ?assertEqual( - emqx_mgmt_cli:plugins(["unload", "emqx_retainer"]), - "Plugin emqx_retainer unloaded successfully.\n" - ), - ?assertEqual( - emqx_mgmt_cli:plugins(["load", "emqx_retainer"]), - "Plugin emqx_retainer loaded successfully.\n" - ), - ?assertEqual( - emqx_mgmt_cli:plugins(["unload", "emqx_management"]), - "Plugin emqx_management can not be unloaded.~n" - ), - unmock_print(). - -t_cli(_) -> - mock_print(), - ?assertMatch({match, _}, re:run(emqx_mgmt_cli:status([""]), "status")), - [?assertMatch({match, _}, re:run(Value, "broker")) - || Value <- emqx_mgmt_cli:broker([""])], - [?assertMatch({match, _}, re:run(Value, "cluster")) - || Value <- emqx_mgmt_cli:cluster([""])], - [?assertMatch({match, _}, re:run(Value, "clients")) - || Value <- emqx_mgmt_cli:clients([""])], - [?assertMatch({match, _}, re:run(Value, "routes")) - || Value <- emqx_mgmt_cli:routes([""])], - [?assertMatch({match, _}, re:run(Value, "subscriptions")) - || Value <- emqx_mgmt_cli:subscriptions([""])], - [?assertMatch({match, _}, re:run(Value, "plugins")) - || Value <- emqx_mgmt_cli:plugins([""])], - [?assertMatch({match, _}, re:run(Value, "listeners")) - || Value <- emqx_mgmt_cli:listeners([""])], - [?assertMatch({match, _}, re:run(Value, "vm")) - || Value <- emqx_mgmt_cli:vm([""])], - [?assertMatch({match, _}, re:run(Value, "mnesia")) - || Value <- emqx_mgmt_cli:mnesia([""])], - [?assertMatch({match, _}, re:run(Value, "trace")) - || Value <- emqx_mgmt_cli:trace([""])], - [?assertMatch({match, _}, re:run(Value, "mgmt")) - || Value <- emqx_mgmt_cli:mgmt([""])], - unmock_print(). - -mock_print() -> - catch meck:unload(emqx_ctl), - meck:new(emqx_ctl, [non_strict, passthrough]), - meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end), - meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end), - meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end), - meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end). - -unmock_print() -> - meck:unload(emqx_ctl). diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl deleted file mode 100644 index 69be9af32..000000000 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ /dev/null @@ -1,592 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_mgmt_api_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("emqx_management/include/emqx_mgmt.hrl"). - --define(CONTENT_TYPE, "application/x-www-form-urlencoded"). - --define(HOST, "http://127.0.0.1:8081/"). - --define(API_VERSION, "v4"). - --define(BASE_PATH, "api"). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), - Config. - -end_per_suite(Config) -> - emqx_ct_helpers:stop_apps([emqx_management]), - Config. - -init_per_testcase(_, Config) -> - Config. - -end_per_testcase(_, Config) -> - Config. - -set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], - applications =>[#{id => "admin", secret => "public"}]}), - ok; -set_special_configs(_App) -> - ok. - -get(Key, ResponseBody) -> - maps:get(Key, jiffy:decode(list_to_binary(ResponseBody), [return_maps])). - -lookup_alarm(Name, [#{<<"name">> := Name} | _More]) -> - true; -lookup_alarm(Name, [_Alarm | More]) -> - lookup_alarm(Name, More); -lookup_alarm(_Name, []) -> - false. - -is_existing(Name, [#{name := Name} | _More]) -> - true; -is_existing(Name, [_Alarm | More]) -> - is_existing(Name, More); -is_existing(_Name, []) -> - false. - -t_alarms(_) -> - emqx_alarm:activate(alarm1), - emqx_alarm:activate(alarm2), - - ?assert(is_existing(alarm1, emqx_alarm:get_alarms(activated))), - ?assert(is_existing(alarm2, emqx_alarm:get_alarms(activated))), - - {ok, Return1} = request_api(get, api_path(["alarms/activated"]), auth_header_()), - ?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return1))))), - ?assert(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return1))))), - - emqx_alarm:deactivate(alarm1), - - {ok, Return2} = request_api(get, api_path(["alarms"]), auth_header_()), - ?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return2))))), - ?assert(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return2))))), - - {ok, Return3} = request_api(get, api_path(["alarms/deactivated"]), auth_header_()), - ?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return3))))), - ?assertNot(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return3))))), - - emqx_alarm:deactivate(alarm2), - - {ok, Return4} = request_api(get, api_path(["alarms/deactivated"]), auth_header_()), - ?assert(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return4))))), - ?assert(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return4))))), - - {ok, _} = request_api(delete, api_path(["alarms/deactivated"]), auth_header_()), - - {ok, Return5} = request_api(get, api_path(["alarms/deactivated"]), auth_header_()), - ?assertNot(lookup_alarm(<<"alarm1">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return5))))), - ?assertNot(lookup_alarm(<<"alarm2">>, maps:get(<<"alarms">>, lists:nth(1, get(<<"data">>, Return5))))). - -t_apps(_) -> - AppId = <<"123456">>, - meck:new(emqx_mgmt_auth, [passthrough, no_history]), - meck:expect(emqx_mgmt_auth, add_app, 6, fun(_, _, _, _, _, _) -> {error, undefined} end), - {ok, Error1} = request_api(post, api_path(["apps"]), [], - auth_header_(), #{<<"app_id">> => AppId, - <<"name">> => <<"test">>, - <<"status">> => true}), - ?assertMatch(<<"undefined">>, get(<<"message">>, Error1)), - - meck:expect(emqx_mgmt_auth, del_app, 1, fun(_) -> {error, undefined} end), - {ok, Error2} = request_api(delete, api_path(["apps", binary_to_list(AppId)]), auth_header_()), - ?assertMatch(<<"undefined">>, get(<<"message">>, Error2)), - meck:unload(emqx_mgmt_auth), - - {ok, NoApp} = request_api(get, api_path(["apps", binary_to_list(AppId)]), auth_header_()), - ?assertEqual(0, maps:size(get(<<"data">>, NoApp))), - {ok, NotFound} = request_api(put, api_path(["apps", binary_to_list(AppId)]), [], - auth_header_(), #{<<"name">> => <<"test 2">>, - <<"status">> => true}), - ?assertEqual(<<"not_found">>, get(<<"message">>, NotFound)), - - {ok, _} = request_api(post, api_path(["apps"]), [], - auth_header_(), #{<<"app_id">> => AppId, - <<"name">> => <<"test">>, - <<"status">> => true}), - {ok, _} = request_api(get, api_path(["apps"]), auth_header_()), - {ok, _} = request_api(get, api_path(["apps", binary_to_list(AppId)]), auth_header_()), - {ok, _} = request_api(put, api_path(["apps", binary_to_list(AppId)]), [], - auth_header_(), #{<<"name">> => <<"test 2">>, - <<"status">> => true}), - {ok, AppInfo} = request_api(get, api_path(["apps", binary_to_list(AppId)]), auth_header_()), - ?assertEqual(<<"test 2">>, maps:get(<<"name">>, get(<<"data">>, AppInfo))), - {ok, _} = request_api(delete, api_path(["apps", binary_to_list(AppId)]), auth_header_()), - {ok, Result} = request_api(get, api_path(["apps"]), auth_header_()), - [App] = get(<<"data">>, Result), - ?assertEqual(<<"admin">>, maps:get(<<"app_id">>, App)). - -t_banned(_) -> - Who = <<"myclient">>, - {ok, _} = request_api(post, api_path(["banned"]), [], - auth_header_(), #{<<"who">> => Who, - <<"as">> => <<"clientid">>, - <<"reason">> => <<"test">>, - <<"by">> => <<"dashboard">>, - <<"at">> => erlang:system_time(second), - <<"until">> => erlang:system_time(second) + 10}), - - {ok, Result} = request_api(get, api_path(["banned"]), auth_header_()), - [Banned] = get(<<"data">>, Result), - ?assertEqual(Who, maps:get(<<"who">>, Banned)), - - {ok, _} = request_api(delete, api_path(["banned", "clientid", binary_to_list(Who)]), auth_header_()), - {ok, Result2} = request_api(get, api_path(["banned"]), auth_header_()), - ?assertEqual([], get(<<"data">>, Result2)). - -t_brokers(_) -> - {ok, _} = request_api(get, api_path(["brokers"]), auth_header_()), - {ok, _} = request_api(get, api_path(["brokers", atom_to_list(node())]), auth_header_()), - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, lookup_broker, 1, fun(_) -> {error, undefined} end), - {ok, Error} = request_api(get, api_path(["brokers", atom_to_list(node())]), auth_header_()), - ?assertEqual(<<"undefined">>, get(<<"message">>, Error)), - meck:unload(emqx_mgmt). - -t_clients(_) -> - process_flag(trap_exit, true), - Username1 = <<"user1">>, - Username2 = <<"user2">>, - ClientId1 = <<"client1">>, - ClientId2 = <<"client2">>, - {ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}), - {ok, _} = emqtt:connect(C1), - {ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}), - {ok, _} = emqtt:connect(C2), - - timer:sleep(300), - - {ok, Clients1} = request_api(get, api_path(["clients", binary_to_list(ClientId1)]) - , auth_header_()), - ?assertEqual(<<"client1">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients1)))), - - {ok, Clients2} = request_api(get, api_path(["nodes", atom_to_list(node()), - "clients", binary_to_list(ClientId2)]) - , auth_header_()), - ?assertEqual(<<"client2">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients2)))), - - {ok, Clients3} = request_api(get, api_path(["clients", - "username", binary_to_list(Username1)]), - auth_header_()), - ?assertEqual(<<"client1">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients3)))), - - {ok, Clients4} = request_api(get, api_path(["nodes", atom_to_list(node()), - "clients", - "username", binary_to_list(Username2)]) - , auth_header_()), - ?assertEqual(<<"client2">>, maps:get(<<"clientid">>, lists:nth(1, get(<<"data">>, Clients4)))), - - {ok, Clients5} = request_api(get, api_path(["clients"]), "_limit=100&_page=1", auth_header_()), - ?assertEqual(2, maps:get(<<"count">>, get(<<"meta">>, Clients5))), - - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, kickout_client, 1, fun(_) -> {error, undefined} end), - - {ok, MeckRet1} = request_api(delete, api_path(["clients", binary_to_list(ClientId1)]), auth_header_()), - ?assertEqual(?ERROR1, get(<<"code">>, MeckRet1)), - - meck:expect(emqx_mgmt, clean_acl_cache, 1, fun(_) -> {error, undefined} end), - {ok, MeckRet2} = request_api(delete, api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), auth_header_()), - ?assertEqual(?ERROR1, get(<<"code">>, MeckRet2)), - - meck:expect(emqx_mgmt, list_acl_cache, 1, fun(_) -> {error, undefined} end), - {ok, MeckRet3} = request_api(get, api_path(["clients", binary_to_list(ClientId2), "acl_cache"]), auth_header_()), - ?assertEqual(?ERROR1, get(<<"code">>, MeckRet3)), - - meck:unload(emqx_mgmt), - - {ok, Ok} = request_api(delete, api_path(["clients", binary_to_list(ClientId1)]), auth_header_()), - ?assertEqual(?SUCCESS, get(<<"code">>, Ok)), - - timer:sleep(300), - - {ok, NotFound0} = request_api(delete, api_path(["clients", binary_to_list(ClientId1)]), auth_header_()), - ?assertEqual(?ERROR12, get(<<"code">>, NotFound0)), - - {ok, Clients6} = request_api(get, api_path(["clients"]), "_limit=100&_page=1", auth_header_()), - ?assertEqual(1, maps:get(<<"count">>, get(<<"meta">>, Clients6))), - - {ok, NotFound1} = request_api(get, api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), auth_header_()), - ?assertEqual(?ERROR12, get(<<"code">>, NotFound1)), - - {ok, NotFound2} = request_api(delete, api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), auth_header_()), - ?assertEqual(?ERROR12, get(<<"code">>, NotFound2)), - - {ok, EmptyAclCache} = request_api(get, api_path(["clients", binary_to_list(ClientId2), "acl_cache"]), auth_header_()), - ?assertEqual(0, length(get(<<"data">>, EmptyAclCache))), - - {ok, Ok1} = request_api(delete, api_path(["clients", binary_to_list(ClientId2), "acl_cache"]), auth_header_()), - ?assertEqual(?SUCCESS, get(<<"code">>, Ok1)). - -receive_exit(0) -> - ok; -receive_exit(Count) -> - receive - {'EXIT', Client, {shutdown, tcp_closed}} -> - ct:log("receive exit signal, Client: ~p", [Client]), - receive_exit(Count - 1); - {'EXIT', Client, _Reason} -> - ct:log("receive exit signal, Client: ~p", [Client]), - receive_exit(Count - 1) - after 1000 -> - ct:log("timeout") - end. - -t_listeners(_) -> - {ok, _} = request_api(get, api_path(["listeners"]), auth_header_()), - {ok, _} = request_api(get, api_path(["nodes", atom_to_list(node()), "listeners"]), auth_header_()), - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, list_listeners, 0, fun() -> [{node(), {error, undefined}}] end), - {ok, Return} = request_api(get, api_path(["listeners"]), auth_header_()), - [Error] = get(<<"data">>, Return), - ?assertEqual(<<"undefined">>, - maps:get(<<"error">>, maps:get(<<"listeners">>, Error))), - meck:unload(emqx_mgmt). - -t_metrics(_) -> - {ok, _} = request_api(get, api_path(["metrics"]), auth_header_()), - {ok, _} = request_api(get, api_path(["nodes", atom_to_list(node()), "metrics"]), auth_header_()), - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, get_metrics, 1, fun(_) -> {error, undefined} end), - {ok, "{\"message\":\"undefined\"}"} = request_api(get, api_path(["nodes", atom_to_list(node()), "metrics"]), auth_header_()), - meck:unload(emqx_mgmt). - -t_nodes(_) -> - {ok, _} = request_api(get, api_path(["nodes"]), auth_header_()), - {ok, _} = request_api(get, api_path(["nodes", atom_to_list(node())]), auth_header_()), - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, list_nodes, 0, fun() -> [{node(), {error, undefined}}] end), - {ok, Return} = request_api(get, api_path(["nodes"]), auth_header_()), - [Error] = get(<<"data">>, Return), - ?assertEqual(<<"undefined">>, maps:get(<<"error">>, Error)), - meck:unload(emqx_mgmt). - -% t_plugins(_) -> -% application:ensure_all_started(emqx_retainer), -% {ok, Plugins1} = request_api(get, api_path(["plugins"]), auth_header_()), -% [Plugins11] = filter(get(<<"data">>, Plugins1), <<"node">>, atom_to_binary(node(), utf8)), -% [Plugin1] = filter(maps:get(<<"plugins">>, Plugins11), <<"name">>, <<"emqx_retainer">>), -% ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin1)), -% ?assertEqual(true, maps:get(<<"active">>, Plugin1)), -% -% {ok, _} = request_api(put, -% api_path(["plugins", -% atom_to_list(emqx_retainer), -% "unload"]), -% auth_header_()), -% {ok, Error1} = request_api(put, -% api_path(["plugins", -% atom_to_list(emqx_retainer), -% "unload"]), -% auth_header_()), -% ?assertEqual(<<"not_started">>, get(<<"message">>, Error1)), -% {ok, Plugins2} = request_api(get, -% api_path(["nodes", atom_to_list(node()), "plugins"]), -% auth_header_()), -% [Plugin2] = filter(get(<<"data">>, Plugins2), <<"name">>, <<"emqx_retainer">>), -% ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin2)), -% ?assertEqual(false, maps:get(<<"active">>, Plugin2)), -% -% {ok, _} = request_api(put, -% api_path(["nodes", -% atom_to_list(node()), -% "plugins", -% atom_to_list(emqx_retainer), -% "load"]), -% auth_header_()), -% {ok, Plugins3} = request_api(get, -% api_path(["nodes", atom_to_list(node()), "plugins"]), -% auth_header_()), -% [Plugin3] = filter(get(<<"data">>, Plugins3), <<"name">>, <<"emqx_retainer">>), -% ?assertEqual(<<"emqx_retainer">>, maps:get(<<"name">>, Plugin3)), -% ?assertEqual(true, maps:get(<<"active">>, Plugin3)), -% -% {ok, _} = request_api(put, -% api_path(["nodes", -% atom_to_list(node()), -% "plugins", -% atom_to_list(emqx_retainer), -% "unload"]), -% auth_header_()), -% {ok, Error2} = request_api(put, -% api_path(["nodes", -% atom_to_list(node()), -% "plugins", -% atom_to_list(emqx_retainer), -% "unload"]), -% auth_header_()), -% ?assertEqual(<<"not_started">>, get(<<"message">>, Error2)), -% application:stop(emqx_retainer). - -t_acl_cache(_) -> - ClientId = <<"client1">>, - Topic = <<"mytopic">>, - {ok, C1} = emqtt:start_link(#{clientid => ClientId}), - {ok, _} = emqtt:connect(C1), - {ok, _, _} = emqtt:subscribe(C1, Topic, 2), - %% get acl cache, should not be empty - {ok, Result} = request_api(get, api_path(["clients", binary_to_list(ClientId), "acl_cache"]), [], auth_header_()), - #{<<"code">> := 0, <<"data">> := Caches} = jiffy:decode(list_to_binary(Result), [return_maps]), - ?assert(length(Caches) > 0), - ?assertMatch(#{<<"access">> := <<"subscribe">>, - <<"topic">> := Topic, - <<"result">> := <<"allow">>, - <<"updated_time">> := _}, hd(Caches)), - %% clear acl cache - {ok, Result2} = request_api(delete, api_path(["clients", binary_to_list(ClientId), "acl_cache"]), [], auth_header_()), - ?assertMatch(#{<<"code">> := 0}, jiffy:decode(list_to_binary(Result2), [return_maps])), - %% get acl cache again, after the acl cache is cleared - {ok, Result3} = request_api(get, api_path(["clients", binary_to_list(ClientId), "acl_cache"]), [], auth_header_()), - #{<<"code">> := 0, <<"data">> := Caches3} = jiffy:decode(list_to_binary(Result3), [return_maps]), - ?assertEqual(0, length(Caches3)), - ok = emqtt:disconnect(C1). - -t_pubsub(_) -> - Qos1Received = emqx_metrics:val('messages.qos1.received'), - Qos2Received = emqx_metrics:val('messages.qos2.received'), - Received = emqx_metrics:val('messages.received'), - - ClientId = <<"client1">>, - Options = #{clientid => ClientId, - proto_ver => 5}, - Topic = <<"mytopic">>, - {ok, C1} = emqtt:start_link(Options), - {ok, _} = emqtt:connect(C1), - - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, subscribe, 2, fun(_, _) -> {error, undefined} end), - {ok, NotFound1} = request_api(post, api_path(["mqtt/subscribe"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => Topic, - <<"qos">> => 2}), - ?assertEqual(?ERROR12, get(<<"code">>, NotFound1)), - meck:unload(emqx_mgmt), - - {ok, BadTopic1} = request_api(post, api_path(["mqtt/subscribe"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topics">> => <<"">>, - <<"qos">> => 2}), - ?assertEqual(?ERROR15, get(<<"code">>, BadTopic1)), - - {ok, BadTopic2} = request_api(post, api_path(["mqtt/publish"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topics">> => <<"">>, - <<"qos">> => 1, - <<"payload">> => <<"hello">>}), - ?assertEqual(?ERROR15, get(<<"code">>, BadTopic2)), - - {ok, BadTopic3} = request_api(post, api_path(["mqtt/unsubscribe"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => <<"">>}), - ?assertEqual(?ERROR15, get(<<"code">>, BadTopic3)), - - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, unsubscribe, 2, fun(_, _) -> {error, undefined} end), - {ok, NotFound2} = request_api(post, api_path(["mqtt/unsubscribe"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => Topic}), - ?assertEqual(?ERROR12, get(<<"code">>, NotFound2)), - meck:unload(emqx_mgmt), - - {ok, Code} = request_api(post, api_path(["mqtt/subscribe"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => Topic, - <<"qos">> => 2}), - ?assertEqual(?SUCCESS, get(<<"code">>, Code)), - {ok, Code} = request_api(post, api_path(["mqtt/publish"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => <<"mytopic">>, - <<"qos">> => 1, - <<"payload">> => <<"hello">>}), - ?assert(receive - {publish, #{payload := <<"hello">>}} -> - true - after 100 -> - false - end), - %% json payload - {ok, Code} = request_api(post, api_path(["mqtt/publish"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => <<"mytopic">>, - <<"qos">> => 1, - <<"payload">> => #{body => "hello world"}}), - Payload = emqx_json:encode(#{body => "hello world"}), - ?assert(receive - {publish, #{payload := Payload}} -> - true - after 100 -> - false - end), - - {ok, Code} = request_api(post, api_path(["mqtt/unsubscribe"]), [], auth_header_(), - #{<<"clientid">> => ClientId, - <<"topic">> => Topic}), - - %% tests subscribe_batch - Topic_list = [<<"mytopic1">>, <<"mytopic2">>], - [ {ok, _, [2]} = emqtt:subscribe(C1, Topics, 2) || Topics <- Topic_list], - - Body1 = [ #{<<"clientid">> => ClientId, <<"topic">> => Topics, <<"qos">> => 2} || Topics <- Topic_list], - {ok, Data1} = request_api(post, api_path(["mqtt/subscribe_batch"]), [], auth_header_(), Body1), - loop(maps:get(<<"data">>, jiffy:decode(list_to_binary(Data1), [return_maps]))), - - %% tests publish_batch - Body2 = [ #{<<"clientid">> => ClientId, <<"topic">> => Topics, <<"qos">> => 2, <<"retain">> => <<"false">>, <<"payload">> => #{body => "hello world"}} || Topics <- Topic_list ], - {ok, Data2} = request_api(post, api_path(["mqtt/publish_batch"]), [], auth_header_(), Body2), - loop(maps:get(<<"data">>, jiffy:decode(list_to_binary(Data2), [return_maps]))), - [ ?assert(receive - {publish, #{topic := Topics}} -> - true - after 100 -> - false - end) || Topics <- Topic_list ], - - %% tests unsubscribe_batch - Body3 = [#{<<"clientid">> => ClientId, <<"topic">> => Topics} || Topics <- Topic_list], - {ok, Data3} = request_api(post, api_path(["mqtt/unsubscribe_batch"]), [], auth_header_(), Body3), - loop(maps:get(<<"data">>, jiffy:decode(list_to_binary(Data3), [return_maps]))), - - ok = emqtt:disconnect(C1), - - ?assertEqual(2, emqx_metrics:val('messages.qos1.received') - Qos1Received), - ?assertEqual(2, emqx_metrics:val('messages.qos2.received') - Qos2Received), - ?assertEqual(4, emqx_metrics:val('messages.received') - Received). - -loop([]) -> []; - -loop(Data) -> - [H | T] = Data, - ct:pal("H: ~p~n", [H]), - ?assertEqual(0, maps:get(<<"code">>, H)), - loop(T). - -t_routes_and_subscriptions(_) -> - ClientId = <<"myclient">>, - Topic = <<"mytopic">>, - {ok, NonRoute} = request_api(get, api_path(["routes"]), auth_header_()), - ?assertEqual([], get(<<"data">>, NonRoute)), - {ok, NonSubscription} = request_api(get, api_path(["subscriptions"]), auth_header_()), - ?assertEqual([], get(<<"data">>, NonSubscription)), - {ok, NonSubscription1} = request_api(get, api_path(["nodes", atom_to_list(node()), "subscriptions"]), auth_header_()), - ?assertEqual([], get(<<"data">>, NonSubscription1)), - {ok, NonSubscription2} = request_api(get, - api_path(["subscriptions", binary_to_list(ClientId)]), - auth_header_()), - ?assertEqual([], get(<<"data">>, NonSubscription2)), - {ok, NonSubscription3} = request_api(get, api_path(["nodes", - atom_to_list(node()), - "subscriptions", - binary_to_list(ClientId)]) - , auth_header_()), - ?assertEqual([], get(<<"data">>, NonSubscription3)), - {ok, C1} = emqtt:start_link(#{clean_start => true, - clientid => ClientId, - proto_ver => ?MQTT_PROTO_V5}), - {ok, _} = emqtt:connect(C1), - {ok, _, [2]} = emqtt:subscribe(C1, Topic, qos2), - {ok, Result} = request_api(get, api_path(["routes"]), auth_header_()), - [Route] = get(<<"data">>, Result), - ?assertEqual(Topic, maps:get(<<"topic">>, Route)), - - {ok, Result2} = request_api(get, api_path(["routes", binary_to_list(Topic)]), auth_header_()), - [Route] = get(<<"data">>, Result2), - - {ok, Result3} = request_api(get, api_path(["subscriptions"]), auth_header_()), - [Subscription] = get(<<"data">>, Result3), - ?assertEqual(Topic, maps:get(<<"topic">>, Subscription)), - ?assertEqual(ClientId, maps:get(<<"clientid">>, Subscription)), - - {ok, Result3} = request_api(get, api_path(["nodes", atom_to_list(node()), "subscriptions"]), auth_header_()), - - {ok, Result4} = request_api(get, api_path(["subscriptions", binary_to_list(ClientId)]), auth_header_()), - [Subscription] = get(<<"data">>, Result4), - {ok, Result4} = request_api(get, api_path(["nodes", atom_to_list(node()), "subscriptions", binary_to_list(ClientId)]) - , auth_header_()), - - ok = emqtt:disconnect(C1). - -t_stats(_) -> - {ok, _} = request_api(get, api_path(["stats"]), auth_header_()), - {ok, _} = request_api(get, api_path(["nodes", atom_to_list(node()), "stats"]), auth_header_()), - meck:new(emqx_mgmt, [passthrough, no_history]), - meck:expect(emqx_mgmt, get_stats, 1, fun(_) -> {error, undefined} end), - {ok, Return} = request_api(get, api_path(["nodes", atom_to_list(node()), "stats"]), auth_header_()), - ?assertEqual(<<"undefined">>, get(<<"message">>, Return)), - meck:unload(emqx_mgmt). - -request_api(Method, Url, Auth) -> - request_api(Method, Url, [], Auth, []). - -request_api(Method, Url, QueryParams, Auth) -> - request_api(Method, Url, QueryParams, Auth, []). - -request_api(Method, Url, QueryParams, Auth, []) -> - NewUrl = case QueryParams of - "" -> Url; - _ -> Url ++ "?" ++ QueryParams - end, - do_request_api(Method, {NewUrl, [Auth]}); -request_api(Method, Url, QueryParams, Auth, Body) -> - NewUrl = case QueryParams of - "" -> Url; - _ -> Url ++ "?" ++ QueryParams - end, - do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}). - -do_request_api(Method, Request)-> - ct:pal("Method: ~p, Request: ~p", [Method, Request]), - case httpc:request(Method, Request, [], []) of - {error, socket_closed_remotely} -> - {error, socket_closed_remotely}; - {ok, {{"HTTP/1.1", Code, _}, _, Return} } - when Code =:= 200 orelse Code =:= 201 -> - {ok, Return}; - {ok, {Reason, _, _}} -> - {error, Reason} - end. - -auth_header_() -> - AppId = <<"admin">>, - AppSecret = <<"public">>, - auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). - -auth_header_(User, Pass) -> - Encoded = base64:encode_to_string(lists:append([User,":",Pass])), - {"Authorization","Basic " ++ Encoded}. - -api_path(Parts)-> - ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts). - -filter(List, Key, Value) -> - lists:filter(fun(Item) -> - maps:get(Key, Item) == Value - end, List). diff --git a/apps/emqx_management/test/etc/emqx_management.conf b/apps/emqx_management/test/etc/emqx_management.conf deleted file mode 100644 index 0fb2e4250..000000000 --- a/apps/emqx_management/test/etc/emqx_management.conf +++ /dev/null @@ -1,43 +0,0 @@ -emqx_management:{ - applications: [ - { - id: "admin", - secret: "public" - } - ] - max_row_limit: 10000 - listeners: [ - { - num_acceptors: 4 - max_connections: 512 - protocol: http - port: 8080 - backlog: 512 - send_timeout: 15s - send_timeout_close: on - inet6: false - ipv6_v6only: false - } -## , -## { -## protocol: https -## port: 8081 -## acceptors: 2 -## backlog: 512 -## send_timeout: 15s -## send_timeout_close: on -## inet6: false -## ipv6_v6only: false -## certfile = "etc/certs/cert.pem" -## keyfile = "etc/certs/key.pem" -## cacertfile = "etc/certs/cacert.pem" -## verify = verify_peer -## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" -## ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## fail_if_no_peer_cert = true -## inet6 = false -## ipv6_v6only = false -## } - ] -} - diff --git a/apps/emqx_management/test/etc/emqx_reloader.conf b/apps/emqx_management/test/etc/emqx_reloader.conf deleted file mode 100644 index 0919c8411..000000000 --- a/apps/emqx_management/test/etc/emqx_reloader.conf +++ /dev/null @@ -1,24 +0,0 @@ -##-------------------------------------------------------------------- -## Reloader Plugin -##-------------------------------------------------------------------- - -## Interval of hot code reloading. -## -## Value: Duration -## - h: hour -## - m: minute -## - s: second -## -## Examples: -## - 2h: 2 hours -## - 30m: 30 minutes -## - 20s: 20 seconds -## -## Defaut: 60s -reloader.interval = 60s - -## Logfile of reloader. -## -## Value: File -reloader.logfile = reloader.log - diff --git a/apps/emqx_management/test/rfc6455_client.erl b/apps/emqx_management/test/rfc6455_client.erl deleted file mode 100644 index 987b72407..000000000 --- a/apps/emqx_management/test/rfc6455_client.erl +++ /dev/null @@ -1,252 +0,0 @@ -%% The contents of this file are subject to the Mozilla Public License -%% Version 1.1 (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.mozilla.org/MPL/ -%% -%% Software distributed under the License is distributed on an "AS IS" -%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -%% License for the specific language governing rights and limitations -%% under the License. -%% -%% The Original Code is RabbitMQ Management Console. -%% -%% The Initial Developer of the Original Code is GoPivotal, Inc. -%% Copyright (c) 2012-2016 Pivotal Software, Inc. All rights reserved. -%% - --module(rfc6455_client). - --export([new/2, open/1, recv/1, send/2, send_binary/2, close/1, close/2]). - --record(state, {host, port, addr, path, ppid, socket, data, phase}). - -%% -------------------------------------------------------------------------- - -new(WsUrl, PPid) -> - crypto:start(), - "ws://" ++ Rest = WsUrl, - [Addr, Path] = split("/", Rest, 1), - [Host, MaybePort] = split(":", Addr, 1, empty), - Port = case MaybePort of - empty -> 80; - V -> {I, ""} = string:to_integer(V), I - end, - State = #state{host = Host, - port = Port, - addr = Addr, - path = "/" ++ Path, - ppid = PPid}, - spawn(fun() -> - start_conn(State) - end). - -open(WS) -> - receive - {rfc6455, open, WS, Opts} -> - {ok, Opts}; - {rfc6455, close, WS, R} -> - {close, R} - end. - -recv(WS) -> - receive - {rfc6455, recv, WS, Payload} -> - {ok, Payload}; - {rfc6455, recv_binary, WS, Payload} -> - {binary, Payload}; - {rfc6455, close, WS, R} -> - {close, R} - end. - -send(WS, IoData) -> - WS ! {send, IoData}, - ok. - -send_binary(WS, IoData) -> - WS ! {send_binary, IoData}, - ok. - -close(WS) -> - close(WS, {1000, ""}). - -close(WS, WsReason) -> - WS ! {close, WsReason}, - receive - {rfc6455, close, WS, R} -> - {close, R} - end. - - -%% -------------------------------------------------------------------------- - -start_conn(State) -> - {ok, Socket} = gen_tcp:connect(State#state.host, State#state.port, - [binary, - {packet, 0}]), - Key = base64:encode_to_string(crypto:strong_rand_bytes(16)), - gen_tcp:send(Socket, - "GET " ++ State#state.path ++ " HTTP/1.1\r\n" ++ - "Host: " ++ State#state.addr ++ "\r\n" ++ - "Upgrade: websocket\r\n" ++ - "Connection: Upgrade\r\n" ++ - "Sec-WebSocket-Key: " ++ Key ++ "\r\n" ++ - "Origin: null\r\n" ++ - "Sec-WebSocket-Protocol: mqtt\r\n" ++ - "Sec-WebSocket-Version: 13\r\n\r\n"), - - loop(State#state{socket = Socket, - data = <<>>, - phase = opening}). - -do_recv(State = #state{phase = opening, ppid = PPid, data = Data}) -> - case split("\r\n\r\n", binary_to_list(Data), 1, empty) of - [_Http, empty] -> State; - [Http, Data1] -> - %% TODO: don't ignore http response data, verify key - PPid ! {rfc6455, open, self(), [{http_response, Http}]}, - State#state{phase = open, - data = Data1} - end; -do_recv(State = #state{phase = Phase, data = Data, socket = Socket, ppid = PPid}) - when Phase =:= open orelse Phase =:= closing -> - R = case Data of - <> - when L < 126 -> - {F, O, Payload, Rest}; - - <> -> - {F, O, Payload, Rest}; - - <> -> - {F, O, Payload, Rest}; - - <<_:1, _:3, _:4, 1:1, _/binary>> -> - %% According o rfc6455 5.1 the server must not mask any frames. - die(Socket, PPid, {1006, "Protocol error"}, normal); - _ -> - moredata - end, - case R of - moredata -> - State; - _ -> do_recv2(State, R) - end. - -do_recv2(State = #state{phase = Phase, socket = Socket, ppid = PPid}, R) -> - case R of - {1, 1, Payload, Rest} -> - PPid ! {rfc6455, recv, self(), Payload}, - State#state{data = Rest}; - {1, 2, Payload, Rest} -> - PPid ! {rfc6455, recv_binary, self(), Payload}, - State#state{data = Rest}; - {1, 8, Payload, _Rest} -> - WsReason = case Payload of - <> -> {WC, WR}; - <<>> -> {1005, "No status received"} - end, - case Phase of - open -> %% echo - do_close(State, WsReason), - gen_tcp:close(Socket); - closing -> - ok - end, - die(Socket, PPid, WsReason, normal); - {_, _, _, _Rest2} -> - io:format("Unknown frame type~n"), - die(Socket, PPid, {1006, "Unknown frame type"}, normal) - end. - -encode_frame(F, O, Payload) -> - Mask = crypto:strong_rand_bytes(4), - MaskedPayload = apply_mask(Mask, iolist_to_binary(Payload)), - - L = byte_size(MaskedPayload), - IoData = case L of - _ when L < 126 -> - [<>, Mask, MaskedPayload]; - _ when L < 65536 -> - [<>, Mask, MaskedPayload]; - _ -> - [<>, Mask, MaskedPayload] - end, - iolist_to_binary(IoData). - -do_send(State = #state{socket = Socket}, Payload) -> - gen_tcp:send(Socket, encode_frame(1, 1, Payload)), - State. - -do_send_binary(State = #state{socket = Socket}, Payload) -> - gen_tcp:send(Socket, encode_frame(1, 2, Payload)), - State. - -do_close(State = #state{socket = Socket}, {Code, Reason}) -> - Payload = iolist_to_binary([<>, Reason]), - gen_tcp:send(Socket, encode_frame(1, 8, Payload)), - State#state{phase = closing}. - - -loop(State = #state{socket = Socket, ppid = PPid, data = Data, - phase = Phase}) -> - receive - {tcp, Socket, Bin} -> - State1 = State#state{data = iolist_to_binary([Data, Bin])}, - loop(do_recv(State1)); - {send, Payload} when Phase == open -> - loop(do_send(State, Payload)); - {send_binary, Payload} when Phase == open -> - loop(do_send_binary(State, Payload)); - {tcp_closed, Socket} -> - die(Socket, PPid, {1006, "Connection closed abnormally"}, normal); - {close, WsReason} when Phase == open -> - loop(do_close(State, WsReason)) - end. - - -die(Socket, PPid, WsReason, Reason) -> - gen_tcp:shutdown(Socket, read_write), - PPid ! {rfc6455, close, self(), WsReason}, - exit(Reason). - - -%% -------------------------------------------------------------------------- - -split(SubStr, Str, Limit) -> - split(SubStr, Str, Limit, ""). - -split(SubStr, Str, Limit, Default) -> - Acc = split(SubStr, Str, Limit, [], Default), - lists:reverse(Acc). -split(_SubStr, Str, 0, Acc, _Default) -> [Str | Acc]; -split(SubStr, Str, Limit, Acc, Default) -> - {L, R} = case string:str(Str, SubStr) of - 0 -> {Str, Default}; - I -> {string:substr(Str, 1, I-1), - string:substr(Str, I+length(SubStr))} - end, - split(SubStr, R, Limit-1, [L | Acc], Default). - - -apply_mask(Mask, Data) when is_number(Mask) -> - apply_mask(<>, Data); - -apply_mask(<<0:32>>, Data) -> - Data; -apply_mask(Mask, Data) -> - iolist_to_binary(lists:reverse(apply_mask2(Mask, Data, []))). - -apply_mask2(M = <>, <>, Acc) -> - T = Data bxor Mask, - apply_mask2(M, Rest, [<> | Acc]); -apply_mask2(<>, <>, Acc) -> - T = Data bxor Mask, - [<> | Acc]; -apply_mask2(<>, <>, Acc) -> - T = Data bxor Mask, - [<> | Acc]; -apply_mask2(<>, <>, Acc) -> - T = Data bxor Mask, - [<> | Acc]; -apply_mask2(_, <<>>, Acc) -> - Acc. diff --git a/apps/emqx_management/test/test_utils.erl b/apps/emqx_management/test/test_utils.erl deleted file mode 100644 index 337a9499b..000000000 --- a/apps/emqx_management/test/test_utils.erl +++ /dev/null @@ -1,19 +0,0 @@ -%% @author: -%% @description: --module(test_utils). -%% ==================================================================== -%% API functions -%% ==================================================================== --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx_rule_engine/include/rule_engine.hrl"). - --compile([export_all, nowarn_export_all]). - -%% ==================================================================== -%% Internal functions -%% ==================================================================== -resource_is_alive(Id) -> - {ok, #resource_params{status = #{is_alive := Alive}} = Params} = emqx_rule_registry:find_resource_params(Id), - ct:pal("Id: ~p, Alive: ~p, Resource ===> :~p~n", [Id, Alive, Params]), - ?assertEqual(true, Alive), - Alive. diff --git a/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl b/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl index d78b3f18a..2f8fbd017 100644 --- a/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl +++ b/apps/emqx_modules/src/emqx_mod_api_topic_metrics.erl @@ -16,8 +16,6 @@ -module(emqx_mod_api_topic_metrics). --import(minirest, [return/1]). - -rest_api(#{name => list_all_topic_metrics, method => 'GET', path => "/topic-metrics", @@ -203,3 +201,7 @@ rpc_call(Node, Fun, Args) -> {badrpc, Reason} -> {error, Reason}; Res -> Res end. + +return(_) -> +%% TODO: V5 API + ok. diff --git a/apps/emqx_modules/src/emqx_modules_api.erl b/apps/emqx_modules/src/emqx_modules_api.erl index 3a4b05fd0..99a3b89f9 100644 --- a/apps/emqx_modules/src/emqx_modules_api.erl +++ b/apps/emqx_modules/src/emqx_modules_api.erl @@ -16,8 +16,6 @@ -module(emqx_modules_api). --import(minirest, [return/1]). - -rest_api(#{name => list_all_modules, method => 'GET', path => "/modules/", @@ -167,3 +165,7 @@ name(emqx_mod_presence) -> presence; name(emqx_mod_recon) -> recon; name(emqx_mod_rewrite) -> rewrite; name(emqx_mod_topic_metrics) -> topic_metrics. + +return(_) -> +%% TODO: V5 API + ok. diff --git a/apps/emqx_modules/test/emqx_modules_SUITE.erl b/apps/emqx_modules/test/emqx_modules_SUITE.erl index ec717381c..0ee097258 100644 --- a/apps/emqx_modules/test/emqx_modules_SUITE.erl +++ b/apps/emqx_modules/test/emqx_modules_SUITE.erl @@ -58,59 +58,60 @@ t_list(_) -> ?assertMatch([_ | _ ], emqx_modules:list()), emqx_modules:unload(presence). -t_modules_api(_) -> - emqx_modules:load(presence, #{qos => 1}), - timer:sleep(50), - {ok, Modules1} = request_api(get, api_path(["modules"]), auth_header_()), - [Modules11] = filter(get(<<"data">>, Modules1), <<"node">>, atom_to_binary(node(), utf8)), - [Module1] = filter(maps:get(<<"modules">>, Modules11), <<"name">>, <<"presence">>), - ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module1)), - {ok, _} = request_api(put, - api_path(["modules", - atom_to_list(presence), - "unload"]), - auth_header_()), - {ok, Error1} = request_api(put, - api_path(["modules", - atom_to_list(presence), - "unload"]), - auth_header_()), - ?assertEqual(<<"not_started">>, get(<<"message">>, Error1)), - {ok, Modules2} = request_api(get, - api_path(["nodes", atom_to_list(node()), "modules"]), - auth_header_()), - [Module2] = filter(get(<<"data">>, Modules2), <<"name">>, <<"presence">>), - ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module2)), - - {ok, _} = request_api(put, - api_path(["nodes", - atom_to_list(node()), - "modules", - atom_to_list(presence), - "load"]), - auth_header_()), - {ok, Modules3} = request_api(get, - api_path(["nodes", atom_to_list(node()), "modules"]), - auth_header_()), - [Module3] = filter(get(<<"data">>, Modules3), <<"name">>, <<"presence">>), - ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module3)), - - {ok, _} = request_api(put, - api_path(["nodes", - atom_to_list(node()), - "modules", - atom_to_list(presence), - "unload"]), - auth_header_()), - {ok, Error2} = request_api(put, - api_path(["nodes", - atom_to_list(node()), - "modules", - atom_to_list(presence), - "unload"]), - auth_header_()), - ?assertEqual(<<"not_started">>, get(<<"message">>, Error2)), - emqx_modules:unload(presence). +%% TODO: V5 API +%%t_modules_api(_) -> +%% emqx_modules:load(presence, #{qos => 1}), +%% timer:sleep(50), +%% {ok, Modules1} = request_api(get, api_path(["modules"]), auth_header_()), +%% [Modules11] = filter(get(<<"data">>, Modules1), <<"node">>, atom_to_binary(node(), utf8)), +%% [Module1] = filter(maps:get(<<"modules">>, Modules11), <<"name">>, <<"presence">>), +%% ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module1)), +%% {ok, _} = request_api(put, +%% api_path(["modules", +%% atom_to_list(presence), +%% "unload"]), +%% auth_header_()), +%% {ok, Error1} = request_api(put, +%% api_path(["modules", +%% atom_to_list(presence), +%% "unload"]), +%% auth_header_()), +%% ?assertEqual(<<"not_started">>, get(<<"message">>, Error1)), +%% {ok, Modules2} = request_api(get, +%% api_path(["nodes", atom_to_list(node()), "modules"]), +%% auth_header_()), +%% [Module2] = filter(get(<<"data">>, Modules2), <<"name">>, <<"presence">>), +%% ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module2)), +%% +%% {ok, _} = request_api(put, +%% api_path(["nodes", +%% atom_to_list(node()), +%% "modules", +%% atom_to_list(presence), +%% "load"]), +%% auth_header_()), +%% {ok, Modules3} = request_api(get, +%% api_path(["nodes", atom_to_list(node()), "modules"]), +%% auth_header_()), +%% [Module3] = filter(get(<<"data">>, Modules3), <<"name">>, <<"presence">>), +%% ?assertEqual(<<"presence">>, maps:get(<<"name">>, Module3)), +%% +%% {ok, _} = request_api(put, +%% api_path(["nodes", +%% atom_to_list(node()), +%% "modules", +%% atom_to_list(presence), +%% "unload"]), +%% auth_header_()), +%% {ok, Error2} = request_api(put, +%% api_path(["nodes", +%% atom_to_list(node()), +%% "modules", +%% atom_to_list(presence), +%% "unload"]), +%% auth_header_()), +%% ?assertEqual(<<"not_started">>, get(<<"message">>, Error2)), +%% emqx_modules:unload(presence). t_modules_cmd(_) -> diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 29acc72f6..04ebd78d3 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -25,8 +25,6 @@ -include_lib("prometheus/include/prometheus.hrl"). -include_lib("prometheus/include/prometheus_model.hrl"). --import(minirest, [return/1]). - -rest_api(#{name => stats, method => 'GET', path => "/emqx_prometheus", @@ -610,3 +608,7 @@ emqx_cluster_data() -> #{running_nodes := Running, stopped_nodes := Stopped} = ekka_mnesia:cluster_info(), [{nodes_running, length(Running)}, {nodes_stopped, length(Stopped)}]. + +%% TODO: V5 API +return(_) -> + ok. diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 237a3b19c..1b5b8adcc 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -36,7 +36,7 @@ lookup_config(_Bindings, _Params) -> Config = emqx_config:get([emqx_retainer]), - minirest:return({ok, Config}). + return({ok, Config}). update_config(_Bindings, Params) -> try @@ -47,9 +47,9 @@ update_config(_Bindings, Params) -> #{emqx_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), Action = proplists:get_value(<<"action">>, Params, undefined), do_update_config(Action, Conf), - minirest:return() + return() catch _:_:Reason -> - minirest:return({error, Reason}) + return({error, Reason}) end. %%------------------------------------------------------------------------------ @@ -59,3 +59,9 @@ do_update_config(undefined, Config) -> emqx_retainer:update_config(Config); do_update_config(<<"test">>, _) -> ok. + +%% TODO: V5 API +return() -> + ok. +return(_) -> + ok. diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl index 5fd1ff4e6..1f5a32542 100644 --- a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl @@ -35,7 +35,9 @@ -define(BASE_PATH, "api"). all() -> - emqx_ct:all(?MODULE). +%% TODO: V5 API +%% emqx_ct:all(?MODULE). + []. groups() -> []. diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl index 4fa3b8aa3..24b4d2c13 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl @@ -21,8 +21,6 @@ -logger_header("[RuleEngineAPI]"). --import(minirest, [return/1]). - -rest_api(#{name => create_rule, method => 'POST', path => "/rules/", @@ -552,3 +550,6 @@ get_rule_metrics(Id) -> get_action_metrics(Id) -> [maps:put(node, Node, rpc:call(Node, emqx_rule_metrics, get_action_metrics, [Id])) || Node <- ekka_mnesia:running_nodes()]. + +%% TODO: V5 API +return(_) -> ok. \ No newline at end of file diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index da3e963f0..a056d0c26 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -54,14 +54,15 @@ groups() -> [t_inspect_action ,t_republish_action ]}, - {api, [], - [t_crud_rule_api, - t_list_actions_api, - t_show_action_api, - t_crud_resources_api, - t_list_resource_types_api, - t_show_resource_type_api - ]}, +%% TODO: V5 API +%% {api, [], +%% [t_crud_rule_api, +%% t_list_actions_api, +%% t_show_action_api, +%% t_crud_resources_api, +%% t_list_resource_types_api, +%% t_show_resource_type_api +%% ]}, {cli, [], [t_rules_cli, t_actions_cli, diff --git a/apps/emqx_telemetry/src/emqx_telemetry_api.erl b/apps/emqx_telemetry/src/emqx_telemetry_api.erl index 8bb97086e..798d114eb 100644 --- a/apps/emqx_telemetry/src/emqx_telemetry_api.erl +++ b/apps/emqx_telemetry/src/emqx_telemetry_api.erl @@ -44,8 +44,6 @@ , get_telemetry_data/0 ]). --import(minirest, [return/1]). - %%-------------------------------------------------------------------- %% CLI %%-------------------------------------------------------------------- @@ -129,3 +127,6 @@ rpc_call(Node, Module, Fun, Args) -> {badrpc, Reason} -> {error, Reason}; Result -> Result end. + +%% TODO: V5 API +return(_) -> ok. diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 4cad21569..6ebbf5121 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -162,7 +162,7 @@ spec: {{ end }} readinessProbe: httpGet: - path: /status + path: /api/v5/status port: {{ .Values.emqxConfig.EMQX_MANAGEMENT__LISTENER__HTTP | default 8081 }} initialDelaySeconds: 5 periodSeconds: 5 diff --git a/rebar.config b/rebar.config index fc863f7c4..597a2cbfc 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} % TODO: delete when all apps moved to hocon - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "0.3.6"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.1.1"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, {git, "https://github.com/emqx/replayq", {tag, "0.3.2"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} From 3195561a7908e5472c24e680de3ed8ed012e7682 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 9 Jul 2021 14:28:48 +0800 Subject: [PATCH 128/379] feat(authz connector): match directly using the results returned by sql Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz_mongo.erl | 3 +-- apps/emqx_authz/src/emqx_authz_mysql.erl | 19 +------------------ apps/emqx_authz/src/emqx_authz_pgsql.erl | 19 +------------------ 3 files changed, 3 insertions(+), 38 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl index 04af8f1ec..a32054997 100644 --- a/apps/emqx_authz/src/emqx_authz_mongo.erl +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -60,8 +60,7 @@ match(Client, PubSub, Topic, <<"permission">> := Permission, <<"action">> := Action }) -> - Rule = #{<<"principal">> => all, - <<"permission">> => Permission, + Rule = #{<<"permission">> => Permission, <<"topics">> => Topics, <<"action">> => Action }, diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index 4c769085d..0ab1418f2 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -77,13 +77,9 @@ format_result(Columns, Row) -> match(Client, PubSub, Topic, #{<<"permission">> := Permission, <<"action">> := Action, - <<"clientid">> := ClientId, - <<"username">> := Username, - <<"ipaddress">> := IpAddress, <<"topic">> := TopicFilter }) -> - Rule = #{<<"principal">> => principal(IpAddress, Username, ClientId), - <<"topics">> => [TopicFilter], + Rule = #{<<"topics">> => [TopicFilter], <<"action">> => Action, <<"permission">> => Permission }, @@ -99,19 +95,6 @@ match(Client, PubSub, Topic, false -> nomatch end. -principal(CIDR, Username, ClientId) -> - Cols = [{<<"ipaddress">>, CIDR}, {<<"username">>, Username}, {<<"clientid">>, ClientId}], - case [#{C => V} || {C, V} <- Cols, not empty(V)] of - [] -> throw(undefined_who); - [Who] -> Who; - Conds -> #{<<"and">> => Conds} - end. - -empty(null) -> true; -empty("") -> true; -empty(<<>>) -> true; -empty(_) -> false. - replvar(Params, ClientInfo) -> replvar(Params, ClientInfo, []). diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index d74db36b2..c990a29d3 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -81,13 +81,9 @@ format_result(Columns, Row) -> match(Client, PubSub, Topic, #{<<"permission">> := Permission, <<"action">> := Action, - <<"clientid">> := ClientId, - <<"username">> := Username, - <<"ipaddress">> := IpAddress, <<"topic">> := TopicFilter }) -> - Rule = #{<<"principal">> => principal(IpAddress, Username, ClientId), - <<"topics">> => [TopicFilter], + Rule = #{<<"topics">> => [TopicFilter], <<"action">> => Action, <<"permission">> => Permission }, @@ -103,19 +99,6 @@ match(Client, PubSub, Topic, false -> nomatch end. -principal(CIDR, Username, ClientId) -> - Cols = [{<<"ipaddress">>, CIDR}, {<<"username">>, Username}, {<<"clientid">>, ClientId}], - case [#{C => V} || {C, V} <- Cols, not empty(V)] of - [] -> throw(undefined_who); - [Who] -> Who; - Conds -> #{<<"and">> => Conds} - end. - -empty(null) -> true; -empty("") -> true; -empty(<<>>) -> true; -empty(_) -> false. - replvar(Params, ClientInfo) -> replvar(Params, ClientInfo, []). From 9cda6ab3c8e2d3bd22830d184f6f80a3f8a821d9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 9 Jul 2021 19:09:44 +0800 Subject: [PATCH 129/379] feat(alarm): update the validity_period timer --- apps/emqx/src/emqx_alarm.erl | 41 ++++++++++++++----- apps/emqx/test/emqx_alarm_SUITE.erl | 28 ++++--------- .../src/emqx_data_bridge_app.erl | 5 ++- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 5b2fa6f40..0eaa16507 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -17,6 +17,7 @@ -module(emqx_alarm). -behaviour(gen_server). +-behaviour(emqx_config_handler). -include("emqx.hrl"). -include("logger.hrl"). @@ -29,6 +30,8 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). +-export([handle_update_config/2]). + -export([ start_link/0 , stop/0 ]). @@ -75,7 +78,7 @@ }). -record(state, { - timer = undefined :: undefined | reference() + timer :: reference() }). -define(ACTIVATED_ALARM, emqx_activated_alarm). @@ -148,14 +151,20 @@ get_alarms(activated) -> get_alarms(deactivated) -> gen_server:call(?MODULE, {get_alarms, deactivated}). +handle_update_config(#{<<"validity_period">> := Period0} = NewConf, OldConf) -> + ?MODULE ! {update_timer, hocon_postprocess:duration(Period0)}, + maps:merge(OldConf, NewConf); +handle_update_config(NewConf, OldConf) -> + maps:merge(OldConf, NewConf). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- init([]) -> deactivate_all_alarms(), - ensure_delete_timer(), - {ok, #state{}}. + emqx_config_handler:add_handler([alarm], ?MODULE), + {ok, #state{timer = ensure_timer(undefined, get_validity_period())}}. %% suppress dialyzer warning due to dirty read/write race condition. %% TODO: change from dirty_read/write to transactional. @@ -215,11 +224,15 @@ handle_cast(Msg, State) -> ?LOG(error, "Unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({timeout, _TRef, delete_expired_deactivated_alarm}, State) -> - ValidityPeriod = emqx_config:get([alarm, validity_period]), - delete_expired_deactivated_alarms(erlang:system_time(microsecond) - ValidityPeriod * 1000), - ensure_delete_timer(), - {noreply, State}; +handle_info({timeout, _TRef, delete_expired_deactivated_alarm}, + #state{timer = TRef} = State) -> + Period = get_validity_period(), + delete_expired_deactivated_alarms(erlang:system_time(microsecond) - Period * 1000), + {noreply, State#state{timer = ensure_timer(TRef, Period)}}; + +handle_info({update_timer, Period}, #state{timer = TRef} = State) -> + ?LOG(warning, "update the 'validity_period' timer to ~p", [Period]), + {noreply, State#state{timer = ensure_timer(TRef, Period)}}; handle_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), @@ -235,6 +248,9 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%------------------------------------------------------------------------------ +get_validity_period() -> + timer:seconds(emqx_config:get([alarm, validity_period])). + deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name, details = Details0, message = Msg0}) -> SizeLimit = emqx_config:get([alarm, size_limit]), @@ -290,9 +306,12 @@ clear_table(TableName) -> ok end. -ensure_delete_timer() -> - emqx_misc:start_timer(emqx_config:get([alarm, validity_period]), - delete_expired_deactivated_alarm). +ensure_timer(OldTRef, Period) -> + case is_reference(OldTRef) of + true -> _ = erlang:cancel_timer(OldTRef); + false -> ok + end, + emqx_misc:start_timer(Period, delete_expired_deactivated_alarm). delete_expired_deactivated_alarms(Checkpoint) -> delete_expired_deactivated_alarms(mnesia:dirty_first(?DEACTIVATED_ALARM), Checkpoint). diff --git a/apps/emqx/test/emqx_alarm_SUITE.erl b/apps/emqx/test/emqx_alarm_SUITE.erl index db6cdfe7f..e21dff30a 100644 --- a/apps/emqx/test/emqx_alarm_SUITE.erl +++ b/apps/emqx/test/emqx_alarm_SUITE.erl @@ -27,27 +27,17 @@ all() -> emqx_ct:all(?MODULE). init_per_testcase(t_size_limit, Config) -> emqx_ct_helpers:boot_modules(all), - emqx_ct_helpers:start_apps([], - fun(emqx) -> - application:set_env(emqx, alarm, [{actions, [log,publish]}, - {size_limit, 2}, - {validity_period, 3600}]), - ok; - (_) -> - ok - end), + emqx_ct_helpers:start_apps([]), + emqx_config:update_config([alarm], #{ + <<"size_limit">> => 2 + }), Config; init_per_testcase(t_validity_period, Config) -> emqx_ct_helpers:boot_modules(all), - emqx_ct_helpers:start_apps([], - fun(emqx) -> - application:set_env(emqx, alarm, [{actions, [log,publish]}, - {size_limit, 1000}, - {validity_period, 1}]), - ok; - (_) -> - ok - end), + emqx_ct_helpers:start_apps([]), + emqx_config:update_config([alarm], #{ + <<"validity_period">> => <<"1s">> + }), Config; init_per_testcase(_, Config) -> emqx_ct_helpers:boot_modules(all), @@ -89,7 +79,7 @@ t_size_limit(_) -> ok = emqx_alarm:activate(b), ok = emqx_alarm:deactivate(b), ?assertNotEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))), - ?assertNotEqual({error, not_found}, get_alarm(a, emqx_alarm:get_alarms(deactivated))), + ?assertNotEqual({error, not_found}, get_alarm(b, emqx_alarm:get_alarms(deactivated))), ok = emqx_alarm:activate(c), ok = emqx_alarm:deactivate(c), ?assertNotEqual({error, not_found}, get_alarm(c, emqx_alarm:get_alarms(deactivated))), diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl index 967791643..ccd98f010 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl @@ -34,7 +34,10 @@ stop(_State) -> handle_update_config({update, Bridge = #{<<"name">> := Name}}, OldConf) -> [Bridge | remove_bridge(Name, OldConf)]; handle_update_config({delete, Name}, OldConf) -> - remove_bridge(Name, OldConf). + remove_bridge(Name, OldConf); +handle_update_config(NewConf, _OldConf) when is_list(NewConf) -> + %% overwrite the entire config! + NewConf. remove_bridge(_Name, undefined) -> []; From 6b3cfd7c5dce815efc8e9453c4ccea4b330e1d88 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 10 Jul 2021 02:51:35 +0200 Subject: [PATCH 130/379] fix(boot): boot exit on config error (#5200) * fix(hocon): fail on hocon command errors call_hocon bash function respects exit code * fix(bin/emax): get dist_port config from hocon after config file refactoring (to hocon format) the grep pattern no longer works * fix(bin/emqx): set -o pipefail * feat(bin/emqx): add a DEBUG option --- apps/emqx/rebar.config | 2 +- bin/emqx | 10 ++++++++-- rebar.config | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 1dd82ceaa..24a0544c8 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -16,7 +16,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} diff --git a/bin/emqx b/bin/emqx index a5f76ac72..f0a53cd45 100755 --- a/bin/emqx +++ b/bin/emqx @@ -3,6 +3,11 @@ # ex: ts=4 sw=4 et set -e +set -o pipefail + +if [ -n "$DEBUG" ]; then + set -x +fi ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)" # shellcheck disable=SC1090 @@ -196,6 +201,7 @@ call_hocon() { export RUNNER_ROOT_DIR export REL_VSN "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" hocon "$@" + return $? } # Run an escript in the node's environment @@ -251,7 +257,7 @@ generate_config() { ARG_KEY=$(echo "$ARG_LINE" | awk '{$NF="";print}') ARG_VALUE=$(echo "$ARG_LINE" | awk '{print $NF}') ## use the key to look up in vm.args file for the value - TMP_ARG_VALUE=$(grep "^$ARG_KEY" "$TMP_ARG_FILE" | awk '{print $NF}') + TMP_ARG_VALUE=$(grep "^$ARG_KEY" "$TMP_ARG_FILE" || true | awk '{print $NF}') ## compare generated (to override) value to original (to be overriden) value if [ "$ARG_VALUE" != "$TMP_ARG_VALUE" ] ; then ## if they are different @@ -365,7 +371,7 @@ if [ -z "$COOKIE" ]; then fi # Support for IPv6 Dist. See: https://github.com/emqtt/emqttd/issues/1460 -PROTO_DIST=$(grep -E '^[ \t]*cluster.proto_dist[ \t]*=[ \t]*' "$RUNNER_ETC_DIR/emqx.conf" 2> /dev/null | tail -1 | awk -F"= " '{print $NF}') +PROTO_DIST="$(call_hocon -s emqx_schema -c "$RUNNER_ETC_DIR"/emqx.conf get cluster.proto_dist | tr -d \")" if [ -z "$PROTO_DIST" ]; then PROTO_DIST_ARG="" else diff --git a/rebar.config b/rebar.config index ae87d37bd..c932362c6 100644 --- a/rebar.config +++ b/rebar.config @@ -61,7 +61,7 @@ , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 , {getopt, "1.0.1"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} ]}. From bcf2256dac06893632883d8d43443500fbb98636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=97=AA=D1=94=CE=BD=CE=B9=CE=B7=20=E1=97=B7=CF=85=D0=BD?= =?UTF-8?q?=CA=9F?= Date: Fri, 9 Jul 2021 22:33:36 -0400 Subject: [PATCH 131/379] feat(helm): add externalIPs to chart (#5201) * fix(helm): add externalIPs to chart * Update values.yaml * Update service.yaml * Update README.md * Update Chart.yaml * do not update helm version --- deploy/charts/emqx/README.md | 1 + deploy/charts/emqx/templates/service.yaml | 3 +++ deploy/charts/emqx/values.yaml | 3 +++ 3 files changed, 7 insertions(+) diff --git a/deploy/charts/emqx/README.md b/deploy/charts/emqx/README.md index 535ccb8e1..446b26f07 100644 --- a/deploy/charts/emqx/README.md +++ b/deploy/charts/emqx/README.md @@ -63,6 +63,7 @@ The following table lists the configurable parameters of the emqx chart and thei | `service.nodePorts.dashboard` | Kubernetes node port for dashboard. |nil| | `service.loadBalancerIP` | loadBalancerIP for Service | nil | | `service.loadBalancerSourceRanges` | Address(es) that are allowed when service is LoadBalancer | [] | +| `service.externalIPs` | ExternalIPs for the service | [] | | `service.annotations` | Service annotations | {}(evaluated as a template)| | `ingress.dashboard.enabled` | Enable ingress for EMQX Dashboard | false | | `ingress.dashboard.path` | Ingress path for EMQX Dashboard | / | diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index b3c0d6017..6e31a97c3 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -21,6 +21,9 @@ spec: {{- if .Values.service.loadBalancerSourceRanges }} loadBalancerSourceRanges: {{- toYaml .Values.service.loadBalancerSourceRanges | nindent 4 }} {{- end }} + {{- if .Values.service.externalIPs }} + externalIPs: {{- toYaml .Values.service.externalIPs | nindent 4 }} + {{- end }} {{- end }} ports: - name: mqtt diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index 3e145aafe..36e9be47a 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -145,6 +145,9 @@ service: ## - 10.10.10.0/24 ## loadBalancerSourceRanges: [] + ## Set the ExternalIPs + ## + externalIPs: [] ## Provide any additional annotations which may be required. Evaluated as a template ## annotations: {} From 4c122d07220d7adb4a266b499f0b6045567d79d3 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sat, 10 Jul 2021 14:29:45 +0800 Subject: [PATCH 132/379] fix(test): update test cases for emqx_channel_SUITE --- apps/emqx/src/emqx_channel.erl | 2 +- apps/emqx/test/emqx_channel_SUITE.erl | 174 +++++++++++++++++++++++--- 2 files changed, 159 insertions(+), 17 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 9b43b3bf3..971b4d5d4 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -207,7 +207,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, Peercert = maps:get(peercert, ConnInfo, undefined), Protocol = maps:get(protocol, ConnInfo, mqtt), MountPoint = case get_mqtt_conf(Zone, Listener, mountpoint) of - "" -> undefined; + <<>> -> undefined; MP -> MP end, QuotaPolicy = emqx_config:get_listener_conf(Zone, Listener, [rate_limit, quota]), diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 09ac7a683..2b2b628ad 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -24,7 +24,149 @@ -include_lib("eunit/include/eunit.hrl"). -all() -> emqx_ct:all(?MODULE). +all() -> + emqx_ct:all(?MODULE). + +mqtt_conf() -> + #{await_rel_timeout => 300, + idle_timeout => 15000, + ignore_loop_deliver => false, + keepalive_backoff => 0.75, + max_awaiting_rel => 100, + max_clientid_len => 65535, + max_inflight => 32, + max_mqueue_len => 1000, + max_packet_size => 1048576, + max_qos_allowed => 2, + max_subscriptions => infinity, + max_topic_alias => 65535, + max_topic_levels => 65535, + mountpoint => <<>>, + mqueue_default_priority => highest, + mqueue_priorities => [], + mqueue_store_qos0 => true, + peer_cert_as_clientid => disabled, + peer_cert_as_username => disabled, + response_information => [], + retain_available => true, + retry_interval => 30, + server_keepalive => disabled, + session_expiry_interval => 7200, + shared_subscription => true, + strict_mode => false, + upgrade_qos => false, + use_username_as_clientid => false, + wildcard_subscription => true}. + +listener_mqtt_tcp_conf() -> + #{acceptors => 16, + access_rules => ["allow all"], + bind => {{0,0,0,0},1883}, + max_connections => 1024000, + proxy_protocol => false, + proxy_protocol_timeout => 3000, + rate_limit => + #{conn_bytes_in => + ["100KB","10s"], + conn_messages_in => + ["100","10s"], + max_conn_rate => 1000, + quota => + #{conn_messages_routing => infinity, + overall_messages_routing => infinity}}, + tcp => + #{active_n => 100, + backlog => 1024, + buffer => 4096, + high_watermark => 1048576, + send_timeout => 15000, + send_timeout_close => + true}, + type => tcp}. + +listener_mqtt_ws_conf() -> + #{acceptors => 16, + access_rules => ["allow all"], + bind => {{0,0,0,0},8083}, + max_connections => 1024000, + proxy_protocol => false, + proxy_protocol_timeout => 3000, + rate_limit => + #{conn_bytes_in => + ["100KB","10s"], + conn_messages_in => + ["100","10s"], + max_conn_rate => 1000, + quota => + #{conn_messages_routing => infinity, + overall_messages_routing => infinity}}, + tcp => + #{active_n => 100, + backlog => 1024, + buffer => 4096, + high_watermark => 1048576, + send_timeout => 15000, + send_timeout_close => + true}, + type => ws, + websocket => + #{allow_origin_absence => + true, + check_origin_enable => + false, + check_origins => [], + compress => false, + deflate_opts => + #{client_max_window_bits => + 15, + mem_level => 8, + server_max_window_bits => + 15}, + fail_if_no_subprotocol => + true, + idle_timeout => 86400000, + max_frame_size => infinity, + mqtt_path => "/mqtt", + mqtt_piggyback => multiple, + proxy_address_header => + "x-forwarded-for", + proxy_port_header => + "x-forwarded-port", + supported_subprotocols => + ["mqtt","mqtt-v3", + "mqtt-v3.1.1", + "mqtt-v5"]}}. + +default_zone_conf() -> + #{zones => + #{default => + #{ acl => #{ + cache => #{enable => true,max_size => 32, ttl => 60000}, + deny_action => ignore, + enable => false + }, + auth => #{enable => false}, + overall_max_connections => infinity, + stats => #{enable => true}, + conn_congestion => + #{enable_alarm => true, min_alarm_sustain_duration => 60000}, + flapping_detect => + #{ban_time => 300000,enable => true, + max_count => 15,window_time => 60000}, + force_gc => + #{bytes => 16777216,count => 16000, + enable => true}, + force_shutdown => + #{enable => true, + max_heap_size => 4194304, + max_message_queue_len => 1000}, + mqtt => mqtt_conf(), + listeners => + #{mqtt_tcp => listener_mqtt_tcp_conf(), + mqtt_ws => listener_mqtt_ws_conf()} + } + } + }. %%-------------------------------------------------------------------- %% CT Callbacks @@ -50,6 +192,9 @@ init_per_suite(Config) -> ok = meck:new(emqx_metrics, [passthrough, no_history, no_link]), ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end), ok = meck:expect(emqx_metrics, inc, fun(_, _) -> ok end), + %% Ban + meck:new(emqx_banned, [passthrough, no_history, no_link]), + ok = meck:expect(emqx_banned, check, fun(_ConnInfo) -> false end), Config. end_per_suite(_Config) -> @@ -62,11 +207,10 @@ end_per_suite(_Config) -> ]). init_per_testcase(_TestCase, Config) -> - meck:new(emqx_zone, [passthrough, no_history, no_link]), + emqx_config:put(default_zone_conf()), Config. end_per_testcase(_TestCase, Config) -> - meck:unload([emqx_zone]), Config. %%-------------------------------------------------------------------- @@ -241,7 +385,7 @@ t_bad_receive_maximum(_) -> fun(true, _ClientInfo, _ConnInfo) -> {ok, #{session => session(), present => false}} end), - ok = meck:expect(emqx_zone, response_information, fun(_) -> test end), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, response_information], test), C1 = channel(#{conn_state => idle}), {shutdown, protocol_error, _, _} = emqx_channel:handle_in( @@ -254,8 +398,8 @@ t_override_client_receive_maximum(_) -> fun(true, _ClientInfo, _ConnInfo) -> {ok, #{session => session(), present => false}} end), - ok = meck:expect(emqx_zone, response_information, fun(_) -> test end), - ok = meck:expect(emqx_zone, max_inflight, fun(_) -> 0 end), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, response_information], test), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_inflight], 0), C1 = channel(#{conn_state => idle}), ClientCapacity = 2, {ok, [{event, connected}, _ConnAck], C2} = @@ -506,7 +650,7 @@ t_handle_out_connack_response_information(_) -> fun(true, _ClientInfo, _ConnInfo) -> {ok, #{session => session(), present => false}} end), - ok = meck:expect(emqx_zone, response_information, fun(_) -> test end), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, response_information], test), IdleChannel = channel(#{conn_state => idle}), {ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, #{'Response-Information' := test})}], @@ -520,7 +664,7 @@ t_handle_out_connack_not_response_information(_) -> fun(true, _ClientInfo, _ConnInfo) -> {ok, #{session => session(), present => false}} end), - ok = meck:expect(emqx_zone, response_information, fun(_) -> test end), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, response_information], test), IdleChannel = channel(#{conn_state => idle}), {ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, AckProps)}], _} = emqx_channel:handle_in( @@ -660,9 +804,6 @@ t_enrich_conninfo(_) -> t_enrich_client(_) -> {ok, _ConnPkt, _Chan} = emqx_channel:enrich_client(connpkt(), channel()). -t_check_banned(_) -> - ok = emqx_channel:check_banned(connpkt(), channel()). - t_auth_connect(_) -> {ok, _Chan} = emqx_channel:auth_connect(connpkt(), channel()). @@ -709,7 +850,7 @@ t_packing_alias(_) -> channel())). t_check_pub_acl(_) -> - ok = meck:expect(emqx_zone, enable_acl, fun(_) -> true end), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), ok = emqx_channel:check_pub_acl(Publish, channel()). @@ -719,7 +860,7 @@ t_check_pub_alias(_) -> ok = emqx_channel:check_pub_alias(#mqtt_packet{variable = Publish}, Channel). t_check_sub_acls(_) -> - ok = meck:expect(emqx_zone, enable_acl, fun(_) -> true end), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), TopicFilter = {<<"t">>, ?DEFAULT_SUBOPTS}, [{TopicFilter, 0}] = emqx_channel:check_sub_acls([TopicFilter], channel()). @@ -763,7 +904,7 @@ t_ws_cookie_init(_) -> conn_mod => emqx_ws_connection, ws_cookie => WsCookie }, - Channel = emqx_channel:init(ConnInfo, [{zone, zone}]), + Channel = emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), ?assertMatch(#{ws_cookie := WsCookie}, emqx_channel:info(clientinfo, Channel)). %%-------------------------------------------------------------------- @@ -788,7 +929,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, [{zone, zone}]), + emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), maps:merge(#{clientinfo => clientinfo(), session => session(), conn_state => connected @@ -796,7 +937,8 @@ channel(InitFields) -> clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> - maps:merge(#{zone => zone, + maps:merge(#{zone => default, + listener => mqtt_tcp, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, From 042ff2e0d7b85654c3fdf7a91c88379bb4458ce8 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sat, 10 Jul 2021 18:01:45 +0800 Subject: [PATCH 133/379] fix(test): update test cases for emqx_connection_SUITE --- apps/emqx/src/emqx_misc.erl | 2 +- apps/emqx/src/emqx_session.erl | 35 +++++++++--------- apps/emqx/test/emqx_channel_SUITE.erl | 7 ++-- apps/emqx/test/emqx_connection_SUITE.erl | 45 +++++++++++------------- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/apps/emqx/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl index 640ed5704..d45b6f7ce 100644 --- a/apps/emqx/src/emqx_misc.erl +++ b/apps/emqx/src/emqx_misc.erl @@ -198,7 +198,7 @@ check_oom(Policy) -> -spec(check_oom(pid(), emqx_types:oom_policy()) -> ok | {shutdown, term()}). check_oom(_Pid, #{enable := false}) -> ok; -check_oom(Pid, #{message_queue_len := MaxQLen, +check_oom(Pid, #{max_message_queue_len := MaxQLen, max_heap_size := MaxHeapSize}) -> case process_info(Pid, [message_queue_len, total_heap_size]) of undefined -> ok; diff --git a/apps/emqx/src/emqx_session.erl b/apps/emqx/src/emqx_session.erl index 9463345d4..995aff713 100644 --- a/apps/emqx/src/emqx_session.erl +++ b/apps/emqx/src/emqx_session.erl @@ -92,8 +92,6 @@ -export_type([session/0]). --import(emqx_zone, [get_env/3]). - -record(session, { %% Client’s Subscriptions. subscriptions :: map(), @@ -159,27 +157,28 @@ %%-------------------------------------------------------------------- -spec(init(emqx_types:clientinfo(), emqx_types:conninfo()) -> session()). -init(#{zone := Zone}, #{receive_maximum := MaxInflight}) -> - #session{max_subscriptions = get_env(Zone, max_subscriptions, 0), +init(#{zone := Zone, listener := Listener}, #{receive_maximum := MaxInflight}) -> + #session{max_subscriptions = get_conf(Zone, Listener, max_subscriptions), subscriptions = #{}, - upgrade_qos = get_env(Zone, upgrade_qos, false), + upgrade_qos = get_conf(Zone, Listener, upgrade_qos), inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone), + mqueue = init_mqueue(Zone, Listener), next_pkt_id = 1, - retry_interval = timer:seconds(get_env(Zone, retry_interval, 0)), + retry_interval = timer:seconds(get_conf(Zone, Listener, retry_interval)), awaiting_rel = #{}, - max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100), - await_rel_timeout = timer:seconds(get_env(Zone, await_rel_timeout, 300)), + max_awaiting_rel = get_conf(Zone, Listener, max_awaiting_rel), + await_rel_timeout = timer:seconds(get_conf(Zone, Listener, await_rel_timeout)), created_at = erlang:system_time(millisecond) }. %% @private init mq -init_mqueue(Zone) -> - emqx_mqueue:init(#{max_len => get_env(Zone, max_mqueue_len, 1000), - store_qos0 => get_env(Zone, mqueue_store_qos0, true), - priorities => get_env(Zone, mqueue_priorities, none), - default_priority => get_env(Zone, mqueue_default_priority, lowest) - }). +init_mqueue(Zone, Listener) -> + emqx_mqueue:init(#{ + max_len => get_conf(Zone, Listener, max_mqueue_len), + store_qos0 => get_conf(Zone, Listener, mqueue_store_qos0), + priorities => get_conf(Zone, Listener, mqueue_priorities), + default_priority => get_conf(Zone, Listener, mqueue_default_priority) + }). %%-------------------------------------------------------------------- %% Info, Stats @@ -253,7 +252,7 @@ subscribe(ClientInfo = #{clientid := ClientId}, TopicFilter, SubOpts, end. -compile({inline, [is_subscriptions_full/1]}). -is_subscriptions_full(#session{max_subscriptions = 0}) -> +is_subscriptions_full(#session{max_subscriptions = infinity}) -> false; is_subscriptions_full(#session{subscriptions = Subs, max_subscriptions = MaxLimit}) -> @@ -302,7 +301,7 @@ publish(_PacketId, Msg, Session) -> {ok, emqx_broker:publish(Msg), Session}. -compile({inline, [is_awaiting_full/1]}). -is_awaiting_full(#session{max_awaiting_rel = 0}) -> +is_awaiting_full(#session{max_awaiting_rel = infinity}) -> false; is_awaiting_full(#session{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLimit}) -> @@ -697,3 +696,5 @@ set_field(Name, Value, Session) -> Pos = emqx_misc:index_of(Name, record_info(fields, session)), setelement(Pos+1, Session, Value). +get_conf(Zone, Listener, Key) -> + emqx_config:get_listener_conf(Zone, Listener, [mqtt, Key]). diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 2b2b628ad..4d108bb94 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -151,7 +151,7 @@ default_zone_conf() -> conn_congestion => #{enable_alarm => true, min_alarm_sustain_duration => 60000}, flapping_detect => - #{ban_time => 300000,enable => true, + #{ban_time => 300000,enable => false, max_count => 15,window_time => 60000}, force_gc => #{bytes => 16777216,count => 16000, @@ -168,6 +168,9 @@ default_zone_conf() -> } }. +set_default_zone_conf() -> + emqx_config:put(default_zone_conf()). + %%-------------------------------------------------------------------- %% CT Callbacks %%-------------------------------------------------------------------- @@ -207,7 +210,7 @@ end_per_suite(_Config) -> ]). init_per_testcase(_TestCase, Config) -> - emqx_config:put(default_zone_conf()), + set_default_zone_conf(), Config. end_per_testcase(_TestCase, Config) -> diff --git a/apps/emqx/test/emqx_connection_SUITE.erl b/apps/emqx/test/emqx_connection_SUITE.erl index a6b2b614a..3e6281fc0 100644 --- a/apps/emqx/test/emqx_connection_SUITE.erl +++ b/apps/emqx/test/emqx_connection_SUITE.erl @@ -57,6 +57,7 @@ init_per_suite(Config) -> ok = meck:expect(emqx_alarm, deactivate, fun(_) -> ok end), ok = meck:expect(emqx_alarm, deactivate, fun(_, _) -> ok end), + emqx_channel_SUITE:set_default_zone_conf(), Config. end_per_suite(_Config) -> @@ -120,14 +121,13 @@ t_info(_) -> end end), #{sockinfo := SockInfo} = emqx_connection:info(CPid), - ?assertMatch(#{active_n := 100, - peername := {{127,0,0,1},3456}, + ?assertMatch(#{ peername := {{127,0,0,1},3456}, sockname := {{127,0,0,1},1883}, sockstate := idle, socktype := tcp}, SockInfo). t_info_limiter(_) -> - St = st(#{limiter => emqx_limiter:init(external, [])}), + St = st(#{limiter => emqx_limiter:init(default, [])}), ?assertEqual(undefined, emqx_connection:info(limiter, St)). t_stats(_) -> @@ -219,8 +219,10 @@ t_handle_msg_deliver(_) -> t_handle_msg_inet_reply(_) -> ok = meck:expect(emqx_pd, get_counter, fun(_) -> 10 end), - ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st(#{active_n => 0}))), - ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st(#{active_n => 100}))), + emqx_config:put_listener_conf(default, mqtt_tcp, [tcp, active_n], 0), + ?assertMatch({ok, _St}, handle_msg({inet_reply, for_testing, ok}, st())), + emqx_config:put_listener_conf(default, mqtt_tcp, [tcp, active_n], 100), + ?assertEqual(ok, handle_msg({inet_reply, for_testing, ok}, st())), ?assertMatch({stop, {shutdown, for_testing}, _St}, handle_msg({inet_reply, for_testing, {error, for_testing}}, st())). @@ -331,12 +333,12 @@ t_ensure_rate_limit(_) -> ?assertEqual(undefined, emqx_connection:info(limiter, State)), ok = meck:expect(emqx_limiter, check, - fun(_, _) -> {ok, emqx_limiter:init(external, [])} end), + fun(_, _) -> {ok, emqx_limiter:init(default, [])} end), State1 = emqx_connection:ensure_rate_limit(#{}, st(#{limiter => #{}})), ?assertEqual(undefined, emqx_connection:info(limiter, State1)), ok = meck:expect(emqx_limiter, check, - fun(_, _) -> {pause, 3000, emqx_limiter:init(external, [])} end), + fun(_, _) -> {pause, 3000, emqx_limiter:init(default, [])} end), State2 = emqx_connection:ensure_rate_limit(#{}, st(#{limiter => #{}})), ?assertEqual(undefined, emqx_connection:info(limiter, State2)), ?assertEqual(blocked, emqx_connection:info(sockstate, State2)). @@ -386,8 +388,7 @@ t_start_link_exit_on_activate(_) -> t_get_conn_info(_) -> with_conn(fun(CPid) -> #{sockinfo := SockInfo} = emqx_connection:info(CPid), - ?assertEqual(#{active_n => 100, - peername => {{127,0,0,1},3456}, + ?assertEqual(#{peername => {{127,0,0,1},3456}, sockname => {{127,0,0,1},1883}, sockstate => running, socktype => tcp @@ -397,16 +398,12 @@ t_get_conn_info(_) -> t_oom_shutdown(init, Config) -> ok = snabbkaffe:start_trace(), ok = meck:new(emqx_misc, [non_strict, passthrough, no_history, no_link]), - ok = meck:new(emqx_zone, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_zone, oom_policy, - fun(_Zone) -> #{message_queue_len => 10, max_heap_size => 8000000} end), meck:expect(emqx_misc, check_oom, fun(_) -> {shutdown, "fake_oom"} end), Config; t_oom_shutdown('end', _Config) -> snabbkaffe:stop(), meck:unload(emqx_misc), - meck:unload(emqx_zone), ok. t_oom_shutdown(_) -> @@ -455,13 +452,11 @@ exit_on_activate_error(SockErr, Reason) -> with_conn(TestFun) -> with_conn(TestFun, #{trap_exit => false}). -with_conn(TestFun, Options) when is_map(Options) -> - with_conn(TestFun, maps:to_list(Options)); - -with_conn(TestFun, Options) -> - TrapExit = proplists:get_value(trap_exit, Options, false), +with_conn(TestFun, Opts) when is_map(Opts) -> + TrapExit = maps:get(trap_exit, Opts, false), process_flag(trap_exit, TrapExit), - {ok, CPid} = emqx_connection:start_link(emqx_transport, sock, Options), + {ok, CPid} = emqx_connection:start_link(emqx_transport, sock, + maps:merge(Opts, #{zone => default, listener => mqtt_tcp})), TestFun(CPid), TrapExit orelse emqx_connection:stop(CPid), ok. @@ -483,7 +478,8 @@ st() -> st(#{}, #{}). st(InitFields) when is_map(InitFields) -> st(InitFields, #{}). st(InitFields, ChannelFields) when is_map(InitFields) -> - St = emqx_connection:init_state(emqx_transport, sock, [#{zone => external}]), + St = emqx_connection:init_state(emqx_transport, sock, #{zone => default, + listener => mqtt_tcp}), maps:fold(fun(N, V, S) -> emqx_connection:set_field(N, V, S) end, emqx_connection:set_field(channel, channel(ChannelFields), St), InitFields @@ -503,7 +499,8 @@ channel(InitFields) -> receive_maximum => 100, expiry_interval => 0 }, - ClientInfo = #{zone => zone, + ClientInfo = #{zone => default, + listener => mqtt_tcp, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -512,13 +509,13 @@ channel(InitFields) -> peercert => undefined, mountpoint => undefined }, - Session = emqx_session:init(#{zone => external}, + Session = emqx_session:init(#{zone => default, listener => mqtt_tcp}, #{receive_maximum => 0} ), maps:fold(fun(Field, Value, Channel) -> - emqx_channel:set_field(Field, Value, Channel) + emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, [{zone, zone}]), + emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), maps:merge(#{clientinfo => ClientInfo, session => Session, conn_state => connected From 54c776ebdf6ee23b5311c1237028bb237e4fd020 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 12 Jul 2021 13:46:38 +0800 Subject: [PATCH 134/379] fix(emqx-edge): fix sed error when emqx-edge start --- bin/emqx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/emqx b/bin/emqx index f0a53cd45..0e019a4fc 100755 --- a/bin/emqx +++ b/bin/emqx @@ -263,7 +263,7 @@ generate_config() { ## if they are different if [ -n "$TMP_ARG_VALUE" ]; then ## if the old value is present, replace it with generated value - sh -c "$SED_REPLACE 's/^$ARG_KEY.*$/$ARG_LINE/' $TMP_ARG_FILE" + sh -c "$SED_REPLACE 's|^$ARG_KEY.*$|$ARG_LINE|' $TMP_ARG_FILE" else ## otherwise append generated value to the end echo "$ARG_LINE" >> "$TMP_ARG_FILE" From df92a600858b081121d6e60607b422025e8915c3 Mon Sep 17 00:00:00 2001 From: tigercl Date: Mon, 12 Jul 2021 15:35:06 +0800 Subject: [PATCH 135/379] feat(http connector): support http connector (#5192) - support http connector - support http authn --- .../src/simple_authn/emqx_authn_http.erl | 299 ++++++++++++++++++ .../src/simple_authn/emqx_authn_jwt.erl | 10 +- .../src/simple_authn/emqx_authn_mysql.erl | 9 +- .../src/emqx_connector_http.erl | 213 +++++++++++++ rebar.config | 2 +- 5 files changed, 525 insertions(+), 8 deletions(-) create mode 100644 apps/emqx_authn/src/simple_authn/emqx_authn_http.erl create mode 100644 apps/emqx_connector/src/emqx_connector_http.erl diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl new file mode 100644 index 000000000..71929b944 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -0,0 +1,299 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_http). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + , validations/0 + ]). + +-type accept() :: 'application/json' | 'application/x-www-form-urlencoded'. +-type content_type() :: accept(). + +-reflect_type([ accept/0 + , content_type/0 + ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [""]. + +fields("") -> + [ {config, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) + ])}} + ]; + +fields(get) -> + [ {method, #{type => get, + default => get}} + ] ++ common_fields(); + +fields(post) -> + [ {method, #{type => post, + default => get}} + , {content_type, fun content_type/1} + ] ++ common_fields(). + +common_fields() -> + [ {url, fun url/1} + , {accept, fun accept/1} + , {headers, fun headers/1} + , {form_data, fun form_data/1} + , {request_timeout, fun request_timeout/1} + ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)). + +validations() -> + [ {check_ssl_opts, fun check_ssl_opts/1} ]. + +url(type) -> binary(); +url(nullable) -> false; +url(validate) -> [fun check_url/1]; +url(_) -> undefined. + +accept(type) -> accept(); +accept(default) -> 'application/json'; +accept(_) -> undefined. + +content_type(type) -> content_type(); +content_type(default) -> 'application/json'; +content_type(_) -> undefined. + +headers(type) -> list(); +headers(default) -> []; +headers(_) -> undefined. + +form_data(type) -> binary(); +form_data(nullable) -> false; +form_data(validate) -> [fun check_form_data/1]; +form_data(_) -> undefined. + +request_timeout(type) -> non_neg_integer(); +request_timeout(default) -> 5000; +request_timeout(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, AuthenticatorName, + #{method := Method, + url := URL, + accept := Accept, + content_type := ContentType, + headers := Headers, + form_data := FormData, + request_timeout := RequestTimeout} = Config) -> + NHeaders = maps:merge(#{<<"accept">> => atom_to_binary(Accept, utf8), + <<"content-type">> => atom_to_binary(ContentType, utf8)}, Headers), + NFormData = preprocess_form_data(FormData), + #{path := Path, + query := Query} = URIMap = parse_url(URL), + BaseURL = generate_base_url(URIMap), + State = #{method => Method, + path => Path, + base_query => cow_qs:parse_qs(Query), + accept => Accept, + content_type => ContentType, + headers => NHeaders, + form_data => NFormData, + request_timeout => RequestTimeout}, + ResourceID = <>, + case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url := BaseURL}) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_http, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(ClientInfo, #{resource_id := ResourceID, + method := Method, + request_timeout := RequestTimeout} = State) -> + Request = generate_request(ClientInfo, State), + case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of + {ok, 204, _Headers} -> ok; + {ok, 200, Headers, Body} -> + ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), + case safely_parse_body(ContentType, Body) of + {ok, _NBody} -> + %% TODO: Return by user property + ok; + {error, Reason} -> + {stop, Reason} + end; + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +check_url(URL) -> + case emqx_http_lib:uri_parse(URL) of + {ok, _} -> true; + {error, _} -> false + end. + +check_form_data(FormData) -> + KVs = binary:split(FormData, [<<"&">>], [global]), + case false =:= lists:any(fun(T) -> T =:= <<>> end, KVs) of + true -> + NKVs = [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs], + false =:= + lists:any(fun({K, V}) -> + K =:= <<>> orelse V =:= <<>>; + (_) -> + true + end, NKVs); + false -> + false + end. + +check_ssl_opts(Conf) -> + URL = hocon_schema:get_value("url", Conf), + {ok, #{scheme := Scheme}} = emqx_http_lib:uri_parse(URL), + SSLOpts = hocon_schema:get_value("ssl_opts", Conf), + case {Scheme, SSLOpts} of + {http, undefined} -> true; + {http, _} -> false; + {https, undefined} -> false; + {https, _} -> true + end. + +preprocess_form_data(FormData) -> + KVs = binary:split(FormData, [<<"&">>], [global]), + [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs]. + +parse_url(URL) -> + {ok, URIMap} = emqx_http_lib:uri_parse(URL), + case maps:get(query, URIMap, undefined) of + undefined -> + URIMap#{query => ""}; + _ -> + URIMap + end. + +generate_base_url(#{scheme := Scheme, + host := Host, + port := Port}) -> + iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])). + +generate_request(ClientInfo, #{method := Method, + path := Path, + base_query := BaseQuery, + content_type := ContentType, + headers := Headers, + form_data := FormData0}) -> + FormData = replace_placeholders(FormData0, ClientInfo), + case Method of + get -> + NPath = append_query(Path, BaseQuery ++ FormData), + {NPath, Headers}; + post -> + NPath = append_query(Path, BaseQuery), + Body = serialize_body(ContentType, FormData), + {NPath, Headers, Body} + end. + +replace_placeholders(FormData0, ClientInfo) -> + FormData = lists:map(fun({K, V0}) -> + case replace_placeholder(V0, ClientInfo) of + undefined -> {K, undefined}; + V -> {K, bin(V)} + end + end, FormData0), + lists:filter(fun({_, V}) -> + V =/= undefined + end, FormData). + +replace_placeholder(<<"${mqtt-username}">>, ClientInfo) -> + maps:get(username, ClientInfo, undefined); +replace_placeholder(<<"${mqtt-clientid}">>, ClientInfo) -> + maps:get(clientid, ClientInfo, undefined); +replace_placeholder(<<"${ip-address}">>, ClientInfo) -> + maps:get(peerhost, ClientInfo, undefined); +replace_placeholder(<<"${cert-subject}">>, ClientInfo) -> + maps:get(dn, ClientInfo, undefined); +replace_placeholder(<<"${cert-common-name}">>, ClientInfo) -> + maps:get(cn, ClientInfo, undefined); +replace_placeholder(Constant, _) -> + Constant. + +append_query(Path, []) -> + Path; +append_query(Path, Query) -> + Path ++ "?" ++ binary_to_list(qs(Query)). + +qs(KVs) -> + qs(KVs, []). + +qs([], Acc) -> + <<$&, Qs/binary>> = iolist_to_binary(lists:reverse(Acc)), + Qs; +qs([{K, V} | More], Acc) -> + qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]). + +serialize_body('application/json', FormData) -> + emqx_json:encode(FormData); +serialize_body('application/x-www-form-urlencoded', FormData) -> + qs(FormData). + +safely_parse_body(ContentType, Body) -> + try parse_body(ContentType, Body) of + Result -> Result + catch + _Class:_Reason -> + {error, invalid_body} + end. + +parse_body(<<"application/json">>, Body) -> + {ok, emqx_json:decode(Body)}; +parse_body(<<"application/x-www-form-urlencoded">>, Body) -> + {ok, cow_qs:parse_qs(Body)}; +parse_body(ContentType, _) -> + {error, {unsupported_content_type, ContentType}}. + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index f737d5168..8fae45ff4 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -294,12 +294,16 @@ do_verify_claims(Claims, [{Name, Value} | More]) -> {error, {claims, {Name, Value0}}} end. -check_verify_claims([]) -> +check_verify_claims(Conf) -> + Claims = hocon_schema:get_value("verify_claims", Conf), + do_check_verify_claims(Claims). + +do_check_verify_claims([]) -> false; -check_verify_claims([{Name, Expected} | More]) -> +do_check_verify_claims([{Name, Expected} | More]) -> check_claim_name(Name) andalso check_claim_expected(Expected) andalso - check_verify_claims(More). + do_check_verify_claims(More). check_claim_name(exp) -> false; diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index 3b5384d9c..cc4445eaf 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -58,10 +58,11 @@ query_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ -create(ChainID, ServiceName, #{query := Query0, - password_hash_algorithm := Algorithm} = Config) -> +create(ChainID, AuthenticatorName, + #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> {Query, PlaceHolders} = parse_query(Query0), - ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, AuthenticatorName])), State = #{query => Query, placeholders => PlaceHolders, password_hash_algorithm => Algorithm}, @@ -74,7 +75,7 @@ create(ChainID, ServiceName, #{query := Query0, {error, Reason} end. -update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> +update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) -> case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl new file mode 100644 index 000000000..807243038 --- /dev/null +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -0,0 +1,213 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_connector_http). + +-include("emqx_connector.hrl"). + +-include_lib("typerefl/include/types.hrl"). +-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). + +%% callbacks of behaviour emqx_resource +-export([ on_start/2 + , on_stop/2 + , on_query/4 + , on_health_check/2 + ]). + +-export([ structs/0 + , fields/1 + , validations/0]). + +-type connect_timeout() :: non_neg_integer() | infinity. +-type pool_type() :: random | hash. + +-reflect_type([ connect_timeout/0 + , pool_type/0 + ]). + +%%===================================================================== +%% Hocon schema +structs() -> [""]. + +fields("") -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]; + +fields(config) -> + [ {base_url, fun base_url/1} + , {connect_timeout, fun connect_timeout/1} + , {max_retries, fun max_retries/1} + , {retry_interval, fun retry_interval/1} + , {keepalive, fun keepalive/1} + , {pool_type, fun pool_type/1} + , {pool_size, fun pool_size/1} + , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), + nullable => true}} + ]; + +fields(ssl_opts) -> + [ {cacertfile, fun cacertfile/1} + , {keyfile, fun keyfile/1} + , {certfile, fun certfile/1} + , {verify, fun verify/1} + ]. + +validations() -> + [ {check_ssl_opts, fun check_ssl_opts/1} ]. + +base_url(type) -> binary(); +base_url(nullable) -> false; +base_url(validate) -> [fun check_base_url/1]; +base_url(_) -> undefined. + +connect_timeout(type) -> connect_timeout(); +connect_timeout(default) -> 5000; +connect_timeout(_) -> undefined. + +max_retries(type) -> non_neg_integer(); +max_retries(default) -> 5; +max_retries(_) -> undefined. + +retry_interval(type) -> non_neg_integer(); +retry_interval(default) -> 1000; +retry_interval(_) -> undefined. + +keepalive(type) -> non_neg_integer(); +keepalive(default) -> 5000; +keepalive(_) -> undefined. + +pool_type(type) -> pool_type(); +pool_type(default) -> random; +pool_type(_) -> undefined. + +pool_size(type) -> non_neg_integer(); +pool_size(default) -> 8; +pool_size(_) -> undefined. + +cacertfile(type) -> string(); +cacertfile(nullable) -> true; +cacertfile(_) -> undefined. + +keyfile(type) -> string(); +keyfile(nullable) -> true; +keyfile(_) -> undefined. + +certfile(type) -> string(); +certfile(nullable) -> false; +certfile(_) -> undefined. + +verify(type) -> boolean(); +verify(default) -> false; +verify(_) -> undefined. + +%% =================================================================== +on_start(InstId, #{url := URL, + connect_timeout := ConnectTimeout, + max_retries := MaxRetries, + retry_interval := RetryInterval, + keepalive := Keepalive, + pool_type := PoolType, + pool_size := PoolSize} = Config) -> + logger:info("starting http connector: ~p, config: ~p", [InstId, Config]), + {ok, #{scheme := Scheme, + host := Host, + port := Port, + path := BasePath}} = emqx_http_lib:uri_parse(URL), + {Transport, TransportOpts} = case Scheme of + http -> + {tcp, []}; + https -> + SSLOpts = emqx_plugin_libs_ssl:save_files_return_opts( + maps:get(ssl_opts, Config), "connectors", InstId), + {tls, SSLOpts} + end, + NTransportOpts = emqx_misc:ipv6_probe(TransportOpts), + PoolOpts = [ {host, Host} + , {port, Port} + , {connect_timeout, ConnectTimeout} + , {retry, MaxRetries} + , {retry_timeout, RetryInterval} + , {keepalive, Keepalive} + , {pool_type, PoolType} + , {pool_size, PoolSize} + , {transport, Transport} + , {transport, NTransportOpts}], + PoolName = emqx_plugin_libs_pool:pool_name(InstId), + {ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts), + {ok, #{pool_name => PoolName, + host => Host, + port => Port, + base_path => BasePath}}. + +on_stop(InstId, #{pool_name := PoolName}) -> + logger:info("stopping http connector: ~p", [InstId]), + ehttpc_sup:stop_pool(PoolName). + +on_query(InstId, {Method, Request}, AfterQuery, State) -> + on_query(InstId, {undefined, Method, Request, 5000}, AfterQuery, State); +on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) -> + on_query(InstId, {undefined, Method, Request, Timeout}, AfterQuery, State); +on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery, #{pool_name := PoolName, + base_path := BasePath} = State) -> + logger:debug("http connector ~p received request: ~p, at state: ~p", [InstId, Request, State]), + NRequest = update_path(BasePath, Request), + case Result = ehttpc:request(case KeyOrNum of + undefined -> PoolName; + _ -> {PoolName, KeyOrNum} + end, Method, NRequest, Timeout) of + {error, Reason} -> + logger:debug("http connector ~p do reqeust failed, sql: ~p, reason: ~p", [InstId, NRequest, Reason]), + emqx_resource:query_failed(AfterQuery); + _ -> + emqx_resource:query_success(AfterQuery) + end, + Result. + +on_health_check(_InstId, #{server := {Host, Port}} = State) -> + case gen_tcp:connect(Host, Port, emqx_misc:ipv6_probe([]), 3000) of + {ok, Sock} -> + gen_tcp:close(Sock), + {ok, State}; + {error, _Reason} -> + {error, test_query_failed, State} + end. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +check_base_url(URL) -> + case emqx_http_lib:uri_parse(URL) of + {error, _} -> false; + {ok, #{query := _}} -> false; + _ -> true + end. + +check_ssl_opts(Conf) -> + URL = hocon_schema:get_value("url", Conf), + {ok, #{scheme := Scheme}} = emqx_http_lib:uri_parse(URL), + SSLOpts = hocon_schema:get_value("ssl_opts", Conf), + case {Scheme, SSLOpts} of + {http, undefined} -> true; + {http, _} -> false; + {https, undefined} -> false; + {https, _} -> true + end. + +update_path(BasePath, {Path, Headers}) -> + {filename:join(BasePath, Path), Headers}; +update_path(BasePath, {Path, Headers, Body}) -> + {filename:join(BasePath, Path), Headers, Body}. \ No newline at end of file diff --git a/rebar.config b/rebar.config index abdc316b7..aa6b2dd4c 100644 --- a/rebar.config +++ b/rebar.config @@ -43,7 +43,7 @@ {deps, [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps - , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.6"}}} + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.7"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} From ea68beeef6b340f2960006f40056b615269f156a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 12 Jul 2021 15:41:43 +0800 Subject: [PATCH 136/379] fix(test): update test cases for emqx_client_SUITE --- apps/emqx/test/emqx_client_SUITE.erl | 20 +++++++------------- apps/emqx/test/emqx_cm_SUITE.erl | 4 ++-- apps/emqx/test/emqx_misc_SUITE.erl | 5 +++-- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 73a92024b..552ad307c 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -78,17 +78,14 @@ groups() -> init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), - emqx_ct_helpers:start_apps([], fun set_special_confs/1), + emqx_ct_helpers:start_apps([]), + emqx_config:put_listener_conf(default, mqtt_ssl, [ssl, verify], verify_peer), + emqx_listeners:restart_listener('default:mqtt_ssl'), Config. end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -set_special_confs(emqx) -> - emqx_ct_helpers:change_emqx_opts(ssl_twoway, [{peer_cert_as_username, cn}]); -set_special_confs(_) -> - ok. - %%-------------------------------------------------------------------- %% Test cases for MQTT v3 %%-------------------------------------------------------------------- @@ -104,8 +101,7 @@ t_basic_v4(_Config) -> t_basic([{proto_ver, v4}]). t_cm(_) -> - IdleTimeout = emqx_zone:get_env(external, idle_timeout, 30000), - emqx_zone:set_env(external, idle_timeout, 1000), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, idle_timeout], 1000), ClientId = <<"myclient">>, {ok, C} = emqtt:start_link([{clientid, ClientId}]), {ok, _} = emqtt:connect(C), @@ -115,7 +111,7 @@ t_cm(_) -> ct:sleep(1200), Stats = emqx_cm:get_chan_stats(ClientId), ?assertEqual(1, proplists:get_value(subscriptions_cnt, Stats)), - emqx_zone:set_env(external, idle_timeout, IdleTimeout). + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, idle_timeout], 15000). t_cm_registry(_) -> Info = supervisor:which_children(emqx_cm_sup), @@ -273,15 +269,13 @@ t_basic(_Opts) -> ok = emqtt:disconnect(C). t_username_as_clientid(_) -> - emqx_zone:set_env(external, use_username_as_clientid, true), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, use_username_as_clientid], true), Username = <<"usera">>, {ok, C} = emqtt:start_link([{username, Username}]), {ok, _} = emqtt:connect(C), #{clientinfo := #{clientid := Username}} = emqx_cm:get_chan_info(Username), emqtt:disconnect(C). - - t_certcn_as_clientid_default_config_tls(_) -> tls_certcn_as_clientid(default). @@ -329,7 +323,7 @@ tls_certcn_as_clientid(TLSVsn) -> tls_certcn_as_clientid(TLSVsn, RequiredTLSVsn) -> CN = <<"Client">>, - emqx_zone:set_env(external, use_username_as_clientid, true), + emqx_config:put_listener_conf(default, mqtt_ssl, [mqtt, peer_cert_as_clientid], cn), SslConf = emqx_ct_helpers:client_ssl_twoway(TLSVsn), {ok, Client} = emqtt:start_link([{port, 8883}, {ssl, true}, {ssl_opts, SslConf}]), {ok, _} = emqtt:connect(Client), diff --git a/apps/emqx/test/emqx_cm_SUITE.erl b/apps/emqx/test/emqx_cm_SUITE.erl index 3f6950b3b..75d0a899c 100644 --- a/apps/emqx/test/emqx_cm_SUITE.erl +++ b/apps/emqx/test/emqx_cm_SUITE.erl @@ -89,7 +89,7 @@ t_open_session(_) -> ok = meck:expect(emqx_connection, call, fun(_, _) -> ok end), ok = meck:expect(emqx_connection, call, fun(_, _, _) -> ok end), - ClientInfo = #{zone => external, + ClientInfo = #{zone => default, listener => mqtt_tcp, clientid => <<"clientid">>, username => <<"username">>, peerhost => {127,0,0,1}}, @@ -114,7 +114,7 @@ rand_client_id() -> t_open_session_race_condition(_) -> ClientId = rand_client_id(), - ClientInfo = #{zone => external, + ClientInfo = #{zone => default, listener => mqtt_tcp, clientid => ClientId, username => <<"username">>, peerhost => {127,0,0,1}}, diff --git a/apps/emqx/test/emqx_misc_SUITE.erl b/apps/emqx/test/emqx_misc_SUITE.erl index f933fb498..c3580545a 100644 --- a/apps/emqx/test/emqx_misc_SUITE.erl +++ b/apps/emqx/test/emqx_misc_SUITE.erl @@ -119,8 +119,9 @@ t_index_of(_) -> ?assertEqual(3, emqx_misc:index_of(a, [b, c, a, e, f])). t_check(_) -> - Policy = #{message_queue_len => 10, - max_heap_size => 1024 * 1024 * 8}, + Policy = #{max_message_queue_len => 10, + max_heap_size => 1024 * 1024 * 8, + enable => true}, [self() ! {msg, I} || I <- lists:seq(1, 5)], ?assertEqual(ok, emqx_misc:check_oom(Policy)), [self() ! {msg, I} || I <- lists:seq(1, 6)], From 88638da2879ac66c014eb12c7fa3fe70a06da660 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 12 Jul 2021 16:00:04 +0800 Subject: [PATCH 137/379] fix(test): always init session with zone and listener names --- apps/emqx/test/emqx_channel_SUITE.erl | 5 +++-- apps/emqx/test/emqx_session_SUITE.erl | 6 ++++-- apps/emqx/test/emqx_ws_connection_SUITE.erl | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 4d108bb94..f9db11c27 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -624,7 +624,7 @@ t_handle_deliver_nl(_) -> Channel = channel(#{clientinfo => ClientInfo, session => Session}), Msg = emqx_message:make(<<"clientid">>, ?QOS_1, <<"t1">>, <<"qos1">>), NMsg = emqx_message:set_flag(nl, Msg), - {ok, Channel} = emqx_channel:handle_deliver([{deliver, <<"t1">>, NMsg}], Channel). + {ok, _} = emqx_channel:handle_deliver([{deliver, <<"t1">>, NMsg}], Channel). %%-------------------------------------------------------------------- %% Test cases for handle_out @@ -973,7 +973,8 @@ session(InitFields) when is_map(InitFields) -> maps:fold(fun(Field, Value, Session) -> emqx_session:set_field(Field, Value, Session) end, - emqx_session:init(#{zone => channel}, #{receive_maximum => 0}), + emqx_session:init(#{zone => default, listener => mqtt_tcp}, + #{receive_maximum => 0}), InitFields). %% conn: 5/s; overall: 10/s diff --git a/apps/emqx/test/emqx_session_SUITE.erl b/apps/emqx/test/emqx_session_SUITE.erl index cb7c10cae..5c96c96df 100644 --- a/apps/emqx/test/emqx_session_SUITE.erl +++ b/apps/emqx/test/emqx_session_SUITE.erl @@ -50,7 +50,8 @@ end_per_testcase(_TestCase, Config) -> %%-------------------------------------------------------------------- t_session_init(_) -> - Session = emqx_session:init(#{zone => zone}, #{receive_maximum => 64}), + Session = emqx_session:init(#{zone => default, listener => mqtt_tcp}, + #{receive_maximum => 64}), ?assertEqual(#{}, emqx_session:info(subscriptions, Session)), ?assertEqual(0, emqx_session:info(subscriptions_cnt, Session)), ?assertEqual(0, emqx_session:info(subscriptions_max, Session)), @@ -375,7 +376,8 @@ session(InitFields) when is_map(InitFields) -> maps:fold(fun(Field, Value, Session) -> emqx_session:set_field(Field, Value, Session) end, - emqx_session:init(#{zone => channel}, #{receive_maximum => 0}), + emqx_session:init(#{zone => default, listener => mqtt_tcp}, + #{receive_maximum => 0}), InitFields). diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index 93c192b86..cfa45f1ad 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -502,7 +502,7 @@ channel(InitFields) -> peercert => undefined, mountpoint => undefined }, - Session = emqx_session:init(#{zone => external}, + Session = emqx_session:init(#{zone => default, listener => mqtt_tcp}, #{receive_maximum => 0} ), maps:fold(fun(Field, Value, Channel) -> From 5bb55332a52f88df1adf9da474e8aab2092cd5cb Mon Sep 17 00:00:00 2001 From: zhouzb Date: Mon, 12 Jul 2021 16:29:44 +0800 Subject: [PATCH 138/379] chore(auhtn): keep one ssl opts checking func --- .../emqx_authn/src/simple_authn/emqx_authn_http.erl | 13 +------------ apps/emqx_connector/src/emqx_connector_http.erl | 2 ++ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 71929b944..692ff924e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -72,7 +72,7 @@ common_fields() -> ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)). validations() -> - [ {check_ssl_opts, fun check_ssl_opts/1} ]. + [ {check_ssl_opts, fun emqx_connector_http:check_ssl_opts/1} ]. url(type) -> binary(); url(nullable) -> false; @@ -190,17 +190,6 @@ check_form_data(FormData) -> false end. -check_ssl_opts(Conf) -> - URL = hocon_schema:get_value("url", Conf), - {ok, #{scheme := Scheme}} = emqx_http_lib:uri_parse(URL), - SSLOpts = hocon_schema:get_value("ssl_opts", Conf), - case {Scheme, SSLOpts} of - {http, undefined} -> true; - {http, _} -> false; - {https, undefined} -> false; - {https, _} -> true - end. - preprocess_form_data(FormData) -> KVs = binary:split(FormData, [<<"&">>], [global]), [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs]. diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 807243038..bef5c26f3 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -32,6 +32,8 @@ , fields/1 , validations/0]). +-export([ check_ssl_opts/1 ]). + -type connect_timeout() :: non_neg_integer() | infinity. -type pool_type() :: random | hash. From 734a5b94202d6fbf3b20f67d12568aae8413d04b Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 12 Jul 2021 18:10:40 +0800 Subject: [PATCH 139/379] test: add clients api SUITE & add delete sub api --- .../src/emqx_mgmt_api_clients.erl | 57 ++++++- .../test/emqx_mgmt_clients_api_SUITE.erl | 156 ++++++++++++++++++ 2 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 1afd906f1..74cd995a4 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -269,7 +269,6 @@ clients_acl_cache_api() -> {"/clients/:clientid/acl_cache", Metadata, acl_cache}. subscribe_api() -> - Path = "/clients/:clientid/subscribe", Metadata = #{ post => #{ description => "subscribe", @@ -282,7 +281,7 @@ subscribe_api() -> default => 123456 }, #{ - name => topics, + name => topic_data, in => body, schema => #{ type => object, @@ -300,8 +299,28 @@ subscribe_api() -> ], responses => #{ <<"404">> => emqx_mgmt_util:not_found_schema(<<"Client id not found">>), - <<"200">> => #{description => <<"publish ok">>}}}}, - {Path, Metadata, subscribe}. + <<"200">> => #{description => <<"subscribe ok">>}}}, + delete => #{ + description => "unsubscribe", + parameters => [ + #{ + name => clientid, + in => path, + type => string, + required => true, + default => 123456 + }, + #{ + name => topic, + in => query, + required => true, + default => <<"topic_1">> + } + ], + responses => #{ + <<"404">> => emqx_mgmt_util:not_found_schema(<<"Client id not found">>), + <<"200">> => #{description => <<"unsubscribe ok">>}}}}, + {"/clients/:clientid/subscribe", Metadata, subscribe}. %%%============================================================================================== %% parameters trans @@ -330,7 +349,12 @@ subscribe(post, Request) -> TopicInfo = emqx_json:decode(Body, [return_maps]), Topic = maps:get(<<"topic">>, TopicInfo), Qos = maps:get(<<"qos">>, TopicInfo, 0), - subscribe(#{clientid => ClientID, topic => Topic, qos => Qos}). + subscribe(#{clientid => ClientID, topic => Topic, qos => Qos}); + +subscribe(delete, Request) -> + ClientID = cowboy_req:binding(clientid, Request), + #{topic := Topic} = cowboy_req:match_qs([topic], Request), + unsubscribe(#{clientid => ClientID, topic => Topic}). %% TODO: batch subscribe_batch(post, Request) -> @@ -359,7 +383,7 @@ lookup(#{clientid := ClientID}) -> {404, ?CLIENT_ID_NOT_FOUND}; ClientInfo -> Response = emqx_json:encode(hd(ClientInfo)), - {ok, Response} + {200, Response} end. kickout(#{clientid := ClientID}) -> @@ -393,11 +417,22 @@ subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) -> {404, ?CLIENT_ID_NOT_FOUND}; {error, Reason} -> Body = emqx_json:encode(#{code => <<"UNKNOW_ERROR">>, reason => io_lib:format("~p", [Reason])}), - {200, Body}; + {500, Body}; ok -> {200} end. +unsubscribe(#{clientid := ClientID, topic := Topic}) -> + case do_unsubscribe(ClientID, Topic) of + {error, channel_not_found} -> + {404, ?CLIENT_ID_NOT_FOUND}; + {error, Reason} -> + Body = emqx_json:encode(#{code => <<"UNKNOW_ERROR">>, reason => io_lib:format("~p", [Reason])}), + {500, Body}; + {unsubscribe, [{Topic, #{}}]} -> + {200} + end. + subscribe_batch(#{clientid := ClientID, topics := Topics}) -> ArgList = [[ClientID, Topic, Qos]|| #{topic := Topic, qos := Qos} <- Topics], emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList). @@ -477,6 +512,14 @@ do_subscribe(ClientID, Topic0, Qos) -> {error, unknow_error} end end. + +do_unsubscribe(ClientID, Topic) -> + case emqx_mgmt:unsubscribe(ClientID, Topic) of + {error, Reason} -> + {error, Reason}; + Res -> + Res + end. %%%============================================================================================== %% Query Functions diff --git a/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl new file mode 100644 index 000000000..199028c04 --- /dev/null +++ b/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl @@ -0,0 +1,156 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_clients_api_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-define(APP, emqx_management). + +-define(SERVER, "http://127.0.0.1:8081"). +-define(BASE_PATH, "/api/v5"). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ekka_mnesia:start(), + emqx_mgmt_auth:mnesia(boot), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([emqx_management]). + +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), + ok; +set_special_configs(_App) -> + ok. + +t_clients(_) -> + process_flag(trap_exit, true), + + Username1 = <<"user1">>, + ClientId1 = <<"client1">>, + + Username2 = <<"user2">>, + ClientId2 = <<"client2">>, + + Topic = <<"topic_1">>, + Qos = 0, + + {ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}), + {ok, _} = emqtt:connect(C1), + {ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}), + {ok, _} = emqtt:connect(C2), + + timer:sleep(300), + + %% get /clients + {ok, Clients} = request_api(get, api_path(["clients"])), + ClientsResponse = emqx_json:decode(Clients, [return_maps]), + ClientsMeta = maps:get(<<"meta">>, ClientsResponse), + ClientsPage = maps:get(<<"page">>, ClientsMeta), + ClientsLimit = maps:get(<<"limit">>, ClientsMeta), + ClientsCount = maps:get(<<"count">>, ClientsMeta), + ?assertEqual(ClientsPage, 1), + ?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()), + ?assertEqual(ClientsCount, 2), + + %% get /clients/:clientid + {ok, Client1} = request_api(get, api_path(["clients", binary_to_list(ClientId1)])), + Client1Response = emqx_json:decode(Client1, [return_maps]), + ?assertEqual(Username1, maps:get(<<"username">>, Client1Response)), + ?assertEqual(ClientId1, maps:get(<<"clientid">>, Client1Response)), + + %% delete /clients/:clientid kickout + {ok, _} = request_api(delete, api_path(["clients", binary_to_list(ClientId2)])), + AfterKickoutResponse = request_api(get, api_path(["clients", binary_to_list(ClientId2)])), + ?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse), + + %% get /clients/:clientid/acl_cache should has no acl cache + {ok, Client1AclCache} = request_api(get, + api_path(["clients", binary_to_list(ClientId1), "acl_cache"])), + ?assertEqual("[]", Client1AclCache), + + %% get /clients/:clientid/acl_cache should has no acl cache + {ok, Client1AclCache} = request_api(get, + api_path(["clients", binary_to_list(ClientId1), "acl_cache"])), + ?assertEqual("[]", Client1AclCache), + + %% post /clients/:clientid/subscribe + SubscribeBody = #{topic => Topic, qos => Qos}, + SubscribePath = api_path(["clients", binary_to_list(ClientId1), "subscribe"]), + {ok, _} = request_api(post, SubscribePath, "", auth_header_(), SubscribeBody), + [{{_, AfterSubTopic}, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1), + ?assertEqual(AfterSubTopic, Topic), + ?assertEqual(AfterSubQos, Qos), + + %% delete /clients/:clientid/subscribe + UnSubscribeQuery = "topic=" ++ binary_to_list(Topic), + {ok, _} = request_api(delete, SubscribePath, UnSubscribeQuery, auth_header_()), + ?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)). + +%%%============================================================================================== +%% test util function +request_api(Method, Url) -> + request_api(Method, Url, [], auth_header_(), []). + +request_api(Method, Url, Auth) -> + request_api(Method, Url, [], Auth, []). + +request_api(Method, Url, QueryParams, Auth) -> + request_api(Method, Url, QueryParams, Auth, []). + +request_api(Method, Url, QueryParams, Auth, []) -> + NewUrl = case QueryParams of + "" -> Url; + _ -> Url ++ "?" ++ QueryParams + end, + do_request_api(Method, {NewUrl, [Auth]}); +request_api(Method, Url, QueryParams, Auth, Body) -> + NewUrl = case QueryParams of + "" -> Url; + _ -> Url ++ "?" ++ QueryParams + end, + do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}). + +do_request_api(Method, Request)-> + ct:pal("Method: ~p, Request: ~p", [Method, Request]), + case httpc:request(Method, Request, [], []) of + {error, socket_closed_remotely} -> + {error, socket_closed_remotely}; + {ok, {{"HTTP/1.1", Code, _}, _, Return} } + when Code =:= 200 orelse Code =:= 201 -> + {ok, Return}; + {ok, {Reason, _, _}} -> + {error, Reason} + end. + +auth_header_() -> + AppId = <<"admin">>, + AppSecret = <<"public">>, + auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). + +auth_header_(User, Pass) -> + Encoded = base64:encode_to_string(lists:append([User,":",Pass])), + {"Authorization","Basic " ++ Encoded}. + +api_path(Parts)-> + ?SERVER ++ filename:join([?BASE_PATH | Parts]). From 499ab5d9c490e1cca0e7af70ee7633d6fdd4d2f9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 12 Jul 2021 18:10:47 +0800 Subject: [PATCH 140/379] fix(config): configure a plain map for mqueue_priorities --- apps/emqx/etc/emqx.conf | 22 +++++++++++--------- apps/emqx/include/emqx.hrl | 6 ------ apps/emqx/src/emqx_frame.erl | 2 +- apps/emqx/src/emqx_mqueue.erl | 2 ++ apps/emqx/src/emqx_schema.erl | 2 +- apps/emqx/test/emqx_flapping_SUITE.erl | 12 ++--------- apps/emqx/test/emqx_mqtt_SUITE.erl | 9 ++++++++ apps/emqx_coap/test/emqx_coap_SUITE.erl | 2 +- apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl | 1 - 9 files changed, 28 insertions(+), 30 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index ec2636c40..5c8bd5265 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1039,26 +1039,28 @@ zones.default { ## ## There's no priority table by default, hence all messages ## are treated equal. - ## The top topicname in the table has the highest priority, and then - ## the next one has the second highest priority, etc. - ## Messages for topics not in the priority table are treated as + ## + ## Priority number [1-255] + ## + ## NOTE: comma and equal signs are not allowed for priority topic names + ## NOTE: Messages for topics not in the priority table are treated as ## either highest or lowest priority depending on the configured ## value for mqtt.mqueue_default_priority ## ## @doc zones..mqtt.mqueue_priorities - ## ValueType: Array + ## ValueType: Map | disabled ## Examples: - ## To configure "t/1" > "t/2" > "t/3": - ## mqueue_priorities: [t/1,t/2,t/3] - ## Default: [] - mqueue_priorities: [] + ## To configure "topic/1" > "topic/2": + ## mqueue_priorities: {"topic/1": 10, "topic/2": 8} + ## Default: disabled + mqueue_priorities: disabled ## Default to highest priority for topics not matching priority table ## ## @doc zones..mqtt.mqueue_default_priority ## ValueType: highest | lowest - ## Default: highest - mqueue_default_priority: highest + ## Default: lowest + mqueue_default_priority: lowest ## Whether to enqueue QoS0 messages. ## diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index a11c30cb4..60dccd9a3 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -35,12 +35,6 @@ -define(ERTS_MINIMUM_REQUIRED, "10.0"). -%%-------------------------------------------------------------------- -%% Configs -%%-------------------------------------------------------------------- - --define(NO_PRIORITY_TABLE, none). - %%-------------------------------------------------------------------- %% Topics' prefix: $SYS | $queue | $share %%-------------------------------------------------------------------- diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 1737e8791..0bab10e0c 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -646,7 +646,7 @@ serialize_properties(Props) when is_map(Props) -> Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>, [serialize_variable_byte_integer(byte_size(Bin)), Bin]. -serialize_property(_, undefined) -> +serialize_property(_, Disabled) when Disabled =:= disabled; Disabled =:= undefined -> <<>>; serialize_property('Payload-Format-Indicator', Val) -> <<16#01, Val>>; diff --git a/apps/emqx/src/emqx_mqueue.erl b/apps/emqx/src/emqx_mqueue.erl index d0c6365ff..d625209ca 100644 --- a/apps/emqx/src/emqx_mqueue.erl +++ b/apps/emqx/src/emqx_mqueue.erl @@ -67,6 +67,8 @@ , dropped/1 ]). +-define(NO_PRIORITY_TABLE, disabled). + -export_type([mqueue/0, options/0]). -type(topic() :: emqx_topic:topic()). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 72fe3832b..738623f72 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -277,7 +277,7 @@ fields("mqtt") -> , {"await_rel_timeout", t(duration_s(), undefined, "300s")} , {"session_expiry_interval", t(duration_s(), undefined, "2h")} , {"max_mqueue_len", maybe_infinity(integer(), 1000)} - , {"mqueue_priorities", t(comma_separated_list(), undefined, "none")} + , {"mqueue_priorities", maybe_disabled(map())} , {"mqueue_default_priority", t(union(highest, lowest), undefined, lowest)} , {"mqueue_store_qos0", t(boolean(), undefined, true)} , {"use_username_as_clientid", t(boolean(), undefined, false)} diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index 8f069b747..79eb64b45 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -25,18 +25,10 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), - emqx_ct_helpers:start_apps([], fun set_special_configs/1), + emqx_ct_helpers:start_apps([]), + emqx_config:put_listener_conf(default, mqtt_tcp, [flapping_detect, enable], true), Config. -set_special_configs(emqx) -> - emqx_zone:set_env(external, enable_flapping_detect, true), - application:set_env(emqx, flapping_detect_policy, - #{threshold => 3, - duration => 100, - banned_interval => 2 - }); -set_special_configs(_App) -> ok. - end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]), ekka_mnesia:delete_schema(), %% Clean emqx_banned table diff --git a/apps/emqx/test/emqx_mqtt_SUITE.erl b/apps/emqx/test/emqx_mqtt_SUITE.erl index c86d6334a..42a4e5780 100644 --- a/apps/emqx/test/emqx_mqtt_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_SUITE.erl @@ -156,6 +156,15 @@ t_async_set_keepalive('end', _Config) -> ok. t_async_set_keepalive(_) -> + case os:type() of + {unix, darwin} -> + %% Mac OSX don't support the feature + ok; + _ -> + do_async_set_keepalive() + end. + +do_async_set_keepalive() -> ClientID = <<"client-tcp-keepalive">>, {ok, Client} = emqtt:start_link([{host, "localhost"}, {proto_ver,v5}, diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl index 9618425a3..d8670eb5c 100644 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_coap/test/emqx_coap_SUITE.erl @@ -265,7 +265,7 @@ t_kick_1(_Config) -> end. % mqtt connection kicked by coap with same client id -t_acl(Config) -> +t_acl(_Config) -> OldPath = emqx:get_env(plugins_etc_dir), application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authz, "test")), diff --git a/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl index 0947bdaca..1371c5123 100644 --- a/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl @@ -170,7 +170,6 @@ t_subscribe_case02(_) -> ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = ?CLIENTID, send_connect_msg(Socket, ?CLIENTID), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), From 9081a22b8c045642f66fc592d25a8dfc0ee7ac8a Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 12 Jul 2021 15:18:19 +0800 Subject: [PATCH 141/379] chore(deps): deps in rebar must not be use branch Can use tags or commit ids in rebar deps Signed-off-by: zhanghongtong --- apps/emqx/rebar.config | 2 +- rebar.config | 2 +- scripts/check-deps-integrity.escript | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 24a0544c8..add05186c 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -17,7 +17,7 @@ , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} - , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "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"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} ]}. diff --git a/rebar.config b/rebar.config index aa6b2dd4c..2a58dc142 100644 --- a/rebar.config +++ b/rebar.config @@ -54,7 +54,7 @@ , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.1.1"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, {git, "https://github.com/emqx/replayq", {tag, "0.3.2"}}} - , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "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.4.2"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.2"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} diff --git a/scripts/check-deps-integrity.escript b/scripts/check-deps-integrity.escript index d875a2c40..3cc8fdc53 100755 --- a/scripts/check-deps-integrity.escript +++ b/scripts/check-deps-integrity.escript @@ -48,7 +48,7 @@ do_collect_deps([{Name, Ref} | Deps], File, Acc) -> count_bad_deps([]) -> 0; count_bad_deps([{Name, Refs0} | Rest]) -> Refs = lists:keysort(1, Refs0), - case is_unique_ref(Refs) of + case is_unique_ref(Refs) andalso not_branch_ref(Refs) of true -> count_bad_deps(Rest); false -> @@ -61,3 +61,7 @@ is_unique_ref([{Ref, _File1}, {Ref, File2} | Rest]) -> is_unique_ref([{Ref, File2} | Rest]); is_unique_ref(_) -> false. + +not_branch_ref([]) -> true; +not_branch_ref([{{git, _Repo, {branch, _Branch}}, _File} | _Rest]) -> false; +not_branch_ref([_Ref | Rest]) -> not_branch_ref(Rest). From 400e08e229b56666aad95ddd6cb35256011520e7 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 12 Jul 2021 20:43:11 +0800 Subject: [PATCH 142/379] fix(flapping): make the flapping work with the new config structure --- apps/emqx/src/emqx_flapping.erl | 82 ++++++++++++++------------ apps/emqx/test/emqx_flapping_SUITE.erl | 12 +++- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/apps/emqx/src/emqx_flapping.erl b/apps/emqx/src/emqx_flapping.erl index a0eab9c18..b48f43094 100644 --- a/apps/emqx/src/emqx_flapping.erl +++ b/apps/emqx/src/emqx_flapping.erl @@ -45,9 +45,9 @@ -define(FLAPPING_DURATION, 60000). -define(FLAPPING_BANNED_INTERVAL, 300000). -define(DEFAULT_DETECT_POLICY, - #{threshold => ?FLAPPING_THRESHOLD, - duration => ?FLAPPING_DURATION, - banned_interval => ?FLAPPING_BANNED_INTERVAL + #{max_count => ?FLAPPING_THRESHOLD, + window_time => ?FLAPPING_DURATION, + ban_time => ?FLAPPING_BANNED_INTERVAL }). -record(flapping, { @@ -69,33 +69,28 @@ stop() -> gen_server:stop(?MODULE). %% @doc Detect flapping when a MQTT client disconnected. -spec(detect(emqx_types:clientinfo()) -> boolean()). -detect(Client) -> detect(Client, get_policy()). - -detect(#{clientid := ClientId, peerhost := PeerHost}, Policy = #{threshold := Threshold}) -> - try ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}) of +detect(#{clientid := ClientId, peerhost := PeerHost, zone := Zone, listener := Listener}) -> + Policy = #{max_count := Threshold} = get_policy(Zone, Listener), + %% The initial flapping record sets the detect_cnt to 0. + InitVal = #flapping{ + clientid = ClientId, + peerhost = PeerHost, + started_at = erlang:system_time(millisecond), + detect_cnt = 0 + }, + case ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}, InitVal) of Cnt when Cnt < Threshold -> false; - _Cnt -> case ets:take(?FLAPPING_TAB, ClientId) of - [Flapping] -> - ok = gen_server:cast(?MODULE, {detected, Flapping, Policy}), - true; - [] -> false - end - catch - error:badarg -> - %% Create a flapping record. - Flapping = #flapping{clientid = ClientId, - peerhost = PeerHost, - started_at = erlang:system_time(millisecond), - detect_cnt = 1 - }, - true = ets:insert(?FLAPPING_TAB, Flapping), - false + _Cnt -> + case ets:take(?FLAPPING_TAB, ClientId) of + [Flapping] -> + ok = gen_server:cast(?MODULE, {detected, Flapping, Policy}), + true; + [] -> false + end end. --compile({inline, [get_policy/0, now_diff/1]}). - -get_policy() -> - emqx:get_env(flapping_detect_policy, ?DEFAULT_DETECT_POLICY). +get_policy(Zone, Listener) -> + emqx_config:get_listener_conf(Zone, Listener, [flapping_detect]). now_diff(TS) -> erlang:system_time(millisecond) - TS. @@ -105,11 +100,12 @@ now_diff(TS) -> erlang:system_time(millisecond) - TS. init([]) -> ok = emqx_tables:new(?FLAPPING_TAB, [public, set, - {keypos, 2}, + {keypos, #flapping.clientid}, {read_concurrency, true}, {write_concurrency, true} ]), - {ok, ensure_timer(#{}), hibernate}. + start_timers(), + {ok, #{}, hibernate}. handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), @@ -119,11 +115,11 @@ handle_cast({detected, #flapping{clientid = ClientId, peerhost = PeerHost, started_at = StartedAt, detect_cnt = DetectCnt}, - #{duration := Duration, banned_interval := Interval}}, State) -> - case now_diff(StartedAt) < Duration of + #{window_time := WindTime, ban_time := Interval}}, State) -> + case now_diff(StartedAt) < WindTime of true -> %% Flapping happened:( ?LOG(error, "Flapping detected: ~s(~s) disconnected ~w times in ~wms", - [ClientId, inet:ntoa(PeerHost), DetectCnt, Duration]), + [ClientId, inet:ntoa(PeerHost), DetectCnt, WindTime]), Now = erlang:system_time(second), Banned = #banned{who = {clientid, ClientId}, by = <<"flapping detector">>, @@ -141,11 +137,13 @@ handle_cast(Msg, State) -> ?LOG(error, "Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, TRef, expired_detecting}, State = #{expired_timer := TRef}) -> - Timestamp = erlang:system_time(millisecond) - maps:get(duration, get_policy()), +handle_info({timeout, _TRef, {garbage_collect, Zone, Listener}}, State) -> + Timestamp = erlang:system_time(millisecond) + - maps:get(window_time, get_policy(Zone, Listener)), MatchSpec = [{{'_', '_', '_', '$1', '_'},[{'<', '$1', Timestamp}], [true]}], ets:select_delete(?FLAPPING_TAB, MatchSpec), - {noreply, ensure_timer(State), hibernate}; + start_timer(Zone, Listener), + {noreply, State, hibernate}; handle_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), @@ -157,7 +155,13 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -ensure_timer(State) -> - Timeout = maps:get(duration, get_policy()), - TRef = emqx_misc:start_timer(Timeout, expired_detecting), - State#{expired_timer => TRef}. \ No newline at end of file +start_timer(Zone, Listener) -> + WindTime = maps:get(window_time, get_policy(Zone, Listener)), + emqx_misc:start_timer(WindTime, {garbage_collect, Zone, Listener}). + +start_timers() -> + lists:foreach(fun({Zone, ZoneConf}) -> + lists:foreach(fun({Listener, _}) -> + start_timer(Zone, Listener) + end, maps:to_list(maps:get(listeners, ZoneConf, #{}))) + end, maps:to_list(emqx_config:get([zones], #{}))). \ No newline at end of file diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index 79eb64b45..e5b12a122 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -26,7 +26,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:put_listener_conf(default, mqtt_tcp, [flapping_detect, enable], true), + emqx_config:put_listener_conf(default, mqtt_tcp, [flapping_detect], + #{max_count => 3, + window_time => 100, + ban_time => 2 + }), Config. end_per_suite(_Config) -> @@ -35,7 +39,8 @@ end_per_suite(_Config) -> ok. t_detect_check(_) -> - ClientInfo = #{zone => external, + ClientInfo = #{zone => default, + listener => mqtt_tcp, clientid => <<"clientid">>, peerhost => {127,0,0,1} }, @@ -56,7 +61,8 @@ t_detect_check(_) -> ok = emqx_flapping:stop(). t_expired_detecting(_) -> - ClientInfo = #{zone => external, + ClientInfo = #{zone => default, + listener => mqtt_tcp, clientid => <<"clientid">>, peerhost => {127,0,0,1}}, false = emqx_flapping:detect(ClientInfo), From 6d9918d3e55ac19359a8424b060eadda90d7eb93 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 13:52:29 +0800 Subject: [PATCH 143/379] fix(test): update the testcases for emqx_vm_mon_SUITE --- apps/emqx/src/emqx_vm_mon.erl | 11 +++++---- apps/emqx/test/emqx_vm_mon_SUITE.erl | 35 +++++++++++----------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/apps/emqx/src/emqx_vm_mon.erl b/apps/emqx/src/emqx_vm_mon.erl index 79b1537d4..13a470959 100644 --- a/apps/emqx/src/emqx_vm_mon.erl +++ b/apps/emqx/src/emqx_vm_mon.erl @@ -60,11 +60,12 @@ handle_info({timeout, _Timer, check}, State) -> ProcHighWatermark = emqx_config:get([sysmon, vm, process_high_watermark]), ProcLowWatermark = emqx_config:get([sysmon, vm, process_low_watermark]), ProcessCount = erlang:system_info(process_count), - case ProcessCount / erlang:system_info(process_limit) * 100 of + case ProcessCount / erlang:system_info(process_limit) of Percent when Percent >= ProcHighWatermark -> - emqx_alarm:activate(too_many_processes, #{usage => Percent, - high_watermark => ProcHighWatermark, - low_watermark => ProcLowWatermark}); + emqx_alarm:activate(too_many_processes, #{ + usage => io_lib:format("~p%", [Percent*100]), + high_watermark => ProcHighWatermark, + low_watermark => ProcLowWatermark}); Percent when Percent < ProcLowWatermark -> emqx_alarm:deactivate(too_many_processes); _Precent -> @@ -89,4 +90,4 @@ code_change(_OldVsn, State, _Extra) -> start_check_timer() -> Interval = emqx_config:get([sysmon, vm, process_check_interval]), - emqx_misc:start_timer(timer:seconds(Interval), check). + emqx_misc:start_timer(Interval, check). diff --git a/apps/emqx/test/emqx_vm_mon_SUITE.erl b/apps/emqx/test/emqx_vm_mon_SUITE.erl index 5f9f4084c..5b39746a1 100644 --- a/apps/emqx/test/emqx_vm_mon_SUITE.erl +++ b/apps/emqx/test/emqx_vm_mon_SUITE.erl @@ -23,17 +23,16 @@ all() -> emqx_ct:all(?MODULE). -init_per_testcase(t_api, Config) -> +init_per_testcase(t_alarms, Config) -> emqx_ct_helpers:boot_modules(all), - emqx_ct_helpers:start_apps([], - fun(emqx) -> - application:set_env(emqx, vm_mon, [{check_interval, 1}, - {process_high_watermark, 80}, - {process_low_watermark, 75}]), - ok; - (_) -> - ok - end), + emqx_ct_helpers:start_apps([]), + emqx_config:put([sysmon, vm], #{ + process_high_watermark => 0, + process_low_watermark => 0, + process_check_interval => 100 %% 1s + }), + ok = supervisor:terminate_child(emqx_sys_sup, emqx_vm_mon), + {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_vm_mon), Config; init_per_testcase(_, Config) -> emqx_ct_helpers:boot_modules(all), @@ -43,18 +42,12 @@ init_per_testcase(_, Config) -> end_per_testcase(_, _Config) -> emqx_ct_helpers:stop_apps([]). -t_api(_) -> - ?assertEqual(1, emqx_vm_mon:get_check_interval()), - ?assertEqual(80, emqx_vm_mon:get_process_high_watermark()), - ?assertEqual(75, emqx_vm_mon:get_process_low_watermark()), - emqx_vm_mon:set_process_high_watermark(0), - emqx_vm_mon:set_process_low_watermark(60), - ?assertEqual(0, emqx_vm_mon:get_process_high_watermark()), - ?assertEqual(60, emqx_vm_mon:get_process_low_watermark()), - timer:sleep(emqx_vm_mon:get_check_interval() * 1000 * 2), +t_alarms(_) -> + timer:sleep(500), ?assert(is_existing(too_many_processes, emqx_alarm:get_alarms(activated))), - emqx_vm_mon:set_process_high_watermark(70), - timer:sleep(emqx_vm_mon:get_check_interval() * 1000 * 2), + emqx_config:put([sysmon, vm, process_high_watermark], 70), + emqx_config:put([sysmon, vm, process_low_watermark], 60), + timer:sleep(500), ?assertNot(is_existing(too_many_processes, emqx_alarm:get_alarms(activated))). is_existing(Name, [#{name := Name} | _More]) -> From 500047fa302524e4d97e6ca3ac394835457c36d2 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 12 Jul 2021 13:59:52 +0800 Subject: [PATCH 144/379] refactor: nodes api ; add: api test util module --- .../src/emqx_mgmt_api_nodes.erl | 175 ++++++++++++++---- apps/emqx_management/src/emqx_mgmt_util.erl | 2 + .../test/emqx_mgmt_api_test_util.erl | 85 +++++++++ .../test/emqx_mgmt_clients_api_SUITE.erl | 92 ++------- .../test/emqx_mgmt_nodes_api_SUITE.erl | 58 ++++++ 5 files changed, 303 insertions(+), 109 deletions(-) create mode 100644 apps/emqx_management/test/emqx_mgmt_api_test_util.erl create mode 100644 apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 42970cdfb..3cb38c3a7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -13,49 +13,158 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- - -module(emqx_mgmt_api_nodes). --rest_api(#{name => list_nodes, - method => 'GET', - path => "/nodes/", - func => list, - descr => "A list of nodes in the cluster"}). +-behavior(minirest_api). --rest_api(#{name => get_node, - method => 'GET', - path => "/nodes/:atom:node", - func => get, - descr => "Lookup a node in the cluster"}). +-export([api_spec/0]). --export([ list/2 - , get/2 - ]). +-export([ nodes/2 + , node/2]). -list(_Bindings, _Params) -> - emqx_mgmt:return({ok, [format(Node, Info) || {Node, Info} <- emqx_mgmt:list_nodes()]}). +-include_lib("emqx/include/emqx.hrl"). -get(#{node := Node}, _Params) -> - emqx_mgmt:return({ok, emqx_mgmt:lookup_node(Node)}). +api_spec() -> + {apis(), schemas()}. -format(Node, {error, Reason}) -> #{node => Node, error => Reason}; +apis() -> + [ nodes_api() + , node_api()]. +schemas() -> + [node_schema()]. + +node_schema() -> + DefinitionName = <<"node">>, + DefinitionProperties = #{ + <<"node">> => #{ + type => <<"string">>, + description => <<"Node name">>}, + <<"connections">> => #{ + type => <<"integer">>, + description => <<"Number of clients currently connected to this node">>}, + <<"load1">> => #{ + type => <<"string">>, + description => <<"CPU average load in 1 minute">>}, + <<"load5">> => #{ + type => <<"string">>, + description => <<"CPU average load in 5 minute">>}, + <<"load15">> => #{ + type => <<"string">>, + description => <<"CPU average load in 15 minute">>}, + <<"max_fds">> => #{ + type => <<"integer">>, + description => <<"Maximum file descriptor limit for the operating system">>}, + <<"memory_total">> => #{ + type => <<"string">>, + description => <<"VM allocated system memory">>}, + <<"memory_used">> => #{ + type => <<"string">>, + description => <<"VM occupied system memory">>}, + <<"node_status">> => #{ + type => <<"string">>, + description => <<"Node status">>}, + <<"otp_release">> => #{ + type => <<"string">>, + description => <<"Erlang/OTP version used by EMQ X Broker">>}, + <<"process_available">> => #{ + type => <<"integer">>, + description => <<"Number of available processes">>}, + <<"process_used">> => #{ + type => <<"integer">>, + description => <<"Number of used processes">>}, + <<"uptime">> => #{ + type => <<"string">>, + description => <<"EMQ X Broker runtime">>}, + <<"version">> => #{ + type => <<"string">>, + description => <<"EMQ X Broker version">>}, + <<"sys_path">> => #{ + type => <<"string">>, + description => <<"EMQ X system file location">>}, + <<"log_path">> => #{ + type => <<"string">>, + description => <<"EMQ X log file location">>}, + <<"config_path">> => #{ + type => <<"string">>, + description => <<"EMQ X config file location">>} + }, + {DefinitionName, DefinitionProperties}. + +nodes_api() -> + Metadata = #{ + get => #{ + description => "List EMQ X nodes", + responses => #{ + <<"200">> => #{description => <<"List EMQ X Nodes">>, + schema => #{ + type => array, + items => cowboy_swagger:schema(<<"node">>)}}}}}, + {"/nodes", Metadata, nodes}. + +node_api() -> + Metadata = #{ + get => #{ + description => "Get node info", + parameters => [#{ + name => node_name, + in => path, + description => "node name", + type => string, + required => true, + default => node()}], + responses => #{ + <<"400">> => + emqx_mgmt_util:not_found_schema(<<"Node error">>, [<<"SOURCE_ERROR">>]), + <<"200">> => #{ + description => <<"Get EMQ X Nodes info by name">>, + schema => cowboy_swagger:schema(<<"node">>)}}}}, + {"/nodes/:node_name", Metadata, node}. + +%%%============================================================================================== +%% parameters trans +nodes(get, _Request) -> + list(#{}). + +node(get, Request) -> + NodeName = cowboy_req:binding(node_name, Request), + Node = binary_to_atom(NodeName, utf8), + get_node(#{node => Node}). + +%%%============================================================================================== +%% api apply +list(#{}) -> + NodesInfo = [format(Node, NodeInfo) || {Node, NodeInfo} <- emqx_mgmt:list_nodes()], + Response = emqx_json:encode(NodesInfo), + {200, Response}. + +get_node(#{node := Node}) -> + case emqx_mgmt:lookup_node(Node) of + #{node_status := 'ERROR'} -> + {400, emqx_json:encode(#{code => 'SOURCE_ERROR', reason => <<"rpc_failed">>})}; + NodeInfo -> + Response = emqx_json:encode(format(Node, NodeInfo)), + {200, Response} + end. + +%%============================================================================================================ +%% internal function format(_Node, Info = #{memory_total := Total, memory_used := Used}) -> {ok, SysPathBinary} = file:get_cwd(), - SysPath = list_to_binary(SysPathBinary), - ConfigPath = <>, - LogPath = case log_path() of - undefined -> - <<"not found">>; - Path0 -> - Path = list_to_binary(Path0), - <> - end, - Info#{ memory_total := emqx_mgmt_util:kmg(Total) - , memory_used := emqx_mgmt_util:kmg(Used) - , sys_path => SysPath - , config_path => ConfigPath - , log_path => LogPath}. + SysPath = list_to_binary(SysPathBinary), + ConfigPath = <>, + LogPath = case log_path() of + undefined -> + <<"not found">>; + Path0 -> + Path = list_to_binary(Path0), + <> + end, + Info#{ memory_total := emqx_mgmt_util:kmg(Total) + , memory_used := emqx_mgmt_util:kmg(Used) + , sys_path => SysPath + , config_path => ConfigPath + , log_path => LogPath}. log_path() -> Configs = logger:get_handler_config(), diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 8cbe8bdf4..4197973e7 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -22,6 +22,7 @@ , ntoa/1 , merge_maps/2 , not_found_schema/1 + , not_found_schema/2 , batch_operation/3 ]). @@ -94,6 +95,7 @@ not_found_schema(Description, Enum) -> reason => #{ type => string}}} }. + batch_operation(Module, Function, ArgsList) -> Failed = batch_operation(Module, Function, ArgsList, []), Len = erlang:length(Failed), diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl new file mode 100644 index 000000000..1925b52e5 --- /dev/null +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -0,0 +1,85 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_api_test_util). +-compile(export_all). +-compile(nowarn_export_all). + +-define(SERVER, "http://127.0.0.1:8081"). +-define(BASE_PATH, "/api/v5"). + +default_init() -> + ekka_mnesia:start(), + emqx_mgmt_auth:mnesia(boot), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + ok. + + +default_end() -> + emqx_ct_helpers:stop_apps([emqx_management]). + +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), + ok; +set_special_configs(_App) -> + ok. + + +request_api(Method, Url) -> + request_api(Method, Url, [], auth_header_(), []). + +request_api(Method, Url, Auth) -> + request_api(Method, Url, [], Auth, []). + +request_api(Method, Url, QueryParams, Auth) -> + request_api(Method, Url, QueryParams, Auth, []). + +request_api(Method, Url, QueryParams, Auth, []) -> + NewUrl = case QueryParams of + "" -> Url; + _ -> Url ++ "?" ++ QueryParams + end, + do_request_api(Method, {NewUrl, [Auth]}); +request_api(Method, Url, QueryParams, Auth, Body) -> + NewUrl = case QueryParams of + "" -> Url; + _ -> Url ++ "?" ++ QueryParams + end, + do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}). + +do_request_api(Method, Request)-> + ct:pal("Method: ~p, Request: ~p", [Method, Request]), + case httpc:request(Method, Request, [], []) of + {error, socket_closed_remotely} -> + {error, socket_closed_remotely}; + {ok, {{"HTTP/1.1", Code, _}, _, Return} } + when Code =:= 200 orelse Code =:= 201 -> + {ok, Return}; + {ok, {Reason, _, _}} -> + {error, Reason} + end. + +auth_header_() -> + AppId = <<"admin">>, + AppSecret = <<"public">>, + auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). + +auth_header_(User, Pass) -> + Encoded = base64:encode_to_string(lists:append([User,":",Pass])), + {"Authorization","Basic " ++ Encoded}. + +api_path(Parts)-> + ?SERVER ++ filename:join([?BASE_PATH | Parts]). diff --git a/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl index 199028c04..688989211 100644 --- a/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl @@ -21,27 +21,15 @@ -define(APP, emqx_management). --define(SERVER, "http://127.0.0.1:8081"). --define(BASE_PATH, "/api/v5"). - all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> - ekka_mnesia:start(), - emqx_mgmt_auth:mnesia(boot), - emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + emqx_mgmt_api_test_util:default_init(), Config. end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_management]). - -set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], - applications =>[#{id => "admin", secret => "public"}]}), - ok; -set_special_configs(_App) -> - ok. + emqx_mgmt_api_test_util:default_end(). t_clients(_) -> process_flag(trap_exit, true), @@ -55,6 +43,8 @@ t_clients(_) -> Topic = <<"topic_1">>, Qos = 0, + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + {ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}), {ok, _} = emqtt:connect(C1), {ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}), @@ -63,7 +53,8 @@ t_clients(_) -> timer:sleep(300), %% get /clients - {ok, Clients} = request_api(get, api_path(["clients"])), + ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]), + {ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath), ClientsResponse = emqx_json:decode(Clients, [return_maps]), ClientsMeta = maps:get(<<"meta">>, ClientsResponse), ClientsPage = maps:get(<<"page">>, ClientsMeta), @@ -74,83 +65,32 @@ t_clients(_) -> ?assertEqual(ClientsCount, 2), %% get /clients/:clientid - {ok, Client1} = request_api(get, api_path(["clients", binary_to_list(ClientId1)])), + Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]), + {ok, Client1} = emqx_mgmt_api_test_util:request_api(get, Client1Path), Client1Response = emqx_json:decode(Client1, [return_maps]), ?assertEqual(Username1, maps:get(<<"username">>, Client1Response)), ?assertEqual(ClientId1, maps:get(<<"clientid">>, Client1Response)), %% delete /clients/:clientid kickout - {ok, _} = request_api(delete, api_path(["clients", binary_to_list(ClientId2)])), - AfterKickoutResponse = request_api(get, api_path(["clients", binary_to_list(ClientId2)])), + Client2Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId2)]), + {ok, _} = emqx_mgmt_api_test_util:request_api(delete, Client2Path), + AfterKickoutResponse = emqx_mgmt_api_test_util:request_api(get, Client2Path), ?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse), %% get /clients/:clientid/acl_cache should has no acl cache - {ok, Client1AclCache} = request_api(get, - api_path(["clients", binary_to_list(ClientId1), "acl_cache"])), - ?assertEqual("[]", Client1AclCache), - - %% get /clients/:clientid/acl_cache should has no acl cache - {ok, Client1AclCache} = request_api(get, - api_path(["clients", binary_to_list(ClientId1), "acl_cache"])), + Client1AclCachePath = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1), "acl_cache"]), + {ok, Client1AclCache} = emqx_mgmt_api_test_util:request_api(get, Client1AclCachePath), ?assertEqual("[]", Client1AclCache), %% post /clients/:clientid/subscribe SubscribeBody = #{topic => Topic, qos => Qos}, - SubscribePath = api_path(["clients", binary_to_list(ClientId1), "subscribe"]), - {ok, _} = request_api(post, SubscribePath, "", auth_header_(), SubscribeBody), + SubscribePath = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1), "subscribe"]), + {ok, _} = emqx_mgmt_api_test_util:request_api(post, SubscribePath, "", AuthHeader, SubscribeBody), [{{_, AfterSubTopic}, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1), ?assertEqual(AfterSubTopic, Topic), ?assertEqual(AfterSubQos, Qos), %% delete /clients/:clientid/subscribe UnSubscribeQuery = "topic=" ++ binary_to_list(Topic), - {ok, _} = request_api(delete, SubscribePath, UnSubscribeQuery, auth_header_()), + {ok, _} = emqx_mgmt_api_test_util:request_api(delete, SubscribePath, UnSubscribeQuery, AuthHeader), ?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)). - -%%%============================================================================================== -%% test util function -request_api(Method, Url) -> - request_api(Method, Url, [], auth_header_(), []). - -request_api(Method, Url, Auth) -> - request_api(Method, Url, [], Auth, []). - -request_api(Method, Url, QueryParams, Auth) -> - request_api(Method, Url, QueryParams, Auth, []). - -request_api(Method, Url, QueryParams, Auth, []) -> - NewUrl = case QueryParams of - "" -> Url; - _ -> Url ++ "?" ++ QueryParams - end, - do_request_api(Method, {NewUrl, [Auth]}); -request_api(Method, Url, QueryParams, Auth, Body) -> - NewUrl = case QueryParams of - "" -> Url; - _ -> Url ++ "?" ++ QueryParams - end, - do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}). - -do_request_api(Method, Request)-> - ct:pal("Method: ~p, Request: ~p", [Method, Request]), - case httpc:request(Method, Request, [], []) of - {error, socket_closed_remotely} -> - {error, socket_closed_remotely}; - {ok, {{"HTTP/1.1", Code, _}, _, Return} } - when Code =:= 200 orelse Code =:= 201 -> - {ok, Return}; - {ok, {Reason, _, _}} -> - {error, Reason} - end. - -auth_header_() -> - AppId = <<"admin">>, - AppSecret = <<"public">>, - auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). - -auth_header_(User, Pass) -> - Encoded = base64:encode_to_string(lists:append([User,":",Pass])), - {"Authorization","Basic " ++ Encoded}. - -api_path(Parts)-> - ?SERVER ++ filename:join([?BASE_PATH | Parts]). diff --git a/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl new file mode 100644 index 000000000..52d2bf626 --- /dev/null +++ b/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl @@ -0,0 +1,58 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_nodes_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-define(APP, emqx_management). + +-define(SERVER, "http://127.0.0.1:8081"). +-define(BASE_PATH, "/api/v5"). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ekka_mnesia:start(), + emqx_mgmt_auth:mnesia(boot), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([emqx_management]). + +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), + ok; +set_special_configs(_App) -> + ok. + +t_nodes_api(_) -> + NodesPath = emqx_mgmt_api_test_util:api_path(["nodes"]), + {ok, Nodes} = emqx_mgmt_api_test_util:request_api(get, NodesPath), + NodesResponse = emqx_json:decode(Nodes, [return_maps]), + LocalNodeInfo = hd(NodesResponse), + Node = binary_to_atom(maps:get(<<"node">>, LocalNodeInfo), utf8), + ?assertEqual(Node, node()), + + NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]), + {ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath), + NodeNameResponse = binary_to_atom(maps:get(<<"node">>, emqx_json:decode(NodeInfo, [return_maps])), utf8), + ?assertEqual(node(), NodeNameResponse). From 868b31d123186cc718ad935fc39d2eea1da526af Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 16:37:18 +0800 Subject: [PATCH 145/379] fix(test): update the testcases for emqx_mqtt_protocol_v5_SUITE --- apps/emqx/src/emqx_connection.erl | 13 ++-- apps/emqx/src/emqx_quic_connection.erl | 2 - .../emqx/test/emqx_mqtt_protocol_v5_SUITE.erl | 68 ++++++------------- 3 files changed, 29 insertions(+), 54 deletions(-) diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 59c735afc..9e688fc0f 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -466,14 +466,13 @@ handle_msg({Passive, _Sock}, State) handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{zone = Zone, listener = Listener} = State) -> - ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), + ActiveN = get_active_n(Zone, Listener), Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); %% Something sent handle_msg({inet_reply, _Sock, ok}, State = #state{zone = Zone, listener = Listener}) -> - case emqx_pd:get_counter(outgoing_pubs) > - emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of + case emqx_pd:get_counter(outgoing_pubs) > get_active_n(Zone, Listener) of true -> Pubs = emqx_pd:reset_counter(outgoing_pubs), Bytes = emqx_pd:reset_counter(outgoing_bytes), @@ -823,7 +822,7 @@ activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{transport = Transport, socket = Socket, zone = Zone, listener = Listener}) -> - ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), + ActiveN = get_active_n(Zone, Listener), case Transport:setopts(Socket, [{active, ActiveN}]) of ok -> {ok, State#state{sockstate = running}}; Error -> Error @@ -905,3 +904,9 @@ get_state(Pid) -> State = sys:get_state(Pid), maps:from_list(lists:zip(record_info(fields, state), tl(tuple_to_list(State)))). + +get_active_n(Zone, Listener) -> + case emqx_config:get([zones, Zone, listeners, Listener, type]) of + quic -> 100; + _ -> emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) + end. diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl index b83522c6e..cd41e74a7 100644 --- a/apps/emqx/src/emqx_quic_connection.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -21,6 +21,4 @@ ]). new_conn(Conn, {_L, COpts, _S}) when is_map(COpts) -> - new_conn(Conn, maps:to_list(COpts)); -new_conn(Conn, COpts) -> emqx_connection:start_link(emqx_quic_stream, Conn, COpts). diff --git a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl index 2f3048277..607bee44b 100644 --- a/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_protocol_v5_SUITE.erl @@ -217,10 +217,14 @@ t_connect_will_message(Config) -> ok = emqtt:disconnect(Client4). t_batch_subscribe(init, Config) -> + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), + emqx_config:put_listener_conf(default, mqtt_quic, [acl, enable], true), ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end), Config; t_batch_subscribe('end', _Config) -> + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], false), + emqx_config:put_listener_conf(default, mqtt_quic, [acl, enable], false), meck:unload(emqx_access_control). t_batch_subscribe(Config) -> @@ -284,52 +288,22 @@ t_connect_will_retain(Config) -> t_connect_idle_timeout(_Config) -> IdleTimeout = 2000, - emqx_zone:set_env(external, idle_timeout, IdleTimeout), - + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, idle_timeout], IdleTimeout), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, idle_timeout], IdleTimeout), {ok, Sock} = emqtt_sock:connect({127,0,0,1}, 1883, [], 60000), timer:sleep(IdleTimeout), ?assertMatch({error, closed}, emqtt_sock:recv(Sock,1024)). -t_connect_limit_timeout(init, Config) -> - ok = meck:new(proplists, [non_strict, passthrough, no_history, no_link, unstick]), - meck:expect(proplists, get_value, fun(active_n, _Options, _Default) -> 1; - (Arg1, ARg2, Arg3) -> meck:passthrough([Arg1, ARg2, Arg3]) - end), - Config; -t_connect_limit_timeout('end', _Config) -> - catch meck:unload(proplists). - -t_connect_limit_timeout(Config) -> - ConnFun = ?config(conn_fun, Config), - Topic = nth(1, ?TOPICS), - emqx_zone:set_env(external, publish_limit, {3, 5}), - - {ok, Client} = emqtt:start_link([{proto_ver, v5},{keepalive, 60} | Config]), - {ok, _} = emqtt:ConnFun(Client), - [ClientPid] = emqx_cm:lookup_channels(client_info(clientid, Client)), - - ?assertEqual(undefined, emqx_connection:info(limit_timer, sys:get_state(ClientPid))), - Payload = <<"t_shared_subscriptions_client_terminates_when_qos_eq_2">>, - {ok, 2} = emqtt:publish(Client, Topic, Payload, 1), - {ok, 3} = emqtt:publish(Client, Topic, Payload, 1), - {ok, 4} = emqtt:publish(Client, Topic, Payload, 1), - timer:sleep(250), - ?assert(is_reference(emqx_connection:info(limit_timer, sys:get_state(ClientPid)))), - - ok = emqtt:disconnect(Client), - emqx_zone:set_env(external, publish_limit, undefined), - meck:unload(proplists). - t_connect_emit_stats_timeout(init, Config) -> NewIdleTimeout = 1000, - OldIdleTimeout = emqx_zone:get_env(external, idle_timeout), - emqx_zone:set_env(external, idle_timeout, NewIdleTimeout), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, idle_timeout], NewIdleTimeout), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, idle_timeout], NewIdleTimeout), ok = snabbkaffe:start_trace(), - [{idle_timeout, NewIdleTimeout}, {old_idle_timeout, OldIdleTimeout} | Config]; -t_connect_emit_stats_timeout('end', Config) -> + [{idle_timeout, NewIdleTimeout} | Config]; +t_connect_emit_stats_timeout('end', _Config) -> snabbkaffe:stop(), - {_, OldIdleTimeout} = lists:keyfind(old_idle_timeout, 1, Config), - emqx_zone:set_env(external, idle_timeout, OldIdleTimeout), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, idle_timeout], 15000), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, idle_timeout], 15000), ok. t_connect_emit_stats_timeout(Config) -> @@ -497,7 +471,8 @@ t_connack_session_present(Config) -> t_connack_max_qos_allowed(init, Config) -> Config; t_connack_max_qos_allowed('end', _Config) -> - emqx_zone:set_env(external, max_qos_allowed, 2), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_qos_allowed], 2), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, max_qos_allowed], 2), ok. t_connack_max_qos_allowed(Config) -> ConnFun = ?config(conn_fun, Config), @@ -505,9 +480,8 @@ t_connack_max_qos_allowed(Config) -> Topic = nth(1, ?TOPICS), %% max_qos_allowed = 0 - emqx_zone:set_env(external, max_qos_allowed, 0), - persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), - persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_qos_allowed], 0), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, max_qos_allowed], 0), {ok, Client1} = emqtt:start_link([{proto_ver, v5} | Config]), {ok, Connack1} = emqtt:ConnFun(Client1), @@ -532,9 +506,8 @@ t_connack_max_qos_allowed(Config) -> waiting_client_process_exit(Client2), %% max_qos_allowed = 1 - emqx_zone:set_env(external, max_qos_allowed, 1), - persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), - persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_qos_allowed], 1), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, max_qos_allowed], 1), {ok, Client3} = emqtt:start_link([{proto_ver, v5} | Config]), {ok, Connack3} = emqtt:ConnFun(Client3), @@ -559,9 +532,8 @@ t_connack_max_qos_allowed(Config) -> waiting_client_process_exit(Client4), %% max_qos_allowed = 2 - emqx_zone:set_env(external, max_qos_allowed, 2), - persistent_term:erase({emqx_zone, external, '$mqtt_caps'}), - persistent_term:erase({emqx_zone, external, '$mqtt_pub_caps'}), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_qos_allowed], 2), + emqx_config:put_listener_conf(default, mqtt_quic, [mqtt, max_qos_allowed], 2), {ok, Client5} = emqtt:start_link([{proto_ver, v5} | Config]), {ok, Connack5} = emqtt:ConnFun(Client5), From 871353704ab0010cf949f21eb60d3b19a82f11da Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 16:38:06 +0800 Subject: [PATCH 146/379] fix(test): update the testcases for emqx_channel_SUITE --- apps/emqx/etc/emqx.conf | 2 +- apps/emqx/src/emqx_channel.erl | 29 +++++++++++++++------------ apps/emqx/src/emqx_schema.erl | 4 ++-- apps/emqx/test/emqx_channel_SUITE.erl | 3 ++- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 5c8bd5265..1a5ccf16c 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -879,7 +879,7 @@ zones.default { ## Maximum MQTT packet size allowed. ## ## @doc zones..mqtt.max_packet_size - ## ValueType: Bytes | infinity + ## ValueType: Bytes ## Default: 1MB max_packet_size: 1MB diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 971b4d5d4..98206c37a 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -193,8 +193,8 @@ stats(#channel{session = Session})-> emqx_session:stats(Session). -spec(caps(channel()) -> emqx_types:caps()). -caps(#channel{clientinfo = #{zone := Zone}}) -> - emqx_mqtt_caps:get_caps(Zone). +caps(#channel{clientinfo = #{zone := Zone, listener := Listener}}) -> + emqx_mqtt_caps:get_caps(Zone, Listener). %%-------------------------------------------------------------------- @@ -1193,8 +1193,8 @@ run_conn_hooks(ConnPkt, Channel = #channel{conninfo = ConnInfo}) -> %%-------------------------------------------------------------------- %% Check Connect Packet -check_connect(ConnPkt, #channel{clientinfo = #{zone := Zone}}) -> - emqx_packet:check(ConnPkt, emqx_mqtt_caps:get_caps(Zone)). +check_connect(ConnPkt, #channel{clientinfo = #{zone := Zone, listener := Listener}}) -> + emqx_packet:check(ConnPkt, emqx_mqtt_caps:get_caps(Zone, Listener)). %%-------------------------------------------------------------------- %% Enrich Client Info @@ -1434,8 +1434,8 @@ check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{topic_name = Topic} }, - #channel{clientinfo = #{zone := Zone}}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain, topic => Topic}). + #channel{clientinfo = #{zone := Zone, listener := Listener}}) -> + emqx_mqtt_caps:check_pub(Zone, Listener, #{qos => QoS, retain => Retain, topic => Topic}). %%-------------------------------------------------------------------- %% Check Sub ACL @@ -1463,8 +1463,9 @@ check_sub_acl(TopicFilter, #channel{clientinfo = ClientInfo}) -> %%-------------------------------------------------------------------- %% Check Sub Caps -check_sub_caps(TopicFilter, SubOpts, #channel{clientinfo = #{zone := Zone}}) -> - emqx_mqtt_caps:check_sub(Zone, TopicFilter, SubOpts). +check_sub_caps(TopicFilter, SubOpts, #channel{clientinfo = #{zone := Zone, + listener := Listener}}) -> + emqx_mqtt_caps:check_sub(Zone, Listener, TopicFilter, SubOpts). %%-------------------------------------------------------------------- %% Enrich SubId @@ -1486,14 +1487,15 @@ enrich_subopts(SubOpts, #channel{clientinfo = #{zone := Zone, is_bridge := IsBri %%-------------------------------------------------------------------- %% Enrich ConnAck Caps -enrich_connack_caps(AckProps, ?IS_MQTT_V5 = #channel{clientinfo = #{zone := Zone}}) -> +enrich_connack_caps(AckProps, ?IS_MQTT_V5 = #channel{clientinfo = #{ + zone := Zone, listener := Listener}}) -> #{max_packet_size := MaxPktSize, max_qos_allowed := MaxQoS, retain_available := Retain, max_topic_alias := MaxAlias, shared_subscription := Shared, wildcard_subscription := Wildcard - } = emqx_mqtt_caps:get_caps(Zone), + } = emqx_mqtt_caps:get_caps(Zone, Listener), NAckProps = AckProps#{'Retain-Available' => flag(Retain), 'Maximum-Packet-Size' => MaxPktSize, 'Topic-Alias-Maximum' => MaxAlias, @@ -1517,7 +1519,7 @@ enrich_connack_caps(AckProps, _Channel) -> AckProps. enrich_server_keepalive(AckProps, #channel{clientinfo = #{zone := Zone, listener := Listener}}) -> case get_mqtt_conf(Zone, Listener, server_keepalive) of - undefined -> AckProps; + disabled -> AckProps; Keepalive -> AckProps#{'Server-Keep-Alive' => Keepalive} end. @@ -1562,9 +1564,9 @@ ensure_connected(Channel = #channel{conninfo = ConnInfo, init_alias_maximum(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5, properties = Properties}, - #{zone := Zone} = _ClientInfo) -> + #{zone := Zone, listener := Listener} = _ClientInfo) -> #{outbound => emqx_mqtt_props:get('Topic-Alias-Maximum', Properties, 0), - inbound => emqx_mqtt_caps:get_caps(Zone, max_topic_alias, ?MAX_TOPIC_AlIAS) + inbound => maps:get(max_topic_alias, emqx_mqtt_caps:get_caps(Zone, Listener)) }; init_alias_maximum(_ConnPkt, _ClientInfo) -> undefined. @@ -1579,6 +1581,7 @@ ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel = #channel{conninfo ensure_keepalive(_AckProps, Channel = #channel{conninfo = ConnInfo}) -> ensure_keepalive_timer(maps:get(keepalive, ConnInfo), Channel). +ensure_keepalive_timer(0, Channel) -> Channel; ensure_keepalive_timer(disabled, Channel) -> Channel; ensure_keepalive_timer(Interval, Channel = #channel{clientinfo = #{zone := Zone, listener := Listener}}) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 738623f72..14626db79 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -256,7 +256,7 @@ fields("acl_cache") -> fields("mqtt") -> [ {"mountpoint", t(binary(), undefined, <<>>)} , {"idle_timeout", maybe_infinity(duration(), "15s")} - , {"max_packet_size", maybe_infinity(bytesize(), "1MB")} + , {"max_packet_size", t(bytesize(), undefined, "1MB")} , {"max_clientid_len", t(integer(), undefined, 65535)} , {"max_topic_levels", t(integer(), undefined, 65535)} , {"max_qos_allowed", t(range(0, 2), undefined, 2)} @@ -479,7 +479,7 @@ fields("sysmon") -> ]; fields("sysmon_vm") -> - [ {"process_check_interval", t(duration_s(), undefined, 30)} + [ {"process_check_interval", t(duration(), undefined, "30s")} , {"process_high_watermark", t(percent(), undefined, "80%")} , {"process_low_watermark", t(percent(), undefined, "60%")} , {"long_gc", maybe_disabled(duration())} diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index f9db11c27..b6b72b4de 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -206,7 +206,8 @@ end_per_suite(_Config) -> emqx_session, emqx_broker, emqx_hooks, - emqx_cm + emqx_cm, + emqx_banned ]). init_per_testcase(_TestCase, Config) -> From b123299c70c6f4b3563f83b474268b174340731a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 16:39:59 +0800 Subject: [PATCH 147/379] fix(config): make flapping work with the new config --- apps/emqx/src/emqx_alarm.erl | 4 ++-- apps/emqx/src/emqx_flapping.erl | 2 +- apps/emqx/src/emqx_os_mon.erl | 2 +- apps/emqx/test/emqx_broker_SUITE.erl | 18 +++++++++--------- apps/emqx/test/emqx_flapping_SUITE.erl | 14 ++++++++------ apps/emqx/test/emqx_session_SUITE.erl | 8 ++++---- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 0eaa16507..1ae985deb 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -382,9 +382,9 @@ normalize_message(high_system_memory_usage, #{high_watermark := HighWatermark}) normalize_message(high_process_memory_usage, #{high_watermark := HighWatermark}) -> list_to_binary(io_lib:format("Process memory usage is higher than ~p%", [HighWatermark])); normalize_message(high_cpu_usage, #{usage := Usage}) -> - list_to_binary(io_lib:format("~p% cpu usage", [Usage])); + list_to_binary(io_lib:format("~s cpu usage", [Usage])); normalize_message(too_many_processes, #{usage := Usage}) -> - list_to_binary(io_lib:format("~p% process usage", [Usage])); + list_to_binary(io_lib:format("~s process usage", [Usage])); normalize_message(partition, #{occurred := Node}) -> list_to_binary(io_lib:format("Partition occurs at node ~s", [Node])); normalize_message(<<"resource", _/binary>>, #{type := Type, id := ID}) -> diff --git a/apps/emqx/src/emqx_flapping.erl b/apps/emqx/src/emqx_flapping.erl index b48f43094..dcc88f6b4 100644 --- a/apps/emqx/src/emqx_flapping.erl +++ b/apps/emqx/src/emqx_flapping.erl @@ -125,7 +125,7 @@ handle_cast({detected, #flapping{clientid = ClientId, by = <<"flapping detector">>, reason = <<"flapping is detected">>, at = Now, - until = Now + Interval}, + until = Now + (Interval div 1000)}, emqx_banned:create(Banned); false -> ?LOG(warning, "~s(~s) disconnected ~w times in ~wms", diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index 8768586ce..3e161030c 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -143,7 +143,7 @@ handle_info({timeout, _Timer, check}, State) -> case emqx_vm:cpu_util() of %% TODO: should be improved? 0 -> ok; Busy when Busy >= CPUHighWatermark -> - emqx_alarm:activate(high_cpu_usage, #{usage => Busy, + emqx_alarm:activate(high_cpu_usage, #{usage => io_lib:format("~p%", [Busy]), high_watermark => CPUHighWatermark, low_watermark => CPULowWatermark}), start_check_timer(); diff --git a/apps/emqx/test/emqx_broker_SUITE.erl b/apps/emqx/test/emqx_broker_SUITE.erl index d6fa36c18..fe754e9df 100644 --- a/apps/emqx/test/emqx_broker_SUITE.erl +++ b/apps/emqx/test/emqx_broker_SUITE.erl @@ -42,19 +42,19 @@ end_per_suite(_Config) -> %%-------------------------------------------------------------------- t_stats_fun(_) -> - ?assertEqual(0, emqx_stats:getstat('subscribers.count')), - ?assertEqual(0, emqx_stats:getstat('subscriptions.count')), - ?assertEqual(0, emqx_stats:getstat('suboptions.count')), + Subscribers = emqx_stats:getstat('subscribers.count'), + Subscriptions = emqx_stats:getstat('subscriptions.count'), + Subopts = emqx_stats:getstat('suboptions.count'), ok = emqx_broker:subscribe(<<"topic">>, <<"clientid">>), ok = emqx_broker:subscribe(<<"topic2">>, <<"clientid">>), emqx_broker:stats_fun(), ct:sleep(10), - ?assertEqual(2, emqx_stats:getstat('subscribers.count')), - ?assertEqual(2, emqx_stats:getstat('subscribers.max')), - ?assertEqual(2, emqx_stats:getstat('subscriptions.count')), - ?assertEqual(2, emqx_stats:getstat('subscriptions.max')), - ?assertEqual(2, emqx_stats:getstat('suboptions.count')), - ?assertEqual(2, emqx_stats:getstat('suboptions.max')). + ?assertEqual(Subscribers + 2, emqx_stats:getstat('subscribers.count')), + ?assertEqual(Subscribers + 2, emqx_stats:getstat('subscribers.max')), + ?assertEqual(Subscriptions + 2, emqx_stats:getstat('subscriptions.count')), + ?assertEqual(Subscriptions + 2, emqx_stats:getstat('subscriptions.max')), + ?assertEqual(Subopts + 2, emqx_stats:getstat('suboptions.count')), + ?assertEqual(Subopts + 2, emqx_stats:getstat('suboptions.max')). t_subscribed(_) -> emqx_broker:subscribe(<<"topic">>), diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index e5b12a122..b4318ff64 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -28,8 +28,8 @@ init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), emqx_config:put_listener_conf(default, mqtt_tcp, [flapping_detect], #{max_count => 3, - window_time => 100, - ban_time => 2 + window_time => 100, % 0.1s + ban_time => 2000 %% 2s }), Config. @@ -41,7 +41,7 @@ end_per_suite(_Config) -> t_detect_check(_) -> ClientInfo = #{zone => default, listener => mqtt_tcp, - clientid => <<"clientid">>, + clientid => <<"client007">>, peerhost => {127,0,0,1} }, false = emqx_flapping:detect(ClientInfo), @@ -50,6 +50,8 @@ t_detect_check(_) -> false = emqx_banned:check(ClientInfo), true = emqx_flapping:detect(ClientInfo), timer:sleep(50), + ct:pal("the table emqx_banned: ~p, nowsec: ~p", [ets:tab2list(emqx_banned), + erlang:system_time(second)]), true = emqx_banned:check(ClientInfo), timer:sleep(3000), false = emqx_banned:check(ClientInfo), @@ -63,11 +65,11 @@ t_detect_check(_) -> t_expired_detecting(_) -> ClientInfo = #{zone => default, listener => mqtt_tcp, - clientid => <<"clientid">>, + clientid => <<"client008">>, peerhost => {127,0,0,1}}, false = emqx_flapping:detect(ClientInfo), - ?assertEqual(true, lists:any(fun({flapping, <<"clientid">>, _, _, _}) -> true; + ?assertEqual(true, lists:any(fun({flapping, <<"client008">>, _, _, _}) -> true; (_) -> false end, ets:tab2list(emqx_flapping))), timer:sleep(200), - ?assertEqual(true, lists:all(fun({flapping, <<"clientid">>, _, _, _}) -> false; + ?assertEqual(true, lists:all(fun({flapping, <<"client008">>, _, _, _}) -> false; (_) -> true end, ets:tab2list(emqx_flapping))). \ No newline at end of file diff --git a/apps/emqx/test/emqx_session_SUITE.erl b/apps/emqx/test/emqx_session_SUITE.erl index 5c96c96df..87dd66183 100644 --- a/apps/emqx/test/emqx_session_SUITE.erl +++ b/apps/emqx/test/emqx_session_SUITE.erl @@ -54,7 +54,7 @@ t_session_init(_) -> #{receive_maximum => 64}), ?assertEqual(#{}, emqx_session:info(subscriptions, Session)), ?assertEqual(0, emqx_session:info(subscriptions_cnt, Session)), - ?assertEqual(0, emqx_session:info(subscriptions_max, Session)), + ?assertEqual(infinity, emqx_session:info(subscriptions_max, Session)), ?assertEqual(false, emqx_session:info(upgrade_qos, Session)), ?assertEqual(0, emqx_session:info(inflight_cnt, Session)), ?assertEqual(64, emqx_session:info(inflight_max, Session)), @@ -73,13 +73,13 @@ t_session_init(_) -> t_session_info(_) -> ?assertMatch(#{subscriptions := #{}, upgrade_qos := false, - retry_interval := 0, + retry_interval := 30, await_rel_timeout := 300 }, emqx_session:info(session())). t_session_stats(_) -> Stats = emqx_session:stats(session()), - ?assertMatch(#{subscriptions_max := 0, + ?assertMatch(#{subscriptions_max := infinity, inflight_max := 0, mqueue_len := 0, mqueue_max := 1000, @@ -153,7 +153,7 @@ t_publish_qos2_with_error_return(_) -> {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} = emqx_session:publish(3, Msg, Session1). t_is_awaiting_full_false(_) -> - Session = session(#{max_awaiting_rel => 0}), + Session = session(#{max_awaiting_rel => infinity}), ?assertNot(emqx_session:is_awaiting_full(Session)). t_is_awaiting_full_true(_) -> From a0bddfc834fc53539231bde1e904d13e8f7cc5b1 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 16:41:20 +0800 Subject: [PATCH 148/379] fix(config): start quic connection with zone and listener names --- apps/emqx/src/emqx_listeners.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index bc0fc24b2..8e6dc1ab1 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -97,6 +97,8 @@ do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Op ConnectionOpts = #{conn_callback => emqx_quic_connection , peer_unidi_stream_count => 1 , peer_bidi_stream_count => 10 + , zone => ZoneName + , listener => ListenerName }, StreamOpts = [], quicer:start_listener(listener_id(ZoneName, ListenerName), From 01c4c655fbe662fc4a1e310d7c1d14f607114907 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 16:42:32 +0800 Subject: [PATCH 149/379] fix(config): make emqx_caps work with the new config struct --- apps/emqx/src/emqx_mqtt_caps.erl | 71 +++++++------------------------- 1 file changed, 15 insertions(+), 56 deletions(-) diff --git a/apps/emqx/src/emqx_mqtt_caps.erl b/apps/emqx/src/emqx_mqtt_caps.erl index b1be5d5a5..883f7354d 100644 --- a/apps/emqx/src/emqx_mqtt_caps.erl +++ b/apps/emqx/src/emqx_mqtt_caps.erl @@ -20,19 +20,13 @@ -include("emqx_mqtt.hrl"). -include("types.hrl"). --export([ check_pub/2 - , check_sub/3 +-export([ check_pub/3 + , check_sub/4 ]). --export([ get_caps/1 - , get_caps/2 - , get_caps/3 +-export([ get_caps/2 ]). --export([default_caps/0]). - --export([default/0]). - -export_type([caps/0]). -type(caps() :: #{max_packet_size => integer(), @@ -46,7 +40,7 @@ shared_subscription => boolean() }). --define(UNLIMITED, 0). +-define(MAX_TOPIC_LEVELS, 65535). -define(PUBCAP_KEYS, [max_topic_levels, max_qos_allowed, @@ -62,7 +56,7 @@ -define(DEFAULT_CAPS, #{max_packet_size => ?MAX_PACKET_SIZE, max_clientid_len => ?MAX_CLIENTID_LEN, max_topic_alias => ?MAX_TOPIC_AlIAS, - max_topic_levels => ?UNLIMITED, + max_topic_levels => ?MAX_TOPIC_LEVELS, max_qos_allowed => ?QOS_2, retain_available => true, wildcard_subscription => true, @@ -70,18 +64,18 @@ shared_subscription => true }). --spec(check_pub(emqx_types:zone(), +-spec(check_pub(emqx_types:zone(), atom(), #{qos := emqx_types:qos(), retain := boolean(), topic := emqx_topic:topic()}) -> ok_or_error(emqx_types:reason_code())). -check_pub(Zone, Flags) when is_map(Flags) -> +check_pub(Zone, Listener, Flags) when is_map(Flags) -> do_check_pub(case maps:take(topic, Flags) of {Topic, Flags1} -> Flags1#{topic_levels => emqx_topic:levels(Topic)}; error -> Flags - end, get_caps(Zone, publish)). + end, maps:with(?PUBCAP_KEYS, get_caps(Zone, Listener))). do_check_pub(#{topic_levels := Levels}, #{max_topic_levels := Limit}) when Limit > 0, Levels > Limit -> @@ -93,12 +87,12 @@ do_check_pub(#{retain := true}, #{retain_available := false}) -> {error, ?RC_RETAIN_NOT_SUPPORTED}; do_check_pub(_Flags, _Caps) -> ok. --spec(check_sub(emqx_types:zone(), +-spec(check_sub(emqx_types:zone(), atom(), emqx_types:topic(), emqx_types:subopts()) -> ok_or_error(emqx_types:reason_code())). -check_sub(Zone, Topic, SubOpts) -> - Caps = get_caps(Zone, subscribe), +check_sub(Zone, Listener, Topic, SubOpts) -> + Caps = maps:with(?SUBCAP_KEYS, get_caps(Zone, Listener)), Flags = lists:foldl( fun(max_topic_levels, Map) -> Map#{topic_levels => emqx_topic:levels(Topic)}; @@ -119,42 +113,7 @@ do_check_sub(#{is_shared := true}, #{shared_subscription := false}) -> {error, ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}; do_check_sub(_Flags, _Caps) -> ok. -default_caps() -> - ?DEFAULT_CAPS. - -get_caps(Zone, Cap, Def) -> - emqx_zone:get_env(Zone, Cap, Def). - -get_caps(Zone, publish) -> - with_env(Zone, '$mqtt_pub_caps', - fun() -> - filter_caps(?PUBCAP_KEYS, get_caps(Zone)) - end); - -get_caps(Zone, subscribe) -> - with_env(Zone, '$mqtt_sub_caps', - fun() -> - filter_caps(?SUBCAP_KEYS, get_caps(Zone)) - end). - -get_caps(Zone) -> - with_env(Zone, '$mqtt_caps', - fun() -> - maps:map(fun(Cap, Def) -> - emqx_zone:get_env(Zone, Cap, Def) - end, ?DEFAULT_CAPS) - end). - -filter_caps(Keys, Caps) -> - maps:filter(fun(Key, _Val) -> lists:member(Key, Keys) end, Caps). - --spec(default() -> caps()). -default() -> ?DEFAULT_CAPS. - -with_env(Zone, Key, InitFun) -> - case emqx_zone:get_env(Zone, Key) of - undefined -> Caps = InitFun(), - ok = emqx_zone:set_env(Zone, Key, Caps), - Caps; - ZoneCaps -> ZoneCaps - end. +get_caps(Zone, Listener) -> + lists:foldl(fun({K, V}, Acc) -> + Acc#{K => emqx_config:get_listener_conf(Zone, Listener, [mqtt, K], V)} + end, #{}, maps:to_list(?DEFAULT_CAPS)). From 3aaf7041a6fc0906d5a7c10e1ee20dc41cccbbfe Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 17:41:15 +0800 Subject: [PATCH 150/379] fix(test): update the testcases for emqx_os_mon_SUITE --- apps/emqx/src/emqx_os_mon.erl | 47 +------------------------- apps/emqx/test/emqx_os_mon_SUITE.erl | 50 +++++++++++----------------- 2 files changed, 20 insertions(+), 77 deletions(-) diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index 3e161030c..ee509e54f 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -24,13 +24,7 @@ -export([start_link/0]). --export([ get_cpu_check_interval/0 - , set_cpu_check_interval/1 - , get_cpu_high_watermark/0 - , set_cpu_high_watermark/1 - , get_cpu_low_watermark/0 - , set_cpu_low_watermark/1 - , get_mem_check_interval/0 +-export([ get_mem_check_interval/0 , set_mem_check_interval/1 , get_sysmem_high_watermark/0 , set_sysmem_high_watermark/1 @@ -58,24 +52,6 @@ start_link() -> %% API %%-------------------------------------------------------------------- -get_cpu_check_interval() -> - call(get_cpu_check_interval). - -set_cpu_check_interval(Seconds) -> - call({set_cpu_check_interval, Seconds}). - -get_cpu_high_watermark() -> - call(get_cpu_high_watermark). - -set_cpu_high_watermark(Float) -> - call({set_cpu_high_watermark, Float}). - -get_cpu_low_watermark() -> - call(get_cpu_low_watermark). - -set_cpu_low_watermark(Float) -> - call({set_cpu_low_watermark, Float}). - get_mem_check_interval() -> memsup:get_check_interval() div 1000. @@ -96,9 +72,6 @@ get_procmem_high_watermark() -> set_procmem_high_watermark(Float) -> memsup:set_procmem_high_watermark(Float). -call(Req) -> - gen_server:call(?OS_MON, Req, infinity). - %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- @@ -111,24 +84,6 @@ init([]) -> start_check_timer(), {ok, #{}}. -handle_call(get_cpu_check_interval, _From, State) -> - {reply, maps:get(cpu_check_interval, State, undefined), State}; - -handle_call({set_cpu_check_interval, Seconds}, _From, State) -> - {reply, ok, State#{cpu_check_interval := Seconds}}; - -handle_call(get_cpu_high_watermark, _From, State) -> - {reply, maps:get(cpu_high_watermark, State, undefined), State}; - -handle_call({set_cpu_high_watermark, Float}, _From, State) -> - {reply, ok, State#{cpu_high_watermark := Float}}; - -handle_call(get_cpu_low_watermark, _From, State) -> - {reply, maps:get(cpu_low_watermark, State, undefined), State}; - -handle_call({set_cpu_low_watermark, Float}, _From, State) -> - {reply, ok, State#{cpu_low_watermark := Float}}; - handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, State}. diff --git a/apps/emqx/test/emqx_os_mon_SUITE.erl b/apps/emqx/test/emqx_os_mon_SUITE.erl index f7abd094b..6c9ac51c2 100644 --- a/apps/emqx/test/emqx_os_mon_SUITE.erl +++ b/apps/emqx/test/emqx_os_mon_SUITE.erl @@ -24,46 +24,34 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> + emqx_config:put([sysmon, os], #{ + cpu_check_interval => 60,cpu_high_watermark => 0.8, + cpu_low_watermark => 0.6,mem_check_interval => 60, + procmem_high_watermark => 0.05,sysmem_high_watermark => 0.7}), application:ensure_all_started(os_mon), Config. end_per_suite(_Config) -> application:stop(os_mon). -% t_set_mem_check_interval(_) -> -% error('TODO'). - -% t_set_sysmem_high_watermark(_) -> -% error('TODO'). - -% t_set_procmem_high_watermark(_) -> -% error('TODO'). - t_api(_) -> gen_event:swap_handler(alarm_handler, {emqx_alarm_handler, swap}, {alarm_handler, []}), - {ok, _} = emqx_os_mon:start_link([{cpu_check_interval, 1}, - {cpu_high_watermark, 5}, - {cpu_low_watermark, 80}, - {mem_check_interval, 60}, - {sysmem_high_watermark, 70}, - {procmem_high_watermark, 5}]), - ?assertEqual(1, emqx_os_mon:get_cpu_check_interval()), - ?assertEqual(5, emqx_os_mon:get_cpu_high_watermark()), - ?assertEqual(80, emqx_os_mon:get_cpu_low_watermark()), - ?assertEqual(60, emqx_os_mon:get_mem_check_interval()), - ?assertEqual(70, emqx_os_mon:get_sysmem_high_watermark()), - ?assertEqual(5, emqx_os_mon:get_procmem_high_watermark()), - % timer:sleep(2000), - % ?assertEqual(true, lists:keymember(cpu_high_watermark, 1, alarm_handler:get_alarms())), + {ok, _} = emqx_os_mon:start_link(), + + ?assertEqual(60, emqx_os_mon:get_mem_check_interval()), + ?assertEqual(ok, emqx_os_mon:set_mem_check_interval(30)), + ?assertEqual(60, emqx_os_mon:get_mem_check_interval()), + ?assertEqual(ok, emqx_os_mon:set_mem_check_interval(122)), + ?assertEqual(120, emqx_os_mon:get_mem_check_interval()), + + ?assertEqual(70, emqx_os_mon:get_sysmem_high_watermark()), + ?assertEqual(ok, emqx_os_mon:set_sysmem_high_watermark(0.8)), + ?assertEqual(80, emqx_os_mon:get_sysmem_high_watermark()), + + ?assertEqual(5, emqx_os_mon:get_procmem_high_watermark()), + ?assertEqual(ok, emqx_os_mon:set_procmem_high_watermark(0.11)), + ?assertEqual(11, emqx_os_mon:get_procmem_high_watermark()), - emqx_os_mon:set_cpu_check_interval(0.05), - emqx_os_mon:set_cpu_high_watermark(80), - emqx_os_mon:set_cpu_low_watermark(75), - ?assertEqual(0.05, emqx_os_mon:get_cpu_check_interval()), - ?assertEqual(80, emqx_os_mon:get_cpu_high_watermark()), - ?assertEqual(75, emqx_os_mon:get_cpu_low_watermark()), - % timer:sleep(3000), - % ?assertEqual(false, lists:keymember(cpu_high_watermark, 1, alarm_handler:get_alarms())), ?assertEqual(ignored, gen_server:call(emqx_os_mon, ignored)), ?assertEqual(ok, gen_server:cast(emqx_os_mon, ignored)), emqx_os_mon ! ignored, From e4e7eb81e44ec045bd71f667c4402d330a9b0add Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 17:42:44 +0800 Subject: [PATCH 151/379] fix(test): update the testcases for emqx_mqtt_caps_SUITE --- apps/emqx/test/emqx_channel_SUITE.erl | 4 +-- apps/emqx/test/emqx_mqtt_caps_SUITE.erl | 37 ++++++++++++------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index b6b72b4de..86f565ea7 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -231,7 +231,7 @@ t_chan_caps(_) -> #{max_clientid_len := 65535, max_qos_allowed := 2, max_topic_alias := 65535, - max_topic_levels := 0, + max_topic_levels := 65535, retain_available := true, shared_subscription := true, subscription_identifiers := true, @@ -871,7 +871,7 @@ t_check_sub_acls(_) -> t_enrich_connack_caps(_) -> ok = meck:new(emqx_mqtt_caps, [passthrough, no_history]), ok = meck:expect(emqx_mqtt_caps, get_caps, - fun(_Zone) -> + fun(_Zone, _Listener) -> #{max_packet_size => 1024, max_qos_allowed => ?QOS_2, retain_available => true, diff --git a/apps/emqx/test/emqx_mqtt_caps_SUITE.erl b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl index d6cd5925b..ac6b71c9f 100644 --- a/apps/emqx/test/emqx_mqtt_caps_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl @@ -25,39 +25,36 @@ all() -> emqx_ct:all(?MODULE). t_check_pub(_) -> - PubCaps = #{max_qos_allowed => ?QOS_1, - retain_available => false - }, - emqx_zone:set_env(zone, '$mqtt_pub_caps', PubCaps), + OldConf = emqx_config:get(), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_qos_allowed], ?QOS_1), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, retain_available], false), timer:sleep(50), - ok = emqx_mqtt_caps:check_pub(zone, #{qos => ?QOS_1, - retain => false}), + ok = emqx_mqtt_caps:check_pub(default, mqtt_tcp, #{qos => ?QOS_1, retain => false}), PubFlags1 = #{qos => ?QOS_2, retain => false}, ?assertEqual({error, ?RC_QOS_NOT_SUPPORTED}, - emqx_mqtt_caps:check_pub(zone, PubFlags1)), + emqx_mqtt_caps:check_pub(default, mqtt_tcp, PubFlags1)), PubFlags2 = #{qos => ?QOS_1, retain => true}, ?assertEqual({error, ?RC_RETAIN_NOT_SUPPORTED}, - emqx_mqtt_caps:check_pub(zone, PubFlags2)), - emqx_zone:unset_env(zone, '$mqtt_pub_caps'). + emqx_mqtt_caps:check_pub(default, mqtt_tcp, PubFlags2)), + emqx_config:put(OldConf). t_check_sub(_) -> + OldConf = emqx_config:get(), SubOpts = #{rh => 0, rap => 0, nl => 0, qos => ?QOS_2 }, - SubCaps = #{max_topic_levels => 2, - max_qos_allowed => ?QOS_2, - shared_subscription => false, - wildcard_subscription => false - }, - emqx_zone:set_env(zone, '$mqtt_sub_caps', SubCaps), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_topic_levels], 2), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, max_qos_allowed], ?QOS_1), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, shared_subscription], false), + emqx_config:put_listener_conf(default, mqtt_tcp, [mqtt, wildcard_subscription], false), timer:sleep(50), - ok = emqx_mqtt_caps:check_sub(zone, <<"topic">>, SubOpts), + ok = emqx_mqtt_caps:check_sub(default, mqtt_tcp, <<"topic">>, SubOpts), ?assertEqual({error, ?RC_TOPIC_FILTER_INVALID}, - emqx_mqtt_caps:check_sub(zone, <<"a/b/c/d">>, SubOpts)), + emqx_mqtt_caps:check_sub(default, mqtt_tcp, <<"a/b/c/d">>, SubOpts)), ?assertEqual({error, ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}, - emqx_mqtt_caps:check_sub(zone, <<"+/#">>, SubOpts)), + emqx_mqtt_caps:check_sub(default, mqtt_tcp, <<"+/#">>, SubOpts)), ?assertEqual({error, ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}, - emqx_mqtt_caps:check_sub(zone, <<"topic">>, SubOpts#{share => true})), - emqx_zone:unset_env(zone, '$mqtt_pub_caps'). + emqx_mqtt_caps:check_sub(default, mqtt_tcp, <<"topic">>, SubOpts#{share => true})), + emqx_config:put(OldConf). From 7d9321a14193cabfba3eb9641a642057ec740f9d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 18:39:57 +0800 Subject: [PATCH 152/379] fix(test): apply default configs for emqx_session_SUITE --- apps/emqx/test/emqx_channel_SUITE.erl | 4 +-- apps/emqx/test/emqx_session_SUITE.erl | 5 +-- apps/emqx/test/emqx_ws_connection_SUITE.erl | 40 ++++++++------------- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 86f565ea7..6b468c480 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -42,8 +42,8 @@ mqtt_conf() -> max_topic_alias => 65535, max_topic_levels => 65535, mountpoint => <<>>, - mqueue_default_priority => highest, - mqueue_priorities => [], + mqueue_default_priority => lowest, + mqueue_priorities => #{}, mqueue_store_qos0 => true, peer_cert_as_clientid => disabled, peer_cert_as_username => disabled, diff --git a/apps/emqx/test/emqx_session_SUITE.erl b/apps/emqx/test/emqx_session_SUITE.erl index 87dd66183..67d06c281 100644 --- a/apps/emqx/test/emqx_session_SUITE.erl +++ b/apps/emqx/test/emqx_session_SUITE.erl @@ -29,6 +29,7 @@ all() -> emqx_ct:all(?MODULE). %%-------------------------------------------------------------------- init_per_suite(Config) -> + emqx_channel_SUITE:set_default_zone_conf(), ok = meck:new([emqx_hooks, emqx_metrics, emqx_broker], [passthrough, no_history, no_link]), ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end), @@ -59,7 +60,7 @@ t_session_init(_) -> ?assertEqual(0, emqx_session:info(inflight_cnt, Session)), ?assertEqual(64, emqx_session:info(inflight_max, Session)), ?assertEqual(1, emqx_session:info(next_pkt_id, Session)), - ?assertEqual(0, emqx_session:info(retry_interval, Session)), + ?assertEqual(30, emqx_session:info(retry_interval, Session)), ?assertEqual(0, emqx_mqueue:len(emqx_session:info(mqueue, Session))), ?assertEqual(0, emqx_session:info(awaiting_rel_cnt, Session)), ?assertEqual(100, emqx_session:info(awaiting_rel_max, Session)), @@ -100,7 +101,7 @@ t_subscribe(_) -> ?assertEqual(1, emqx_session:info(subscriptions_cnt, Session)). t_is_subscriptions_full_false(_) -> - Session = session(#{max_subscriptions => 0}), + Session = session(#{max_subscriptions => infinity}), ?assertNot(emqx_session:is_subscriptions_full(Session)). t_is_subscriptions_full_true(_) -> diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index cfa45f1ad..73e84633a 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -55,13 +55,6 @@ init_per_testcase(TestCase, Config) when ok = meck:expect(cowboy_req, sock, fun(_) -> {{127,0,0,1}, 18083} end), ok = meck:expect(cowboy_req, cert, fun(_) -> undefined end), ok = meck:expect(cowboy_req, parse_cookies, fun(_) -> error(badarg) end), - %% Mock emqx_zone - ok = meck:new(emqx_zone, [passthrough, no_history, no_link]), - ok = meck:expect(emqx_zone, oom_policy, - fun(_) -> #{max_heap_size => 838860800, - message_queue_len => 8000 - } - end), %% Mock emqx_access_control ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), @@ -96,7 +89,6 @@ end_per_testcase(TestCase, _Config) when -> lists:foreach(fun meck:unload/1, [cowboy_req, - emqx_zone, emqx_access_control, emqx_broker, emqx_hooks, @@ -124,12 +116,16 @@ t_info(_) -> sockstate := running } = SockInfo. +set_ws_opts(Key, Val) -> + emqx_config:put_listener_conf(default, mqtt_ws, [websocket, Key], Val). + t_header(_) -> - ok = meck:expect(cowboy_req, header, fun(<<"x-forwarded-for">>, _, _) -> <<"100.100.100.100, 99.99.99.99">>; - (<<"x-forwarded-port">>, _, _) -> <<"1000">> end), - {ok, St, _} = ?ws_conn:websocket_init([req, [{zone, external}, - {proxy_address_header, <<"x-forwarded-for">>}, - {proxy_port_header, <<"x-forwarded-port">>}]]), + ok = meck:expect(cowboy_req, header, + fun(<<"x-forwarded-for">>, _, _) -> <<"100.100.100.100, 99.99.99.99">>; + (<<"x-forwarded-port">>, _, _) -> <<"1000">> end), + set_ws_opts(proxy_address_header, <<"x-forwarded-for">>), + set_ws_opts(proxy_port_header, <<"x-forwarded-port">>), + {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => mqtt_ws}]), WsPid = spawn(fun() -> receive {call, From, info} -> gen_server:reply(From, ?ws_conn:info(St)) @@ -450,15 +446,6 @@ t_run_gc(_) -> WsSt = st(#{gc_state => GcSt}), ?ws_conn:run_gc(#{cnt => 100, oct => 10000}, WsSt). -t_check_oom(_) -> - %%Policy = #{max_heap_size => 10, message_queue_len => 10}, - %%meck:expect(emqx_zone, oom_policy, fun(_) -> Policy end), - _St = ?ws_conn:check_oom(st()), - ok = timer:sleep(10). - %%receive {shutdown, proc_heap_too_large} -> ok - %%after 0 -> error(expect_shutdown) - %%end. - t_enqueue(_) -> Packet = ?PUBLISH_PACKET(?QOS_0), St = ?ws_conn:enqueue(Packet, st()), @@ -473,7 +460,7 @@ t_shutdown(_) -> st() -> st(#{}). st(InitFields) when is_map(InitFields) -> - {ok, St, _} = ?ws_conn:websocket_init([req, [{zone, external}]]), + {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => mqtt_ws}]), maps:fold(fun(N, V, S) -> ?ws_conn:set_field(N, V, S) end, ?ws_conn:set_field(channel, channel(), St), InitFields @@ -493,7 +480,8 @@ channel(InitFields) -> receive_maximum => 100, expiry_interval => 0 }, - ClientInfo = #{zone => zone, + ClientInfo = #{zone => default, + listener => mqtt_ws, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -502,13 +490,13 @@ channel(InitFields) -> peercert => undefined, mountpoint => undefined }, - Session = emqx_session:init(#{zone => default, listener => mqtt_tcp}, + Session = emqx_session:init(#{zone => default, listener => mqtt_ws}, #{receive_maximum => 0} ), maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, [{zone, zone}]), + emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_ws}), maps:merge(#{clientinfo => ClientInfo, session => Session, conn_state => connected From 25d76168811c78b4cedfe11ba8bf68de3d75d9cb Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 13 Jul 2021 19:58:54 +0800 Subject: [PATCH 153/379] fix(test): update the testcases for emqx_ws_connection_SUITE --- apps/emqx/test/emqx_ws_connection_SUITE.erl | 56 +++++++-------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index 73e84633a..a00442de6 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -48,6 +48,7 @@ init_per_testcase(TestCase, Config) when TestCase =/= t_ws_pingreq_before_connected, TestCase =/= t_ws_non_check_origin -> + emqx_channel_SUITE:set_default_zone_conf(), %% Mock cowboy_req ok = meck:new(cowboy_req, [passthrough, no_history, no_link]), ok = meck:expect(cowboy_req, header, fun(_, _, _) -> <<>> end), @@ -78,6 +79,7 @@ init_per_testcase(TestCase, Config) when Config; init_per_testcase(_, Config) -> + ok = emqx_ct_helpers:start_apps([]), Config. end_per_testcase(TestCase, _Config) when @@ -96,6 +98,7 @@ end_per_testcase(TestCase, _Config) when ]); end_per_testcase(_, Config) -> + emqx_ct_helpers:stop_apps([]), Config. %%-------------------------------------------------------------------- @@ -110,7 +113,6 @@ t_info(_) -> end), #{sockinfo := SockInfo} = ?ws_conn:call(WsPid, info), #{socktype := ws, - active_n := 100, peername := {{127,0,0,1}, 3456}, sockname := {{127,0,0,1}, 18083}, sockstate := running @@ -171,12 +173,10 @@ t_call(_) -> ?assertEqual(Info, ?ws_conn:call(WsPid, info)). t_ws_pingreq_before_connected(_) -> - ok = emqx_ct_helpers:start_apps([]), {ok, _} = application:ensure_all_started(gun), {ok, WPID} = gun:open("127.0.0.1", 8083), ws_pingreq(#{}), - gun:close(WPID), - emqx_ct_helpers:stop_apps([]). + gun:close(WPID). ws_pingreq(State) -> receive @@ -205,14 +205,11 @@ ws_pingreq(State) -> end. t_ws_sub_protocols_mqtt(_) -> - ok = emqx_ct_helpers:start_apps([]), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, - start_ws_client(#{protocols => [<<"mqtt">>]})), - emqx_ct_helpers:stop_apps([]). + start_ws_client(#{protocols => [<<"mqtt">>]})). t_ws_sub_protocols_mqtt_equivalents(_) -> - ok = emqx_ct_helpers:start_apps([]), {ok, _} = application:ensure_all_started(gun), %% also support mqtt-v3, mqtt-v3.1.1, mqtt-v5 ?assertMatch({gun_upgrade, _}, @@ -222,58 +219,39 @@ t_ws_sub_protocols_mqtt_equivalents(_) -> ?assertMatch({gun_upgrade, _}, start_ws_client(#{protocols => [<<"mqtt-v5">>]})), ?assertMatch({gun_response, {_, 400, _}}, - start_ws_client(#{protocols => [<<"not-mqtt">>]})), - emqx_ct_helpers:stop_apps([]). + start_ws_client(#{protocols => [<<"not-mqtt">>]})). t_ws_check_origin(_) -> - emqx_ct_helpers:start_apps([], - fun(emqx) -> - {ok, Listeners} = application:get_env(emqx, listeners), - NListeners = lists:map(fun(#{listen_on := 8083, opts := Opts} = Listener) -> - NOpts = proplists:delete(check_origin_enable, Opts), - Listener#{opts => [{check_origin_enable, true} | NOpts]}; - (Listener) -> - Listener - end, Listeners), - application:set_env(emqx, listeners, NListeners), - ok; - (_) -> ok - end), + emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origin_enable], true), + emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origins], + [<<"http://localhost:18083">>]), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, start_ws_client(#{protocols => [<<"mqtt">>], headers => [{<<"origin">>, <<"http://localhost:18083">>}]})), ?assertMatch({gun_response, {_, 500, _}}, start_ws_client(#{protocols => [<<"mqtt">>], - headers => [{<<"origin">>, <<"http://localhost:18080">>}]})), - emqx_ct_helpers:stop_apps([]). + headers => [{<<"origin">>, <<"http://localhost:18080">>}]})). t_ws_non_check_origin(_) -> - emqx_ct_helpers:start_apps([]), + emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origin_enable], false), + emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origins], []), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, start_ws_client(#{protocols => [<<"mqtt">>], headers => [{<<"origin">>, <<"http://localhost:18083">>}]})), ?assertMatch({gun_upgrade, _}, start_ws_client(#{protocols => [<<"mqtt">>], - headers => [{<<"origin">>, <<"http://localhost:18080">>}]})), - emqx_ct_helpers:stop_apps([]). - + headers => [{<<"origin">>, <<"http://localhost:18080">>}]})). t_init(_) -> - Opts = [{idle_timeout, 300000}, - {fail_if_no_subprotocol, false}, - {supported_subprotocols, ["mqtt"]}], - WsOpts = #{compress => false, - deflate_opts => #{}, - max_frame_size => infinity, - idle_timeout => 300000 - }, + Opts = #{listener => mqtt_ws, zone => default}, ok = meck:expect(cowboy_req, parse_header, fun(_, req) -> undefined end), - {cowboy_websocket, req, [req, Opts], WsOpts} = ?ws_conn:init(req, Opts), + ok = meck:expect(cowboy_req, reply, fun(_, Req) -> Req end), + {ok, req, _} = ?ws_conn:init(req, Opts), ok = meck:expect(cowboy_req, parse_header, fun(_, req) -> [<<"mqtt">>] end), ok = meck:expect(cowboy_req, set_resp_header, fun(_, <<"mqtt">>, req) -> resp end), - {cowboy_websocket, resp, [req, Opts], WsOpts} = ?ws_conn:init(req, Opts). + {cowboy_websocket, resp, [req, Opts], _} = ?ws_conn:init(req, Opts). t_websocket_handle_binary(_) -> {ok, _} = websocket_handle({binary, <<>>}, st()), From 157b97eb8a51eff5dcf3a1ffb6c6c5895a7b7fc7 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 14 Jul 2021 10:09:12 +0800 Subject: [PATCH 154/379] fix(config): workaround for emqx_sn to use configs of mqtt_tcp listener --- apps/emqx_sn/src/emqx_sn_gateway.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_sn/src/emqx_sn_gateway.erl b/apps/emqx_sn/src/emqx_sn_gateway.erl index 1bccf0c1a..715cdb3cb 100644 --- a/apps/emqx_sn/src/emqx_sn_gateway.erl +++ b/apps/emqx_sn/src/emqx_sn_gateway.erl @@ -103,7 +103,7 @@ -define(STAT_TIMEOUT, 10000). -define(IDLE_TIMEOUT, 30000). --define(DEFAULT_CHAN_OPTIONS, [{max_packet_size, 256}, {zone, external}]). +-define(DEFAULT_CHAN_OPTIONS, #{zone => default, listener => mqtt_tcp}). -define(NEG_QOS_CLIENT_ID, <<"NegQoS-Client">>). From 6a8e35ce3ae909e0b1eaa49fe387e6b626fee935 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 14 Jul 2021 16:53:52 +0800 Subject: [PATCH 155/379] feat(one authn): merge simple authn and enhanced authn --- apps/emqx/src/emqx_access_control.erl | 33 +- apps/emqx/src/emqx_channel.erl | 194 +++++----- apps/emqx/src/emqx_reason_codes.erl | 7 +- apps/emqx_authn/etc/emqx_authn.conf | 32 +- apps/emqx_authn/include/emqx_authn.hrl | 13 +- apps/emqx_authn/src/emqx_authn.erl | 168 +++------ apps/emqx_authn/src/emqx_authn_api.erl | 339 ++++-------------- apps/emqx_authn/src/emqx_authn_app.erl | 45 +-- apps/emqx_authn/src/emqx_authn_schema.erl | 105 ++---- apps/emqx_authn/src/emqx_authn_utils.erl | 5 + .../emqx_enhanced_authn_mnesia.erl | 17 - .../emqx_enhanced_authn_scram_mnesia.erl | 240 +++++++++++++ .../src/simple_authn/emqx_authn_http.erl | 48 +-- .../emqx_authn_jwks_connector.erl | 5 +- .../src/simple_authn/emqx_authn_jwt.erl | 74 ++-- .../src/simple_authn/emqx_authn_mnesia.erl | 28 +- .../src/simple_authn/emqx_authn_mysql.erl | 15 +- .../src/simple_authn/emqx_authn_pgsql.erl | 19 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 100 ++---- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 70 ++-- .../test/emqx_authn_mnesia_SUITE.erl | 122 +++---- .../emqx_exproto/src/emqx_exproto_channel.erl | 17 +- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 12 +- apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl | 9 +- rebar.config | 1 + 25 files changed, 742 insertions(+), 976 deletions(-) delete mode 100644 apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl create mode 100644 apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 8da3277a9..b1c38bc41 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -18,27 +18,22 @@ -include("emqx.hrl"). --export([authenticate/1]). - --export([ authorize/3 +-export([ authenticate/1 + , authorize/3 ]). --type(result() :: #{auth_result := emqx_types:auth_result(), - anonymous := boolean() - }). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- --spec(authenticate(emqx_types:clientinfo()) -> {ok, result()} | {error, term()}). -authenticate(ClientInfo = #{zone := Zone}) -> - AuthResult = default_auth_result(Zone), - case emqx_zone:get_env(Zone, bypass_auth_plugins, false) of +-spec(authenticate(emqx_types:clientinfo()) -> + ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). +authenticate(Credential = #{zone := Zone}) -> + case emqx_zone:get_env(Zone, bypass_authentication, false) of true -> - return_auth_result(AuthResult); + ok; false -> - return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult)) + run_hooks('client.authenticate', [Credential], ok) end. %% @doc Check ACL @@ -65,18 +60,6 @@ do_authorize(ClientInfo, PubSub, Topic) -> _Other -> deny end. -default_auth_result(Zone) -> - case emqx_zone:get_env(Zone, allow_anonymous, false) of - true -> #{auth_result => success, anonymous => true}; - false -> #{auth_result => not_authorized, anonymous => false} - end. - -compile({inline, [run_hooks/3]}). run_hooks(Name, Args, Acc) -> ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc). - --compile({inline, [return_auth_result/1]}). -return_auth_result(Result = #{auth_result := success}) -> - {ok, Result}; -return_auth_result(Result) -> - {error, maps:get(auth_result, Result, unknown_error)}. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 99f8cb5df..911a44283 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -98,7 +98,7 @@ -type(channel() :: #channel{}). --type(conn_state() :: idle | connecting | connected | disconnected). +-type(conn_state() :: idle | connecting | connected | reauthenticating | disconnected). -type(reply() :: {outgoing, emqx_types:packet()} | {outgoing, [emqx_types:packet()]} @@ -272,7 +272,8 @@ take_ws_cookie(ClientInfo, ConnInfo) -> | {ok, replies(), channel()} | {shutdown, Reason :: term(), channel()} | {shutdown, Reason :: term(), replies(), channel()}). -handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = connected}) -> +handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> @@ -281,56 +282,64 @@ handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> fun check_connect/2, fun enrich_client/2, fun set_log_meta/2, - fun check_banned/2, - fun auth_connect/2 + fun check_banned/2 ], ConnPkt, Channel#channel{conn_state = connecting}) of {ok, NConnPkt, NChannel = #channel{clientinfo = ClientInfo}} -> NChannel1 = NChannel#channel{ will_msg = emqx_packet:will_msg(NConnPkt), alias_maximum = init_alias_maximum(NConnPkt, ClientInfo) }, - case enhanced_auth(?CONNECT_PACKET(NConnPkt), NChannel1) of + case authenticate(?CONNECT_PACKET(NConnPkt), NChannel1) of {ok, Properties, NChannel2} -> process_connect(Properties, ensure_connected(NChannel2)); {continue, Properties, NChannel2} -> handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, Properties}, NChannel2); - {error, ReasonCode, NChannel2} -> - handle_out(connack, ReasonCode, NChannel2) + {error, ReasonCode} -> + handle_out(connack, ReasonCode, NChannel1) end; {error, ReasonCode, NChannel} -> handle_out(connack, ReasonCode, NChannel) end; -handle_in(Packet = ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, _Properties), +handle_in(Packet = ?AUTH_PACKET(ReasonCode, _Properties), Channel = #channel{conn_state = ConnState}) -> - case enhanced_auth(Packet, Channel) of - {ok, NProperties, NChannel} -> + try + case {ReasonCode, ConnState} of + {?RC_CONTINUE_AUTHENTICATION, connecting} -> ok; + {?RC_CONTINUE_AUTHENTICATION, reauthenticating} -> ok; + {?RC_RE_AUTHENTICATE, connected} -> ok; + _ -> error(protocol_error) + end, + case authenticate(Packet, Channel) of + {ok, NProperties, NChannel} -> + case ConnState of + connecting -> + process_connect(NProperties, ensure_connected(NChannel)); + _ -> + handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel#channel{conn_state = connected}) + end; + {continue, NProperties, NChannel} -> + handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel#channel{conn_state = reauthenticating}); + {error, NReasonCode} -> + case ConnState of + connecting -> + handle_out(connack, NReasonCode, Channel); + _ -> + handle_out(disconnect, NReasonCode, Channel) + end + end + catch + _Class:_Reason -> case ConnState of connecting -> - process_connect(NProperties, ensure_connected(NChannel)); - connected -> - handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel); + handle_out(connack, ?RC_PROTOCOL_ERROR, Channel); _ -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel) - end; - {continue, NProperties, NChannel} -> - handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel); - {error, NReasonCode, NChannel} -> - handle_out(connack, NReasonCode, NChannel) + end end; -handle_in(Packet = ?AUTH_PACKET(?RC_RE_AUTHENTICATE, _Properties), - Channel = #channel{conn_state = connected}) -> - case enhanced_auth(Packet, Channel) of - {ok, NProperties, NChannel} -> - handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel); - {continue, NProperties, NChannel} -> - handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel); - {error, NReasonCode, NChannel} -> - handle_out(disconnect, NReasonCode, NChannel) - end; - -handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState}) when ConnState =/= connected -> +handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState}) + when ConnState =/= connected andalso ConnState =/= reauthenticating -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); handle_in(Packet = ?PUBLISH_PACKET(_QoS), Channel) -> @@ -469,9 +478,11 @@ handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connec handle_in({frame_error, Reason}, Channel = #channel{conn_state = connecting}) -> shutdown(Reason, ?CONNACK_PACKET(?RC_MALFORMED_PACKET), Channel); -handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connected}) -> +handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, {?RC_PACKET_TOO_LARGE, frame_too_large}, Channel); -handle_in({frame_error, Reason}, Channel = #channel{conn_state = connected}) -> +handle_in({frame_error, Reason}, Channel = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel); handle_in({frame_error, Reason}, Channel = #channel{conn_state = disconnected}) -> @@ -967,8 +978,9 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting}) shutdown(Reason, Channel); handle_info({sock_closed, Reason}, Channel = - #channel{conn_state = connected, - clientinfo = ClientInfo = #{zone := Zone}}) -> + #channel{conn_state = ConnState, + clientinfo = ClientInfo = #{zone := Zone}}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> emqx_zone:enable_flapping_detect(Zone) andalso emqx_flapping:detect(ClientInfo), Channel1 = ensure_disconnected(Reason, mabye_publish_will_msg(Channel)), @@ -1241,75 +1253,60 @@ check_banned(_ConnPkt, #channel{clientinfo = ClientInfo = #{zone := Zone}}) -> end. %%-------------------------------------------------------------------- -%% Auth Connect +%% Authenticate -auth_connect(#mqtt_packet_connect{password = Password}, +authenticate(?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5, + properties = #{'Authentication-Method' := AuthMethod} = Properties}), + #channel{clientinfo = ClientInfo, + auth_cache = AuthCache} = Channel) -> + AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), + do_authenticate(ClientInfo#{auth_method => AuthMethod, + auth_data => AuthData, + auth_cache => AuthCache}, Channel); + +authenticate(?CONNECT_PACKET(#mqtt_packet_connect{password = Password}), #channel{clientinfo = ClientInfo} = Channel) -> - #{clientid := ClientId, - username := Username} = ClientInfo, - case emqx_access_control:authenticate(ClientInfo#{password => Password}) of - {ok, AuthResult} -> - is_anonymous(AuthResult) andalso - emqx_metrics:inc('client.auth.anonymous'), - NClientInfo = maps:merge(ClientInfo, AuthResult), - {ok, Channel#channel{clientinfo = NClientInfo}}; - {error, Reason} -> - ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", - [ClientId, Username, Reason]), - {error, emqx_reason_codes:connack_error(Reason)} + do_authenticate(ClientInfo#{password => Password}, Channel); + +authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties), + #channel{clientinfo = ClientInfo, + conninfo = #{conn_props := ConnProps}, + auth_cache = AuthCache} = Channel) -> + case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of + AuthMethod -> + AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), + do_authenticate(ClientInfo#{auth_method => AuthMethod, + auth_data => AuthData, + auth_cache => AuthCache}, Channel); + _ -> + {error, ?RC_BAD_AUTHENTICATION_METHOD} end. -is_anonymous(#{anonymous := true}) -> true; -is_anonymous(_AuthResult) -> false. - -%%-------------------------------------------------------------------- -%% Enhanced Authentication - -enhanced_auth(?CONNECT_PACKET(#mqtt_packet_connect{ - proto_ver = Protover, - properties = Properties - }), Channel) -> - case Protover of - ?MQTT_PROTO_V5 -> - AuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined), - AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), - do_enhanced_auth(AuthMethod, AuthData, Channel); - _ -> - {ok, #{}, Channel} +do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> + Properties = #{'Authentication-Method' => AuthMethod}, + case emqx_access_control:authenticate(Credential) of + ok -> + {ok, Properties, Channel#channel{auth_cache = #{}}}; + {ok, AuthData} -> + {ok, Properties#{'Authentication-Data' => AuthData}, + Channel#channel{auth_cache = #{}}}; + {continue, AuthCache} -> + {continue, Properties, Channel#channel{auth_cache = AuthCache}}; + {continue, AuthData, AuthCache} -> + {continue, Properties#{'Authentication-Data' => AuthData}, + Channel#channel{auth_cache = AuthCache}}; + {error, Reason} -> + {error, emqx_reason_codes:connack_error(Reason)} end; -enhanced_auth(?AUTH_PACKET(_ReasonCode, Properties), Channel = #channel{conninfo = ConnInfo}) -> - AuthMethod = emqx_mqtt_props:get('Authentication-Method', - emqx_mqtt_props:get(conn_props, ConnInfo, #{}), - undefined - ), - NAuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined), - AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), - case NAuthMethod =:= undefined orelse NAuthMethod =/= AuthMethod of - true -> - {error, emqx_reason_codes:connack_error(bad_authentication_method), Channel}; - false -> - do_enhanced_auth(AuthMethod, AuthData, Channel) - end. - -do_enhanced_auth(undefined, undefined, Channel) -> - {ok, #{}, Channel}; -do_enhanced_auth(undefined, _AuthData, Channel) -> - {error, emqx_reason_codes:connack_error(not_authorized), Channel}; -do_enhanced_auth(_AuthMethod, undefined, Channel) -> - {error, emqx_reason_codes:connack_error(not_authorized), Channel}; -do_enhanced_auth(AuthMethod, AuthData, Channel = #channel{auth_cache = Cache}) -> - case run_hooks('client.enhanced_authenticate', [AuthMethod, AuthData], Cache) of - {ok, NAuthData, NCache} -> - NProperties = #{'Authentication-Method' => AuthMethod, - 'Authentication-Data' => NAuthData}, - {ok, NProperties, Channel#channel{auth_cache = NCache}}; - {continue, NAuthData, NCache} -> - NProperties = #{'Authentication-Method' => AuthMethod, - 'Authentication-Data' => NAuthData}, - {continue, NProperties, Channel#channel{auth_cache = NCache}}; - _ -> - {error, emqx_reason_codes:connack_error(not_authorized), Channel} +do_authenticate(Credential, Channel) -> + case emqx_access_control:authenticate(Credential) of + ok -> + {ok, #{}, Channel}; + {error, Reason} -> + {error, emqx_reason_codes:connack_error(Reason)} end. %%-------------------------------------------------------------------- @@ -1703,7 +1700,8 @@ shutdown(Reason, Reply, Packet, Channel) -> {shutdown, Reason, Reply, Packet, Channel}. disconnect_and_shutdown(Reason, Reply, Channel = ?IS_MQTT_V5 - = #channel{conn_state = connected}) -> + = #channel{conn_state = ConnState}) + when ConnState =:= connected orelse ConnState =:= reauthenticating -> shutdown(Reason, Reply, ?DISCONNECT_PACKET(reason_code(Reason)), Channel); disconnect_and_shutdown(Reason, Reply, Channel) -> diff --git a/apps/emqx/src/emqx_reason_codes.erl b/apps/emqx/src/emqx_reason_codes.erl index 893084b9d..b98fc1263 100644 --- a/apps/emqx/src/emqx_reason_codes.erl +++ b/apps/emqx/src/emqx_reason_codes.erl @@ -170,16 +170,11 @@ frame_error(frame_too_large) -> ?RC_PACKET_TOO_LARGE; frame_error(_) -> ?RC_MALFORMED_PACKET. connack_error(protocol_error) -> ?RC_PROTOCOL_ERROR; -connack_error(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID; connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD; -connack_error(bad_clientid_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD; -connack_error(username_or_password_undefined) -> ?RC_BAD_USER_NAME_OR_PASSWORD; -connack_error(password_error) -> ?RC_BAD_USER_NAME_OR_PASSWORD; connack_error(not_authorized) -> ?RC_NOT_AUTHORIZED; connack_error(server_unavailable) -> ?RC_SERVER_UNAVAILABLE; connack_error(server_busy) -> ?RC_SERVER_BUSY; connack_error(banned) -> ?RC_BANNED; connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD; -%% TODO: ??? -connack_error(_) -> ?RC_NOT_AUTHORIZED. +connack_error(_) -> ?RC_UNSPECIFIED_ERROR. diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index ecd49d5a5..5194445b8 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,26 +1,12 @@ emqx_authn: { - chains: [ - # { - # id: "chain1" - # type: simple - # authenticators: [ - # { - # name: "authenticator1" - # type: built-in-database - # config: { - # user_id_type: clientid - # password_hash_algorithm: { - # name: sha256 - # } - # } - # } - # ] - # } - ] - bindings: [ - # { - # chain_id: "chain1" - # listeners: ["mqtt-tcp", "mqtt-ssl"] - # } + authenticators: [ + # { + # name: "authenticator1" + # mechanism: password-based + # config: { + # server_type: built-in-database + # user_id_type: clientid + # } + # } ] } diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index 46c1cf7ca..bb353348f 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -15,16 +15,15 @@ %%-------------------------------------------------------------------- -define(APP, emqx_authn). +-define(CHAIN, <<"mqtt">>). -type chain_id() :: binary(). --type authn_type() :: simple | enhanced. -type authenticator_name() :: binary(). --type authenticator_type() :: mnesia | jwt | mysql | postgresql. --type listener_id() :: binary(). +-type mechanism() :: 'password-based' | jwt | scram. -record(authenticator, { name :: authenticator_name() - , type :: authenticator_type() + , mechanism :: mechanism() , provider :: module() , config :: map() , state :: map() @@ -32,16 +31,10 @@ -record(chain, { id :: chain_id() - , type :: authn_type() , authenticators :: [{authenticator_name(), #authenticator{}}] , created_at :: integer() }). --record(binding, - { bound :: {listener_id(), authn_type()} - , chain_id :: chain_id() - }). - -define(AUTH_SHARD, emqx_authn_shard). -define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)). diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 24bdb21e7..611b47cc2 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -22,16 +22,12 @@ , disable/0 ]). --export([authenticate/1]). +-export([authenticate/2]). -export([ create_chain/1 , delete_chain/1 , lookup_chain/1 , list_chains/0 - , bind/2 - , unbind/2 - , list_bindings/1 - , list_bound_chains/1 , create_authenticator/2 , delete_authenticator/2 , update_authenticator/3 @@ -55,10 +51,8 @@ -boot_mnesia({mnesia, [boot]}). -define(CHAIN_TAB, emqx_authn_chain). --define(BINDING_TAB, emqx_authn_binding). -rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). --rlog_shard({?AUTH_SHARD, ?BINDING_TAB}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap @@ -75,13 +69,6 @@ mnesia(boot) -> {record_name, chain}, {local_content, true}, {attributes, record_info(fields, chain)}, - {storage_properties, StoreProps}]), - %% Binding table - ok = ekka_mnesia:create_table(?BINDING_TAB, [ - {ram_copies, [node()]}, - {record_name, binding}, - {local_content, true}, - {attributes, record_info(fields, binding)}, {storage_properties, StoreProps}]). enable() -> @@ -94,39 +81,35 @@ disable() -> emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), ok. -authenticate(#{listener_id := ListenerID} = ClientInfo) -> - case lookup_chain_by_listener(ListenerID, simple) of - {error, _} -> - {error, no_authenticators}; - {ok, ChainID} -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [#chain{authenticators = []}] -> - {error, no_authenticators}; - [#chain{authenticators = Authenticators}] -> - do_authenticate(Authenticators, ClientInfo); - [] -> - {error, no_authenticators} - end +authenticate(Credential, _AuthResult) -> + case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of + [#chain{authenticators = Authenticators}] -> + do_authenticate(Authenticators, Credential); + [] -> + {stop, {error, not_authorized}} end. do_authenticate([], _) -> - {error, user_not_found}; -do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], ClientInfo) -> - case Provider:authenticate(ClientInfo, State) of - ignore -> do_authenticate(More, ClientInfo); - ok -> ok; - {ok, NewClientInfo} -> {ok, NewClientInfo}; - {stop, Reason} -> {error, Reason} + {stop, {error, not_authorized}}; +do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], Credential) -> + case Provider:authenticate(Credential, State) of + ignore -> + do_authenticate(More, Credential); + Result -> + %% ok + %% {ok, AuthData} + %% {continue, AuthCache} + %% {continue, AuthData, AuthCache} + %% {error, Reason} + {stop, Result} end. -create_chain(#{id := ID, - type := Type}) -> +create_chain(#{id := ID}) -> trans( fun() -> case mnesia:read(?CHAIN_TAB, ID, write) of [] -> Chain = #chain{id = ID, - type = Type, authenticators = [], created_at = erlang:system_time(millisecond)}, mnesia:write(?CHAIN_TAB, Chain, write), @@ -160,85 +143,20 @@ list_chains() -> Chains = ets:tab2list(?CHAIN_TAB), {ok, [serialize_chain(Chain) || Chain <- Chains]}. -bind(ChainID, Listeners) -> - %% TODO: ensure listener id is valid - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{type = AuthNType}] -> - Result = lists:foldl( - fun(ListenerID, Acc) -> - case mnesia:read(?BINDING_TAB, {ListenerID, AuthNType}, write) of - [] -> - Binding = #binding{bound = {ListenerID, AuthNType}, chain_id = ChainID}, - mnesia:write(?BINDING_TAB, Binding, write), - Acc; - _ -> - [ListenerID | Acc] - end - end, [], Listeners), - case Result of - [] -> ok; - Listeners0 -> {error, {already_bound, Listeners0}} - end - end - end). - -unbind(ChainID, Listeners) -> - trans( - fun() -> - Result = lists:foldl( - fun(ListenerID, Acc) -> - MatchSpec = [{{binding, {ListenerID, '_'}, ChainID}, [], ['$_']}], - case mnesia:select(?BINDING_TAB, MatchSpec, write) of - [] -> - [ListenerID | Acc]; - [#binding{bound = Bound}] -> - mnesia:delete(?BINDING_TAB, Bound, write), - Acc - end - end, [], Listeners), - case Result of - [] -> ok; - Listeners0 -> - {error, {not_found, Listeners0}} - end - end). - -list_bindings(ChainID) -> - trans( - fun() -> - MatchSpec = [{{binding, {'$1', '_'}, ChainID}, [], ['$1']}], - Listeners = mnesia:select(?BINDING_TAB, MatchSpec), - {ok, #{chain_id => ChainID, listeners => Listeners}} - end). - -list_bound_chains(ListenerID) -> - trans( - fun() -> - MatchSpec = [{{binding, {ListenerID, '_'}, '_'}, [], ['$_']}], - Bindings = mnesia:select(?BINDING_TAB, MatchSpec), - Chains = [{AuthNType, ChainID} || #binding{bound = {_, AuthNType}, - chain_id = ChainID} <- Bindings], - {ok, maps:from_list(Chains)} - end). - create_authenticator(ChainID, #{name := Name, - type := Type, + mechanism := Mechanism, config := Config}) -> UpdateFun = - fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) -> + fun(Chain = #chain{authenticators = Authenticators}) -> case lists:keymember(Name, 1, Authenticators) of true -> {error, {already_exists, {authenticator, Name}}}; false -> - Provider = authenticator_provider(AuthNType, Type), + Provider = authenticator_provider(Mechanism, Config), case Provider:create(ChainID, Name, Config) of {ok, State} -> Authenticator = #authenticator{name = Name, - type = Type, + mechanism = Mechanism, provider = Provider, config = Config, state = State}, @@ -367,12 +285,18 @@ list_users(ChainID, AuthenticatorName) -> %% Internal functions %%------------------------------------------------------------------------------ -authenticator_provider(simple, 'built-in-database') -> emqx_authn_mnesia; -authenticator_provider(simple, jwt) -> emqx_authn_jwt; -authenticator_provider(simple, mysql) -> emqx_authn_mysql; -authenticator_provider(simple, postgresql) -> emqx_authn_pgsql. - -% authenticator_provider(enhanced, 'enhanced-built-in-database') -> emqx_enhanced_authn_mnesia. +authenticator_provider('password-based', #{server_type := 'built-in-database'}) -> + emqx_authn_mnesia; +authenticator_provider('password-based', #{server_type := 'mysql'}) -> + emqx_authn_mysql; +authenticator_provider('password-based', #{server_type := 'pgsql'}) -> + emqx_authn_pgsql; +authenticator_provider('password-based', #{server_type := 'http-server'}) -> + emqx_authn_http; +authenticator_provider(jwt, _) -> + emqx_authn_jwt; +authenticator_provider(scram, #{server_type := 'built-in-database'}) -> + emqx_enhanced_authn_scram_mnesia. do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> Provider:destroy(State). @@ -429,13 +353,13 @@ update_chain(ChainID, UpdateFun) -> end end). -lookup_chain_by_listener(ListenerID, AuthNType) -> - case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of - [] -> - {error, not_found}; - [#binding{chain_id = ChainID}] -> - {ok, ChainID} - end. +% lookup_chain_by_listener(ListenerID, AuthNType) -> +% case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of +% [] -> +% {error, not_found}; +% [#binding{chain_id = ChainID}] -> +% {ok, ChainID} +% end. call_authenticator(ChainID, AuthenticatorName, Func, Args) -> @@ -457,11 +381,9 @@ call_authenticator(ChainID, AuthenticatorName, Func, Args) -> end. serialize_chain(#chain{id = ID, - type = Type, authenticators = Authenticators, created_at = CreatedAt}) -> #{id => ID, - type => Type, authenticators => serialize_authenticators(Authenticators), created_at => CreatedAt}. @@ -474,10 +396,10 @@ serialize_authenticators(Authenticators) -> [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. serialize_authenticator(#authenticator{name = Name, - type = Type, + mechanism = Mechanism, config = Config}) -> #{name => Name, - type => Type, + mechanism => Mechanism, config => Config}. trans(Fun) -> diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c24b790a3..c875cc717 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -18,15 +18,7 @@ -include("emqx_authn.hrl"). --export([ create_chain/2 - , delete_chain/2 - , lookup_chain/2 - , list_chains/2 - , bind/2 - , unbind/2 - , list_bindings/2 - , list_bound_chains/2 - , create_authenticator/2 +-export([ create_authenticator/2 , delete_authenticator/2 , update_authenticator/2 , lookup_authenticator/2 @@ -40,135 +32,79 @@ , list_users/2 ]). --rest_api(#{name => create_chain, - method => 'POST', - path => "/authentication/chains", - func => create_chain, - descr => "Create a chain" - }). - --rest_api(#{name => delete_chain, - method => 'DELETE', - path => "/authentication/chains/:bin:id", - func => delete_chain, - descr => "Delete chain" - }). - --rest_api(#{name => lookup_chain, - method => 'GET', - path => "/authentication/chains/:bin:id", - func => lookup_chain, - descr => "Lookup chain" - }). - --rest_api(#{name => list_chains, - method => 'GET', - path => "/authentication/chains", - func => list_chains, - descr => "List all chains" - }). - --rest_api(#{name => bind, - method => 'POST', - path => "/authentication/chains/:bin:id/bindings/bulk", - func => bind, - descr => "Bind" - }). - --rest_api(#{name => unbind, - method => 'DELETE', - path => "/authentication/chains/:bin:id/bindings/bulk", - func => unbind, - descr => "Unbind" - }). - --rest_api(#{name => list_bindings, - method => 'GET', - path => "/authentication/chains/:bin:id/bindings", - func => list_bindings, - descr => "List bindings" - }). - --rest_api(#{name => list_bound_chains, - method => 'GET', - path => "/authentication/listeners/:bin:listener_id/bound_chains", - func => list_bound_chains, - descr => "List bound chains" - }). - -rest_api(#{name => create_authenticator, method => 'POST', - path => "/authentication/chains/:bin:id/authenticators", + path => "/authentication/authenticators", func => create_authenticator, - descr => "Create authenticator to chain" + descr => "Create authenticator" }). -rest_api(#{name => delete_authenticator, method => 'DELETE', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + path => "/authentication/authenticators/:bin:name", func => delete_authenticator, - descr => "Delete authenticator from chain" + descr => "Delete authenticator" }). -rest_api(#{name => update_authenticator, method => 'PUT', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + path => "/authentication/authenticators/:bin:name", func => update_authenticator, - descr => "Update authenticator in chain" + descr => "Update authenticator" }). -rest_api(#{name => lookup_authenticator, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + path => "/authentication/authenticators/:bin:name", func => lookup_authenticator, - descr => "Lookup authenticator in chain" + descr => "Lookup authenticator" }). -rest_api(#{name => list_authenticators, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators", + path => "/authentication/authenticators", func => list_authenticators, - descr => "List authenticators in chain" + descr => "List authenticators" }). -rest_api(#{name => move_authenticator, method => 'POST', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position", + path => "/authentication/authenticators/:bin:name/position", func => move_authenticator, descr => "Change the order of authenticators" }). -rest_api(#{name => import_users, method => 'POST', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/import-users", + path => "/authentication/authenticators/:bin:name/import-users", func => import_users, descr => "Import users" }). -rest_api(#{name => add_user, method => 'POST', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + path => "/authentication/authenticators/:bin:name/users", func => add_user, descr => "Add user" }). -rest_api(#{name => delete_user, method => 'DELETE', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + path => "/authentication/authenticators/:bin:name/users/:bin:user_id", func => delete_user, descr => "Delete user" }). -rest_api(#{name => update_user, method => 'PUT', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + path => "/authentication/authenticators/:bin:name/users/:bin:user_id", func => update_user, descr => "Update user" }). -rest_api(#{name => lookup_user, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + path => "/authentication/authenticators/:bin:name/users/:bin:user_id", func => lookup_user, descr => "Lookup user" }). @@ -176,129 +112,24 @@ %% TODO: Support pagination -rest_api(#{name => list_users, method => 'GET', - path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + path => "/authentication/authenticators/:bin:name/users", func => list_users, descr => "List all users" }). -create_chain(Binding, Params) -> - do_create_chain(uri_decode(Binding), maps:from_list(Params)). - -do_create_chain(_Binding, Chain0) -> - Config = #{<<"authn">> => #{<<"chains">> => [Chain0#{<<"authenticators">> => []}], - <<"bindings">> => []}}, - #{authn := #{chains := [Chain1]}} - = hocon_schema:check_plain(emqx_authn_schema, Config, - #{atom_key => true, nullable => true}), - case emqx_authn:create_chain(Chain1) of - {ok, Chain2} -> - return({ok, Chain2}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -delete_chain(Binding, Params) -> - do_delete_chain(uri_decode(Binding), maps:from_list(Params)). - -do_delete_chain(#{id := ChainID}, _Params) -> - case emqx_authn:delete_chain(ChainID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_chain(Binding, Params) -> - do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_chain(#{id := ChainID}, _Params) -> - case emqx_authn:lookup_chain(ChainID) of - {ok, Chain} -> - return({ok, Chain}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_chains(Binding, Params) -> - do_list_chains(uri_decode(Binding), maps:from_list(Params)). - -do_list_chains(_Binding, _Params) -> - {ok, Chains} = emqx_authn:list_chains(), - return({ok, Chains}). - -bind(Binding, Params) -> - do_bind(uri_decode(Binding), lists_to_map(Params)). - -do_bind(#{id := ChainID}, #{<<"listeners">> := Listeners}) -> - % Config = #{<<"authn">> => #{<<"chains">> => [], - % <<"bindings">> => [#{<<"chain">> := ChainID, - % <<"listeners">> := Listeners}]}}, - % #{authn := #{bindings := [#{listeners := Listeners}]}} - % = hocon_schema:check_plain(emqx_authn_schema, Config, - % #{atom_key => true, nullable => true}), - case emqx_authn:bind(ChainID, Listeners) of - ok -> - return(ok); - {error, {alread_bound, Listeners}} -> - {ok, #{code => <<"ALREADY_EXISTS">>, - message => <<"ALREADY_BOUND">>, - detail => Listeners}}; - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_bind(_, _) -> - return(serialize_error({missing_parameter, <<"listeners">>})). - -unbind(Binding, Params) -> - do_unbind(uri_decode(Binding), lists_to_map(Params)). - -do_unbind(#{id := ChainID}, #{<<"listeners">> := Listeners0}) -> - case emqx_authn:unbind(ChainID, Listeners0) of - ok -> - return(ok); - {error, {not_found, Listeners1}} -> - {ok, #{code => <<"NOT_FOUND">>, - detail => Listeners1}}; - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_unbind(_, _) -> - return(serialize_error({missing_parameter, <<"listeners">>})). - -list_bindings(Binding, Params) -> - do_list_bindings(uri_decode(Binding), lists_to_map(Params)). - -do_list_bindings(#{id := ChainID}, _) -> - {ok, Binding} = emqx_authn:list_bindings(ChainID), - return({ok, Binding}). - -list_bound_chains(Binding, Params) -> - do_list_bound_chains(uri_decode(Binding), lists_to_map(Params)). - -do_list_bound_chains(#{listener_id := ListenerID}, _) -> - {ok, Chains} = emqx_authn:list_bound_chains(ListenerID), - return({ok, Chains}). - create_authenticator(Binding, Params) -> do_create_authenticator(uri_decode(Binding), lists_to_map(Params)). -do_create_authenticator(#{id := ChainID}, Authenticator0) -> - case emqx_authn:lookup_chain(ChainID) of - {ok, #{type := Type}} -> - Chain = #{<<"id">> => ChainID, - <<"type">> => Type, - <<"authenticators">> => [Authenticator0]}, - Config = #{<<"authn">> => #{<<"chains">> => [Chain], - <<"bindings">> => []}}, - #{authn := #{chains := [#{authenticators := [Authenticator1]}]}} - = hocon_schema:check_plain(emqx_authn_schema, Config, - #{atom_key => true, nullable => true}), - case emqx_authn:create_authenticator(ChainID, Authenticator1) of - {ok, Authenticator2} -> - return({ok, Authenticator2}); - {error, Reason} -> - return(serialize_error(Reason)) - end; +do_create_authenticator(_Binding, Authenticator0) -> + Config = #{<<"emqx_authn">> => #{ + <<"authenticators">> => [Authenticator0] + }}, + #{emqx_authn := #{authenticators := [Authenticator1]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_authenticator(?CHAIN, Authenticator1) of + {ok, Authenticator2} -> + return({ok, Authenticator2}); {error, Reason} -> return(serialize_error(Reason)) end. @@ -306,9 +137,8 @@ do_create_authenticator(#{id := ChainID}, Authenticator0) -> delete_authenticator(Binding, Params) -> do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)). -do_delete_authenticator(#{id := ChainID, - authenticator_name := AuthenticatorName}, _Params) -> - case emqx_authn:delete_authenticator(ChainID, AuthenticatorName) of +do_delete_authenticator(#{name := Name}, _Params) -> + case emqx_authn:delete_authenticator(?CHAIN, Name) of ok -> return(ok); {error, Reason} -> @@ -320,36 +150,26 @@ update_authenticator(Binding, Params) -> do_update_authenticator(uri_decode(Binding), lists_to_map(Params)). %% TOOD: PUT method supports creation and update -do_update_authenticator(#{id := ChainID, - authenticator_name := AuthenticatorName}, AuthenticatorConfig0) -> - case emqx_authn:lookup_chain(ChainID) of - {ok, #{type := ChainType}} -> - case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of - {ok, #{type := Type}} -> - Authenticator = #{<<"name">> => AuthenticatorName, - <<"type">> => Type, - <<"config">> => AuthenticatorConfig0}, - Chain = #{<<"id">> => ChainID, - <<"type">> => ChainType, - <<"authenticators">> => [Authenticator]}, - Config = #{<<"authn">> => #{<<"chains">> => [Chain], - <<"bindings">> => []}}, - #{ - authn := #{ - chains := [#{ - authenticators := [#{ - config := AuthenticatorConfig1 - }] - }] - } - } = hocon_schema:check_plain(emqx_authn_schema, Config, - #{atom_key => true, nullable => true}), - case emqx_authn:update_authenticator(ChainID, AuthenticatorName, AuthenticatorConfig1) of - {ok, NAuthenticator} -> - return({ok, NAuthenticator}); - {error, Reason} -> - return(serialize_error(Reason)) - end; +do_update_authenticator(#{name := Name}, NewConfig0) -> + case emqx_authn:lookup_authenticator(?CHAIN, Name) of + {ok, #{mechanism := Mechanism}} -> + Authenticator = #{<<"name">> => Name, + <<"mechanism">> => Mechanism, + <<"config">> => NewConfig0}, + Config = #{<<"emqx_authn">> => #{ + <<"authenticators">> => [Authenticator] + }}, + #{ + emqx_authn := #{ + authenticators := [#{ + config := NewConfig1 + }] + } + } = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:update_authenticator(?CHAIN, Name, NewConfig1) of + {ok, NAuthenticator} -> + return({ok, NAuthenticator}); {error, Reason} -> return(serialize_error(Reason)) end; @@ -360,9 +180,8 @@ do_update_authenticator(#{id := ChainID, lookup_authenticator(Binding, Params) -> do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)). -do_lookup_authenticator(#{id := ChainID, - authenticator_name := AuthenticatorName}, _Params) -> - case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of +do_lookup_authenticator(#{name := Name}, _Params) -> + case emqx_authn:lookup_authenticator(?CHAIN, Name) of {ok, Authenticator} -> return({ok, Authenticator}); {error, Reason} -> @@ -372,8 +191,8 @@ do_lookup_authenticator(#{id := ChainID, list_authenticators(Binding, Params) -> do_list_authenticators(uri_decode(Binding), maps:from_list(Params)). -do_list_authenticators(#{id := ChainID}, _Params) -> - case emqx_authn:list_authenticators(ChainID) of +do_list_authenticators(_Binding, _Params) -> + case emqx_authn:list_authenticators(?CHAIN) of {ok, Authenticators} -> return({ok, Authenticators}); {error, Reason} -> @@ -383,25 +202,22 @@ do_list_authenticators(#{id := ChainID}, _Params) -> move_authenticator(Binding, Params) -> do_move_authenticator(uri_decode(Binding), maps:from_list(Params)). -do_move_authenticator(#{id := ChainID, - authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the front">>}) -> - case emqx_authn:move_authenticator_to_the_front(ChainID, AuthenticatorName) of +do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the front">>}) -> + case emqx_authn:move_authenticator_to_the_front(?CHAIN, Name) of ok -> return(ok); {error, Reason} -> return(serialize_error(Reason)) end; -do_move_authenticator(#{id := ChainID, - authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the end">>}) -> - case emqx_authn:move_authenticator_to_the_end(ChainID, AuthenticatorName) of +do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the end">>}) -> + case emqx_authn:move_authenticator_to_the_end(?CHAIN, Name) of ok -> return(ok); {error, Reason} -> return(serialize_error(Reason)) end; -do_move_authenticator(#{id := ChainID, - authenticator_name := AuthenticatorName}, #{<<"position">> := N}) when is_number(N) -> - case emqx_authn:move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) of +do_move_authenticator(#{name := Name}, #{<<"position">> := N}) when is_number(N) -> + case emqx_authn:move_authenticator_to_the_nth(?CHAIN, Name, N) of ok -> return(ok); {error, Reason} -> @@ -413,9 +229,9 @@ do_move_authenticator(_Binding, _Params) -> import_users(Binding, Params) -> do_import_users(uri_decode(Binding), maps:from_list(Params)). -do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName}, +do_import_users(#{name := Name}, #{<<"filename">> := Filename}) -> - case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of + case emqx_authn:import_users(?CHAIN, Name, Filename) of ok -> return(ok); {error, Reason} -> @@ -428,9 +244,8 @@ do_import_users(_Binding, Params) -> add_user(Binding, Params) -> do_add_user(uri_decode(Binding), maps:from_list(Params)). -do_add_user(#{id := ChainID, - authenticator_name := AuthenticatorName}, UserInfo) -> - case emqx_authn:add_user(ChainID, AuthenticatorName, UserInfo) of +do_add_user(#{name := Name}, UserInfo) -> + case emqx_authn:add_user(?CHAIN, Name, UserInfo) of {ok, User} -> return({ok, User}); {error, Reason} -> @@ -440,10 +255,9 @@ do_add_user(#{id := ChainID, delete_user(Binding, Params) -> do_delete_user(uri_decode(Binding), maps:from_list(Params)). -do_delete_user(#{id := ChainID, - authenticator_name := AuthenticatorName, +do_delete_user(#{name := Name, user_id := UserID}, _Params) -> - case emqx_authn:delete_user(ChainID, AuthenticatorName, UserID) of + case emqx_authn:delete_user(?CHAIN, Name, UserID) of ok -> return(ok); {error, Reason} -> @@ -453,10 +267,9 @@ do_delete_user(#{id := ChainID, update_user(Binding, Params) -> do_update_user(uri_decode(Binding), maps:from_list(Params)). -do_update_user(#{id := ChainID, - authenticator_name := AuthenticatorName, +do_update_user(#{name := Name, user_id := UserID}, NewUserInfo) -> - case emqx_authn:update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) of + case emqx_authn:update_user(?CHAIN, Name, UserID, NewUserInfo) of {ok, User} -> return({ok, User}); {error, Reason} -> @@ -466,10 +279,9 @@ do_update_user(#{id := ChainID, lookup_user(Binding, Params) -> do_lookup_user(uri_decode(Binding), maps:from_list(Params)). -do_lookup_user(#{id := ChainID, - authenticator_name := AuthenticatorName, +do_lookup_user(#{name := Name, user_id := UserID}, _Params) -> - case emqx_authn:lookup_user(ChainID, AuthenticatorName, UserID) of + case emqx_authn:lookup_user(?CHAIN, Name, UserID) of {ok, User} -> return({ok, User}); {error, Reason} -> @@ -479,9 +291,8 @@ do_lookup_user(#{id := ChainID, list_users(Binding, Params) -> do_list_users(uri_decode(Binding), maps:from_list(Params)). -do_list_users(#{id := ChainID, - authenticator_name := AuthenticatorName}, _Params) -> - case emqx_authn:list_users(ChainID, AuthenticatorName) of +do_list_users(#{name := Name}, _Params) -> + case emqx_authn:list_users(?CHAIN, Name) of {ok, Users} -> return({ok, Users}); {error, Reason} -> @@ -526,11 +337,7 @@ serialize_error(_) -> {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. serialize_type(authenticator) -> - "Authenticator"; -serialize_type(chain) -> - "Chain"; -serialize_type(authenticator_type) -> - "Authenticator type". + "Authenticator". get_missed_params(Actual, Expected) -> Keys = lists:foldl(fun(Key, Acc) -> diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 033c760af..a78fa54f1 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -38,40 +38,19 @@ stop(_State) -> ok. initialize() -> - #{chains := Chains, - bindings := Bindings} = emqx_config:get([authn], #{chains => [], bindings => []}), - initialize_chains(Chains), - initialize_bindings(Bindings). + #{authenticators := Authenticators} = emqx_config:get([emqx_authn], #{authenticators => []}), + initialize(Authenticators). -initialize_chains([]) -> +initialize(Authenticators) -> + {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), + initialize_authenticators(Authenticators). + +initialize_authenticators([]) -> ok; -initialize_chains([#{id := ChainID, - type := Type, - authenticators := Authenticators} | More]) -> - case emqx_authn:create_chain(#{id => ChainID, - type => Type}) of +initialize_authenticators([#{name := Name} = Authenticator | More]) -> + case emqx_authn:create_authenticator(?CHAIN, Authenticator) of {ok, _} -> - initialize_authenticators(ChainID, Authenticators), - initialize_chains(More); + initialize_authenticators(More); {error, Reason} -> - ?LOG(error, "Failed to create chain '~s': ~p", [ChainID, Reason]) - end. - -initialize_authenticators(_ChainID, []) -> - ok; -initialize_authenticators(ChainID, [#{name := Name} = Authenticator | More]) -> - case emqx_authn:create_authenticator(ChainID, Authenticator) of - {ok, _} -> - initialize_authenticators(ChainID, More); - {error, Reason} -> - ?LOG(error, "Failed to create authenticator '~s' in chain '~s': ~p", [Name, ChainID, Reason]) - end. - -initialize_bindings([]) -> - ok; -initialize_bindings([#{chain_id := ChainID, listeners := Listeners} | More]) -> - case emqx_authn:bind(Listeners, ChainID) of - ok -> initialize_bindings(More); - {error, Reason} -> - ?LOG(error, "Failed to bind: ~p", [Reason]) - end. + ?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason]) + end. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index d9bf72910..8b844ab69 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -21,94 +21,55 @@ -behaviour(hocon_schema). --export([structs/0, fields/1]). +-export([ structs/0 + , fields/1 + ]). --reflect_type([ chain_id/0 - , authenticator_name/0 +-reflect_type([ authenticator_name/0 ]). structs() -> ["emqx_authn"]. fields("emqx_authn") -> - [ {chains, fun chains/1} - , {bindings, fun bindings/1}]; + [ {authenticators, fun authenticators/1} ]; -fields('simple-chain') -> - [ {id, fun chain_id/1} - , {type, {enum, [simple]}} - , {authenticators, fun simple_authenticators/1} +fields('password-based') -> + [ {name, fun authenticator_name/1} + , {mechanism, {enum, ['password-based']}} + , {config, hoconsc:t(hoconsc:union( + [ hoconsc:ref(emqx_authn_mnesia, config) + , hoconsc:ref(emqx_authn_mysql, config) + , hoconsc:ref(emqx_authn_pgsql, config) + , hoconsc:ref(emqx_authn_http, get) + , hoconsc:ref(emqx_authn_http, post) + ]))} ]; -% fields('enhanced-chain') -> -% [ {id, fun chain_id/1} -% , {type, {enum, [enhanced]}} -% , {authenticators, fun enhanced_authenticators/1} -% ]; - -fields(binding) -> - [ {chain_id, fun chain_id/1} - , {listeners, fun listeners/1} - ]; - -fields('built-in-database') -> - [ {name, fun authenticator_name/1} - , {type, {enum, ['built-in-database']}} - , {config, hoconsc:t(hoconsc:ref(emqx_authn_mnesia, config))} - ]; - -% fields('enhanced-built-in-database') -> -% [ {name, fun authenticator_name/1} -% , {type, {enum, ['built-in-database']}} -% , {config, hoconsc:t(hoconsc:ref(emqx_enhanced_authn_mnesia, config))} -% ]; - fields(jwt) -> - [ {name, fun authenticator_name/1} - , {type, {enum, [jwt]}} - , {config, hoconsc:t(hoconsc:ref(emqx_authn_jwt, config))} + [ {name, fun authenticator_name/1} + , {mechanism, {enum, [jwt]}} + , {config, hoconsc:t(hoconsc:union( + [ hoconsc:ref(emqx_authn_jwt, 'hmac-based') + , hoconsc:ref(emqx_authn_jwt, 'public-key') + , hoconsc:ref(emqx_authn_jwt, 'jwks') + ]))} ]; -fields(mysql) -> - [ {name, fun authenticator_name/1} - , {type, {enum, [mysql]}} - , {config, hoconsc:t(hoconsc:ref(emqx_authn_mysql, config))} - ]; - -fields(pgsql) -> - [ {name, fun authenticator_name/1} - , {type, {enum, [postgresql]}} - , {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))} +fields(scram) -> + [ {name, fun authenticator_name/1} + , {mechanism, {enum, [scram]}} + , {config, hoconsc:t(hoconsc:union( + [ hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) + ]))} ]. -chains(type) -> hoconsc:array({union, [hoconsc:ref(?MODULE, 'simple-chain')]}); -chains(default) -> []; -chains(_) -> undefined. - -chain_id(type) -> chain_id(); -chain_id(nullable) -> false; -chain_id(_) -> undefined. - -simple_authenticators(type) -> - hoconsc:array({union, [ hoconsc:ref(?MODULE, 'built-in-database') +authenticators(type) -> + hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based') , hoconsc:ref(?MODULE, jwt) - , hoconsc:ref(?MODULE, mysql) - , hoconsc:ref(?MODULE, pgsql)]}); -simple_authenticators(default) -> []; -simple_authenticators(_) -> undefined. - -% enhanced_authenticators(type) -> -% hoconsc:array({union, [hoconsc:ref('enhanced-built-in-database')]}); -% enhanced_authenticators(default) -> []; -% enhanced_authenticators(_) -> undefined. + , hoconsc:ref(?MODULE, scram)]}); +authenticators(default) -> []; +authenticators(_) -> undefined. authenticator_name(type) -> authenticator_name(); authenticator_name(nullable) -> false; authenticator_name(_) -> undefined. - -bindings(type) -> hoconsc:array(hoconsc:ref(?MODULE, binding)); -bindings(default) -> []; -bindings(_) -> undefined. - -listeners(type) -> hoconsc:array(binary()); -listeners(default) -> []; -listeners(_) -> undefined. diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl index 98e27e76c..2a91584f0 100644 --- a/apps/emqx_authn/src/emqx_authn_utils.erl +++ b/apps/emqx_authn/src/emqx_authn_utils.erl @@ -17,6 +17,7 @@ -module(emqx_authn_utils). -export([ replace_placeholder/2 + , gen_salt/0 ]). %%------------------------------------------------------------------------------ @@ -41,6 +42,10 @@ replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Da replace_placeholder([_ | More], Data, Acc) -> replace_placeholder(More, Data, [null | Acc]). +gen_salt() -> + <> = crypto:strong_rand_bytes(16), + iolist_to_binary(io_lib:format("~32.16.0b", [X])). + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl deleted file mode 100644 index 207e93495..000000000 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl +++ /dev/null @@ -1,17 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_enhanced_authn_mnesia). diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl new file mode 100644 index 000000000..d1d564bf3 --- /dev/null +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -0,0 +1,240 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_enhanced_authn_scram_mnesia). + +-include("emqx_authn.hrl"). +-include_lib("esasl/include/esasl_scram.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +-export([ add_user/2 + , delete_user/2 + , update_user/3 + , lookup_user/2 + , list_users/1 + ]). + +-define(TAB, ?MODULE). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-rlog_shard({?AUTH_SHARD, ?TAB}). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot | copy) -> ok). +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {disc_copies, [node()]}, + {record_name, scram_user_credentail}, + {attributes, record_info(fields, scram_user_credentail)}, + {storage_properties, [{ets, [{read_concurrency, true}]}]}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB, disc_copies). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {server_type, fun server_type/1} + , {algorithm, fun algorithm/1} + , {iteration_count, fun iteration_count/1} + ]. + +server_type(type) -> hoconsc:enum(['built-in-database']); +server_type(default) -> 'built-in-database'; +server_type(_) -> undefined. + +algorithm(type) -> hoconsc:enum([sha256, sha256]); +algorithm(default) -> sha256; +algorithm(_) -> undefined. + +iteration_count(type) -> non_neg_integer(); +iteration_count(default) -> 4096; +iteration_count(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, Authenticator, #{algorithm := Algorithm, + iteration_count := IterationCount}) -> + State = #{user_group => {ChainID, Authenticator}, + algorithm => Algorithm, + iteration_count => IterationCount}, + {ok, State}. + +update(_ChainID, _Authenticator, _Config, _State) -> + {error, update_not_suppored}. + +authenticate(#{auth_method := AuthMethod, + auth_data := AuthData, + auth_cache := AuthCache}, State) -> + case ensure_auth_method(AuthMethod, State) of + true -> + case AuthCache of + #{next_step := client_final} -> + check_client_final_message(AuthData, AuthCache, State); + _ -> + check_client_first_message(AuthData, AuthCache, State) + end; + false -> + ignore + end; +authenticate(_Credential, _State) -> + ignore. + +destroy(#{user_group := UserGroup}) -> + trans( + fun() -> + MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun(UserCredential) -> + mnesia:delete_object(?TAB, UserCredential, write) + end, mnesia:select(?TAB, MatchSpec, write)) + end). + +%% TODO: binary to atom +add_user(#{<<"user_id">> := UserID, + <<"password">> := Password}, #{user_group := UserGroup} = State) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + add_user(UserID, Password, State), + {ok, #{user_id => UserID}}; + [_] -> + {error, already_exist} + end + end). + +delete_user(UserID, #{user_group := UserGroup}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + {error, not_found}; + [_] -> + mnesia:delete(?TAB, {UserGroup, UserID}, write) + end + end). + +update_user(UserID, #{<<"password">> := Password}, + #{user_group := UserGroup} = State) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + {error, not_found}; + [_] -> + add_user(UserID, Password, State), + {ok, #{user_id => UserID}} + end + end). + +lookup_user(UserID, #{user_group := UserGroup}) -> + case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of + [#scram_user_credentail{user_id = {_, UserID}}] -> + {ok, #{user_id => UserID}}; + [] -> + {error, not_found} + end. + +%% TODO: Support Pagination +list_users(#{user_group := UserGroup}) -> + Users = [#{user_id => UserID} || + #scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], + {ok, Users}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +ensure_auth_method('SCRAM-SHA-256', #{algorithm := sha256}) -> + true; +ensure_auth_method('SCRAM-SHA-512', #{algorithm := sha512}) -> + true; +ensure_auth_method(_, _) -> + false. + +check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> + LookupFun = fun(Username) -> + lookup_user2(Username, State) + end, + case esasl_scram:check_client_first_message( + Bin, + #{iteration_count => IterationCount, + lookup => LookupFun} + ) of + {cotinue, ServerFirstMessage, Cache} -> + {cotinue, ServerFirstMessage, Cache}; + {error, _Reason} -> + {error, not_authorized} + end. + +check_client_final_message(Bin, Cache, #{algorithm := Alg}) -> + case esasl_scram:check_client_final_message( + Bin, + Cache#{algorithm => Alg} + ) of + {ok, ServerFinalMessage} -> + {ok, ServerFinalMessage}; + {error, _Reason} -> + {error, not_authorized} + end. + +add_user(UserID, Password, State) -> + UserCredential = esasl_scram:generate_user_credential(UserID, Password, State), + mnesia:write(?TAB, UserCredential, write). + +lookup_user2(UserID, #{user_group := UserGroup}) -> + case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of + [#scram_user_credentail{} = UserCredential] -> + {ok, UserCredential}; + [] -> + {error, not_found} + end. + +%% TODO: Move to emqx_authn_utils.erl +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 692ff924e..0b36cf350 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -46,10 +46,9 @@ structs() -> [""]. fields("") -> - [ {config, #{type => hoconsc:union( - [ hoconsc:ref(?MODULE, get) - , hoconsc:ref(?MODULE, post) - ])}} + [ {config, {union, [ hoconsc:t(get) + , hoconsc:t(post) + ]}} ]; fields(get) -> @@ -64,7 +63,8 @@ fields(post) -> ] ++ common_fields(). common_fields() -> - [ {url, fun url/1} + [ {server_type, {enum, ['http-server']}} + , {url, fun url/1} , {accept, fun accept/1} , {headers, fun headers/1} , {form_data, fun form_data/1} @@ -142,10 +142,12 @@ update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = Stat {error, Reason} -> {error, Reason} end. -authenticate(ClientInfo, #{resource_id := ResourceID, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(Credential, #{resource_id := ResourceID, method := Method, request_timeout := RequestTimeout} = State) -> - Request = generate_request(ClientInfo, State), + Request = generate_request(Credential, State), case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of {ok, 204, _Headers} -> ok; {ok, 200, Headers, Body} -> @@ -154,8 +156,8 @@ authenticate(ClientInfo, #{resource_id := ResourceID, {ok, _NBody} -> %% TODO: Return by user property ok; - {error, Reason} -> - {stop, Reason} + {error, _Reason} -> + ok end; {error, _Reason} -> ignore @@ -208,13 +210,13 @@ generate_base_url(#{scheme := Scheme, port := Port}) -> iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])). -generate_request(ClientInfo, #{method := Method, +generate_request(Credential, #{method := Method, path := Path, base_query := BaseQuery, content_type := ContentType, headers := Headers, form_data := FormData0}) -> - FormData = replace_placeholders(FormData0, ClientInfo), + FormData = replace_placeholders(FormData0, Credential), case Method of get -> NPath = append_query(Path, BaseQuery ++ FormData), @@ -225,9 +227,9 @@ generate_request(ClientInfo, #{method := Method, {NPath, Headers, Body} end. -replace_placeholders(FormData0, ClientInfo) -> +replace_placeholders(FormData0, Credential) -> FormData = lists:map(fun({K, V0}) -> - case replace_placeholder(V0, ClientInfo) of + case replace_placeholder(V0, Credential) of undefined -> {K, undefined}; V -> {K, bin(V)} end @@ -236,16 +238,16 @@ replace_placeholders(FormData0, ClientInfo) -> V =/= undefined end, FormData). -replace_placeholder(<<"${mqtt-username}">>, ClientInfo) -> - maps:get(username, ClientInfo, undefined); -replace_placeholder(<<"${mqtt-clientid}">>, ClientInfo) -> - maps:get(clientid, ClientInfo, undefined); -replace_placeholder(<<"${ip-address}">>, ClientInfo) -> - maps:get(peerhost, ClientInfo, undefined); -replace_placeholder(<<"${cert-subject}">>, ClientInfo) -> - maps:get(dn, ClientInfo, undefined); -replace_placeholder(<<"${cert-common-name}">>, ClientInfo) -> - maps:get(cn, ClientInfo, undefined); +replace_placeholder(<<"${mqtt-username}">>, Credential) -> + maps:get(username, Credential, undefined); +replace_placeholder(<<"${mqtt-clientid}">>, Credential) -> + maps:get(clientid, Credential, undefined); +replace_placeholder(<<"${ip-address}">>, Credential) -> + maps:get(peerhost, Credential, undefined); +replace_placeholder(<<"${cert-subject}">>, Credential) -> + maps:get(dn, Credential, undefined); +replace_placeholder(<<"${cert-common-name}">>, Credential) -> + maps:get(cn, Credential, undefined); replace_placeholder(Constant, _) -> Constant. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl index 95e4b3d6d..d6e977be6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl @@ -132,10 +132,7 @@ handle_options(#{endpoint := Endpoint, refresh_interval => limit_refresh_interval(RefreshInterval0), ssl_opts => maps:to_list(SSLOpts), jwks => [], - request_id => undefined}; - -handle_options(#{enable_ssl := false} = Opts) -> - handle_options(Opts#{ssl_opts => #{}}). + request_id => undefined}. refresh_jwks(#{endpoint := Endpoint, ssl_opts := SSLOpts} = State) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 8fae45ff4..2605a682d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -35,21 +35,14 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +structs() -> [""]. fields("") -> - [{config, {union, [ hoconsc:t('hmac-based') - , hoconsc:t('public-key') - , hoconsc:t('jwks') - , hoconsc:t('jwks-using-ssl') - ]}}]; - -fields(config) -> - [{union, [ hoconsc:t('hmac-based') - , hoconsc:t('public-key') - , hoconsc:t('jwks') - , hoconsc:t('jwks-using-ssl') - ]}]; + [ {config, {union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + ]}} + ]; fields('hmac-based') -> [ {use_jwks, {enum, [false]}} @@ -67,35 +60,35 @@ fields('public-key') -> ]; fields('jwks') -> - [ {enable_ssl, {enum, [false]}} - ] ++ jwks_fields(); + [ {use_jwks, {enum, [true]}} + , {endpoint, fun endpoint/1} + , {refresh_interval, fun refresh_interval/1} + , {verify_claims, fun verify_claims/1} + , {ssl, #{type => hoconsc:union( + [ hoconsc:ref(?MODULE, ssl_enable) + , hoconsc:ref(?MODULE, ssl_disable) + ]), + default => #{<<"enable">> => false}}} + ]; -fields('jwks-using-ssl') -> - [ {enable_ssl, {enum, [true]}} - , {ssl_opts, fun ssl_opts/1} - ] ++ jwks_fields(); - -fields(ssl_opts) -> - [ {cacertfile, fun cacertfile/1} +fields(ssl_enable) -> + [ {enable, #{type => true}} + , {cacertfile, fun cacertfile/1} , {certfile, fun certfile/1} , {keyfile, fun keyfile/1} , {verify, fun verify/1} , {server_name_indication, fun server_name_indication/1} ]; +fields(ssl_disable) -> + [ {enable, #{type => false}} ]; + fields(claim) -> [ {"$name", fun expected_claim_value/1} ]. validations() -> [ {check_verify_claims, fun check_verify_claims/1} ]. -jwks_fields() -> - [ {use_jwks, {enum, [true]}} - , {endpoint, fun endpoint/1} - , {refresh_interval, fun refresh_interval/1} - , {verify_claims, fun verify_claims/1} - ]. - secret(type) -> string(); secret(_) -> undefined. @@ -109,9 +102,9 @@ certificate(_) -> undefined. endpoint(type) -> string(); endpoint(_) -> undefined. -ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); -ssl_opts(default) -> []; -ssl_opts(_) -> undefined. +% ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); +% ssl_opts(default) -> []; +% ssl_opts(_) -> undefined. refresh_interval(type) -> integer(); refresh_interval(default) -> 300; @@ -169,7 +162,9 @@ update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Conn update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> create(Config). -authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(Credential = #{password := JWT}, #{jwk := JWK, verify_claims := VerifyClaims0}) -> JWKs = case erlang:is_pid(JWK) of false -> @@ -178,11 +173,11 @@ authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, {ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK), JWKs0 end, - VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), + VerifyClaims = replace_placeholder(VerifyClaims0, Credential), case verify(JWT, JWKs, VerifyClaims) of ok -> ok; {error, invalid_signature} -> ignore; - {error, {claims, _}} -> {stop, bad_password} + {error, {claims, _}} -> {error, bad_username_or_password} end. destroy(#{jwk := Connector}) when is_pid(Connector) -> @@ -222,8 +217,13 @@ create2(#{use_jwks := false, verify_claims => VerifyClaims}}; create2(#{use_jwks := true, - verify_claims := VerifyClaims} = Config) -> - case emqx_authn_jwks_connector:start_link(Config) of + verify_claims := VerifyClaims, + ssl := #{enable := Enable} = SSL} = Config) -> + SSLOpts = case Enable of + true -> maps:without(enable, SSL); + false -> #{} + end, + case emqx_authn_jwks_connector:start_link(Config#{ssl_opts => SSLOpts}) of {ok, Connector} -> {ok, #{jwk => Connector, verify_claims => VerifyClaims}}; diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 26b20c517..4b1bcbb76 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -55,7 +55,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --define(TAB, mnesia_basic_auth). +-define(TAB, ?MODULE). -rlog_shard({?AUTH_SHARD, ?TAB}). %%------------------------------------------------------------------------------ @@ -81,7 +81,8 @@ mnesia(copy) -> structs() -> [config]. fields(config) -> - [ {user_id_type, fun user_id_type/1} + [ {server_type, {enum, ['built-in-database']}} + , {user_id_type, fun user_id_type/1} , {password_hash_algorithm, fun password_hash_algorithm/1} ]; @@ -95,11 +96,11 @@ fields(other_algorithms) -> ]. user_id_type(type) -> user_id_type(); -user_id_type(default) -> clientid; +user_id_type(default) -> username; user_id_type(_) -> undefined. password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; -password_hash_algorithm(default) -> sha256; +password_hash_algorithm(default) -> #{<<"name">> => sha256}; password_hash_algorithm(_) -> undefined. salt_rounds(type) -> integer(); @@ -130,11 +131,13 @@ create(ChainID, AuthenticatorName, #{user_id_type := Type, update(ChainID, AuthenticatorName, Config, _State) -> create(ChainID, AuthenticatorName, Config). -authenticate(ClientInfo = #{password := Password}, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(#{password := Password} = Credential, #{user_group := UserGroup, user_id_type := Type, password_hash_algorithm := Algorithm}) -> - UserID = get_user_identity(ClientInfo, Type), + UserID = get_user_identity(Credential, Type), case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; @@ -145,7 +148,7 @@ authenticate(ClientInfo = #{password := Password}, end, case PasswordHash =:= hash(Algorithm, Password, Salt) of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end end. @@ -330,8 +333,7 @@ gen_salt(#{password_hash_algorithm := bcrypt, {ok, Salt} = bcrypt:gen_salt(Rounds), Salt; gen_salt(_) -> - <> = crypto:strong_rand_bytes(16), - iolist_to_binary(io_lib:format("~32.16.0b", [X])). + emqx_authn_utils:gen_salt(). hash(bcrypt, Password, Salt) -> {ok, Hash} = bcrypt:hashpw(Password, Salt), @@ -343,10 +345,10 @@ insert_user(UserGroup, UserID, PasswordHash) -> insert_user(UserGroup, UserID, PasswordHash, <<>>). insert_user(UserGroup, UserID, PasswordHash, Salt) -> - Credential = #user_info{user_id = {UserGroup, UserID}, - password_hash = PasswordHash, - salt = Salt}, - mnesia:write(?TAB, Credential, write). + UserInfo = #user_info{user_id = {UserGroup, UserID}, + password_hash = PasswordHash, + salt = Salt}, + mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> mnesia:delete_object(?TAB, UserInfo, write). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index cc4445eaf..c76ece1c4 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -36,7 +36,8 @@ structs() -> [config]. fields(config) -> - [ {password_hash_algorithm, fun password_hash_algorithm/1} + [ {server_type, {enum, [mysql]}} + , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} , {query_timeout, fun query_timeout/1} @@ -81,12 +82,14 @@ update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = Stat {error, Reason} -> {error, Reason} end. -authenticate(#{password := Password} = ClientInfo, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(#{password := Password} = Credential, #{resource_id := ResourceID, placeholders := PlaceHolders, query := Query, query_timeout := Timeout} = State) -> - Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential), case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> @@ -106,14 +109,14 @@ destroy(#{resource_id := ResourceID}) -> %%------------------------------------------------------------------------------ check_password(undefined, _Algorithm, _Selected) -> - {stop, bad_password}; + {error, bad_username_or_password}; check_password(Password, #{password_hash := Hash}, #{password_hash_algorithm := bcrypt}) -> {ok, Hash0} = bcrypt:hashpw(Password, Hash), case list_to_binary(Hash0) =:= Hash of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end; check_password(Password, #{password_hash := Hash} = Selected, @@ -126,7 +129,7 @@ check_password(Password, end, case Hash0 =:= Hash of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end. %% TODO: Support prepare diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index c9046c606..700298c46 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -36,9 +36,10 @@ structs() -> [config]. fields(config) -> - [ {password_hash_algorithm, fun password_hash_algorithm/1} - , {salt_position, {enum, [prefix, suffix]}} - , {query, fun query/1} + [ {server_type, {enum, [pgsql]}} + , {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} ] ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). @@ -75,11 +76,13 @@ update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> {error, Reason} -> {error, Reason} end. -authenticate(#{password := Password} = ClientInfo, +authenticate(#{auth_method := _}, _) -> + ignore; +authenticate(#{password := Password} = Credential, #{resource_id := ResourceID, query := Query, placeholders := PlaceHolders} = State) -> - Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential), case emqx_resource:query(ResourceID, {sql, Query, Params}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> @@ -99,14 +102,14 @@ destroy(#{resource_id := ResourceID}) -> %%------------------------------------------------------------------------------ check_password(undefined, _Algorithm, _Selected) -> - {stop, bad_password}; + {error, bad_username_or_password}; check_password(Password, #{password_hash := Hash}, #{password_hash_algorithm := bcrypt}) -> {ok, Hash0} = bcrypt:hashpw(Password, Hash), case list_to_binary(Hash0) =:= Hash of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end; check_password(Password, #{password_hash := Hash} = Selected, @@ -119,7 +122,7 @@ check_password(Password, end, case Hash0 =:= Hash of true -> ok; - false -> {stop, bad_password} + false -> {error, bad_username_or_password} end. %% TODO: Support prepare diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 17c08cc70..827eb49ab 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include("emqx_authn.hrl"). + -define(AUTH, emqx_authn). all() -> @@ -40,16 +42,17 @@ end_per_suite(_) -> set_special_configs(emqx_authn) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authn, "test")), - Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), ok; set_special_configs(_App) -> ok. t_chain(_) -> + ?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)), + ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, + Chain = #{id => ChainID}, ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), @@ -57,86 +60,37 @@ t_chain(_) -> ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), ok. -t_binding(_) -> - Listener1 = <<"listener1">>, - Listener2 = <<"listener2">>, - ChainID = <<"mychain">>, - - ?assertEqual({error, {not_found, {chain, ChainID}}}, ?AUTH:bind(ChainID, [Listener1])), - - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - - ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1])), - ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener2])), - ?assertEqual({error, {already_bound, [Listener1]}}, ?AUTH:bind(ChainID, [Listener1])), - {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), - ?assertEqual(2, length(Listeners)), - ?assertMatch({ok, #{simple := ChainID}}, ?AUTH:list_bound_chains(Listener1)), - - ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1])), - ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener2])), - ?assertEqual({error, {not_found, [Listener1]}}, ?AUTH:unbind(ChainID, [Listener1])), - - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - -t_binding2(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - - Listener1 = <<"listener1">>, - Listener2 = <<"listener2">>, - - ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1, Listener2])), - {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), - ?assertEqual(2, length(Listeners)), - ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1, Listener2])), - ?assertMatch({ok, #{listeners := []}}, ?AUTH:list_bindings(ChainID)), - - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - t_authenticator(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), - AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorConfig1 = #{name => AuthenticatorName1, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName1)), - ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), - ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)), + ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), - ?assertMatch({ok, #{id := ChainID, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), + ?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(ChainID, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 1)), - ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 0)), - ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName2)), - ?assertEqual({ok, []}, ?AUTH:list_authenticators(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), + ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 27f34f936..008deca3d 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include("emqx_authn.hrl"). + -define(AUTH, emqx_authn). all() -> @@ -39,18 +41,13 @@ end_per_suite(_) -> set_special_configs(emqx_authn) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authn, "test")), - Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), ok; set_special_configs(_App) -> ok. t_jwt_authenticator(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - AuthenticatorName = <<"myauthenticator">>, Config = #{use_jwks => false, algorithm => 'hmac-based', @@ -58,84 +55,74 @@ t_jwt_authenticator(_) -> secret_base64_encoded => false, verify_claims => []}, AuthenticatorConfig = #{name => AuthenticatorName, - type => jwt, + mechanism => jwt, config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), - - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), ClientInfo2 = ClientInfo#{password => BadJWS}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), %% secret_base64_encoded Config2 = Config#{secret => base64:encode(<<"abcdef">>), secret_base64_encoded => true}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config2)), - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config2)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config3)), - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>})), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config3)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), %% Expiration Payload3 = #{ <<"username">> => <<"myuser">> , <<"exp">> => erlang:system_time(second) - 60}, JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), ClientInfo3 = ClientInfo#{password => JWS3}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), Payload4 = #{ <<"username">> => <<"myuser">> , <<"exp">> => erlang:system_time(second) + 60}, JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), %% Issued At Payload5 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) - 60}, JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo5)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), Payload6 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) + 60}, JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), ClientInfo6 = ClientInfo#{password => JWS6}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo6)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)), %% Not Before Payload7 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) - 60}, JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo7)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), Payload8 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) + 60}, JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), ClientInfo8 = ClientInfo#{password => JWS8}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo8)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ok. t_jwt_authenticator2(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - Dir = code:lib_dir(emqx_authn, test), PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), @@ -145,23 +132,18 @@ t_jwt_authenticator2(_) -> certificate => PublicKey, verify_claims => []}, AuthenticatorConfig = #{name => AuthenticatorName, - type => jwt, + mechanism => jwt, config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), - - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('public-key', Payload, PrivateKey), - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ok. generate_jws('hmac-based', Payload, Secret) -> diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index abc7ad149..75dd497ae 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include("emqx_authn.hrl"). + -define(AUTH, emqx_authn). all() -> @@ -39,149 +41,125 @@ end_per_suite(_) -> set_special_configs(emqx_authn) -> application:set_env(emqx, plugins_etc_dir, emqx_ct_helpers:deps_path(emqx_authn, "test")), - Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}}, ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), ok; set_special_configs(_App) -> ok. t_mnesia_authenticator(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ct:pal("11111 ~p~n", [?AUTH:list_authenticators(<<"mqtt">>)]), + AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), UserInfo = #{<<"user_id">> => <<"myuser">>, <<"password">> => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), - - ClientInfo = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo = #{username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)), ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), - ?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName)), - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertEqual([], ets:tab2list(mnesia_basic_auth)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ok. t_import(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), Dir = code:lib_dir(emqx_authn, test), - ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser3">>)), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)), - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), - - ClientInfo1 = #{listener_id => ListenerID, - username => <<"myuser1">>, + ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, password => <<"mypassword3">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), - - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ok. t_multi_mnesia_authenticator(_) -> - ChainID = <<"mychain">>, - Chain = #{id => ChainID, - type => simple}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorConfig1 = #{name => AuthenticatorName1, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => username, password_hash_algorithm => #{ name => sha256 }}}, AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorConfig2 = #{name => AuthenticatorName2, - type => 'built-in-database', + mechanism => 'password-based', config => #{ + server_type => 'built-in-database', user_id_type => clientid, password_hash_algorithm => #{ name => sha256 }}}, - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(ChainID, AuthenticatorName1, + ?AUTH:add_user(?CHAIN, AuthenticatorName1, #{<<"user_id">> => <<"myuser">>, <<"password">> => <<"mypass1">>})), ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(ChainID, AuthenticatorName2, + ?AUTH:add_user(?CHAIN, AuthenticatorName2, #{<<"user_id">> => <<"myclient">>, <<"password">> => <<"mypass2">>})), - ListenerID = <<"listener1">>, - ?AUTH:bind(ChainID, [ListenerID]), - - ClientInfo1 = #{listener_id => ListenerID, - username => <<"myuser">>, + ClientInfo1 = #{username => <<"myuser">>, clientid => <<"myclient">>, password => <<"mypass1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?AUTH:unbind([ListenerID], ChainID), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), ok. diff --git a/apps/emqx_exproto/src/emqx_exproto_channel.erl b/apps/emqx_exproto/src/emqx_exproto_channel.erl index d45f445ab..5b66cacbc 100644 --- a/apps/emqx_exproto/src/emqx_exproto_channel.erl +++ b/apps/emqx_exproto/src/emqx_exproto_channel.erl @@ -260,25 +260,21 @@ handle_call({auth, ClientInfo0, Password}, Channel = #channel{conninfo = ConnInfo, clientinfo = ClientInfo}) -> ClientInfo1 = enrich_clientinfo(ClientInfo0, ClientInfo), - NConnInfo = enrich_conninfo(ClientInfo1, ConnInfo), + ConnInfo1 = enrich_conninfo(ClientInfo1, ConnInfo), - Channel1 = Channel#channel{conninfo = NConnInfo, + Channel1 = Channel#channel{conninfo = ConnInfo1, clientinfo = ClientInfo1}, #{clientid := ClientId, username := Username} = ClientInfo1, case emqx_access_control:authenticate(ClientInfo1#{password => Password}) of - {ok, AuthResult} -> + ok -> emqx_logger:set_metadata_clientid(ClientId), - is_anonymous(AuthResult) andalso - emqx_metrics:inc('client.auth.anonymous'), - NClientInfo = maps:merge(ClientInfo1, AuthResult), - NChannel = Channel1#channel{clientinfo = NClientInfo}, - case emqx_cm:open_session(true, NClientInfo, NConnInfo) of + case emqx_cm:open_session(true, ClientInfo1, ConnInfo1) of {ok, _Session} -> ?LOG(debug, "Client ~s (Username: '~s') authorized successfully!", [ClientId, Username]), - {reply, ok, [{event, connected}], ensure_connected(NChannel)}; + {reply, ok, [{event, connected}], ensure_connected(Channel1)}; {error, Reason} -> ?LOG(warning, "Client ~s (Username: '~s') open session failed for ~0p", [ClientId, Username, Reason]), @@ -393,9 +389,6 @@ terminate(Reason, Channel) -> Req = #{reason => stringfy(Reason)}, try_dispatch(on_socket_closed, wrap(Req), Channel). -is_anonymous(#{anonymous := true}) -> true; -is_anonymous(_AuthResult) -> false. - %%-------------------------------------------------------------------- %% Sub/UnSub %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 5dadf2aca..f43620996 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -33,7 +33,7 @@ %% Gateway ID , type := gateway_type() %% Autenticator - , auth := allow_anonymous | emqx_authentication:chain_id() + , auth := emqx_authn:chain_id() %% The ConnectionManager PID , cm := pid() }. @@ -66,19 +66,19 @@ -spec authenticate(context(), emqx_types:clientinfo()) -> {ok, emqx_types:clientinfo()} | {error, any()}. -authenticate(_Ctx = #{auth := allow_anonymous}, ClientInfo) -> - {ok, ClientInfo#{anonymous => true}}; authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> ClientInfo = ClientInfo0#{ zone => undefined, chain_id => ChainId }, case emqx_access_control:authenticate(ClientInfo) of - {ok, AuthResult} -> - {ok, mountpoint(maps:merge(ClientInfo, AuthResult))}; + ok -> + {ok, mountpoint(ClientInfo)}; {error, Reason} -> {error, Reason} - end. + end; +authenticate(_Ctx, ClientInfo) -> + {ok, ClientInfo}. %% @doc Register the session to the cluster. %% diff --git a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl index 34c72dcca..51c278b56 100644 --- a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl +++ b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl @@ -86,15 +86,14 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> ClientInfo = clientinfo(Lwm2mState), _ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined), case emqx_access_control:authenticate(ClientInfo) of - {ok, AuthResult} -> + ok -> _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined), - ClientInfo1 = maps:merge(ClientInfo, AuthResult), Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683), - ClientInfo2 = maps:put(sockport, Sockport, ClientInfo1), + ClientInfo1 = maps:put(sockport, Sockport, ClientInfo), Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(), - mountpoint = maps:get(mountpoint, ClientInfo2)}, - run_hooks('client.connected', [ClientInfo2, conninfo(Lwm2mState1)]), + mountpoint = maps:get(mountpoint, ClientInfo1)}, + run_hooks('client.connected', [ClientInfo1, conninfo(Lwm2mState1)]), erlang:send(CoapPid, post_init), erlang:send_after(2000, CoapPid, auto_observe), diff --git a/rebar.config b/rebar.config index 2a58dc142..0c741e907 100644 --- a/rebar.config +++ b/rebar.config @@ -63,6 +63,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} + , {esasl, {git, "https://github.com/emqx/esasl", {branch, "refactor/sasl"}}} ]}. {xref_ignores, From 42c54325146aed446535bbeaa1f9a894d4897c07 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 14 Jul 2021 18:20:46 +0800 Subject: [PATCH 156/379] fix(http authn): fix bugs for http authn and http connector --- apps/emqx_authn/src/emqx_authn.app.src | 2 +- .../src/simple_authn/emqx_authn_http.erl | 19 ++++++++++++------ .../src/emqx_connector_http.erl | 20 +++++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index c997582ec..208e27b85 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {modules, []}, {registered, [emqx_authn_sup, emqx_authn_registry]}, - {applications, [kernel,stdlib]}, + {applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql]}, {mod, {emqx_authn_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 0b36cf350..14240b578 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -72,7 +72,7 @@ common_fields() -> ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)). validations() -> - [ {check_ssl_opts, fun emqx_connector_http:check_ssl_opts/1} ]. + [ {check_ssl_opts, fun check_ssl_opts/1} ]. url(type) -> binary(); url(nullable) -> false; @@ -108,26 +108,30 @@ create(ChainID, AuthenticatorName, #{method := Method, url := URL, accept := Accept, - content_type := ContentType, headers := Headers, form_data := FormData, request_timeout := RequestTimeout} = Config) -> - NHeaders = maps:merge(#{<<"accept">> => atom_to_binary(Accept, utf8), - <<"content-type">> => atom_to_binary(ContentType, utf8)}, Headers), + ContentType = maps:get(content_type, Config, undefined), + DefaultHeader0 = case ContentType of + undefined -> #{}; + _ -> #{<<"content-type">> => atom_to_binary(ContentType, utf8)} + end, + DefaultHeader = DefaultHeader0#{<<"accept">> => atom_to_binary(Accept, utf8)}, + NHeaders = maps:to_list(maps:merge(DefaultHeader, maps:from_list(Headers))), NFormData = preprocess_form_data(FormData), #{path := Path, query := Query} = URIMap = parse_url(URL), BaseURL = generate_base_url(URIMap), State = #{method => Method, path => Path, - base_query => cow_qs:parse_qs(Query), + base_query => cow_qs:parse_qs(list_to_binary(Query)), accept => Accept, content_type => ContentType, headers => NHeaders, form_data => NFormData, request_timeout => RequestTimeout}, ResourceID = <>, - case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url := BaseURL}) of + case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url => BaseURL}) of {ok, _} -> {ok, State#{resource_id => ResourceID}}; {error, already_created} -> @@ -192,6 +196,9 @@ check_form_data(FormData) -> false end. +check_ssl_opts(Conf) -> + emqx_connector_http:check_ssl_opts("url", Conf). + preprocess_form_data(FormData) -> KVs = binary:split(FormData, [<<"&">>], [global]), [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs]. diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index bef5c26f3..94940a065 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -32,7 +32,7 @@ , fields/1 , validations/0]). --export([ check_ssl_opts/1 ]). +-export([ check_ssl_opts/2 ]). -type connect_timeout() :: non_neg_integer() | infinity. -type pool_type() :: random | hash. @@ -57,7 +57,7 @@ fields(config) -> , {pool_type, fun pool_type/1} , {pool_size, fun pool_size/1} , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), - nullable => true}} + default => #{}}} ]; fields(ssl_opts) -> @@ -107,8 +107,9 @@ keyfile(type) -> string(); keyfile(nullable) -> true; keyfile(_) -> undefined. +%% TODO: certfile is required certfile(type) -> string(); -certfile(nullable) -> false; +certfile(nullable) -> true; certfile(_) -> undefined. verify(type) -> boolean(); @@ -178,7 +179,7 @@ on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery, #{pool_name : end, Result. -on_health_check(_InstId, #{server := {Host, Port}} = State) -> +on_health_check(_InstId, #{host := Host, port := Port} = State) -> case gen_tcp:connect(Host, Port, emqx_misc:ipv6_probe([]), 3000) of {ok, Sock} -> gen_tcp:close(Sock), @@ -199,13 +200,16 @@ check_base_url(URL) -> end. check_ssl_opts(Conf) -> - URL = hocon_schema:get_value("url", Conf), + check_ssl_opts("base_url", Conf). + +check_ssl_opts(URLFrom, Conf) -> + URL = hocon_schema:get_value(URLFrom, Conf), {ok, #{scheme := Scheme}} = emqx_http_lib:uri_parse(URL), SSLOpts = hocon_schema:get_value("ssl_opts", Conf), - case {Scheme, SSLOpts} of - {http, undefined} -> true; + case {Scheme, maps:size(SSLOpts)} of + {http, 0} -> true; {http, _} -> false; - {https, undefined} -> false; + {https, 0} -> false; {https, _} -> true end. From 3bb41ae36774028b3d988937bf5d9d6a8c677b84 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Wed, 14 Jul 2021 10:07:09 +0200 Subject: [PATCH 157/379] chore(ekka): Bump version to 0.10.3 --- apps/emqx/rebar.config | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index add05186c..5a68f658e 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -13,7 +13,7 @@ , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.3"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} diff --git a/rebar.config b/rebar.config index 2a58dc142..ed9874fd8 100644 --- a/rebar.config +++ b/rebar.config @@ -48,7 +48,7 @@ , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.0"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.3"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} % TODO: delete when all apps moved to hocon , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.1.1"}}} From d2430e70a8ef9f69709368f29c828ed36f713a35 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 6 Jul 2021 19:14:35 +0800 Subject: [PATCH 158/379] refactor(gw): move mqtt-sn to gateway --- apps/emqx_gateway/etc/emqx_gateway.conf | 43 +++++ .../etc/emqx_sn.conf | 0 .../etc}/priv/emqx_sn.schema | 0 .../src/bhvrs/emqx_gateway_impl.erl | 1 + apps/emqx_gateway/src/emqx_gateway_app.erl | 8 +- .../src/emqx_gateway_insta_sup.erl | 1 - .../emqx_gateway/src/emqx_gateway_metrics.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 53 +++++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 8 + .../src/mqttsn}/README.md | 25 +-- .../src/mqttsn}/emqx_sn_app.erl | 0 .../src/mqttsn}/emqx_sn_asleep_timer.erl | 0 .../src/mqttsn}/emqx_sn_broadcast.erl | 2 +- .../src/mqttsn}/emqx_sn_frame.erl | 2 +- .../src/mqttsn}/emqx_sn_gateway.erl | 17 +- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 161 ++++++++++++++++++ .../src/mqttsn}/emqx_sn_registry.erl | 4 +- .../src/mqttsn}/emqx_sn_sup.erl | 12 +- .../src/mqttsn}/include/emqx_sn.hrl | 0 .../src/stomp/emqx_stomp_impl.erl | 22 +-- .../test/broadcast_test.py | 0 .../test/emqx_sn_frame_SUITE.erl | 0 .../test/emqx_sn_protocol_SUITE.erl | 0 .../test/emqx_sn_registry_SUITE.erl | 0 .../test}/intergration_test/Makefile | 0 .../test}/intergration_test/README.md | 0 .../add_emqx_sn_to_project.py | 0 .../intergration_test/client/case1_qos0pub.c | 0 .../intergration_test/client/case1_qos0sub.c | 0 .../intergration_test/client/case2_qos0pub.c | 0 .../intergration_test/client/case2_qos0sub.c | 0 .../intergration_test/client/case3_qos0pub.c | 0 .../intergration_test/client/case3_qos0sub.c | 0 .../intergration_test/client/case4_qos3pub.c | 0 .../intergration_test/client/case4_qos3sub.c | 0 .../intergration_test/client/case5_qos3pub.c | 0 .../intergration_test/client/case5_qos3sub.c | 0 .../intergration_test/client/case6_sleep.c | 0 .../client/case7_double_connect.c | 0 .../client/int_test_result.c | 0 .../client/int_test_result.h | 0 .../test}/intergration_test/disable_qos3.py | 0 .../test}/intergration_test/enable_qos3.py | 0 .../test/props/emqx_sn_proper_types.erl | 0 .../test/props/prop_emqx_sn_frame.erl | 0 apps/emqx_sn/.gitignore | 40 ----- apps/emqx_sn/examples/simple_example.erl | 126 -------------- apps/emqx_sn/examples/simple_example2.erl | 120 ------------- apps/emqx_sn/examples/simple_example3.erl | 120 ------------- apps/emqx_sn/examples/simple_example4.erl | 151 ---------------- apps/emqx_sn/rebar.config | 26 --- apps/emqx_sn/src/emqx_sn.app.src | 14 -- apps/emqx_sn/src/emqx_sn.appup.src | 19 --- apps/emqx_sn/vars | 8 - 54 files changed, 305 insertions(+), 680 deletions(-) rename apps/{emqx_sn => emqx_gateway}/etc/emqx_sn.conf (100%) rename apps/{emqx_sn => emqx_gateway/etc}/priv/emqx_sn.schema (100%) rename apps/{emqx_sn => emqx_gateway/src/mqttsn}/README.md (94%) rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_app.erl (100%) rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_asleep_timer.erl (100%) rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_broadcast.erl (98%) rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_frame.erl (99%) rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_gateway.erl (99%) create mode 100644 apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_registry.erl (98%) rename apps/{emqx_sn/src => emqx_gateway/src/mqttsn}/emqx_sn_sup.erl (85%) rename apps/{emqx_sn => emqx_gateway/src/mqttsn}/include/emqx_sn.hrl (100%) rename apps/{emqx_sn => emqx_gateway}/test/broadcast_test.py (100%) rename apps/{emqx_sn => emqx_gateway}/test/emqx_sn_frame_SUITE.erl (100%) rename apps/{emqx_sn => emqx_gateway}/test/emqx_sn_protocol_SUITE.erl (100%) rename apps/{emqx_sn => emqx_gateway}/test/emqx_sn_registry_SUITE.erl (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/Makefile (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/README.md (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/add_emqx_sn_to_project.py (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case1_qos0pub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case1_qos0sub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case2_qos0pub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case2_qos0sub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case3_qos0pub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case3_qos0sub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case4_qos3pub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case4_qos3sub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case5_qos3pub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case5_qos3sub.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case6_sleep.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/case7_double_connect.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/int_test_result.c (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/client/int_test_result.h (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/disable_qos3.py (100%) rename apps/{emqx_sn => emqx_gateway/test}/intergration_test/enable_qos3.py (100%) rename apps/{emqx_sn => emqx_gateway}/test/props/emqx_sn_proper_types.erl (100%) rename apps/{emqx_sn => emqx_gateway}/test/props/prop_emqx_sn_frame.erl (100%) delete mode 100644 apps/emqx_sn/.gitignore delete mode 100644 apps/emqx_sn/examples/simple_example.erl delete mode 100644 apps/emqx_sn/examples/simple_example2.erl delete mode 100644 apps/emqx_sn/examples/simple_example3.erl delete mode 100644 apps/emqx_sn/examples/simple_example4.erl delete mode 100644 apps/emqx_sn/rebar.config delete mode 100644 apps/emqx_sn/src/emqx_sn.app.src delete mode 100644 apps/emqx_sn/src/emqx_sn.appup.src delete mode 100644 apps/emqx_sn/vars diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index ab5b52143..ba1e8168b 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -27,4 +27,47 @@ emqx_gateway: { active_n: 100 } } + + mqttsn.1: { + ## The MQTT-SN Gateway ID in ADVERTISE message. + gateway_id: 1 + + ## Enable broadcast this gateway to WLAN + broadcast: true + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats: true + + ## To control whether accept and process the received + ## publish message with qos=-1. + enable_qos3: true + + ## Idle timeout for a MQTT-SN channel + idle_timeout: 30s + + ## The pre-defined topic name corresponding to the pre-defined topic + ## id of N. + ## Note that the pre-defined topic id of 0 is reserved. + predefined: [ + { id: 1 + topic: "/predefined/topic/name/hello" + }, + { id: 2 + topic: "/predefined/topic/name/nice" + } + ] + + ### ClientInfo override + clientinfo_override: { + username: "mqtt_sn_user" + password: "abc" + } + + listener.udp.1: { + bind: 1884 + max_connections: 10240000 + max_conn_rate: 1000 + } + } } diff --git a/apps/emqx_sn/etc/emqx_sn.conf b/apps/emqx_gateway/etc/emqx_sn.conf similarity index 100% rename from apps/emqx_sn/etc/emqx_sn.conf rename to apps/emqx_gateway/etc/emqx_sn.conf diff --git a/apps/emqx_sn/priv/emqx_sn.schema b/apps/emqx_gateway/etc/priv/emqx_sn.schema similarity index 100% rename from apps/emqx_sn/priv/emqx_sn.schema rename to apps/emqx_gateway/etc/priv/emqx_sn.schema diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl index 9726dad02..8d413e49c 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl @@ -31,6 +31,7 @@ ) -> {error, reason()} | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + %% TODO: v0.2 The child spec is better for restarting child process | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}. %% @doc diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index c99228f17..3982e260b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -45,7 +45,7 @@ load_default_gateway_applications() -> gateway_type_searching() -> %% FIXME: Hardcoded apps - [emqx_stomp_impl]. + [emqx_stomp_impl, emqx_sn_impl]. load(Mod) -> try @@ -65,7 +65,7 @@ create_gateway_by_default([]) -> create_gateway_by_default([{Type, Name, Confs}|More]) -> case emqx_gateway_registry:lookup(Type) of undefined -> - ?LOG(error, "Skip to start ~p#~p: not_registred_type", + ?LOG(error, "Skip to start ~s#~s: not_registred_type", [Type, Name]); _ -> case emqx_gateway:create(Type, @@ -73,9 +73,9 @@ create_gateway_by_default([{Type, Name, Confs}|More]) -> <<>>, Confs) of {ok, _} -> - ?LOG(debug, "Start ~p#~p successfully!", [Type, Name]); + ?LOG(debug, "Start ~s#~s successfully!", [Type, Name]); {error, Reason} -> - ?LOG(error, "Start ~p#~p failed: ~0p", + ?LOG(error, "Start ~s#~s failed: ~0p", [Type, Name, Reason]) end end, diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 9f21f0e05..7994a6cea 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -54,7 +54,6 @@ start_link(Insta, Ctx, GwDscrptr) -> gen_server:start_link( - {local, ?MODULE}, ?MODULE, [Insta, Ctx, GwDscrptr], [] diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl index 04b711d0a..461eb3344 100644 --- a/apps/emqx_gateway/src/emqx_gateway_metrics.erl +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -49,7 +49,7 @@ %%-------------------------------------------------------------------- start_link(Type) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [Type], []). + gen_server:start_link(?MODULE, [Type], []). -spec inc(gateway_type(), atom()) -> ok. inc(Type, Name) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 8f05582c7..f1b1f9fa4 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -35,7 +35,9 @@ structs() -> ["emqx_gateway"]. fields("emqx_gateway") -> - [{stomp, t(ref(stomp))}]; + [{stomp, t(ref(stomp))}, + {mqttsn, t(ref(mqttsn))} + ]; fields(stomp) -> [{"$id", t(ref(stomp_structs))}]; @@ -44,7 +46,7 @@ fields(stomp_structs) -> [ {frame, t(ref(stomp_frame))} , {clientinfo_override, t(ref(clientinfo_override))} , {authenticator, t(union([allow_anonymous]))} - , {listener, t(ref(listener))} + , {listener, t(ref(tcp_listener_group))} ]; fields(stomp_frame) -> @@ -53,13 +55,38 @@ fields(stomp_frame) -> , {max_body_length, t(integer(), undefined, 8192)} ]; +fields(mqttsn) -> + [{"$id", t(ref(mqttsn_structs))}]; + +fields(mqttsn_structs) -> + [ {gateway_id, t(integer())} + , {broadcast, t(boolean())} + , {enable_stats, t(boolean())} + , {enable_qos3, t(boolean())} + , {idle_timeout, t(duration())} + , {predefined, hoconsc:array(ref(mqttsn_predefined))} + , {clientinfo_override, t(ref(clientinfo_override))} + , {listener, t(ref(udp_listener_group))} + ]; + +fields(mqttsn_predefined) -> + %% FIXME: How to check the $id is a integer ??? + [ {id, t(integer())} + , {topic, t(string())} + ]; + fields(clientinfo_override) -> [ {username, t(string())} , {password, t(string())} , {clientid, t(string())} ]; -fields(listener) -> +fields(udp_listener_group) -> + [ {udp, t(ref(udp_listener))} + , {dtls, t(ref(dtls_listener))} + ]; + +fields(tcp_listener_group) -> [ {tcp, t(ref(tcp_listener))} , {ssl, t(ref(ssl_listener))} ]; @@ -70,7 +97,14 @@ fields(tcp_listener) -> fields(ssl_listener) -> [ {"$name", t(ref(ssl_listener_settings))}]; +fields(udp_listener) -> + [ {"$name", t(ref(udp_listener_settings))}]; + +fields(dtls_listener) -> + [ {"$name", t(ref(dtls_listener_settings))}]; + fields(listener_settings) -> + % FIXME: %[ {"bind", t(union(ip_port(), integer()))} [ {bind, t(integer())} , {acceptors, t(integer(), undefined, 8)} @@ -107,6 +141,19 @@ fields(ssl_listener_settings) -> , depth => 10 , reuse_sessions => true}) ++ fields(listener_settings); +fields(udp_listener_settings) -> + [ + %% some special confs for udp listener + ] ++ fields(listener_settings); + +fields(dtls_listener_settings) -> + [ + %% some special confs for dtls listener + ] ++ + ssl(undefined, #{handshake_timeout => "15s" + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); + fields(access) -> [ {"$id", #{type => string(), nullable => true}}]; diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 184c3ff87..b7e6658d1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -25,6 +25,7 @@ ]). -export([ apply/2 + , format_listenon/1 ]). -export([ normalize_rawconf/1 @@ -89,6 +90,13 @@ apply(F, A2) when is_function(F), is_list(A2) -> erlang:apply(F, A2). +format_listenon(Port) when is_integer(Port) -> + io_lib:format("0.0.0.0:~w", [Port]); +format_listenon({Addr, Port}) when is_list(Addr) -> + io_lib:format("~s:~w", [Addr, Port]); +format_listenon({Addr, Port}) when is_tuple(Addr) -> + io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). + -type listener() :: #{}. -type rawconf() :: diff --git a/apps/emqx_sn/README.md b/apps/emqx_gateway/src/mqttsn/README.md similarity index 94% rename from apps/emqx_sn/README.md rename to apps/emqx_gateway/src/mqttsn/README.md index d7251c49c..8179dde62 100644 --- a/apps/emqx_sn/README.md +++ b/apps/emqx_gateway/src/mqttsn/README.md @@ -1,10 +1,9 @@ -emqx-sn -======= +# MQTT-SN Gateway EMQ X MQTT-SN Gateway. -Configure Plugin ----------------- +## Configure Plugin + File: etc/emqx_sn.conf @@ -72,8 +71,7 @@ mqtt.sn.password = abc - mqtt.sn.password * This parameter is optional. Pair with username above. -Load Plugin ------------ +## Load Plugin ``` ./bin/emqx_ctl plugins load emqx_sn @@ -95,23 +93,18 @@ Load Plugin - https://github.com/njh/mqtt-sn-tools - https://github.com/arobenko/mqtt-sn -sleeping device ------------ +### sleeping device PINGREQ must have a ClientId which is identical to the one in CONNECT message. Without ClientId, emqx-sn will ignore such PINGREQ. -pre-defined topics ------------ +### pre-defined topics The mapping of a pre-defined topic id and topic name should be known inadvance by both client's application and gateway. We define this mapping info in emqx_sn.conf file, and which shall be kept equivalent in all client's side. -License -------- +## License Apache License Version 2.0 -Author ------- - -EMQ X-Men Team. +## Author +EMQ X Team. diff --git a/apps/emqx_sn/src/emqx_sn_app.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_app.erl similarity index 100% rename from apps/emqx_sn/src/emqx_sn_app.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_app.erl diff --git a/apps/emqx_sn/src/emqx_sn_asleep_timer.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_asleep_timer.erl similarity index 100% rename from apps/emqx_sn/src/emqx_sn_asleep_timer.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_asleep_timer.erl diff --git a/apps/emqx_sn/src/emqx_sn_broadcast.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_broadcast.erl similarity index 98% rename from apps/emqx_sn/src/emqx_sn_broadcast.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_broadcast.erl index a1630b844..69eb9a2c5 100644 --- a/apps/emqx_sn/src/emqx_sn_broadcast.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_broadcast.erl @@ -18,7 +18,7 @@ -behaviour(gen_server). --include("emqx_sn.hrl"). +-include("src/mqttsn/include/emqx_sn.hrl"). -export([ start_link/2 , stop/0 diff --git a/apps/emqx_sn/src/emqx_sn_frame.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_frame.erl similarity index 99% rename from apps/emqx_sn/src/emqx_sn_frame.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_frame.erl index eed32803d..301247fbc 100644 --- a/apps/emqx_sn/src/emqx_sn_frame.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_frame.erl @@ -17,7 +17,7 @@ -module(emqx_sn_frame). --include("emqx_sn.hrl"). +-include("src/mqttsn/include/emqx_sn.hrl"). -export([ parse/1 , serialize/1 diff --git a/apps/emqx_sn/src/emqx_sn_gateway.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl similarity index 99% rename from apps/emqx_sn/src/emqx_sn_gateway.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl index 1bccf0c1a..27eb16498 100644 --- a/apps/emqx_sn/src/emqx_sn_gateway.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl @@ -18,7 +18,7 @@ -behaviour(gen_statem). --include("emqx_sn.hrl"). +-include("src/mqttsn/include/emqx_sn.hrl"). -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/logger.hrl"). @@ -915,7 +915,8 @@ handle_unsubscribe(_, _TopicId, MsgId, State) -> {keep_state, send_message(?SN_UNSUBACK_MSG(MsgId), State)}. do_publish(?SN_NORMAL_TOPIC, TopicName, Data, Flags, MsgId, State) -> - %% XXX: Handle normal topic id as predefined topic id, to be compatible with paho mqtt-sn library + %% XXX: Handle normal topic id as predefined topic id, to be + %% compatible with paho mqtt-sn library <> = TopicName, do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, State); do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, @@ -972,8 +973,11 @@ do_puback(TopicId, MsgId, ReturnCode, StateName, undefined -> {keep_state, State}; TopicName -> %%notice that this TopicName maybe normal or predefined, - %% involving the predefined topic name in register to enhance the gateway's robustness even inconsistent with MQTT-SN channels - {keep_state, send_register(TopicName, TopicId, MsgId, State)} + %% involving the predefined topic name in register to + %% enhance the gateway's robustness even inconsistent + %% with MQTT-SN channels + {keep_state, send_register(TopicName, TopicId, + MsgId, State)} end; _ -> ?LOG(error, "CAN NOT handle PUBACK ReturnCode=~p", [ReturnCode]), @@ -1070,8 +1074,9 @@ handle_outgoing(Packet, State) -> send_message(mqtt2sn(Packet, State), State). cache_no_reg_publish_message(Pendings, TopicId, PubPkt, State) -> - ?LOG(debug, "cache non-registered publish message for topic-id: ~p, msg: ~0p, pendings: ~0p", - [TopicId, PubPkt, Pendings]), + ?LOG(debug, "cache non-registered publish message " + "for topic-id: ~p, msg: ~0p, pendings: ~0p", + [TopicId, PubPkt, Pendings]), Msgs = maps:get(pending_topic_ids, Pendings, []), Pendings#{TopicId => Msgs ++ [mqtt2sn(PubPkt, State)]}. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl new file mode 100644 index 000000000..c3b679381 --- /dev/null +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -0,0 +1,161 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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 The MQTT-SN Gateway Implement interface +-module(emqx_sn_impl). + +-behavior(emqx_gateway_impl). + +%% APIs +-export([ load/0 + , unload/0 + ]). + +-export([]). + +-export([ init/1 + , on_insta_create/3 + , on_insta_update/4 + , on_insta_destroy/3 + ]). + +-define(UDP_SOCKOPTS, []). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +load() -> + RegistryOptions = [ {cbkmod, ?MODULE} + ], + YourOptions = [params1, params2], + emqx_gateway_registry:load(mqttsn, RegistryOptions, YourOptions). + +unload() -> + emqx_gateway_registry:unload(mqttsn). + +init(_) -> + GwState = #{}, + {ok, GwState}. + +%%-------------------------------------------------------------------- +%% emqx_gateway_registry callbacks +%%-------------------------------------------------------------------- + +on_insta_create(_Insta = #{ id := InstaId, + rawconf := RawConf + }, Ctx, _GwState) -> + + %% We Also need to start `emqx_sn_broadcast` & + %% `emqx_sn_registry` process + SnGwId = maps:get(gateway_id, RawConf), + case maps:get(broadcast, RawConf) of + false -> + ok; + true -> + %% FIXME: + Port = 1884, + _ = emqx_sn_broadcast:start_link(SnGwId, Port) + end, + + PredefTopics = maps:get(predefined, RawConf), + {ok, RegistrySvr} = emqx_sn_registry:start_link(PredefTopics), + + NRawConf = maps:without( + [gateway_id, broadcast, predefined], + RawConf#{registry => RegistrySvr} + ), + Listeners = emqx_gateway_utils:normalize_rawconf(NRawConf), + + ListenerPids = lists:map(fun(Lis) -> + start_listener(InstaId, Ctx, Lis) + end, Listeners), + {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + +on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> + InstaId = maps:get(id, NewInsta), + try + %% XXX: 1. How hot-upgrade the changes ??? + %% XXX: 2. Check the New confs first before destroy old instance ??? + on_insta_destroy(OldInsta, GwInstaState, GwState), + on_insta_create(NewInsta, Ctx, GwState) + catch + Class : Reason : Stk -> + logger:error("Failed to update stomp instance ~s; " + "reason: {~0p, ~0p} stacktrace: ~0p", + [InstaId, Class, Reason, Stk]), + {error, {Class, Reason}} + end. + +on_insta_destroy(_Insta = #{ id := InstaId, + rawconf := RawConf + }, _GwInstaState, _GwState) -> + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + lists:foreach(fun(Lis) -> + stop_listener(InstaId, Lis) + end, Listeners). + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> + ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), + case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + {ok, Pid} -> + io:format("Start mqttsn ~s:~s listener on ~s successfully.~n", + [InstaId, Type, ListenOnStr]), + Pid; + {error, Reason} -> + io:format(standard_error, + "Failed to start mqttsn ~s:~s listener on ~s: ~0p~n", + [InstaId, Type, ListenOnStr, Reason]), + throw({badconf, Reason}) + end. + +start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(InstaId, Type), + esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), + {emqx_sn_gateway, start_link, [Cfg#{ctx => Ctx}]}). + +name(InstaId, Type) -> + list_to_atom(lists:concat([InstaId, ":", Type])). + +merge_default(Options) -> + case lists:keytake(udp_options, 1, Options) of + {value, {udp_options, TcpOpts}, Options1} -> + [{udp_options, emqx_misc:merge_opts(?UDP_SOCKOPTS, TcpOpts)} | Options1]; + false -> + [{udp_options, ?UDP_SOCKOPTS} | Options] + end. + +stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), + ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), + case StopRet of + ok -> io:format("Stop mqttsn ~s:~s listener on ~s successfully.~n", + [InstaId, Type, ListenOnStr]); + {error, Reason} -> + io:format(standard_error, + "Failed to stop mqttsn ~s:~s listener on ~s: ~0p~n", + [InstaId, Type, ListenOnStr, Reason] + ) + end, + StopRet. + +stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(InstaId, Type), + esockd:close(Name, ListenOn). diff --git a/apps/emqx_sn/src/emqx_sn_registry.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl similarity index 98% rename from apps/emqx_sn/src/emqx_sn_registry.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl index 903f61c70..f2f87d93b 100644 --- a/apps/emqx_sn/src/emqx_sn_registry.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl @@ -18,7 +18,7 @@ -behaviour(gen_server). --include("emqx_sn.hrl"). +-include("src/mqttsn/include/emqx_sn.hrl"). -define(LOG(Level, Format, Args), emqx_logger:Level("MQTT-SN(registry): " ++ Format, Args)). @@ -132,7 +132,7 @@ init([PredefTopics]) -> %% {ClientId, TopicId} -> TopicName %% {ClientId, TopicName} -> TopicId MaxPredefId = lists:foldl( - fun({TopicId, TopicName}, AccId) -> + fun(#{id := TopicId, topic := TopicName}, AccId) -> ekka_mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicId}, value = TopicName}), ekka_mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicName}, diff --git a/apps/emqx_sn/src/emqx_sn_sup.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_sup.erl similarity index 85% rename from apps/emqx_sn/src/emqx_sn_sup.erl rename to apps/emqx_gateway/src/mqttsn/emqx_sn_sup.erl index 3d4fe602f..e78b41766 100644 --- a/apps/emqx_sn/src/emqx_sn_sup.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_sup.erl @@ -33,11 +33,9 @@ init([{_Ip, Port}, GwId, PredefTopics]) -> type => worker, modules => [emqx_sn_broadcast]}, Registry = #{id => emqx_sn_registry, - start => {emqx_sn_registry, start_link, [PredefTopics]}, - restart => permanent, - shutdown => brutal_kill, - type => worker, - modules => [emqx_sn_registry]}, + start => {emqx_sn_registry, start_link, [PredefTopics]}, + restart => permanent, + shutdown => brutal_kill, + type => worker, + modules => [emqx_sn_registry]}, {ok, {{one_for_one, 10, 3600}, [Broadcast, Registry]}}. - - diff --git a/apps/emqx_sn/include/emqx_sn.hrl b/apps/emqx_gateway/src/mqttsn/include/emqx_sn.hrl similarity index 100% rename from apps/emqx_sn/include/emqx_sn.hrl rename to apps/emqx_gateway/src/mqttsn/include/emqx_sn.hrl diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index e6e62565a..86cce9c91 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -71,13 +71,12 @@ on_insta_create(_Insta = #{ id := InstaId, %% FIXME: Assign ctx to InstaState {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. -%% @private -on_insta_update(NewInsta, OldInstace, GwInstaState = #{ctx := Ctx}, GwState) -> +on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> InstaId = maps:get(id, NewInsta), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_insta_destroy(OldInstace, GwInstaState, GwState), + on_insta_destroy(OldInsta, GwInstaState, GwState), on_insta_create(NewInsta, Ctx, GwState) catch Class : Reason : Stk -> @@ -100,15 +99,16 @@ on_insta_destroy(_Insta = #{ id := InstaId, %%-------------------------------------------------------------------- start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> + ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> io:format("Start stomp ~s:~s listener on ~s successfully.~n", - [InstaId, Type, format(ListenOn)]), + [InstaId, Type, ListenOnStr]), Pid; {error, Reason} -> io:format(standard_error, "Failed to start stomp ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, format(ListenOn), Reason]), + [InstaId, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -128,22 +128,16 @@ merge_default(Options) -> [{tcp_options, ?TCP_OPTS} | Options] end. -format(Port) when is_integer(Port) -> - io_lib:format("0.0.0.0:~w", [Port]); -format({Addr, Port}) when is_list(Addr) -> - io_lib:format("~s:~w", [Addr, Port]); -format({Addr, Port}) when is_tuple(Addr) -> - io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). - stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), + ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of ok -> io:format("Stop stomp ~s:~s listener on ~s successfully.~n", - [InstaId, Type, format(ListenOn)]); + [InstaId, Type, ListenOnStr]); {error, Reason} -> io:format(standard_error, "Failed to stop stomp ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, format(ListenOn), Reason] + [InstaId, Type, ListenOnStr, Reason] ) end, StopRet. diff --git a/apps/emqx_sn/test/broadcast_test.py b/apps/emqx_gateway/test/broadcast_test.py similarity index 100% rename from apps/emqx_sn/test/broadcast_test.py rename to apps/emqx_gateway/test/broadcast_test.py diff --git a/apps/emqx_sn/test/emqx_sn_frame_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_frame_SUITE.erl similarity index 100% rename from apps/emqx_sn/test/emqx_sn_frame_SUITE.erl rename to apps/emqx_gateway/test/emqx_sn_frame_SUITE.erl diff --git a/apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl similarity index 100% rename from apps/emqx_sn/test/emqx_sn_protocol_SUITE.erl rename to apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl diff --git a/apps/emqx_sn/test/emqx_sn_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl similarity index 100% rename from apps/emqx_sn/test/emqx_sn_registry_SUITE.erl rename to apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl diff --git a/apps/emqx_sn/intergration_test/Makefile b/apps/emqx_gateway/test/intergration_test/Makefile similarity index 100% rename from apps/emqx_sn/intergration_test/Makefile rename to apps/emqx_gateway/test/intergration_test/Makefile diff --git a/apps/emqx_sn/intergration_test/README.md b/apps/emqx_gateway/test/intergration_test/README.md similarity index 100% rename from apps/emqx_sn/intergration_test/README.md rename to apps/emqx_gateway/test/intergration_test/README.md diff --git a/apps/emqx_sn/intergration_test/add_emqx_sn_to_project.py b/apps/emqx_gateway/test/intergration_test/add_emqx_sn_to_project.py similarity index 100% rename from apps/emqx_sn/intergration_test/add_emqx_sn_to_project.py rename to apps/emqx_gateway/test/intergration_test/add_emqx_sn_to_project.py diff --git a/apps/emqx_sn/intergration_test/client/case1_qos0pub.c b/apps/emqx_gateway/test/intergration_test/client/case1_qos0pub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case1_qos0pub.c rename to apps/emqx_gateway/test/intergration_test/client/case1_qos0pub.c diff --git a/apps/emqx_sn/intergration_test/client/case1_qos0sub.c b/apps/emqx_gateway/test/intergration_test/client/case1_qos0sub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case1_qos0sub.c rename to apps/emqx_gateway/test/intergration_test/client/case1_qos0sub.c diff --git a/apps/emqx_sn/intergration_test/client/case2_qos0pub.c b/apps/emqx_gateway/test/intergration_test/client/case2_qos0pub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case2_qos0pub.c rename to apps/emqx_gateway/test/intergration_test/client/case2_qos0pub.c diff --git a/apps/emqx_sn/intergration_test/client/case2_qos0sub.c b/apps/emqx_gateway/test/intergration_test/client/case2_qos0sub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case2_qos0sub.c rename to apps/emqx_gateway/test/intergration_test/client/case2_qos0sub.c diff --git a/apps/emqx_sn/intergration_test/client/case3_qos0pub.c b/apps/emqx_gateway/test/intergration_test/client/case3_qos0pub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case3_qos0pub.c rename to apps/emqx_gateway/test/intergration_test/client/case3_qos0pub.c diff --git a/apps/emqx_sn/intergration_test/client/case3_qos0sub.c b/apps/emqx_gateway/test/intergration_test/client/case3_qos0sub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case3_qos0sub.c rename to apps/emqx_gateway/test/intergration_test/client/case3_qos0sub.c diff --git a/apps/emqx_sn/intergration_test/client/case4_qos3pub.c b/apps/emqx_gateway/test/intergration_test/client/case4_qos3pub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case4_qos3pub.c rename to apps/emqx_gateway/test/intergration_test/client/case4_qos3pub.c diff --git a/apps/emqx_sn/intergration_test/client/case4_qos3sub.c b/apps/emqx_gateway/test/intergration_test/client/case4_qos3sub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case4_qos3sub.c rename to apps/emqx_gateway/test/intergration_test/client/case4_qos3sub.c diff --git a/apps/emqx_sn/intergration_test/client/case5_qos3pub.c b/apps/emqx_gateway/test/intergration_test/client/case5_qos3pub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case5_qos3pub.c rename to apps/emqx_gateway/test/intergration_test/client/case5_qos3pub.c diff --git a/apps/emqx_sn/intergration_test/client/case5_qos3sub.c b/apps/emqx_gateway/test/intergration_test/client/case5_qos3sub.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case5_qos3sub.c rename to apps/emqx_gateway/test/intergration_test/client/case5_qos3sub.c diff --git a/apps/emqx_sn/intergration_test/client/case6_sleep.c b/apps/emqx_gateway/test/intergration_test/client/case6_sleep.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case6_sleep.c rename to apps/emqx_gateway/test/intergration_test/client/case6_sleep.c diff --git a/apps/emqx_sn/intergration_test/client/case7_double_connect.c b/apps/emqx_gateway/test/intergration_test/client/case7_double_connect.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/case7_double_connect.c rename to apps/emqx_gateway/test/intergration_test/client/case7_double_connect.c diff --git a/apps/emqx_sn/intergration_test/client/int_test_result.c b/apps/emqx_gateway/test/intergration_test/client/int_test_result.c similarity index 100% rename from apps/emqx_sn/intergration_test/client/int_test_result.c rename to apps/emqx_gateway/test/intergration_test/client/int_test_result.c diff --git a/apps/emqx_sn/intergration_test/client/int_test_result.h b/apps/emqx_gateway/test/intergration_test/client/int_test_result.h similarity index 100% rename from apps/emqx_sn/intergration_test/client/int_test_result.h rename to apps/emqx_gateway/test/intergration_test/client/int_test_result.h diff --git a/apps/emqx_sn/intergration_test/disable_qos3.py b/apps/emqx_gateway/test/intergration_test/disable_qos3.py similarity index 100% rename from apps/emqx_sn/intergration_test/disable_qos3.py rename to apps/emqx_gateway/test/intergration_test/disable_qos3.py diff --git a/apps/emqx_sn/intergration_test/enable_qos3.py b/apps/emqx_gateway/test/intergration_test/enable_qos3.py similarity index 100% rename from apps/emqx_sn/intergration_test/enable_qos3.py rename to apps/emqx_gateway/test/intergration_test/enable_qos3.py diff --git a/apps/emqx_sn/test/props/emqx_sn_proper_types.erl b/apps/emqx_gateway/test/props/emqx_sn_proper_types.erl similarity index 100% rename from apps/emqx_sn/test/props/emqx_sn_proper_types.erl rename to apps/emqx_gateway/test/props/emqx_sn_proper_types.erl diff --git a/apps/emqx_sn/test/props/prop_emqx_sn_frame.erl b/apps/emqx_gateway/test/props/prop_emqx_sn_frame.erl similarity index 100% rename from apps/emqx_sn/test/props/prop_emqx_sn_frame.erl rename to apps/emqx_gateway/test/props/prop_emqx_sn_frame.erl diff --git a/apps/emqx_sn/.gitignore b/apps/emqx_sn/.gitignore deleted file mode 100644 index 46861cdec..000000000 --- a/apps/emqx_sn/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -.eunit -deps -*.o -*.beam -*.plt -erl_crash.dump -ebin -rel/example_project -.concrete/DEV_MODE -.rebar -_rel/ -emqx_sn.d -logs/ -.erlang.mk/ -data/ -.idea/ -*.iml -*.d -_build/ -.rebar3 -rebar3.crashdump -.DS_Store -bbmustache/ -etc/gen.emqx.conf -cuttlefish -rebar.lock -xrefr -intergration_test/emqx-rel/ -intergration_test/paho.mqtt-sn.embedded-c/ -intergration_test/client/*.exe -intergration_test/client/*.txt -.DS_Store -cover/ -ct.coverdata -eunit.coverdata -test/ct.cover.spec -erlang.mk -etc/emqx_sn.conf.rendered -.rebar3/ -*.swp diff --git a/apps/emqx_sn/examples/simple_example.erl b/apps/emqx_sn/examples/simple_example.erl deleted file mode 100644 index ce19c4133..000000000 --- a/apps/emqx_sn/examples/simple_example.erl +++ /dev/null @@ -1,126 +0,0 @@ --module(simple_example). - --include("emqx_sn.hrl"). - --define(HOST, {127,0,0,1}). --define(PORT, 1884). - --export([start/0]). - -start() -> - io:format("start to connect ~p:~p~n", [?HOST, ?PORT]), - - %% create udp socket - {ok, Socket} = gen_udp:open(0, [binary]), - - %% connect to emqx_sn broker - Packet = gen_connect_packet(<<"client1">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet), - io:format("send connect packet=~p~n", [Packet]), - %% receive message - wait_response(), - - %% register topic_id - RegisterPacket = gen_register_packet(<<"TopicA">>, 0), - ok = gen_udp:send(Socket, ?HOST, ?PORT, RegisterPacket), - io:format("send register packet=~p~n", [RegisterPacket]), - TopicId = wait_response(), - - %% subscribe - SubscribePacket = gen_subscribe_packet(TopicId), - ok = gen_udp:send(Socket, ?HOST, ?PORT, SubscribePacket), - io:format("send subscribe packet=~p~n", [SubscribePacket]), - wait_response(), - - %% publish - PublishPacket = gen_publish_packet(TopicId, <<"Payload...">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, PublishPacket), - io:format("send publish packet=~p~n", [PublishPacket]), - wait_response(), - - % wait for subscribed message from broker - wait_response(), - - %% disconnect from emqx_sn broker - DisConnectPacket = gen_disconnect_packet(), - ok = gen_udp:send(Socket, ?HOST, ?PORT, DisConnectPacket), - io:format("send disconnect packet=~p~n", [DisConnectPacket]). - - - -gen_connect_packet(ClientId) -> - Length = 6+byte_size(ClientId), - MsgType = ?SN_CONNECT, - Dup = 0, - QoS = 0, - Retain = 0, - Will = 0, - CleanSession = 1, - TopicIdType = 0, - Flag = <>, - ProtocolId = 1, - Duration = 10, - <>. - -gen_subscribe_packet(TopicId) -> - Length = 7, - MsgType = ?SN_SUBSCRIBE, - Dup = 0, - Retain = 0, - Will = 0, - QoS = 1, - CleanSession = 0, - TopicIdType = 1, - Flag = <>, - MsgId = 1, - <>. - -gen_register_packet(Topic, TopicId) -> - Length = 6+byte_size(Topic), - MsgType = ?SN_REGISTER, - MsgId = 1, - <>. - -gen_publish_packet(TopicId, Payload) -> - Length = 7+byte_size(Payload), - MsgType = ?SN_PUBLISH, - Dup = 0, - QoS = 1, - Retain = 0, - Will = 0, - CleanSession = 0, - MsgId = 1, - TopicIdType = 1, - Flag = <>, - <>. - -gen_disconnect_packet()-> - Length = 2, - MsgType = ?SN_DISCONNECT, - <>. - -wait_response() -> - receive - {udp, _Socket, _, _, Bin} -> - case Bin of - <<_Len:8, ?SN_PUBLISH, _Flag:8, TopicId:16, MsgId:16, Data/binary>> -> - io:format("recv publish TopicId: ~p, MsgId: ~p, Data: ~p~n", [TopicId, MsgId, Data]); - <<_Len:8, ?SN_CONNACK, 0:8>> -> - io:format("recv connect ack~n"); - <<_Len:8, ?SN_REGACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv regack TopicId=~p, MsgId=~p~n", [TopicId, MsgId]), - TopicId; - <<_Len:8, ?SN_SUBACK, Flags:8, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv suback Flags=~p TopicId=~p, MsgId=~p~n", [Flags, TopicId, MsgId]); - <<_Len:8, ?SN_PUBACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv puback TopicId=~p, MsgId=~p~n", [TopicId, MsgId]); - _ -> - io:format("ignore bin=~p~n", [Bin]) - end; - Any -> - io:format("recv something else from udp socket ~p~n", [Any]) - after - 2000 -> - io:format("Error: receive timeout!~n"), - wait_response() - end. diff --git a/apps/emqx_sn/examples/simple_example2.erl b/apps/emqx_sn/examples/simple_example2.erl deleted file mode 100644 index b9ada6d22..000000000 --- a/apps/emqx_sn/examples/simple_example2.erl +++ /dev/null @@ -1,120 +0,0 @@ --module(simple_example2). - --include("emqx_sn.hrl"). - --define(HOST, "localhost"). --define(PORT, 1884). - --export([start/0]). - -start() -> - io:format("start to connect ~p:~p~n", [?HOST, ?PORT]), - - %% create udp socket - {ok, Socket} = gen_udp:open(0, [binary]), - - %% connect to emqx_sn broker - Packet = gen_connect_packet(<<"client1">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet), - io:format("send connect packet=~p~n", [Packet]), - %% receive message - wait_response(), - - %% subscribe, SHORT TOPIC NAME - SubscribePacket = gen_subscribe_packet(<<"T1">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, SubscribePacket), - io:format("send subscribe packet=~p~n", [SubscribePacket]), - wait_response(), - - %% publish, SHORT TOPIC NAME - PublishPacket = gen_publish_packet(<<"T1">>, <<"Payload...">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, PublishPacket), - io:format("send publish packet=~p~n", [PublishPacket]), - wait_response(), - - % wait for subscribed message from broker - wait_response(), - - %% disconnect from emqx_sn broker - DisConnectPacket = gen_disconnect_packet(), - ok = gen_udp:send(Socket, ?HOST, ?PORT, DisConnectPacket), - io:format("send disconnect packet=~p~n", [DisConnectPacket]). - - - -gen_connect_packet(ClientId) -> - Length = 6+byte_size(ClientId), - MsgType = ?SN_CONNECT, - Dup = 0, - QoS = 0, - Retain = 0, - Will = 0, - CleanSession = 1, - TopicIdType = 0, - Flag = <>, - ProtocolId = 1, - Duration = 10, - <>. - -gen_subscribe_packet(ShortTopic) -> - Length = 7, - MsgType = ?SN_SUBSCRIBE, - Dup = 0, - Retain = 0, - Will = 0, - QoS = 1, - CleanSession = 0, - TopicIdType = 2, % SHORT TOPIC NAME - Flag = <>, - MsgId = 1, - <>. - -gen_register_packet(Topic, TopicId) -> - Length = 6+byte_size(Topic), - MsgType = ?SN_REGISTER, - MsgId = 1, - <>. - -gen_publish_packet(ShortTopic, Payload) -> - Length = 7+byte_size(Payload), - MsgType = ?SN_PUBLISH, - Dup = 0, - QoS = 1, - Retain = 0, - Will = 0, - CleanSession = 0, - MsgId = 1, - TopicIdType = 2, % SHORT TOPIC NAME - Flag = <>, - <>. - -gen_disconnect_packet()-> - Length = 2, - MsgType = ?SN_DISCONNECT, - <>. - -wait_response() -> - receive - {udp, _Socket, _, _, Bin} -> - case Bin of - <<_Len:8, ?SN_PUBLISH, _Flag:8, TopicId:16, MsgId:16, Data/binary>> -> - io:format("recv publish TopicId: ~p, MsgId: ~p, Data: ~p~n", [TopicId, MsgId, Data]); - <<_Len:8, ?SN_CONNACK, 0:8>> -> - io:format("recv connect ack~n"); - <<_Len:8, ?SN_REGACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv regack TopicId=~p, MsgId=~p~n", [TopicId, MsgId]), - TopicId; - <<_Len:8, ?SN_SUBACK, Flags:8, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv suback Flags=~p TopicId=~p, MsgId=~p~n", [Flags, TopicId, MsgId]); - <<_Len:8, ?SN_PUBACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv puback TopicId=~p, MsgId=~p~n", [TopicId, MsgId]); - _ -> - io:format("ignore bin=~p~n", [Bin]) - end; - Any -> - io:format("recv something else from udp socket ~p~n", [Any]) - after - 2000 -> - io:format("Error: receive timeout!~n"), - wait_response() - end. diff --git a/apps/emqx_sn/examples/simple_example3.erl b/apps/emqx_sn/examples/simple_example3.erl deleted file mode 100644 index 40f0bf572..000000000 --- a/apps/emqx_sn/examples/simple_example3.erl +++ /dev/null @@ -1,120 +0,0 @@ --module(simple_example3). - --include("emqx_sn.hrl"). - --define(HOST, "localhost"). --define(PORT, 1884). - --export([start/0]). - -start() -> - io:format("start to connect ~p:~p~n", [?HOST, ?PORT]), - - %% create udp socket - {ok, Socket} = gen_udp:open(0, [binary]), - - %% connect to emqx_sn broker - Packet = gen_connect_packet(<<"client1">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet), - io:format("send connect packet=~p~n", [Packet]), - %% receive message - wait_response(), - - %% subscribe normal topic name - SubscribePacket = gen_subscribe_packet(<<"T3">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, SubscribePacket), - io:format("send subscribe packet=~p~n", [SubscribePacket]), - wait_response(), - - %% publish SHORT TOPIC NAME - PublishPacket = gen_publish_packet(<<"T3">>, <<"Payload...">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, PublishPacket), - io:format("send publish packet=~p~n", [PublishPacket]), - wait_response(), - - % wait for subscribed message from broker - wait_response(), - - %% disconnect from emqx_sn broker - DisConnectPacket = gen_disconnect_packet(), - ok = gen_udp:send(Socket, ?HOST, ?PORT, DisConnectPacket), - io:format("send disconnect packet=~p~n", [DisConnectPacket]). - - - -gen_connect_packet(ClientId) -> - Length = 6+byte_size(ClientId), - MsgType = ?SN_CONNECT, - Dup = 0, - QoS = 0, - Retain = 0, - Will = 0, - CleanSession = 1, - TopicIdType = 0, - Flag = <>, - ProtocolId = 1, - Duration = 10, - <>. - -gen_subscribe_packet(ShortTopic) -> - Length = 7, - MsgType = ?SN_SUBSCRIBE, - Dup = 0, - Retain = 0, - Will = 0, - QoS = 1, - CleanSession = 0, - TopicIdType = 0, % normal topic name - Flag = <>, - MsgId = 1, - <>. - -gen_register_packet(Topic, TopicId) -> - Length = 6+byte_size(Topic), - MsgType = ?SN_REGISTER, - MsgId = 1, - <>. - -gen_publish_packet(ShortTopic, Payload) -> - Length = 7+byte_size(Payload), - MsgType = ?SN_PUBLISH, - Dup = 0, - QoS = 1, - Retain = 0, - Will = 0, - CleanSession = 0, - MsgId = 1, - TopicIdType = 2, % SHORT TOPIC NAME - Flag = <>, - <>. - -gen_disconnect_packet()-> - Length = 2, - MsgType = ?SN_DISCONNECT, - <>. - -wait_response() -> - receive - {udp, _Socket, _, _, Bin} -> - case Bin of - <<_Len:8, ?SN_PUBLISH, _Flag:8, TopicId:16, MsgId:16, Data/binary>> -> - io:format("recv publish TopicId: ~p, MsgId: ~p, Data: ~p~n", [TopicId, MsgId, Data]); - <<_Len:8, ?SN_CONNACK, 0:8>> -> - io:format("recv connect ack~n"); - <<_Len:8, ?SN_REGACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv regack TopicId=~p, MsgId=~p~n", [TopicId, MsgId]), - TopicId; - <<_Len:8, ?SN_SUBACK, Flags:8, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv suback Flags=~p TopicId=~p, MsgId=~p~n", [Flags, TopicId, MsgId]); - <<_Len:8, ?SN_PUBACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv puback TopicId=~p, MsgId=~p~n", [TopicId, MsgId]); - _ -> - io:format("ignore bin=~p~n", [Bin]) - end; - Any -> - io:format("recv something else from udp socket ~p~n", [Any]) - after - 2000 -> - io:format("Error: receive timeout!~n"), - wait_response() - end. diff --git a/apps/emqx_sn/examples/simple_example4.erl b/apps/emqx_sn/examples/simple_example4.erl deleted file mode 100644 index 6beb5835c..000000000 --- a/apps/emqx_sn/examples/simple_example4.erl +++ /dev/null @@ -1,151 +0,0 @@ --module(simple_example4). - --include("emqx_sn.hrl"). - --define(HOST, {127,0,0,1}). --define(PORT, 1884). - --export([start/0]). - -start(LoopTimes) -> - io:format("start to connect ~p:~p~n", [?HOST, ?PORT]), - - %% create udp socket - {ok, Socket} = gen_udp:open(0, [binary]), - - %% connect to emqx_sn broker - Packet = gen_connect_packet(<<"client1">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet), - io:format("send connect packet=~p~n", [Packet]), - %% receive message - wait_response(), - - %% register topic_id - RegisterPacket = gen_register_packet(<<"TopicA">>, 0), - ok = gen_udp:send(Socket, ?HOST, ?PORT, RegisterPacket), - io:format("send register packet=~p~n", [RegisterPacket]), - TopicId = wait_response(), - - %% subscribe - SubscribePacket = gen_subscribe_packet(TopicId), - ok = gen_udp:send(Socket, ?HOST, ?PORT, SubscribePacket), - io:format("send subscribe packet=~p~n", [SubscribePacket]), - wait_response(), - - %% loop publish - [begin - timer:sleep(1000), - io:format("~n-------------------- publish ~p start --------------------~n", [N]), - - PublishPacket = gen_publish_packet(TopicId, <<"Payload...">>), - ok = gen_udp:send(Socket, ?HOST, ?PORT, PublishPacket), - io:format("send publish packet=~p~n", [PublishPacket]), - % wait for publish ack - wait_response(), - % wait for subscribed message from broker - wait_response(), - - PingReqPacket = gen_pingreq_packet(), - ok = gen_udp:send(Socket, ?HOST, ?PORT, PingReqPacket), - % wait for pingresp - wait_response(), - - io:format("--------------------- publish ~p end ---------------------~n", [N]) - end || N <- lists:seq(1, LoopTimes)], - - %% disconnect from emqx_sn broker - DisConnectPacket = gen_disconnect_packet(), - ok = gen_udp:send(Socket, ?HOST, ?PORT, DisConnectPacket), - io:format("send disconnect packet=~p~n", [DisConnectPacket]). - - - -gen_connect_packet(ClientId) -> - Length = 6+byte_size(ClientId), - MsgType = ?SN_CONNECT, - Dup = 0, - QoS = 0, - Retain = 0, - Will = 0, - CleanSession = 1, - TopicIdType = 0, - Flag = <>, - ProtocolId = 1, - Duration = 10, - <>. - -gen_subscribe_packet(TopicId) -> - Length = 7, - MsgType = ?SN_SUBSCRIBE, - Dup = 0, - Retain = 0, - Will = 0, - QoS = 1, - CleanSession = 0, - TopicIdType = 1, - Flag = <>, - MsgId = 1, - <>. - -gen_register_packet(Topic, TopicId) -> - Length = 6+byte_size(Topic), - MsgType = ?SN_REGISTER, - MsgId = 1, - <>. - -gen_publish_packet(TopicId, Payload) -> - Length = 7+byte_size(Payload), - MsgType = ?SN_PUBLISH, - Dup = 0, - QoS = 1, - Retain = 0, - Will = 0, - CleanSession = 0, - MsgId = 1, - TopicIdType = 1, - Flag = <>, - <>. - -gen_puback_packet(TopicId, MsgId) -> - Length = 7, - MsgType = ?SN_PUBACK, - <>. - -gen_pingreq_packet() -> - Length = 2, - MsgType = ?SN_PINGREQ, - <>. - -gen_disconnect_packet()-> - Length = 2, - MsgType = ?SN_DISCONNECT, - <>. - -wait_response() -> - receive - {udp, Socket, _, _, Bin} -> - case Bin of - <<_Len:8, ?SN_PUBLISH, _Flag:8, TopicId:16, MsgId:16, Data/binary>> -> - io:format("recv publish TopicId: ~p, MsgId: ~p, Data: ~p~n", [TopicId, MsgId, Data]), - ok = gen_udp:send(Socket, ?HOST, ?PORT, gen_puback_packet(TopicId, MsgId)); - <<_Len:8, ?SN_CONNACK, 0:8>> -> - io:format("recv connect ack~n"); - <<_Len:8, ?SN_REGACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv regack TopicId=~p, MsgId=~p~n", [TopicId, MsgId]), - TopicId; - <<_Len:8, ?SN_SUBACK, Flags:8, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv suback Flags=~p TopicId=~p, MsgId=~p~n", [Flags, TopicId, MsgId]); - <<_Len:8, ?SN_PUBACK, TopicId:16, MsgId:16, 0:8>> -> - io:format("recv puback TopicId=~p, MsgId=~p~n", [TopicId, MsgId]); - <<_Len:8, ?SN_PINGRESP>> -> - io:format("recv pingresp~n"); - _ -> - io:format("ignore bin=~p~n", [Bin]) - end; - Any -> - io:format("recv something else from udp socket ~p~n", [Any]) - after - 2000 -> - io:format("Error: receive timeout!~n"), - wait_response() - end. diff --git a/apps/emqx_sn/rebar.config b/apps/emqx_sn/rebar.config deleted file mode 100644 index cbdac78f6..000000000 --- a/apps/emqx_sn/rebar.config +++ /dev/null @@ -1,26 +0,0 @@ -{deps, []}. -{plugins, [rebar3_proper]}. - -{deps, - [{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.7.4"}}} - ]}. - -{edoc_opts, [{preprocess, true}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{dialyzer, [{warnings, [unmatched_returns, error_handling, race_conditions]} - ]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. - -{plugins, [coveralls]}. diff --git a/apps/emqx_sn/src/emqx_sn.app.src b/apps/emqx_sn/src/emqx_sn.app.src deleted file mode 100644 index 0e4e53dc8..000000000 --- a/apps/emqx_sn/src/emqx_sn.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emqx_sn, - [{description, "EMQ X MQTT-SN Plugin"}, - {vsn, "4.4.0"}, % strict semver, bump manually! - {modules, []}, - {registered, []}, - {applications, [kernel,stdlib,esockd]}, - {mod, {emqx_sn_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQ X Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-sn"} - ]} - ]}. diff --git a/apps/emqx_sn/src/emqx_sn.appup.src b/apps/emqx_sn/src/emqx_sn.appup.src deleted file mode 100644 index 2bd6f5646..000000000 --- a/apps/emqx_sn/src/emqx_sn.appup.src +++ /dev/null @@ -1,19 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.2", [ - {load_module, emqx_sn_gateway, brutal_purge, soft_purge, []} - ]}, - {<<"4.3.[0-1]">>, [ - {restart_application, emqx_sn} - ]} - ], - [ - {"4.3.2", [ - {load_module, emqx_sn_gateway, brutal_purge, soft_purge, []} - ]}, - {<<"4.3.[0-1]">>, [ - {restart_application, emqx_sn} - ]} - ] -}. diff --git a/apps/emqx_sn/vars b/apps/emqx_sn/vars deleted file mode 100644 index e39aa2801..000000000 --- a/apps/emqx_sn/vars +++ /dev/null @@ -1,8 +0,0 @@ -%% vars here are for test only, not intended for release - -{platform_bin_dir, "bin"}. -{platform_data_dir, "data"}. -{platform_etc_dir, "etc"}. -{platform_lib_dir, "lib"}. -{platform_log_dir, "log"}. -{platform_plugins_dir, "data/plugins"}. From fc5baf8fd40c39782ee12b96e8efd0a670aea6e8 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 7 Jul 2021 18:29:47 +0800 Subject: [PATCH 159/379] refactor(gw-sn): support mutil-registry process --- .../src/mqttsn/emqx_sn_gateway.erl | 68 ++++--- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 6 +- .../src/mqttsn/emqx_sn_registry.erl | 167 +++++++++++------- 3 files changed, 142 insertions(+), 99 deletions(-) diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl index 27eb16498..28d461b9b 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_gateway.erl @@ -76,6 +76,7 @@ }). -record(state, {gwid :: integer(), + registry :: emqx_sn_registry:registry(), socket :: port(), sockpid :: pid(), sockstate :: emqx_types:sockstate(), @@ -145,16 +146,18 @@ kick(GwPid) -> %%-------------------------------------------------------------------- init([{_, SockPid, Sock}, Peername, Options]) -> - GwId = proplists:get_value(gateway_id, Options), - Username = proplists:get_value(username, Options, undefined), - Password = proplists:get_value(password, Options, undefined), - EnableQos3 = proplists:get_value(enable_qos3, Options, false), - IdleTimeout = proplists:get_value(idle_timeout, Options, 30000), - EnableStats = proplists:get_value(enable_stats, Options, false), + GwId = maps:get(gateway_id, Options), + Registry = maps:get(registry, Options), + Username = maps:get(username, Options, undefined), + Password = maps:get(password, Options, undefined), + EnableQos3 = maps:get(enable_qos3, Options, false), + IdleTimeout = maps:get(idle_timeout, Options, 30000), + EnableStats = maps:get(enable_stats, Options, false), case inet:sockname(Sock) of {ok, Sockname} -> Channel = emqx_channel:init(?CONN_INFO(Sockname, Peername), ?DEFAULT_CHAN_OPTIONS), State = #state{gwid = GwId, + registry = Registry, username = Username, password = Password, socket = Sock, @@ -202,10 +205,16 @@ idle(cast, {incoming, ?SN_PUBLISH_MSG(_Flag, _TopicId, _MsgId, _Data)}, State = idle(cast, {incoming, ?SN_PUBLISH_MSG(#mqtt_sn_flags{qos = ?QOS_NEG1, topic_id_type = TopicIdType }, TopicId, _MsgId, Data)}, - State = #state{clientid = ClientId}) -> + State = #state{registry = Registry, clientid = ClientId}) -> TopicName = case (TopicIdType =:= ?SN_SHORT_TOPIC) of - false -> emqx_sn_registry:lookup_topic(ClientId, TopicId); - true -> <> + false -> + emqx_sn_registry:lookup_topic( + Registry, + ClientId, + TopicId + ); + true -> + <> end, _ = case TopicName =/= undefined of true -> @@ -290,9 +299,9 @@ wait_for_will_msg(EventType, EventContent, State) -> handle_event(EventType, EventContent, wait_for_will_msg, State). connected(cast, {incoming, ?SN_REGISTER_MSG(_TopicId, MsgId, TopicName)}, - State = #state{clientid = ClientId}) -> + State = #state{registry = Registry, clientid = ClientId}) -> State0 = - case emqx_sn_registry:register_topic(ClientId, TopicName) of + case emqx_sn_registry:register_topic(Registry, ClientId, TopicName) of TopicId when is_integer(TopicId) -> ?LOG(debug, "register ClientId=~p, TopicName=~p, TopicId=~p", [ClientId, TopicName, TopicId]), send_message(?SN_REGACK_MSG(TopicId, MsgId, ?SN_RC_ACCEPTED), State); @@ -579,13 +588,13 @@ handle_event(EventType, EventContent, StateName, State) -> [StateName, {EventType, EventContent}]), {keep_state, State}. -terminate(Reason, _StateName, #state{channel = Channel}) -> +terminate(Reason, _StateName, #state{registry = Registry, channel = Channel}) -> ClientId = emqx_channel:info(clientid, Channel), case Reason of {shutdown, takeovered} -> ok; _ -> - emqx_sn_registry:unregister_topic(ClientId) + emqx_sn_registry:unregister_topic(Registry, ClientId) end, emqx_channel:terminate(Reason, Channel), ok. @@ -721,12 +730,13 @@ mqtt2sn(?PUBCOMP_PACKET(MsgId), _State) -> mqtt2sn(?UNSUBACK_PACKET(MsgId), _State)-> ?SN_UNSUBACK_MSG(MsgId); -mqtt2sn(?PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #state{channel = Channel}) -> +mqtt2sn(?PUBLISH_PACKET(QoS, Topic, PacketId, Payload), + #state{registry = Registry, channel = Channel}) -> NewPacketId = if QoS =:= ?QOS_0 -> 0; true -> PacketId end, ClientId = emqx_channel:info(clientid, Channel), - {TopicIdType, TopicContent} = case emqx_sn_registry:lookup_topic_id(ClientId, Topic) of + {TopicIdType, TopicContent} = case emqx_sn_registry:lookup_topic_id(Registry, ClientId, Topic) of {predef, PredefTopicId} -> {?SN_PREDEFINED_TOPIC, PredefTopicId}; TopicId when is_integer(TopicId) -> @@ -848,9 +858,9 @@ do_connect(ClientId, CleanStart, WillFlag, Duration, State) -> end. handle_subscribe(?SN_NORMAL_TOPIC, TopicName, QoS, MsgId, - State=#state{channel = Channel}) -> + State=#state{registry = Registry, channel = Channel}) -> ClientId = emqx_channel:info(clientid, Channel), - case emqx_sn_registry:register_topic(ClientId, TopicName) of + case emqx_sn_registry:register_topic(Registry, ClientId, TopicName) of {error, too_large} -> State0 = send_message(?SN_SUBACK_MSG(#mqtt_sn_flags{qos = QoS}, ?SN_INVALID_TOPIC_ID, @@ -864,9 +874,9 @@ handle_subscribe(?SN_NORMAL_TOPIC, TopicName, QoS, MsgId, end; handle_subscribe(?SN_PREDEFINED_TOPIC, TopicId, QoS, MsgId, - State = #state{channel = Channel}) -> + State = #state{registry = Registry, channel = Channel}) -> ClientId = emqx_channel:info(clientid, Channel), - case emqx_sn_registry:lookup_topic(ClientId, TopicId) of + case emqx_sn_registry:lookup_topic(Registry, ClientId, TopicId) of undefined -> State0 = send_message(?SN_SUBACK_MSG(#mqtt_sn_flags{qos = QoS}, TopicId, @@ -895,9 +905,9 @@ handle_unsubscribe(?SN_NORMAL_TOPIC, TopicId, MsgId, State) -> proto_unsubscribe(TopicId, MsgId, State); handle_unsubscribe(?SN_PREDEFINED_TOPIC, TopicId, MsgId, - State = #state{channel = Channel}) -> + State = #state{registry = Registry, channel = Channel}) -> ClientId = emqx_channel:info(clientid, Channel), - case emqx_sn_registry:lookup_topic(ClientId, TopicId) of + case emqx_sn_registry:lookup_topic(Registry, ClientId, TopicId) of undefined -> {keep_state, send_message(?SN_UNSUBACK_MSG(MsgId), State)}; PredefinedTopic -> @@ -920,11 +930,11 @@ do_publish(?SN_NORMAL_TOPIC, TopicName, Data, Flags, MsgId, State) -> <> = TopicName, do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, State); do_publish(?SN_PREDEFINED_TOPIC, TopicId, Data, Flags, MsgId, - State=#state{channel = Channel}) -> + State=#state{registry = Registry, channel = Channel}) -> #mqtt_sn_flags{qos = QoS, dup = Dup, retain = Retain} = Flags, NewQoS = get_corrected_qos(QoS), ClientId = emqx_channel:info(clientid, Channel), - case emqx_sn_registry:lookup_topic(ClientId, TopicId) of + case emqx_sn_registry:lookup_topic(Registry, ClientId, TopicId) of undefined -> {keep_state, maybe_send_puback(NewQoS, TopicId, MsgId, ?SN_RC_INVALID_TOPIC_ID, State)}; @@ -963,13 +973,13 @@ do_publish_will(#state{will_msg = WillMsg, clientid = ClientId}) -> ok. do_puback(TopicId, MsgId, ReturnCode, StateName, - State=#state{channel = Channel}) -> + State=#state{registry = Registry, channel = Channel}) -> case ReturnCode of ?SN_RC_ACCEPTED -> handle_incoming(?PUBACK_PACKET(MsgId), StateName, State); ?SN_RC_INVALID_TOPIC_ID -> ClientId = emqx_channel:info(clientid, Channel), - case emqx_sn_registry:lookup_topic(ClientId, TopicId) of + case emqx_sn_registry:lookup_topic(Registry, ClientId, TopicId) of undefined -> {keep_state, State}; TopicName -> %%notice that this TopicName maybe normal or predefined, @@ -1061,10 +1071,10 @@ handle_outgoing(Packets, State) when is_list(Packets) -> end, State, Packets); handle_outgoing(PubPkt = ?PUBLISH_PACKET(_, TopicName, _, _), - State = #state{channel = Channel}) -> + State = #state{registry = Registry, channel = Channel}) -> ?LOG(debug, "Handle outgoing publish: ~0p", [PubPkt]), ClientId = emqx_channel:info(clientid, Channel), - TopicId = emqx_sn_registry:lookup_topic_id(ClientId, TopicName), + TopicId = emqx_sn_registry:lookup_topic_id(Registry, ClientId, TopicName), case (TopicId == undefined) andalso (byte_size(TopicName) =/= 2) of true -> register_and_notify_client(PubPkt, State); false -> send_message(mqtt2sn(PubPkt, State), State) @@ -1089,11 +1099,11 @@ replay_no_reg_pending_publishes(TopicId, #state{pending_topic_ids = Pendings} = State#state{pending_topic_ids = maps:remove(TopicId, Pendings)}. register_and_notify_client(?PUBLISH_PACKET(QoS, TopicName, PacketId, Payload) = PubPkt, - State = #state{pending_topic_ids = Pendings, channel = Channel}) -> + State = #state{registry = Registry, pending_topic_ids = Pendings, channel = Channel}) -> MsgId = message_id(PacketId), #mqtt_packet{header = #mqtt_packet_header{dup = Dup, retain = Retain}} = PubPkt, ClientId = emqx_channel:info(clientid, Channel), - TopicId = emqx_sn_registry:register_topic(ClientId, TopicName), + TopicId = emqx_sn_registry:register_topic(Registry, ClientId, TopicName), ?LOG(debug, "Register TopicId=~p, TopicName=~p, Payload=~p, Dup=~p, QoS=~p, " "Retain=~p, MsgId=~p", [TopicId, TopicName, Payload, Dup, QoS, Retain, MsgId]), NewPendings = cache_no_reg_publish_message(Pendings, TopicId, PubPkt, State), diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index c3b679381..3085afe89 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -72,11 +72,11 @@ on_insta_create(_Insta = #{ id := InstaId, end, PredefTopics = maps:get(predefined, RawConf), - {ok, RegistrySvr} = emqx_sn_registry:start_link(PredefTopics), + {ok, RegistrySvr} = emqx_sn_registry:start_link(InstaId, PredefTopics), NRawConf = maps:without( - [gateway_id, broadcast, predefined], - RawConf#{registry => RegistrySvr} + [broadcast, predefined], + RawConf#{registry => emqx_sn_registry:lookup_name(RegistrySvr)} ), Listeners = emqx_gateway_utils:normalize_rawconf(NRawConf), diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl index f2f87d93b..30583c443 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl @@ -14,6 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% @doc The MQTT-SN Topic Registry +%% +%% XXX: -module(emqx_sn_registry). -behaviour(gen_server). @@ -23,16 +26,15 @@ -define(LOG(Level, Format, Args), emqx_logger:Level("MQTT-SN(registry): " ++ Format, Args)). --export([ start_link/1 - , stop/0 +-export([ start_link/2 ]). --export([ register_topic/2 - , unregister_topic/1 +-export([ register_topic/3 + , unregister_topic/2 ]). --export([ lookup_topic/2 - , lookup_topic_id/2 +-export([ lookup_topic/3 + , lookup_topic_id/3 ]). %% gen_server callbacks @@ -44,51 +46,54 @@ , code_change/3 ]). +-export([lookup_name/1]). + -define(SN_SHARD, emqx_sn_shard). --define(TAB, ?MODULE). - --record(state, {max_predef_topic_id = 0}). +-record(state, {tabname, max_predef_topic_id = 0}). -record(emqx_sn_registry, {key, value}). %% Mnesia bootstrap --export([mnesia/1]). +%-export([mnesia/1]). --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). +%-boot_mnesia({mnesia, [boot]}). +%-copy_mnesia({mnesia, [copy]}). --rlog_shard({?SN_SHARD, ?TAB}). +%-rlog_shard({?SN_SHARD, ?TAB}). -%% @doc Create or replicate tables. --spec(mnesia(boot | copy) -> ok). -mnesia(boot) -> - %% Optimize storage - StoreProps = [{ets, [{read_concurrency, true}]}], - ok = ekka_mnesia:create_table(?MODULE, [ - {attributes, record_info(fields, emqx_sn_registry)}, - {ram_copies, [node()]}, - {storage_properties, StoreProps}]); +%%% @doc Create or replicate tables. +%-spec(mnesia(boot | copy) -> ok). +%mnesia(boot) -> +% %% Optimize storage +% StoreProps = [{ets, [{read_concurrency, true}]}], +% ok = ekka_mnesia:create_table(?MODULE, [ +% {attributes, record_info(fields, emqx_sn_registry)}, +% {ram_copies, [node()]}, +% {storage_properties, StoreProps}]); +% +%mnesia(copy) -> +% ok = ekka_mnesia:copy_table(?MODULE, ram_copies). -mnesia(copy) -> - ok = ekka_mnesia:copy_table(?MODULE, ram_copies). +-type registry() :: {Tab :: atom(), + RegistryPid :: pid()}. %%----------------------------------------------------------------------------- --spec(start_link(list()) -> {ok, pid()} | ignore | {error, Reason :: term()}). -start_link(PredefTopics) -> - ekka_rlog:wait_for_shards([?SN_SHARD], infinity), - gen_server:start_link({local, ?MODULE}, ?MODULE, [PredefTopics], []). +-spec start_link(atom(), list()) + -> ignore + | {ok, pid()} + | {error, Reason :: term()}. +start_link(InstaId, PredefTopics) -> + gen_server:start_link(?MODULE, [InstaId, PredefTopics], []). --spec(stop() -> ok). -stop() -> - gen_server:stop(?MODULE, normal, infinity). - --spec(register_topic(binary(), binary()) -> integer() | {error, term()}). -register_topic(ClientId, TopicName) when is_binary(TopicName) -> +-spec register_topic(registry(), emqx_types:clientid(), emqx_types:topic()) + -> integer() + | {error, term()}. +register_topic({_, Pid}, ClientId, TopicName) when is_binary(TopicName) -> case emqx_topic:wildcard(TopicName) of false -> - gen_server:call(?MODULE, {register, ClientId, TopicName}); + gen_server:call(Pid, {register, ClientId, TopicName}); %% TopicId: in case of “accepted” the value that will be used as topic %% id by the gateway when sending PUBLISH messages to the client (not %% relevant in case of subscriptions to a short topic name or to a topic @@ -96,22 +101,24 @@ register_topic(ClientId, TopicName) when is_binary(TopicName) -> true -> {error, wildcard_topic} end. --spec(lookup_topic(binary(), pos_integer()) -> undefined | binary()). -lookup_topic(ClientId, TopicId) when is_integer(TopicId) -> - case lookup_element(?TAB, {predef, TopicId}, 3) of +-spec lookup_topic(registry(), emqx_types:clientid(), pos_integer()) + -> undefined + | binary(). +lookup_topic({Tab, _}, ClientId, TopicId) when is_integer(TopicId) -> + case lookup_element(Tab, {predef, TopicId}, 3) of undefined -> - lookup_element(?TAB, {ClientId, TopicId}, 3); + lookup_element(Tab, {ClientId, TopicId}, 3); Topic -> Topic end. --spec(lookup_topic_id(binary(), binary()) - -> undefined - | pos_integer() - | {predef, integer()}). -lookup_topic_id(ClientId, TopicName) when is_binary(TopicName) -> - case lookup_element(?TAB, {predef, TopicName}, 3) of +-spec lookup_topic_id(registry(), emqx_types:clientid(), emqx_types:topic()) + -> undefined + | pos_integer() + | {predef, integer()}. +lookup_topic_id({Tab, _}, ClientId, TopicName) when is_binary(TopicName) -> + case lookup_element(Tab, {predef, TopicName}, 3) of undefined -> - lookup_element(?TAB, {ClientId, TopicName}, 3); + lookup_element(Tab, {ClientId, TopicName}, 3); TopicId -> {predef, TopicId} end. @@ -120,46 +127,69 @@ lookup_topic_id(ClientId, TopicName) when is_binary(TopicName) -> lookup_element(Tab, Key, Pos) -> try ets:lookup_element(Tab, Key, Pos) catch error:badarg -> undefined end. --spec(unregister_topic(binary()) -> ok). -unregister_topic(ClientId) -> - gen_server:call(?MODULE, {unregister, ClientId}). +-spec unregister_topic(registry(), emqx_types:clientid()) -> ok. +unregister_topic({_, Pid}, ClientId) -> + gen_server:call(Pid, {unregister, ClientId}). + +lookup_name(Pid) -> + gen_server:call(Pid, name). %%----------------------------------------------------------------------------- -init([PredefTopics]) -> +name(InstaId) -> + list_to_atom(lists:concat([emqx_sn_, InstaId, '_registry'])). + +init([InstaId, PredefTopics]) -> %% {predef, TopicId} -> TopicName %% {predef, TopicName} -> TopicId %% {ClientId, TopicId} -> TopicName %% {ClientId, TopicName} -> TopicId + Tab = name(InstaId), + ok = ekka_mnesia:create_table(Tab, [ + {ram_copies, [node()]}, + {record_name, emqx_sn_registry}, + {attributes, record_info(fields, emqx_sn_registry)}, + {storage_properties, [{ets, [{read_concurrency, true}]}]} + ]), + ok = ekka_mnesia:copy_table(Tab, ram_copies), + % FIXME: + %ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), MaxPredefId = lists:foldl( fun(#{id := TopicId, topic := TopicName}, AccId) -> - ekka_mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicId}, - value = TopicName}), - ekka_mnesia:dirty_write(#emqx_sn_registry{key = {predef, TopicName}, - value = TopicId}), + ekka_mnesia:dirty_write(Tab, #emqx_sn_registry{ + key = {predef, TopicId}, + value = TopicName} + ), + ekka_mnesia:dirty_write(Tab, #emqx_sn_registry{ + key = {predef, TopicName}, + value = TopicId} + ), if TopicId > AccId -> TopicId; true -> AccId end end, 0, PredefTopics), - {ok, #state{max_predef_topic_id = MaxPredefId}}. + {ok, #state{tabname = Tab, max_predef_topic_id = MaxPredefId}}. handle_call({register, ClientId, TopicName}, _From, - State = #state{max_predef_topic_id = PredefId}) -> - case lookup_topic_id(ClientId, TopicName) of + State = #state{tabname = Tab, max_predef_topic_id = PredefId}) -> + case lookup_topic_id({Tab, self()}, ClientId, TopicName) of {predef, PredefTopicId} when is_integer(PredefTopicId) -> {reply, PredefTopicId, State}; TopicId when is_integer(TopicId) -> {reply, TopicId, State}; undefined -> - case next_topic_id(?TAB, PredefId, ClientId) of + case next_topic_id(Tab, PredefId, ClientId) of TopicId when TopicId >= 16#FFFF -> {reply, {error, too_large}, State}; TopicId -> Fun = fun() -> - mnesia:write(#emqx_sn_registry{key = {ClientId, next_topic_id}, - value = TopicId + 1}), - mnesia:write(#emqx_sn_registry{key = {ClientId, TopicName}, - value = TopicId}), - mnesia:write(#emqx_sn_registry{key = {ClientId, TopicId}, - value = TopicName}) + mnesia:write(Tab, #emqx_sn_registry{ + key = {ClientId, next_topic_id}, + value = TopicId + 1}, write), + mnesia:write(Tab, #emqx_sn_registry{ + key = {ClientId, TopicName}, + value = TopicId}, write), + mnesia:write(Tab, #emqx_sn_registry{ + key = {ClientId, TopicId}, + value = TopicName}, write) end, case ekka_mnesia:transaction(?SN_SHARD, Fun) of {atomic, ok} -> @@ -170,11 +200,14 @@ handle_call({register, ClientId, TopicName}, _From, end end; -handle_call({unregister, ClientId}, _From, State) -> - Registry = mnesia:dirty_match_object({?TAB, {ClientId, '_'}, '_'}), - lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(?TAB, R) end, Registry), +handle_call({unregister, ClientId}, _From, State = #state{tabname = Tab}) -> + Registry = mnesia:dirty_match_object({Tab, {ClientId, '_'}, '_'}), + lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(Tab, R) end, Registry), {reply, ok, State}; +handle_call(name, _From, State = #state{tabname = Tab}) -> + {reply, {Tab, self()}, State}; + handle_call(Req, _From, State) -> ?LOG(error, "Unexpected request: ~p", [Req]), {reply, ignored, State}. From 980c7d91db30e22d95fd6f9d82dec40b78de3f76 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 12 Jul 2021 15:42:45 +0800 Subject: [PATCH 160/379] chore(gw): fix mqtt-sn test cases --- apps/emqx_gateway/etc/emqx_gateway.conf | 1 + apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 2 +- .../src/mqttsn/emqx_sn_registry.erl | 9 +- .../emqx_gateway/test/emqx_sn_frame_SUITE.erl | 2 +- .../test/emqx_sn_protocol_SUITE.erl | 43 ++++-- .../test/emqx_sn_registry_SUITE.erl | 126 +++++++++--------- .../test/props/emqx_sn_proper_types.erl | 2 +- .../test/props/prop_emqx_sn_frame.erl | 2 +- 8 files changed, 108 insertions(+), 79 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index ba1e8168b..591f2523d 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -5,6 +5,7 @@ ## TODO: emqx_gateway: { + stomp.1: { frame: { max_headers: 10 diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 3085afe89..1e5c5d8cd 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -68,7 +68,7 @@ on_insta_create(_Insta = #{ id := InstaId, true -> %% FIXME: Port = 1884, - _ = emqx_sn_broadcast:start_link(SnGwId, Port) + _ = emqx_sn_broadcast:start_link(SnGwId, Port), ok end, PredefTopics = maps:get(predefined, RawConf), diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl index 30583c443..0fac56ae0 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl @@ -201,8 +201,13 @@ handle_call({register, ClientId, TopicName}, _From, end; handle_call({unregister, ClientId}, _From, State = #state{tabname = Tab}) -> - Registry = mnesia:dirty_match_object({Tab, {ClientId, '_'}, '_'}), - lists:foreach(fun(R) -> ekka_mnesia:dirty_delete_object(Tab, R) end, Registry), + Registry = mnesia:dirty_match_object( + Tab, + {emqx_sn_registry, {ClientId, '_'}, '_'} + ), + lists:foreach(fun(R) -> + ekka_mnesia:dirty_delete_object(Tab, R) + end, Registry), {reply, ok, State}; handle_call(name, _From, State = #state{tabname = Tab}) -> diff --git a/apps/emqx_gateway/test/emqx_sn_frame_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_frame_SUITE.erl index 85042a4be..8b68a6145 100644 --- a/apps/emqx_gateway/test/emqx_sn_frame_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_frame_SUITE.erl @@ -19,7 +19,7 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("emqx_sn/include/emqx_sn.hrl"). +-include_lib("emqx_gateway/src/mqttsn/include/emqx_sn.hrl"). -include_lib("eunit/include/eunit.hrl"). -import(emqx_sn_frame, [ parse/1 diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 0947bdaca..a63e248ec 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -19,7 +19,7 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("emqx_sn/include/emqx_sn.hrl"). +-include_lib("emqx_gateway/src/mqttsn/include/emqx_sn.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -59,23 +59,41 @@ all() -> init_per_suite(Config) -> logger:set_module_level(emqx_sn_gateway, debug), - emqx_ct_helpers:start_apps([emqx_sn], fun set_special_confs/1), + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_confs/1), Config. end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_sn]). + emqx_ct_helpers:stop_apps([emqx_gateway]). set_special_confs(emqx) -> application:set_env(emqx, plugins_loaded_file, emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins")); -set_special_confs(emqx_sn) -> - application:set_env(emqx_sn, enable_qos3, ?ENABLE_QOS3), - application:set_env(emqx_sn, enable_stats, true), - application:set_env(emqx_sn, username, <<"user1">>), - application:set_env(emqx_sn, password, <<"pw123">>), - application:set_env(emqx_sn, predefined, - [{?PREDEF_TOPIC_ID1, ?PREDEF_TOPIC_NAME1}, - {?PREDEF_TOPIC_ID2, ?PREDEF_TOPIC_NAME2}]); +set_special_confs(emqx_gateway) -> + emqx_config:put( + [emqx_gateway], + #{ mqttsn => + #{'1' => + #{broadcast => true, + clientinfo_override => + #{password => "pw123", + username => "user1" + }, + enable_qos3 => true, + enable_stats => true, + gateway_id => 1, + idle_timeout => 30000, + listener => + #{udp => + #{'1' => + #{acceptors => 8,active_n => 100,backlog => 1024,bind => 1884, + high_watermark => 1048576,max_conn_rate => 1000, + max_connections => 10240000,send_timeout => 15000, + send_timeout_close => true}}}, + predefined => + [#{id => ?PREDEF_TOPIC_ID1, topic => ?PREDEF_TOPIC_NAME1}, + #{id => ?PREDEF_TOPIC_ID2, topic => ?PREDEF_TOPIC_NAME2}]}} + }); + set_special_confs(_App) -> ok. @@ -87,7 +105,7 @@ set_special_confs(_App) -> %% Connect t_connect(_) -> - SockName = {'mqttsn:udp', {{0,0,0,0}, 1884}}, + SockName = {'mqttsn#1:udp', 1884}, ?assertEqual(true, lists:keymember(SockName, 1, esockd:listeners())), {ok, Socket} = gen_udp:open(0, [binary]), @@ -170,7 +188,6 @@ t_subscribe_case02(_) -> ReturnCode = 0, {ok, Socket} = gen_udp:open(0, [binary]), - ClientId = ?CLIENTID, send_connect_msg(Socket, ?CLIENTID), ?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)), diff --git a/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl index 58a458ecc..6161687f2 100644 --- a/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl @@ -23,8 +23,10 @@ -define(REGISTRY, emqx_sn_registry). -define(MAX_PREDEF_ID, 2). --define(PREDEF_TOPICS, [{1, <<"/predefined/topic/name/hello">>}, - {2, <<"/predefined/topic/name/nice">>}]). +-define(PREDEF_TOPICS, [#{id => 1, topic => <<"/predefined/topic/name/hello">>}, + #{id => 2, topic => <<"/predefined/topic/name/nice">>}]). + +-define(INSTA_ID, 'mqttsn#1'). %%-------------------------------------------------------------------- %% Setups @@ -34,88 +36,92 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> - _ = application:set_env(emqx_sn, predefined, ?PREDEF_TOPICS), + application:ensure_all_started(ekka), + ekka_mnesia:start(), Config. end_per_suite(_Config) -> + application:stop(ekka), ok. init_per_testcase(_TestCase, Config) -> - application:set_env(ekka, strict_mode, true), - ekka_mnesia:start(), - emqx_sn_registry:mnesia(boot), - ekka_mnesia:clear_table(emqx_sn_registry), - PredefTopics = application:get_env(emqx_sn, predefined, []), - {ok, _Pid} = ?REGISTRY:start_link(PredefTopics), - Config. + {ok, Pid} = ?REGISTRY:start_link(?INSTA_ID, ?PREDEF_TOPICS), + {Tab, Pid} = ?REGISTRY:lookup_name(Pid), + [{reg, {Tab, Pid}} | Config]. end_per_testcase(_TestCase, Config) -> - ?REGISTRY:stop(), + {Tab, _Pid} = proplists:get_value(reg, Config), + ekka_mnesia:clear_table(Tab), Config. %%-------------------------------------------------------------------- %% Test cases %%-------------------------------------------------------------------- -t_register(_Config) -> - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic2">>)), - ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)), - emqx_sn_registry:unregister_topic(<<"ClientId">>), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)). +t_register(Config) -> + Reg = proplists:get_value(reg, Config), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic2">>)), + ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)), + emqx_sn_registry:unregister_topic(Reg, <<"ClientId">>), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)). -t_register_case2(_Config) -> - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic2">>)), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic3">>)), - ?REGISTRY:unregister_topic(<<"ClientId">>), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic1">>)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, <<"Topic2">>)). +t_register_case2(Config) -> + Reg = proplists:get_value(reg, Config), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic2">>)), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(<<"Topic1">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(<<"Topic2">>, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic3">>)), + ?REGISTRY:unregister_topic(Reg, <<"ClientId">>), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic1">>)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, <<"Topic2">>)). -t_reach_maximum(_Config) -> - register_a_lot(?MAX_PREDEF_ID+1, 16#ffff), - ?assertEqual({error, too_large}, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicABC">>)), +t_reach_maximum(Config) -> + Reg = proplists:get_value(reg, Config), + register_a_lot(?MAX_PREDEF_ID+1, 16#ffff, Reg), + ?assertEqual({error, too_large}, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicABC">>)), Topic1 = iolist_to_binary(io_lib:format("Topic~p", [?MAX_PREDEF_ID+1])), Topic2 = iolist_to_binary(io_lib:format("Topic~p", [?MAX_PREDEF_ID+2])), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic1)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic2)), - ?REGISTRY:unregister_topic(<<"ClientId">>), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic(<<"ClientId">>, ?MAX_PREDEF_ID+2)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic1)), - ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(<<"ClientId">>, Topic2)). + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic1)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic2)), + ?REGISTRY:unregister_topic(Reg, <<"ClientId">>), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic(Reg, <<"ClientId">>, ?MAX_PREDEF_ID+2)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic1)), + ?assertEqual(undefined, ?REGISTRY:lookup_topic_id(Reg, <<"ClientId">>, Topic2)). -t_register_case4(_Config) -> - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicA">>)), - ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicB">>)), - ?assertEqual(?MAX_PREDEF_ID+3, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicC">>)), - ?REGISTRY:unregister_topic(<<"ClientId">>), - ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(<<"ClientId">>, <<"TopicD">>)). +t_register_case4(Config) -> + Reg = proplists:get_value(reg, Config), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicA">>)), + ?assertEqual(?MAX_PREDEF_ID+2, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicB">>)), + ?assertEqual(?MAX_PREDEF_ID+3, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicC">>)), + ?REGISTRY:unregister_topic(Reg, <<"ClientId">>), + ?assertEqual(?MAX_PREDEF_ID+1, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"TopicD">>)). -t_deny_wildcard_topic(_Config) -> - ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(<<"ClientId">>, <<"/TopicA/#">>)), - ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(<<"ClientId">>, <<"/+/TopicB">>)). +t_deny_wildcard_topic(Config) -> + Reg = proplists:get_value(reg, Config), + ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"/TopicA/#">>)), + ?assertEqual({error, wildcard_topic}, ?REGISTRY:register_topic(Reg, <<"ClientId">>, <<"/+/TopicB">>)). %%-------------------------------------------------------------------- %% Helper funcs %%-------------------------------------------------------------------- -register_a_lot(Max, Max) -> +register_a_lot(Max, Max, _Reg) -> ok; -register_a_lot(N, Max) when N < Max -> +register_a_lot(N, Max, Reg) when N < Max -> Topic = iolist_to_binary(["Topic", integer_to_list(N)]), - ?assertEqual(N, ?REGISTRY:register_topic(<<"ClientId">>, Topic)), - register_a_lot(N+1, Max). + ?assertEqual(N, ?REGISTRY:register_topic(Reg, <<"ClientId">>, Topic)), + register_a_lot(N+1, Max, Reg). diff --git a/apps/emqx_gateway/test/props/emqx_sn_proper_types.erl b/apps/emqx_gateway/test/props/emqx_sn_proper_types.erl index 8d4dae357..96318788d 100644 --- a/apps/emqx_gateway/test/props/emqx_sn_proper_types.erl +++ b/apps/emqx_gateway/test/props/emqx_sn_proper_types.erl @@ -16,7 +16,7 @@ -module(emqx_sn_proper_types). --include_lib("emqx_sn/include/emqx_sn.hrl"). +-include_lib("emqx_gateway/src/mqttsn/include/emqx_sn.hrl"). -include_lib("proper/include/proper.hrl"). -compile({no_auto_import, [register/1]}). diff --git a/apps/emqx_gateway/test/props/prop_emqx_sn_frame.erl b/apps/emqx_gateway/test/props/prop_emqx_sn_frame.erl index 0135ebac7..645d24bed 100644 --- a/apps/emqx_gateway/test/props/prop_emqx_sn_frame.erl +++ b/apps/emqx_gateway/test/props/prop_emqx_sn_frame.erl @@ -16,7 +16,7 @@ -module(prop_emqx_sn_frame). --include_lib("emqx_sn/include/emqx_sn.hrl"). +-include_lib("src/mqttsn/include/emqx_sn.hrl"). -include_lib("proper/include/proper.hrl"). -compile({no_auto_import, [register/1]}). From beecc4c5a22a4533cc9764eeff05c3c98a3d4be3 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 15 Jul 2021 11:36:49 +0800 Subject: [PATCH 161/379] test(authn): fix test case for authn --- apps/emqx/src/emqx_access_control.erl | 3 +- apps/emqx/src/emqx_channel.erl | 3 + apps/emqx/test/emqx_access_control_SUITE.erl | 19 ++---- apps/emqx/test/emqx_channel_SUITE.erl | 67 +++++++++++-------- .../src/simple_authn/emqx_authn_jwt.erl | 2 +- 5 files changed, 51 insertions(+), 43 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index b1c38bc41..f0c39aad6 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -29,7 +29,8 @@ -spec(authenticate(emqx_types:clientinfo()) -> ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). authenticate(Credential = #{zone := Zone}) -> - case emqx_zone:get_env(Zone, bypass_authentication, false) of + %% TODO: Rename to bypass_authentication + case emqx_zone:get_env(Zone, bypass_auth_plugins, false) of true -> ok; false -> diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 911a44283..887a24052 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -276,6 +276,9 @@ handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = ConnState}) when ConnState =:= connected orelse ConnState =:= reauthenticating -> handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); +handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = connecting}) -> + handle_out(connack, ?RC_PROTOCOL_ERROR, Channel); + handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> case pipeline([fun enrich_conninfo/2, fun run_conn_hooks/2, diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index b356402fb..cb5e69362 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -33,10 +33,7 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_authenticate(_) -> - emqx_zone:set_env(zone, allow_anonymous, false), - ?assertMatch({error, _}, emqx_access_control:authenticate(clientinfo())), - emqx_zone:set_env(zone, allow_anonymous, true), - ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). + ?assertMatch(ok, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), @@ -44,21 +41,19 @@ t_authorize(_) -> t_bypass_auth_plugins(_) -> ClientInfo = clientinfo(), - emqx_zone:set_env(bypass_zone, allow_anonymous, true), - emqx_zone:set_env(zone, allow_anonymous, false), emqx_zone:set_env(bypass_zone, bypass_auth_plugins, true), emqx:hook('client.authenticate',{?MODULE, auth_fun, []}), - ?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo#{zone => bypass_zone})), - ?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo)). + ?assertMatch(ok, emqx_access_control:authenticate(ClientInfo#{zone => bypass_zone})), + ?assertMatch({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo)). %%-------------------------------------------------------------------- %% Helper functions %%-------------------------------------------------------------------- -auth_fun(#{zone := bypass_zone}, AuthRes) -> - {stop, AuthRes#{auth_result => password_error}}; -auth_fun(#{zone := _}, AuthRes) -> - {stop, AuthRes#{auth_result => success}}. +auth_fun(#{zone := bypass_zone}, _) -> + {stop, ok}; +auth_fun(#{zone := _}, _) -> + {stop, {error, bad_username_or_password}}. clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 09ac7a683..22f97ffd7 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -36,7 +36,7 @@ init_per_suite(Config) -> %% Access Control Meck ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, - fun(_) -> {ok, #{auth_result => success}} end), + fun(_) -> ok end), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), @@ -120,35 +120,40 @@ t_handle_in_unexpected_packet(_) -> {ok, [{outgoing, Packet}, {close, protocol_error}], Channel} = emqx_channel:handle_in(?PUBLISH_PACKET(?QOS_0), Channel). -t_handle_in_connect_auth_failed(_) -> - ConnPkt = #mqtt_packet_connect{ - proto_name = <<"MQTT">>, - proto_ver = ?MQTT_PROTO_V5, - is_bridge = false, - clean_start = true, - keepalive = 30, - properties = #{ - 'Authentication-Method' => <<"failed_auth_method">>, - 'Authentication-Data' => <<"failed_auth_data">> - }, - clientid = <<"clientid">>, - username = <<"username">> - }, - {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} = - emqx_channel:handle_in(?CONNECT_PACKET(ConnPkt), channel(#{conn_state => idle})). +% t_handle_in_connect_auth_failed(_) -> +% ConnPkt = #mqtt_packet_connect{ +% proto_name = <<"MQTT">>, +% proto_ver = ?MQTT_PROTO_V5, +% is_bridge = false, +% clean_start = true, +% keepalive = 30, +% properties = #{ +% 'Authentication-Method' => <<"failed_auth_method">>, +% 'Authentication-Data' => <<"failed_auth_data">> +% }, +% clientid = <<"clientid">>, +% username = <<"username">> +% }, +% {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} = +% emqx_channel:handle_in(?CONNECT_PACKET(ConnPkt), channel(#{conn_state => idle})). t_handle_in_continue_auth(_) -> Properties = #{ 'Authentication-Method' => <<"failed_auth_method">>, 'Authentication-Data' => <<"failed_auth_data">> }, - {shutdown, bad_authentication_method, ?CONNACK_PACKET(?RC_BAD_AUTHENTICATION_METHOD), _} = - emqx_channel:handle_in(?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION,Properties), channel()), - {shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _} = + + Channel1 = channel(#{conn_state => connected}), + {ok, [{outgoing, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR)}, {close, protocol_error}], Channel1} = + emqx_channel:handle_in(?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, Properties), Channel1), + + Channel2 = channel(#{conn_state => connecting}), + ConnInfo = emqx_channel:info(conninfo, Channel2), + Channel3 = emqx_channel:set_field(conninfo, ConnInfo#{conn_props => Properties}, Channel2), + + {ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS)}], _} = emqx_channel:handle_in( - ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION,Properties), - channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => Properties}}) - ). + ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, Properties), Channel3). t_handle_in_re_auth(_) -> Properties = #{ @@ -167,10 +172,14 @@ t_handle_in_re_auth(_) -> ?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => undefined}}) ), - {ok, [{outgoing, ?DISCONNECT_PACKET(?RC_NOT_AUTHORIZED)}, {close, not_authorized}], _} = + + Channel1 = channel(), + ConnInfo = emqx_channel:info(conninfo, Channel1), + Channel2 = emqx_channel:set_field(conninfo, ConnInfo#{conn_props => Properties}, Channel1), + + {ok, ?AUTH_PACKET(?RC_SUCCESS), _} = emqx_channel:handle_in( - ?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), - channel(#{conninfo => #{proto_ver => ?MQTT_PROTO_V5, conn_props => Properties}}) + ?AUTH_PACKET(?RC_RE_AUTHENTICATE,Properties), Channel2 ). t_handle_in_qos0_publish(_) -> @@ -346,8 +355,8 @@ t_handle_in_disconnect(_) -> t_handle_in_auth(_) -> Channel = channel(#{conn_state => connected}), - Packet = ?DISCONNECT_PACKET(?RC_IMPLEMENTATION_SPECIFIC_ERROR), - {ok, [{outgoing, Packet}, {close, implementation_specific_error}], Channel} = + Packet = ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), + {ok, [{outgoing, Packet}, {close, protocol_error}], Channel} = emqx_channel:handle_in(?AUTH_PACKET(), Channel). t_handle_in_frame_error(_) -> @@ -664,7 +673,7 @@ t_check_banned(_) -> ok = emqx_channel:check_banned(connpkt(), channel()). t_auth_connect(_) -> - {ok, _Chan} = emqx_channel:auth_connect(connpkt(), channel()). + {ok, _, _Chan} = emqx_channel:authenticate(?CONNECT_PACKET(connpkt()), channel()). t_process_alias(_) -> Publish = #mqtt_packet_publish{topic_name = <<>>, properties = #{'Topic-Alias' => 1}}, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 2605a682d..72bbd560a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -220,7 +220,7 @@ create2(#{use_jwks := true, verify_claims := VerifyClaims, ssl := #{enable := Enable} = SSL} = Config) -> SSLOpts = case Enable of - true -> maps:without(enable, SSL); + true -> maps:without([enable], SSL); false -> #{} end, case emqx_authn_jwks_connector:start_link(Config#{ssl_opts => SSLOpts}) of From 7d66760c1ecc58fb685fae5713a7c5d2a6770952 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 11:55:31 +0800 Subject: [PATCH 162/379] fix(hocon): start emqx failed using os env configs --- .ci/docker-compose-file/conf.cluster.env | 2 +- apps/emqx/rebar.config | 2 +- rebar.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/docker-compose-file/conf.cluster.env b/.ci/docker-compose-file/conf.cluster.env index d8294a785..be3cc71ac 100644 --- a/.ci/docker-compose-file/conf.cluster.env +++ b/.ci/docker-compose-file/conf.cluster.env @@ -1,6 +1,6 @@ EMQX_NAME=emqx EMQX_CLUSTER__DISCOVERY=static -EMQX_CLUSTER__STATIC__SEEDS="emqx@node1.emqx.io, emqx@node2.emqx.io" +EMQX_CLUSTER__STATIC__SEEDS="[emqx@node1.emqx.io, emqx@node2.emqx.io]" EMQX_LISTENER__TCP__EXTERNAL__PROXY_PROTOCOL=on EMQX_LISTENER__WS__EXTERNAL__PROXY_PROTOCOL=on EMQX_LOG__LEVEL=debug diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 68463778d..99cca64d6 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -16,7 +16,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {branch, "parse_error_crash"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} diff --git a/rebar.config b/rebar.config index 3e7b9be01..b965c8fba 100644 --- a/rebar.config +++ b/rebar.config @@ -61,7 +61,7 @@ , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 , {getopt, "1.0.1"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {branch, "parse_error_crash"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} ]}. From 4da59a57859ad9c12e6584edb132174a672b9f8d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 11:56:13 +0800 Subject: [PATCH 163/379] fix(dialyzer): some dialyzer problems --- apps/emqx/src/emqx_alarm.erl | 4 ++-- apps/emqx/src/emqx_channel.erl | 6 ++---- apps/emqx/src/emqx_flapping.erl | 2 +- apps/emqx/src/emqx_frame.erl | 7 ------- apps/emqx/src/emqx_os_mon.erl | 4 ++-- apps/emqx/src/emqx_session.erl | 4 ++-- apps/emqx/src/emqx_ws_connection.erl | 2 +- 7 files changed, 10 insertions(+), 19 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 1ae985deb..21581e71a 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -307,8 +307,8 @@ clear_table(TableName) -> end. ensure_timer(OldTRef, Period) -> - case is_reference(OldTRef) of - true -> _ = erlang:cancel_timer(OldTRef); + _ = case is_reference(OldTRef) of + true -> erlang:cancel_timer(OldTRef); false -> ok end, emqx_misc:start_timer(Period, delete_expired_deactivated_alarm). diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 98206c37a..5660863a7 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -253,14 +253,12 @@ set_peercert_infos(Peercert, ClientInfo, Zone, Listener) -> {DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)}, PeercetAs = fun(Key) -> - % esockd_peercert:peercert is opaque - % https://github.com/emqx/esockd/blob/master/src/esockd_peercert.erl case get_mqtt_conf(Zone, Listener, Key) of cn -> CN; dn -> DN; crt -> Peercert; - pem -> base64:encode(Peercert); - md5 -> emqx_passwd:hash(md5, Peercert); + pem when is_binary(Peercert) -> base64:encode(Peercert); + md5 when is_binary(Peercert) -> emqx_passwd:hash(md5, Peercert); _ -> undefined end end, diff --git a/apps/emqx/src/emqx_flapping.erl b/apps/emqx/src/emqx_flapping.erl index dcc88f6b4..64633833b 100644 --- a/apps/emqx/src/emqx_flapping.erl +++ b/apps/emqx/src/emqx_flapping.erl @@ -54,7 +54,7 @@ clientid :: emqx_types:clientid(), peerhost :: emqx_types:peerhost(), started_at :: pos_integer(), - detect_cnt :: pos_integer() + detect_cnt :: integer() }). -opaque(flapping() :: #flapping{}). diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 0bab10e0c..082801bad 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -34,9 +34,6 @@ , serialize/2 ]). --export([ set_opts/2 - ]). - -export_type([ options/0 , parse_state/0 , parse_result/0 @@ -86,10 +83,6 @@ initial_parse_state() -> initial_parse_state(Options) when is_map(Options) -> ?none(maps:merge(?DEFAULT_OPTIONS, Options)). --spec set_opts(parse_state(), options()) -> parse_state(). -set_opts({_, OldOpts}, Opts) -> - maps:merge(OldOpts, Opts). - %%-------------------------------------------------------------------- %% Parse MQTT Frame %%-------------------------------------------------------------------- diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index ee509e54f..b70c27e1b 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -81,7 +81,7 @@ init([]) -> set_mem_check_interval(maps:get(mem_check_interval, Opts)), set_sysmem_high_watermark(maps:get(sysmem_high_watermark, Opts)), set_procmem_high_watermark(maps:get(procmem_high_watermark, Opts)), - start_check_timer(), + _ = start_check_timer(), {ok, #{}}. handle_call(Req, _From, State) -> @@ -95,7 +95,7 @@ handle_cast(Msg, State) -> handle_info({timeout, _Timer, check}, State) -> CPUHighWatermark = emqx_config:get([sysmon, os, cpu_high_watermark]) * 100, CPULowWatermark = emqx_config:get([sysmon, os, cpu_low_watermark]) * 100, - case emqx_vm:cpu_util() of %% TODO: should be improved? + _ = case emqx_vm:cpu_util() of %% TODO: should be improved? 0 -> ok; Busy when Busy >= CPUHighWatermark -> emqx_alarm:activate(high_cpu_usage, #{usage => io_lib:format("~p%", [Busy]), diff --git a/apps/emqx/src/emqx_session.erl b/apps/emqx/src/emqx_session.erl index 995aff713..c1f6767b4 100644 --- a/apps/emqx/src/emqx_session.erl +++ b/apps/emqx/src/emqx_session.erl @@ -96,7 +96,7 @@ %% Client’s Subscriptions. subscriptions :: map(), %% Max subscriptions allowed - max_subscriptions :: non_neg_integer(), + max_subscriptions :: non_neg_integer() | infinity, %% Upgrade QoS? upgrade_qos :: boolean(), %% Client <- Broker: QoS1/2 messages sent to the client but @@ -115,7 +115,7 @@ %% have not been completely acknowledged awaiting_rel :: map(), %% Maximum number of awaiting QoS2 messages allowed - max_awaiting_rel :: non_neg_integer(), + max_awaiting_rel :: non_neg_integer() | infinity, %% Awaiting PUBREL Timeout (Unit: millsecond) await_rel_timeout :: timeout(), %% Created at diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index 50a1cfea1..947658035 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -524,7 +524,7 @@ check_oom(State = #state{channel = Channel}) -> ShutdownPolicy = emqx_config:get_listener_conf(emqx_channel:info(zone, Channel), emqx_channel:info(listener, Channel), [force_shutdown]), case ShutdownPolicy of - #{enable := false} -> ok; + #{enable := false} -> State; #{enable := true} -> case emqx_misc:check_oom(ShutdownPolicy) of Shutdown = {shutdown, _Reason} -> From 533f4cf63e582564095afc2f3d5a06b9ac570546 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 13:52:06 +0800 Subject: [PATCH 164/379] fix(dialyzer): some dialyzer complains --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_config.erl | 2 +- apps/emqx/src/emqx_types.erl | 5 +++-- apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl | 3 ++- rebar.config | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 99cca64d6..ca45b20a9 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -12,7 +12,7 @@ [ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} - , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.1"}}} + , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 18e0d7020..06cd252ed 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -85,7 +85,7 @@ put_listener_conf(Zone, Listener, KeyPath, Conf) -> ?MODULE:put([zones, Zone, listeners, Listener | KeyPath], Conf). -spec find_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> - {ok, term()} | {not_foud, emqx_map_lib:config_key_path(), term()}. + {ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}. find_listener_conf(Zone, Listener, KeyPath) -> %% the configs in listener is prior to the ones in the zone case find([zones, Zone, listeners, Listener | KeyPath]) of diff --git a/apps/emqx/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl index fbe62e4b2..09ec54b9d 100644 --- a/apps/emqx/src/emqx_types.erl +++ b/apps/emqx/src/emqx_types.erl @@ -209,7 +209,8 @@ -type(infos() :: #{atom() => term()}). -type(stats() :: [{atom(), term()}]). --type(oom_policy() :: #{message_queue_len => non_neg_integer(), - max_heap_size => non_neg_integer() +-type(oom_policy() :: #{max_message_queue_len => non_neg_integer(), + max_heap_size => non_neg_integer(), + enable => boolean() }). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 322baa120..4b4feb29d 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -583,7 +583,8 @@ handle_call(discard, Channel) -> % shutdown_and_reply(takeovered, AllPendings, Channel); handle_call(list_acl_cache, Channel) -> - {reply, emqx_acl_cache:list_acl_cache(), Channel}; + %% This won't work + {reply, emqx_acl_cache:list_acl_cache(default, mqtt_tcp), Channel}; %% XXX: No Quota Now % handle_call({quota, Policy}, Channel) -> diff --git a/rebar.config b/rebar.config index b965c8fba..7998cefb9 100644 --- a/rebar.config +++ b/rebar.config @@ -47,7 +47,7 @@ , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} - , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.1"}}} + , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} % TODO: delete when all apps moved to hocon From f6702b020e9f0b2c4275ddc631f523077f09a0b0 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 13:57:24 +0800 Subject: [PATCH 165/379] fix(hocon): update hocon version to 0.10.1 --- apps/emqx/rebar.config | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index ca45b20a9..fa483e90e 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -16,7 +16,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon - , {hocon, {git, "https://github.com/emqx/hocon.git", {branch, "parse_error_crash"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.1"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} diff --git a/rebar.config b/rebar.config index 7998cefb9..268e89965 100644 --- a/rebar.config +++ b/rebar.config @@ -61,7 +61,7 @@ , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 , {getopt, "1.0.1"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {branch, "parse_error_crash"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} ]}. From 5ecc9929441bfdd0102c0cd79e68416c4bcbcdeb Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 15 Jul 2021 14:25:23 +0800 Subject: [PATCH 166/379] chore(authn): fix coap authn and update tag for esasl --- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index 315cfbb5c..a76dc904e 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -113,7 +113,7 @@ init({ClientId, Username, Password, Channel}) -> password = Password}, _ = run_hooks('client.connect', [conninfo(State0)], undefined), case emqx_access_control:authenticate(clientinfo(State0)) of - {ok, _AuthResult} -> + ok -> ok = emqx_cm:discard_session(ClientId), _ = run_hooks('client.connack', [conninfo(State0), success], undefined), diff --git a/rebar.config b/rebar.config index 0c741e907..3bda59133 100644 --- a/rebar.config +++ b/rebar.config @@ -63,7 +63,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} - , {esasl, {git, "https://github.com/emqx/esasl", {branch, "refactor/sasl"}}} + , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}} ]}. {xref_ignores, From bcae0cbb50d924272686f12d92101c385b5d88a9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 14:26:12 +0800 Subject: [PATCH 167/379] fix(gateways): hardcode the listener and zone names --- apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl | 3 ++- apps/emqx_exproto/src/emqx_exproto_conn.erl | 3 ++- apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl index 315cfbb5c..3d09d0f92 100644 --- a/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl +++ b/apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl @@ -371,7 +371,8 @@ clientinfo(#state{peername = {PeerHost, _}, clientid = ClientId, username = Username, password = Password}) -> - #{zone => undefined, + #{zone => default, + listener => mqtt_tcp, %% FIXME: this won't work protocol => coap, peerhost => PeerHost, sockport => 5683, %% FIXME: diff --git a/apps/emqx_exproto/src/emqx_exproto_conn.erl b/apps/emqx_exproto/src/emqx_exproto_conn.erl index da655bcb4..256a69b30 100644 --- a/apps/emqx_exproto/src/emqx_exproto_conn.erl +++ b/apps/emqx_exproto/src/emqx_exproto_conn.erl @@ -219,7 +219,8 @@ send(Data, #state{socket = {esockd_transport, Sock}}) -> -define(DEFAULT_GC_OPTS, #{count => 1000, bytes => 1024*1024}). -define(DEFAULT_IDLE_TIMEOUT, 30000). --define(DEFAULT_OOM_POLICY, #{max_heap_size => 4194304,message_queue_len => 32000}). +-define(DEFAULT_OOM_POLICY, #{enable => true, max_heap_size => 4194304, + max_message_queue_len => 32000}). init(Parent, WrappedSock, Peername, Options) -> case esockd_wait(WrappedSock) of diff --git a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl index 34c72dcca..cacc21a9d 100644 --- a/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl +++ b/apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl @@ -441,7 +441,8 @@ take_place(Text, Placeholder, Value) -> clientinfo(#lwm2m_state{peername = {PeerHost, _}, endpoint_name = EndpointName, mountpoint = Mountpoint}) -> - #{zone => undefined, + #{zone => default, + listener => mqtt_tcp, %% FIXME: this won't work protocol => lwm2m, peerhost => PeerHost, sockport => 5683, %% FIXME: From 3c47ab92d7908b76786fcf34e5c45d421df79d51 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 15:11:54 +0800 Subject: [PATCH 168/379] fix(listeners): update the default tls ciphers --- apps/emqx/src/emqx_schema.erl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 14626db79..b988f9dad 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -417,6 +417,7 @@ fields("ssl_opts") -> , depth => 10 , reuse_sessions => true , versions => default_tls_vsns() + , ciphers => default_ciphers() }); fields("deflate_opts") -> @@ -673,6 +674,27 @@ tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2'; tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1'; tls_vsn(<<"tlsv1">>) -> 'tlsv1'. +default_ciphers() -> [ + "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_CCM_SHA256", "TLS_AES_128_CCM_8_SHA256", "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384", + "ECDHE-ECDSA-DES-CBC3-SHA", "ECDH-ECDSA-AES256-GCM-SHA384", "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", "ECDH-RSA-AES256-SHA384", "DHE-DSS-AES256-GCM-SHA384", + "DHE-DSS-AES256-SHA256", "AES256-GCM-SHA384", "AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", "ECDH-ECDSA-AES128-SHA256", "ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256", "DHE-DSS-AES128-SHA256", "AES128-GCM-SHA256", "AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA", "ECDHE-RSA-AES256-SHA", "DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA", "ECDH-RSA-AES256-SHA", "AES256-SHA", "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", "DHE-DSS-AES128-SHA", "ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA", "AES128-SHA" + ] ++ psk_ciphers(). + +psk_ciphers() -> [ + "PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA" + ]. + %% @private return a list of keys in a parent field -spec(keys(string(), hocon:config()) -> [string()]). keys(Parent, Conf) -> From 07ce6368030d9e22ec39f2a6b62e86c754fd6d09 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 15 Jul 2021 16:16:41 +0800 Subject: [PATCH 169/379] feat(authn): support enable authn in config --- apps/emqx_authn/etc/emqx_authn.conf | 1 + apps/emqx_authn/src/emqx_authn_app.erl | 11 +++++++---- apps/emqx_authn/src/emqx_authn_schema.erl | 8 +++++++- apps/emqx_authn/test/emqx_authn_SUITE.erl | 9 +++++++++ apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl | 14 ++++++++++---- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 5194445b8..3e69ae46d 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,4 +1,5 @@ emqx_authn: { + enable: false authenticators: [ # { # name: "authenticator1" diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index a78fa54f1..225969cd2 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -38,12 +38,15 @@ stop(_State) -> ok. initialize() -> - #{authenticators := Authenticators} = emqx_config:get([emqx_authn], #{authenticators => []}), - initialize(Authenticators). + AuthNConfig = emqx_config:get([emqx_authn], #{enable => false, + authenticators => []}), + initialize(AuthNConfig). -initialize(Authenticators) -> +initialize(#{enable := Enable, authenticators := Authenticators}) -> {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), - initialize_authenticators(Authenticators). + initialize_authenticators(Authenticators), + Enable =:= true andalso emqx_authn:enable(), + ok. initialize_authenticators([]) -> ok; diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 8b844ab69..7ed5a9999 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -31,7 +31,9 @@ structs() -> ["emqx_authn"]. fields("emqx_authn") -> - [ {authenticators, fun authenticators/1} ]; + [ {enable, fun enable/1} + , {authenticators, fun authenticators/1} + ]; fields('password-based') -> [ {name, fun authenticator_name/1} @@ -63,6 +65,10 @@ fields(scram) -> ]))} ]. +enable(type) -> boolean(); +enable(defualt) -> false; +enable(_) -> undefined. + authenticators(type) -> hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based') , hoconsc:ref(?MODULE, jwt) diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 827eb49ab..b93e32e3d 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -94,3 +94,12 @@ t_authenticator(_) -> ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), ok. + +t_authenticate(_) -> + ?assertEqual(false, emqx_zone:get_env(external, bypass_auth_plugins, false)), + ClientInfo = #{zone => external, + username => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + emqx_authn:enable(), + ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)). diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 75dd497ae..fe7d244cd 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -48,9 +48,6 @@ set_special_configs(_App) -> ok. t_mnesia_authenticator(_) -> - ct:pal("11111 ~p~n", [?AUTH:list_authenticators(<<"mqtt">>)]), - - AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, mechanism => 'password-based', @@ -67,13 +64,22 @@ t_mnesia_authenticator(_) -> ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ClientInfo = #{username => <<"myuser">>, + ?assertEqual(false, emqx_zone:get_env(external, bypass_auth_plugins, false)), + + ClientInfo = #{zone => external, + username => <<"myuser">>, password => <<"mypass">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?AUTH:enable(), + ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ClientInfo2 = ClientInfo#{username => <<"baduser">>}, ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), + ClientInfo3 = ClientInfo#{password => <<"badpass">>}, ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), + ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)), From 6fbf20b93037ae524366f63000a135f86bde17ff Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 15:38:47 +0800 Subject: [PATCH 170/379] fix(test): update the testcases --- apps/emqx/src/emqx_listeners.erl | 4 ++-- apps/emqx/src/emqx_sys.erl | 6 ------ apps/emqx/test/props/prop_emqx_sys.erl | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 8e6dc1ab1..8cf3852e2 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -38,11 +38,11 @@ start() -> foreach_listeners(fun start_listener/3). --spec(start_listener(atom()) -> ok). +-spec start_listener(atom()) -> ok | {error, term()}. start_listener(ListenerId) -> apply_on_listener(ListenerId, fun start_listener/3). --spec(start_listener(atom(), atom(), map()) -> ok). +-spec start_listener(atom(), atom(), map()) -> ok | {error, term()}. start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) -> case do_start_listener(ZoneName, ListenerName, Conf) of {ok, _} -> diff --git a/apps/emqx/src/emqx_sys.erl b/apps/emqx/src/emqx_sys.erl index 6b71b7807..2f3f782e6 100644 --- a/apps/emqx/src/emqx_sys.erl +++ b/apps/emqx/src/emqx_sys.erl @@ -32,8 +32,6 @@ , uptime/0 , datetime/0 , sysdescr/0 - , sys_interval/0 - , sys_heatbeat_interval/0 ]). -export([info/0]). @@ -104,13 +102,9 @@ datetime() -> io_lib:format( "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). -%% @doc Get sys interval --spec(sys_interval() -> pos_integer()). sys_interval() -> emqx_config:get([broker, sys_msg_interval]). -%% @doc Get sys heatbeat interval --spec(sys_heatbeat_interval() -> pos_integer()). sys_heatbeat_interval() -> emqx_config:get([broker, sys_heartbeat_interval]). diff --git a/apps/emqx/test/props/prop_emqx_sys.erl b/apps/emqx/test/props/prop_emqx_sys.erl index 67718ec37..b03bbbb7f 100644 --- a/apps/emqx/test/props/prop_emqx_sys.erl +++ b/apps/emqx/test/props/prop_emqx_sys.erl @@ -59,6 +59,8 @@ prop_sys() -> do_setup() -> ok = emqx_logger:set_log_level(emergency), + emqx_config:put([broker, sys_msg_interval], 60000), + emqx_config:put([broker, sys_msg_interval], 30000), [mock(Mod) || Mod <- ?mock_modules], ok. @@ -98,8 +100,6 @@ command(_State) -> {call, emqx_sys, uptime, []}, {call, emqx_sys, datetime, []}, {call, emqx_sys, sysdescr, []}, - {call, emqx_sys, sys_interval, []}, - {call, emqx_sys, sys_heatbeat_interval, []}, %------------ unexpected message ----------------------% {call, emqx_sys, handle_call, [emqx_sys, other, state]}, {call, emqx_sys, handle_cast, [emqx_sys, other]}, From e838df99a9984cadef98c508890470d87f632d29 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 15 Jul 2021 13:56:07 +0800 Subject: [PATCH 171/379] fix: api auth header support --- apps/emqx_management/src/emqx_mgmt_http.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index 178e4b04d..aae681fd5 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -101,10 +101,10 @@ authorize_appid(Req) -> {basic, AppId, AppSecret} -> case emqx_mgmt_auth:is_authorized(AppId, AppSecret) of true -> ok; - false -> {401} + false -> {401, #{<<"WWW-Authenticate">> => <<"Basic Realm=\"minirest-server\"">>}, <<"UNAUTHORIZED">>} end; _ -> - {401} + {401, #{<<"WWW-Authenticate">> => <<"Basic Realm=\"minirest-server\"">>}, <<"UNAUTHORIZED">>} end. format(Port) when is_integer(Port) -> From ddda18bcb8d8769ae4a4458150daf97fb849e1a7 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 15 Jul 2021 17:14:35 +0800 Subject: [PATCH 172/379] chore(authn): add jose dep --- apps/emqx_authn/rebar.config | 4 +++- apps/emqx_authn/src/emqx_authn.app.src | 2 +- apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl | 4 ---- apps/emqx_connector/rebar.config | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/emqx_authn/rebar.config b/apps/emqx_authn/rebar.config index 73696b033..32b5a43e0 100644 --- a/apps/emqx_authn/rebar.config +++ b/apps/emqx_authn/rebar.config @@ -1,4 +1,6 @@ -{deps, []}. +{deps, [ + {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} +]}. {edoc_opts, [{preprocess, true}]}. {erl_opts, [warn_unused_vars, diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index 208e27b85..3b89d2a99 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {modules, []}, {registered, [emqx_authn_sup, emqx_authn_registry]}, - {applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql]}, + {applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql,jose]}, {mod, {emqx_authn_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 72bbd560a..437dac72d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -102,10 +102,6 @@ certificate(_) -> undefined. endpoint(type) -> string(); endpoint(_) -> undefined. -% ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); -% ssl_opts(default) -> []; -% ssl_opts(_) -> undefined. - refresh_interval(type) -> integer(); refresh_interval(default) -> 300; refresh_interval(validator) -> [fun(I) -> I > 0 end]; diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 9bfbb9277..cbeff37eb 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -4,7 +4,6 @@ ]}. {deps, [ - {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}, {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, {epgsql, {git, "https://github.com/epgsql/epgsql", {tag, "4.4.0"}}}, From d5756ecd52e40a23f81097ee0636174af2b2e44a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 17:19:46 +0800 Subject: [PATCH 173/379] fix(test): update the testcases --- apps/emqx/src/emqx_listeners.erl | 4 ++++ apps/emqx/test/props/prop_emqx_sys.erl | 2 +- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 8cf3852e2..973022af8 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -44,6 +44,10 @@ start_listener(ListenerId) -> -spec start_listener(atom(), atom(), map()) -> ok | {error, term()}. start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) -> + dbg:tracer(), +dbg:p(all, c), +dbg:tpl(tls_record, sufficient_crypto_support, '_', cx), + case do_start_listener(ZoneName, ListenerName, Conf) of {ok, _} -> console_print("Start ~s listener ~s on ~s successfully.~n", diff --git a/apps/emqx/test/props/prop_emqx_sys.erl b/apps/emqx/test/props/prop_emqx_sys.erl index b03bbbb7f..170611061 100644 --- a/apps/emqx/test/props/prop_emqx_sys.erl +++ b/apps/emqx/test/props/prop_emqx_sys.erl @@ -60,7 +60,7 @@ prop_sys() -> do_setup() -> ok = emqx_logger:set_log_level(emergency), emqx_config:put([broker, sys_msg_interval], 60000), - emqx_config:put([broker, sys_msg_interval], 30000), + emqx_config:put([broker, sys_heartbeat_interval], 30000), [mock(Mod) || Mod <- ?mock_modules], ok. diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 7530c3183..1e2264c29 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -68,7 +68,9 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo = #{clientid => <<"clientid">>, username => <<"username">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), From a16a15e3a9e175c21d4b0d2a33878e63e5ec8a83 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 18:34:27 +0800 Subject: [PATCH 174/379] fix(test): remove tlsv1.3 provisionally to make test pass --- apps/emqx/src/emqx_listeners.erl | 4 ---- apps/emqx/src/emqx_schema.erl | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 973022af8..8cf3852e2 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -44,10 +44,6 @@ start_listener(ListenerId) -> -spec start_listener(atom(), atom(), map()) -> ok | {error, term()}. start_listener(ZoneName, ListenerName, #{type := Type, bind := Bind} = Conf) -> - dbg:tracer(), -dbg:p(all, c), -dbg:tpl(tls_record, sufficient_crypto_support, '_', cx), - case do_start_listener(ZoneName, ListenerName, Conf) of {ok, _} -> console_print("Start ~s listener ~s on ~s successfully.~n", diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index b988f9dad..62b4c2a21 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -668,7 +668,8 @@ ssl(Defaults) -> , {"user_lookup_fun", t(any(), undefined, {fun emqx_psk:lookup/3, <<>>})} ]. -default_tls_vsns() -> [<<"tlsv1.3">>, <<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. +%% on erl23.2.7.2-emqx-2, sufficient_crypto_support('tlsv1.3') -> false +default_tls_vsns() -> [<<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3'; tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2'; tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1'; From a1488b39461c55f835ab3a774e1a84e491775126 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 18:54:31 +0800 Subject: [PATCH 175/379] fix(test): merge conflicts --- apps/emqx/src/emqx_access_control.erl | 8 ++++---- apps/emqx/src/emqx_channel.erl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index b7731fdb5..71531c421 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -28,12 +28,12 @@ -spec(authenticate(emqx_types:clientinfo()) -> ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). -authenticate(Credential = #{zone := Zone, listener := Listener}) -> - run_hooks('client.authenticate', [Credential], ok) +authenticate(Credential) -> + run_hooks('client.authenticate', [Credential], ok). %% @doc Check ACL --spec(authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) - -> allow | deny). +-spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) + -> allow | deny. authorize(ClientInfo = #{zone := Zone, listener := Listener}, PubSub, Topic) -> case emqx_acl_cache:is_enabled(Zone, Listener) of true -> check_authorization_cache(ClientInfo, PubSub, Topic); diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index c10b937e5..4d9b8ae12 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -995,7 +995,7 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting}) handle_info({sock_closed, Reason}, Channel = #channel{conn_state = ConnState, - clientinfo = ClientInfo = #{zone := Zone listener := Listener}}) + clientinfo = ClientInfo = #{zone := Zone, listener := Listener}}) when ConnState =:= connected orelse ConnState =:= reauthenticating -> emqx_config:get_listener_conf(Zone, Listener, [flapping_detect, enable]) andalso emqx_flapping:detect(ClientInfo), From 187d200cb73418ebf91c6b7b286f92fcb70b9582 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Tue, 13 Jul 2021 14:32:56 +0800 Subject: [PATCH 176/379] refactor: stats api & metrics api; fix: clients api parameter type lose --- apps/emqx_management/src/emqx_mgmt.erl | 35 ++- .../src/emqx_mgmt_api_clients.erl | 1 + .../src/emqx_mgmt_api_metrics.erl | 286 ++++++++++++++++-- .../src/emqx_mgmt_api_nodes.erl | 82 ++++- .../src/emqx_mgmt_api_stats.erl | 108 +++++-- .../test/emqx_mgmt_metrics_api_SUITE.erl | 52 ++++ .../test/emqx_mgmt_nodes_api_SUITE.erl | 31 +- .../test/emqx_mgmt_stats_api_SUITE.erl | 52 ++++ 8 files changed, 592 insertions(+), 55 deletions(-) create mode 100644 apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl create mode 100644 apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index a3102ab66..dc7b24d57 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -175,7 +175,7 @@ broker_info(Node) -> %%-------------------------------------------------------------------- get_metrics() -> - [{Node, get_metrics(Node)} || Node <- ekka_mnesia:running_nodes()]. + nodes_info_count([get_metrics(Node) || Node <- ekka_mnesia:running_nodes()]). get_metrics(Node) when Node =:= node() -> emqx_metrics:all(); @@ -183,13 +183,44 @@ get_metrics(Node) -> rpc_call(Node, get_metrics, [Node]). get_stats() -> - [{Node, get_stats(Node)} || Node <- ekka_mnesia:running_nodes()]. + GlobalStatsKeys = + [ 'retained.count' + , 'retained.max' + , 'routes.count' + , 'routes.max' + , 'subscriptions.shared.count' + , 'subscriptions.shared.max' + ], + CountStats = nodes_info_count([ + begin + Stats = get_stats(Node), + delete_keys(Stats, GlobalStatsKeys) + end || Node <- ekka_mnesia:running_nodes()]), + GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))), + maps:merge(CountStats, GlobalStats). + +delete_keys(List, []) -> + List; +delete_keys(List, [Key | Keys]) -> + delete_keys(proplists:delete(Key, List), Keys). get_stats(Node) when Node =:= node() -> emqx_stats:getstats(); get_stats(Node) -> rpc_call(Node, get_stats, [Node]). +nodes_info_count(PropList) -> + NodeCount = + fun({Key, Value}, Result) -> + Count = maps:get(Key, Result, 0), + Result#{Key => Count + Value} + end, + AllCount = + fun(StatsMap, Result) -> + lists:foldl(NodeCount, Result, StatsMap) + end, + lists:foldl(AllCount, #{}, PropList). + %%-------------------------------------------------------------------- %% Clients %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 74cd995a4..138b61097 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -313,6 +313,7 @@ subscribe_api() -> #{ name => topic, in => query, + type => string, required => true, default => <<"topic_1">> } diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index a4bf652a2..d959b0b17 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -16,27 +16,277 @@ -module(emqx_mgmt_api_metrics). --rest_api(#{name => list_all_metrics, - method => 'GET', - path => "/metrics", - func => list, - descr => "A list of metrics of all nodes in the cluster"}). +-behavior(minirest_api). --rest_api(#{name => list_node_metrics, - method => 'GET', - path => "/nodes/:atom:node/metrics", - func => list, - descr => "A list of metrics of a node"}). +-export([api_spec/0]). -export([list/2]). -list(Bindings, _Params) when map_size(Bindings) == 0 -> - emqx_mgmt:return({ok, [#{node => Node, metrics => maps:from_list(Metrics)} - || {Node, Metrics} <- emqx_mgmt:get_metrics()]}); +api_spec() -> + {[metrics_api()], [metrics_schema()]}. -list(#{node := Node}, _Params) -> - case emqx_mgmt:get_metrics(Node) of - {error, Reason} -> emqx_mgmt:return({error, Reason}); - Metrics -> emqx_mgmt:return({ok, maps:from_list(Metrics)}) - end. +metrics_schema() -> + DefinitionName = <<"metrics">>, + DefinitionProperties = #{ + <<"actions.failure">> => #{ + type => <<"integer">>, + description => <<"Number of failure executions of the rule engine action">>}, + <<"actions.success">> => #{ + type => <<"integer">>, + description => <<"Number of successful executions of the rule engine action">>}, + <<"bytes.received">> => #{ + type => <<"integer">>, + description => <<"Number of bytes received by EMQ X Broker">>}, + <<"bytes.sent">> => #{ + type => <<"integer">>, + description => <<"Number of bytes sent by EMQ X Broker on this connection">>}, + <<"client.authenticate">> => #{ + type => <<"integer">>, + description => <<"Number of client authentications">>}, + <<"client.auth.anonymous">> => #{ + type => <<"integer">>, + description => <<"Number of clients who log in anonymously">>}, + <<"client.connect">> => #{ + type => <<"integer">>, + description => <<"Number of client connections">>}, + <<"client.connack">> => #{ + type => <<"integer">>, + description => <<"Number of CONNACK packet sent">>}, + <<"client.connected">> => #{ + type => <<"integer">>, + description => <<"Number of successful client connections">>}, + <<"client.disconnected">> => #{ + type => <<"integer">>, + description => <<"Number of client disconnects">>}, + <<"client.check_acl">> => #{ + type => <<"integer">>, + description => <<"Number of ACL rule checks">>}, + <<"client.subscribe">> => #{ + type => <<"integer">>, + description => <<"Number of client subscriptions">>}, + <<"client.unsubscribe">> => #{ + type => <<"integer">>, + description => <<"Number of client unsubscriptions">>}, + <<"delivery.dropped.too_large">> => #{ + type => <<"integer">>, + description => <<"The number of messages that were dropped because the length exceeded the limit when sending">>}, + <<"delivery.dropped.queue_full">> => #{ + type => <<"integer">>, + description => <<"Number of messages with a non-zero QoS that were dropped because the message queue was full when sending">>}, + <<"delivery.dropped.qos0_msg">> => #{ + type => <<"integer">>, + description => <<"Number of messages with QoS 0 that were dropped because the message queue was full when sending">>}, + <<"delivery.dropped.expired">> => #{ + type => <<"integer">>, + description => <<"Number of messages dropped due to message expiration on sending">>}, + <<"delivery.dropped.no_local">> => #{ + type => <<"integer">>, + description => <<"Number of messages that were dropped due to the No Local subscription option when sending">>}, + <<"delivery.dropped">> => #{ + type => <<"integer">>, + description => <<"Total number of discarded messages when sending">>}, + <<"messages.delayed">> => #{ + type => <<"integer">>, + description => <<"Number of delay- published messages stored by EMQ X Broker">>}, + <<"messages.delivered">> => #{ + type => <<"integer">>, + description => <<"Number of messages forwarded to the subscription process internally by EMQ X Broker">>}, + <<"messages.dropped">> => #{ + type => <<"integer">>, + description => <<"Total number of messages dropped by EMQ X Broker before forwarding to the subscription process">>}, + <<"messages.dropped.expired">> => #{ + type => <<"integer">>, + description => <<"Number of messages dropped due to message expiration when receiving">>}, + <<"messages.dropped.no_subscribers">> => #{ + type => <<"integer">>, + description => <<"Number of messages dropped due to no subscribers">>}, + <<"messages.forward">> => #{ + type => <<"integer">>, + description => <<"Number of messages forwarded to other nodes">>}, + <<"messages.publish">> => #{ + type => <<"integer">>, + description => <<"Number of messages published in addition to system messages">>}, + <<"messages.qos0.received">> => #{ + type => <<"integer">>, + description => <<"Number of QoS 0 messages received from clients">>}, + <<"messages.qos1.received">> => #{ + type => <<"integer">>, + description => <<"Number of QoS 1 messages received from clients">>}, + <<"messages.qos2.received">> => #{ + type => <<"integer">>, + description => <<"Number of QoS 2 messages received from clients">>}, + <<"messages.qos0.sent">> => #{ + type => <<"integer">>, + description => <<"Number of QoS 0 messages sent to clients">>}, + <<"messages.qos1.sent">> => #{ + type => <<"integer">>, + description => <<"Number of QoS 1 messages sent to clients">>}, + <<"messages.qos2.sent">> => #{ + type => <<"integer">>, + description => <<"Number of QoS 2 messages sent to clients">>}, + <<"messages.received">> => #{ + type => <<"integer">>, + description => <<"Number of messages received from the client, equal to the sum of messages.qos0.received,messages.qos1.received and messages.qos2.received">>}, + <<"messages.sent">> => #{ + type => <<"integer">>, + description => <<"Number of messages sent to the client, equal to the sum of messages.qos0.sent,messages.qos1.sent and messages.qos2.sent">>}, + <<"messages.retained">> => #{ + type => <<"integer">>, + description => <<"Number of retained messages stored by EMQ X Broker">>}, + <<"messages.acked">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBACK and PUBREC packet">>}, + <<"packets.received">> => #{ + type => <<"integer">>, + description => <<"Number of received packet">>}, + <<"packets.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent packet">>}, + <<"packets.connect.received">> => #{ + type => <<"integer">>, + description => <<"Number of received CONNECT packet">>}, + <<"packets.connack.auth_error">> => #{ + type => <<"integer">>, + description => <<"Number of received CONNECT packet with failed authentication">>}, + <<"packets.connack.error">> => #{ + type => <<"integer">>, + description => <<"Number of received CONNECT packet with unsuccessful connections">>}, + <<"packets.connack.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent CONNACK packet">>}, + <<"packets.publish.received">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBLISH packet">>}, + <<"packets.publish.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent PUBLISH packet">>}, + <<"packets.publish.inuse">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBLISH packet with occupied identifiers">>}, + <<"packets.publish.auth_error">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBLISH packets with failed the ACL check">>}, + <<"packets.publish.error">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBLISH packet that cannot be published">>}, + <<"packets.publish.dropped">> => #{ + type => <<"integer">>, + description => <<"Number of messages discarded due to the receiving limit">>}, + <<"packets.puback.received">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBACK packet">>}, + <<"packets.puback.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent PUBACK packet">>}, + <<"packets.puback.inuse">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBACK packet with occupied identifiers">>}, + <<"packets.puback.missed">> => #{ + type => <<"integer">>, + description => <<"Number of received packet with identifiers.">>}, + <<"packets.pubrec.received">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBREC packet">>}, + <<"packets.pubrec.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent PUBREC packet">>}, + <<"packets.pubrec.inuse">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBREC packet with occupied identifiers">>}, + <<"packets.pubrec.missed">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBREC packet with unknown identifiers">>}, + <<"packets.pubrel.received">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBREL packet">>}, + <<"packets.pubrel.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent PUBREL packet">>}, + <<"packets.pubrel.missed">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBREC packet with unknown identifiers">>}, + <<"packets.pubcomp.received">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBCOMP packet">>}, + <<"packets.pubcomp.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent PUBCOMP packet">>}, + <<"packets.pubcomp.inuse">> => #{ + type => <<"integer">>, + description => <<"Number of received PUBCOMP packet with occupied identifiers">>}, + <<"packets.pubcomp.missed">> => #{ + type => <<"integer">>, + description => <<"Number of missed PUBCOMP packet">>}, + <<"packets.subscribe.received">> => #{ + type => <<"integer">>, + description => <<"Number of received SUBSCRIBE packet">>}, + <<"packets.subscribe.error">> => #{ + type => <<"integer">>, + description => <<"Number of received SUBSCRIBE packet with failed subscriptions">>}, + <<"packets.subscribe.auth_error">> => #{ + type => <<"integer">>, + description => <<"Number of received SUBACK packet with failed ACL check">>}, + <<"packets.suback.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent SUBACK packet">>}, + <<"packets.unsubscribe.received">> => #{ + type => <<"integer">>, + description => <<"Number of received UNSUBSCRIBE packet">>}, + <<"packets.unsubscribe.error">> => #{ + type => <<"integer">>, + description => <<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>}, + <<"packets.unsuback.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent UNSUBACK packet">>}, + <<"packets.pingreq.received">> => #{ + type => <<"integer">>, + description => <<"Number of received PINGREQ packet">>}, + <<"packets.pingresp.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent PUBRESP packet">>}, + <<"packets.disconnect.received">> => #{ + type => <<"integer">>, + description => <<"Number of received DISCONNECT packet">>}, + <<"packets.disconnect.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent DISCONNECT packet">>}, + <<"packets.auth.received">> => #{ + type => <<"integer">>, + description => <<"Number of received AUTH packet">>}, + <<"packets.auth.sent">> => #{ + type => <<"integer">>, + description => <<"Number of sent AUTH packet">>}, + <<"rules.matched">> => #{ + type => <<"integer">>, + description => <<"Number of rule matched">>}, + <<"session.created">> => #{ + type => <<"integer">>, + description => <<"Number of sessions created">>}, + <<"session.discarded">> => #{ + type => <<"integer">>, + description => <<"Number of sessions dropped because Clean Session or Clean Start is true">>}, + <<"session.resumed">> => #{ + type => <<"integer">>, + description => <<"Number of sessions resumed because Clean Session or Clean Start is false">>}, + <<"session.takeovered">> => #{ + type => <<"integer">>, + description => <<"Number of sessions takeovered because Clean Session or Clean Start is false">>}, + <<"session.terminated">> => #{ + type => <<"integer">>, + description => <<"Number of terminated sessions">>}}, + {DefinitionName, DefinitionProperties}. +metrics_api() -> + Metadata = #{ + get => #{ + description => "EMQ X metrics", + responses => #{ + <<"200">> => #{ + schema => cowboy_swagger:schema(<<"metrics">>)}}}}, + {"/metrics", Metadata, list}. + +%%%============================================================================================== +%% api apply +list(get, _) -> + Response = emqx_json:encode(emqx_mgmt:get_metrics()), + {200, Response}. diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 3cb38c3a7..1aaecffdc 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -20,7 +20,9 @@ -export([api_spec/0]). -export([ nodes/2 - , node/2]). + , node/2 + , node_metrics/2 + , node_stats/2]). -include_lib("emqx/include/emqx.hrl"). @@ -29,9 +31,13 @@ api_spec() -> apis() -> [ nodes_api() - , node_api()]. + , node_api() + , node_metrics_api() + , node_stats_api()]. schemas() -> + %% notice: node api used schema metrics and stats + %% see these schema in emqx_mgmt_api_metrics emqx_mgmt_api_status [node_schema()]. node_schema() -> @@ -121,15 +127,60 @@ node_api() -> schema => cowboy_swagger:schema(<<"node">>)}}}}, {"/nodes/:node_name", Metadata, node}. +node_metrics_api() -> + Metadata = #{ + get => #{ + description => "Get node metrics", + parameters => [#{ + name => node_name, + in => path, + description => "node name", + type => string, + required => true, + default => node()}], + responses => #{ + <<"400">> => + emqx_mgmt_util:not_found_schema(<<"Node error">>, [<<"SOURCE_ERROR">>]), + <<"200">> => #{ + description => <<"Get EMQ X Node Metrics">>, + schema => cowboy_swagger:schema(<<"metrics">>)}}}}, + {"/nodes/:node_name/metrics", Metadata, node_metrics}. + +node_stats_api() -> + Metadata = #{ + get => #{ + description => "Get node stats", + parameters => [#{ + name => node_name, + in => path, + description => "node name", + type => string, + required => true, + default => node()}], + responses => #{ + <<"400">> => + emqx_mgmt_util:not_found_schema(<<"Node error">>, [<<"SOURCE_ERROR">>]), + <<"200">> => #{ + description => <<"Get EMQ X Node Stats">>, + schema => cowboy_swagger:schema(<<"stats">>)}}}}, + {"/nodes/:node_name/stats", Metadata, node_metrics}. + %%%============================================================================================== %% parameters trans nodes(get, _Request) -> list(#{}). node(get, Request) -> - NodeName = cowboy_req:binding(node_name, Request), - Node = binary_to_atom(NodeName, utf8), - get_node(#{node => Node}). + Params = node_name_path_parameter(Request), + get_node(Params). + +node_metrics(get, Request) -> + Params = node_name_path_parameter(Request), + get_metrics(Params). + +node_stats(get, Request) -> + Params = node_name_path_parameter(Request), + get_stats(Params). %%%============================================================================================== %% api apply @@ -147,8 +198,29 @@ get_node(#{node := Node}) -> {200, Response} end. +get_metrics(#{node := Node}) -> + case emqx_mgmt:get_metrics(Node) of + {error, _} -> + {400, emqx_json:encode(#{code => 'SOURCE_ERROR', reason => <<"rpc_failed">>})}; + Metrics -> + {200, emqx_json:encode(Metrics)} + end. + +get_stats(#{node := Node}) -> + case emqx_mgmt:get_stats(Node) of + {error, _} -> + {400, emqx_json:encode(#{code => 'SOURCE_ERROR', reason => <<"rpc_failed">>})}; + Stats -> + {200, emqx_json:encode(Stats)} + end. + %%============================================================================================================ %% internal function +node_name_path_parameter(Request) -> + NodeName = cowboy_req:binding(node_name, Request), + Node = binary_to_atom(NodeName, utf8), + #{node => Node}. + format(_Node, Info = #{memory_total := Total, memory_used := Used}) -> {ok, SysPathBinary} = file:get_cwd(), SysPath = list_to_binary(SysPathBinary), diff --git a/apps/emqx_management/src/emqx_mgmt_api_stats.erl b/apps/emqx_management/src/emqx_mgmt_api_stats.erl index e57c3dc0e..55c24189a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_stats.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_stats.erl @@ -13,33 +13,93 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- - -module(emqx_mgmt_api_stats). --rest_api(#{name => list_stats, - method => 'GET', - path => "/stats/", - func => list, - descr => "A list of stats of all nodes in the cluster"}). +-behavior(minirest_api). --rest_api(#{name => lookup_node_stats, - method => 'GET', - path => "/nodes/:atom:node/stats/", - func => lookup, - descr => "A list of stats of a node"}). +-export([api_spec/0]). --export([ list/2 - , lookup/2 - ]). +-export([list/2]). -%% List stats of all nodes -list(Bindings, _Params) when map_size(Bindings) == 0 -> - emqx_mgmt:return({ok, [#{node => Node, stats => maps:from_list(Stats)} - || {Node, Stats} <- emqx_mgmt:get_stats()]}). +api_spec() -> + {stats_api(), stats_schema()}. -%% List stats of a node -lookup(#{node := Node}, _Params) -> - case emqx_mgmt:get_stats(Node) of - {error, Reason} -> emqx_mgmt:return({error, Reason}); - Stats -> emqx_mgmt:return({ok, maps:from_list(Stats)}) - end. +stats_schema() -> + DefinitionName = <<"stats">>, + DefinitionProperties = #{ + <<"connections.count">> => #{ + type => <<"integer">>, + description => <<"Number of current connections">>}, + <<"connections.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of connections">>}, + <<"channels.count">> => #{ + type => <<"integer">>, + description => <<"sessions.count">>}, + <<"channels.max">> => #{ + type => <<"integer">>, + description => <<"session.max">>}, + <<"sessions.count">> => #{ + type => <<"integer">>, + description => <<"Number of current sessions">>}, + <<"sessions.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of sessions">>}, + <<"topics.count">> => #{ + type => <<"integer">>, + description => <<"Number of current topics">>}, + <<"topics.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of topics">>}, + <<"suboptions.count">> => #{ + type => <<"integer">>, + description => <<"subscriptions.count">>}, + <<"suboptions.max">> => #{ + type => <<"integer">>, + description => <<"subscriptions.max">>}, + <<"subscribers.count">> => #{ + type => <<"integer">>, + description => <<"Number of current subscribers">>}, + <<"subscribers.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of subscribers">>}, + <<"subscriptions.count">> => #{ + type => <<"integer">>, + description => <<"Number of current subscriptions, including shared subscriptions">>}, + <<"subscriptions.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of subscriptions">>}, + <<"subscriptions.shared.count">> => #{ + type => <<"integer">>, + description => <<"Number of current shared subscriptions">>}, + <<"subscriptions.shared.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of shared subscriptions">>}, + <<"routes.count">> => #{ + type => <<"integer">>, + description => <<"Number of current routes">>}, + <<"routes.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of routes">>}, + <<"retained.count">> => #{ + type => <<"integer">>, + description => <<"Number of currently retained messages">>}, + <<"retained.max">> => #{ + type => <<"integer">>, + description => <<"Historical maximum number of retained messages">>}}, + [{DefinitionName, DefinitionProperties}]. + +stats_api() -> + Metadata = #{ + get => #{ + description => "EMQ X stats", + responses => #{ + <<"200">> => #{ + schema => cowboy_swagger:schema(<<"stats">>)}}}}, + [{"/stats", Metadata, list}]. + +%%%============================================================================================== +%% api apply +list(get, _Request) -> + Response = emqx_json:encode(emqx_mgmt:get_stats()), + {200, Response}. diff --git a/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl new file mode 100644 index 000000000..7bd5891c8 --- /dev/null +++ b/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl @@ -0,0 +1,52 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_metrics_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ekka_mnesia:start(), + emqx_mgmt_auth:mnesia(boot), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([emqx_management]). + +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), + ok; +set_special_configs(_App) -> + ok. + +t_metrics_api(_) -> + MetricsPath = emqx_mgmt_api_test_util:api_path(["metrics"]), + SystemMetrics = emqx_mgmt:get_metrics(), + {ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath), + Metrics = emqx_json:decode(MetricsResponse, [return_maps]), + ?assertEqual(erlang:length(maps:keys(SystemMetrics)), erlang:length(maps:keys(Metrics))), + Fun = + fun(Key) -> + ?assertEqual(maps:get(Key, SystemMetrics), maps:get(atom_to_binary(Key, utf8), Metrics)) + end, + lists:foreach(Fun, maps:keys(SystemMetrics)). diff --git a/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl index 52d2bf626..f0829b7fb 100644 --- a/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl @@ -20,11 +20,6 @@ -include_lib("eunit/include/eunit.hrl"). --define(APP, emqx_management). - --define(SERVER, "http://127.0.0.1:8081"). --define(BASE_PATH, "/api/v5"). - all() -> emqx_ct:all(?MODULE). @@ -54,5 +49,29 @@ t_nodes_api(_) -> NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]), {ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath), - NodeNameResponse = binary_to_atom(maps:get(<<"node">>, emqx_json:decode(NodeInfo, [return_maps])), utf8), + NodeNameResponse = + binary_to_atom(maps:get(<<"node">>, emqx_json:decode(NodeInfo, [return_maps])), utf8), ?assertEqual(node(), NodeNameResponse). + +t_node_stats_api() -> + StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]), + SystemStats= emqx_mgmt:get_stats(), + {ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath), + Stats = emqx_json:decode(StatsResponse, [return_maps]), + Fun = + fun(Key) -> + ?assertEqual(maps:get(Key, SystemStats), maps:get(atom_to_binary(Key, utf8), Stats)) + end, + lists:foreach(Fun, maps:keys(SystemStats)). + +t_node_metrics_api() -> + MetricsPath = + emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]), + SystemMetrics= emqx_mgmt:get_metrics(), + {ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath), + Metrics = emqx_json:decode(MetricsResponse, [return_maps]), + Fun = + fun(Key) -> + ?assertEqual(maps:get(Key, SystemMetrics), maps:get(atom_to_binary(Key, utf8), Metrics)) + end, + lists:foreach(Fun, maps:keys(SystemMetrics)). diff --git a/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl new file mode 100644 index 000000000..dbbca9d43 --- /dev/null +++ b/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl @@ -0,0 +1,52 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_stats_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ekka_mnesia:start(), + emqx_mgmt_auth:mnesia(boot), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([emqx_management]). + +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), + ok; +set_special_configs(_App) -> + ok. + +t_stats_api(_) -> + StatsPath = emqx_mgmt_api_test_util:api_path(["stats"]), + SystemStats = emqx_mgmt:get_stats(), + {ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath), + Stats = emqx_json:decode(StatsResponse, [return_maps]), + ?assertEqual(erlang:length(maps:keys(SystemStats)), erlang:length(maps:keys(Stats))), + Fun = + fun(Key) -> + ?assertEqual(maps:get(Key, SystemStats), maps:get(atom_to_binary(Key, utf8), Stats)) + end, + lists:foreach(Fun, maps:keys(SystemStats)). From 1bfa6ead4218324f76d139956233540fb5877665 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 14 Jul 2021 11:46:04 +0800 Subject: [PATCH 177/379] refactor: publish api; add: batch schema util function --- .../src/emqx_mgmt_api_publish.erl | 161 ++++++++++++++++++ apps/emqx_management/src/emqx_mgmt_util.erl | 36 +++- .../test/emqx_mgmt_publish_api_SUITE.erl | 92 ++++++++++ 3 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 apps/emqx_management/src/emqx_mgmt_api_publish.erl create mode 100644 apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl new file mode 100644 index 000000000..a3305bc55 --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -0,0 +1,161 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_api_publish). +%% API +-include_lib("emqx/include/emqx.hrl"). + +-behavior(minirest_api). + +-export([api_spec/0]). + +-export([ publish/2 + , publish_batch/2]). + +api_spec() -> + { + [publish_api(), publish_batch_api()], + [request_message_schema(), mqtt_message_schema()] + }. + +publish_api() -> + MeteData = #{ + post => #{ + description => "publish", + parameters => [#{ + name => message, + in => body, + required => true, + schema => minirest:ref(<<"request_message">>) + }], + responses => #{ + <<"200">> => #{ + description => <<"publish ok">>, + schema => minirest:ref(<<"message">>)}}}}, + {"/publish", MeteData, publish}. + +publish_batch_api() -> + MeteData = #{ + post => #{ + description => "publish", + parameters => [#{ + name => message, + in => body, + required => true, + schema =>#{ + type => array, + items => minirest:ref(<<"request_message">>)} + }], + responses => #{ + <<"200">> => #{ + description => <<"publish result">>, + schema => #{ + type => array, + items => minirest:ref(<<"message">>)}}}}}, + {"/publish_batch", MeteData, publish_batch}. + +request_message_schema() -> + {<<"request_message">>, maps:without([<<"id">>], message_def())}. + +mqtt_message_schema() -> + {<<"message">>, message_def()}. + +message_def() -> + #{ + <<"id">> => #{ + type => <<"string">>, + description => <<"Message ID">>}, + <<"topic">> => #{ + type => <<"string">>, + description => <<"Topic">>}, + <<"qos">> => #{ + type => <<"integer">>, + enum => [0, 1, 2], + description => <<"Qos">>}, + <<"payload">> => #{ + type => <<"string">>, + description => <<"Topic">>}, + <<"from">> => #{ + type => <<"string">>, + description => <<"Message from">>}, + <<"flag">> => #{ + type => <<"object">>, + description => <<"Message flag">>, + properties => #{ + <<"sys">> => #{ + type => <<"boolean">>, + default => false, + description => <<"System message flag, nullable, default false">>}, + <<"dup">> => #{ + type => <<"boolean">>, + default => false, + description => <<"Dup message flag, nullable, default false">>}, + <<"retain">> => #{ + type => <<"boolean">>, + default => false, + description => <<"Retain message flag, nullable, default false">>}}} + }. + +publish(post, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + Message = message(emqx_json:decode(Body, [return_maps])), + _ = emqx_mgmt:publish(Message), + {200, emqx_json:encode(format_message(Message))}. + +publish_batch(post, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + Messages = messages(emqx_json:decode(Body, [return_maps])), + _ = [emqx_mgmt:publish(Message) || Message <- Messages], + ResponseBody = emqx_json:encode(format_message(Messages)), + {200, ResponseBody}. + +message(Map) -> + From = maps:get(<<"from">>, Map, http_api), + QoS = maps:get(<<"qos">>, Map, 0), + Topic = maps:get(<<"topic">>, Map), + Payload = maps:get(<<"payload">>, Map), + Flags = flags(Map), + emqx_message:make(From, QoS, Topic, Payload, Flags, #{}). + +flags(Map) -> + Flags = maps:get(<<"flags">>, Map, #{}), + Retain = maps:get(<<"retain">>, Flags, false), + Sys = maps:get(<<"sys">>, Flags, false), + Dup = maps:get(<<"dup">>, Flags, false), + #{ + retain => Retain, + sys => Sys, + dup => Dup + }. + +messages(List) -> + [message(MessageMap) || MessageMap <- List]. + +format_message(Messages) when is_list(Messages)-> + [format_message(Message) || Message <- Messages]; +format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags}) -> + #{ + id => emqx_guid:to_hexstr(ID), + qos => Qos, + topic => Topic, + payload => Payload, + flag => Flags, + from => to_binary(From) + }. + +to_binary(Data) when is_binary(Data) -> + Data; +to_binary(Data) -> + list_to_binary(io_lib:format("~p", [Data])). diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 4197973e7..6ba7cb93a 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -21,11 +21,13 @@ , kmg/1 , ntoa/1 , merge_maps/2 - , not_found_schema/1 - , not_found_schema/2 , batch_operation/3 ]). +-export([ not_found_schema/1 + , not_found_schema/2 + , batch_response_schema/1]). + -export([urldecode/1]). -define(KB, 1024). @@ -80,6 +82,8 @@ merge_maps(Default, New) -> urldecode(S) -> emqx_http_lib:uri_decode(S). +%%%============================================================================================== +%% schema util not_found_schema(Description) -> not_found_schema(Description, ["RESOURCE_NOT_FOUND"]). @@ -96,6 +100,34 @@ not_found_schema(Description, Enum) -> type => string}}} }. +batch_response_schema(DefName) -> + #{ + type => object, + properties => #{ + success => #{ + type => integer, + description => <<"Success count">>}, + failed => #{ + type => integer, + description => <<"Failed count">>}, + detail => #{ + type => array, + description => <<"Failed object & reason">>, + items => #{ + type => object, + properties => + #{ + data => minirest:ref(DefName), + reason => #{ + type => <<"string">> + } + } + } + } + } + }. + +%%%============================================================================================== batch_operation(Module, Function, ArgsList) -> Failed = batch_operation(Module, Function, ArgsList, []), Len = erlang:length(Failed), diff --git a/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl new file mode 100644 index 000000000..9ecb1a11b --- /dev/null +++ b/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl @@ -0,0 +1,92 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_publish_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-define(CLIENTID, <<"api_clientid">>). +-define(USERNAME, <<"api_username">>). + +-define(TOPIC1, <<"api_topic1">>). +-define(TOPIC2, <<"api_topic2">>). + + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ekka_mnesia:start(), + emqx_mgmt_auth:mnesia(boot), + emqx_ct_helpers:start_apps([emqx_management], fun set_special_configs/1), + Config. + + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([emqx_management]). + +set_special_configs(emqx_management) -> + emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], + applications =>[#{id => "admin", secret => "public"}]}), + ok; +set_special_configs(_App) -> + ok. + +t_publish_api(_) -> + {ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}), + {ok, _} = emqtt:connect(Client), + {ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1), + {ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2), + Payload = <<"hello">>, + Path = emqx_mgmt_api_test_util:api_path(["publish"]), + Auth = emqx_mgmt_api_test_util:auth_header_(), + Body = #{topic => ?TOPIC1, payload => Payload}, + {ok, _} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body), + ?assertEqual(receive_assert(?TOPIC1, 0, Payload), ok), + emqtt:disconnect(Client). + +t_publish_batch_api(_) -> + {ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}), + {ok, _} = emqtt:connect(Client), + {ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1), + {ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2), + Payload = <<"hello">>, + Path = emqx_mgmt_api_test_util:api_path(["publish_batch"]), + Auth = emqx_mgmt_api_test_util:auth_header_(), + Body =[#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}], + {ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body), + ResponseMap = emqx_json:decode(Response, [return_maps]), + ?assertEqual(2, erlang:length(ResponseMap)), + ?assertEqual(receive_assert(?TOPIC1, 0, Payload), ok), + ?assertEqual(receive_assert(?TOPIC2, 0, Payload), ok), + emqtt:disconnect(Client). + +receive_assert(Topic, Qos, Payload) -> + receive + {publish, Message} -> + ReceiveTopic = maps:get(topic, Message), + ReceiveQos = maps:get(qos, Message), + ReceivePayload = maps:get(payload, Message), + ?assertEqual(ReceiveTopic , Topic), + ?assertEqual(ReceiveQos , Qos), + ?assertEqual(ReceivePayload , Payload), + ok + after 5000 -> + timeout + end. + From 543f2c78c549ed5c3d69dc0380ca28f2d28c21ed Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 20:28:12 +0800 Subject: [PATCH 178/379] fix(ekka): cluster cannot get started with the new config --- apps/emqx/src/emqx_schema.erl | 42 ++++++++++++++++++++++++++++-- apps/emqx_authn/src/emqx_authn.erl | 6 ++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 62b4c2a21..f762104f7 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -101,7 +101,7 @@ fields("static") -> fields("mcast") -> [ {"addr", t(string(), undefined, "239.192.0.1")} - , {"ports", t(comma_separated_list(), undefined, "4369")} + , {"ports", t(hoconsc:array(integer()), undefined, [4369, 4370])} , {"iface", t(string(), undefined, "0.0.0.0")} , {"ttl", t(integer(), undefined, 255)} , {"loop", t(boolean(), undefined, true)} @@ -523,12 +523,19 @@ base_listener() -> , {"rate_limit", ref("rate_limit")} ]. -translations() -> ["kernel"]. +translations() -> ["ekka", "kernel"]. + +translation("ekka") -> + [ {"cluster_discovery", fun tr_cluster__discovery/1}]; translation("kernel") -> [ {"logger_level", fun tr_logger_level/1} , {"logger", fun tr_logger/1}]. +tr_cluster__discovery(Conf) -> + Strategy = conf_get("cluster.discovery_strategy", Conf), + {Strategy, filter(options(Strategy, Conf))}. + tr_logger_level(Conf) -> conf_get("log.primary_level", Conf). tr_logger(Conf) -> @@ -806,3 +813,34 @@ to_erl_cipher_suite(Str) -> {error, Reason} -> error({invalid_cipher, Reason}); Cipher -> Cipher end. + +options(static, Conf) -> + [{seeds, [list_to_atom(S) || S <- conf_get("cluster.static.seeds", Conf, [])]}]; +options(mcast, Conf) -> + {ok, Addr} = inet:parse_address(conf_get("cluster.mcast.addr", Conf)), + {ok, Iface} = inet:parse_address(conf_get("cluster.mcast.iface", Conf)), + Ports = conf_get("cluster.mcast.ports", Conf), + [{addr, Addr}, {ports, Ports}, {iface, Iface}, + {ttl, conf_get("cluster.mcast.ttl", Conf, 1)}, + {loop, conf_get("cluster.mcast.loop", Conf, true)}]; +options(dns, Conf) -> + [{name, conf_get("cluster.dns.name", Conf)}, + {app, conf_get("cluster.dns.app", Conf)}]; +options(etcd, Conf) -> + Namespace = "cluster.etcd.ssl", + SslOpts = fun(C) -> + Options = keys(Namespace, C), + lists:map(fun(Key) -> {list_to_atom(Key), conf_get([Namespace, Key], Conf)} end, Options) end, + [{server, conf_get("cluster.etcd.server", Conf)}, + {prefix, conf_get("cluster.etcd.prefix", Conf, "emqxcl")}, + {node_ttl, conf_get("cluster.etcd.node_ttl", Conf, 60)}, + {ssl_options, filter(SslOpts(Conf))}]; +options(k8s, Conf) -> + [{apiserver, conf_get("cluster.k8s.apiserver", Conf)}, + {service_name, conf_get("cluster.k8s.service_name", Conf)}, + {address_type, conf_get("cluster.k8s.address_type", Conf, ip)}, + {app_name, conf_get("cluster.k8s.app_name", Conf)}, + {namespace, conf_get("cluster.k8s.namespace", Conf)}, + {suffix, conf_get("cluster.k8s.suffix", Conf, "")}]; +options(manual, _Conf) -> + []. diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 611b47cc2..731ac31fe 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -49,6 +49,7 @@ -export([mnesia/1]). -boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). -define(CHAIN_TAB, emqx_authn_chain). @@ -69,7 +70,10 @@ mnesia(boot) -> {record_name, chain}, {local_content, true}, {attributes, record_info(fields, chain)}, - {storage_properties, StoreProps}]). + {storage_properties, StoreProps}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies). enable() -> case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of From c834494113a482a0014145b13957aa5b923b8341 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 15 Jul 2021 20:39:06 +0800 Subject: [PATCH 179/379] fix(cli): CLI of emqx_gateway_cli broken --- apps/emqx_gateway/src/emqx_gateway_cli.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index beb3e5eae..2fbc96bce 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -39,7 +39,10 @@ unload() -> lists:foreach(fun(Cmd) -> emqx_ctl:unregister_command(Cmd) end, Cmds). is_cmd(Fun) -> - not lists:member(Fun, [init, load, module_info]). + case atom_to_list(Fun) of + "gateway" ++ _ -> true; + _ -> false + end. %%-------------------------------------------------------------------- From c3d24db642abb6286eb78ee7c6ed658c91342e17 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 16 Jul 2021 14:07:27 +0800 Subject: [PATCH 180/379] fix(authz): update emqx_authz for new config --- apps/emqx_authz/src/emqx_authz.erl | 20 +++--- apps/emqx_authz/src/emqx_authz_app.erl | 2 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 71 +++++++++++-------- .../test/emqx_authz_mongo_SUITE.erl | 41 +++++------ .../test/emqx_authz_mysql_SUITE.erl | 40 +++++------ .../test/emqx_authz_pgsql_SUITE.erl | 40 +++++------ .../test/emqx_authz_redis_SUITE.erl | 28 +++----- 7 files changed, 114 insertions(+), 128 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 78aa47d91..bb5991748 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -26,7 +26,7 @@ , compile/1 , lookup/0 , update/1 - , authorize/5 + , authorize/4 , match/4 ]). @@ -36,19 +36,16 @@ register_metrics() -> init() -> ok = register_metrics(), - Rules = emqx_config:get([emqx_authz, rules], []), - NRules = [compile(Rule) || Rule <- Rules], - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, []}, -1). lookup() -> emqx_config:get([emqx_authz, rules], []). update(Rules) -> emqx_config:put([emqx_authz], #{rules => Rules}), - NRules = [compile(Rule) || Rule <- Rules], Action = find_action_in_hooks(), ok = emqx_hooks:del('client.authorize', Action), - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1), + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, []}, -1), ok = emqx_acl_cache:empty_acl_cache(). %%-------------------------------------------------------------------- @@ -147,12 +144,11 @@ b2l(B) when is_binary(B) -> binary_to_list(B). %%-------------------------------------------------------------------- %% @doc Check AuthZ --spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) - -> {stop, allow} | {ok, deny}). -authorize(#{username := Username, - peerhost := IpAddress - } = Client, PubSub, Topic, _DefaultResult, Rules) -> - case do_authorize(Client, PubSub, Topic, Rules) of +-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), + emqx_permission_rule:acl_result()) -> {stop, allow} | {ok, deny}). +authorize(#{username := Username, peerhost := IpAddress} = Client, + PubSub, Topic, _DefaultResult) -> + case do_authorize(Client, PubSub, Topic, [compile(Rule) || Rule <- lookup()]) of {matched, allow} -> ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), emqx_metrics:inc(?AUTHZ_METRICS(allow)), diff --git a/apps/emqx_authz/src/emqx_authz_app.erl b/apps/emqx_authz/src/emqx_authz_app.erl index dcce015c7..460d7cbf9 100644 --- a/apps/emqx_authz/src/emqx_authz_app.erl +++ b/apps/emqx_authz/src/emqx_authz_app.erl @@ -11,7 +11,7 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_authz_sup:start_link(), - %ok = emqx_authz:init(), + ok = emqx_authz:init(), {ok, Sup}. stop(_State) -> diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 66b2e62de..81574330e 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -29,23 +29,16 @@ groups() -> []. init_per_suite(Config) -> - ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), + ok = emqx_ct_helpers:start_apps([emqx_authz]), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), + emqx_config:put([emqx_authz], #{rules => []}), Config. end_per_suite(_Config) -> file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), emqx_ct_helpers:stop_apps([emqx_authz]). -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, true), - application:set_env(emqx, enable_acl_cache, false), - ok; -set_special_configs(emqx_authz) -> - emqx_config:put([emqx_authz], #{rules => []}), - ok; -set_special_configs(_App) -> - ok. - -define(RULE1, #{principal => all, topics => [<<"#">>], action => all, @@ -86,7 +79,7 @@ t_compile(_) -> action => all, principal => all, topics => [['#']] - },emqx_authz:compile(?RULE1)), + }, emqx_authz:compile(?RULE1)), ?assertEqual(#{permission => allow, action => all, principal => @@ -121,44 +114,62 @@ t_compile(_) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, ClientInfo2 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {192,168,0,10} + peerhost => {192,168,0,10}, + zone => default, + listener => mqtt_tcp }, ClientInfo3 = #{clientid => <<"test">>, username => <<"fake">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, ClientInfo4 = #{clientid => <<"fake">>, username => <<"test">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, - Rules1 = [emqx_authz:compile(Rule) || Rule <- [?RULE1, ?RULE2]], - Rules2 = [emqx_authz:compile(Rule) || Rule <- [?RULE2, ?RULE1]], - Rules3 = [emqx_authz:compile(Rule) || Rule <- [?RULE3, ?RULE4]], - Rules4 = [emqx_authz:compile(Rule) || Rule <- [?RULE4, ?RULE1]], + Rules1 = [?RULE1, ?RULE2], + Rules2 = [?RULE2, ?RULE1], + Rules3 = [?RULE3, ?RULE4], + Rules4 = [?RULE4, ?RULE1], + emqx_config:put([emqx_authz], #{rules => []}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])), + emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules1}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules2}), ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules3}), ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules4}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules2}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), + emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules3}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules4}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules3}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)), + emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny)), + emqx_config:put([emqx_authz], #{rules => Rules4}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)), + emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny)), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index d2792e388..ab7ff548a 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -31,22 +31,10 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), - ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), - emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, true), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), - ok; -set_special_configs(emqx_authz) -> + ok = emqx_ct_helpers:start_apps([emqx_authz]), + ct:pal("---- emqx_hooks: ~p", [ets:tab2list(emqx_hooks)]), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), Rules = [#{config =>#{}, principal => all, collection => <<"fake">>, @@ -54,9 +42,12 @@ set_special_configs(emqx_authz) -> type => mongo} ], emqx_config:put([emqx_authz], #{rules => Rules}), - ok; -set_special_configs(_App) -> - ok. + Config. + +end_per_suite(_Config) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource). -define(RULE1,[#{<<"topics">> => [<<"#">>], <<"permission">> => <<"deny">>, @@ -78,15 +69,21 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10} + peerhost => {192,168,0,10}, + zone => default, + listener => mqtt_tcp }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, meck:expect(emqx_resource, query, fun(_, _) -> [] end), diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index a9acf5e36..0389ffd6e 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -31,31 +31,21 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), - ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), - emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), - ok; -set_special_configs(emqx_authz) -> + ok = emqx_ct_helpers:start_apps([emqx_authz]), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), Rules = [#{config =>#{}, principal => all, sql => <<"fake">>, type => mysql} ], emqx_config:put([emqx_authz], #{rules => Rules}), - ok; -set_special_configs(_App) -> - ok. + Config. + +end_per_suite(_Config) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource). -define(COLUMNS, [ <<"ipaddress">> , <<"username">> @@ -76,15 +66,21 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10} + peerhost => {192,168,0,10}, + zone => default, + listener => mqtt_tcp }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 03bec2415..5c9c32551 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -31,31 +31,21 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), - ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), - emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), - ok; -set_special_configs(emqx_authz) -> + ok = emqx_ct_helpers:start_apps([emqx_authz]), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), Rules = [#{config =>#{}, principal => all, sql => <<"fake">>, type => pgsql} ], emqx_config:put([emqx_authz], #{rules => Rules}), - ok; -set_special_configs(_App) -> - ok. + Config. + +end_per_suite(_Config) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource). -define(COLUMNS, [ {column, <<"ipaddress">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"username">>, meck, meck, meck, meck, meck, meck, meck} @@ -76,15 +66,21 @@ set_special_configs(_App) -> t_authz(_) -> ClientInfo1 = #{clientid => <<"test">>, username => <<"test">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, - peerhost => {192,168,0,10} + peerhost => {192,168,0,10}, + zone => default, + listener => mqtt_tcp }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, - peerhost => {127,0,0,1} + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 1e2264c29..233680884 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -31,31 +31,21 @@ groups() -> init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), - ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1), - Config. - -end_per_suite(_Config) -> - file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), - emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). - -set_special_configs(emqx) -> - application:set_env(emqx, allow_anonymous, true), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, acl_nomatch, deny), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")), - ok; -set_special_configs(emqx_authz) -> + ok = emqx_ct_helpers:start_apps([emqx_authz]), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), + emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), Rules = [#{config =>#{}, principal => all, cmd => <<"fake">>, type => redis} ], emqx_config:put([emqx_authz], #{rules => Rules}), - ok; -set_special_configs(_App) -> - ok. + Config. + +end_per_suite(_Config) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource). -define(RULE1, [<<"test/%u">>, <<"publish">>]). -define(RULE2, [<<"test/%c">>, <<"publish">>]). From 6d871cc52f1e7b5ca6424e3a6496c7e6988db6e7 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 16 Jul 2021 18:04:12 +0800 Subject: [PATCH 181/379] fix(authz): resources not created when authz started --- apps/emqx/src/emqx_config_handler.erl | 30 +++++----- apps/emqx_authz/src/emqx_authz.erl | 57 ++++++++++++------- apps/emqx_authz/src/emqx_authz_mongo.erl | 2 +- apps/emqx_authz/src/emqx_authz_mysql.erl | 2 +- apps/emqx_authz/src/emqx_authz_pgsql.erl | 2 +- apps/emqx_authz/src/emqx_authz_redis.erl | 2 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 54 +++++++----------- .../test/emqx_authz_mongo_SUITE.erl | 6 +- .../test/emqx_authz_mysql_SUITE.erl | 6 +- .../test/emqx_authz_pgsql_SUITE.erl | 6 +- .../test/emqx_authz_redis_SUITE.erl | 6 +- 11 files changed, 89 insertions(+), 84 deletions(-) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index 6a4ac8703..3f3c43524 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -113,12 +113,9 @@ do_update_config([], Handlers, OldConf, UpdateReq) -> call_handle_update_config(Handlers, OldConf, UpdateReq); do_update_config([ConfKey | ConfKeyPath], Handlers, OldConf, UpdateReq) -> SubOldConf = get_sub_config(ConfKey, OldConf), - case maps:find(ConfKey, Handlers) of - error -> throw({handler_not_found, ConfKey}); - {ok, SubHandlers} -> - NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldConf, UpdateReq), - call_handle_update_config(Handlers, OldConf, #{bin(ConfKey) => NewUpdateReq}) - end. + SubHandlers = maps:get(ConfKey, Handlers, #{}), + NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldConf, UpdateReq), + call_handle_update_config(Handlers, OldConf, #{bin(ConfKey) => NewUpdateReq}). get_sub_config(_, undefined) -> undefined; @@ -131,7 +128,7 @@ call_handle_update_config(Handlers, OldConf, UpdateReq) -> HandlerName = maps:get(?MOD, Handlers, undefined), case erlang:function_exported(HandlerName, handle_update_config, 2) of true -> HandlerName:handle_update_config(UpdateReq, OldConf); - false -> UpdateReq %% the default behaviour is overwriting the old config + false -> merge_to_old_config(UpdateReq, OldConf) end. %% callbacks for the top-level handler @@ -139,11 +136,15 @@ handle_update_config(UpdateReq, OldConf) -> FullRawConf = merge_to_old_config(UpdateReq, OldConf), {maps:keys(UpdateReq), FullRawConf}. -%% default callback of config handlers -merge_to_old_config(UpdateReq, undefined) -> - merge_to_old_config(UpdateReq, #{}); -merge_to_old_config(UpdateReq, RawConf) -> - maps:merge(RawConf, UpdateReq). +%% The default callback of config handlers +%% the behaviour is overwriting the old config if: +%% 1. the old config is undefined +%% 2. either the old or the new config is not of map type +%% the behaviour is merging the new the config to the old config if they are maps. +merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) -> + maps:merge(RawConf, UpdateReq); +merge_to_old_config(UpdateReq, _RawConf) -> + UpdateReq. %%============================================================================ save_configs(RootKeys, RawConf) -> @@ -199,8 +200,9 @@ load_config_file() -> end, #{}, emqx:get_env(config_files, [])). emqx_override_conf_name() -> - filename:join([emqx:get_env(data_dir), "emqx_override.conf"]). - + File = filename:join([emqx:get_env(data_dir), "emqx_override.conf"]), + ok = filelib:ensure_dir(File), + File. to_richmap(Map) -> {ok, RichMap} = hocon:binary(jsx:encode(Map), #{format => richmap}), diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index bb5991748..e759cdfcf 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -15,6 +15,7 @@ %%-------------------------------------------------------------------- -module(emqx_authz). +-behaviour(emqx_config_handler). -include("emqx_authz.hrl"). -include_lib("emqx/include/logger.hrl"). @@ -23,30 +24,41 @@ -export([ register_metrics/0 , init/0 - , compile/1 + , init_rule/1 , lookup/0 , update/1 - , authorize/4 + , authorize/5 , match/4 ]). +-export([handle_update_config/2]). + +-define(CONF_KEY_PATH, [emqx_authz, rules]). + -spec(register_metrics() -> ok). register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS). init() -> ok = register_metrics(), - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, []}, -1). + emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), + NRules = [init_rule(Rule) || Rule <- lookup()], + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> - emqx_config:get([emqx_authz, rules], []). + emqx_config:get(?CONF_KEY_PATH, []). update(Rules) -> - emqx_config:put([emqx_authz], #{rules => Rules}), + emqx_config:update_config(?CONF_KEY_PATH, Rules). + +%% For now we only support re-creating the entire rule list +handle_update_config(Rules, _OldConf) -> + InitedRules = [init_rule(Rule) || Rule <- Rules], Action = find_action_in_hooks(), ok = emqx_hooks:del('client.authorize', Action), - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, []}, -1), - ok = emqx_acl_cache:empty_acl_cache(). + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [InitedRules]}, -1), + ok = emqx_acl_cache:drain_cache(), + Rules. %%-------------------------------------------------------------------- %% Internal functions @@ -74,27 +86,27 @@ create_resource(#{type := DB, error({load_config_error, Reason}) end. --spec(compile(rule()) -> rule()). -compile(#{topics := Topics, - action := Action, - permission := Permission, - principal := Principal +-spec(init_rule(rule()) -> rule()). +init_rule(#{topics := Topics, + action := Action, + permission := Permission, + principal := Principal } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> NTopics = [compile_topic(Topic) || Topic <- Topics], Rule#{principal => compile_principal(Principal), topics => NTopics }; -compile(#{principal := Principal, - type := DB +init_rule(#{principal := Principal, + type := DB } = Rule) when DB =:= redis; DB =:= mongo -> NRule = create_resource(Rule), NRule#{principal => compile_principal(Principal)}; -compile(#{principal := Principal, - type := DB, - sql := SQL +init_rule(#{principal := Principal, + type := DB, + sql := SQL } = Rule) when DB =:= mysql; DB =:= pgsql -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), @@ -144,11 +156,12 @@ b2l(B) when is_binary(B) -> binary_to_list(B). %%-------------------------------------------------------------------- %% @doc Check AuthZ --spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), - emqx_permission_rule:acl_result()) -> {stop, allow} | {ok, deny}). -authorize(#{username := Username, peerhost := IpAddress} = Client, - PubSub, Topic, _DefaultResult) -> - case do_authorize(Client, PubSub, Topic, [compile(Rule) || Rule <- lookup()]) of +-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules()) + -> {stop, allow} | {ok, deny}). +authorize(#{username := Username, + peerhost := IpAddress + } = Client, PubSub, Topic, _DefaultResult, Rules) -> + case do_authorize(Client, PubSub, Topic, Rules) of {matched, allow} -> ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), emqx_metrics:inc(?AUTHZ_METRICS(allow)), diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl index a32054997..c615582d4 100644 --- a/apps/emqx_authz/src/emqx_authz_mongo.erl +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -71,7 +71,7 @@ match(Client, PubSub, Topic, #{<<"simple_rule">> => Rule}, #{atom_key => true}, [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of + case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of true -> {matched, NPermission}; false -> nomatch end. diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index 0ab1418f2..980e9d5c6 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -90,7 +90,7 @@ match(Client, PubSub, Topic, #{<<"simple_rule">> => Rule}, #{atom_key => true}, [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of + case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of true -> {matched, NPermission}; false -> nomatch end. diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl index c990a29d3..607ba3afa 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -94,7 +94,7 @@ match(Client, PubSub, Topic, #{<<"simple_rule">> => Rule}, #{atom_key => true}, [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of + case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of true -> {matched, NPermission}; false -> nomatch end. diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 8d24b4534..43e06dd13 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -74,7 +74,7 @@ match(Client, PubSub, Topic, #{<<"simple_rule">> => Rule}, #{atom_key => true}, [simple_rule]), - case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of + case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of true -> {matched, allow}; false -> nomatch end. diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 81574330e..b9b15954c 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -30,9 +30,9 @@ groups() -> init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), - emqx_config:put([emqx_authz], #{rules => []}), + ok = emqx_config:update_config([zones, default, acl, cache, enable], false), + ok = emqx_config:update_config([zones, default, acl, enable], true), + emqx_authz:update([]), Config. end_per_suite(_Config) -> @@ -74,19 +74,19 @@ end_per_suite(_Config) -> %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ -t_compile(_) -> +t_init_rule(_) -> ?assertEqual(#{permission => deny, action => all, principal => all, topics => [['#']] - }, emqx_authz:compile(?RULE1)), + }, emqx_authz:init_rule(?RULE1)), ?assertEqual(#{permission => allow, action => all, principal => #{ipaddress => {{127,0,0,1},{127,0,0,1},32}}, topics => [#{eq => ['#']}, #{eq => ['+']}] - }, emqx_authz:compile(?RULE2)), + }, emqx_authz:init_rule(?RULE2)), ?assertMatch( #{permission := allow, action := publish, @@ -96,7 +96,7 @@ t_compile(_) -> ] }, topics := [[<<"test">>]] - }, emqx_authz:compile(?RULE3)), + }, emqx_authz:init_rule(?RULE3)), ?assertMatch( #{permission := deny, action := publish, @@ -108,7 +108,7 @@ t_compile(_) -> topics := [#{pattern := [<<"%u">>]}, #{pattern := [<<"%c">>]} ] - }, emqx_authz:compile(?RULE4)), + }, emqx_authz:init_rule(?RULE4)), ok. t_authz(_) -> @@ -137,39 +137,29 @@ t_authz(_) -> listener => mqtt_tcp }, - Rules1 = [?RULE1, ?RULE2], - Rules2 = [?RULE2, ?RULE1], - Rules3 = [?RULE3, ?RULE4], - Rules4 = [?RULE4, ?RULE1], + Rules1 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE1, ?RULE2]], + Rules2 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE2, ?RULE1]], + Rules3 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE3, ?RULE4]], + Rules4 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE4, ?RULE1]], - emqx_config:put([emqx_authz], #{rules => []}), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules1}), + emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules2}), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules3}), + emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules4}), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules2}), + emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules3}), + emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules4}), + emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules3}), + emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny)), - emqx_config:put([emqx_authz], #{rules => Rules4}), + emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)), ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny)), + emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index ab7ff548a..e0a8bcfdf 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -33,15 +33,15 @@ init_per_suite(Config) -> meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), ct:pal("---- emqx_hooks: ~p", [ets:tab2list(emqx_hooks)]), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), + ok = emqx_config:update_config([zones, default, acl, cache, enable], false), + ok = emqx_config:update_config([zones, default, acl, enable], true), Rules = [#{config =>#{}, principal => all, collection => <<"fake">>, find => #{<<"a">> => <<"b">>}, type => mongo} ], - emqx_config:put([emqx_authz], #{rules => Rules}), + emqx_authz:update(Rules), Config. end_per_suite(_Config) -> diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 0389ffd6e..8ba1ae5be 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -32,14 +32,14 @@ init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), + ok = emqx_config:update_config([zones, default, acl, cache, enable], false), + ok = emqx_config:update_config([zones, default, acl, enable], true), Rules = [#{config =>#{}, principal => all, sql => <<"fake">>, type => mysql} ], - emqx_config:put([emqx_authz], #{rules => Rules}), + emqx_authz:update(Rules), Config. end_per_suite(_Config) -> diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 5c9c32551..932c9972d 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -32,14 +32,14 @@ init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), + ok = emqx_config:update_config([zones, default, acl, cache, enable], false), + ok = emqx_config:update_config([zones, default, acl, enable], true), Rules = [#{config =>#{}, principal => all, sql => <<"fake">>, type => pgsql} ], - emqx_config:put([emqx_authz], #{rules => Rules}), + emqx_authz:update(Rules), Config. end_per_suite(_Config) -> diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 233680884..13fd65e95 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -32,14 +32,14 @@ init_per_suite(Config) -> meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), - emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), + ok = emqx_config:update_config([zones, default, acl, cache, enable], false), + ok = emqx_config:update_config([zones, default, acl, enable], true), Rules = [#{config =>#{}, principal => all, cmd => <<"fake">>, type => redis} ], - emqx_config:put([emqx_authz], #{rules => Rules}), + emqx_authz:update(Rules), Config. end_per_suite(_Config) -> From 8c9be070b4e420714b02147934617de97ed86723 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Fri, 16 Jul 2021 19:59:06 +0800 Subject: [PATCH 182/379] chore: update hocon tag --- apps/emqx/rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 8fe62918f..d0b65df28 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -16,7 +16,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.3"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.1"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.3"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} From cde4b9092a426a458fb66fa04b4a8f3589290847 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Fri, 16 Jul 2021 20:00:18 +0800 Subject: [PATCH 183/379] chore: update hocon tag --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index b688df419..bd4f7c80a 100644 --- a/rebar.config +++ b/rebar.config @@ -61,7 +61,7 @@ , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 , {getopt, "1.0.1"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.1"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.3"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}} ]}. From 3a7d0c3e84962c3874883fef1937b95ac9755356 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 16 Jul 2021 13:59:04 +0200 Subject: [PATCH 184/379] fix(script): emqx die if call_hocon failed --- bin/emqx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bin/emqx b/bin/emqx index 0e019a4fc..048bbc34f 100755 --- a/bin/emqx +++ b/bin/emqx @@ -41,6 +41,12 @@ export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH" export ERTS_LIB_DIR="$ERTS_DIR/../lib" MNESIA_DATA_DIR="$RUNNER_DATA_DIR/mnesia/$NAME" +die() { + echo >&2 "$1" + errno=${2:-1} + exit "$errno" +} + relx_usage() { command="$1" @@ -200,8 +206,8 @@ relx_nodetool() { call_hocon() { export RUNNER_ROOT_DIR export REL_VSN - "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" hocon "$@" - return $? + "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" hocon "$@" \ + || die "ERROR: call_hocon failed: $*" $? } # Run an escript in the node's environment @@ -275,8 +281,7 @@ generate_config() { # shellcheck disable=SC2086 if ! relx_nodetool chkconfig $CONFIG_ARGS; then - echoerr "Error reading $CONFIG_ARGS" - exit 1 + die "Error reading $CONFIG_ARGS" fi } @@ -366,8 +371,7 @@ if [ -z "$COOKIE" ]; then fi if [ -z "$COOKIE" ]; then - echoerr "Please set node.cookie in $RUNNER_ETC_DIR/emqx.conf or override from environment variable EMQX_NODE_COOKIE" - exit 1 + die "Please set node.cookie in $RUNNER_ETC_DIR/emqx.conf or override from environment variable EMQX_NODE_COOKIE" fi # Support for IPv6 Dist. See: https://github.com/emqtt/emqttd/issues/1460 From b2b83866853993475d8db2a1a512197be4c23fc3 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 16 Jul 2021 20:09:32 +0800 Subject: [PATCH 185/379] build(helm): delete loaded_plugins loaded_modules and acl --- deploy/charts/emqx/README.md | 1 - deploy/charts/emqx/templates/StatefulSet.yaml | 27 ----------- deploy/charts/emqx/templates/configmap.yaml | 45 ------------------- deploy/charts/emqx/values.yaml | 39 ---------------- 4 files changed, 112 deletions(-) diff --git a/deploy/charts/emqx/README.md b/deploy/charts/emqx/README.md index 446b26f07..428999a44 100644 --- a/deploy/charts/emqx/README.md +++ b/deploy/charts/emqx/README.md @@ -76,4 +76,3 @@ The following table lists the configurable parameters of the emqx chart and thei | `ingress.mgmt.tls` | Ingress tls for EMQX Mgmt API | [] | | `ingress.mgmt.annotations` | Ingress annotations for EMQX Mgmt API | {} | | `emqxConfig` | Emqx configuration item, see the [documentation](https://hub.docker.com/r/emqx/emqx) | | -| `emqxAclConfig` | Emqx acl configuration item, see the [documentation](https://docs.emqx.io/broker/latest/en/advanced/acl-file.html) | | diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 6ebbf5121..38006895a 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -53,24 +53,6 @@ spec: {{- end }} spec: volumes: - - name: emqx-loaded-plugins - configMap: - name: {{ include "emqx.fullname" . }}-loaded-plugins - items: - - key: loaded_plugins - path: loaded_plugins - - name: emqx-loaded-modules - configMap: - name: {{ include "emqx.fullname" . }}-loaded-modules - items: - - key: loaded_modules - path: loaded_modules - - name: emqx-acl - configMap: - name: {{ include "emqx.fullname" . }}-acl - items: - - key: acl.conf - path: acl.conf {{- if not .Values.persistence.enabled }} - name: emqx-data emptyDir: {} @@ -145,15 +127,6 @@ spec: volumeMounts: - name: emqx-data mountPath: "/opt/emqx/data/mnesia" - - name: emqx-acl - mountPath: "/opt/emqx/etc/acl.conf" - subPath: "acl.conf" - - name: emqx-loaded-plugins - mountPath: "/opt/emqx/data/loaded_plugins" - subPath: "loaded_plugins" - - name: emqx-loaded-modules - mountPath: "/opt/emqx/data/loaded_modules" - subPath: "loaded_modules" {{ if .Values.emqxLicneseSecretName }} - name: emqx-license mountPath: "/opt/emqx/etc/emqx.lic" diff --git a/deploy/charts/emqx/templates/configmap.yaml b/deploy/charts/emqx/templates/configmap.yaml index c9c4b4770..ffd1b66dc 100644 --- a/deploy/charts/emqx/templates/configmap.yaml +++ b/deploy/charts/emqx/templates/configmap.yaml @@ -12,48 +12,3 @@ data: {{- range $index, $value := .Values.emqxConfig}} {{$index}}: "{{ $value }}" {{- end}} - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "emqx.fullname" . }}-acl - namespace: {{ .Release.Namespace }} - labels: - app.kubernetes.io/name: {{ include "emqx.name" . }} - helm.sh/chart: {{ include "emqx.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} -data: - "acl.conf": | - {{ .Values.emqxAclConfig }} - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "emqx.fullname" . }}-loaded-plugins - namespace: {{ .Release.Namespace }} - labels: - app.kubernetes.io/name: {{ include "emqx.name" . }} - helm.sh/chart: {{ include "emqx.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} -data: - "loaded_plugins": | - {{ .Values.emqxLoadedPlugins }} - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "emqx.fullname" . }}-loaded-modules - namespace: {{ .Release.Namespace }} - labels: - app.kubernetes.io/name: {{ include "emqx.name" . }} - helm.sh/chart: {{ include "emqx.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} -data: - "loaded_modules": | - {{ .Values.emqxLoadedModules }} diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index 36e9be47a..963fd36c2 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -54,45 +54,6 @@ emqxConfig: ## if EMQX_CLUSTER__K8S__ADDRESS_TYPE eq dns # EMQX_CLUSTER__K8S__SUFFIX: "pod.cluster.local" -## -------------------------------------------------------------------- -## [ACL](https://docs.emqx.io/broker/latest/en/advanced/acl-file.html) - -## -type(who() :: all | binary() | -## {ipaddr, esockd_access:cidr()} | -## {client, binary()} | -## {user, binary()}). - -## -type(access() :: subscribe | publish | pubsub). - -## -type(topic() :: binary()). - -## -type(rule() :: {allow, all} | -## {allow, who(), access(), list(topic())} | -## {deny, all} | -## {deny, who(), access(), list(topic())}). -## -------------------------------------------------------------------- -emqxAclConfig: > - {allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - {allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - {allow, all}. - -emqxLoadedPlugins: > - {emqx_management, true}. - {emqx_recon, true}. - {emqx_retainer, true}. - {emqx_dashboard, true}. - {emqx_telemetry, true}. - {emqx_rule_engine, true}. - {emqx_bridge_mqtt, false}. - -emqxLoadedModules: > - {emqx_mod_presence, true}. - {emqx_mod_delayed, false}. - {emqx_mod_rewrite, false}. - {emqx_mod_subscription, false}. - {emqx_mod_topic_metrics, false}. - ## EMQX Enterprise Edition requires manual creation of a Secret containing the licensed content. Write the name of Secret to the value of "emqxLicneseSecretName" ## Example: ## kubectl create secret generic emqx-license-secret-name --from-file=/path/to/emqx.lic From 9239d3a8404f1188f21edc06f695158cc465a068 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 16 Jul 2021 20:09:58 +0800 Subject: [PATCH 186/379] chore(CI): update emqx cluster docker compose file --- .../docker-compose-emqx-cluster.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml index 81d48aba7..e810e77c3 100644 --- a/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml +++ b/.ci/docker-compose-file/docker-compose-emqx-cluster.yaml @@ -33,13 +33,6 @@ services: - conf.cluster.env environment: - "EMQX_HOST=node1.emqx.io" - command: - - /bin/sh - - -c - - | - sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf - # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins - /opt/emqx/bin/emqx foreground healthcheck: test: ["CMD", "/opt/emqx/bin/emqx_ctl", "status"] interval: 5s @@ -57,13 +50,6 @@ services: - conf.cluster.env environment: - "EMQX_HOST=node2.emqx.io" - command: - - /bin/sh - - -c - - | - sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf - # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins - /opt/emqx/bin/emqx foreground healthcheck: test: ["CMD", "/opt/emqx/bin/emqx", "ping"] interval: 5s From 69f06b36314f40801b4fa576e91e1c05e28c24f6 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 16 Jul 2021 20:10:43 +0800 Subject: [PATCH 187/379] chore(CI): add time sleep for relup test --- .ci/fvt_tests/relup.lux | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/fvt_tests/relup.lux b/.ci/fvt_tests/relup.lux index 91c11d3ae..63cb06661 100644 --- a/.ci/fvt_tests/relup.lux +++ b/.ci/fvt_tests/relup.lux @@ -131,6 +131,7 @@ [shell bench] ???publish complete ??SH-PROMPT: + !sleep 5 !curl http://127.0.0.1:8080/counter ???{"data":300,"code":0} ?SH-PROMPT From ed1cf33b9d4bdfb935fb58d910e293649718a03c Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 16 Jul 2021 23:08:11 +0800 Subject: [PATCH 188/379] chore: merge coap/lwm2m/exhook/exproto to emqx_gateway dir --- apps/emqx_coap/.gitignore | 25 - apps/emqx_coap/TODO | 13 - apps/emqx_coap/docs/rfc7049.pdf | Bin 157813 -> 0 bytes apps/emqx_coap/docs/rfc7228.pdf | Bin 53076 -> 0 bytes apps/emqx_coap/docs/rfc7252.pdf | Bin 493316 -> 0 bytes apps/emqx_coap/intergration_test/Makefile | 129 -- apps/emqx_coap/intergration_test/README.md | 8 - .../intergration_test/check_result.py | 52 - apps/emqx_coap/rebar.config | 4 - apps/emqx_coap/test/emqx_coap_SUITE.erl | 319 --- .../emqx_coap/test/emqx_coap_pubsub_SUITE.erl | 677 ------ apps/emqx_exhook/.gitignore | 29 - apps/emqx_exhook/docs/design-cn.md | 116 - apps/emqx_exhook/rebar.config | 48 - apps/emqx_exhook/src/emqx_exhook.appup.src | 23 - apps/emqx_exhook/test/emqx_exhook_SUITE.erl | 96 - .../emqx_exhook/test/emqx_exhook_demo_svr.erl | 339 --- .../test/props/prop_exhook_hooks.erl | 531 ----- apps/emqx_exproto/.gitignore | 48 - apps/emqx_exproto/docs/design-cn.md | 127 -- .../emqx_exproto/docs/images/exproto-arch.jpg | Bin 72633 -> 0 bytes .../docs/images/exproto-grpc-arch.jpg | Bin 97464 -> 0 bytes apps/emqx_exproto/rebar.config | 51 - apps/emqx_exproto/test/emqx_exproto_SUITE.erl | 454 ---- .../test/emqx_exproto_echo_svr.erl | 278 --- .../etc/emqx_coap.conf | 0 .../etc/emqx_exhook.conf | 0 .../etc/emqx_exproto.conf | 0 .../etc/emqx_lwm2m.conf | 0 .../etc}/priv/emqx_coap.schema | 0 .../etc}/priv/emqx_exhook.schema | 0 .../etc}/priv/emqx_exproto.schema | 0 .../etc}/priv/emqx_lwm2m.schema | 0 .../etc/priv}/exhook.proto | 0 .../etc/priv}/exproto.proto | 0 apps/emqx_gateway/rebar.config | 17 +- .../src/coap}/README.md | 0 .../src/coap}/emqx_coap.app.src | 0 .../src/coap}/emqx_coap_app.erl | 2 +- .../src/coap}/emqx_coap_mqtt_adapter.erl | 2 +- .../src/coap}/emqx_coap_pubsub_resource.erl | 4 +- .../src/coap}/emqx_coap_pubsub_topics.erl | 2 +- .../src/coap}/emqx_coap_registry.erl | 2 +- .../src/coap}/emqx_coap_resource.erl | 4 +- .../src/coap}/emqx_coap_server.erl | 2 +- .../src/coap}/emqx_coap_sup.erl | 0 .../src/coap}/emqx_coap_timer.erl | 2 +- .../src/coap}/include/emqx_coap.hrl | 0 .../src/coap/test/emqx_coap_SUITE.erl | 319 +++ .../src/coap/test/emqx_coap_pubsub_SUITE.erl | 678 ++++++ .../src/exhook}/README.md | 0 .../src/exhook}/emqx_exhook.app.src | 0 .../src/exhook}/emqx_exhook.erl | 2 +- .../src/exhook}/emqx_exhook_app.erl | 2 +- .../src/exhook}/emqx_exhook_cli.erl | 2 +- .../src/exhook}/emqx_exhook_handler.erl | 2 +- .../src/exhook}/emqx_exhook_server.erl | 2 +- .../src/exhook}/emqx_exhook_sup.erl | 0 .../src/exhook}/include/emqx_exhook.hrl | 0 .../src/exhook/prop_exhook_hooks.erl | 531 +++++ .../src/exhook/test/emqx_exhook_SUITE.erl | 97 + .../src/exhook/test/emqx_exhook_demo_svr.erl | 339 +++ .../src/exproto}/README.md | 0 .../src/exproto}/emqx_exproto.app.src | 0 .../src/exproto}/emqx_exproto.erl | 2 +- .../src/exproto}/emqx_exproto_app.erl | 0 .../src/exproto}/emqx_exproto_channel.erl | 3 +- .../src/exproto}/emqx_exproto_conn.erl | 0 .../src/exproto}/emqx_exproto_gcli.erl | 0 .../src/exproto}/emqx_exproto_gsvr.erl | 4 +- .../src/exproto}/emqx_exproto_sup.erl | 0 .../src/exproto}/include/emqx_exproto.hrl | 0 .../src/exproto/test/emqx_exproto_SUITE.erl | 454 ++++ .../exproto/test/emqx_exproto_echo_svr.erl | 278 +++ .../src/lwm2m}/.gitignore | 0 .../src/lwm2m}/README.md | 0 .../src/lwm2m}/binary_util.erl | 0 .../src/lwm2m}/emqx_lwm2m.app.src | 0 .../src/lwm2m}/emqx_lwm2m_api.erl | 0 .../src/lwm2m}/emqx_lwm2m_app.erl | 3 +- .../src/lwm2m}/emqx_lwm2m_cm.erl | 0 .../src/lwm2m}/emqx_lwm2m_cm_sup.erl | 0 .../src/lwm2m}/emqx_lwm2m_cmd_handler.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_coap_resource.erl | 4 +- .../src/lwm2m}/emqx_lwm2m_coap_server.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_json.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_message.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_protocol.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_sup.erl | 0 .../src/lwm2m}/emqx_lwm2m_timer.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_tlv.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_xml_object.erl | 2 +- .../src/lwm2m}/emqx_lwm2m_xml_object_db.erl | 2 +- .../src/lwm2m}/include/emqx_lwm2m.hrl | 0 .../lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml | 0 .../LWM2M_Connectivity_Statistics-v1_0_1.xml | 0 .../lwm2m}/lwm2m_xml/LWM2M_Device-v1_0_1.xml | 0 .../LWM2M_Firmware_Update-v1_0_1.xml | 0 .../lwm2m}/lwm2m_xml/LWM2M_Location-v1_0.xml | 0 .../lwm2m}/lwm2m_xml/LWM2M_Security-v1_0.xml | 0 .../lwm2m}/lwm2m_xml/LWM2M_Server-v1_0.xml | 0 .../src/lwm2m/test/emqx_lwm2m_SUITE.erl | 1953 +++++++++++++++++ .../src/lwm2m/test/emqx_tlv_SUITE.erl | 240 ++ .../src/lwm2m/test/test_mqtt_broker.erl | 171 ++ apps/emqx_lwm2m/integration_test/Makefile | 128 -- apps/emqx_lwm2m/integration_test/case1.py | 65 - apps/emqx_lwm2m/integration_test/case2.py | 60 - apps/emqx_lwm2m/integration_test/case3.py | 60 - .../integration_test/insert_lwm2m_plugin.py | 52 - .../integration_test/object_security.c | 253 --- apps/emqx_lwm2m/rebar.config | 29 - apps/emqx_lwm2m/src/emqx_lwm2m.appup.src | 13 - apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl | 1953 ----------------- apps/emqx_lwm2m/test/emqx_tlv_SUITE.erl | 240 -- apps/emqx_lwm2m/test/test_mqtt_broker.erl | 171 -- 115 files changed, 5107 insertions(+), 6425 deletions(-) delete mode 100644 apps/emqx_coap/.gitignore delete mode 100644 apps/emqx_coap/TODO delete mode 100644 apps/emqx_coap/docs/rfc7049.pdf delete mode 100644 apps/emqx_coap/docs/rfc7228.pdf delete mode 100644 apps/emqx_coap/docs/rfc7252.pdf delete mode 100644 apps/emqx_coap/intergration_test/Makefile delete mode 100644 apps/emqx_coap/intergration_test/README.md delete mode 100644 apps/emqx_coap/intergration_test/check_result.py delete mode 100644 apps/emqx_coap/rebar.config delete mode 100644 apps/emqx_coap/test/emqx_coap_SUITE.erl delete mode 100644 apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl delete mode 100644 apps/emqx_exhook/.gitignore delete mode 100644 apps/emqx_exhook/docs/design-cn.md delete mode 100644 apps/emqx_exhook/rebar.config delete mode 100644 apps/emqx_exhook/src/emqx_exhook.appup.src delete mode 100644 apps/emqx_exhook/test/emqx_exhook_SUITE.erl delete mode 100644 apps/emqx_exhook/test/emqx_exhook_demo_svr.erl delete mode 100644 apps/emqx_exhook/test/props/prop_exhook_hooks.erl delete mode 100644 apps/emqx_exproto/.gitignore delete mode 100644 apps/emqx_exproto/docs/design-cn.md delete mode 100644 apps/emqx_exproto/docs/images/exproto-arch.jpg delete mode 100644 apps/emqx_exproto/docs/images/exproto-grpc-arch.jpg delete mode 100644 apps/emqx_exproto/rebar.config delete mode 100644 apps/emqx_exproto/test/emqx_exproto_SUITE.erl delete mode 100644 apps/emqx_exproto/test/emqx_exproto_echo_svr.erl rename apps/{emqx_coap => emqx_gateway}/etc/emqx_coap.conf (100%) rename apps/{emqx_exhook => emqx_gateway}/etc/emqx_exhook.conf (100%) rename apps/{emqx_exproto => emqx_gateway}/etc/emqx_exproto.conf (100%) rename apps/{emqx_lwm2m => emqx_gateway}/etc/emqx_lwm2m.conf (100%) rename apps/{emqx_coap => emqx_gateway/etc}/priv/emqx_coap.schema (100%) rename apps/{emqx_exhook => emqx_gateway/etc}/priv/emqx_exhook.schema (100%) rename apps/{emqx_exproto => emqx_gateway/etc}/priv/emqx_exproto.schema (100%) rename apps/{emqx_lwm2m => emqx_gateway/etc}/priv/emqx_lwm2m.schema (100%) rename apps/{emqx_exhook/priv/protos => emqx_gateway/etc/priv}/exhook.proto (100%) rename apps/{emqx_exproto/priv/protos => emqx_gateway/etc/priv}/exproto.proto (100%) rename apps/{emqx_coap => emqx_gateway/src/coap}/README.md (100%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap.app.src (100%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_app.erl (97%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_mqtt_adapter.erl (99%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_pubsub_resource.erl (99%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_pubsub_topics.erl (99%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_registry.erl (98%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_resource.erl (98%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_server.erl (98%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_sup.erl (100%) rename apps/{emqx_coap/src => emqx_gateway/src/coap}/emqx_coap_timer.erl (97%) rename apps/{emqx_coap => emqx_gateway/src/coap}/include/emqx_coap.hrl (100%) create mode 100644 apps/emqx_gateway/src/coap/test/emqx_coap_SUITE.erl create mode 100644 apps/emqx_gateway/src/coap/test/emqx_coap_pubsub_SUITE.erl rename apps/{emqx_exhook => emqx_gateway/src/exhook}/README.md (100%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook.app.src (100%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook.erl (98%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook_app.erl (98%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook_cli.erl (98%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook_handler.erl (99%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook_server.erl (99%) rename apps/{emqx_exhook/src => emqx_gateway/src/exhook}/emqx_exhook_sup.erl (100%) rename apps/{emqx_exhook => emqx_gateway/src/exhook}/include/emqx_exhook.hrl (100%) create mode 100644 apps/emqx_gateway/src/exhook/prop_exhook_hooks.erl create mode 100644 apps/emqx_gateway/src/exhook/test/emqx_exhook_SUITE.erl create mode 100644 apps/emqx_gateway/src/exhook/test/emqx_exhook_demo_svr.erl rename apps/{emqx_exproto => emqx_gateway/src/exproto}/README.md (100%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto.app.src (100%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto.erl (99%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto_app.erl (100%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto_channel.erl (99%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto_conn.erl (100%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto_gcli.erl (100%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto_gsvr.erl (98%) rename apps/{emqx_exproto/src => emqx_gateway/src/exproto}/emqx_exproto_sup.erl (100%) rename apps/{emqx_exproto => emqx_gateway/src/exproto}/include/emqx_exproto.hrl (100%) create mode 100644 apps/emqx_gateway/src/exproto/test/emqx_exproto_SUITE.erl create mode 100644 apps/emqx_gateway/src/exproto/test/emqx_exproto_echo_svr.erl rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/.gitignore (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/README.md (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/binary_util.erl (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m.app.src (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_api.erl (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_app.erl (96%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_cm.erl (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_cm_sup.erl (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_cmd_handler.erl (99%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_coap_resource.erl (99%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_coap_server.erl (98%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_json.erl (99%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_message.erl (99%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_protocol.erl (99%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_sup.erl (100%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_timer.erl (97%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_tlv.erl (99%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_xml_object.erl (98%) rename apps/{emqx_lwm2m/src => emqx_gateway/src/lwm2m}/emqx_lwm2m_xml_object_db.erl (99%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/include/emqx_lwm2m.hrl (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Connectivity_Statistics-v1_0_1.xml (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Device-v1_0_1.xml (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Firmware_Update-v1_0_1.xml (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Location-v1_0.xml (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Security-v1_0.xml (100%) rename apps/{emqx_lwm2m => emqx_gateway/src/lwm2m}/lwm2m_xml/LWM2M_Server-v1_0.xml (100%) create mode 100644 apps/emqx_gateway/src/lwm2m/test/emqx_lwm2m_SUITE.erl create mode 100644 apps/emqx_gateway/src/lwm2m/test/emqx_tlv_SUITE.erl create mode 100644 apps/emqx_gateway/src/lwm2m/test/test_mqtt_broker.erl delete mode 100644 apps/emqx_lwm2m/integration_test/Makefile delete mode 100644 apps/emqx_lwm2m/integration_test/case1.py delete mode 100644 apps/emqx_lwm2m/integration_test/case2.py delete mode 100644 apps/emqx_lwm2m/integration_test/case3.py delete mode 100644 apps/emqx_lwm2m/integration_test/insert_lwm2m_plugin.py delete mode 100644 apps/emqx_lwm2m/integration_test/object_security.c delete mode 100644 apps/emqx_lwm2m/rebar.config delete mode 100644 apps/emqx_lwm2m/src/emqx_lwm2m.appup.src delete mode 100644 apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl delete mode 100644 apps/emqx_lwm2m/test/emqx_tlv_SUITE.erl delete mode 100644 apps/emqx_lwm2m/test/test_mqtt_broker.erl diff --git a/apps/emqx_coap/.gitignore b/apps/emqx_coap/.gitignore deleted file mode 100644 index 67eaa0145..000000000 --- a/apps/emqx_coap/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -deps/ -ebin/ -_rel/ -.erlang.mk/ -*.d -*.o -*.exe -data/ -*.iml -.idea/ -logs/ -*.beam -emqx_coap.d -intergration_test/emqx-rel/ -intergration_test/libcoap/ -intergration_test/case*.txt -.DS_Store -_build/ -rebar.lock -rebar3.crashdump -*.swp -erlang.mk -.rebar3/ -etc/emqx_coap.conf.rendered -.tags* diff --git a/apps/emqx_coap/TODO b/apps/emqx_coap/TODO deleted file mode 100644 index a0a1c2aaf..000000000 --- a/apps/emqx_coap/TODO +++ /dev/null @@ -1,13 +0,0 @@ -1. Remove the test/test_mqtt_broker and use emqx-ct-helpers -> Done! - - Enhance all test case - -2. Remove the mqtt adaptor -3. Remove the emqx_coap_pubsub_topics.erl - - -### Problems - -1. The coap-client of libcoap does not support Fragment DTLS handshake frame - * So, the connection will be established failed, if the 'Server Hello' frame is too big - * Why is the 'Server Hello' too big when enable the 'coap.dtls.cacertfile' option? -2. diff --git a/apps/emqx_coap/docs/rfc7049.pdf b/apps/emqx_coap/docs/rfc7049.pdf deleted file mode 100644 index a16db36ef75fada5d370bef3e70df56b733547df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157813 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

U 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 z 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 diff --git a/apps/emqx_coap/docs/rfc7228.pdf b/apps/emqx_coap/docs/rfc7228.pdf deleted file mode 100644 index c9dc1b59fa26ceccf588b86789f0a3f50233e6b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53076 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

^^B>cOQI&ZqEH^1~Wf9?TdakhJL#JlhF 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

S-NSodduv^QKN4zep< zg}#S@#WsN%3RlnUkd=IQ#Z{zYzcSqpymS$PkvfzKfH}5 zc9ZD&gm`7(v5oU1JAev0Tx;R9OPn|v&T{{jQ8yy^)2RA7aFkBqsG85rIthZ5p1YNb zBj=|qnhHg!VT=T}%aag;?}?bwMpmMQM5j;`LhI^No~ zj9(x;{|E<8dx+R{0h}t;Va=ZfkPEKk@mT~0KziQBV~L!eP zn{X$FNOc}9DLt}25W6Nk-#ZNmI{I6vUT8qfXfj!*(UcE+dD6v0&%lSJ`mh)i6zY-U zVY!u`+aTd+U5DYHLLkJFxTuWW@o^!*wsF8*nqqq%-yy-~eB(25X)AzJpaujcW1>k! zagH^id#_xw&N;Qe*CG0RwR|#F@zv8;y29Eco16SN zO7lJa3q(Yr-_pMz(cc{O3yzqWSXh3&|LbqSKdgHHhhpKIZSQX+`sU;NKal8eNs+>@ zzM_AusRZ;z*`+Yj~n8YsqBWTD$h6Mp3ffjJdd zbdxKm$$`c}8|$|r2NlQ{v~!1eame^)2-3_9T45d^LZ`CfHIx)GO$W?=^#wDAHx zC{r1zgb{sxs4fg^6f^VUrtH1B#J=u6Jxcq+!jzuM&ukUqNvR`QaNv3osc*5%%|5Jz zfZ>ZtWSG(im!qkpd>o^S5>HdXBtMsQD>6M~pgk4$)sH(^X?n@pFU1KXSwt_Wb6^@_ zt+b!r7?)2$PG?$e*2V%@q%uwIF%CU7jy#HM%PnF-NK!tgknXb^uz`c%3yx*aphovm zT>L@8q5X)NeB7ri_W^T*4%)j=W=x|2{H10)!}mG(7{t6>BV6ANA%>PjNV^IHmCCkA zs}2#uNQoe3(2Zjdb3^x(=?nKT)c`kK!y~c8kk7DLERv)O;b6ocZ=2MD)%t~g3T>$c z8V3Em>)3+69E66=2<s!&tCOP5q7*Gj1~WsSax_q z2fpF~|D!E@ComE*#TWdTPbFlyc-UmXuu5S$dSEF%#*gYw6>ly`{nQuE($EM3t1j$a zNQy!halEk3r8lDye@V3*EVq`TmZsglqZm=tR9#%4&?*15beAJ&#Ia$Zu$houFc5bk z*LyQx%BIuezX6|O52IQJ)8 z7U0dZvn)#LdvY47gQSWc08>f20sQTnsZs;fLY)wl9f}h(ymDjg3|!O07}sI> zaHQNLo4trWD1GF|4lc9(!r`sJ;pz#6P>p2x9WMDrzOWX8LTwfZ%!pyEZulIsmINxG zi1r2%nRV>!@{NN8)7*Q$VR?S!0rV?A(r2_=G-?d`Fc6yZuptmz4*=NITw!ZTXI$Bo zMyUdPvETtJsy4x*IzJTAY3C(Q5~(in<PaAMz0c~Dq5R4ejRQHJ>3DECYeS_W15OFuMb9>z3FMGujZ&^R8XW8vt zP1N8~KFyPFSTlO2Szy(O$C8^-46=CKzg(nm`<-Ek6^^sE-4QVZEHFLuB*3<>7y};u#>-UM z_WK862{-K5`xofhwrZA52dIPHqE`Jwq~ucNo<``_{})(AFteR%-YN&xqa2i zj;m#eMG4s57Ia;+2ZA`Y8WsD_x8HkYe&21oS`QbvHPqc={Mp62=0$@?E%ceyNrS7W zGfwkM)Pi?!#mafzeR*W!Ru#y5lli%0b4T_Q_#8oY!K2P|+<9Gw7fVCC$Xv^cmPdh# z>8g-zpfZTQ8a-E*^bM3IL-O=*#E-O(B0};ICk0WMlrPR_3>a z?@wI*XZ0|aH$mUuDCAAy_n%V;%Ws{{-zns;tNvNZ|Nj*7*NT4KUz334w{cM6^%X$C zK=A98uhC2+adSV6vFcH8s)Gx^|3sazeAOnOn%HlaxgE>HWjDK7lCCX4eL{t^|2mYZGkHfb*K;B$Pxjx}^?oUp|l5eagBc8R!P7i zLj#gXRKNMYu!fr|xnf3T!bDk(Go`)jC*5KnGMhD9F^BP zm~!ZHz?SaVy&>~YQg3besq+ob4b4$*Y*DI6;bW5Eq#Zl{D}FOtHjf_#@&O-?^d^ul z0*DRhLu3UYRjsL-#50TUzsW6##21?vS?{~#D@*R+5zX~e5+EeklMQ5R>`7DN!>}}Z z!(;Z^l8NOY_W^%MdNcT9z5Hx{dA`^D^6dWdZ0+>20Y5L8 zL3bX7p2e!oz_FG7omSZ#8h@E~xr!7!>OO;<>%}iW$ign!)2|;<20?RvuWcDM(D5Gp zQLW(+cS6Ba5CSC*zkTQ`TxB4>KuM)AccXi6`*Sg(HHv0Ggqt#hW;=ny{$uu1+ zW41{u?)nTWY4x0-qphm?Eph!E7>8d}CPQES#= z(KpK_LSt`@+;&ThL>#uto+iP^JgrU$zKvJQj`$i>c&?w`RBmUD9a0d=oN8wX%o=Rg z)_~elxnX!iONj=eI_*otfqiR_6ewtps%t`$T#_nhGEWg&ZUz1~s?-iRPqEhhxUb>O zunD{C$4B*I@d$zSJ!Q)t}O4x@G3{@&$f?Np^f< zF76bIChmcOzyJm6G*-*llA3dSS49{GWkZ<$L{VKc&8F^Xz8{?(7NbGRY9ND9&=fDd zbYhET1ge}iWBi;rxZ5F&ulFni)EwO{ZuGHet}>r8#c>6dz2|;JrrO6`lLR?p>dTh( zcyB>!*rPZqJ@S`XUA=MFm;mP0Ts!^)UYw&{OIp(-G5_x?Lv8LMFt}Lix0`-QDXH77 zDqo6Bv|Xg3H<%b)$Fz_Ot3U~{)FHQmc(6i%g$K98sW^E0!3>i4{0&+;azh!#)7bU4 zJYd}L?cK$Vfo3m3`@c_tUkEeogLox4^&A^%z5f)s0<>=#+#ySK1pf(BGhKa7c+5!% zV?pC?SNXj{vQ7O-1_m7Gm4XdT!qC(Gr7fwOY`@xybWCD{7y*gZNsDUkXX2JVA3|Ak z=m-xCe6ILwv3qTq=2Sfa_Ij22;vUawx*bI9GDyQkY;MYY!#ehABgHT|-T9vR zIQX&SQHNbz#~jJ*a7M=Hi$as7Bsc`i9zo&f>VbbYU-$WA5)c=O+!z;FyR)4A7t3S! zBrX}#iQ=chGB-=CRqV$i>fyO2iFhbFC8MdTwx+wOaBeM0kG|m^=F&0p7AL{}nQ#HXh)Z|~h9z}(B2Po#VH-t*z5vzf@8UqXWiKULcE&TF}5Qvo^A z8)kMGF&?F{b7QA8i?m$X`+-+3WnJ@MfYR@{{*~dcyGz6$-TxJo-aL9Ne|hv?|9=CdH;*36n@5l3 z&7;Ti|9bRTek=3-Jj@?p`lI*xkIuuf|Jpdb%GJ(i06SfFyI33zZ?HcvjZay`lC|nY zA+eJ}g5{T(c-(s&!}IaC6i2CX7LKIBiul3fISSl1QE?Ta2iCcZ9Dyw6s={}f>m zM`^IDX?*qk@zEfk0yaGuM75p7_tkMy;A+*~GfC40Iri)zQ}iX}=KkX_MWknt8-V`c z$)kKOjs76^)m%fqGSZh3m9%wd=5BY3{_<=qScUHX^Zj-2N=$G#sm#87sVbD^bQs|) zGu>&7N0cfpowXprG?gC_jk%@{h2mi?3`(J0Il2*7;AfN%>pqs0fVt7Zl+%q`F4<|L ze{-jZIm$4SBnc@O4>2LeqPm=E5?+CRPS-fjM_g-aLCi1IFeg2iusTPfhxYK}Rw*8i z|GqmNmBYLiQYD#MS5_y^PgljT10z3MJrm=ol&!J2d$MRLc&>0NJQGY3-5ybJ#5JqQ z8z!N&G#B6KXPB(f@9qiEu*PH0ploAtLzePN5pZq0FApg|9d6#yFmNrjL zX&_Hf3I2O)eTLkHdnOwtagBr;LV8^#_No!Nk- zWQC)bgN&aYhpBBlo>504Cc)xiUupx9ktphT!h5!QBgYcXtiJ9fG^N1rP3S!6iT-1h>N7-Q5Wsb~bx=_xJS~ z-T#}@*Hz=KRqxIGjk(r(CIB8k+dmw!Z)K(dGilKIWk&Ek61hPiecp_b`>+)yUDw4D z$v{kJ9%C-XoL!E)kwU{-RID+; z8)Rlu#Y$$(VaM4Mf(5nGI}I+*Z(EEina-q?@{$3Jbh|b+#n$g9twl<#z6YUc9 zAs4OI*jThWP9=G^Wd@_%Hg#{;8TtXozE_m~vS_M=B(YoZWRg7lLoo0%vtQvx_?gDS zy#biEtP&%CoYdXb0TOYPCi}SrvjKcnq~8W=;Ob*uNfejEmndLBvh-f>E@Sq~C1JN~?TEhSvT*Y=TFnUwTrBX!klH_tbp< zTJhlv$ga!1iV~bMXN#M)K79K=iDR$^Hd$aE?y~+4&d;Byv2j_s#jKY=LKHN^ggL3n znsE20?V;J&rh>ObVyLwAcC2+T>#Mmcm6 z^U@KnQ>$5K)6&dWt_hRVqd;xaIEP<`T(PgD+9)DaVpaa=q<-IjbE{hk23d7cg5;%Z z$Kzy8H5J4qhp*XwuC^vm346Q|wAI&FjE%k!2I~|o;=O<=8aqCK)V|RagXR8BL}M2u zt+d7_N=ZEZ$-?*7R3Hz8L`HK-n_3=FUdncj1|1o0?uU%Cb2K0C5MsuB%JL1`~yT-xZL z9;T>8(CbskM;3GhQ8o3Z6Z|qO;;l8#=ZIQR;eOYne)p$FRB8V3~-#eeA9nQ};MDC~*dVWx$+6eV-f( z3*TPtjQ#QD<4`CGW0$jJZ4j9?lZ;BmW(|lfCoYZ;IDG^4WSm1ya3^PWIy!wZ_UDbk z+@YB-%CKEiki!BfY-V{CRZtwb(L}MnO=*wf_lzt0Tr4|W_L^B*TiBt^Vn6jFroA15 zIX%L|hM+O$0Wrf9MD5=j=DPfg2ZyQ(5E!|Km`Vb3xkez0$Xe^n`g()mLPkD(y=`{o z;wQ-&y`$MupG8Kqk><=l=9bWUw2Bel`$kW?@6Uq_*%%CmLT=9t#^_MB!0rT3Zerx* z4@B)Q-naX3#UY(<=nc+x-af67s+d(c-1-bW%U@H9!dqp{Fg!y2iD%Y}IzM+w&X~w% z2bfHQUG>Q^>(q{<0S5orbXX#MqGAVI&!%{8#_YMY6%Xr!Gj>0_o+E+g6dhmE6(sO7 zUwL}Yu|sP=L&75nlv#W>{k7xZx4aqM;p?jT;&PXo)C}9>U0$w0{TfF-)}N2XgkqZ; zUaoy}BSX{vO&t9lhWbKyB?8{GM8KPt2zb*H0lxvJ*M8ocvjljfl>X7b z`xh7KuQ>X~C;i9%-EW@ZwSPDJM;s-8?caTM&ujUljyK>!S5&JOXJrKq->Ld^-dF|i1Ic5AgLtMb49k(t1Ui%ch@}Nu&AtkBb zyk!-OWm|d%!`sqQt=L&8wp#uAVsg{!GThfSiP&}huDY55Kb;;E?&krCSY!{0Z2drYWTU)EZ6q-U>4Tcxz?Hv zz1MyClzijV$w5`WIW5ijSsv$vc+_rf;`F)Z7TLfSn|H{!GSpIYotQaeh_Ex^M(Lud zh5+T2h~&=*=!W4VRt=Fx|{g91GVJGR;4!OE3$IgVs5*;M)afu ztH^}JHg4qyy<^&3MCZ$?cx%Q!+}_SlpGDQHE&DJ)xnN5+a8{8^+*z-*)B#+!qUrg<{3c15dKBgKe3#-g zdNR1$JY97+=hupB@kDV3B00}j|B@WBWb{BfJY8OjT&qhC?_9hF>oY zx3f8MBAT+n4ZUNy&K{Fo(^+sJD( z=;}(O@jat81amK)=za)+G{mJ5_f9x8vn1^FR&Hc6Q@(uxAlx>vrA|Wwcy4lZztP(X z+^cYBFE!dc_p$zt%!VZ{#b!7_h<)3B$ov9=%ebJ;5fsuf3|vVU2F@|=8%&v?)Kazh zbx{)nq0Nrf%<@b`Z(C1`PWk%tn&Cr<&$Tvm{S3)F7eO%0%OD*|1s9yS1P$FF+7(k; z2}KaDU~C;h9C~$*YJy!sy>_9X)Ho3_p?!NaV(-N>hS92yKUNh-)eranjtq3mfe)m|XhZhk89!LK0;Kb}P9L>VDW1=qwqZ$JyvmIya%g=CZC zcjSfN8tb;=*rRhU0C3;OaOj**My*1ljfWqj)nIHCm`dEc0Nz{o=`dwKtqd1n5Qe;v6W7(BG#AhK@)>{GEsU5K$-MG+L=*+GeT66}+h$9%Vp-g%oW~T3yW^#=K-ah#$h(8VT!3t-i<$br$G*4Qu zYn2E?=`9F!oALUP6lK(1U4>dlAoa;lwDz?+bJ*s6v&}C(+Pg@Q`asV@EUtQH^-}sY zIW)3oH95T~3Q9dc6QB%;gH`nEIp!m1eYZK>i=__5c1TO-gQE6Wr#9q>)gW;P=#hu$ z1PEl(P}OVc5@M3m)^dSJW-(Ncmv6XR1%|9!**4pmUSIIW zLI@e>dgX%yBQ-6)_R>CK3rXtS!>jSjVtVHEkA#L^UtD^Gfbq@xcgE0Vd>2hSb}%=g z>SPbgm^|2o3S91%FIvlvNG>-phk^_VOB=W#@t=#KlL4Y6*pMP?SI0Y{IeieUIELj; z1HxjHx!t@@NuBMl$SI-aJY+*<^mIXk*PyH|QIhlUMxUl`jT=RY#zd=^ecf?U3>!XL z%s*p4-&kYS-$ZWZsWqSYj%DS`&^~8t3G~Voj+ggOnWgi{)IXnYwU-SEQPRV8J7ZEJ(B^EmmMMM#C4NUT+dpAHZ zWd=@XD{G(-6v)F3Ksfri3?QfM zURxlvh_Z7oTgFe0s)iTIy`Ma4DV$>5oA6kgO32z@J|GW^VCM$`Si6@n5^HU)ofaIA z(6qNi{3Dlie+{H20ri>{n{$Ifd+%K=gri97nwXZy4NOI!+TSl;2L?vMQT`ms=MR^^ zoOX3BnH68bU9OlcYZ?W^{UufcZ@;kdfFKl@GF~ir!KqU;h`LC{z^wNv58QGVvcRP3 zc~>ACTkIF1D+%o9lkIJbv#3SP*@$8_z3p1+ zXjDAsW!HM}NtfsG2-mHlp>|fc(ApTEs7FJ}h>`QOtB*=Kvtu2ES8UxhvlFddD5Ls^ zL{H~1DS}%e2I9u~CVN1G+G;{b=d-U98t23NM2NnVsI$Axi28dC?+fDJad7gr(d>a@ z-F<&1TG((W_&1^RZ=aEc%?F@N#QVpwH(UBh96~^LVhOJPxn?`uUI#b&9Bzu zE3V;2qVE$Qt1zTLGK|ao9^iKhweZeg-N(bZb9fi}!HuAaWVlo)-feTw#1fJ%rtKayRDNZn_M;*rn_a5kkR!=tE4MRU07Ut zEs~;j%Onbv>Ufgk(AtuS63PAj?h%pY3cz0A31uU^t)uAZU>Uu4T@pw)#3zCZnW3>vh*ZUZh&p&=myEd!Bw*2tv;W{iD z;XSH7Zw&{OZa05&&q~C}E__GpqXVa<)UbtN(@aL_JrXHLvk@FtT|#vzaT4(q>L`wo zaDVE*k{T6Lsi#Cj%}5i`qYIIR!nafU+1@pr_HHBs8&p`@wAyUAm49s;ysM^~HwG<9 zIt%sYt6vID^__N-q*YB>Jl(wxrOrnYjAed#ku;cy7%&sKSa~;kPZ%(qVOCFKiC*uib}*EOX@z>8GHhl|Op)FC8i^=w1(uh7 zJ@Mj%7~KillgnQO%aaOspl{nUa_^twm33j%ITk@-;>~@CaM8-+J&BO^BI&fc6D&`Z zut&cs>7PXA7T`2ZEib{(AjdBtkZ_ouUtV_c*T6dRzGxje#|YFnE~!HD|`zX0yz{} znSPQm6rw&5M+tdnU1a}w1sYSnsjHfv_XI23M}sL9+-8S~>hyDEv*d=nY@S};Vl*c~C*+k+&l1XbVV`5e{9uH60^ZvjEVYG8234&GAkYiVL zB^wPD`dqFp7MTLoxNL{;w4M}Ydl*Y4^ua=1vkz#rEkEWWf#WJ% z!Mm(ATZ}Y$2;rb-Vp$y>tUc6B`TIPL-T0TEGvDLlJ^}99Is0QzS&86XKZ0T;{A%Mp zi#N*?re}9{_?oc*JJ{>k`oSe~Cf{#K8qf-C)X2dpd#=c1W}K*>E77c2q4(h?xb;7+ zA?A#;3-Yk_gm>SA+6f0gtyo*c+YVDbL?6vXxpdYZhf|mhSn+^w0Sl|G z;$PK|-?P6R{NC;8O~{2=r<9vnI8l2o-aT$Nwmx!v_sqPc?@kJ*>i^WEoV|YuE$p&( zPoP|B0s+;KJ8*XL@O*QBX^+05U_WX+x)SXjLI*w3d8%mQId)iW=fv}LdTEURW9I5c zoab6RaK^?vL>9SuUat5UuxE>lNGS(wY#h1a}zz@T&nCl4CH7f^d;o?fyw?J zWmgoNz%2hg?ovU;7z&du>o%6xa-YbCfLi!oeZvSLjNfUo14e4s*)=HHAXM@>Wv`JS zcD`sP~e&Q zz-2Jzmz=&$K7z@w2*H~Wwh}BN6*(}H2PbtJ(;BTmb&_126-hOU9V{U}7*6tC5gJyl z`#J}WUUHAr#tB9q6e4NZI;D+WB`#1-T6ce}^883Z@19er{YBlQQKBh+`&&LaQ$!;eSA{A^ z?2GOY?6;DpGpBn}dc5hXH0wHr%1lrAZn?55R{ztETLtY7_~;7y_W=i@?nP`I^ajuT|p*eE*r!n$>I z7p209Jlh(^`DB{{nN|B&>NMdv`Ruw(%th4F@Y+6h(uFk>9)VsOR+lFa=`J2)x5&2) zo*zInr@+(~tjsc;ouO%W5J~TVz>Xa6-ADW4qI%X1^>p!2ytZGOmtqLnUs8m7_%l0lVXh$t51b2<{UbjR^ zZ`mV3clt_O)$!gw8 zU|&pn9TT5=y?*1HxZX~(V=hu^kizmwF=J$1Oj@OF{^{3Coy&S+T z;7=sg2+T?$&`SuYdr}4(#Z)xw)J84P!W39JiCbYqI!^}R@ z7lnx}n_cp(p@SoCviFdChZ&)`UuS6eN7>PR8n%~u&95T71*6K-mLXD zvbS|NYf3=w9dXdE_DT#=f+0U=<5=f$7xJoN+G+l0E-1xB6_{# z1kupzIJ#c=K!k*S^6@zv`jn6h0?-7p_l-PEub9R#Y~ER-Lai=V9(vh+e??7=l!%rI zr!={CS(m^8aFwG$_q!dLI>&dnh|T+L=&Vy#La{DteVBke4fwp?m|x26VP>E=9Tni* zBJ5<3!w38|1cgG)D4b&^h_O#9&A-^0w&S^e3ITd()c{fr`zeoFUYF8-VOi#nV~+VnPfmUff7y?I4|b^&q^f*>_&A`&|ib@-fni^LL}oSco8$R~hJ) zsib>COiE2$do<%TYswUt`1E_$9h^L9WvER$;A02anFG)Z z{@ff+FC)>T5MvIo;u&`R;lJYH@l`RkE!77t3 zmk)c?V)EUUGkB6CrkluR4QA=*F^b%jB+*hj($ms z#Eigv1)j+k+>`SNATNWE28OQ3sReym*=bj=pa~}kp7xtnX-(9|OMrq)Ow2=RC=w>y z^LBzj_bNVC>(IO0*L3p6x?#h|R!J=xF`dY+E<6SwuCYLSkkotDrEeMHy?uRjA&!Ow z>;uI%DVHa^W80criqfv_MVr>VUZgFy^+P_8>co*GvdU4maNN=qN7&>L)>bGMWV3!9 zvda%T)xo$GvJRzY&7f%mNP1g@^Fc9I{kDU+a!U71!GYL4 zaK*q?|K-DnYp{JqUkN`E_yl6?y?3Rb$cmJ=OxbZpK0Li!v#MDQY@Osi%0kI$WFKj2 zw*#M!{eTka-%|6#u(0!egmOk|)GyIy*pJI{)br4H=;R)_ekcMzTXP{GBXGMefC;?V zUob*%W{=Dy>gd~bl5&oQ8Z7~)>h!HJOR*S0CsI_><|?Q_Yjti5d_wHV=%X>o$l2nDFx?POL?zE;l~ed@D=opk^*Lvd^?bjYox*`DORCy}7oC ziO%4&5g+=1>XaQ=H1AyC$28c!S`$ye*dyKt*7}CTb+e2bEhu%8@OLT+{!pc`acF*M zZvn!Y2!tpF-Ut60^~UPhe15u!V2yp2uHtxy|L`^Z*-!sni-1z#i>8m3VLW3nYC{b& z>7wZ-v?bKDJp**B-AeUWUAiw-Rjml#bt;^&9h5P*<&!r8+3>q_Tt4skbizLx%5rCD z`Y&_dr5@`7$moALoaw!+ALgGcxZK&m9c|W3$h;g`aI>~X%vxJ!DKfA4Ei;3W{PxxUCBFQNo%%nBFTYpx$D4SKFK=cW;LTkFya{K3H{lHM zCY%A@gfqaKaQ2TF^P4#P>$UzBW8S>8{}^L}UpqVt{l7ar4cSLus629JZI7Xn=d@O> zcw)vYkJK|h!7%&Hcpyg`Z}|Eu#DYhT5L>@yo5|M{Gsaa(bQSFLIoyE5i5h!B73M)2 z!W&yA5>+bsAqmF&E{Fp&UU&A)-26g>iS6CRGIDGZXKJ|#-4C;uFe*0T>$Gg|NUv39 zy7asZeo>TaoRR&+QL2Yf29o7nzM-#~OuX*Dq92Et?;EY`I{NWA04VgdOuFe_`XZ;~ zd}x==0|%r79-wR6A-U}H%X#B8RU5ifdGTNk&yE>@0GSti%WR~vK*u2-*Q3xn*W~qy zQ*`b=l1G2T2g96bIx%HTR#9OV_wW$yFkw}PdqHJ3WI)Ew}TWEcO@%?=%@}7vG!{9O#8b}6rxp8>6jJ-=~ z#iS@9oUKO)QAq1adigf2QuWXQjG!o!U?r8AFs_8UGgu!J0Rpf!xr0y)t7-Fl!wCK) zUDYN8NdF6_fX2Il64ApuDSWsiosezFw0_Pn^m6^&RX_+^AoqIAuhu@FQLA!v}# z{63#Aow+3lTR^Jf2AW2cE5NvqYXnh1TP%B;#DdatLNhDBWbF0)bfN3!s_(r8txU0~ z-J0?ZFSm#|(6G4q0bk@)*i+X&zuUpq8e`QwSH!-^OP{9QZ=l$1_w8}22)4v8ZsB@` z;g)??o>O7vs4v8;dBq>lF-b!E9(#HSBnWmMx|5-^(?&}~IGH&pV%+b09C>9i2uvBU*;b*u1*<6xAYi`?#JdRc>?uB$RnYsfKB z?a<9{wzXm+DBdKM;!FpcF#2?k45YVGW{u|hr;|;$Kd<>B)*bz=9;@#m*>u#N8S>IFnjXZRtGg z?av1KIa`A%s z(bVSe%;=84?z|e?r@bNDS8KI)zj?mn$x$AfWb0Pn9>NF_rx-Jszt1#3O_-ZO&gz}~ zfIw+~K6^Jdeq0>ccUV3IM~Ih7?6wH2ARRir)~VbUym9i&KkGQ1oi4h#Z=vfO>|^D$ zGNy@1jo~6 z>Lt+W1cD03*T9K&0n@_6ov3L6SW+{c3IWds%lD~E(YEW~Jua@iG;uT-h1;fjuSvcE ztk*suU1?^Only2-uar?%Ut#e}hL-5;0*4A_MbNJcOf3A8^LJZ>x! zAi=rZd85sCFLyh8Mmq2+C4@;OFt3KORos}|W^6UPhjon_i|i}K%y%t!@D7*UU-Tqueud0a*XeR=>T|ze2!&nXP{FWv{J{e`KpSE%pD9t={mmf7(s^ z7ZmM(kgfiPU0<`+o97C6L)QRra2nu^DEmi-`fZf`_4@wGP;X$_f6P#sudSd3N#s9n z4j7>>Mx3M^p~*;Y=t1#_>quGFZKHle#a6g)c*98 znp{0^blYr85#2p`Q@OQZ)`DhMcK0ekU-=n6SJ&6G^!Z#u zhvkz^pRfBx(0sDZ;@LEWSmUpLorZqyZ^ahLFJHrY)w)Zxj1g9_&36L^BcvgX(jq?1 z^_o?%`*USsbd{_OWNAa(IGk;>x8p=n5;0CJjs-?0-suJm^z1%WRr0ukW-!upNu%0y z%%vI&HxkgU#CZnk80Y0W_u}cVj7LnjrTGtbosBJCZEWrxR43}MFBNK3OQyN89z=sr zs?pQ4eF$Z~ppitaoMq+D4jf}KV>1~^>#8Otp_mP1HrYP9D=UCH)BTc@KUbSiHY1EB z3q4>E>c%B0vAM3nEl_+N5U7Qmsnq5Bbwi(J)X_yeRnf)z6yP74yGrR3FYcL{NNyip6PDMprW(Jv24Rwl+7Wx*8m26YYA^_2A$T40KZkf&)5%elb+#rMq za`QHSaVXN26vr0T3&vuE3k)!ghZbd~BlLOi5?1OLXMC|94MA-fXV%2^$h19Ms&3Tk zY6pQHVn>?aF)$)tv|5|2@KAybJBSsnekVE+CLbr=%mx&6R*O1W7g~!?T`SYyR2^=} z59eo=iDw#Jl_Eh^uP1C*Rgl$P{CHbc^4Ee4DF>4hlI@mw$M)f(VEC`d^8_cAvTJch zTtXE>P$N8SE5rg#GWu;|Q8KAwBubVFDm6BuI&Zi_fX&K@mDXM z?-+sMymRl0V`W)bWI>owQ~hQ;1+2Fm!r`8{(qAzVcEP8G%`%7m*z-x%zp5u-Jbvzb z^OO^xN$Z=l)Ks0*2`JvyPyv-bRw-s&+IiX71{qWJA0 zNFdGL27W@n0P85<$5L97(S<2T6c?vJT91$d`F*PLuksLFQz&{k=i<_Gt@O>ZG&JLg zMwLWSYRN!S{VyMQ!`Za=cqpn}`BZ9*tmCqXG&_pNAOV{KgPgbznjRp4s^erT;Z608tD+@hb%rDKygND zc4rHc_Pq^##9tN00#hl8yY$IC(dpZgOCx;xD&bi3-dlX>9bTPsiPBkD-oc?b>poG= zfW`c9L8$g=(8I_f%G!<_;fgkJH4ir)X9R&6i;t9?Nc!-lAHm588z$@hiH(|%h+U0R za*x~X)w)Mh9Fr<)pMPU!XqFsi#g~;=$lpuLU+Z-53)>v18GxPDBDxPn;=41YufdQ_ zklD**U?88iD^f@Z+Vxsb(SRa9e{IHz(_<>*d4FFo6~XJ^s`1xzE_0b#=xkjg#R@N= zuoIu<|4n87T`zuDW&q3IGwuMt8O*;*%YRv!--yhAs?2YQ=KrBGzww&?RGEK2^{Ii==^6yIgr|$kAD{=HIxwIjU`MN2ylveDJ zl%IQDnMv7qZd#Wlh2K5J|I^*P0MbLklG)NQ)HmBv2NfH6c#TGPZqH}cQT|rrA+Tct zT|fy_3HM=&>VXL7!QKvHJ2~jFhRgd7$5&G;f`qZie$dWhuP{**X9W+S#myRUwoJ5} z=$`sG{2fh53f1jLvAY=2j$WV%;ZxO|n1Z-fxfh9GQ(cu`*f|x6FraU9^_Y98#YRAxv+rylgGly}i9&-n@+V z-1x4O-=7_EW;zi((!scW=$IN47%QHag+EPwQ%fu&B|&%-mNhk7S$iBxX~F@mJ1(m> zQP16E?Z%?qSDLbpp~8ZpKbRZ~Caj)8IE^>mudcO_CJZ1OIFL@M71H2|9a*a>gHwyI zzskOUIybnZ5D-Y*tUnCAKKx#lzoh$>o6)6 z-#!c}!=2n0J|V4}npt(qE?8j`!WfMY#Vb$4nr{16INLBfdp6Uyho59W?liBXeWZ)M zPyxBC2Uc+_x9V^d4xjG4^H@iHjkSH?#xpe^&j!JNH6ul$qtbNLR*376$CNJKzUN^z zw9Mfxk&6{(E%4kufnpf8l{e)QT{C87q-CSOn>f~?OIti(*ukW`_mj*6UXKq zn#t=lc_(9|lMo#H@~-Q;g+}c0LcBEThQroQ%?aT+V_6}B^HMx29lLbhWqr%c94dr1 zi_bAfuy{ZS3L!=SMXqb8;lAvJdB2{Pj1z*$RiuTQk*Z7*O?`n&lM)DPd)o#VN>y{rgXXGoJ4Dz_m!jb z2xa>bkC)v~$%MYy=Z|J>4#@(1PY+DKdnvclmO@)%Bm_7TD@+gy+c7k3O9&*Slb&lwaS|OA%p5*#EQPODF+cM0%se&Cz(OO>X! z!?9D>>*P-sAW@8f55oCzi?!2uYewZYq8d-I>*PwXRXjV|c^b=ANG&DXsGfH7?ohjD zmPvVC7v!JdiaN0R(78B@hqw-N$u7Y)_7u(xZz31ZW*z?JN8@0tAA}J znt=!7ut~;eiG%rcPhE+KF0{sYZ=teT!qBvAxB5Krrcc^^A1h6ws%PUVyN4s=Q4GTU zEOJE}GYO8)1BurC@n%Mw_#k&vj~{<&p6OH~YqNEO6SO9bIRoVAgLoVvd$#+uZI@NN z{x)K`Oj8*T)Mco<2xpt7?t9<}MzI655<4UclulD;yRL1nJR5rp9t*oJ6^Qb7BIaHq zN1N?c#HJ(sFrIyn!nj81}sYm$qgzqB&Mrmp%E^+65g`q z?wC}3<2Lf#DW4OLKbG|D#R(&_ixv6%y{ZDP-~goFukVJ2Hb&{3RfK3cugUkD7M?(U zEWXs3Iqq;sWpnR1=IS1se!d3=l1`P$>D$7MT4dL)3%TSyNnZzGyCsm5V(ylVbiV&0 zrIFe0WPHY$ID*%{_Z*0m)Vg(!E$spWE@I$jD4V&d@*NCXT%Q}-v5_^x(;}*SvN<(S zlpCKlCKfK>-I^(O1|$gGFa7W-JnRI?c31+s%s>M_lAi-o^OUFlt*$b3JZWEOfBiUi zy6TMpKbRh=z~_c1BtiyB4>m&RUpb$`03sB14N=U2%2r}-%iqrRjl=GSKJj+EctX|a6I zB%^@y%s1H0yHBrT7SO+Cjp60v!K^X{t=fA(+Q*~5j$O)g$v_;0$CFR|f%pt}VK;L4 zN5DJjj&bx0^o1Q+E-j{r?&jhyo_u_B#~Gr;$MKKbl6C5hI)}vfjYtOy4ylf`tLqls9H7MYZeb#EX~<<2`#ZXPm1-iuw?StFiADHc{wpL7QCWr|bF#<*WTB z9Sfq_!8lWNOs9nD2?~9YjA6s3@zuZsp`} zSNg%mDuqA0U|k+;dG`sF70heFK(TFo{u#vOm@{fen66mL3l?a z>w^AsM8ZsWR||UfD`O8_>C2Y@DtUU_qlZ=Z;BpyV1z9cYK2@dH`GK(s@utnZzcz_l z9=?A4cfsl}Q<9mB>mRq){)GwuFN4*aSNYGu>W{qfe?k=ARLOq|R)3q5|D#)LzgP4Y ztlq3kz?*dmc(X16Z`LK?&AJ4!hh1_ZnvoEt`*q_?b4rRDkj2`vVKkctKT=+B#|XAyU(i*Rvdc}`xf zZ3S25$7LqPW&-3n@_`O*+hr46g!Mho@;!EU3LxyB#t z_dLYaP=?-MQQvEZ+!;)8PZt&s6PM?(9}-2E`oT90nve$OdGfJ!=2{Fp zSl!%Y5Xj9qkYRsjU9P${=&`*}*!sDf+?}P%%bL)AzQ63#DSyA$)rN6ixhVTfUWjAX z@yb;(yZH}Az>~9It@S7p)K?(@70eu+-ae9Y3}L416^1Ai`eKrIA`%~V{`P%P7=$$u zckxGwX=Ut|h6KXK4opSfD+@B&yORssQv55JA7Q~=qU+;vqp)^E4=hRT+H0=%!nE1y z{#_pBTN+1J-oi9`{X%|P#972uirYdmOruqcIbE8k!TK{lT6w_9lyHXnb6IU)k2uhc z8oA~^;$I5#=FZ#h2`aWncKZV1D-o%Ggt~K(hvoUJmZL?5?ja4s5YH`X3*1B!=G4P2 z4Kd;WMAOQK-!5E5%wnd@XEdwM<>UJL3tqi{6J`KG{JQol^iFHlRX+b`E(?f4zo5$f zyFFnAqpQzud51X-;U23oe@f3{zx3iP03WMOPI^N)&lJ zMn;KlAEf$Yd1ax_onCSR*)tzTVZsOzxCP6^=x6)oIFOti1^A=!8ppg;#!}z+Nv^5H zdJZHMZWCo4lO)f>*oO3t&SnvIPZ?p;70tS%TyKk1o%Ve}+_>7v#Ykpbh^wKngR`qZ zMVaWR#0~EPt2_<@J#J3uv6b}C9Pit=hwxHePHUxgBHF+|BIU<}k?48TWSw)~#(+?H&~|rvr0silYt+&6W$lIa9jTbpk*~g+ zCL*~e{~XbK>{F!BmmWd^{2ZV@Dr*)niv-=6H$$!drvetycd&+g1(5&$Lj7ks_@hQJ zzZ%Pb?M<-%<{AI09RFRRW`9Ex|52#_tUv#kLe0+fryul>LjC7c|60la_Z^TwSMkzZQv5)L;>?8Yc6Y8HjA?*J`Gre^}*xv-F|5$K?HTCQk#gV;kt2MUAG~VR+eR+P5e|~J!#F8k706s2W+Sz<9T$6wL|Od?rILx0*w8Rb>;3I137(cJN~XnPRK z1^1)Adb;q|*hlKYkYwS~D=lK<1!PoaEVZWal!nfotWgfQRC>R%6u?c6IEh4!iGoYyfYFn6Ek48TVw-nD!@ zcJ-{xtJbqw)gleQtJ?MqcWt8pbNBuP~(sMP_}GU;_m|U3={nlLem7H2C7z& z6^JQrFTo2KsOcSP%HzUZ%*CWW;VXu#X_Jx$zS|LmYR^c6t!~+XQp4UiM)piT?m2hJ z__kdJWHVRW>9G9C2+tuzOUfB6X`rMv%8He$S|R?8A!-X#G-ez890bgNP!9!km>?cl zU;D94N=7rK00dokCFzG3(!y+1Pw>MxCEiW}IU#?DLk%S>b(lfe0g|)(v4KR>3;-6P z#mP4?U;b4fH-ng#4Y)9kb>t3)MMsNrPB*8;eVtOl0 zw$zg*XniXysegMXY=#?$@-VT;oV>$=7lw_o5|Dx2L=ADg{6^cqDtS&uu3VaW0N2SG zfI4?w0gQy&0`*sWpos(3RTnxGP)PLosfo&EVmZ6qM4C90wdVL0U`A+9;bNKA;A z7zw}WxDrRq13wEP##bPwf^1(U0BU1QqJ=PQ`CWS|+ zY!Ho~%g8iB1e!+?d-1O7R z?9p1YQNEkJS))nFd)Z}9bGl{L2=9c=%I_s5@$_dSA@6SQuGe>5SX#clT%Cq43RJy( zbYj9DTeDKY5uJ^x00u|yh&^^@2!^=vG|$v_SFjm7>D{b7hbM7!)R_?h)d)db4~rA7 zQL$VYaK9w>;;?}2bkTZqVk7>_*1Yld%VrNZh#GLjV`l-_>Tur-eqJ~ zM9t+g_JvvnJIrI5D4+1IoJu;+iTLOrtb64Q5_ukk_{+#VxGm^E+rS2H+nU&SyrTO~ z8eXsMD%FWHq>${k^-=A&ATBK-3uqfT;46-5+1-KUpvhd$)(^MpF8Umzz#0->I%Q>c zPKtqDT9|pR2j*Io&VKOwNOPlg<}nMW8W2+uIpQBZus_IiGBMspE0?k(=Vq89M{*$bBt-2`;W?*} z)rtk9u%S*PtndBsuNfFh`6!b8t?ptO>c_|a`JX3VSV9V?+Hm>du&T$ftf;f+e|&A- zU3kp5r|kbB?yjQh%(6C6;{+$TTd?4|k>Kv`?(XjH?(XgccY+2I+}(q_L$E`oNLBaw z`;6|sIeiHi-(LG0j4@x7XE_H&N9)FSp$?iJPO?tLdNnn;)lt!M`t!*?rOn@yDq*&E{K` zsE@5Rwd)`n5_z`48Y(L3B7ez{V*l z{h%AU!e*b?Ka?_4f8^TXzQ=cR6px~&d@rhGp{jIRRi>%f_7mnwOnb23IzccYh|5&; zi#)&eDSPxq6vyS#_SFsdoz9#r3LAYO&5JlcSt0TFGwq@BO4Y9)JZ{tB45f^!?wQZY zML`BGP35PSMuO&+E%wu_OfI?IzNr56I%0cm3MubOdpw>*A#q<6L%MFAfaL8KO8t=U zDGxwCmFXQA@jn)674(; zWKpL5WjZ$gw23{`QW2j9l^?1CuPCS`oFMlo*w=h+k$n(D`=mNl^o8V}7@MML$G4s0 zd;zNnofYZ^u)yw`;sXhjUPbe85~7Tz^8Tw*^MhfG<|*RSXA;|sZ9E!GYq^EW+_tQE zyeE&a%%KY?N=}U?IqtB=d}#)B&!JtEqDeG9y(=oYsA^YZvd*)! z;e2FgwRd)$eZiiyqEBF7*DZS=C^5j9R+C!MvdfHfmXJ>#JJ7zcUS-np0JDw(K6~M% zYUyfDjCqihN!9cU@XyXQ9I@BLhqRgfNc-k{C?MO&gYTaJQz^b6`$0UOQbMj*)hq#! z&>-vH3>?ZbhR48r(eTmL|Z*IRrk z<-IN5KMG#gK7@s6OO9JZnSR5;N#i>>=yInNpmkE4rc@FN-F1;|1%i%B{W(nm2h#^E zdu(|^I4DW8R)5CEia;m6FJBvtR_&Y5e=ph^lMiIMI=1JNm@tfC7j2$!gZ zS@}3ls>9N1(K?n*3M+3CY4Wt%V8us6AiWHZ+DG#=E)&R=GWgV-M)PLg9|BsfG6M5U za&zRZL6fsPHQWxkO`TEkFkZ&S`Fv@^0A|S3^SKJ%2VZ2!eLfQ4dRA-uLFHa*|Lztq z@dsgpNc!vtng)4%p_6ZBGZtg^aVDh*z-=-b{9To59y@%FIaP_SE!6f7X;{=b9~|`b z0b1i1XSi`M1I}*o@RUx$m<@-$}RZc5B5z)EOs8w4p947PAh zKC;i)sA}^|Ba;nH1`>f>RVp!Ke~~ zFTyC(-Av)Zt;U?Fi7RmY&yn;;)n)XQ*o_2uy# zHpZ-zFE6Lo+@23a-^fKc$=eKaUHn@@_H$g#=t@8Fr(@Bh4|h~hU_5jz@=uyOj3>j8 zgWK-7sMg9f=3ZKSmHLh%`M$?)7~50;5W=xGFg>b8&%hUa5f^9&*I!j*qkCVks0fZ6 z(_j)DKbUTwHSrQddekQ9nzGiPVeb=9pg(j0LNFp+ZokI2vJiT{D{Cp$ zmPQ>yza*`1(&my?P5u>sTWT-Qk7l>_rR3)9q7UJO*$IWB1xQWN{F~un;LK9C)@BwFGQjVk*#&Ivv>v<-suG!6e+8xkiyZdUENx7ue1XMQH7FEs}aZjRbzE%o>xR z3if?Yo(`i6IQ}Ud9Bs3TJvgzcOn{uAORH59Iu?^uZtYYX%duJ)9zAWu7m2%)U~cjr zd}?7s2D5toD-@Vyy9|=K;%6D=goYVMjw%d_4_n0?Tu3)@G+DtEj;7O3`4wEa>uq?h zLGLo3mTzPF$+2)PnF<~jINi?&An`8z&&Ix$VGQ`WyF0l!TaWH&w0_w_78zp%jHspCJ)KmSJ) z1pL>UAmIOKf`I?ACiv%y-ty0zCJ1=b1OacFAmB|C1iWd2fA?E{ae}{Jb!0FJh;_qbdtTuj}h3ut&aIfT@=hb-m5 zGQ{tCvadYZUe}t(Ap3Ym~nRXTqRuRV|4dv3y5a zoCAL=Q`s`XNn=^5CCaxTV#5}t!h3NoLk6;rZu5?rvYp^(s*=NSjCAZcTrr9IiKWUi z*N?=?CnlA{Z9Sy5Wy6bU+&s6P4Q2Fj7nTWmgeY@PW!YX4N%@z6ALQ$G-z}i{H|*ol z_D(8E!FkC!m`u%5lOIk+({o3VUF8hkhX&LNyi;yGt5n(g%{ zwXDS!hP=^d%E5=HWYp&Xm$&QvyqlcuRgg)cNF8~w_+7HFXyoSI4NbJc%_MC0RpMwg zzh$+2Se{6;DOMzOSeHuD`yP?l`YG$dveHF%c?4_+$z)s;;9`Dol?h%mj(2!Xg+ki> z5`%PzoiLl)wXlTRC(oL;S{Xe+;*7_hn{+zBj6rtI_S^ zi_7(Q=fo#9STk6o+AHJ1J35whlg$ixua(=8(iO>~6eEM?R+RAF9{`gsH*_u)ZB}so zY$LAE-tzd2Dm@0~Nc6sVzmJFf2Lzn_ymuD?b&slmn+c z-A$MQY0^BaC6Dq;9P-)uYf*Ew50)UL0qA7s*Nym)oh&|hV^VA=*{cl;%GCY|FP&wE zp%*}0+}}Pxhkcx%T^x=ffYsY5{sy+BeG*i;n)ao{4u@B}mH8_co5cE+N2+KnEK=5& zCkp4cju&1^)HQz?AhD7ono zED^RJoDny^BATgKw&cv*fZAXP_4YJTZ1!h${Wt=Nn8SzOY$amVHYp?ZLj0`1ht4O7 zX6&`St@RmN2gK(kC=ecJ<3eFccg1#xCNPUO4?b(xoX}MLQwYR*B$OKELYLe6_5{3j z46?)qH2**e=UI5btS~71=h2);dkKMB+jp^@X-7Fk--heyFxnxM7lWEO1KrSc*xdcN zto*4zpPeyM;`uj)VibG^kLOq{0ev?ulZJK+KKFRwm^}5ON)e;wG2QAFRuO! z#t>W29L(zLQ_l{J$ShX@(Y)?(WbnRfx=XdQDql>S)H7?sIb*>z@h`Cjx$Q9+ZEeTj zZ5r^gwe)4P9Y}SL$1VeDwE%()3)11Lr}5R0SVp_4$16sU8QCeJFE59vHpN(wqwXZ~}6kJj(-p z8LZmbCM!l*UkA;|=0|bLlrBf?@sQ^34<7km!=3H z2&_HSGVf3N+_t)hTTrpd>D+2EvA~Y89_}u|1)9wrkMGu>0bng8LQm%<;jB`qWb<93 zd0Mn+EMr1=eH3~7%wCY0=@phbU%4ENkIWv)`)&2g`tjp;?0_y)@P#fT>hD+^!JidK z7HAQIV@CO2N2T}%sFwF4GewK-r*^folnss&vii$UA3mR)2dFd5rfr>R;1Y|NeN-7# z4rCpkMtjC3b91mAv5GXNCMAekg>Z5+`O#qVK< zGWjElJ~+ST?h+ez;642{OvGp&(8kY8Ld?ybdWr z@%q!V84jv+8yMy-Zjo+|`a$qPwP8v!y z!&E$rxjf!*i{!efQt91tnmTG zP2m|?wos3kFTk8Wh>`yy%>Cu@G0?Fx{MEbrg&Y4)A^%~R`)%_5HCF_9qmlpf)60N2 zCix#Wv;Ij_{vY)2{!$EIdv|XRA>hpt1iYzzfH$=d@TT?w-qb$8o7(qxxckNS{eG># z!`++V_aB5iP3J{c#Me{K!}@koc`DCNw;#9bmrjJ!AzB~CuXve3mNCNBh@c)0Z^as+ z5VweT!3@SVMusy#i5f0%v4Qvv?r~Dq6L~KIRfCdw4 zsA(N;<}2JVZJr@4mkT8#ZJvsqad7VVM7;S;mz3myUm;eK{?Qj z_GQrf{VN7b+*MaHwVQ?AYh1utrMf?p4{5P$3}tJAk-7M{n;k@`&R)@@#_HROjhc8E zFc<{aV|Ng@jxnNLpEhcp-gzN$4X3P~aO-&u`pt=)4nTn(y1{d^w|z2Rxk>KF9H8G) zO~PY?o%GhI6mJ3fnAUn%5aGe^ure`0#T%uzgM>N#5J5pv@im*<@%*GaV*UMCTyl!f zahX0z<2sd#H3~stiAkN)<6dWuEuE-ijS3c${Ri_&9UO8lAe^66u(ex9t(U2sBI7J3xQOq zeQHn;V#3YDBa1_Nw`vDvS>A<|nX9V3jM(v@lLw)F6`hsO=sJNu_EXwi74yHg-c6ZR zg0q~@zAk}#vc5?NU3H@uucg|%ZoIy#As43zcUSG6sH!*w|0yQK5m>Y5L2QOnw$(#jM;30q5ctxsLfFPv z*(cmmT-F|2Saq%pg6L-$vV%BIjKsPMyh+VxqEIu6U=0b@E!pTzVA)E^8j5FspM0>` zToBX^GXs*1$0R`2Ve_E5{?_Tc*&T;2aVA%8(Fb~UGd1CijmB^b+~oRURJrg)f(a1; zb*3nF3m^swm6$@#JTMl7{N(gVa^AcmV}Poa=48FOmE;UO5XAd%m&Iz4HTLV}_>+m} zh~Ad5yaXcSAFNW@Yf`H#&zDwfLZ{Opl0R&Xw@QFLto;rz0uR0<+pQGMhEb#RNTD5_ zc;FXU+CoD1)1ldp(lz4F81?l$b$CKb$S38=IzU88z0>lBcp5kU^oiDxTLL*qYASxF z33`i%+S{@qC`+yMxL%Y$y>Z=M%gV^fi2}xi{yLIh1GU%- zf$~ChUTZD_U)!2{@n;QV>Zj-#FyW!%D+d!|TPn|$nt#RZl=vIAnhP(>`{jD*nIi^R3p#ps{U>h1JQ zFl3Vn&yBWa7k8gtjv8Q!w~rWaY-LsxF~v5B&mrv@!)oe&G{(KF$vy;@W=(r1?Oy-r zgJ+$!?=*Zxa7mAQ^I4LYU%`-WL4)XlQa8&IM_bd6wbvn`K-i*5PrSK3q`zwxQ4p2b z6NliZICJI3g;2`&J^>h)&?ND`>6*{l!d-3bbtC8Cp5ekh=JzNv&e(w1Fa1;7*L+z{ zz0x`2vXj_A8i`qr$=Zi{I_^JsztJn5l4i%YJ-tqhKakxc>F@YJaU0np-a;a+C%@XH znNhi?_f(Rb&lroxG~i^k`5eHYE=RWzU6#RA8C)C#l?2^4#xJ%HxS5hkwU0F&1k+*U zxqOh^$e>~2ZbhbQ;g8Sjbw=mMUsv({@7#9*L;}$x9iAMvw|dc}`tEJU?W&E{_s}eM z%ibK6k@S`17x^o83X@FGiCpaYW)kQ`$u5~@aw6+SMooo)_+lXuhseuyaT+cw}3Yi5Aep?0p0*Pz#Aa< zca{Dn$o>8jzpL~cC--lwbc2dyeE#c^*9T>X5z`_wC4V5o`K5|FQ9$BTeX(7Ly$(;~ zLVp*CR`TWHCGiJuouhArsihYzE0gZk+D&&-+54Z^?$Bd%#+^%*t{_pb=&V>t;A}6~ zh_B9IxKx;k!C(fRulV=sVhTdeRLUoo12zKII#cEPh%U9b#iiujBHEZVCwd*q^rdzk z#GRR*#H@??zaX&McaURcYMpQI(u*oL-|%(V7!!QhB}AqX#qDoW71mT@@SZBnvsGoX z#5E2S+pj6RpbA~8pxTqyY&4YHVQRxoj?Ol6-U(Zq4J4Y|cwX7-sZHHtoiB$8!P&KF z5+Iq7&+Pcwu5{hF)UyX$P7~6r+N|9QJ{;g8fuz`g<_9we$zTu_(SzqR{|@ck>iNKP zne?^S{4U#-19}vAx&KZE?O+Z30*6$ z#+Pho0FM-Fl##xc}a+{mx*9BoBctExC&_z0ZdizCG`()0k! zdXTH@B;won!_9M!4yDa@P3M$&*UKH(N&xG`PNL5!0R3c$ql*Y>VMyqB;tSnkP=xz1 ziKApVWiWQ{%a-X#K5F@o+f0O8uMV?Ut|%XesM z?bPZ|aZVeMT=NX!2in#=98q-5ASpC!!h4DJIuqyD{7!pf$+;0@N;+j{csnZ!YM$H* zo#42q&|-RLxagTUo=q7F z5%7zzDrCe(JEe5LkEyH6gNABGgKJ^jyyM-(h>|IQ#w3(MMyFp=>!;@1SzxJaaFi`C z&_J=to<6yB6+cf5$YA^!6c8bznpm2*GMDvAE$C2Q@qf!J`C=gwaI85_QM!iFvPBo# zX%2E_0JQnl5duBpGPvnJYILaL-Z%Q0;T7hS{itK1v7kDV#wx~PQ2}vux)5{q zKIy*QiV474A^FTmc_f+sDsYH{5OSDcWXE2yoJdxNr%kqOuY$weMAa&4+NCF>sDfZz zGg0Lzu@6Jl<0B*$??AAOU1DdaAG$E;=H#hwr+Klo4G&7}$#)a>TZdYnrO@#-ST|uE zRZA_^2GIQ50_Q4Q1FKq`xS^&5>abd+()Zbb+Do-_z9>^5`+3z5Qy;#$lB7s2Qr~tt zv_+^>#2aWHS~(ZWwG-%ZU=d)Je;sBXJ5a3X=u}#pE892Y4t0#@P5wG?+!}O0Ec=bC zmw>s=6S=rve&^_fK|=-&B58eCKQ<|1XkW^Z7<#hdaQ~L+{(5Wblv{O*ca$mI*Ti2f zk79MlFhfgpKi{rHM_VLV$v?rwn&*_Q_g&sCb58hzrT;!sv{(l#my7REh7U)4P4*cb z*Snp?ZLN#$C?&AA~eRn382V*FCU1>dX`xX(0OhZL_aZ{HsC5Q=+$^e z#ar7rQBV8f#aj!u@P^V(Lo?f3Q;g*~bc(b1^Z2fTlaas8S3Q}K(Zv8&zJ+Wi)*oQcwfdQpaO;Vt$&`Iqp zU*c2NQAAM-_VoNimq=dan`0CUTEgHe_G$tgufpuiW3&TUy@LYXI zSX4ROuQ9#8dq;Qcx{#=XBh>Gok6hK#ORrk?=;|&lmR&XOBK1tl&a1ISz+9=TM7DOk z`gkv@>`V4i8%hYdcyuxnLAG*k!!YY?gR$0eoE49H(z=dvFz9pcqeM)g|KOA`1$OJn z^#VHRm(Tt${yqOycK$5*Y^;A3{9o4T?+W!F7W_8{^&iffyh*A5MZy0?x&2eY|Mk@0 zEBPN3{J(Iy*Mk2>!~xzcSHK%0_j)_uuUQ|!8@UE}Bi8_LM>9CBswfvr+T?8H=H_;m>Qy}UW1Z~J!?=Pkd0{8`SfZ-@laEZ8D=)~_ z7Ff_A@8eF-s^`(^1$q6}8d}xU^@%s?UQ}d>kS^0+gyQlBktYjhqE3`4x$GN?l&{j1 zyeDocmv;F-z5*d9qhHPk5D;Du<9_mpr9DwWEQ}Cbm+LFO13l1;7TZ}fE8pW)mQ*hm zEVOfLTT2#9YJ44h;jdKB+>gW4=G<$-$J0)?e)K3oX^FbhUK`Q)qZgy1SukizlSkrK z5{`zk-9e!^i>8e-6D-G^0P-MGStO=xebeylMpXt^O0jmBXNJvtl{CE66jMk>v}_aN zGRSz|#5lv$HR%ptlUrQRJN@oPf2P3P@k?JdOQqMPe^vGrdvS%t$nI2-UBeE_3W#$- zh<2Q(A5B>@yM2wD-*O}rQKn1)mtZ4uQpQIOM#PDcGGRGlodPza9vu4T3s})!ax^Ip zQ7rCAN+!1k*;vFC|+(jtc_1>oC$pJ(op5 zY~E$$Bla;cp&*fyWweZ(AB`qyD!Fs#$qi>2W2)(6Y;3_NoC8luCP+4U@=@GklOj*< zX?s~A7NnX2#((RUPz`V9F}rfb!L#va4(BUnHl6*EqF`qR`c@i zK)-)`+Xj1vk4@;h(=Qw%B(86glmAl`!l+o{Y;<-1qbu(b)POh@5|iF{KWonK5^a*| zoz7!PP~zwsHCal^J`;WnB2>@{xK7d{Ia9eptK-21-diPT%nmzDW7nfNJXSoN(ikTQSbsyD8gZHx_3t@n<8VD8wLJJP0LD1r z1Bcj)V)5i51BZNF%*^Z-T%U$0OvEECxTGOkhYv*?N=9cne?Ti$#Z<__E`5DoM%rW|R*xFI#d>?1o{p-@Bo&rH4QN8du5KLX% z1rWNO2sn&wU;Wd5MNi55p3C@rgip}m6)A#Jz@w^?-w5pYhbM{>g_ziFb-j|DYIKvrh(nIvk)6&)ezxkd4bJmx00|`6 zL3C^H31CL*k(kU$)mjS~4llIH8?3@9o3~KMh|_Qk2kG?|qI3knPGng%5~KvtW`qi1 z+TN?L>k=6tE%a}JWMKbH{m{n_>aX{RXT*vevA}P59(qj*dTCX(6nuO`6pnw;eigq+ zgz;6MQneh;nu>ts^Z3W!13!zaau_jN%CRiS0Zi32rxZ&2p&`1>_LBh7xon~3v|Tbmq7&^Z2d6hR_10TqZ7bHlsLEZ*oAJ2T56q z<+$rk_W2W?N5exG2ye2FozhOK_u#Y}u zB003&A8*+OezXUtw^_g88^W+O2d9mkA6Fw?JTOG=v0B5mZpVcyj#lxwiTHX+SI=h$ zH_cQ?hV)@;E8b#E%i}X$h_MvbJ<~Fnv;j>fYw>*lC6Kin3~4R*i4b-xQ@V?HSAB3u zr~KiSOygDY#+FS=;b0U@uM^SL{(`Kq$!c6wdoHXaXAR~l#ykFdt^QQsrFZs@=)$$TTGH$F#p>Da!VHA`I?Xjt*5FZEh z3mo#Ld^4@SS{2nH(aA1rJ~dSJVGGfj0gRmY57a@mO4yDRxVQl#dZ%GDpek01_YN8{ zJqi@IyLH|}XstfqCdSL8>ZFZd<~r_fAp^v;9D#g7Xo?FV7}&w?Vo2P;1>6 zUPhc;jzWty;nmk;oKSKaf|8v{sL$80Z(o2B;4AC@MOgYvLwn`({~8zu{1Q}uSMmQa zEd3VI{_bVG`K$j$SbDQq|Dl)hPh{)=AT0f5$i0T8Hw6yxCb(*SQ~ z8sL{_BS!#u6KMc{DrwMwH?!vN-IHI$%kS6yJ2bs{FaLJ;B=F5t+ok+DQPn_(wUX#5 zR$^dIYfBR&W?6Zwi|8*)M4b1YaP|AZ={_$AtiPTxD+p6@Tk+Wb2hYOWtHj?;j{JRO z#VZR7;_aYLQ0~*R z!D2VYtGp zfxE7QGs2-9r&Q{B(&CX96yMYHz?DR=7dNRByW8wLWYTL3t;?48u+5SK2_J`&8zxXu zQBx&5iLV(QFhjs(Z&Ys^NEge5smj#a2|_T@Mwn+{+!w(?5CthMBizOl%EOG(W+gMX za5-NvKa&Zz(%t~0%~diz!!9H|9zfHbOh@^RZD!m3al>6z|oN z4~@z@D3+%?=C`1c+iJsFfX+eS$Rp-h;*L`z|iaY_^aXxBpY95=62W2R*8Xn-^Md95WSOx70(DlHI8`H2*f zfr{dafz^M7@fy0VSv_`GZMQ#fTtjZjesA+Bc>aXzC4Mj$m3|PLOw~?n+z6kw*5!4s z$kR#PjDYWXwYquGqRuvK>9>?qE3s=8L;}b$Ll2bs*It9Zdnr7-Q?g&PV$f&P_Lk$qMQPpR7IRtHwFePoKwf&p zZs#`d+>{A&w1^A4c(_wrBk=IGk8xU*>{`L$IaxM&5o(VDf=&SCsb9IYa2|EjLa4-X z&7eD-!_PD_^tn~Fb~t3=E5o^ZezMeSqH$1)%U?eJ464SalupJbm(T-VT9?6P&Q=fY zZ3nm&`oL%_DD15#X6o$epA%S>WQ~~ykKvCkNaUnKAZ{VzkvR^Ie0x{F&r@;Da_3_o z5a#gB^P+NY?#Y5}ZkFys5U~)xp{4x2CE1LI4u6GFHcL^U$`40y3#)!?3bkjM36O+^ zqk$WqG`etIyKYRC^y`es_a+NCmc#_`s?3lBeWxE{gp#8{W4MmAlh_NatLX2PXU}r2 z8*wcr(zINhwzpv|8|WE7SMq~j>-hOk=YFzQX~1^?*QCtlNC`d=m*rfxiPQXm-zB5N zIw3hqWU#F~QDH)BjA6h<741(#Y1k>Fj*s=(XZ|!1*EI5WwJuj#b8UHi;Cl&p^dRt{ zLeDNSd`tTJV^5huI^OBjzC+(>T-Ic@3pIWNA=`*}1{wV#zV{J=1^25xbr=R$xh4~J+f!1q^N7nbg)*eu(0IIwsnkEpy3D>VVVVjHN- z!PdjqGKfi75RA6IT(I;oWk{m7ACRAsI={1TX6gz<@hU4|q>?rB+d(k<9Evx$_1Wh8 zfZG-avWpGHT7}>>9X+A455ezS`c@q*CA(_G4tzM7%)|uOg0N0zCDzTBIO;hGvGwzq z98T7Kg58oJg{Q5LzAwoDfFMl2$GQxFaVG~UKc`92@;e^|vcZC(?GoXxOALb_Zu!z$ zevE0-*I-^aW0vwWrxu8THxjR;Qa{PgM?)0jDRlMRx>@rAE32KZJ%DG zN+@iwFx7euA=sflU{}#P6_{irWwg7nCbN3r07oGT2YmP%3tO-?Km@NBjI-VvpRH|W zZ4DU>jDgv6+7JJpwYzISI={(lJlEQ5T-XCpgbzxWMLS9CWbReJr?A^JX?^vFZ`mx@NIsQPP?zK3TwAUQszQ9?3t%~3pESlui! zrqlRrQJ6SgDXcUA#mmKyx|MI-r+4?_F;7dN`Ishc?xZYP^Tt|ul3yQxz&F9)c;!5# zSD)3xA)s(L)%|J&J?)niKxN%_yx_;yXjuM>sPn6!{!wM=S?T|tLHdhY`~SFH@n6j# z{qj^^_cQ*EI&VnZe-U+9|I}3eA?o~-((*ruI=@!*$NPDWI;?*(a)37l2Jj}i0N%_L zz?+%!cZB(cru@Gn%$uO{ZzD|P>lxhF5$xA9xJ51^s)|ezx4j?nE!d?p`r<4n6ZsQ* zp*7;teN^5dh(11CQN2q;K&(|Dc~3xB_i1WsrHj)u*S+=}n;5viQ@pU?3wg2f)c4$a zqJc-Hj~zcz$&^sReNp4AFSF;H^uqkWyv5nn(&*qf7M<`CENTuiHMdk2HrS$%(^b z9@M5~mt{18-?#l%(FBX+WGOSJcID>SXE0YfQx2N4t*eJ|zp%6r&pADoPXrpOH(Kgf z(})=?8Ml$RrNheYojYzSXPDqiI^azk+*eB=sZpyEq2+<9qaPbQ9K?t!5`Rsf@a1rUk5 zbeNJOY#iK!gC4BeBeS!r_VS|9K~Z-~=-UPUvdvhN5WKOB{|-GUDV})3wb`5#bzkk7 zu!;1CSL99nhlxWL>v!3c_=D?hNvFD2QM`+rhwi&tQ|_UD)(TY@sFKoHpQ)~c)NQ63 z*B9&8Tf~4@brW26LosKBM=DH8#i#CX9Kh+Q;MRew+$wD{61(tuO=9$>u*gw{XrFEeGRPcGkxppK~0#5DqV56JHDtmt4qRswif+B1jis5GO2=BsuqmYXsC-{ za;j^|4FkC_pvzjvxGmU2Q=e6|b>_k^}hXML2wP%U?$ zx@3G}F_LpQ>7?gUvg*AITt$}|rLqk=W??{vJ5MHkRnYDH3KhU_4|t8#Ep9)Q1kq@4 z#LV%RX0mbs12@7P?X<3LwanI|>9qK-=<>5g$tuzjc>V;WPhF4TkXs(+*t21Z9Q`4< za!!pLBh71;LHGOXgOn>)@lKbmUha_C@WVfW_jHHYIABG(0XNb*^p+4Ob(AGbwAiu)>e&-1Rd z3A|jeky(&410-Jx_QOg-DVTM{ZT3*UE#{gQ+^cav1oIQIUS9~zvbN*q1ON;0m#W3) zSzIZ;;Um^(((mJ`#Fj-c2$H_Y=XwX+#({12Iu~kX{_dwfeBvNUg|7xVFF8z`KX?UD zC(y7CRP#LLuzZv|#AK@tw`e;H*IFbHgr6iru={;CmDj*zJ0U+1@d+Ibd7~|niV@|j z@rxy$#*+(y^l2QD>|}yuJ{_VLSgDMnWJFKT>FbcGxkxm{5P5P5#Lit*<(OR3b|)(+ zn`N^M?a2hO?fRaTwurf*bS*vRs3jFroZ>ds=I=2e_Xz$oyt$ zq=|dYUmn=ZLTI{S8AXU7S!1v&zo?%)%yd7CWwd1u23aMQu@eZ=YZ)cg;Mywku%T+( zJP+Hb<3Y%vY_)TGDEZ*7Ou{dgEP{=+!Z-CA#HJJZItEP6U-?iDH0B~ywNsO)1C0); z7~`-6q}Sl52TKlP7Jiq4ASb(jhF~fFG)Reyg3y6epN#|rDyQFV%|INqrs4rjyw7b$ z$LP9|E~+2F!Fw!XqulpQRuAl?^1S?!WlY<}V#YFTOrKDX4irapSe)V32OM>_^6&yp zn-;nJ>F|$h>*f9DiOQKc4@f~>Mbzlz?`~19BgpQ8OFeg`*+YP&{U(R^C}>IyzIlS7 z?oARn(Ms}HI(%5O1KB%Dr62gA2!x0XLwQ#^Psa#b-o4%0dRAeDZv_WK4&d&X9^x^D zT1SFJ_yKE?QhPQnk`s$-c^SpdEKJ0En&>0H5&UJ;`#A(3VI57wPO^1s;^>jp0}J;4 zWZ@2ze-(efD<(TWXA7uF%0bWF*gSQ`k_>DC1@r6Up0N@?w;)t7=K%kyxA0m6q_;O3 zW#}B{i^C!Ox!zWd=L3D_F&?FnyqB;Z+wsPUcWbvh97o5zMFG*Gj1Q+{aDIf5OWnn` z_w;C8?Z1cyzY5i_XaHdPyJhqX1p8gn{=;36H!SQQqQM&{_FqJUH)!l1qQS4H{$9!d zAR7Ey(I21x8V%m~GnPO3GXj=3{*2|9T_gAU^PjCFmN)S1uZY0%i*fn=CEj*9Sl&#R ze;W~Al@|M@MkLSM>iK9%f6!!{VHzuWjwv%@I`+E zTsLVYgZupa7?*?>B?95%tEaF4p&TtV`G#VW{&YGa#X|M9)`X^fy3$ad61JNsYl0xn zQeNr47K>#(3yG(z?Oc23%f%$uxv(-Nv`3AH>y&l6zV;ByIzy&4*9Fn3-Y!eL8r0e? z(&+i7ZS>w8-^sz(dO@k%3l)GJBbr_9_jYW7xzP z1nZ;H4^#!@QW_O5a}0Zj5MNTlB9Kwur|7nmTkhqWACna=G)5z7%3s5G-$@^9H8Rpa z&5JXT(X)?;ESCgZ1vfYaYy&brTJz%9_ZYcw11>X2HX%z|8?4WjL3e1X1|n+Iu*JSOizvP=5%wX)oUIzAZNxJMyBBFag8#0DAU4Su8v|Lw~p7 zkaY&=b~-U!j{6FgBMV$Y9@HlTq52Hxlj6Fg7DCrzW+y;Ak;VaoC@u$^cA9;>40MP8 z&&)*&->Z3t?Vy{BHa=tpxB&*9mJ;Jl2)b-l=qA?_Cn(wRMx6DHuThS8KY-W+Jeset zXSabBLM)4YNvPOQX?s6+wV>wzDXwtu=b*Lx*Y; z8Je%+EY@TbBAtm@Mt=acBL^eBC^lZxZDE8G71F$#PB*%ah8$dx(Q5%+T3cII$GBp*$oHN zWEt{PyU`}i2Ww1H#vXfw+!K3It20n!$12Q)l@jhEA+qs@~7q%w%_#*8`?ep`|`ZC8`6X=lZLxKuZ zWbzvs$ z-vwb9vU;5yp%OCiys%B`nQOybuRt@Z@h^&ae6ENPU7pkjUSnicZX%kOerR^lAvFlO zC)R`H_r(f{N_{`Z!_%A3+TN~sx6PVi=ekx+zYS0AxkrmC)vo`t*G>;*Q%;;TU7aO? z#{FTH4F^}5o3Iw9x}vYW_a4r(KC1G|k3Dmayk%ceZXfE1Vr5w$p{V$En}%G7^G0FP z<+6-LpQMGfn;R14W(~IUbA}{D%pKO{yL$rdW5s)$K2(S{?XKe^^BKKb@eca>S%#O} zBkQO1FQ@l?4Jg{e5`E3uE=SdOCrXVUxhhZYh{lnhmNuNVHZI_)FBhmu6=cEMqpYpP z)HZ&eRlk7RIJ#v0-zfBFLH?sk)3dVv)evI&h0Oh~-TxvAvAhX#w0}!2EN@KP-%;q# z1n|0VB57z~s>^5X0dd%bxaFvybL6d-1&e}oO!SMB0w7gbU)~{C>viuTa{_*t8 zpHKb0lK=ms(4Q;%^0OZ_D1w_ zEoqb-Bw6b3f>&uRZiC_NJBU*=xz4Y9aRsf`;SmnHcG04vkn6e%MbytBtLs)NQ>**a zrZTA%<%{!vuyvG|@fnzYFc7_MurMHnacl34KRv;>9!CDsNJ8d(a>r6fV{!a2>8c4g zWqNkkhje>{QOasm6Awf`v=y;OGcud!fmNZRll6(}ChiUv5KukF+dfSB@w?DOv9q@v zggH*CJgUxmn1E>QsH6h7{)w)>l^k7y;wmz&Rlc)UQly87Hm8d9BN}P5ds!3M#`0^O&&4*GNu6vi7fd>F%~1LM?~rMG)2vwZ;#$)Wl?cvbt`Zj&J ziwE-rlhozo%e&=;b@!ehxyj+&+K@1d?WtB}8QY@`FN;QyFfDVm*iU_+=6t@B$0Z)2;tpg z?M|u(MZY%&P>Bj`A}_^wu{8qB&&LN4{UUw!QJN2j!X}YQ@Sb$~g=H;0ueTmmoFeU) zGO^tq3R$w8k#Yoa1>{8v*cb+kP)8{P<={V14S$`!%{6`^U#0Bl%|;S6!9X-ean^(m<)GS;JP<6t{E~-+$aK)Z_v7D8V^|b=|ptHrqGP8E<~*-4O4=dU!!3B zPSv6jCmvW4_T!W=J6G1M#ZJ=DpnE9Hj4>ui2wvu1v#ACcWL+-z&epXPwaj+ja2Q}0 znGwM_@Mrkw5S-SzdYpue~O$sTTD-|$K~u73??@V3=vmu^o6 zacy=px|p!Z+}P)Ail0;s7Q0{0afQZpoQ_982!HXPar-f&2$sfS7*<2rc1^PIs44hv6d4pJ4?|3Rd+Oiqa>#|8iAU$OKI1?8&I(3#a^%Qcl{!gy*^%_#D$81Oi|vCv=I`z zzw3$@U}q4ErlY9s31PX^v&%!PH+>5j1%&g@;#iCf6j|g0>A@e?uS}{>LX%P_crX*8 zo43|Qzw@_ps&~dG;yd-A&hd@K-Z?r)`X!Dj`DX_N|W^@HCs2rcz9&u{LM%G+=d3+Ot33u5kZd7w*e?2of$~s3+7Gli;L4Y%1FIok)WQD` zad#P%SC;h;J3w$JxVsY|$bI7u!QCan-Q8V+LvVNZ;O-FI-3jg%9R75gbobOdRnN?q zck+oUs?OP`NY(nCefGN6hx!jfw_s``Xj8uUExZ0>c&6cs_MkPN5tgykAflPE_+aTO z>+)l%93LSaCLV7kMrQ@|F@brT`z&q9l8X{hd_$I$qhnP$CPNT@SIDe;QU{U2vntR4 zAXD5&nOqCTwZM^RW5P>cr_UQDW4D%?JN9{ABPsH$`^(-=v#gHT2JP!|+!#5a5@j_B&Jjhw0|GtM=D7 zAi%Gp{a>YXPR#$t#|Ql7z`UfJS0x7U>ahS`Jr=;L#{ziuSOBjc3*c7+ z^ved&YYQ{rPl4s{v%!GB`hx#>xZkPfwL$nl*Z`XU;~SCNjL(v@@qh zbI(iB8m`F^{K~w_o~dkk+AjQc2RdCN#M7?~it3d3fp*%=yz^tHsi%e{;gZPu18y6|>qxof=+qIdG!c+q5^Qcy+ zb%X4t-=c5TEYOBSG?evIwy1LhyR_rUtYgvGTIfTHJ_h4)Esul*UQ0V}Us_{P%9H}teJ}um^+Q-p-_pXCLb^depz|RCN zrYd{QX)*()gR+o_vzJrTtR}?ey3nH`5HX}Kjtsa?jniVOm@oyt+yW-zt0>VA z{+$XPR~R%c>*)=)e@l5XayG4&viDB^?eptI~GU0kOe8QMs)fTX;LPkrhycGg12B^2=QSs!HVR z$O+Qzozt}_9o(&)JZZ<&o`jEdnJFJIWgA$@n{}J_96Bb=G4>Wn&G(=XTc`bwq%U;T zZ+9|<>lFR6=au<8=xal6OL|JslMm-FvqHG5Wy-*%k3z?{C*&&nfVja`MHeOP6GdLM zgx*RVw3=4c02>brmP(w|{s3DXFpT6j*P$6;`&~Qk2b$9#E<-3C`(B!&bBP;iR1F9coIxuH4jTQgoC80h znoS>38{)~0c*k^)`!5^JThrDM^C{hw@bn;`5o@!DO6ZzZXn@Uje8h~5?@CP+SdM(=>);i5^NtEj$(bX<%aw3-L zEW!^6VVW4a8dO1qBkv26KIa`RGN?a&RKEblz#m(+i|kuC_a84jDA5^&WONFlsR_=+ z8WvwLHR#GJQx?cuNCPk5ium|=3c4W7I{PtOA_CEKjUiAo1=IdWnS&3L%3Gc7W7AyG z^1hQa%-DQpUP!qRI7{fD!emwGS9*+5mgk4tGLuNZSJZHBgP#> zEh}doB!{-IT->xBfiOy55ip<&UjuvXo<|(2+xu7up1|H$kF@@qXz;5L{fY*xe;*G4 z{EMaWA4Y>$>*609AFoEof3flL>SX*Q8vMHI?~w98*!cLhqCf8c5)EGU4#2D40eICr z0Izxn;8%C`OB8tRj|RMY9e;0d{MB^)&nWQPef%FpftTYVA5oeQXsDg4i1jcLH);S8 zhx2E>ex;0i30)?Lk`g?L0qoB$)J zk^}WfA0=7b>+3+!;y|y>Ved|*n=5Nj`?UCN(>?WU>9d(f&L;bl!(1hJ>wngfZD}2A zU7RpyFo!i0=I9F%#EE-s+X$TO#>GA7jt*8Hd~Ju#=4@kY*}OlwjtQ^^F7-oRiy-+D zUTj3lc?`JmwyV&yE*~t^3KyuA?deX&6d{#PuyA26@AJoB-W+Rrx;Zu5J3bT_ zllglvw6*L-4A0GOz_Ur_2S&8m{a6B6GR5j^q5tTg>p#LjYw*BRXu-p0iZw?1PzELSPPKX9`M>#CQBf$o~kArv!3SYo<9hEE^&%><{zkkXy5pxEB zBpalqZ||r=9C}dTjd+Rs9}ln?^Cq`cMVl;t&?1{T4}UXbQZ>O@40?9*t_6Q#5p zR$){vNFu3uXj$YM|I0gX(b;8<-5cRD4$kMTP9?=?!InI=T37Qla+%Er4Degnh`?ha zwe_bKwK@(2{+S6g#Gsm5&1r^+mM@0FF9Y*#g+x%?3$3XqMkj)*NVS7gP4ved#=y&9 zZFg#nTRpXCWk1|$@X%x*z-S~EVK#mcwXI$+g!7`z$cvtdiliahGlW!8D;^fc=8R1m zVO|h(J@i@Mm{zEH=oTNBz%V+;sB1JC1RKyIu1`A^DO)8H&KA!DUC@bGML|s?x@loJ zxGoDZcvl#9(GU+g5B0w8#L7xmZ5UU9Fl-%+0aAS%>|@uL8DaCY{K0_AuFNuJ<`Lr2 zh^oU6OOwD*LE}=~4Z=Hvh18XOwHUX6xO?kTv*LXpwzq50L(Hc>K%4`&e3lZbSfhYE zt0LPe&|dcau~RAf+7tE1H2M_Yo^I$#vz#+c^Ng9L3E`<))V0Wij~;;_t4RBbn}+`5 zGx$vBP8^Dh&ij0tB5&8O5tggsa1?2zaB?iV)Ifzk-|^ax^8i=Asb$X;93ZXKV4-M# z&f3z2bQ;M82M*f8OooYczzgrzhyo)bOAZHYn-6?R5lHAkQr|;q|p z6L3O`A&D^8%Vh1MT!w~C5{UT34sV~ZzW{`4wu_OM`I$wNDTqWq(@@k=t(7Tz4FeOT z*Q9{h*TG=@W&1ia3TeGX175WaSLXtk)gVT}3(d0G+bx#ej!fX7%s&WYLQNZ0O6d!F zma3&_O$zF9UDsQyIG0<>+&9f}r7~s$KRFZ+mr{Pr`-?Pcn`dZ^`cTy4f8Bx}L-nQY z7Z!%$T@7jw0w85>@Qx|i1ASxBkkCppg??ZbBQa{jW-GM>5v{(f|BhSxKIFYna7ak- z{=$4%t8;ki#m&eEh9Ksk)5CBB5xlPS1&KGknHW9u{V8CdSFf3lZ!kz)boxGP>=32s zac33PKSykxYh!9U_WOblkL~SBq7w9RJ+`e>7bIDT% za)mx^j1Okfs8!h!H~&~y{)uOm9vNF+V8&d_C%Q8;9EncqKg_ieg?e)ztQKN+Y|{Lb`^BoCZy`J?Rm3juzzb$9dCc1eK!N z_zffoip?oPQR!Xv;mG=DeRTw$9y;G^%z`-vQ6H-xY`xtJBjFFJm2mCYmzZw)<6~Ut zhh1mOW~|AGgtX@p>zdWs zW@W{{V8blv>Vo$$q^}z+Y14KJ62Njt9YK+heH_sZfMV_p3MMetO#9RTOEDe; zitfv|u2U&dyx{NO;CtL*b1=XFf=zyE`#op{{w;WbcZlq$hW?5U+GQC%P1W8{yKrp5 z_TIi?`I8G5DTz0bwl@uWf>+%c-&!v6(A-cxQ7Yo#5tF&IK=Rgl9I|VzJV)+6AX2b? z9YfUKiAEMqKjeWr6dyJY%-j>xzKi3s!5SDrbNcpPGvY(PdwsoL_gu4$VW})nt!0tc zH!cHYYL+KnN&)qj(!;lEZ@i(RZI!>xeYdf%Cz;ogBCP&e4o)90cF~pN6 zs5IyHfd`BMQ{evTYx{kVb(hN0#g4z$SR3clduq4dz|l^(+E{2s)a?9#cmNsQDb?nu zXRySZjKY7D!hU6dUnz`@jpdKu|M4^6UyPLhFonGuCjUrbulCA+k-}d6lYgYJUswHO zP5+|}nqMpW{Vp}ys9jKSCs|utJVD_eZ4lT1701Lzth)W9pL{=U$3p;|3Ug% zwArjhal2L?%GT7^bw;+#r$!8?xgIF9%_+S=+5sUa3SksAf;Ooc@!a;5LUgLs6Nu5Q z(UL>OW}2LwT*G)79vvCDN*O|_`F4u>j)3KIedHzv=lrPr`(BrH{h1zRu%HBN#Iq;{ z@gIW%EQ{aIIAEop9$wQ72pW{%T5iZk`*k83DNY7X2hVDh?nRdwT>%~|Y`6&SiJ`t7 zZrtEJKc0n06TW}V~{|8W)von*hq>!t0eM%FGLXR@ZsOo6X%@nujz6^*^X zb?LEP8I4PQ^JLvMqVi9iwqtA|?;ZNG_flr+PA^jm!Cs~mt_X=w6=-0|JMbE(_3x^U ztJSgWv9MGr8PM1Y&B^E%d64J};QgF0DlcN$-^Q&V2!4x$3x+=_QEh%zhrxIGZRJ94 zmaCRoJ=QT`9ZSekaj|Oy!ZTYTfb=HvpmAoH&1w{5FXljbSL$mCHMMtFC3Z(mlNDOa zs8&i&=gVY6cciuw(VtN=nUyudjye10xu2|f;%|jDZE@i%`eSa^{E@G#=-^g)zC8oO z{nPcoyDn7{5{eeL9>(lJ@mcr6Oc&PHUQ$^#nVH>1?nvKMFULc0P|wbdvK<-`@$-bN z>Mrs|_~!I!UMG2lJrp6e?%A1M8`iEdf2@weo30X&8)7>ZB19)@e6FA7bw(IRZZ?CV zhF2L+sFHzevWT5;CU27smMWR_FLa_P(#oLOvOc@Dvd>e8L z&`0oqQuO8zrV}Gybd+Ro3bCeyh&yTFS1;lr1?d(IEA**m_wyzJMsP~YnN$JznFh){ z=ez5va6e*W+2~ME{iA{&rWi#A^VVd;uoxWPs=bYV%gRIDzC9iOol9briXL-^pr(ai z)IbNGA33m4Hx$$-5Mh)ln|;nFZCK>soI4Lv=1bt9w6E|SJE101e=B`-|9E+m0ksle zNt-|~4*-HKUrcmy02XBSjO%GtAULpiQYC~L8PgGgVMw)XoK#}7BpCwNo@@waPZMC& z>X+mK>v#7q*rzv-7UDs+S@~G4QiECj(i{kLVk!>s>PF|Qu8<1^07ej#3_;(i-tVo3JP6fW^@#nX zI(IQQ8{ z4!c0ebZ!mej>C82ibM8d2^~^`2m?>=+m@HIfbMlsxd(mHG;*s>jcRFAPJRdZi|5ck zD|{&YZQy9x@#v-?$$<`Y;Mr=XYa-YBbjbJ2)uUszkDmpC>-u4;7F=DN+=nJ?xyeGA z>euEnVc15i>THwkHzd(*T8hWWF*AEsIl2^g)eXq!puY7);6WcdgT0f6*)7kR@1Spo z2E|REZW4PkG`M2$8w|xOg}TrOvFIgs2gDjT+D{VD)(0>lX+fl;aRiN`TYq2!LRWNN zQb3(n$N8gqKgoch*m%vv5Hd60YmGQH5&F8_SLy;0wy_4mf!JNbtPGpy(87o*OBo0H zOg(I9h?*$U(xV09k-b)GmD%v6jdSi1vJbG&deTvxWp}IHrlta;QAyaEiuS40jCBrjyKdW;S1J$Wyhb zVt4?_a07BBei9sHWrua>Is$^UI)(aBfx6dM;U7)R+vr-^j^Zhji=&|*@cl&C!?IDJ z2_O_blM=qr1+CkpP~mBz!)@yb))kPf?CO^gu%D9exNx0fSOKH5aqhM1tV11yqgj|A zOZJ6+S}AZ*D|wIOro<+`NC`=T;T=Yq9o_0yXFt>MXrGUIGL`N7L+z_E=Ov0r@GBQN zQVn=6K7S()WNo$|)J-!tSvOq0w==U=ozW*-+}F<&aVN0(v3WInR-j>_a250wsyCvf zVrqP}%9ni(Kvt}@Jt~rZh2gn!fvYa)8CL1TU463pZiVj16&Mlc_`oWA^SE^iC_jTh zqOgZ!Uhh+oM-`mh5)u+`_I=oVHhJ3}`ESC}uM+($90C89Z2u+Q zYW|CG^lHWYBOLv@>i;kt{aVp$IC>Rq0Iz}#;8n2sI|ThAY<@5H_t&UDJ)8ezYQUo0 zW-q$?4voX`I|aUoZ`cKX2F0(|I> zPd}#o$v^55iRVa8+h{?|AECodR-r^MT&&j`XDaGgG_IMf7)HD6&{SH?c|biq=3dMm zTbzVXe=>FSxEXufUG7fu-F9hhef3e~m1XPiE~gv|KZZg%u_|p-#!kn<={))XPB|XUlyx1@@B~VNqEpSJLYfRD*JZZtnz@R#$B%3!GVW84)pBNzokVCFPc7epTkj>j$KH3pV z*iCB?ysm{gwXG@V!fy$)tKKQYDt)xGgD8mI)jvKD zP*5+G@d5#>g?ks5`Y|JQ1odi6u+BoUO$|&yx5D!z(zN=_5U+fJ0oZELre66OI(wmY4YqOp)M}(P-saCmT_2!>mq0o+1g(XLxxV ziwXYo9^zEe9-i5R*!co_GmK1KI9d2*JyBgt&*#qw4Sb>N?B9dkFEplEve9B@0Razl zatK(tBGOKs1T1$fnf|I;DJKPlFd5wW#+3)^2&g(#g1SkURj{7C)pU19=v|9O9GDygq;I}f-#9fu7)fl;y@eyJvlJwbsP}(UrGL&7s)J^zL(AGjJwP`T08o3%vlY3ot@!U zq8&fZ_tG!kJ56m1GcO_@ew!j6QNAN)qW{YUDCz+cD-loH7!0B$9m{BJCN^bK;$U&B zf3nexbxA8#LfH4u!bDR)-AFz5zc6>_uq{O+ePOISvFnVU+{JES3@CwSmMq*?<2pDQ zvgNAs+ndEI{sNnemyQ8%z>87Ut(806QFNjd2}|Q(8aXdiDviCAjLw^x*S6RTzY^HfDaLY2J>v|WF2{{lm*Mc(_ zf2aXevoi_ZzScAe*+Ul%GYqomfRfYiX9Iv}_hr${mGf}P-@p5?W`c1B2>>{yu;`7RnrwP#jW$6< z>S#Z1V?Yxc-#+m)AJPXEi&(~6JnR4VJf`fTeGmk z1z2gl*+Epv5uVLJ*4PpvhG3B*DslDU4f@Tk0 zDZ;+&FaWx15YfuOZKggnB^gVM!e^XkL8sL)bz}0^B)I$#ubrPkff7O$I0IgAEGQJS zbP+6~x(_ahT`|RMuJ;mt$%YO9S1fEDmi;(`9U$Rw2?vFVuRIDr6n^4`NM<8-aESAj z+af`&Ra<`Rwz;toOdCs*fwn@y+HUSr|LesY90zmLu#-17+jd%Y@d1e|!aNjo%b1(p zVdOV8W|dpopgdQK@N(JYVEJZD2_o*0O84hqsNN_$C^??#R@YpKkM4qBzQ5FG?x?Km z>lt*2nRXd?r|agguai*etjRcR$@`JX*rCHdADMop%A-?^m{Gi0XY$KdLi3oW(kK<_sP>{kkMn5@8;3kE|ppG(*!VT2|o$n2qk*% z;~TI@>!nYtAiM){Z$(QV)5ylBKsjFitpV-?S1H>q)$$Zbk*TCSBQul%>^W-kMxOSP zm8jH$9yJ(9qXU%=yu=KfgH-3k+N&_#+PkE={-rj61L{ zeDTcB$ty$d*M>wP8&%D7kG(tTmXr|_Mt52BPHU=+aIkU0G0BwQtM_~Aspr#ggD1mK z8nsk6EORr|$y`+jSza2+NXjkSG!`r$*rB9kk;vmQL~4h92zkqp31NJE z&AymWx%gL{b0}X{Y_vD_v7$b+S}jk?*|-a((9NSQ^PsFQUE~?}L@UW%@1Vo$#_8J1 zSH0^NH185xeXmx=teBN4Rg~84wN^hCthK3>(h-$v}jvNlu&`+}9{cke2>8%GNMAQ_M zBKx@V8|B;>ORPQZ5h`g1RaD|`iF7Qtr=#@@3r}0rvRSKTYy>1tzoKx3%^@j~5Vn3c6~)*;|OYp&*zHOMbi3U_Y`#w1eK&X z{G!7d6f9}jT{<|Gj`q_Qxl&v!)tW)2$nAj&uLWDuVM0h%t=>TANp=UN)$Lp#> zd{HYeA4$4~wW58teu^7k@W)`*Y2`Pz?=J{-omE{fc@eWgjl#2j(p}EfehN_*;Spf4 zYTh(?3<50Wg_dEX^%txn_eh2$fLk}j+YAI3l29Bq&zTNqeFnetc>rk-Mr{7{JwWS1 zh(>La)CI1A0JUY2jg|O_gIR2zvyvvY#NfHOptuy#WdInoSzqE6P{Ov?0{$srS-~2m%4n z&5-m(L!GJYpKztN7@$<73IT7%-0uyzg@D}vDjTIZf05c9BvLnf>YR-hlgw#MuKqz3 zl}n3$ewz}v8qgEg;|0pN95ilgN)wn9;8+*BNG$fMz%5yc(Nl?%py&IG zbgwG7fnkj;r!#lj#Q=P_W8B(OVxMvT^Af<=HAKTSg zJV2S+YgI~VLkF8a2WECj1q?A|T)1)kN>t{C--Kp>@`3SwYjgR2HlLianaRvn!zpDU zfzn9l#<~D3a7YkyF5j)~nW+3?nVXqkosOYd1|G9hl0uSSeIz}B#2SVQ{u+FVa;OZJ zr-5s=7APWJJX88@B(#*@F4Yy7f!=r zb+5u8SqLv<)y_A`6S7dS@wf3~y}i^r1jAN3@Qrf<-*-)Ghe5+3rGh6}fGvh`l%UnY zpXfaT`(>jteQhF;?KoIr*;*W|14m%K#}x-^BIzSb2p=2k?_MyqnS?`Mr-bB}vgf2k zq`+n1w9X0fP+~^D`;r490kPEUos1vyV?!$G&8p52$qy3d9kIX}Z?6Ohdtw`%bxa>D z7Zvsq0(B4D44hG$-f|1Rnytz0ZYmRt<96qs{srNpP4u)ZN?p|yXTR+hyTI-Ey6wxL zqc;AEqaJB6<%mtBsgJH=`w45^RKjk|@9Wanhg5~cRG|*eIb%++-XO5kQiuE9*%p zvQOcV#ubO_TjH}w*QH>mq6NEYG$U}*on4l-0$2+u#h1tWQ%pN&B_FL9Dg>6|Y?IMy zZp*d>l(Ri_X({9Xz#_+QeKT6|eE=)Md^){vpK4 z#L}C)CuETn{5~us%N@fRFyWLImSHr6HyqEl1W(PSak$6V<s}R$dm}&cK*O8VdsgpepET@H5$uzQp zn9DC8T&z{CgQ+*}RD~`#5Ff0Rqs*BCX$4xG&@cLH+?(TUzRl_Y*B4g7&J)YqiXG#> z9wddr!h~NEL&XezmX_Yb0)HN}XK-xkN@Q^NWa2zq-y&=1M1SPduV>7TvW=Mc(ID&D zud!e+%^~REo6t{Fw8tV(4BgA3oFIicARXP!8#>?|kiR>>{Lof^doGcF^X)q+w6FRo zYw5h$b#xoQzp+vCFsDH{-D;>#54>-~K;L(3VY>^)-O-5fLL{6N=e(0Jeu&A~VxX9a zD#1Bs!`(`MQCR`EABz^LN8x#1C@ERx(7s+eA$}d=@~69vm90XxK7nBKRXjG>8Umi? zJath?b4zA}K7pKepmR$!D8?ey8fY*#i7}XvL(A~|sU(|5|E6RUp)a{wX)981(8YXJhx^(9^+3jY%W;ufw@sY_ zk_%@`61(d}EkB>DHfw0pZ4yc%AM!^vfQRp|><41Wqr!LPA<|-nAXpVI8 zAnyzte-2KH4ZH*6@;M*Y-QN%Dp3&*OT@W>=n-9flcUaN29Tn3x!&uU*6}ae(G#13b ze~dM(vWL|lTdW5W=dQBmEzg73LxI&1VGZV1XbICjt}z?AsS0ZenIx~3Or9^;qk8;- z{ZNFD&2gtqlLa&^J8F=ZEh2uiT{Ks06%lO2Tn`o*E>!mBn$(_3g-~qJHj!stv}Z)g za#GGt&EfcMin>vD9#-}C^i}e~S|i4Xu&6DS1e2x2ZCIJ%qrK;@33oX|<^`X`T8^#} zO>CFAaDD2I39AkM%o^S!%^IFCqFuNa8WToBwK9&$qc9y;2BB%es1j<$i;N-4w`GVj zbW&;(lb8r^-+VowQL=*>a%<(7+;1xb4z^R8Hf~pMH0C_)WZ`5$1&>?;h2yNZ&9|?o zs}yX~`Z$O1TD+qC`)83N%YSfxM}g03`C+V>L7eb?IZ^hEJrsfDixpR#q5^qc|5B;M zh3gO_1t!8b$bB4*9<~m>iWHK`D#rPtSX}_X`z`bLu5xKY@F4?3&;_|z0(}nM0=~CO z=~eb17do1>1PvY-}uc2yTU#NV*V&P1s}pg?TfEs?n+>!W;CkcSdvz)AS_ zR6Xj1i0V$1_XsLFX0>IOsYmYyGb_c~SeJpD*x3XXTTpGjwbuYoG)ZkAv(ANev$vd> zQlq1Sa3~~E_ltEG%xFgjGwU08j;&k9z*^dI=mgfoGGFcvk0*_h9ILkwd41Z`w7wM( z)L&(>sV|ozl37BgQ9@|bIto5aJkL22)nBo&iNL!Kn~p({%Lawh^x}P@+XMEBP!u^;FqKbBfW? z0LrDB?;s3;VM(fS%>QaTW|1r(h@?&J<`}t0H}sXopK1x4(}AT(M8HZWV7#uw^C*54 zu%6m|`m@FnzDMBtD6D8OH7y)|L9=L4HMH(i)AeqUx=g*IZaroU9(#1yhm$r7xrKG| zxqDBLu83=^e-k8rm9sx;93w06?;!C@ocmq*{=*>gs?q&p-{jS+`!9mTFYM=EI?aDw z_4i8t2SMU5e&{7gydpw?S5Xh}$_xQssUW~B$^(2wd4R7d5AYS`0se{dzyM!S9^kKj zemTOw0tfJy!}I%LUjqm5cL)Ff)p1^RUM{xABCi4n_a4 zz;d9F+GiXhYs9F2=u@GYwWD z75k#9&d62^o){{C)uNby>+w~6ys#%nja6wVF;XSICm1lmh0P1e-r(6^1A63elvWtshPWDztgFcvqQrCF8k z1%OR_-&j9r9}BZ!C1gzIME611E+j<(r%>=4jo!{~5y&nffAX!h8!k(^`j3;K>b!gU z6A&j!DrrePIU)jm#y%D~uGp>3N778P@>ZhzMWS5>9VJYWzOf={Pi?V_8S3o4BdJT>plGgSsBiPZcwfb!!LN5)+l@{bIWrkZcRz-Q1CxIUhjfBxfru#z zv{1$>Hfj|mVHCcpJF%dEmP7Ez=|nI$xR3J0JqfnFh(0(josEs_3-iO|!k{<%oOkPO zDTIqr7D+40f9+~v~2>z&}qK$sKi zG&rac>G~(xwk~@O^m^HdnY1jAnak&wal07>7`k%-kEr$>YU|JRvelkp0a@=KM{I9H zGwyAh;g?{+0-$m5;0)oud-y03&&o-!`DYd`j#cMH=#v}D=}8OX?PAlPeulk+GI{Cp zQPpz^HWfauZcDDkT9{lLug0>tt6$NYaSO^xJ7JhTAt~fktH#^M3WZOv15>trJE^p9 z?U%_5JKqd2C4}~-N-*j1zT;F>?B}Tkio1SK4Xx3hvbC_9NdZG? z)<78&9EU8!ORkhJ1Pd7hYIX1>ei{yeE(}%!-Jt@s_Q|C^^f_-8n3cDXw7mB438-KS z9wa2$yD9*s*+D|Ac{Qd=z@;-mW*7vD<{doty4JcFyti=CV{H>%Y`_b25LhSHqB@$J zDJ;nw35}U8GAT2s*qRwMNkapqXb)tEkpG>*@A!7e_I_Z<8tfF+Z!g8S)_lRG=!95_O*%bd+dx@ht_(IF z%hDwOPb-H{lvCRA0c4)@bR(c%edMVc2? z4QNru6ch_3*~{aH+j*taX9hcm>&+pL{>b~?M4T(yH(`TUX)KT8BrpGU4E$V}wN7=o zcWqL~e6L{;zS!CXJvqLgg~nAWUt<90J^mptZDLLw7P-1i0UTG+@=NeSrdZaFe|{%5 zd`7q3%O`!y@q2zwX2Bibtt1?J73Nn4 zU;zOB`28P01Ak#Mzl+ho$pFA#l-x_J_21eI@KuWWI|KY#(*B!%Y~ZWs^7m%KpI808 zlK=lRz@IDnAgN)pUeR2d-^zp>eP8huJn(^OII z6i-PMV~XzB@JMq#_8z?q=a%=X>j&4j{#crYI5qZ7zprpNXwgx5?At8RdNK-$HWSD| zpgQzHOelO*iEd^Fj^g2GPq0ngqV7eijo4(+S)`J>ORXAKaxn{6E|c1$B)xL$Dn*LF zY#O~(dA(ar4l`lCr+C*Mr1jYlJ1cMolCKwH4Bt#`*{cIsw_o0J58FJXjSEQ z0vM&o_L-)4H+F^TpWHaVDL*t?qWXhGjG_z7^URqGCo_OOx7s(8`?EM#6(_A zE_6)Ueo*a=W5r1h@ktv<+P;tpuWz&i-$db~22JGc?Ft+5&H;*)XzPNS?sAs~HI2N{ zAY5$EN6?xlt`m~(BKHVnFCSl>n+s;kV3=0E!j)n;vr~aKw+Z*DUpbdIw(9C4gs{d)t6UslHsf+`T|NW`1IyM7bl#APOnn-yiti~eBxnVmIz1QHF@&n z{2*e~5;gUN_9MZ`n*c&l+l(i>{UEXFVh7l{N$g`l*giXNi+r$#>~;p8FjWY@7{uhe zLFJ-~D3H5Q0`>DSqi$XD4J`2l;CxI_q1xmed&-9}_l9V|$R#RMN;ILiaoX0`_BZjB zMyAUXr6t8Syq%8Zcsp=x>WlJ|eQfG#A3i2z=Tb^wQqEZJZ6@MCxV8|*LCSZC>LJ5s z8KVZM;ZyVwelBEUAOdF?tSVz5x~wvn%}#KCWEY`_0!<;vQ{5hL8!d@b4ORn{NvIO< zLl4)X?oua%n7+)6#6v=myR9k=);5F1>2mWFg1gyD3{-ftVlL8>7uKwpv!E2)4^0}e z&IS$U=7mN9S=(2TXsTy>4WAzM*~d_x6*5m4CA)MV(8y_b(qqCn@8M$`7d+HO1{KLZ zy;2lcwvy83c@MiUJLQU0l~WZB2s^$j0wOKr_n02xqBc^|FqtpA{jd(7PBDlwo#gWO zexs*|7<0S;H%laFgq02=o(Yi1`nMw%qJ}$r^zaA1SBpdEK%~bh8#! z6R=B(z_t+aHrXc9fH!AEN4#y%fx@<@3Lnqb^d{2pvpf*P_*Jw;Z|S_{4&jy(xh8hg z$_(r9*=hOFzt}D4thtd&NK7Fs1-YuhmoD5eC6ybFoRk(^0wD`=*G8!W^%I{@CX2%O83 zZa5SbGB47h4$&zIC%N)amR1Ef7e5YvIb7kouL0SIgX~`SlTx=a)TsTm1DE2WX0c`Q zc0iFw+(F7FrO%WYmxB3+ZULSHagDur?{_!X+W$zzH)`cOD=pB!X-tJdYkD*@e&-VEQvAWg?RS8)g zXK-OZNMyX!FFZb;TB9?R0NP?*mo^uyxl-29NeH&725+mE!H!8FC)rL&jzw@*q7aYz z5t|Q&h9%q*dP76HInK7r+E?slGOz88qdZIdkEhCJ8*>ggV7sk};$3MFnDsuW)lDDK zI)}f3`ifLVnTQSw*J!DxxSQNNRLVkb%A~X19(-$&Km&+dxpmUpJp6CP-`VXCt-vG_YXA z9sosTWab!wg$`cZIQbo96we%2@bbqRgcCJGVT78jXCG>QuCjjn)Gm-T*xldb)ZA@${oU~e;vI7vTQ7vhQ5lO>AtM^1F#E^1V z`g4qom^#XCEr-UGoqs;?uMiv0uU)BAWe)hW@{fE{4m(cQ3 z%m30=fPdO)|Ai$7eDzuWQQiN=efb|$_rI98m+Jniy8&Na8Q`l81AJ9jfUgP*@Ks>} zzA7xhSA_-mr@{gQd{tP0uL=wBPlbgL_^XlqpKtts^}GM02?+SRb^ZT(<4;wQw0${2 z(Q=@CxT`lx%D{*6usbY!l$Q#$CN^*>aP^{^(~*pV5xvcBYegia5=S-4&odwuCwHi{ zTy51ng>=s)__0Q&!xwcTm%A%Xy{o5WD`vW}t(b3jj^j~ZDhh=jd9qvAZ;0$q-0PZ4 z4(NrWSh^?5Oz7EedV*u~&+Mxg{~jzAEx0IQu^SXlbsIQ>viJyMSccK}^$~4##1OTy z^9W-P(AsC^uuE$N$Ci5d#S}oNe*No$U{mb=TjC)KWId*GwwggWR@nE*s2t zo0m{SR%Vc_0(eVEICCbxMnjc$Q}K{=%ZKqhT53Tk==pm z-LWSx9dQJkmY#RxK6F_yxoH|)h&>d21~sAKF;vkwLXofox$h6hT+=#j8Gi^ZC2G15 zA%mE04FhNKAMqJiZ{Jz^qCe%K2U70NvPf8sWE?B`^l->D3oyl~%cc?%&;+0=8+oTDjqrHfKJ6(F z1T(MlDw7R=^F+(1H~o56oXy?Hi0;y<%}LyL#p>l*CZ{XRfWYh^IX|#;&AEL`k~cP! zR5iL8sQ1BWzSSy`0znL9+~6}kd+vwxn&^mmlW$qrd~(iUm50>0zy z{3!0lx~oV=QryC@JaBX$&Lg*+m6lBAH97^$R>t%C6*UpvP>sS>LFQ)lm(j75N`5R; z7Oze9)FTC=gB#nW4+SSzsnukAH;eS_GkE0rj0uf<23A(145#9|oPN&n8$!|rOvFWI zoCvk3mJb$nK>1qdfgGNU4qJ#Y8ia3JofD7X(u|dlK;SN0&z|_J=|xB9*SNgTWVF#m ze_J>fZ(iWU5%~sHe9;CMJsk55@kN}Gp`xM^>D;ESp=H6O%ew+NA~Q~=_56mnmApG0 zp)c-kp2Cb6lP`XpYU5J=8s+(q23)R1Vdh&xMrVLH^f@b`lVnj*r2JbQ%!Gj&Dtt5J zQtDw5dDsMkMxVv=vx+=U3IlE21$)@dRjp7;IWxi9$%^;N3K6tvE`aI}x^u8AOO(7E z2z9!SJckFF^0N8hkl4@=%jo*cdQ=cO`OPt~3Uz6LmqFD>@mIiVi(RJo{FWK1_rL{c|Ita_% zS_|#N>SXQ8c_jC;TX$E2wZ-O`>BRdrYZLq1l0#i7HbDrfK%s96w^x%Ka`4}Ap%6B_$Buh)N>A&ksSe_^gKpY8u~An-pdz^@+3KML?~ zxa&X0T!F7@$Uh43U#7_arknQ96}`S*zgi|Q678R_(XW;X@YON_zFH>0SIY$Wt0!6R zMXvpGkPq-ry#xmM+II|m?HvZbcIg6NyLA74E&rE^)W2WLe-D`cKMSz(qD}S-^14IQ zc&VqXstbV$fp1RMWG!di455ra`2T0?N0hxr!IUbG6t@En=`_h7ZbrMjjhS8YI_Zup&$!{!frT2R0Y*}W1-)GJ{*RA;{PM=t%CC0vbJk1xVyW%!-KmM+}#2M zcLKrPgFC_9-66OJcbDMq&QE8vckj2puIlcSzt2*Ys+*M@Ft0V|8ehkrx-0TL+*iOmyLld zC~TD~OC=hB_-mqjPa5dW`BLm6UZmHA$af$ZdQsT#q9iVc{3$K45m38<>pDA#sV~}R z=%uSbco>aV$bC;5#g7B&6Dr#gRU?b#7bn>fe2zFE8oVVET{`p`0aCrf)cwA=g&h{n z{*SAzxZ*QwfiocM(D;ksi<+|p0xWApeAjoh1duEGkIz)PvlvmxFVrpNN`Sbc&hbzy z7k4L`)XyF5^X%1{g2bYMwn5e6zF>NJlxf1!|V@T+$3v6%E`2gJ2_zU;fxOsqAvPutaFgg zsJB>oUs>rBX zr*yX}cZUjv?l@LX`HXAq9gK}Q05gw1^HKS{dHUlX%V=g)Ws|fmI^Egp$qn>@q8Kz3 z9tjmUm-BhqDK%TYzjOX<`q=%=wFj<;NefO;cq{Mc?Wtup2ehP-`LL8>Q^#? zGnWDn6bZU`Mr*~>;HmJYq$!2?3M+_EeGe_Pn+&1+D2p0ziq=5fOXX$rCn(u+21pG0olf)lA*U4#8e#xyrI#Ak&B0>Ct;~2C#NW&itkEj9Tk7 zr;1lf=I26EtQ`?(+3drIMWKe`an+xvH|zv{IC}_r zyflYo7!95kx@`@i)=y7U;Wytoh z|9E~HWKS?qZ2qt)TFCpAyZs44Hi#di!QZOR zyW8MDAMA4e#v}he*ya5F)jv=2Ke+b&1#G@u``)oj&UgC)=R26m`3_fdzGIA>?-(QJ zJI2WQ8!vdPK7RlL|CDTU{-4#oe^!-uDBwS6_G-DLiXnQ=RV4NAOfOb04+1=GXL4mz z8zbqy^Raq#nmL&xl>l9kiSxIw0nrSb(E;GN2-WpF?sw{0zGlf?1>ipU{#J)8Us0f4 zxhYEyQOAr{4DDSrU?!=cf)SIhZ3rKQC1n%-s8aSbXNe%?d7Mf@j%J;EB9IIvmXk>A zlXm09zf9YrdKRcKdqQZ#TgpJlvuz{A;_=wJSP3TPupJGfZ8Q>$V@CIpMjG41nHCI^ z8fDL*!QRg<9(GhHgi0?{y(gAF44~i3p04}p`0=|J=6+4sSVanlrxr%2w)jH@_*gI< zZa=UZ2DN<}Z@maV{Ff~wFB|J5;YambWtK*~AF2K0c@)GF=IS^)C{}ygFu3CL75C7O zWUmF%GY7c<>%DaPMDqx40%fL>UCw(A*(oPoqe!E@NYr!Ht*CiAAHG98O}g9T&X+;E z9RpZXn3$fvG{T|4ld2kg*WJt8sg3^HpF4PuaQi9`CS|t$SbaQ=J`kwQ@(~cYTlivC zw%MfhTkd$|Tkutv+jGAR?@G!u=RPeVw%RF-*owoi5SpFS%xLaXe#L}NCXL3+*j-%X z)(2hFxriOD&%%j6Y)Py___{GjTHF1|>pI)DWXHuKaknGnro}jEl7xn9p)!k~a z@K?h_lC1!R&4v9-?MH4S%Lcq07o#;}4+>O;7;$6~)?mMyMSI*C`w8PUwVEAkE1|4v1i56=xB7{cn2M*~O*U7K5_gZ9MD=wQd zAlv3M9$MR9lIfw-9)pO4^yOadDe<95Ktk&7CGt$69lzg(^&j5@s-g^QneE(ewUPKP&b{MUY- zW>M`&8+)fW^@zgDqk<#i1yU<7&SmR%M;5=TwxJrh@8PYf8@S$Eo5dqeKtgBY!2_$F>mLJKO>XwqG` z;k0v!r%=eb{gt4(?pPX=D>_5r6V0e8s?ge1o$SoE3T=gZ&I)w%aKK%SWl4^yJMyAN zj-L0J%>mtg?2@3|kBtz^44m{Lefj9S$rSEB%7+Y5>)R|}MxAmHqiU{EMW*HZL3@rocS%1*}o}E|4C_4>zLjm;Q(Z8xu?t-KJlG*X_l~ z%F>#jjRSMAdgtBr#;kF(CEy;h&>DHnwL%^DvD%k!5zb^8ov+|>s4@Zl|OmktO{w)>IE?ih%xX#0H64*^o+6r@Zqm; zr;0YZfzlpT@$@@@ppV{>=$5N5!B)yAYjxDlU+9oKsl)Kn$=%BWIkn00P{wnd^W?$P zVS^gTDnarxM5}H)dZ{7f3(LN;BmPp7SsKjOoo({Z|_K4f_9?0sg}~@H@Qrw>0v**#1`|=6CSzZ+GC|U;XnW|ARF0 z`$Yfv`EP0D9mnH*$MHDdaXij<9FOxI$NT3U_WwNl|1)j;`HKJF-C-l%HrFqSA-^pu zal9WakLhdFi=hy;b8f4%Gmb4+rXB(XgYDJu0}2NwWzp*>fWXud0E$#5gC<;nw0yG1)|Pery0_wdav4p-!!J9g>$x?>poZ$6 zW!f>|!_9)P!-13~&ZGrZI#)A6ll=WycTz=XzkF1g45ryKMe&zL!9i&GQgg{=b8hOK z+V!vNQJl+&*Q zN{UKw1RCyh1jaK8v2_cA&ebYjB?pnsZBH2%a~MoQwNy(&sdrjMhwYNOn9i?Oa_kW-@BA?hoR-k zs-9L9DSL~lmb=36Al>cPUYyMif!hk8h-?3->w5)#GT@?tZbjg1rzFC&WoQBQO(6Fs zUPB^s7##i)F#KA~a3N^^#kxyR=DOLspolNbs+UQOFLbKk+{^c6_j^T=j|!8Lv205- z8!&G|^5ZDR7{~;Tb-Dx)*Nb^Otmw+AGqoDe@R6wqF}C`Yk5;jXJA+olz^aGQgF?EU z)nWu^!b+eyp(026Mw0p7C(>pkP-~iDl56Z>k9leRdSVSU*)qgs{2yjqK<>pmgz*5~~%Vi}Hbhc?6sOt(QbEnb3VbqVo;L0pM{zbFb5^xnr)Pcm|3J> zd_}q+9N9A}GKzRjr!p0>utd!)saC`c#J}=1Q5)0pDmqAbK_;UBpR)GSPN*>esHyY_<;I!|^_;HLaUuyuLL&O4FxLl=6xxK9<2ED$w(ZhrfO z-0~!gne@_R(ZH1=;%y8TP4q~E#+(c=up~|+o z49r{1;l?nZM0d7Y4oSAfi@ak}v9zR@^&NH3pq2cQ*sc<*>XkkSL|Lco)K?!a4Z*Su zwEb|bIA-LB%-Y*29ueUg*`g$fMwK9H>FwI%{_;Z$%yMb7x@CaW6 zrwgjz3#x9leoW%eO|0=-ZOUcZUfSlpC3AUP9asytr)k2DA7m0KLvz?4R+ZT8Dki7Qd0$UmP{0yA4Nfxh ztKj%T2qFcZ$J2hf@hiEF0(?mN2iXe|TZB;29hQ7Ns)As)GgtQ#i}`}T6qaIOHsiMvM{3yuY)Uj7nU znf5se`^3y94oYhKlH0D(Rlw3!DE6)X%YZKz&ksoOs@&~K>z&*bqRW}rV2`+pANe@B4-Dg(W9e}BtBe=&vsqdCA| z%-~xFdM64w-`PFRcXp5So!#SnXZJYY**(s8c8~KnxbXJ@`(J#+-v{h}q6+^m8@(wC z9Jbhz-v;cCqk|dMOkf?!ynt0>jv`|gmPuv!b9_SR>LmHxE`+6r+eaY8Y7wDo7j2UW z+A4zP$LmAuCt1Bcn~O6P6QsN>xrp4gqHMWax`I}+a<9cSzGoOOS4*l;b(yRJ&paY& zsu1QU>ut-jQQIz;4n6rk<~O$qd0Gsq{&GQY0v{dv(2k+3nxkwO|9()w;Ukl&=Qop@ zDT?+NSGf>^`x`+g+cBSkFGIxNkxj^I3u}PmuR40gCOvfObVx#_%J-}rXu`A|MRg{2 z^G^<1>GwAd+S?zWY|FmiHC2$_<0Dje9r|XUG(aMMVtd$sVd9E>eKDxh6?_+52K(7>%p@oIA$Q4qSBI0 zH;rT5uC3MwW#D(Ss0{=~t5j=RVpor1s+^SBXVo3)yEskpHXCkfc|igl*)RE4iW6N7 z3tf3d;s@PZN+AiePH*k!V;4?8=vSP1JLpJJ4fboSb8Ib_M;RcU!MYtmh3F5;%l7Ah zD*%kqWal}gGrfasR+4CFE=%Pju4Ff>*e}j9M(HzivR=udR%1mh{XMz(pX4Abi8>0r z$WI*S^u^gqaTZSWMQc7T9JNQ>#Rrk0)C`Yk_>EPO-11vV=d{w|s~{>&)1)$j*=F+m z5>zkRf@scp18-ont{1|W69JIoxrZ{M`48ii#=v7SRE0!jSZ+R5uSML`lVXl{d3U)| zFcnTJCz45Bx(Ro&eQiLP545n=b{{uPbZuOm@;*=Edy*|>`&fh;v$a~Gwhwk9?sT(Y z0D(q{N_~+tf%hgpbyL{96W$oYP!Xg9dQ{!LY5er+1ItRP2Nown{OkII2&GY)aipeb zwM<0zQJm74=o=sOsLC!{SFR@v)U|gN(!Mk;J8^8|n8~2;#sq5QSCkkCJWg;(KCU{V z$))DCN1=et$bdQbwWqNkM2oxmaS(SkWQi6{fwc+BS66RNgyMqJ6U45gZCj}c@c zd;1EkhGu)Q5DL0?9xK1jXhFFqR|VZ|74?2cUb#XlUxMe{y|YiAfILK4=9_Ys9D_2- zhj3~wmh5&t^`9|T0=G<6rXj-e;DXqIOI?F6FbO$q=RH{z1za~?2U5ZL0$KGQDbmf= z;hs^YV0?@!g zArsF(0^PqZhb9^>8AUVHH&P2I%4XW7R*KJBPVoe#`iG03&pRo9qOYXMZX}KI-W>PS zii9=HP-;~{)daQh17DN6-xZ8Q4}5J8Okln$F?b2O!F~oA`6mP(-31hGL{L_?6X=T9 zbEEY&djb7hYbFnpNJ9-Hl3ie50e7kt)sl_$nYj%!PSNoDZrp8<+9)!LV7zB#CkGY^ z%~1#Vyn}8vE<9)EpxBHk4J4QupOzi_G22_l)M9-@W^Fk8)LXa*32`?Q@nf2sVhbNJ z<>PuYEp1Vq&h6y>YFg*%XKjn-1I)_s;aUnrRW#8R4#2c1L)|=lD6ZNECo15AmvC;F zyy2b7>xZxEfIyX>M*S-pbd&dQk8*E@`R)3v6}L`HX0l>P52UD0tG@ehkNvvmW(Ntd z#lhw-zZO}T4YE+?UNG$9UQh{I8vijwDOb^(n0XfnvyijNT^A<`i+x((gI|7psgz&~ zAMuNc0B!9n$uJo`B59I*_M0;+)uoVrB6AfzV&IkQex&t&t47m4XRm|Zh>({zb@=e{ zo^bKf-KjSsdr}v!;vRi&vT^ltdVpXGy+zB>m7B&qGVGk;6_h!MCgopbw7+meW=5ue zq7eT_1n?hbw0D&8Z+)si%>n=UTa9<%@NXIIFC6iIl+k{l=)F(%jzMz1V~}q))IZ*A zykn4ncMKBnjzI$6F-X847$hO!4-66-@Qy(O-nSS3)zVNE~xul`q zF}518VqFaed_i4>vN<|&rTsZ#P#1(gPFmIH*C!19wx7lc6cx%lHxH6Y!beAEK|@31 zIo-41^sTcc0TSwwh%M`-AWDF*DQPY}`BYEkafe1=sRa#!6mxp^eIA2JRM590Lf2}9 zIp*w$!9q&JaP`uHlq!^mNJlgA%AjqBZ4Spx+@ zP= z*?TT3hzcZ1Q0AzwMWEdznD^bhi&tLM3h#Q zhuWR)=4j0)-RHxoem18FRf%dNu^IR4Hi(8|Sjw+c$}@MUkBbgvKDJ{{V^Q^Svm^bf z5XS&w$e83LM2cYhlc{o>yQJ@{9>tb1WklmRFE+^+foP<1YE+yR^;|9S+;L>#$P$B1 zaY#;eR7B)98Vz{WRJYV7-f~bMADgW%cZJqiKHL=VU(_-v`?)j_3u9)JCPP?Xsj^|& zmr~K$13}98uwl3;9nh-DmP3Fg1yn#6M4B#@i*1_dIo`K7ie3?(ixO`I?3GGTR%OqX zxi%YFX$nqMNEiWS#r$w2qlpxX-24>h!FEdKSIxk{44E5WRnffSrksfBr~O-yoRFI zx8Az01tU7usO@{ruPKL(MGrBKo?9v3wH}jQ%{=R$8=FNjw)FG9vNBugK~mu9ON#r6(_G0r(I~7m z2NOB!Cn&;!Q_rn&uzwPiB!1SS0^|r)P49y6X)dKb5?bum? zAK_#yJMyE2>kHF{I_zSnm=IDhJS8OltPo9i`UUTERlt1KFD@F! zz#yZ~kCD>1&~9~HJG)X1x-~QLqOeWoLSxaHl)f9WEwv0Zsu&34)K@}_ye)Af%?x+; zb~&s`&1d2Cn@YB+BOUi+QP(xzgtg>;CNf$FKv`nA4IG*Azr7@cSU5ndiun%n(H+SkW1F-Is#YsdNr2E4u_m$gWC_G?)WH;DUREhl%&1SS>v3@Yv4huv z9Iqj}Kg+h&R?x5SrwuDO#cJ0`^-HaL`c5in_cx3&uI3KNHW2SI{ z{$ifNS$M38seXJpuf3IOA!xY|Xaxp6>a+MAEDM-D7a$NdoNgNg{6PurUMrYjFCqg2 zCQ2LSvzxC?8PYY(p`HEr${x$L2!AgFDozB{$9DOmc3L_58)Vv98b($q@UIwM-E&By z3B-Q&>HYUWOf0($m>?~oc$|DMwOOie@;7`H6;Q>k?AQV6Xu}<#v89C zxZ^e%QFk z+y_+uSG-$<>Zy`8P7haBluF?!gs*MXxgOlD`DEoHc20YhI&eijxahcR<4yRSAMEsxul{+G|Nm!# zf1K!#tOa<-P66-ODc~JD1-xUYfOqT^@Q$4Ve&aoggn)NI2k-}I`p*|ifWLMy|E(_u z_;a1|zw3(yyy2*C;K|q9ipF!LWjlS}wXcpm<`n7Vc7`Tg>CFg;A0$hugn%UqhAK`$ zo+`-{;&@5msGx#cmR7XRxIMB?w>S8Qh^2(T*XHsUWv6m?Q*rdHbPVqAa4f51g%YDH z?JXp-8dH%e1gg<7Bv@m8ygO;ZGNE;tyW!o4BL&{#8)bK7NnKzwm39+WV|`3q9qVf+ zaM*#+aISg{tLRu&*cA7C6>hP!Atm0W3i~Q`L~THDogF51VpE^sSU|00p`dL4kHs?@ z+u3id#;xWIwHg_3dy`VbZIs#xMh8bc>Dt!DOz)d&V(ju;Nfh#zRfT#H_MOLM^@-iG zu*>rs2E{L{W)D<#&EzVQt!9g?COVeVl1^38fuW&b)K6{E`JX&&Q}2ol-P7`f7j>#g z`gjwL&`vfRRhGjva21*BK}a5i>(JcX84lP&KA+p~N|RiduPq}1S(wQ(F-!Yfk`qmK%L+ac-PrU@b){wC*GbaZW6LGWCd7nGMzIClj5L16hDNd5w3Ru;*LrGR)zqPilWADB8-nnwwjgpo@E zWQ{iB4c{k_eNd-53LPWRG%%snpPOBrxB3~1o@hc%zrM?d+e1&RT9$K5feU2K5Hwi~ zjZP;lp!C2{gYe}rJgWekAdZ`vjar{(m3Ud3q?lRBj%nm+TY`H+inh=yGinWJx+yne zTuvQZzFb*W+FxDHi6L0MXDT}S4F8v#c~I<(HD^-Uh+YhfeccayH~N#o2@$+!?_R;s z9}WDjgE4I%^oGqTvE7Jo*|#buqKeyAWWcu5$R>h2VcI@xgJ@;-ih*GE%2BSbl51w; zDT6+8;n9!i^TinZyFEgl)(m8AkKbaG5I~($>KVPMXoUMB=cp6syD8X^7-cbc1|)p7 zpv+4T8q`JTEI+ODoY2Lu4u~c$)KKCuP9ZzbuazXo`O9WH@n9KrbZ-@m!N(>ZO-{bV zR1HJX21bwhQeth)!uGJ$En|0;ui%zCf#Mx6D&Jgo+S?S(^s!YC$P3AxQTrRf#u^)w z2TR3$&co)Of#8Q64AQ`{qrlwKl{o$!#QU~n`qKm;aSI~t!+wZUx#UaYuvNs-IykLl zLIC@EqZ5?Xmwboo4c;0inUn&diwfM^xTo$oWVwlD^PT(8&=oz0A5 zrv!h?C58)wdK{gMUX5CK~RE(tYs*=ukFTZX=JGdE*#g_D>#I&+d$@@x& z9>7w&_aINMbvTuD!4gi5PcTH<3X9Cg?(;F8wz2ek6{-UEF6d#BJgOj3_xqAt|~r#y)pJh>9;-*pHbMSaaul%y4-N=ZeAdQGvFhwMKYiZ4d-j4 ztFi?4;cGnAe@*kl>Hp>acDrxyCM;(nAOGIg5rln*=4N09etJ`TtKoir+omoV8sqh# zEl>S0!8h6LZW|TO1bD|~|Lu$E zbakDmf9NsZ^2pFsd2Kr5`o@->1>2ifX2Ce5xOgc@gQwoy+YYFI!E24BJE$r`7F(+t zF0e^&H_bh$;wrj5#;+k5EWjaAO4LhHumS_EAeY#BO+~1}t3Sj^REY8WPUJo0E^V%W z$Q|vBrAgw3zlB!>m0>h5`x`W{aDz zO+$Vix}jx)^CVn-o5#2-ud|f-wDhNDiLu_f*n|*BC_$|Cp6JX)xB(3p)W#_}7NNT`ZIWuI1K7P7h*6DPx-tEOzDoF`!u@#7#; z(LE4ZhgRvZo9j6rT8f6+QMN>j7yJDGWO!ThU%8Eu}x@iC7p%6AFdlkI^tK7^_7;t0>e^xo33y z2a63&i!EWzAL;~FrwZ-sSb-kWIG*(5;g&dS@S5QDT=@-fN3qjMpfg;P)6M4k)FGpj z{fr)N_o6`T2gw5kD{N6lkG-jhVcY0FM)xcWljN7@!4X&0L`sn+=vuf~U9lg>~cYoZm$Y1{}&K)71B z?U$!BAG?j*3fbY`S+~U1h4fsYE9xDO0}Gr4jh(O4ew3gLq$Oku_6$SMjls4|Vjcm& z^dub(v2kgs#LC1thD`g_Vh?pNOlhjray%147)@E_Qw}O>17m!1FdtRalu^$R-+L0| z?D|#4YOY*4SC5;V3dSlKG+!XYB(3WfZ|*vuyB81j08qN0vXZNZ!Tb*K%@RMd7T|YI zRP5Hukvl)Xu4`W;#eVhj zAXS|J zU%bMmwWe5pyH{CvPbfmE#V#__VAeVk3mSFn)alJS`GNFmDwRQDyR{v4=yXiNk`yBoZp(geoNtU!uX!WiHxf5uyXzoRS|z`W#W zDF7eMzw*&I%3*J$>AF8+C`0cAGN)n$s4QpBC_>$&sg#o5n*T*n z{#_dWQ7@R-82|EC0)G1z{;VSZVNw1K;=YyOzaG;7-fa#4Io|ZvzxX}+_*+r_KX~u| zpn33@SVHTq*#0q?0leeGfOiBK@DAJlvk3pDZU6l5`?CnYqqqOI2uG+dST9MSzda<@ z-lGDxOnID)5bSm)2ff{FB>8ap?Ji3sfz-K?l6JCwhkw`WVVSr{1l7C_PCcJ=^*O^K zbZt-WL5$q9Z6Ig7t|;b8k9zMwRRfi#`l=#y|A61sC}-dj_PciMtA9C?;=1Ua*5HC> zY8B*Kn}(vX$U}-AXqay&6K!e&K!+~T(xEAz<0|28A6rMF(b_Ftw{y0c^Vk0V+XmG6 zjFq(kpHvcjIvQ4xdRrekr9$h(hmB(aoB~H^d{BR2LQX z!Nn_^DD_s$d3Kc$WN+!B!rBMTNuB5mqdQpu=p@$LY$BA|khhYW%`!x0386)^1!(J* z2SP*1^#%{pP6=5@2(f4J+K1W7Zt9e69o1-tChR}QIbWpBYH6C7JY8P^dbN^*gvw0v z_8d;Fi^s~buyiH(THqj{)}CHAZW`K!85wbKl;%QU-MczT`ZaC$-DxP$DAsP_Xt)-T zNtfeOra$)WH>V!LNLiHAFw4nn*uxEPw4YRho4q7*9GM4tqa;7{e-U5vTbQ~d6Ep5}uy;0eCmyW=G6K-!15eSX&ofmFZ&XIr2bIwaLyNdDvGjO0>t_V|g0 z9CINDUNdQzhyEPq#W+k~5)!2&1NM(N71GK%AIH8M><^ zg!ZstVq}eB-rvxI_k%nPQ!HIT8iF6vTsV9QVZ_F%!M}GoHg(I-o%!z=2D7P4p>it3 z4b;sFtt>V7kArK%Jl9OGFakBEd1JpVjS=B&bX%f~{MHgKI$gJZwzJf1eZFgX0t`8U zEv>u630@9UPIFh)_to?f)KE?^;cy{v>jO!qPxM}3oy}pg@x*y#QZ#9BZN!1xNGu28rhazxJc1Pqt=^$HfPJ@&%lpJo)Z~%RHz| zFG*L$+?TIjvN4uTzET#^uyiD06S>xXCBY=fXbBpoEvw?d!fA7NwHJ-icSEI5&fke& z)uRRHRC`M1PIH!RTR? za!MJ7i4X|)yTdJjhb)}C$0>h?tad52EgjsnksVsknv~7Tvzf?tC)X1o!fX@%N_)|y zQypwqi-fbRoT&6X=?=Mu4>^r341AZsKn&q+*t_UgrW;lDqS0=yo?aFWhjX}kmKN++ zxN&<9YsI6O79Xk*;ri&LZziQ>(Fa^c%h>;-+#LM#n$W#JGp%GflK&1xJt2|uZEV6P z;DAQto7H*SX@B1ZNrM7rpI&qkf9-(Vf7V*%JQlh_?|v|X~9G}`0%>3Qqk?f7Y9AR}!UW@|Bm#TD(1 z>CNhoDyCp^F$put&H}77r8Bt^ z0VxqUv_4b){B`2hDQE)#b$^x9 zQauilvAQ~Xai<=Lh&!GTkjFC^FRRVm_gpd~$UkcC>BMG}w`-cRm4X$H@3hJ5gT!}l z@-d}*&(7_e6^!HhJ;~5^xk@rv5%?Jg`c;)jh}YcBb{(M(jrR4k)IL(FqY(jVnmCD*MhDGRs19S2`gl zaW@{<2N{VGbohGW=_5fA0wE{;41^&39IXVq?RMDIy`}2K93q5~m02p|B>H++t3LT0 zL^f}%s=7{^V`JfuSU*q)ySWr=ReGh?;4W7WyW_=$$ya<-xVf*%Z$Ij`sYdh z2buBriT?5P-!kL7SOxGdRssAWRv`qui&X&cVimxkl;?;F*mL!nB&Q+N0ZLF^!F$Jw@6RO{(tq7wp@KVIsA=u%I zO-pPB5TpGZ*F3FQJIJD3LN?hj7MO*Y;Z<7EQRybQO7{E~)Ik|W3Fq;d^*NEcxt%S> zUU>33Ij#IP0wEzVTcQWm?I|vhIFEGbYUDx)D(ldQ>WOeztitR3*PNEr)4-|M0L(2ovs7~EK!JabzsKwZJxx=; zW8Qv!dAOCb!aiWdlHDYlMr#^vg{`jajaLf_TYA_z*+_rvwXw<@yfwbFb~Ie*y5u>U zfe^`XsnyEv0xHzTh#F|1I|@ZDgE0#IELmJv(oA)PG6Po>dgbOfSF$)ST}`8-V6cDt zIC!OPjTH=Ladf*(BFj1zXVlWG%`u>>992xa;~Q)@KOI3f_4C55t%ycP<14=@W9`Pa zU)sYB(InFaT0@<@Lo3_!^+zBnnJGfASjg@im_O_5SL- z``N#5ZY#*e)Wv^4JeHO-a>UgTW>jg8XsoDA|1%E5xDY z!Z6x+q0<(hs4#`Bc?+{GblNQkr~AXQupPNoTmW(GLZ|i#Vfg;eZHr#JjP=W%z?K(d zun5OMtrn<@MO)-JP+Vya1 z1*c=$YBxk{p0iZuNI%%Fr-CL%8oaViPU6mo>z|j2iBE~wPyGV?2{=)RgYlA$bc9Oc zSS-J=QI<!ckeu^s-BgglRrS&!36E$3OOFUj%F(X!gs%a z(i<;))`m1rk#s`dTyNH}FwLA=5s_JxzAH;IX>Ym6^@4dMQyQrIp3)p6&*msyRv(+& zK#nJR1Nb#|y%$lRp{++d)lkESo^cr4WS9u@X4~_W=QqIRprSx@5nIt@V(t_1zady!ms&OM|4CZqdqmhj5ym^V@BS zl_yqk6TPSv*C{pZt+vaswY1(wMV8&UkAhTG5_xAa#@;{DOICS_&c?RVZ@k<|)WHDX z1_daVlaT>+vLKLxMmr5F0~I` zQ}ZNsgn3CmjA`4@G86d0xU^Qyz2gylrNs~;xkGGPqed1m^!+ToGg{*m#S)kmZixH9oaf&o|L$R`ei zn#lZz$Uaw}{3y8;Nr*B3zMM5Q<>?A)FLWf-&ETy$}l^`c0~WV)(dFNPkJ#F`z6$+x>hVVV zwoY8N6Lj14g2pTobgX2+hHr&Ef+AqFLf>Ywe2QGBwkEy$`9c$Jrnd`95=L~XkVF~^ zp&qx;oz9z26X9n#$5mfwFYW#sngD`u=E7i2NqBpd8z3MyhGCA3fJmuX6NVtD(-q>_ z0G<(l<$A5?OT)MywAD8G3l?11&q%s(qolvbaP%jXjGg22%R$S@=p1(=b7))JT_$_& z@rmEcM`vEXre7^4oKlSmYtLhIjokqbLH$2zv)DvEYrVLs&3)Z1zrTXNo>F1{izM{B z-u|P~Grbwq{z^jsN6GUaCZTs_&fk*IyXxn^NJ8(roxdfa-(US7CZXRadQU>{5+K04 z1PJgh0Rp^BfB^3jAi(>UV8Cyw!QT)xz+dizzbBqQH3t7a@f?5Ll0bhRQ_?1in3niq zcb{X)k`t|-kYO^zEiA0rqSzhPRfH(V?Y!umi4}1}x9hR9OpW7f6qHN|>~r@I`+L!Y z6u~}C(;al|x6RHRsVT3b%zX_y$}-8M3a-3;xw~$Ux9u$w(AqEP5Fcd}lGfex5YYR= z3BTSDut=r#wC;Y7&ZNA++nBHvomvRD`iXuO8EJMylRg&6#}Ke}FKK`F5K?k$iuxsM zj(5PP1r$FZIDI@PxC|_P&Tp0`dCszEzm)bMUr{4#*Rff5SED$klyJp@@8S*(Nj70$m?TsE!*qt$M}YcY(#gh z;*o$(_wM{7Vx7ieTsZ|z2w1CvLb8l|b91JP#y+J)zs3+olb=}aQMEfI){Qy;%?Ax6 zv)NqFmTW16)gOrRG1%Y5rmK?;d)%)N}#qORXIrXQ~QS-&3z1a<~l0$1YqC>^6rhTL@xp`Me z*rv)VQ>#2`6CqRS6WZZev4>3ljb%AMiMtR-nhktG+0j1963cu%OSjXv3aV?HYAZ>SLRzw+>ooav7r~;x7*F2O)zh(R7K=m_%vwLGd zJxe8eubVeWs;T*pPS6%Vz3oJ$T45RB6J3p5Qr6AWBH+>W){?sXv3U$yaH8#-;3mH< z_b%F=1$+AhlP&@q-t=dYZ#`w7Dz{uDQdZV5E7(UU~mp!KR=Ckb6rSBiENKhZD z!)yp@bJg<9iYqkjmVt)qSe=}`YhKC>k^FpAr>q1)tV|D>kyzrQkhLnm%-!4k6{>ekVDYd zSLn;fRb4~`Ue81hL|vkzPR2~m=B%N`p4?#YFVMM69-hvs7&OH;y~8a<7@glOi+?)i z49(zSA~mvs+)o1WpHR;@7G-2~%vaTe49 zuE}jY;jjE5)@PoeIdz{Rn`zr}Q{t+st~AD@055~42n@tjg*jE$Oywh{661i9v#e?L zI7ofd^%ZT+6>_tYk>+9ZeNI-K$wK#*yjS69)%Xuwd6bTEYtT*~<;{Ct3k1A7$td(@ zM3?HX0drMDRH^3a`x0`W>ccgWS0VV{fsY;`oh&3|krB>r0|`CFtI$wb`TNTz1V?aR z`*6!W--BT5jbg177D27;y9p431TwK_>a+2wr*Q2DPx|0mknKOPF>ib`t5C1xT*6K; z=P*A@0{?*eBaoQAQefc#DD85?WInXR8IgZe^Z2F>N3K$%G7p4v8X-h{b!%qM4Aq2c z0k zvA-FQ*x!st>~F>+_Ft|9KyvBLcEJATIAVYEA+Z1LG=OG*vj?!h*#p?$>;eBuLjBSQ z{C@qvFFM~`0{`)%GwsW=#WKM9JO3FL7Nt@yWmjlBT1vJ?SQ#$1z&}2?r7#G2DDWqMn#&aq zQhL~lCVb!U0~nN;xeil)4xeFvf@1ABle{t5VL;ocvHXC7!S^uY;U~n;=C41kCzcQc zRl%M#Ns%_Wjql8A{Mc%US&#g#?b*`RnTJ5T#o-yyUABlgS?$_gCw5Nx%wb}Q95>_< z26r1-AE1!Nf9e%20@i6ZqhsyyL)#Jlk})e zp7uJSF-g;pxS*eAuW>|qL=hhtO*y;~xNR4Vm`w6Q#)PBMBUjFVsgS&J(Ek0~Yic%0 zXh;#!BI;@_tcTjg^8%}i-34cL{*jW3ev%}Ga%!63?n-$avBwsKeNgV^8N|d2(xI*LM{CCguAys<_81y?L4X(}l0d z=RN7Fb#eTR{fQy=+1UJGk_HhC%frga-In(`5rf=>l`FFm=3^YcPtZ{apfm&dHo9L| z$|A;c|D5^+-qOX9ztPMGlWX!*7^}o!l6#1CcuF5L5=xjVX=l%=2SPC$MlT$qLkcOU zii$n_3~qEm1}a3DJ9bx&~{YVyFHE{tE2;5XRsE|{*d0_s2kB_HbtSTVC$03 zPc~`?kw={V2TX`vC3=O?DPXo4niW%KpTipsC@Bse1}wRiLu0>*4618m#vif1+tIL` zwHUr42fih+mHci)j6EP=WyAb=kxINO>X;A&y@fyL{bHA876Htt#Rq85q(LHZ!AQ+- z2Cf_&>g46eSI}o180D_2g_w=?gy7J?BfZ?K}JFVbryDBG2&2bLqP2vBnS=6KrOaN}o`1xd9D z331Cm5o@=2e0+=IpH~vg!lgo>x}@l*VIkIfs@OZp;H{8^kN3fmf!)Vw#w~@XTj?;^ zf=`-el2-*SzdL_rHRd3*!oiPqBMx_xf8{ydwPB1Cb>M>L9&WOuH%Q zY3BGYf#GQhwe_s)rPk9?7YX1KOX!H*Wlg7^1C&8*u~qVFhG^mNHB9ccK`Iyk^=~k5 z-xH25_H?p(rCQmmyzdsZW))FWI_ExDGKQsz9efOwP~+Z~E7Rwybwg0zp{q7*tm`d; zXj|(vI(YI&IFlHK@q`A&t8Ap57%j*uU(udLS%#pf=Ehd;GMxYj3*QBDnduEd`(9K* zN^m|Hf1&y4sOst`c2DJ`)tEB0$+J~E=pfZ?%mwmJc%x*3vWs3%-R_olAKI!05@9)L zex4~tZiGnIA>>iwWX#(PqRZ?X+?10q&4aIjDrJ(9nGWi3C7QNHcOr{wlVJTxXNMCU zN8ktUZyp<;ZKIOCsX|pow<1aKBI~)9W-h*miR-Dk2{dYU6Ub4p8+r$4kI!9pT#P$@ zNF3cO|G1U_5!{SURgSouYKX*Qp7^1Huh3Ek7%Fk%@)kJ6Jaf%7Cx}$b*G!%F+&e%K zv!pV$$h>=#@jf&JiR`?M_NLF|1xK@vjRnVRy?UPt{9As$P$6BsVN`2~6;I{I7Lm4d zVEn8xWv#AC=E2!9(P4C)S(Sx1eN{$9++ZM3s@hHjwq?z(zDR z7-oXkG?sEvgR_mGa#A3DdWI$57GuB8*xx+Oz9xL!P8vw>;8uz;h)>Pn2R=-?w%r4U zC?|Z>Q6D~&T9qbIlc z#5y*)uE#RV)JfVk-*#6VFiy<=d3#X}$NTwOR^ual-_A~&*cR|tK9ELIQC5#`(sH;- zT1l24cr=54EOQZUiG7WYKksR~+|v914bc1wlYhlddRA7Ze}LwfnCEvy{V##$P1^GZ z(7b7V{tvmMH{s78K=bRVzfbZ%0GfYhCj)NL-Xzc0+V08 z^uG`P8%*9B>;DTd8J6D?Mp=ELvZxZ}1H-D*q#6BS4ND@eMYo`c)=llZ(kHGrNIZyG zd_Ve-k!m5yozy&e+mUw+T=WBlBmoB|k`fdmF`Kz|v$sn2ZmGh9CHT z@R;1mJ@}TuIDPrNcFTHN0Zt;>U;4cehZT$3=9IplB+H@7w4`l_@*rbSeTqQpk_aYS zP`c>5dY>Eyid^ypxkLz)q^>g;UhebcR?th|S&3k@vDH)V{3Z({ENE4-Xq5J`kVFqA z3)Wbn=ZU1d+F)xYd4alr3GBzxOY(2|A%i#fu{tUfPn03+2V#eN8|0t3^^(;jJ9;^` zIVSZ7>`?MM8D8z&-QS5=YzW8UX^n=fOp!PhG(A9Fi7M^4F$$k8QH1IRz4Jw}mbTq5@)OINk9&7;0 zm=851Oj0MYYvQ0jj&EWA^fKXc`EcV|2->o!Rjgd1LDEQqF@w^q#t?O^nDvJkIwcrU z1fUB|VOVMsW3SqdMI+~dMqS2`CyS5^$xj)io&562lU8km{q&0yB2QT&wUg!E?-(4HfWhqc1DDpcA)t%b_M ztneeyJJ%`CN^p|MlhE@AVZ?zNcT5f>%w@&IVq6zj&4xjL zE-3ZFF`ACPq&w=3q5({urVt}sB+&+&`XYNVI)!a>WinA*Y7TBPb zZt`17F;;%1+1*h8NZ@hLla)!WG4UR3Pm;|%xp+yD`d~w(yYi8#v9102XI9pJQU=hz z3ReF?37XtVhob)^QQ)%F(x+WWLM+w5u~n=^Qt!@X%F9J&SaE!_PG5r>w4;Izeb{d$)@~x+m z9E-cCVU`sE=m=ZzE#ZUIB21N%1s50htyyf#)C6%;C$1een2#a>Dw4kSh~`Yw(J97C z@@03#r8;=~??ss*gHDNSILJgw1jSTti3&t;9i(8L+l_M6K88%+_&*#Tm{D#SMd532 zcR^tAun?+)IG!vQ9me6}R5cC1Lxl(0bNVqE>I@JoSIHcs$ z(p5K_$GURGzxvv!DE4Njf(||I2gJhqQV{)m$50OoDMH;t-s={TRPR{z0iccBdB+BRG%`7SuC3>2>6F`x+<{ z(h5?9T8sqvxkdfuxb#i!-Bz5J8x zw(PHY5$ob(%BrCPjd%TEq{`VmSFZzE*{h1ZQqM)7Dyp!?4M-$So;av&7a7?o zO}XV^#T9$w(VoqmJa5IhZ@#w3N1JlK%$72z*k@eL@6f@8MZhYLgIhzF^++JfcXy*r zF1Je0)|B4%+l(DF;8NX+OomYJ-ODNb*jxxsdq*)n{+)wv%eB+LsWkmcF@4D<`I0V} zhSQs~o17rFff;HkxR|gmyYz~ZHlvVF`cPRQ%#=!nkbao^beCygqPqFRBw0r$6D-XF zej24zhQa9 zwDmPx7ni0*nhVBCS?#slKgA}x`yJ`Tn2>|Hm!ZA4Wf$By-!Db#D(6QNs}HK4P?Av5 z0_HF*3^-GrgLSP1VBrxF3K5qe)xA=OJm2{0)GA?+t`+r{eP+(f-bS1<_{_AQ-)ziy z=cFPb*7n2JZELX{)gPr^RaktNVYUs^k(7&b{z=ml$!c7buTUt-_>9&fgkNnwMZfR^ zXRU1K-~dYOIYivxt>=>OtG;i>D3z}f~&bteYoF7Zw%TLNVM*I3{1*W)V z#2$^sFuferM>!mu6+#+kEDR~KA;y5w5m|;4u@XVas0XUlFk%y9%*lgsyfnrk`J4M- z%0GJiht!Qy0d%p8vGxygE@yY0ulgA|VuR?*-&RXA<+y09 z@Ge|e$g7BA_x0&y<9S(|eDTOUFa1g~M-AF&N-8^YW!`B9h>+yab@d+Vi!chd|6o$> zBgdL0`q>_9oung|s2$X+^Hcu>FCHzj^VWbkl7ffBj`fEUScaH#nZi)dui-_EPlp>u zZJ;1}Dr1pOQs#6|n99?_$BTn?#ZAeRVvqh%aajoU`iJ5cfse_#H+mX(wj|5C(c8ul z5?rz$u%;oDu89?N+n3E?nt+q*QjdI0wh;s~=Ax_2-?65VB}4n3%@#Z@BPfD+lNrRF zd4qh&AA8qVmNy{l4qF(yH1e=*5?7E3d64c!-4A9}1`7%4a_KK9SAs|AX^UKCN5`0o zp>8{1pB1+hX}>TnwKLIq1pXcG-oX&z8c(({ZK2S{pa|K-Jkol60)GNpDkCE3YdCrl9Io(r9^v-v(crq3l_D*uA8+=vawp4K~ghGvd;=u z10?vME5p<8GW-QeZG-T{YmgOhy`meb=LX2zA8Z3r@tvBxDP@O5)tL`nbw-^IhQFL1 z!B-;>pbVp=Iq=wbqmI*sDW^0+^ZHc=fgMvLSFojE-}91XjmflPqr4o-9I@rH(bUQ{ z$?Ilu6&Shl@3COrzzSzWy`kP)s4Hz!&|VshPuubrrDz4RM|s(q_cXZ1ypB?VY?qXk z_Q*@XPcY6hI^; z^KG&*o%6XNL1T8r_yB=?ByZ$R?NCC;EHIi*0Nu$rZlwLDFz*-v%71+PUrX@o2A5p&ehH>nb`O}U>6IE+aleA-kmQ(|&JoqZR5`J9FZo(!f{ek*O#6||F*wuAcvjo*UaAs8Eg4W|9W&*$|-#;^qwuz)RUDJ7biE< z`HM|T2(jUEo;L!AW(+4Ci_X_hkyLWv4ER2gAaeCgj=tLrU>V|IussI~aoyGy%UR~v%gUeg zRNZ7+e5^s&pXeOPgs$qS<6S%vuT-6w?vr#+^|OaeR27MyIU48O?_FurN&H?CG&z$K zHJ6CDTE6vT>^izunz5hspL#TRYSZz~@8Xi>!l6z6IJZ`w!0Dg-{t81rc|-93g|Rm_ z_=`0FmcoDTlmOez0JQkcK>r<%0lxHibJ)Ll46yYVko;eo(g9y#|MiuB0omJwf1l=m zLS%oP={J$RDT3(f-aJ2m#cwowbLs$g{zfyv;;$De58${rtqx%Ei^iYH6>(B!=H)`7|@BQ3E@8m<#3^0Wo?^Z)~s94r- zUL)l&n$c{H>hqTuB`es~CP+0Vk1d^fj#_xM;N#L8DW13us#{B72A{A&=I(rC>w>SvD`ekF#l@?h8! zRh`d76bp+suh#axx@zq{)w?m-H<;>ru4oyHv1=J)w}=^9j-8EXNu z?&wD13C3!xk_HSPle91~>P{G(-L{YBjn{aSuT zW$8=YVKm|a`#oLtFU`J0P5A}f(UVD21eP9WK|LSPOFx|H~AYTAeu@ykZsEk^hwVTh&J>oZPPU}#|n^J zkMdjYPo@+47oT8|Jr${$Ueg2`K&e8vK1nlfU);bE`xr(T0tsw$K_~0Oy8@L!@R@yA z$tJ#;T&C~hH^}OLoWD1)kHAO3=dqz#4$Jn@o*DrbY#*GgAkSn`C5!il8;R&z%S#5k zaF>|zb!s7czO)lNPB_rJgUJ`Soz~hPyyG7Z%So4C=f81ZnB!}xO^C^)hAVIGP z?1nZYwkB1&3_^lZ`5S{q_@n}QIwmTl(C`oyzw)4@LaWk+K@P z7Ji#!ogK1+Qx$!`DQ`e%u%@OlkU&oy!6#pyv`$_%5NNCs^iZ_|E;TCzU8M|*Vlwo! z0(U_+eKk>qFndJ{uXg<`1OK~ZUjzbzd9<*lO?$x>*LVXCKy;@7xBn2n^QbKYntUA0 zwThBB4tWUeszHq=3jujwBZ+8pg)Mp$r|A@4avylm=90Ug$}>VojKDDq)1AI@_#Sd< zUgh0$o+6covVnXek;$akUbYk34%IUpOpzyuuLIq;Pgy+!wwXM-f<$5geh2`PO_BS&*=6B^uFb>mopm=x_VtPYv%Pr(ue@?Np& zpeFOeNOCOW(bKKePA1~8@juYc4i52?yXvJ)gQdlM?*bz(w!)pj2-ef6YloGke)5Cx3oZud*Wsx~;Z2WDBO*wRo9BC+_HO1x%;c&5|vrf6rk_0gJJe z4^RcwmzcHgi+QEgo;l@n#fZoab8L3{eEg%50&WB0`}}-Y_|?zLxGVic-@Q6CMUPEt zQ}G;Tpty|+D@6p?G3OxKWL_WccgEL8gm;8WuKlr^s}NRI0e*yO+2v66n9BIM8>p3z zDjXa^oEQfAIp`48vmtbXd&OAxRjE^@X;zH;yO{6ZukP+VFQ%6ji_iTC5q1uT&Li^$ zh|)eTKroX>kD89r^jx;j6FQ(0uAtg94)(3AC33RimIB=1+`g^L*QriRP?<13&x`EM z?@Gopo6=#DOqQY-1klqI-|IugA4#)TB6oifoNwC&BT@Q%%927h)x$+~?UTMZIF9Q1 zuHXRFFCv0*MRit`uwa~xyYoXOVLNgq3%DM}H6o5Px?OM#W+X*_;Zn(!g|;1J{T&^U zrpXXvIuVb*et?`xDXP`PG|cm@DG|FRZ2cV>fdCyljPp{aGMkq-@tI(kJ#k`?H6pKb zl!~y5oNK{uDtTuUJV;L5`}}c=a&6dmj<8%BL1r((r$Q4^LonryYFBu*r@Zxi)$wGfkD$`Ko8CgfoHVg6Z*OA3KR+ zn3M5>01B7$Z-MR~g$@HCwe|Z>4FD2ep3ag)~w~w5`s`KrC%GmRec95vw|eB}GO5212@MHJB$BoLQb zp*Zo7S52X;m%@WO{lNU`3*{x(?sU)8YZnv@#W~T>ik`tPYF*^^r>9UNw4}R7_DTv! zbpuYO;R9EeX;VOzwwJW=?bqgHIJfnNq%Gme!R4=9uFbg}7pX4+99E`4ll20->?r$6 zAGi<20>pO#L)DsN{fep5(TK7_sz;_0=ZmQK+oS73^@U~dKkoECPZvG(W=IhZR2>f< z3`SKZ>8mW1_@JoFby@@^gFqJNtCRO8cmt5HNSHtzC8%&rvn{`+{~+`}L=xFVQI$IA zJA`QqHLugN!xIVes=XB}K5&PrIO>I@n7nO{DQ5&7CBffrzIF(8?6?q|aD%(K9WNqR-U|aeU(5gsMU zN=Ge6bdQ{nIoBL=4YaAuV7yDotITawr&T?iC_oXv9Bx+d>nw3*)-W|IPT^^Aj><%E zAwzDaoX(_Pb9S7VHb@qd*&vXqCv3%M9COXcf#F9dh#2&Rkz0PtiV?)tXiJ7cWDwRH z1|>gqnp7m20{}go_hyQh#Pg}(9&3Lgdc zyly(~sUzdi%7;m->CBzh@Xw-;Y58^uwun%cya$$U1`!ATN1P*MMp4Gd*DsXoi_^v#yWBfoljmkBV44`Gm=I>y235o5r-I$9J^PzZ{+*4< zOU~EJJUwZGm3*C%nnswXz*Ai4Vne1;=8>D&jR>7FpN|xh`o}kYRyT7t-66w z@uEYK$_OWYGXwAfzQ{WcImh);wGqNPU6UJ3D+?k2|Xh4qBWRf}IEkAd2W8{yp*W;tRXx$##NkO9VkOk`!$CTQZsO%9PPk5WWd<;=?W zaCL8;{RpdDLURL2E_q7=31Oz!8}hCXFi_6TtxiQC;E=12YNnhwTZ*G-oDMcad51kB zLqryrZI2wsD$03hZR*CBw?@RHy~b*FWcFBdBD=`So|UEVUQf4g8I-skG19JsU>Ned z@I5rX)~1nqZ|5;Cn+%d*zQ*TI=y9rfdCD{Vr6RYc8;D!rPehL*6M6o$Bx=f9J zIp?4P@_ma&d3a5yy@u18RtH{YTvbw&8PFwV%`OJ;2ee&`No63pP{h?bqC^$R;s!D` z#M1%dl-Fsh1xu#9*9qpd>Fx_|wk!nX1HhcIlBYSL11Z==DBJzvsm+H3N&Xt^#(2_N z#bvVqT%N~hWJxWOXJDPLuNSnd>KYnyM(p>{DW0$|Wj~!gF3_dt{0ir*oj-Ng>*#)) z*q6bljIxizT_kVyN4^I|bIZV%g|96mS*{+7?7BrAWv!E?F|UocJ7#w6W$k*eHSQgB z4Po|p#Z@+r`s~j*dv&%~jO<|){b^I@@w7&C>$bOdptkJ2EP9vO6R1?-yW5n|1ApD_ z7Uy=CLhL@2q@2CmSc}#u78+qf#h5vDeEj8dyx63uOt}lXa|nY4bkYeb=NJ^kS8 zi^J*Ot&`)7<&u`L?6=#w+k9y`+Nhuu(F+w`n=mUz=x44qUM87h8!_q(c3 zyUT6u%v(?EPv#Ge^K}-Mff~}IaAYC*36J%dHOmdFwy9gn=ws9Ix5B_X+w5z%Ct~2c zZdtUi5ZqXDL;o%>_A6HZ6~XBN!zBKNQUAD*{wi_)uQBS)lJsX{>`kQff5E6X$I_n| z_3OdEIpcqUQNPag8>4>d5B|z^y%`Jui{ELmH_HKF=kJ8z9|^5rT7cib;U89dlLY*i z2`xm|z0V^#Zu%^bnnjn}vI(gbW9Vc*#5H=qeaQv0n}e7XzqmN7a3s}+<^MssELvH^ zIK4Vul5y?l9_@$Eb4hWHQ%vHP9AMz=I!kq?`pgBuRY)SsT-S?6GE2I)Bc-ehQQUWv5vF(p z7bjGogT+}kZ0LIPV%^?|vf_Z*|KTb3NnQc_u1!TMP6|~bTBw?4Iy>J+|KnLhOsu?| z+?=AQB6x~RGm;QLt#P~twB;i8%IBZVY-j?g38L=?$)hBYhJI!cjMo|sySHXki_bSE z+}voPdqZ&Pp+$5}ukggj1=X7un-@jmFbHo}FDWB!%zCf_WW)NhX!^A^wdgHjapRjMwOT4G&gp0;uR=^S8EAX$C;C=Z0!=tR+Y zMn50!J^dqY1aUW^b7A;H7^_Aj{I?=!8eho?V)ZX^O5v*?9m#*PAk#zJQMB&Sjr)1G z=202tJUv63MUf1s2Qaa|0IHK4NBBS#a{@N3muY()Xm+0_f%ffaqI}o*B3G4k(lkj<`at2(Wv>RFtrrhC#f~$|m5U^;eXkt?wQm;=h-3>#RokYhm zSz$w2?7q)14A)H@B{&;40U2DQIM0ke?)fvbBbPboSm=1>MZnLMUCrvUY1E7dr^AU% zMU{H3-c=)R6fIKO7xE757rLMtND(aA{o6eqS_uL9-{YH4^npO`sa5RtH z^Fk8o9$wNosB@J>Dm1xF-<$>F_E~&b_@?>NuFdkAH;Q{~g&fE8RzvEp;Z?RqN)V1a z2je*!aj?1I3w38pZa1Zd+K*?^G>wdh+sMd|64aJNqzdGQ@lu~Vj{3;s z$_-&OX{^&aHbHj9`^%yN!-qjw?INsTsqNGB_pJAYDuK9aWvrcA))8I~hJa5^Re))4 zCZ2pRJUr6`BC(88*Q?NN(BOjFETAX+pi>s}2s}*c1gK}JkD@_MkEx#_MB+x{U8AL9B+>&${Sdy$Rp z@#zXg0bvVY5fKcw@{41*F149a_ezI&M(BbX%BQtJBBiF?Sd)6dWsbe6p{r?avkF1A zc(9%b_5f%{;-FI5-t)s{6W=>EMo(`g1!HD1CDuWU3i!m>^TmQ1cx*>*{0|!#JkH=$2F(jBfcKdKmMTaeYPpbxuGPeQJfhLVrVB_@FL1Ghp1!vexeEdt_ea^Pu~$>Wa)>>uyp69M6GM7} ztT<#HTKGRKWs*T%*8Et8B6BG%V$-t%QxdGqgM4-Loj9Y@Pv}n}K({Vj#z$?HIl5Ru zaguRs-tm|W7gS{6;~Qk0ogpM(ay_^p#y(TFeQ^+Z#XqZ~B4K)8z?%wQ3wj_;vIKTr z=TBEGtV4jhBvLw*FZx)Cj~2r!!$SQ9F)&A$-HKP>TaJ@0W=a8c`R*0G0WNbA<5$}~3SOkYZY4^Gk*X*47XQC7N{Am9+ zYHIYZR$(%G>X_Tj&BK!>)AMyd3RHAT;wjXag_ZpaQ^o&;a6xWqKtpXBEMqcFBV~C`h!J&d3AmV&i|T4-mE)+vdEj5=l{YY zZ_b`SS>)G)e;?<6!6LuT^w$CYDw}$f2>}+r^EPifA;8Y>yv>_X2(a^an(GfJ`NwMT zN25JG-ETv|e@ft#sVrM;2_w1PeMZ?e2FJK7{APGNcyX-H(kUE-dJJ;GC962BGZNx2 zwOI0fyoks+1ck!TD-IaDy0^Hz*tS@@?)&8Ubr{f?4=mF6QO+I>G=nc@742KDfMKnm`MDHV#k*b5ku_*fo(j zI{+yAF`>Oo@z4R*qzAOEP+>z)5yq{U`QJbc{rkG|)TGn!L ztyG#`-r~fldxoL$l%A5nnlG&nsCh8?>b-t697hxAER1t7QLx z2r_9m<^hFF9adp+GCXBL#}C{yDi5Bz>(nl4TZ*)bp`Up-_)8%BgrNipN;C`xlNE_p z6KS#=W(cq|KJ%#EIeinMWHrPn#4_(rMvp0mNCg@0-&(4T&4b{?XZAI`sP z=}C@=4Rl*dETCrgWgHA(A@8-FJH8imOW)Ih_2xamXympHJO9zjF0V`!nO$a5BY`!9 zMH#lZ5OJ@Nj88HrV!Lg-p_@Mzy&^Afv37mpGBkSu)95UUOloXWnpaDif`vH5xco)2 zLh5NPcdb|ByYo z5lsXvs1r#17vmjDkpl%&g=^bY(I&>e$lw&=bAur`rDX}fIh012ohLaij`tJHqN)%S z@ZY~M8c z?o6EoigzC%&?U}q2W54qG?Lsntx}NAWH5QdR#F~0WWslIiydF@59aZCa!%28dRuL} zjJ&z5Tq;-G9SYT}MG{?VefsRcde$+7zs7eU8iMfULkuJ6?SMJr1)_==<{*p(x*w$+ zLD4{HD4U8rynoQqlLTQOyg8uLVpMRQ5oO*LH#A^EYA0$}Q((pLF;d!1FAeYdV=4`4MOWJB_zq~=weNUHsBnGx&rq~f7>K=azbW9v`mkVhV4rA<~j@qc; z>i4;vMAn;SJX8$NmlQqnpf{39n~8oZzOHH6>l^kppzE%`g#zppKk0!+3hCe}G{Mch zyo{?GqH)5emAvOTooM6LGj?pC(1(}hn>1S<4>*UEZocuHy-g&&Bwf{HrIh!j)raK^ zDCu_pr8(*IPFof!aRZ`A8|JJFK_0#B1~P~}FkGU`Nr9stDOl&nQ8O2wWEgv? z0U;=!wy>31F!axpu@}mEqg(=9>vHAz;Xang+;Lk^caNMj`8-u;$wDYt9LYjTpOTi6 ze?q>m>kTcTtyM!P@kaMYW`s-68W1;0r|Oy+N`}=!$_J9KPf;g+w9-Um$|#_`+VV5P zr3SKQ8#CBP$ zFq1fZ?Pknxp9or6kgLL%hAmMLvRVuBEv6RWV1Cyd_OYO}RHFH@|JU2XK7mWH@zjNwwqiyr=$G}T4(1ByY$LR3boGUg^=5`$3t3jqIr zgBVtYk?T0KuWQq^<)qAni)N?+GdRFjOG3_Jklxdk1ho@2(+Ekgv-PP(tv{jf|Ilt@ zEYmWjz9*3SNw8CW`e=+G$JL}|MmET6ei7r$v0L-@HXY}QGPP#*ARV5cp_vnh&x2k& z!sz0g^FUK!ZCOb+&*NQ*S1=!+#(4h_!}K*qVMXNo<97JCF?3dlEJ7??m`IC`YayO_ z!tx^>+r$IMEiWRq)0x<=L3Db-2SqaN$r-}e*D+qQKG<|qVp>=|mY*UIAx<4#Jj1?Y z{efm$-e-ay(NNPL+UryIO5PPAtD4TK%YKqZ(Rzl=XZGYS_2_@0DC9kASOi_4D~-@A z=?Y}%@RL%&Hxsa*y`2Y!YALsZM0Q@jB+I!36L)$)ix!@5z8ZlT2)Ys zIcz1}s4ZhhYTuWDY7XO}nAT8QUR!5}N$co5WsEU8m@YFg^+PWSD>s(d;|5#wOWM(T zxU;9DzxbXhoB7D8Lb*O@L(Ly>Jjs&JYhKlrUD=3X6ma!P3||U#Q^3;mwsBx^59*9< zKg5EL{?g;Q)lAA&<(6(!?IYfmHwn9wNH>rL`vFr zBx`NOVSb4gI!rNG(gD}7*h44#KsJ4GUEnTVxJuS&c}mJ+L%FXbcq zoa3|RXYp|^FFOi;LJjc|yrRY5($<96LkSeZ=X;%{yPA$;9>L>M43KBR z-fi*At@OKY?^BBh{;vnO=68%VR!|YchhUa!z^K~-$Jdq>HqG7Nf!$v~ZSy``{yPNx z$6~}lPtWkLbkzULfcRe{;F~1rPXzpJP5iIh{{b-ccWKg}2>6fd=zk*scstW?1bnkB z0d{|9q24@8fStdyP=LkX1^|uiLP^b0gR{|`4mwCOZ4*)xVKlu-E z{N)+=O~LJP+{IlIfEDh3$(J82mMEo+EthR12M3)akyl$t{ z_G~D!=(E=uEJ?j5emX~t&Q?G`Dr$Q+j|Q*J*kXYWtx_8-W7FP&!7v;{i$(?2I~o?S;jgOXI0ht5c*@$U>lKZ zWCn<1yZ&yBo;#GvLk_hRuM_uj=VB!ZM=Uwe_J&;LM8oAL;|Q%iQWNTu;;lx@aeOHr z*0P8udYEZPJ;N8HhzD>ea0qm}0t`Gb&w^(9g=`c#xw!ty@!fEp=JTpAhA(q?P4ywY z#jDCo@=fit(i0u*28Ah$7&hrJDoVZdmowSHsv)WDPg?oOJOTWg9PsO0KORS#_pWW) z)L6lj@`$38++hbhZLB`UeVrl>PT3Nyzc{-Nh_l8TNha*Zdf>ewo=m~C5ewHYs1ijk zoNh1< zW%-pb%JRmisI!Ll2oAIb3xhIULDqC$;9K80q24z+(*DlnJO}q$KGy56628n#<7hMd z75>g=kuf5}MPIF8LcN5W$crfnwA8}i@5*5DISeHkxK)X9sxH*FIlyo;50y26NIq#{ z4=naVl8~dtm4a;A6Ajjz&s`g>X5jkR_VWwEPd2smb&n1S3X1Xae4%t{ICMs_VYK3A z9ky656eu<08>xZ`l%Bi@>o#*cqeWxXYI4H7glMsfu`)dgQfPaV z8MFg0&f*_}%K34~)8^!&-?}vQY~FAWjuLw|B>sCqrNa`p(fs5NDTga83yq+B9PpwJ zY}nGwC#1k`W!6NASoQ>(qI~v!B9c@_CdP8?UTKD?L59q)DCyFBpV{C!YOB`B!Zqio6|Xhl=v&f`>MVB5@Nt?fk2!h!*qB2hC?c=8 zs-k%HDN0aJSHho%ud!(Lqiy|GyVt2_6zP$tD9IY$K( zW9ZB^xzWU~2J%qK!Mn{n8cAP}<+}FsSerK6D0&`RL8_|8K_V@(8M|QVB;#4)B0THj z&eQNyr4ap&!WLnLqqD_ivGObhH4wA7N(u2`PoIu4-Tl6@(|5OgViX$;A@{r=f4Y-g z-#+M{-#i>($=C#b_M=&fYJ(1wxwd9|c5Ue~{FH70R0n&7L0&=EF^DKFpPGw_ql5SV z5cgL>c_?YyHcYVK?jGD-gS)%Cy99R#?jGFTJ-E9&!Gc?GcmG&fuqN;SPt`NCHG5r6 zs;jwE#dX|Q_j$C*^b7PSQ5~QQ9`OKpo+&qH?d7ZGgBy&PMaRNIRM8NkFsTC94;$}>R>ZR*%L%y&V#VV@si)v%!e{~d<>u48{y zH#%mPKW>Zvmh8M2z5f_P-V{84V#u4N=fCMhy$O8&#E{<$zi;P%(TV!Kr@uDvS10Pt zEc7~fzf<(LTj;MJ{oR}T10R0th2FRR4?g@hBD~(B|G)Z9uTE>bO=g7G$5a!CrkjTH z@1CML*raUh6P!0o?Q3BVVj=VKf{kM)5OR^X=d@u(#Hv9=k(-Ce)et}#rl*%XSnhq> zqC;2zHd+&UWF=c2*9bQfk3a6GT0Q%JrDPP0MwH(@&tSBd6U&DBis?efGVJU({>+*; zGC0D}PK-k&y5`I!Nzs%fatl* zl)aNDwI$zG;-~*3^b>hf#-7|%TCu}oP|8Fhq_6)o{{kV(^RR z|G*dxFe!FBL0_H1OjGs%av4V_cQ?g5_Us`%l=kYVHZ=}Sn9^;`h1uulzR~5SW zW?PMeNXYd|R$tQU$d-x8x8jI3ZtS!-RP#-%;S0B5AR#$SqGab1-Df3i2*$X^jmm{A zG_KhkI;+NGfmYbgig9*K1uM0rN^( zHdrYh^BmB11wd69ltVBr0oxZn?7r zt1-JKQsMw`69kN#Q^M2Z{@mvNq+l_FL^?f#Yk7H@JMYo~YT(gub7Uekv2%m#v^k8| z*9J=v2vQ46zmrxB>tq1ot6rTj3TN&C+9Nv2p4+`(UT9Msq;HNp*#)XAvf<761@8BH z0Z09hc82Js6`PKcag>m0grG-#M!`33U)33MFqO8!^hm(^nfjVwr#b~0{2Bl=(Pe>D z$0i&Y1RiR9*6*uh*IENsF<4$UFE_#&o225sg}kUpF61D+y)o#}(b5IaLW+!hwv4;= z5^J#AC$a3QGfnGZ=f?Slb13NpB!K#%E#h!y8RZHSiYcOT1;iO`SK<;YYW+;Sj8ytU zk}Mh!9v(eQoAuXv>{>``C+0lw7~_%3q5*(c^IUolnU#gVV%tBYZWsE`4hP;1s?yJ2m|zSp|($C<&9UnEs0ihfJOul zk=Rf~n$Y*d^}Cp$v35UZNx)X&SUm)=-&m+|p+!;00JXs7e)P!;7qYx+m|ijzT1NH( zbk0e{#NkY6z-6_G0^MXiO8u}VUmvA0h9y~=1yQ`9@rq+5IbW6l9a*+_!yfN76vpjh zNNaB^kAAj-9jfA(A=iB%B72SxIKTj@!f1t06>C;P_*8agz=uBf;0?Ic!X4qt$F_w#+56;@lm+5K`Kmk%POk}L(eX(HIcGzG9cbxA- zc03tZk1YYQ)C2B~L(DFrBhk{+rT1l;@!M!VrVQg2HIjodeaD&;bD5gO^zPa7qqDMexvMj{yt zgGp;Qq^ppBMOq*9Ltv@-yjr`DE%T0)1wnD^#c!U=x1Ym*0sz~s=}h_D#5h63Ro*rcR{O;A)wRyVCr{DpX0l z!UsTzt;+g1)~Q@^$*Ip}wJlqw{9Scz4(NI-=|{l+_IVWrl(W^o%F&h8zD@eksA{ zerEKbkdX1t2}cg>$bDlSXPUZ+QjH?_#-qoEtzgePQ7{@5b^3!@-M`#=po-E&@UGW8^%lcEIenMO08j8SHpP5t6K3&v+*6SIkT?}emH3>i zcIUxes|04P72XpkQ)YvyKUM*#Omn~hwf#W%C~0xVlKc@V|FY<~*timQ*0__wIRUU0 zAxN(5Cxy>5n$zSuSHSePE|)AXY+xE`-N-^YU#@!9!VC-{o$eM(gP-cFV-rOOT3%bQ zl8rWYCOE1bRvA&97z$|VhX+IhbR2~ zsg5+(swM(swmYc6GHbU3pGBT8=9TUA&~DGBPo{0Y1{(Y_JT$kk169M`@0FtZ5<6=E zS0ddpb-{z8^|+EP5nt`T0ss)zjqu=p!C<<8$kM=>#0=Pf)SXcQON$uW@HN5(Af)E_ zKiAOl;gRY4g~LhO6Ytk4Slct~Ja|d|3=GlL z!h9!&BiZ~xhSkqGPVH;KA0=hCdXSnP%`ra9i)ACVW3s7YFyO4Wa?_AH$Xz79ykQ;{ zxof>j0;&-1mE>h_g=jiphlS7i*LPjN>VCUKgse(TjBw75_8x)RKDAI`G5v{Q( zQqQnE1d;Lqg82YzO1y6`%Plign?@>Xel+dY5)?L{;Q{Tb|8S~&oHUC}uD?da=xsr14Rq>`1welg~QbU3vyS8sdo5XvU z^Yw#;T1Dj}d$L1J56dMS#%OFrSw#wkP|xLYv%MZ<(SGQF!mVR$k^24O(>?p{OJ;=e zRDf|oj-hq!O>D(r$jP@ec3L?T;4O??*CAwT*_Mzs@!wO%!KHV=1%iO)g0pQF1fpD2QaV8KeGHkS zwehjXzzQs32jB{uKxIluqFT}f7ez&GXHYKmEc@B^D>X-X2{anfMII6aN{9_D(S%Tk zhYPhb`ULRXSQ`|Qs6kB8 z3bf1jzD<>#HBl0t;;y9+=fR^pPs?=3lfin7gMTV_?ayfwEWq$ah*GnyBUU;?5^6&f z5N7fXy2Av7utxpLO{z(g38OO!>CvAd_r)|UtT62Z)}#kn31s*mPICgeQui zm!|0C<4*VreP%#$jiy_RcMz0uh)8){O!Cd{Ks1>;7QA2eYyB*gIh$Jxl_J{4ZJ-@o zO`Ej7Fjb<7XlC=u`h25hY6yq7Ojc=-E}aWkV*x4A-by|OIgqy2$?$YE<$-UVV^-(F zjF=Yjo;t+4Dm?*7KNGIh?a2Nds9P`3gjiYlKRwht#B&JkBxqEMWIhcgv4g zZ+*p_Hmuak$LNK211;PiMgO9yAkf341g^wH@w>-jalP^VXD~ppvYzbGq`5HVp%5kt zn`LAXc9Tb#$rX=gE%&D8d)oGk$Gv5X>xbo764eUBfGc1@vN0nJnb*ZIz0o!&v=U$e z&j7w=hR2{$C`_cFsp=qw=9sJoIVkwdgLifInZ!ME1^vi5n!enPePs0J3hmbqnQl)o z2mi%$l0}0KK(DCi4^Q?;I9KW?;HFBNRnjx7L*!&G4O*cDy;4xM30h^zx9>6SXQPVJ z!3Wx>C!7l`8ba67%G0W1t5&}+D3Up-(s(oP|Xy`cc@aoC27K`>IA zpG*ik7#|~m347OMLF75-#GO=s|c~5y;Pr@tRTg84nrwNW{2{7<;D!v z#!|JR!JNIr46pWbJHEQdT%bw)jIvXA%$SS%*a>g%jt7Uy~7N%o!9( zaR}2yFP6}{_E(kBnboR;H^e|1Rp}u5sh`iK1YB%V!nRdZ~x|9RzdeJt9U&>e*e+`Z}*Yzx2)p*|LC1a>E6s0 z|7Z6xO~u@9dH~_+SbGvz6d5XcQa|^UY|hVLbAG7YBXk*`Y)&A3Ud>=ogg)x)*YPk) z-2>!na>Y7i^(T&H57)K{+)IC~$Gd3hERw>B%V>I6HX62GBAW;0zRPpeM}7I2lkZ*} z{VjetkfQhST9BM>m3pk}^)IS&J@{^4R)|Q*H2XlPoGxt}8R-*XFT>M770Za`&)L_m7KoM> z6OQlZ50r3>$v1g0k5^b((HtVS<3MY6SV)`+Z|la_AjBTyV}W2->a2A_S6s}^)ohy` zKa-tS`zom?sje6^BoY_doK_C8`v{fh7Kdd2wQyHm&eE=>7Nq?mqSr(^!vns)Ub_yU>5fIkUh4N zH<8S6>~YvQ3L4lDnjPazL_89f+RwRjydBPd)t>`D)MWVGUaJ-{71fLs*%(17`81Ef zl6uWs5mDJ&cn`BdN?4A_DCpOIl2~x{)w81Usw^RXz%75ycayY9=On}g*0DrfQCSc~ zFX*^QgbaLmK87)k!!u3`i=QwWs&ri<00y%v{w`UY_t+`4%khNh%Gue%t5b2MWqc4L zGex{*Q@x5GzWu3)5nHS^;MoaOoIOpk?p%CpE>=ZTm$VYRti1b?S0aTz8}>z$GRw4m zJhYg1N=?z_>^6Hyn=pKqA%!?LD+9!rN2^V{w0x&xRV!t#cV>UHd3$kY=1Q+uupxPz zyOd8SZKyS3k~oU*_NHi(0hEy?MnX>J;LrdO8!auk9@^Q+?=l7Lyi!47yN6?iJBNK4#+hpz84xf%j}J{z57&U#u8 zdM^%p)gUo!BtU)+8nZsH@KqULko$HPvw)z^FY1b(^~bAP0ZnUADEHI7;j{}(t1D>jcZe~K5GsnE9QkO#lA_i-Dq9D? zm1eWJRLLuf&aG0U#bvT@OA}55*?@OEh3&pDB=t8kP<9(3O0tDsEVoVMbzZI{hepHN zkt|&i@?V@__y_2Aha{WuJ`^8+8nwpCw(5$e1TZU`D3{1&w#MJdh0St(Xq9#jnhFYH z|6yd5UtIPxS*x}?lQ=_1ZPA?*gmTXKS!aRzJS~6plWGanVcA5{b8*xeK^?p6VJs7f z{byv|Wr0HfVL&IaN$cHD+$yg2vjvlG4`=Imv=PE>akJrP!cNa~fj6NCccQUJen}4Z z32X2d!hTEG!;S@;WkWHx#=)Ce64jCkJ`&E5^h8gebkJAC(OU9hADaisBOD9_oisnT z(5hiNhr|!|yZ<1TgPZU?t`}tC3N1X6u zy#b#NGYo$#Z49bTU$>Y_IJQ;ne%X3d#4%eabsWbzKxeTNs)9DcFMwz;%z!Lg9y*ss zG6ZE=G(aWInBbIdK%@_!N+aMoy5W73EUhgs2>~2+1HS?J!Hv}tOocRZ=u$fTXh=mm z(zmxJd+^L4X4YEZtttK}%cfiOvG3PVCQbREg|Nc6pAVu0nid{ymmhPVCFp^f`N+nr z*xii0Y&f}7Dh)$pkkXE2e#x~)# z9(&kVXjh8!0l|@CvX-)Uvk`^4#^p`yieQH^`2;xfkZGAkvOw$#Dds*ytEaG{ggymD z^bN=M(8QA#*k~k$7@9=Sp@lkp?>JTgCyAiE16v@{+tsf2+pp^5XUn7n*7Fi!h~g#S z9_t>i$HpFXoR8dxl0HKG56_ocTq~n&h2e)Ksum8ek)owG8}UBtshXh>-z1P(4^>de zpWUxDJudcyEEyw^PcV8#s69$xU3Wq^`C1ZFHg)0uiXD>pN{Tt6&2neC$N7bz<)(o zbZ;K3Kj`Z3h2OXHKj_o_y{Er6@E2XZ`?cua{aSSIel5B;zt$@hy<^s!2IF<+9kc#+ zYrWFdn+@Z2@QzvU;t0Aoam2q)a_N5iBHllOe^}~G9`S#()N6v^^>yOchhcu`1I|O8 zpl%IPxG^tav!{!69^%u5q0GdyW{JE82y)t2ZwNsupfi?PIz+3uC|F4r@hb@NEUN~7FUaP?M0$RH3nW^K=Nds$zParLEw z<92r*S!_u|IEHOZ+p=IU*oITNC)r)4FWPsFtnS@^n^?bXzTDm~#^SSBKYTUlw)7S1 z4=(G-3(C7~ofR#JyBv3?GF3w!Rm9M{vI9$asT$>7XKq7RYxq8m;cqtGdXt89MuVbWC>MGjp zwWt(BI1qlMQkBr-OWK8b{aoVQxJtct*}fgYye&5sDpM0nu5QDV9D@>*E2G+$Q>+}q zQte~4yvPM+tPn^?k4rd-2P)GVtEhF%Ty28Xa>P^k*i*~OU zEu>meVih9jt?YYd)HIrSo%b3X)i1bLM!7W4#PwZY<*|T_Gp1$-mLJyE&Z&a8sjG#a zoX2}Ptv#>zhU-@=M7N5U)DseV#b)(~yp44Y_VYDRN{l>Ys+VZQn_BNrw0zYFW(&8k zbC&F#3<(w`GAa8N7PDTzXa-;mSkeKK#)#*M4BD$3k4E%B+=i@$=ELtvJ7kUes=Yq( z98Ly{uquWn&z(hg(l)YpFnT$BbTqnrHCoHGAp*x1k4U&;3Rq$2KfCoA__|^^VbjD1 zPI=)wC2R9iuUP@c;8AFkYdq7YABH@1gnf>|JU$hr1@(23E4lgo_0;{E6B<^1P1ix`h0fzh^k(8IVseEs zY%wQ+22lP@UMCkHvvk|!$Lck20!#eVN7@wA{XORmT-OCTfBEpUavVMdJ z+0{rrpb#A+3i~sZw5U?nGT=ovfHD5O4<<)j&_0X%(b+d*tsEfzI(cj~ zT4k(dzH@{_VYZxN<-#ANX%7@}8GF;S>q^{I$< zJn`K&5NVEh30CMP5yadu+=+;maVrA=;XLb z?k!1LUz^tZ7iKmyGIla>Dy)nmBQUw83hE>2W+UkG(gQo`=`-2DxVO*vDm0R7*erGs z_H^2KOPtFZK45x(uab(eN-qae?_!|Gp$_}jJz0F>Dw4hki5A5b2ByKyQAoOZeyr!u zPaW~J8}BsVab=mT6LEODG|@$;k0Ed`f2?049b{5wp=LULQ2f90USf zJ+Gu#@=f8cTjtMQ;XU;3U1!J6lJvuCE5~K>>89M#?KUT>Tf1YDchxbx;V*F>qUi}6 zsK`d_!@S*h=gF(Oz&B2WP$cjwu71>S!XaYY65wLWo3P?ieOC+!9fV52pR_O!_GIPR zuzCTda3~<;L^Sd9L&w`{%ZDQ8*Feg)RuL8SR?wA3!l)bqJQdr3_?+F_~uLZCX(K1!>*Va~F~jmXo2{{mn3(ps%IR@L;#~dho_A zU}?Z@*HUP2TKr*V$bR;$*J4P!BQw$=!RhNXbQ@!50wnX7)=!q!24L6HBy6LnwWGK% zKyBbdUjL4Kf1}f1Kt<0$`v>{{Hg&yo*1scPx;KT_pJFq*H+R>cHZdf z;r&UzzZZVr&i{gZfA8t94g5vEzjYpSTF z)_1&;@4LQ(?oHqEI`bBspnD4szs|g`{FX3&{VMVI%6~xd?=bHBr}GaKza?}311Mgw zS{(R0R!ato6)>8XND{db#`#SmRfLxs4ii+ej!>$BRHM}7`Qmw;bB556it1j^!a17q zYVYS*5&27|^rf$adsCLUx4taV9tHBHksM@*Il}(;h`rGhXFb_(2N*bP#9tw?2=G>+ z&st-Q?Ht1JTCVHS{e=o~E~K@iP8I_yP2#6h7Q)6;NB5A^rVqa~CGTmSfOl7SpIW?N zXRJyN)N7bK6KF5}z=MECD;MO-*nlcD<|HqQT#%c}rQFXCnc^e1Z&Wih#bPp5ov>77 zHq^=cvSC$&I4g`MjI{` z-GQPs)vQzHi;TfpWHc|dIAbaZs;*XPu`-Bj2d1>>?g#nk`;i5)R`5P2E|$1jhf#I1 zr4-2%KVvHK3o;E^@r=B(ww0C}j0fh#d~Z|b=X}dGhc-UWjvn;%?}&K}nz2}m%!h~{ zmoSGkyWx|==>6fbjKT1koLQBQ!>XGUxWdf@&^rw)+ReXP@Mrua)&D|xSvL|5a;Y3E z-t)LzQ2%u~4fYo{o?eLs8-=1My>JVU@^S%;8PStjs|75ho~ZMoRsTq1IaU-6b8L*zRJ;r2j_ zv`0L?Xq>Z#vpyDV>T#mj1ces`bGYp-U&YVl`c~OYNbwBD5moR-Q=p0POvj4gHMG&X zple1dQSm~eP!4KTvkA$mv)Zg3x7WH8_QDR7)`32wclv{=duDzDyPk&-Y$%qCPxQVv zN3Ta-R2amy47ue++lN#jOPFe*QaIFg`i}m=!Mz&>2%7UWsex&1E3+Cl$3hnHOOkwR zjE!&;tq1F4Y|nh1;XFzJ;EvGdH%=gQJ zcc-|NPAN1!hE=Mz?(8K22ONjnMdd0lFCQp5z-*b-07OjJ*u}< zkY+rUFh|Y_Pq@jX@{Fqy!DsR*{5ow&JN!4!*=_>nJ&bXNgrNmB z? za%0L&L+YAfao^{GrTd5t#nXBCo#9Bp@*^OFU-_2GXU^%1W2cKe*sF9vh|7K@o@LR% z`rBH_<(q7c?h2--*QZoM_~1f%Y4C)Q7K>x-R6h}73ZmlvEEQE=XsF=8EUtQNL9Bjf`LAalP0!$-=5f{R*G%^ z2(*n_k7^2!w@U2(4*MmfA3U5mT6#r}Q0!p$%+}G>O!v{{cwhy~_k+OUl0Dx_pTNN{ z-valpwqJ5=;@3Y#VGnfhJpuHWWg+^}fKGXnev10Za9o57bI3l#>gsmuiT6V@Fdnlwii|@$ybi3uhFf6z2U_AdtBU~b&nZz8 z5Rm6tjt-%QKnq$qpLT_aFM(Y`NL=KKT@nxs2|9N558;AsYg7yIFy=vv<8?yIu6F!g z!ynPNb;=>t^v!TS)J(#GN#~3eX4%VH|G-xp7DhtJHNnxX+%5+`h_Ncdr(?`Io4*J7 z>h@vsoR&g|rhtJ?{ACZ0t)n7NJFd!l`{A-#qkxH0uJc3*O{Q4at*4^9*rG?_H1otY znF*9nXHmuvWkE4 zC~2Y_>!kXqgULs}e~rAZw*CeSNxz~EdzeK^)$WCY;UKf8U&As9@8c@FMp%x$0{cub z&zv^CzwIDoQ_Yz9%H}1cVd*d0Y=;ytdjo7@zGDab%Zc&RRjNEP4idx_`BY zf7?*rx$8fM z4BcDQ`2R-YS5@$$$m=c8*T68aB3@~8tcPBaMNFAwe2>%-|A22)pEGlH-{b)6^TgwL zI*w=?3~6nYmw(yb@E(_k+Ug#GM*&@1H)Ag0X(7rbtfIX~iRKVf<1h6c`zO$^&OeX> zqy#z>zY=$Io3$XFJ6RTj9%VdY56ZaYPQGQXDx(oD2bR;1rQB z0WYI!lX9k1xMIBirGjGtOiUrw9H?sY0jXxS#df^X^xfMMGY6&AD&e z0>f%g^DX=GPCl{+?y&$>r-WxB0LB~pNA|A$dv{gQGn*9IbBY0AH7?)IGw|=;=W(qzLnqhHQ%AX_^PI3lrGCHE;ULKpY?d0aKd=k6ct#fe#efqEYgLk<#6k7L*D-FhjR&#az8`})_@HXW1Ag3&U9`*B-R1OXNfdT%AZkE& z05$F>Q4Tc*NCj<79WUTzS|J)^4M>4ukpv`C^5Wvz<@BG26=y&6Rj75yu#z(IX8erR zega9xW7@1{AY0(2)4?Khl4Gg0C&c_BZT|g5oHn59pg}h#o%oz-U;0PQ7<0mIy{-Pz z$x@3D1$8H_4@xl{(MLT1;d;vIZG^I^!YtN~ORZFu zH(z>*5Kqo8G;2Wygw~>H z9`O=WqcK2V0$!U(R6H?aYXIbnT}G?0%!|Nonyf;a%_UWlWc}J!iHShU*;vDEaYsTf zO{i>&Hxw=OEyW07?>n_YgcJ_K#cRx;AsXc{m*EdIDIp3ZPb{ytgz5OONRLjB4HBgm zUw%AnTBc;^EwGB0fYuml7@VZ+K%F(#V<;Yk{ha4s_&xc+I@&V8A~1b3u*!cJWr4-- zN2Z%uKJk7I$J($56f?&`$ah}gKb1If0MTA0OdH zW1?2o*kd@pDapz1_Jlox!9u2+Z+H0X0G-d;LD`JXB-{N+m@pM|BV+W!RPB?^P=fNO z>I);!X&`6&K9TX1J;-OR$RK)QsdDV1VYc&}E$!cHi^cOpINnsp@u!rk6>=IK)YNiK zZ}Sxlh)%H!#kDG3iw@Iy<}Sf7<2*>w4#zXRD5MMKSN45xhMqYszRWA7Qa7ziu*xUK z>aY*c!8yj1nAgK-WLz^?{s6&oG8#l?sTshuh2xyxKE1wmbvKZ0oHB}_DJz$*OXZWN zWLAr9*2Df|mKt8@jQ+==++<0~P9BGg7BDidYR#diU+Hdh__@pi7jE=HHr?_!UlKc6 zE}bhxI4Yt_w%cSzYYa89>6)?gFCR~8QMHdNCDv_L=2mE13LGBrP93Lem$%|^Q9oO` zEj*2(I`;&ka~i>8PW$lDENMlEC9i$j?0*5kY57+D?|A8Vo&5_0UhgXYE6_~$+x_tl zDE~1py=g)I)@Sp-Xh(v zGw;p7xAgNL!1Fu({Qk@Q(*t~qLjRw@6Rxsg`x<+3pHm*uS0L(S4I1{0LDnu}jek{- zE2IVzRT9Gcs<@)U^|nioQ}d+YTO=082p|ZIPfkw{(_E&vMMY+?Mv|rY=Mp^$LUT4U zeLxNXe9(t*J%^wvh3>6Nm>Aj7UKklB4YzxoJ=Zh*f(Tdc(U!S1d}Q~W0vekijp|o3 zKlQEG=cG|(lEz*%^kuhK@9>`Rw2O3{MvD7veVZyz7Yhb=tK$j22XCN-H8=Kz9#e+K zRiQevS7@rtOrk_^W+2AOvAKy{K)op7xW-H|%k2JgxU8x1!QPbnOnpuS;z`HR^>X2q zhNLcNbOL_er@5>y0rnzgu`eY?l3kx*>V>kiZ6-}j6$;?h4GU$d&;<=^)kLXM93d89 zTMkt=D;g-u>Q;S}MIgQS!C=M-Hn=SEOyDxtdt`&bdkBGwHaNJ)Yt4rC+sN5v*%RBG zYhEvD2Z59cVby))pADGsA)8>lcw`(OFeneEpgq9ulqY7(6f!3n?af|RU3N-EbgLj#=I>BgG7saq12)yxmI zQ8D=4iq+WCz|?f4ygov%DW#LMq=mCZkW4q*Z>&&e0mobjI8+7vWD#LwZx!HTwW;4bpA+Y};`ZkhQvU!PxI zzrKV^%OOu7n5VSMG)p^Y0>LiHkr)nt2au>)G=Mn-0Ma8?ZK~bg^BIMzby2l~uBXdQ z8_9o)sHTMLQRCMj6g3&9Y~-|p3r*Kg>#i@o-B+V(Q>}&Ub=Y7%$L?zUTA68&F`D}F z!TnjNy$glT7FlkNxK&~{zaop5LWR8*KWvyrS@}D&2#06*1U#hX5v0u9CI>_HuJDvZ z2e~;ehg`*l_k=4-Gd5H;0i9Schx)5XcLtos-X(?UL_A1cAH1lz9~o zAXi<(&?S@217PB#iPy*GPt3#^E6tjnRKaM5HJTRi%i?q~=Mk)I1T4i6SxL;4c0gSh zEj4m^9uo(Ms5E|7t=HgfVUHGw1+kXi3lg9m5Ss~F)tgUp{_IfC>Mt%P9B>RO_uV|> zvnlq5yW0gz_2;^un$s+sW>H)R#&9D@my`j5V#35Q9A2><^b8r1SOER1`{fHhp^jpr zIkaBr=(6VdJFwMqtce}qR>)Ea?t2|I-wyNXFYH*Ox6a}P)SRL2POW_OtfyUvPE%i9 zZ>A+G6EOs}ZqFSRb!LDZBcIlJ>FK8d`o}1i&x3TppOG9KHkcNc#GPro!1XJdnjU`v zogrDpcZ3rXAb+IBeGU+0Lds-`TZMp(WGMK#6MoD;RA4=wgxWJWKnbY4Zsn?t9vsr8 zdqZvs8@i<7ruIp;d*5cSw#%YvP7_alLKKW4HY^N3v&<)ZMTpCeq)<-O34bZbtpE(J z9AX$K*GRAucV7H#!T)qq-%LdJ&`lPG+rhC5z`32IbKR4m6A}Sats%4Qma?dOu&d0L zORtiMx`zJO&yf2c79byPu~T`+S*o>g%7f~^g!^Q0{G!rBXkx7C$lACVbUPm8b^;t` zlzhf6hN%~EyEz&>TJmIHpMH#Q*FOWisd-ViIBNx?EE$Kv&!cKlb9Z-@oyJ-nX?elD zzaFjpcck#UK>by==$IM)6_BO-E%kV>X8$oM{EpSW-m?2wtby)L3i98O!ds^HPg3}Q z^1A;EQus$;@>gf&&0F$1cn5^HOz$5!@H;#E{sI1j18+gv{{ROh?G~BgS`U;DZ;{3c z!P>d=3lx+V?UVbecK5Q5y@@{sQwfp-8>+sv!2*z}A_(-QS4k%I=&c^RJ#?I0hPQq5 z|H6Guc8+Dn`Xq?#kqVJQ&s%qog6{E%j7te}#|1g?eJgT6|F7pPGoUjlf6z0puVf&G zsFs#*pG@S_+9w^!ROd&E;?GH-Tt--t-6a?W?>%=IMPT-Sn=3eU!4QDr9;j2o!;l z{~6l=`5Gu4yg2eV(H7!-;%R4)wtfwEE_>0{6oCxXSh-%OFZ6KO_FLIPPZCKBO+Q;Zl{<}O(1PlBUVBrEJWXNm4>WH&DPV;=bO%p<{Mey%0pPE+UKug zAwIN!mA@4dd9^_`U#jil_0{vRYCd8dkQB6u(>vB#f_l4HKhp&q&y(drKYrw1vHZ02 zBd#TP6CZzT4=tiy#9`zkyF%UgvzpFs6`~L*KFmdhxrwMlWxEOrGWbf%=`VnrTCehP zclPt~N>F%iQ7+cqboRI*lN+oD40>Sn<78b*-F`!FOmr|Pc#8Y6Jx0=s#>`?XCS6yQ z`d*Bfxz%EWv#2YcZ!UB~+9nqC6<;Ewi5X}<`9B}HOYF7M>Y&*xt4UDPF6oq93f3!k zuR{C+Thdz7rBAspLAByLXi;=3MC}XBy^_6yw#*0ppp7q4m;8QT8Gf9rKLxRJq@r<6IIVvE4yI@mzJV zkZ?wP{8>Tzd~;1xvOW_c*1^MRB7eWW1}5J$xNmPqfL(MizNF!wp+RA!i-lp7VMg4s zdO^3@8(Jf7hKqNQPt7w|;H0V+KM{thO12&R1q{{@OKqwg^bw2A2CN$%BNkk5ny7eL zU6^4Rs$oJHrfNND64Siilhc?4B5^w);A}Bi`ee`+%Somg=q6XrRC&QXcI&FB6$=cL zwGut6vx%6(fU;pWmWzs)2X3QbF6bsMf!3*PqEV~}3`o`^w9&@ov^3BsT4iSOJn?6e zre~=b6n?SI*Y6ZMl@HjzPCj-uMQLMNdaNr?QbTZhcY|v4pM#W5W5tB%Uv^vEUkxWL zWH^0iCk4y5lx5+LE?j3)>a|ro{s{P)6}TyY3sez?WR~w)~*47yq#I4H1c@S}gyr z8jOX4b-5~7ay)C1Rl>c-{4oow`kF>mwpmb*HgA$rLFieQ&|np$b!mk_aCXqa!q2MS z4OZF?%0};i~M~Jj#;a zh2~pwh4Wp@SLqf(st;3xb1fhv5bL?3-(RGj3uvC(q!X}CBA)jqfhkN1RwX5O_Ln0C zFN)lKdo4U(djqr12Y&So@s|T1$_AYtySH<5*;d><9&lHgNLBw`#s6J{{;v3R|9VN0 zj`gpvw(kY&Kd$(1LWn;r{+l)8zp40dHi$yc?j&ffpcxtlfD zs##;ro8HzKy|?F2_x$^~=G&b=4DdeF`F7`@O@jY{fwv6uzuxU17@(#8=gj3lfPqD; zHyC(pnm^haNC-e!q1unyu#~Oa?0$m*n*&a6Lc$6nNT2MDy;Em^@J1y$1LxjKLZl#T zyW1ncwh6@tK~%Tr-`UovS0wURFO3ysru{Q*5~8PE>t6DFtz$qJ6xSpdO23r*=9I)@ zEx{7?H`=^3LD$aL7z@b0U#dQdiaMnc8ja|PC|<=zy6+QzY~(CYc+0llx(fCCUPfl) zlGi3}x`gpg1Jmy`@ElbP`faGt&^T2QKWko3c8kp1T6w~iQZ-@FGqtpI`{-rAq;>Pw zl&HVL$2Zql%H`N3-snst0Wj|x z1IBc|eAnsB<(WWa6G2mEN-MQqWRHro*pW<*O}{npxli?iR7;zx9&krlJBCHfduz3a z&7%fghjvUzjQ(Hs-LTTK+!~E)$?ECOk$WvMoA06hpBWH@y5jNK`% z`#b6PlZvl{7N$0U+X|0)4eWs1EeaAfop}u2zrRmumW4j`81CZJ;Gb zyTxp4_yA}4V!1u3iabq&S4He_TNyvjREshAz7%(9>ootulI1_*2ztIlsv7-;) z$lye^ecMuUPA2(0=8D+7x5c?3Re#HuG~cz9Hh8Lg;ui;Sn`Rct=^97vjAz|(1O4m= zN50(q{HXyTA7wJT9fF*7u6;(n?MFFJb8xvBWjn`%SjAgTkC%(_eY=yDDqrd)%;Cb!JY@k7UNgq?A(lnPrFYlNV8LpV{ zmVk#9_tY0%2BPaoNnOwit&ibDAS}_fuBLgbt=s0v&Tx3XV zi^yHzmf(Gny_4-!@ra5UG?Bky#faSKh)v^kfP9SPVz8^RXD4kojF7*$uyrP3vQbm7dtl|`iYX&s zDEEznFyi#aLTlA~TBHx@jmJnlb|5>ECD{d@sB0PEKIimXNtPUN5D;9cMlMA)@0x^HtbeZ{(1Z}q5<6AijZ5# zC=It9tw(yd|2rOSsdg1E4A=u)2M;YjoZ)O9?33}8*(sz>1GNyYt8^SOrEsHcpVLZ> z7IT(H`X%eyw>;n&8P+kVMqvMSUeg1ET3Za0lZK>chDa-tUiJ#Fpkl@JijonBlSdgG zs@O#gk%F+m)*mkeGI^pgOG;~`8nKu#`Ji9zl{pnEX!zDZ;H-I>XQ~!@V1})7lA6C> zOr|@AaZMg)U`<4zY*1f=3%FW0C5L95Q{I(ma8rBigx2jpgs|oo#&8aaiZ_KdhEHjy zKO=7Dm-Trzg`2UVqrxH=8G>-UG(}`{d{-nhlCq};AM}^l^`KnY({qD00@nv&LZ1oh zD|D$iKv?jSL6`k{1jpzOQ+wP21c!zIljB<$s()}7jx%~7*B_1M?`6!~FKOAUmY@%% z&7ilq_tQFa*I1gu?i9i;6iwrLOMiv;qhrX7ga!NS=@#4N&Z+ZuK<8c>HkELW{w2u1 zQ)$|`5`CM(SVFw%id~sufIc7dv+`iFk6h&G-D6#p=Z*vG^|kxl5yZa>Wd5gm{euyV z|8vCfmd^ZFsryecg7)18@E1n>Q#1doi9K5Ce^dg0VZ=WV{y)Tsf7bMl5ww342ej1h ziUZnz6bHC(BYgifY5WBZzZD06mi{kjc=sLr2hcF^>p-F9lKjCSFPjEz5ujlpSes3q zFvwzC;}HOm50S<{qe4mVh3++q$xDEk1qNSUS^#k`Rec4>EerlC1nUXRk~`v5hH_u3 zaBseHJ({6=#5`)ED(}+XjNk%3+&l6ax0< z7a~=sSVC64%3PAG=*YG`0j(y^t>Nnp2TO;(<5Mvtv$asB124AN;A9~R3z0q2mv~zm z9iq!96GBS)>1taQIoiCU8kn=+MDvhM=L`pK_(0!gF2EK_M6Hik`Vko=EF|<6^i|#$!(#EeajyWT#|@d85&}XIW5Q+k5pk~3+AnCIMeKR2o~Tlq3HY`lb_$Tx@R$` zA*Vn#7^I>{`6gaH=b)4(EnsL7jZAV+?BbN0^DasXYB>5a#*QuZNN+|Xt&&h4#|m|9 zbgdocWGmvB^=RazzKLFGh^bPvkUbTbUplH|{R7}$&1zM*70o#>=a6ha(|}+~Z3Z^S zfP2-f_?j;Ra|)Zmt#+baic||=2cew5tgcDMY+?wT>{OoIUEom)vs~i#Dm-)7fLryn zz&P+Ab+UoB;u$|w>QbxMK4-91ofA2PzrEUMW=^bz&f}Thmjhfxv7Q2stlHQ8<;Lk= zk3XiWTsCmGo`M~1J?L#JU#_S24l}Qdf3oD@!Dndd#$`JFTUrl$rDC$4XVtH4)gt z*hI5BSzBXFA-dz~1{Q88AP8UbP-w=jJt@BN=2m9;$RAk8c-&!3vqd_=mNIryVL#By zJ%sf4q`}emEqRO048n}W9i!Y%NrGd6I|z_Q8J+tNoG7mFp=ky`lgoQeNT?Y&cn-^< z%<;I}`JuGq!L(rogEEjLT}EL?T95K0d^$wLou)!BY5GjZyfA=qRqZ-_XU)Z3*kGa? z!jbkRo@_Bp6OLa8@5t*j{VU#$q=HH)1XOX+r+Z6;0$Up09-&+j>v@8X4dzOOo4&3!K&^;LzB0Y)S$z7uEPixIP-ZtMU5LI&UVY9)*~4Q ze8~wTSjrwzvqFod&^Wm*#k`ch35 z#Am(eH5`;d{iEDN+&*)`_%!1!Ea;l}@&c`qabp_#rk#iY8a+E~M9`J4?-oCv3y*>s za|-svqnu1vGJ&S`TP|B;>GVu83^pX3E;9*#?W}@Bcf*9ECX6aE1v8srs=_Q??EED# zbModfT_ZxU(sZTP^<#@6HZK;B!eM70i}INoT%Wcoz*$l2Ynyz=BG1l5rIV7})K`w~ zh7J~*r_l1fPVXB&??aTvjfKmP!ww^j>)nD0dP38>#Xi|>n30YHHOP3UHu)}K{ z%^bWYph#V46le3ZRRlU%sKB(vK{r4c8z@Sl5lz-+keVuCtZZJ)l3|yP_L%7yEA)xX zV{kE)$(x_`8hc<$GdnvI!hfNK`zY0PHSWG#uJ+M^ty<-PiU|F570$hivQ-$snHdQF zhi5Ia2JYsffC#xu%=!BFOg8J9fAf$2Ej#7C=DrQ9{9TIw|NNt;efQ}6U5ejjJO9N$ zdRm%)=Fa~v#lIi?r=I;E{G`D4%s3^|d<=&RZFOwgC>H;byTy{k=v(#U~Z$MUC)i=1fY-G)y4 z%@AIs{kf&zB_fGBDf*E@FLZu*?8PNXF%2bIh71%nKy*^`&t*Ua)>9A<@F8H?w1FJU zH8GGQp9tlt9p?9xv@md`v)ELFQhJNtKOWrrh>FpmwABBo$qr4PZqb^n~G`vO)zj{FsRfr0n4I`6Ai0Q7q9z+^v6R%4;eXh~Dz@@m%4 zz-m4~zUb>Z_w;jX)B5?>)Iy*2;+MDR)4tG2LDk*Y9)z%0vlk z4|md*W5;Kk1S%aVlmI!rAdakxw<(dZi9_j8GrcGAOplIuNhkyQUOGA0a;7(fp0Xzr ztKqw_#6*IIoQfv=bNaCBe=uq5E#c{3tiP!;>yqhmN;3#aO3b9T09a&g09A9jm2Xr9LF?Q*)L-nv*N=F10y-~rD$*Z&< zot0e`OZeuXySyJitSb4VR6}^2=N~*Q`+mAbTrBD>itfKTPP}QT? zBhW4yy39V;Is9m`vylg}%4k8Wi6=ak}N;c_dDTlT!MX0@;M$UK*5grK1hRH_8HaY253qpH}3%0gJ6L`XB z*eAG-T}u};EC+7t{h!7Rbk=~}do@39=`v2^S;O!J)FF*E+RIH=)4|I=Z38k{(k{8g{a^)6Hgo!9#n~nu!DFUJ2YGVKKg_|j6hC_P!5*pB zTr^m6Rm}<6hwp@HKZR5(ouTTZ9R?|6DxbJlMmA_-7zij(2X-P2dq^zJQEhIKBHG4C zChlO>M>Ed~uUFEwEVH^6mskdFB1X27 z;=LFH!&ryG8`bkS@EP&DJg_t!fvdS%G?&}gFe%B%bbQeO9wj=ydBJAAyMP1`0OG|P z*|{^Q(c(N{@rCUmHLB}&XWJ}Sbfyt;T!XPFL}v+RX|};@!(+s<=9^w?_5}w|Qj4cz zx)Nbpb)2e+qP(l=1%%LFJ=CSzhYJQ=&taOQPht8zh>*yVqDcrZD}o6e?HSLmh zfT_LnZG3iOkmVi~&G0M#TS93qiJWO{%(`_3yFGp^zhii^@&-Ke=3)|CPH)O`ffXVP zSc7ohuH_nUTK##CH6qNe<@0Ah2w&iaEOB`J(K+dS_&%>)Z>Q*OK=1wXgRN@vyBoVJ zBVw!{j`(n)FuRQQs`9Ja2ii#l##>uGW8a`@qwIV_T7AG=EX&&NoXB~C1RUlx(joUN zxJ14!-A4_EpP;&|VEDgS-4)w06Z9x+^{a*k+eHiRuU{1AvL#QxytrSSoErZ-ko(OK zzo_DEr-1&~7k*v8$$b8Q?Ww%0fBpu!cR$d719I=)pua)x_k;hi&;JF;{a(|r1-u>2 z(Ejlm(f;Ez`gQ01M27Z{&xrPq&**K${*9D>!QSuj`rjA74`JS3^81|pUyS#!k@LSt zSZLp8&;M7(^HCbI+U$i}J|zFDJ|Z_?eMeknoxH_)Kx`=}s!Nk95M(0a>ZfBoN6Gf^ zh(#~Ll1tiar<_Xx#N6Hin7F#f^dKna(&s3yjQo{+AKqy1W#Uc_{bIR}Z13f}gSI|t zXp46y(d!^SRTll|c~eYK3Ek$epyWg=D*M@|#E0nIj9u{R(tWoUy`)1**YvRJ7p~{} z)CZRhXP)@opj&&Fhy932TsU7$ma}bdq1CZw=Je@9)0m}MC#rpErrug!hRSTe zt_&HUa#5zMpGHN{-zF~ogr3i?o+g&hSG5VqlpZGf1ke~CTYZ_<=v&}dgs3;$(X!;W zBjSj~_&F`xgs|q`0OaVcyOSeOE3C~qN?R=XD(6&JGc{xbARe5r#sr-_yl6)a!q31` znTHFyg<>Bj^>SX(k_bAEp6f#`kth$afMkUuE)*11~In>>Iq7}h8f+K*-6r&T3!%_uZ?&V)+roW-2a@ugJ} zF1E(;n-$D#$@STZ19o>EodV<-Q0*~?!ix>;Cow!rn38?WY74hEf<)&(WNka!l{C#Y z3hQI_MW8+0$1kM+L{A1J953d9`_#h;FIZ+P(Qbt#-HF{RIw5(jvLL31JGM{H)eaaxg_Hv{o0?fIljp%YYy0v|vf)U=Z>P%8s2Ns-44wTL?p z3nFf(b?_GkR%D{q6I$QQZBD%*XG#!F(FUJ0-huWG%Gq3NEB#PE;4^eRYO&;IFY!fG zqhfdp`riArbBGcZ-+QIpwe!`(>#3I0dzfQT4gj%4#>q@=Up*26%&cOkeHj+nE_7=u z;Y%9X^&OJxXbTN=PhJ(Gpb|{VI=C`YD1faCRy79(h&u;Z!$o%5DpVBp~mpeeQR8)6u+*hgd!6n3d;IWBQuMB z4pzW)^|>hU`>v~&Or9kp4U=ES7Hc{x39}(jOw|}CaV+N+680GV8k^MB(SR$i4LKuMQ2kvj{e@FxX&ll&=Sf_0XthI6dSz%D5 zqP_n$fOnK$A6dJ`zv>KW?o=WtTnO$lcYMRbi>6Y@eWg=BFT*3R=`{iKfH+6C2d|B; zz8qD^kP#2Extkto5qcRb6!R#7$;_+jG0QR;_Y3tyUW|~}4WJ3F^O79gQ$xx*hQ{nj z6(kQcqd0)y^gfe)%AAdfrOAd$hR}o{ePp%_>R|cY=-oAq_)`G!eiBZT?pV)(1fm+2 zZXS~)G<5{@#L}dCo0VI6_((Cw1QI!Kyre%*8nbW+z$ZW`?ZUpLkyuR*9;l_qFYt5Z zG(I56!1%VG4CBG)$fqFl*+E(~$s3liI|=&|V~Mk5Va6b){R>$ucx{$9k#&P|30$kI zBf&c`B;JyBTwVa?C;fmEL$7U#7tnz_rSclZj_vY7NLxEQ=c(bCNavJ0Y>Mo#7RNm+ zd!zw*{YV{kM(abgOhos)=TLM50t)91+O)6wig)1!MKQ$Hm!YG6b_I~y-1IRp<8ZoA zJ?&l!?)WGkvAeU!S^8^laHD{6-qcobYO64>xjnqPlZQ%hts;RCo{CR$=q=u{_pr!^GqO({)T1RXUGbgtuj2#hJiW zf&|Ia`oc5X$NJ85Dq&zzyQZ589%CHE?b!u6eCAB59w%lJ(Q`n_j@a!R=nlrHO{}^F zw`r>hE<6rX_&W6kFXUvFBN`n(n(|h_5l{mczLj~(qTvQBaRAYX!)Sg zQTbKPL|J`u#1yW&R+(7t!|rMH9{-0;5ME!rnNP@wyaV8(abs=_rwC}xA4b<;XxN@~ zv?R@RGK?KGS8)lDffoX`2_!@8oq|fzU8Q}xfg^FQppYhcm|SGI8b>}=jMVqAP(nr( z@a&g23Sspxm78Df0yl4B54r5hu*ofukw`Hfoij*6IN(omr6r`7hwYME($bJi3x(Xv z8vZ~@^cX2jWDM6*DwQjFfH_;iHFE9l-bz=g0L>Cn#@3=UivvV$f`o!^VqOjHt}wNS zI0QxFc~czsR(3qwy1TV@tD{K`4{&Z&&3()5zqJm&X2ulxX+%{7yMqG4Cx^Scbqo`}sk{|?W7m;T>K#=`tRsvg>Z8GL?m!oNH6qWz=gkA|6k(S?=}6wvp+r_+CM%X+CM%X+CM%X z+JAgJZ|~%fkB9c%$MYAA{r2(v`BnegaC?{Y{I4(;@)kh+-a?~IyGh;r{b7gI?yxps zTC`=DFaeIA2gxW>gS)@*r4<~P?2uQa=EGv92}QZ{O>)(pVA~{<)%izV#GDMNz^OF3 ziN~hitY*A1H%6A0S3vk!*x%`aiByJr+0pzTTR(HE%;~asp1N_pr3W4vUviw2iU9Rp z8mde_?*A-i`8JN}BsS%~=f-;VSh0J@6B`2ex>4uHCi>KW`Z{j1^vQqEvEajfbi8~g z=Rtmu=#6&ch7C!PAUW}8&b-COFZ`p`;V2<7KWXDYT^+4S%=^BtEVSJjezUyzM&-iB zT4ma2`z?7|C{XQotD1i*#sK2gD7U(Q7F4z;7JXWD6iPiZKR?oXC0w0GqFKv9G;Pa! zxBDWQX#|$0r%d1$h@|7oqwH-%t{JQ*>Niqlq#}RAkqiCOatnsMCH9IR`LT(LD&?N) z{G|f(xF}1?tW2|+O{q~Qv@aN#BGbZmk&0@X6MdrA@|Mrrl=w-PRu9izbA*{AY;F#S z#2;d^VHYD~6*qdxJWzG#_Y1z)BS9B?AcUwDC4Wd7Lx~g-JMScqvbjv%Se{Rgd!!bWk{z0)vu}752&`1e8*%7~R|$E=ykg4zsc_Akqm7%v#(-PSJ?_9 zMY;-g!E^7-Mp$1FGQh$=`bt1i##L6RfGyH;_d z-8PdHI_v@K*=|w8IA>fdAhkHQTB9Krz-XY5a@#os8r?M8$;^NtFmvPvrVnVi?D!KQ zB8jp^<89XyJyBO@arB{RR6&APkWat4K^4+H6z0OlYn5iyqUCHm=$S2LBPN?jENnSQ zqRe_ZHwk4nFJe2oX%ZzUfmk9E`;+Yx{bYlMv1 zs>SFbm(R2`JqS`+%9q#C5;Uh{_=F7C>N~jODv^$YvTL6fQ*IWtzG8EUfIpZuZUhq8 zKjnfl1=h8>K4b-Sd`hMw5JSfSRJbL=;Sxr077QZ^{xMkqj?5BZd&+D@-Adp#Ea_-hV0QyCB z=p%4Rtn3NHree&gbl9yAtE*#B30z6wIn%W5os7M^ZjM=Vd|&X+cuAnhDm6p#E}p5R z?742$HbQ;nxghumZo9}`YLH0Kp!Wb7^+m=gm@%$!B6e9sqOr|^o4qe;X)_%{(xgR$ z=5uwi^8+?OQ=8*Q4HCKteVItFz2sbxB=^EQVK|KtMxMUQ@@veO0}{X!Z!ViaADq+y z!S+nOC>LqxD3w;K*|50b`ThB+n-9?pTHBq*3r^jBbMzDKRi9E1Fz^%m-$kKX6ROWxsd!}J5MWE(55 zsvkoRaG3r;!e>)HAkN`*RL}02T8SM_noSR!$Cs6ipy8$sWuK;c0FYqLUh&wOojCgn z!MB&Tt!sTkKc)d1n6?jhkK)kwyVMWs#G|lInwQ(Q_%<+XamE~PpjOwIV9IO!3PJN66cZ@JH#x?zCj z(A?JMceD7LTjfph7Pb~~g&kk#_&TgG%&N*n<8YvUYBe+xGLxGRb~px4b3 zx6P^JX)SmTK*p_T5qD-Jb|iPDHZXD`$Gi}$0JGV{HL31;bgEmh)yO#{=z2BhcNq#k z*8xY~0HmG~S;mCf+fZG=F4|=be?8B_#&lglfY!@~jSG3!WXRe5d4Ij*`L^k_*-fyk z29hM$r$d%XxIe@9O79H+J7)V$2*0@Dtzqe}Ew$gCzdtzfA2Zv# z0`PBUd-nwXH_Z0#5B!_ien0rna{e#2)PAq&*8+ZRsr}aK{MsCQ*W$cgyoa>VzPoeY z?)-CF`WK=7*3A6*4gVsvcjwIiN@zYx-^@1!e`P!yY#G8j7@j9Bfx=ld+jppkCs)aY z`!T~4SP`D{Th-HL*ED{2pXQS>7DLtsj@1==OL&%_o_-L0>vUFGV-x2S1q!Rq=E~1~ z6xx*0>#tcQ&U#Ec^DD`qw!w!&eIkHN_THfGk)kwbyg`pUTfk9_W-V|X4XY5dJYp*q zDhfPNBg)&fO=W3i@HzJ_+D+7n&bfI|6)zgVI2Rp~=g8frd5s@P_Q+>;R z*+@m%_Mv6Miu#Ie%G%QglYWjUFi9wC{eAc2r_3>_tMFYF489@30tspJ{R~rqTBgmM zQMj!IDm9YxsjVZ{{(V`J0{Ner$V8*KHF!L1(z|*jH<5vfZm(B1er<*|Jz}~kOojQX z%{V-Lf)nrZQMLvXj~7fodEk1 z2t81_B}8vXY=O?)nv|N(i4?q7F6&PheG-~oWqa~<37$^>9-0JZ3c3=hfjj-9Cdpn9 zedQ;%J`KPxm{&tuqSOtHKBe6l=YBVt7j%{#^Q4kIlXdu-4Su83r+~hfA5OoBtVDww%CySwD~eZ99GHmMiNQc2H8&}H$8q&`pa zNfSSdkTtD#S*Ht!yPO1om7$}!o8P?k$4x8zYPF|mT9~TC&YOmH?*Z-K|N> zBM}N^$~b6OK5?*!D3f?h$PZ#Qdw^dBtJmwr6?V6^#%BP|57fSHP3PX&7W~Ksq(P`&BJ+5V-@E`8I5WGy zb=iWtK9Fm$LfPnqdnJTR17v;pj_9S}fl_kmVmvV0lw&>Q0g2Nd25QMpbLKg_^?_S= zzH=)c_2_Xygx%A=-TPr|)A571o{>e#)Zq7N#ZiAXf&m0N_lO7x019oX4 zatPoIsH@D7B|h9w2eQiAeDq2>>E-@_^ivpX;IM|NBZ<~)`1a2eP#6K6DH zO*$yDd)Itd&{T0<1&^K)JdXrFDDB*9xrU-RVdGE;S$GH;#6&2A3)$%a#~w#(+42_| z$@B54Q_t)1Pr$zgnrA^$5D1qRZn)RG72QC+`JZ%ZTf-ZIEvqBHF-(t^t?Nevnr11!sP?^oehXhv|KoI>SI@+mE>ak?Wb22 zUUwd0iJ6ZEa}J|9Ww(~R=)JpaNcppIO*;%f$YCT-Mku3PP0H!4&+u(G=tIY1sj!|J ze7VRDiVZ}ml`?!1sTHl*e3Yik+XXln_h<>IeAb#|S;0ms4;LC<`B-B- z(%N&OCOmhvw~BR%&oJSgOiWfW#F7%yML^gXbF|>+TNV6dc2rflu(NTG?Z=?afPSv| z$$#v%{z9kR46vQ$mxc&N1-I6lYjY%I+#U|^61YKAdwLD6?J_sXj7lQbcIEuR^#ufv z^$02+uDMKsuJ9P}75nB~>=1v@iy1?lQX8t??vVHxK#^+Cnu@y_JF6WkG_^a3RP^wa z4LFZ^*RjO{(3zR2R15f3vj-OqMS;7ujnt8&TMH}>fV0#Amu9#=rweLY+a$M$Q!grL z-l0MQ1`M<3u(&?Nn>%;z9_(vZ{LklF=2;%JMJHyW3imTq$oldc-57=zHl(f-PFf#7 z4F%LAZ)Tiq;v%q8;gPMHGmV~H`q-rIm5=%hkU7eoj;k0QO#91_d3=0) zcYW`voqW|zfqTL6>7f+0M|lELQ~2_bOB|bW>3VVoTPA-r)Au3NC=^}l>dJ+)owS>Q zG%4}I?pI?NtaYyfzSsMe%}KaCP)eM8S?<2$G`KKjAE^^+0JNh(fGyfu0M&jcFnZ(-dZfcTG@?A>MeHO-{rL_5VzPHz-+#np zhm1(BV`cGuYPi$Ww|!r3I+D!Ll+$^fj`aD2fe9-p zEq7qXl39M*Klrppg{5T+lZNrpM!tx_y0+8-#>4{MYKuKQn7fo$L=&ouR*bb>%ngx= zK|L{E=pDyGLd?Ec^Y|#%-##%xlMdliK^RI;&GvZCsul0YwvKPSUX}Iyyf$3(k(OS< zT0K9`a~|v@#4_LNeh%>G33nC|{#b?-CT;GUzoi~jlI^@aHv=)%2bx=&$uWu#n?eWbgYfWE3xUFN5<{3a{08peyNJ)SMyek*>CnZ!NkyurAG6h%`5 z#QDyR5vV6Jh+6cR!Iyyh88W%Yd;E71}EA0s~V$4?QQx1O-xcO;(PdCL7Ycp&0 zRVRl{Hgh<6Z`9z)*z#0AV{<%+z<;KKH(i#zLAs={Av=G=sYDbZVX?(Zd>6r>I#IN` z0}0kIh^gK|Kwpn8<`!S-f(CpI67?`e2C{cQ3^;=5>3fwj3~uG|JIq4 z6gf&dLU!zx0+2E^$zXh>>xHqRCey_n*i7154#(t3cd*}Jk@r<5{87$ddPkjzdrroF zIuIl}(;Oe%)dW8s4aYuvHoR>ULs#hRgY@%HJ|>B3*%HP@RMj^zvZd3A1rn$_O9acI zZP3nCHGfiz8eG8gRUmHKK_+loFPtZ{#jD3>4(zjhDM$;P-Jtop9xkA~R1^|JDq}_> z$E|y5&Iuk@*JI(Sl!&sg-^X(|Pq^hvDA=+V>}}>mBL#80SQxJIdEii!PecQvD9$_y z(~fY?2N7+&LO)e`a8qa9(oFE1@jGb?P|=`waRt!9S3P;R^Q;jOoC@x&B!2~Tj?9VA zo+3~0yajvPY;{u~TiU8@J{rj`6YlI6%OcaF^pK>G?*Z-EDa`GTwgWc4@&4GnU^uOT z@3rZ`#|9%j&x!$|5)^0529vH#D^RR{dvmEbPGI5C^C7Q{%Q!P%Qvz3e^sRAUvf8z7 zy{Y1X#FlSj6R4FU314B~h|z4#&94Vc5F(dvyvVcq=eWt@V6S09?ss>W1uwAUo|tyW z9h4sA3PvcHl`hv-SBHhR^^CfyT-#i2K+|-lyw8mweIjQj?eQ*E?oCKcA(|gA_q_GK zeqJ7WRWEk{pAY)!_1d0WIdoS)Hd;Wl{%uszlQ;v7=-zu)+XpKcWO6qro;aR{{jj&} zW|^+S9gpHDIs<(iYu8v~1z9kE=n-NbyBpyFDi$CUQzy?A3KyJWWv*fuQb+*eGDepq z_R}rgNBC+WXE>G+BHGGm4e-;!ABOdCXhlOSL7D6x$AiQ_KWI*rO>#3%S|jQ8-Bq3AgmlN0plj5!gyMaTpV!6*tgklbl9phg0rQy&bb8Of^2fBKHB7VZ1i}tsh9_}nE3I& zgagF!u6TPicxmRVwSiI;`TkX4s>{Yf`9BXte^Prad$^=Dux42@dunU3KNn-xLB~Er z?8f9mK^w@1OMY$U+QZT;|Kez}>xabJD-N}%E3TQU)!o&fqPcLkKc+lpXKYh9-}!{2 z2iw<)wN??DdZ$U|KpmVuaN+n>?c#h&&DpJJwV!#>t)URm7V37|8nBV~Uu8b3Ah z(R8KuZ7+p^!`p_2mMXxLa<8@j;0)c;Xf>QiExxWf3CY6Ev5u(TP$;8xhpx#;;ckLY zEMi=C9f;G(o&BNYF3JcO`_%q<`008Pcn1?l(b2;zQ|zcf+m!Nr-GcotJ+P&wu{nZg zy^CA?Ebn$<)Q05)4 zG#%*1zPE9mYlpDEtU@4R3YtID)jT#h>upaxrKUAI^1RyJsm?g4_-3W2t&6A0NQlHA zt**$?_gyxAe*vPP@)!GeSoFIr|0>or%zqy*{KsD{$F6xzs$nFu;^VV{C4pkbVvK{8GO6*4;KAJL%)4~f8ON3Xy{$;_rKCm z%i9^^<{J&&Dqd~Y&vdN|G!sdwu*|2lII!2|%G6k5<3dFu$b$F*Y){0xzfIKvhyR=- z=oc?ki}+T_FUHIsa?;E4oWMD$e{ud{mqs@#97B>zTHsXgb2_iIhPxck>B|pjV*$$} zBKq?YS;li((M_x|ST5ChU7$|=SI)dQSKpgl3zk%Z;Ejn|bL=l0l&xDcnQSC4zwi4s z9KLnH`$(szalH19gwct1F=MqFm@V1kg7NJ1;N|GW40t%rWJS6TGbQU)$~0v9z?Z)vewss#?CO`mEgSHfrCPsDpvu-{sYxasr&997Uy= z4ZRkudQy&Frl9T`V6a-Rrv*O)!kz@mMWZXoS-M1|w71-=U(7NLco%PyAH!w3b@B*W zAp-+Kjr>TpqbHP#DnESD2e#X598>1lhzP2KTtciu<=j^h6sPM3n2L@6k-ReBNh8dl z&|kj0t=#n61vC=Ixso}(se^KqQUWwQKvjCCftZ9!Xgl#oD~i!e?ZM2?-aFlU7YDJ( z=61~;Ru1x%k3q1C3|1ZltT6qRlyMLtGK?yW2SzX)BkWuwRoXWE33y$(vmnhXfudfo zc}7H-%L?L`R_>ab3Yk1`d*C8LEjxopp)H#X>~GU$nCfxPTK@RF&o7mm3TxM% z1W|;0zyrMY3BLK%{N279L*=m34#S%PXrPW(FlLi5&>HYDj2~621TjT-8ew~VtBB0M zI0;Llyk10em^laE5nd4^Hcns8cp!laPXCyNoy|AQrPwhKd>M+-GB)e9ycKjsKx z9G<@PK=B+zD;-MfmS}trixUb4vC|#iy6?f`iW-L3FDW2a$<{dOoR#Am^%b%XvQg~< z!Q?lls(?G=ph~yabL~CDHBieP52lIQc80~cNXnLq7~%r7?Sw2A8^6{4U61klw5L!& zfj28)LKY$doL-frFfyWqzgDI&?w^+T$laC?KkkZ{YJ; zY$khTCw&AXR0c1nukl^BEU`{eSgfB%uwq_z=kPEHKuDAiwjFm!5BkEGd^i0TmZ&B+ zZ627@I9Aj+SHwrIUq2@9^o$&JZNuMO&$hCHC2rJ1(F*ajv9{;{?e8&%r!3x{TivWr zO)pVD-kkeqWf~_2J~ds^cv|iWSRRnBD3U;2bBIiF4O%!|tHAL2AF$if$0+KfugyWq z2q{{gtWO^s(_F0^WL{Aj&`M+TR_DSAtm`lf^t6b^3MvCMB)))t1FHcDfH<@#obq== zHWmhi+Bhv%dF$iy&achr4lc0%I6&Q*h-WM2p7hGqg$3q@{A$HhCwR{dEY6OVFNJ37 z*h4C?IU-3B_@+W{mgK%}F;hC7+Y*!pN!a^5e2Y@z*n_E{jsW8^oyzbbq(-adR2D(+ z%&$cOdnp}1YiV6gQj8(6699igJ|4cA!mtZB0u^gvE?Wmo6-D z#kJ34cq-$Wr!_RAR-m867i;G{+3NKhkV)oO5h~b!pqJ{S&~RC$qCkV19zw0WXZ5mP1NtI(Zk&8Cw%&>A&dgdfV zHdz2h$;=;-OzJaRL6N3jHjeJ5AS`M;=539RgNDkIn(^Fi#Bh!tc*ujQ^Ns*n`AsHJ2ixA7(xy$u| z*650a7v-FTn*lyJSks1!!8FPc3`xvd#>HghD8ra4Z+-M6Y{m`P7xhW28nhE*CY;~Y z(}*#)IOR+?#$o)vc-f|oXd~AQ9u-9{w%#J(u6#M_#ZeiA4hLk_2zMbzGou++(Wv+O ztWN~BGrx(N>lzT6ZLp#Nc6;Q$oRn5Jl8ztb{?Q|X`Dz?iN59T)ZGY-`IM}o$-Ffkq zvKGB&W9uqpM$$mc5*=qHHO3}L^R#aj@Dw5ZcV2_Fjg_vZ=`ZT}4LG#Fi|#+H!|*?W z4z$13yMHS5f6hAp2zUP80(w{X{x_`iuJrwXSm*Bv%Kr=2`MsuJ@8cKi{B|<_I+lBP zD!yI(13iBs&c8f`|4+~QyRh(oMVwzf>x;ce%a`Ph&6P!4G%I0^5UXi~+L z(IvZTtIf3MTJN5bNem2*^xz8QM~==T;tWwSX-){YICfW*3!KEcTxHK@4--J2D9o`h zLRnNZ-D7CUm|nH1OKjqV7C?^M-q`+h%e5kdoikxW0FFKE0Mo^rjks+>l+gSoRDIyJ z*e=lQag>$7HqXeZZI!7pQu6kbD{aeRQESFnnfc>w8fV+N{ge3~vH(wfv$7h5DhQ8< z8UZ&li2gj1>z?pT%?+oT6dax$1kdcCLtpGxqx)B;7tEr<4g-7UhArpOE~PC9D^VxU zM1v{J2hHotNbH)$3=O-o&q5Gcv@JE}x3Z&?EXuAtqySsE{%+l_2RQR5n6{T3yF8?U zB+YFNGf6ZA^My3tc}v+ft~o+Kx!T-7=~?-GFq(n(7A7^MF+!y=tnWf^i{JWPE7_%5 zVkKqTGVBbJ1+nRgjLmx;8dDQn7e9BcvZrIue5duj;HY!N^K7OVT^^*&*Ig#^m>uN! zJWWG?qb#g-(f6};Tv9pF|1MfXleBWGS1Gw#E5V-~m_Z*leQu0(RaeL+ik9co_*r?ioJI__$Ji)z&jArb`573= zrqU3OP};;GqmU{u5w!whLo2sY=(u>=OVHK0U#Bid!LCbC!HgXUKz_A$LB$`k)$O3r zT9`)_aa+Ji#?;^i-b?G8C!+cXH@jqfWs~b`{_qG)5)t=`*rp8w{UrUYg@hGioVBMR zxN1Sml(7K{c)F^cqnsXhi?^Ned^yGEd<=RpX104!)9fa4|K0-OE_}{0IVlNNQ5uz8 zTECXVI>8hBM&1pG4#TT$2HXs8N0s5>)U=%W&j-GC#{vK|WBhBV$~ zrDdbGUqE?>?wf&}52P7fG3y@i7gh2OC%K_ai=t17t+IuJiZ{t$fIA)nU2yCVM2{oc zKWH+?8I|Cs9QpTjD?G1=se?udr#@$Pl&oAt(nU60k30lt$L+EbDSHY~q#1TjkUqv3 z*dbKxc+dM}m+bp-@KHm#ki&DY2l3*;=~#+om1-mj;!9p0C1>Yl{~+k=wsNbh=)!1k zf`k}N*$yRe+&Chd?*kCSFAmnfBxhG;kRmZc49IM6WpL*rcz)S*=2A=xnXo*){;<{Q zEd@j|8_!R5yGo6;zVFYqoSKK|UTsmyLx8Jpc>Y7$x~$d!Fe@e{IJHPy&0EQw{SpJLacZUV|0bfcZv z_5UO8D}(CT)}}*ncMDE{;JSkccXxMpcMI+w+}$O(6Wl!zg1ZNI2=EaC=j7IRs?K|V z-G8fQW)IJvO-=V&tGgd+HZhzMi)LDE$uEuWc{G=fgHr}Z+Xsn6YSt+{F`Xx_F)HGjWoXbUx8B*Gc=|dbw(+97Am19|V6TLKRLK4b5QD^e(PR zaJLA4OX=nGoplA)VCL}cm7u3VQCBwK&i+V?x+&9C(H_nuboQ(UBnN3l^m_gtsas>J zbi!zYJe9wEY26zgwV*e}i5-Dwans*H19Q=#=c>Xm_A=U)d~7rwuw$iG5L#|4g`0Ir z31ToB4_oegKj{N@B&E9Nrq^kXkO?o-5r!8t38+7;Nz3Uei5h@JT`O6u%EMM$+d0E{ zgCq1Ahp#BT2FZR)^s#FzxR)JKJ84)f**eQ(P*;oyE?uQeL1K4P_+ey@4Mw{8Xz0Qw zKk2;?ta&UfV6A%;Nwz8ujKNI*sNia~>3Dfm66v(H?>;Gd;HY)k+O5#4=?s1!h|2PM z$_Jvib8V@pd+#;-B?$Y0jR{%UM1ChfuW0kku_bICXv>x(=T2CB-k@1ghqW{PO z{y%es=cwL4Il^zSNhPRq;8umfZ{$~UjHfINvB-7742lRQKTo8 z(Vj5Z+3LhoTYsET>eWm(=H2jpdYXD05=7+J5xKM|U*d(PEsqpe>R+9k-T{9MKa4a? zjuE}#Ayd~tPwDWkzo+17bc^`Nj46jijM+k1O^KXxzn67RcdlI5a} zt!~Zet;`HCz&baXk5xRTP$>5b~S_ImTtzpR9~x zw^_gqP$qYwYF{bjozIdfzrg08?l=Civ(jC^CH_#g^oGwS>1u^ z$Tya4lSVyJ<_1vZ`88^>6yx#71aWOYtSAFmn%ST(lCHTq-o*5yG%&U zRt*P$1>xT`^#Ji>pQfMX$t!de$)DEC19q95wm%eH7H}Tey z+oVM-bF|m(R}0^KH6likk&y$oGTtqg!9t|m_a;N%6=+gZr!6fnSxRKm<2O={GbG)* z?UOfDWWyJ+2RUfXl8`kar%^8XLLfW%NJ zbCSE>dF`FY9vS1q(`iFzG!KNeWW>~mtJlC0-;{G9npQWH<*bcDM zCtml3wyz#NE3ioVz~zv!>q*T~w!AJH4w?oY}^<$4=AdPHh3WK7=>)(YpoK#WOMw``q$H zpQ5c#MYV}%CU<%f}9S+%ugA(1ApdPJAzB|TN z*kWvN^Gy|t2mv{1b66MRV(-p%>53snVZLq^|6?6hW9kN|3Hlx!==1%kdK)bAk^RK? z=iY*n)(yV>894~WH_lXfXK3M{_7#nN-KkEIUh6oek{n=-SzAad8^l_e(ZIYBA$G)1 zml7t;FpNd_|6Wc8^LY|xZLQ2R*v7R8748cq!PpptQ%7YPlc<~ycK;MBMP~6xAq1Ld z=xn3lU}(e2wh^5E!eX$2XyUx(sBjz#QRH-KZ;3B8%t=9pGp_H)8jfP>1BvfeAGCO4ipuXwhxqwH~WKge(%kzSXGKeu{NrMzHP z=pH&9w=ikJUWb|V)%>R9tMdFw;-Sbu8j!%5bPN_ih0&dCphuyXukBYDaUC;sD(Me9 zLM1)3}Uz7IL1Y z!&`FVjhzw3^Rokj4kkVGaqjGv8+%FKTRLD=x<-s!2m$O8HhfneM1u?aTWDzm57wnN z@IO%SsS(f`m*cowT!r%!ygO4e`jk)9zY_m#SRFS;sSx)4s+llqTvjahR+6f#3t}X@3q0~f&CHKAa-A!)j7rD9=}_; z*m8G-^Cs^%&QL)%km}XNB{BX;=WJJWQa}-KP#W4GC?)9ma&}I;l>uf~c#ORmp$E@3 zm4cOypbqyegkRf^7u%nmi z&doNRIy2~ZTTv4-MY~c*k48L!0_E&vukH}EcY!}>^eZI6q13oH_n(D!x}R*>HQgd@x-nd znzO{0F3^kolQ)0;2`Usff1k&7j5ApU3#R1e$e=q8QC4mujb8eX7|$1BaHsYej7!m; z><(=sVQNKbBXgj4r;=_z25|E<*rFIwu|UdMcEG&Jk^}sDk_>Mfn$^mfCR-;QT$fk7`o_xMmZ+(sJlqqk*0kfxb8^M))8?_mG<~7}pb27^auY_qf zNQ2P9)RzSAV7DMzY@7sT$+hp}^<8{qBiC|`Dsggb=cnZ|iTgKpnq+6!nYo>r%`BTs zY#S@7eH}%=p5&~YxLIp2f3|+Jdd1veoQ~c9DY8j!9{FbT#O+=HcM0WMz1fA(biv;J z0m?m~%4da#tPi^~R>qN5>*LmngzR>;oOzih2OqW$De<1Un>_ft8(Zp;w~1^H!6v7w z6^$-V+*w&ppaFGd&VHRi^-6Koq^s+r-&$@uw=U`E2WWrJo>AOH9_+nssd8??%D+xz zztU~Kum33ijk%;VGmhA?=&^}FRZDLK!#XoF_0J=kkx?;_e_#c_@#PoXJ+>MC+g8Ky zR}$fWV})jT&f@!n75qlE|0?~L;W^0f4_5H|#=olkRnGr_75uL0R{_6R0o`xQ%;VrW zpoZZkV36TCV33aCDeC7JVR(+}c^tejhu?WVkF4N1lIL;o!W>?rZx~*pZx~*pZx~*p zZx~*pZx~*pZx|S!qi-0Vqi^UKo}BEz{`#|F{c-S8^Kf(6ZjT(6%5Zm(KST#O#t6jta$xdn7UU&2Y{V$9F3cI&owBtmsRk3G z$!<4r3ilx)Ne6!s7*<$dO!<6KN1RK!U$&6HOvKk#FF6=&5hq%=Wy62C9Ub>Dv2SDM zK9q=;v-9IB=oa1<$z%Nx&(6#BZD7hf$`j#w^^Om_W?Z=kHS?rNd$WuJXf*>ncMhs( ziDpphUsK1!M(l(&mxj-8o66%CkU!Se5H78)Z2S=WK6J`%b#Bd<=GYO}r@hH)N+Hkm zAzC9U$5p>Bu&etMMJ3ik9Lr4UK=4%f8+KiJ;=MdiY?t?-BfumG>uG2A1(3`J5vOth zEHg^10Z%7>{?u4=jmbSY^V~rh&W7UBaaH{!o*7LNt;hTaL1P!FV%OPN6GqlLoHjbm z9XO4Za06L<>mjG4VChI2EPPU4)dq^1p~BD0x8KnOHWYK}BovibIeBX=gjj>g)A?ZB z(ZC8q*8qI1Fx495U#+p0F~`BV3J)u1CrDJeUE|o==n}$lT z))AvmGbotZV(YF}_5E3n1d?h@=4BR=#O`(plR5+uy9r?>`hh=6<`oEk%LxUVTVrOV zioTd7O&knBa@Op#FQuwOXguPO*~CQIrwzPLaz@+8)I%Zw>lT{@0q05=1tmUU$4^lWQmsv6B# z%1<)__SeK!V#$l0cKN$ddyF-e4&+uNiGk6_^u=7wDZ%VwAfjLOh;&zwwg?|22 z;_y|n6>qy#+*w5_ZcK&I>fYgvDHlh1Az*~#(U@^t0BuI{JweFr?7q^XT zKTaC!e1NH|xvJRG-=PqtIP-*}7zk!thQ-Z`y<)J+_n@1;th0L;#y1UC2W>P54s;+& zSzxV8JF2+dL)KUmC^@_xh?W3+xXw-khm@|w4yx^2Mkzpkf#$$vRBHEPc_4=eJ~;kF%UW zgdvhr=veX`KguAe)Z?T!wrh+$sbTXg%ry zp2iGMnH4YZv45K6GCYS_(EO3?!SI}7@h6-;M_v3ElU#=92#i1B?CHiY<@_(;?5U<- z1^j}uCkyQ_uz0q>J`R4*QvU#gzig$y0QfH;c=nh6D-h%+YRb+rqjWEwk{#$KTF4DP zEHK68E*zMV5%2uq-SM$TIr>&1JLhE(0RKg>5~mHKyRcJCCtw}-E~gl4XB*r-4^MAp zG=v>WJz0eNK}y0>f?PtFU+qpFvc)-qsf^-;ee;uPfBu+00jP%WmW)^&`Qu{p#U%?r zrOf5Y`(+dmxHZOM#WMqrc)Bf77k)v&L%hURB6ME7x<>2e$FzU(@!)iXflWK{zP(F?L!UEFa!d795B1XlpGHUroLvcEx zTx#Q$eU+VyJ%dv<_D2u&MbPR&O= zpju({l@G~dzh2K}H_vyCyN?9V<&K>@oKyHXh~sfGpykEb#$>k_f;@Psw)v`psUj^j zve_7tg_?shA8(DdVa8qHq~^5|R^HSp+0wCajk_njt0oFPRA_qhUA_}bauL=)~Yk`QYMhC;);*=ev@gQ-0wwvNxkB{i^VgGEX? zci?p={b?Dp{BPT}1mL;)CaVb1;dfkKWy@go7Qkv7RJ-|~Fxp`Q(+;S8pc&8{kk4fd zm&WSo+vcYZxzg%E2>pmY41avaUM4?-$q^nlODf}!IZIt56S;n@UY15`>xsd*VhvG&IMn_B2lfILd=x4 zpbCrED`1Y2&EcC!2crb2-@Kw%G~D^X#9z(I&IpELTA_6fNQo~zbje~DnMEEynQO?!ulY5VxL8XiyFarRtNlLh`eMf^EuR-*@rVeXb{@~rl|nNosxk)> zQ!$+j+ysSyrU=%j%Avm*lvatf;xzRs!fVHX1_t{lY6PPlLY7i0bIeab?Bo zO0&OESszw4n4KpNCO_@-5#>ys!tMyJ%ip!vY!LQ_G6tnsKFUyaR_-XK$zU<}drujY zA_kgI*;?Dzb_mXz=1kR6{%C4d+s4<+KaR0J%YHPQkhzJtgA+vN?Q#35C1_O0J-hi? z1z=1eOFBYNKGw7W_ip%;S(VVKsY?8gxtgw(Xd~)%7HPh^DkO*4c{2Az@Uq+0ogakJ zp@oc7Sh5?zz@E3x4X@meJzOkk>b)8?GoC&}bzc)!ROw2dB7N^3mKojnNc?+qr98yT zTTqCkVJ>N61#69b=8H%Yw|F&S={Ul|D)YIP{Q%nyD@8lvT{Z$6AG7UF?4$P!d#=p1 z8O)lIht5hZMd76rs$xppoM}WvL@tdT7L6uJ8E7aUEA*5u*EDG zePmc+fN@d0Z%JfA^v-3iUsoZ!-@2i|NTa9!^4)`}MQd7bfnd~mDe{GNg8g)gQ|eHR zyy^H4zO)d!#rfLuekem_r5I(K9n`U4_PMzQ3HqcJ!2aoj01cJ(KcV{*-aH}tdzQa9 zk{RCrTde9oqWg0u!k_5=9O&@hp!;*6!k_5=bmRXex+ z|7@>(9K5VbKYK9g7@lpEkAs)F<%?aB;l-}V@M2eFc(yA(Ui>m2es(B49(lR*vyFj{ z;kOgvarXP7_-A-g{4=~L{u!PX|Bu)Fp0)l#ME=qn{s$3Z`u%7Am59_R4a%$upe)={ zSX6kwnctpr`J5~@G)W{6>7)KWL>tY#Ff3NcBs)$kWtHialZ8sOhrTja97ca$pfUe& zZQfNy@+>y3uLC}U*)NSZH^)+LY$~jl?3*4FCu?5u=+&w;^48G1QF_bFULGhX-%A;Z zxG>CQt9=^VV5P~D;ia}be5=I$G4}l8>-p~~G&?EOM)&U)Mgqz%%{S|%0Xs|94?F&^ z1K1YA4A#EA$}$fKWkbQB&JdrgyYOV@skVM_)*pGRG7wpv3ZnG?tws05!mtdGu&l<_y(wF zYN>t-byHBgD@#2-j*em`7SD}J4eHvC#^NAofhIT`R>7D?E%@#4FJm- zIG_C)fF2_D_=4>@J`TTEVj8+`89}d{obJ5>bl~ASQ&wgJ%_hOc7Svn3p5-be4$6uU zb;;eOBv_D}^(UXt(Wee3=f6}Kq&pkak7H|lyIfHEk;b^jTUlCq+{s1Q7}3x^8Z82L zRz^S3g^psZe^Q%au=p4-7@Ac z#I^0bq}>LG0y5Kt22|9(w%}`y{<^eoz^nwGu{XHX`BPWBgU+6MS>%JFsrTc)JBrx@j`U_W_;c*gNTaa+!T_md=hCPO<}%UOR%Js)f$d8+ zUr2SnstxfAGVocfYqf!QzQr%EB3I8wR>WXzoV(Z7LL``upk?*}3a>^KS(j=1&Il(C z6r$4^C^fweuvA;v^L;I5ZoyjNjH_h@*7^D~Buj8Yv|Mk^w>+t%CK(#>{O$cfJ#CV3 z#?<$5^=LTO6)ZqMGS}6ZIRMOGu?gmekzfHiq|$N)!FDYg6p_Pcv%uw&84c8HZc4H2 zDUSU5ATF+?+w$?|Y@q?`aBpS|BkL>+QE%Nv5qx@K_7RON-v;C(PX{X@fP(XNfGq(1 zSRBS!^nx#3QrUjJg!^7fI83IECEJ^vejmwqGF9fg-M1y%IY{>*{Uy7&{`;VyZD_Gr zKaeK9Xs9Aa)5(CgLMriBKoyYgK|q8M6S(I#LGwKnFc?1XG1*O7yn4+J;E7JensI{5 zJT_-A-7V|bSxiD&5YMhObdYQK6zSz!v|4ec?8WO%m-^;192JU?W8bgN6po*%k zm*;%Ll;kxIIa;YE9F>B^v3>>D6~H-mwdb!*3$ovcr_q+fucP-9Ezi7UJI}PDKSqf5 ztY|7oSrD!+0qgzsk5o_N{39{d_A^21sEY3DdE603kdl4HH!R?*Quh(IDA}mwMx=E| z#spy^lFM_oV#65-+w>8BBtg!mF}U62nmOGUk>7>5&$??aqBFZ>vnRN7hvv)p9 zeX0~&t81i+nqi4n3dByFhx(#HDkE;_FZs^O{3z-cSa;3S+fa&ZtmbGm2DW5zX(E-> z3&vrGA#iBZtod;_v#)j>uo&P_8$I)y9CQW=lYO_v096O3J;zuh_^N#Ip*G~u@pZsy z-~BjC5ZA=uh4s*IiSxYn_>Ug;YV8Y$nfowXb|dccn$Q$z(s$umdvTva4V)%V&oz@0 zgW~>)a6IA4Z^FUI`1f+cfB4V-&&vtVy7WH@$BSi6Twl*vo5#`#O7$0&dH=X4&kUef z$ET68w9~fJe>_CPZDC<)_ZTQQe18D^f3z?DYo+0(7qNh`nVr53K8=9c<8$HD*R|Bs$A5ej&#nX~pi<@E zOU}2CEp}tXsFF1C`|Mn-KQQU4(2r$@&aB;r%jx6s+Mbfd^KTLeiI3pM>hE z5IHP$9_TFHV7261uCE3Vhpvw0t4wIhP0T1c$B>^4MTqSDpt`rp4y1dRH8y2wz1`KN z?0u<{HZ)9+yRyh6E0WTswkLUjxaGBW4WQaKW1TXU*gA{W7(cUYOqe~0VDE*GUe~*Q z^v>$8a-24S*Q}LwXKi)Eod)g6cZ}=`o5l{Ax=el@Y3S&KlRw5k561FigxasENc^S|T!K!Kyp}g$szs*$p?%x-DtQEk5NOksMy# zsEo9alVS7`oMHcDza>XCfF*oDsza$YDQ8DzT-Y|=Toh2!l!nYn-&(OSAMEd9^-7bW z>4K3)^T4lQ5-g2QZ##3@40)vQq{N36a>2S`XWl2CoNSa)x5<%+&s&lK2e_O>E zWHZq$QwABxtHOd3#;CWSZ+Is0BM-VovO(G%ZE`tbMqgPb;8tSYNMK}8nr-3R5KtgO zcC#wN2o3MnxAy!ViYCW=!-4rfEOSk8#!A*e$*=XC&{)~2lB8>fCXfD)u8pMyp} zMPSAglOs_z-19c1iiPpx`E@L>-FrOxh*`vAqVECfl&2)B>oyTWeq6(+y#Qmf8Q&(z z)99{9eI^^HXeFrLHUIEAs%F5?OKy|2={EV~%H(J|6m9%D;pQ$G1`_3{jjR#Hd^znc zlXX5Ss34x4&zTLvGN}T@>IAjBG8a`pQ8uVra9$0swpjyZ9HX_-| zlQGTdh=Esc6X&b5OPFAVKm(5()fH{kh*ka+G%vReBdl)%;4v!}g2s-tGS}E}wlCL! z&xB6sZ2lhI#2E(H>Y|yb={h|(`V*kH0G~n{NQL>6>x<<`+_~5H7S1t#RlIGlGLP=L zfZpJl1ziq2{}R`<&cuBHGlUUf*Zq9ya{%NG60YjRmDii=78ccY=Btm3JRl|62k`K| zBdcB<_W1VM+NKuXeU54X-ky02jBL&01zMB2*iXFsBHKFY9l)k;UhwAWlcy^=OIm)d zTfUN42zQ^@eB9{E6dZ&Yxi4m*e z>Q#%Iul4p+)5R}l=Oz5WW`>}zZToA>v^_6=PK2Z?u}ww_hHqI@=vC|xkDiqLWaa7a zur4IHSfyYmZw+l&$O7$?j;SB@GgDuK=#vi2bHg~fBs`R^2c2Ovh!10dUT-`1+2W}! zvj)-IWCOzTSIZ4E6WCtOC3SF^;RLIsGxQ22??{85Y3xCi@4ETvK#WyFxc5H1lrS=u z0FOk~V-&6%ozve;*COIa$T0+%fXa45>(=?+ml_ND11A)f>Bn@9atjN2^S;%w010t_ z#D~(y?SLA2zVFifT?uo%ZY-O)qG?zf-`>A;8*!8f z3!h3IEvy&FPH(O~PYjqM1(jZPf~>Psb6Na6P6P#OZcC#tjMzEWCiJytp7BOg-*KCM z+?YqS|HgvL1(_TLg|Lp4W#VfprJ?)>6Mng(8V-2@ zy7B1}_L7fe5SWjjI8#zFdAPTx&*i*N;n*cS`-(bG*NA9-Ax3>-GId%yDjDx^+aiDf z5*aX`70oK*j)uvD+-tWlm6MShkR9N@&n|2lztnjkXQ=>>qe8< zqA8-AJ)Lv0p@_ZxJDa!cqnQZPZ;H^tBoiEv1Gz=3cRyb#=?FzHXjyDj8;4-dT38>i zK@5=3iaUbbU?oC@;RT0_^s&?+U<7ClqUPZ>V5^tYvOQ)m+t(`0fcBsY4B|S>ZDp=i z^sUyVv5#kUM4Ibol`VkNC3D+rYkX#NKf&xwr-fnYe-#C3nf^J1aega`-^jd>>6No} zrfrt-<*whO9(Nnu*gp`%f`=uBU#zvF9$JnmvvtXSE`aUczSlx?ZID%dxa1&cU%w2Q zC@L3dKg!Kh9sWJ|>4(6$oZbqOE&5{lQ;=aGILc8p*|nlby|la_GG)$=l0CNXJWCkY z+y*`f;wDa7VehNj3V~geQL;wv>6-3AbU3~JYsX8f3_pO4F?0&xVUS?$yR@H1jxH{; zeo{@;)NS-`59+HpR4o_wVI|*9%rd5$C|6x7rQv8n#+V|xkB57el6!l8=D}_}SKY5P zdNrPlaW~_U2fBF@SzU$(ngXT6;SzLlLX;1A&v10F*;{(e`A;p*2mpscUACEk*mjHe%525G)FUmsz?X!LW@S;2fyeJO=&&tEUSrp(&dHC`y zURV_1S%&zp%OtVCQr4M~+@}@oHjHJ0w7k(hQNe^wwQ)Q4NN07?=}VyE)+9~JLAA-~+23qy*|*4)LU2&4cpfmTlf8VwHmOt&1 zWOjsRk#i`Z{Mh7b`0A!JMirMfX#afWLYSleuHnl~$v{nB`k8V$1C|G#O_5@gN^<96 zQ%Jh3#fOnq-tZ-696=_Hur;i*JRMXzD^%isy^_WG2*NIxyXGl-pU97TH#?*q(;IRn zJUv67gxwZP@q(Pa(dQR6Xe?dw<-;~s9igimZ!%B5tSCVAeBw*99`s_jr0y8m7gtCI z+olHC$3v?>yz1#K?>EYh1Z77woDVLr$Kr1%=vU4h1RLKE1#=$=Cf952R&qqvOR~eh zp@fl&=vn``aEBqVHiaOtwkIr=X#|l7@@|28inlDt^$TH5yko5kAkf}JcFNCWs5~Fy zJ$D8vq;dx`xTU2ixm+W+a}v{(+ehKu8mYvHnD;T5rJ z9Ha@N4Z?SGAho%pN!}V$BiRqqveLmIOmE%^tIGRyL#5L==PC%n ze}>B_)JZl?xs&VjXyZd*b803UfNnrjDC2(wPth**U-mgRlcVMIw&1YO=vH zV5~l~8Pe$ngEbZ(#vos@9h@68L%rgm33Wr`n*chc(Ki4JNKH0T%uljVvA5nfHz~02C+toif}(!b zKyvJJ1hUwL+acCT5079Q9XV`y31!IVs3I(8_7oxIb9}$}VI%{=v;H9x?wlQBq4`z$ zC@@9u=_6~P3?}-R%(Z@mcT*Aj(+JBQP9oDYTh(jO)mHEu+IC&73r{Md(=lqn=g$7S zu>w<8NTukATrTefTbw|=hAt6RZ3Hu8mk18$?$QkP%nv@1V4hNG&QruR zJ2tAv~|K^xBPcY=tx+GRWq1a zC8g2K&;@-(!`DsXU(J@3iBcYW`pj;2a4Y9HAFl01qPSgL5CHX7w}W1nmg?$xE4MD| z80kuuNMA1tcS*U-GaewG^Xv%^+-kS|>=clYzCT37mE?@>Yz-HyP5PELERqer1r~S9 zjDvjwN)*dnuq7?cDW+m*5_ot`8ArXV8E-t*0jtyIIlnOGP!@XBhncp4l?o7}NJ z0@>49+<+ntOclaV%k0C(?rAnZa0y|nrCxBDud0dZaNHa{>sDGzR7*=2d^v;2xpF0v3f52)3@$PT}5Iip$8$&Lu>50G&Ghcd$%;@MQr!X zdP!;lx8(-Ng9hPi84BoBHYwaSc1fLT`^iN91Eu6UT7Y{Y7%g${p5d=D58G0uju*2V;L_TRf!n|R6 z0~4bf$;l``k|%YLB{|MF_6hC0fs_l5p!0Jt6$&s%3^_H;!XAWwYE%o-uE6{P_2SoI z1!B;au>HC6AnIvK8pbbT-U;4gy5Z>I)fYEX7N@IFKviM22;?E`;`Vf5qEJLG7``|g zqkUG6Y!9i6B(r87ChDzv8YtnPuei(YGpm9LaNusm;hH@`+1EQsvK7-+f7V*f8nK>yx9FRM%9sIq1!;e1UZbweX^*!bHJao3DqK(t zWWdwEF}l&2${yb3lM+>}n9=mNHH5hJfM|Yc<}4U!iqv9ujvsc9pyE}cYCM%7t_eRI zQ#}&0c|#Vvfz@hDkc|TKRS)DSy%Ge`@oa3Hc%d|sRry_G+q`G6t{GtVmT+Whv6*Yd zVHbr{vFL~ns1p&^bT8->M<9;z>;qX{n;b)Ip2K9f(uw=YUVV!?XkJO{hhk zsL3AKSIA;s+~r&>3K2^3g$mm62}_tT2?`xXd?Iz{K7PH87{BIyB2i9({7KJ87!Zjj zL?mE}Q}zXKpA=~bCz&sH?#~aNhnIw*-CI(8JYafyIQPAFq8b0iLK$>y;#b29h6DtOc3dx37s!S#LL(VPj2 zyYQiNrazU;+I}aoFk@p-HQF!>qc(Guugx0pnU$kGr)>qxtGiWso4v`SvqG;Jj)>j> zgr3|ap&%a&G`ZbUS~ZUwO7*P>5c%1}(y4+oZP^a?)KCC1|l>G54(bo+qU~{@Y*-tY-Pbw1pDCAhchtB$o)ze z#3q(66S*QXU*nkdXnH8vlbEfUt9F%C2%vj11mI!%K|yUZ-RKb(q+F2F<|s!fs)5!x z?XTKrCiF8=)=YHxOT5sV4bf**B6lGS+rPU@Vasg45sOgC3z~WDj_y4C7Q9v@R7|k? zbtHpt`-v};4XsN+uNyTswpNl7Z+*KM{4!WUinwTsZX(1x9ro5dUt@no*XAMe;)02F zO*z8`xZ&C^O~i0qPyjzVdciQU0X<&AoQiCT)gGQJNHzGT3J)jM;upvz%Gf)2xLP{7 zJMp;#XmBLB`#$ZoYlZy5HPZ>=LKFb&t=HRrg(4+Pis@rc!}KZi^g~n$zNn=JC$C<* zr9DGNrc(ovQOh5naDE7N?{9usTrq}O@2IF%7z=+rR@su0`TG zc41DW)IcK|`k? z0|~Et?S@42H`pXT36 zL#kaN;}h>V?~d)ImtxP<*pba8AR^m=DTGeEQwcX)vivbm6ga!6TD&x5Vx4y0Oo2Zy zIg4Cd?QuUyp2EqAt>`iF4*Fwn`rO2;5F;BpW@*NtEQrwoge+24Rx0ex&x*;6L4F2> zSl?A1C2(l>txfI7RKt(ThiU)jxUe2R9r*<^HnYnBGhJ;xv8c`;m=PbaeaKG5-<`eOKGa(kziV!{fG z!wSz!lu(Bm^uCu6_L@!;-oByaf#8Xa?)5@Um@DFY>|=-3Ok}F6+#!UtCe^HmzxaV} zrWQ`#7YLkyu?}c0(5kE>P&{--(kHtbE1Sar1(*omLEA-NTRg2H1_^2k`@mXeK7-xi zDm@9sA6Dv}H!jy?x*;N@x4Ee6Y0&nv*n3vfk@*k|1pcp9rWR%Sc-qyI{IG}gmluNC zNi2oC-G}e4A=FkYoZoCuSR_bUA@sMD3yW zC@f1rmiX$4G@sZ|C>|v107r_3jF^9hq1ScYx@IXD-x@4403qphi(j-RO5siQ0Ztdh zKIWZout%uFH@qhQl%iMmoYLEHHt8)b$P~zTlkT-|g>$$NVFmFmVD|9%4VKaPk?ayX z>x;1J7s?Ft{Mo8VQ>vRXw+Gmx@r8(jLBxS~Ptip_giHI7&@IJz)Rx=^#7o3XNDvXw zm?_-Ro}XG~IHnsEcf`Qz-KgnAWwiJSaLrk&APSG)_((myU&=nd!eS z=$I}w*UC$^&7^DcaHA;BkL|ZkslaRWO}2JC;qtMT7S4eIC~dKH7}K~5Elq`Gk2V+a z%E$DqFOnK=iqZRv21Em?SHYghPCFl$_rv4wUc*_)*GqNNCqsi0*924BX|ZksF5@CTaH?%E z6GYu%U}$RTOSe#1L7bkGU;DCgJh@TTv=*w}1=%ak9lR)ZExwY^hZD1K=!)H2kLi08 zPN#Os+8hpvD+uMT)YFSbS88wkfw$58BEy@*N=&#~Zzt5msy;*4!_(X&?-j{7fs^NH zCywmskMcGU-h5Pd$0OmD2NtSeGS*Uv$>ST2js&h`)Kv`LlJ$9xN=^KTG531M^+44*<K=Tv)sVkMApFH4s@QV zolK?_x+kbZN_`hcizC7^wW8P^%Cd)t@H4|=KSuilXhtB!r_vDW5VW>i>if^bF-E1j zk#uoHM-g8awGSNS7uxo>*#dLw;;=mvfy`uPpVjArLuW<^LaYCBWe(_D1>R3QAi>vp=$sU zzIQ+&;0^D(-*C}8SI@7&FYCE}R$Tn@vDMLCP4PW8a7&Ie34Sb}F;(yWPkCiyF^kOO z*cq^6vAyQ2>Gdi(ZG0UG(rKEv_ofmYR2Jx;#i??>pC3XHb(SVVS9wgUf?E-PKKn1Y0e5b@pW>NK3x zi%S^{9W*jwt4?Np$eX>PEvvpuLnH)(71H6i`fAw9!6DifbzHupOfn)7VRQ%im&FlB zjkFG@wY0-Ya_5`Fn2Ip@QH7A!-C^HzFd3~J7z9!3I23I{GbP_%zZx`GnLJ{(jn`nT z!gU^!+LZ@fT#bA$p}vsd8P)5`B@ruLqy@XTTbUc5=9Qei9&jQO>N@^BaVTNsWs!Gr z5s8vwD|Z4ZG=5(PHD>X{7_Z=!Pwa=nS3l}2^WA@17}^#|r*HVer2P1ih+E6XHp#3Z z)~aP?vutC-7T|6=ia|17WjW43{e>MX=4qi51U%Ds)o<;wUr=E@5fd8+HBH*vyNWZ?!U$60(CP)AJ z8h_k@ofScQ%&7lyA}hm>qCQ4->U&JgQNhq9#bRhe4<9zk6(BHdER1lHe#%*;!s9^c zOJ5K57HWTR*0j7u_2XCcn`E<}*A0hSAAGG=lWXiH`t;8udEs%!~7 ze8a`s?f51B~oA%fNA>^(KT;N7G;lVa{mW&u0shAWfl)@pFLQwOhNubWV z?bAR7N=xrAN$Sn3$>!Zo(g-W6cz7DqK+09@=%TXrYg(B(a;{NSnr;ZiEBp}9xy1;Z z;A8rLOtD<{2P?SU9$N$@E-LnOH_jQ@pv9K#!Kj#;?$MrA{k3Q{N28fIT^4EP3_pcr z@;*`9wZrBc-a*?{#zXQG2PwlweMn0PyLyna>^H}{Cqy=%oK5hrQVk~^x4F3`BK=S0 z)`MoP-MI)$GGN)HTJllIlCRJ{v%Amv29s-OIytkPw!bx3dOhL9vG~G5*jI= z@-lH$J?wh=>gv8h=g+-)^W6)zM|#qPCkN63tGHDNWrIR~pt_e1tPV>6hXJUS`2P`imO*(Y zTh~T{ySpT~OP&WOSkU0^uE8xh!95V%-Q8V+ySuv+LU4zV$-tbPdZ%j6`Tm@LcTrSV z7gg+Dy?b4It#9Y0-Smu+?wHUaBE3=j<{}Lf**V%QC-ZZ|07FKSIufeT+NFd%PPAo5 ztz}|(;Wz=(7&njm4N1sY?>mQYGUfWo`OTYq5tfdMNy(^;Yix?U4t!P+RbL@y12OPpWiuu} z|D^RsR01!hxX=!0rrjT%Z%W#M7=Ycl!YPD>yJDrLm9_Bj>G67`#B~!#eoS1pp7lN5}IrZk*t~Z#UHXRF3@Ed8w z^Xfskf0UtUug|27C2za-RMz)LMql^&y|Y0`KxkDgb{uIx5d3<;_&rXA2qX0r=;e;u z7ql1k&N}|I!y^Yu5~&|~A5F=MbsQ`;4TMXerHI$|VjH_t`MNzc|6KG?S;l~I9>y72 ztV5-(HVqxW#HN$k0QH-U#{;M(1T5l+avUL|%Cwxw2vYe^UjkG{un?XXgJdrnyW2{NZ{<&E!oE(^tW_ z<34=|tjltlxkCQ#4?bQjA?bnZKxUx53-+dt->r^6q$`ySZr@JPD^sBZ_KCAjMLa)B ziz<<_6zx-}o^JZ3e z;iMJYZ3Ph@K=d*MX_1BHwOP)_^PfHDm^bk2K!H|0O^rRnxUrx|Yx_2l+AGgo*CI&# zxowOORGH+Mq`s^58VfPkO)?WCSaUR8z;Hmtx?Us7FwU%iNptRAu;*aOsOuAcA>so6 z&!H%j?ZBx>Q?|}-47)rTQ!D0pJ-ew1xZ5er4uLp{iwDC`AH;1dmrd|JQD}#O-7c?L zntGUqku0$7(n27>Of1M)drSg|3KLR;*^SjC#BGD>yd zy%BYs^>GKNW=YB@P0sEr$Yb{6#Yqh_@PVkHYix*qw&>1^>-?8aXN*G0X#z1IRxuvf zGiKi~*~-6Ysl{q?vB5LljgCxpiIM6(-6RIVFu+jTL!scb|rH zmOEJ?+u6BxRfkp1ZSK2^eOb#^+^X?k3k3(Yp2tYd$lS)Zi(3%am2y8%&~^C0glL;! z;^%$5cd&w1vx)BiXfh5mTUAPg9-2Gl&6b#>cgi+qvp=x$sP8T8;V5sI_yw4TaJ{N; z>jdiCDbUOC2(HKGFZp+3@RtVpLku!AG5?wr_)8c9{xP)?@Dj)P+l&OjGbs4`<^Clp z`oFl`KZhdze!2fEH1U5W27m48k3Y>HV(^821ia9XfEW4^@IpTVUg$@_3;hUqrXPR1 z-2at%^lz&08HM~uUtYD!(^!8YUe_Py|2_ zJZi2l_qx2ncs+iS$w$9eZU1f|B_%a-gLf~vBGM7Nm#~TLB`f+_T3m@0CzqH+)71b} zqq&2p=q>JyH)#&rpqRC?NyQce3F@(k^0w5ex;OLUFQ)y zNvzHL4-T3uD{4tFOZF6p6of~Xy_h+9MDlnu&Wf9hIyuB@f=v4g6jKHAO-*025=njI4 zc%Kd5comh!;IY^EeQv~hoo+!V;gF-$`s+;}JIFm=-=x{VwZCz=etk&JnWx0wcUy9~ zhI8A`$u|uC@<|dVdrJ{O?yot#pKS(N1gs2|oaM)w%BW(_|jqhg8ttS$qh?xQ!CRoJQ7lV&3jo=riNit9N+}+o(cZu9*i~;n7EcAoS#fdOd z4TH0!tSW(eU#7KDoGq9F?dU8__DJ)B=Lt3XoxWofBz>d@^Sp)QS) z6j)RF$HuiY_eV<($`}fh-Y(!52?S%=WA__lbRST0zbQV_PdTcrpc;xiFWHy6+NV(yQB% zOx&ujLN_IbM`g}VrYzRc{?5*YSJhr(mvZu&-Zv6N)MKC72yp6P9PXr22fM8%L=-6j z@A{ZDA~seC%BE=BfiBK^I8ovJb#RQOD}GBFr_u#h0dF7Le;zweN~ca7z7r(Gz3Fs< z8<((2onU{SlV7%#uF+dWm0xsOi&^8tXVz>NUZ;z>#&34K@?LI`iDDn1chp;G8Xiw- z(Rgiw=UiWjl&JVA+`e;5io?JZ+|}f={>r2YYiEn+aRF&kwH0omX}t&ntsj)grp%M2mD1tepqG=!ezpA&RysIXt_u zg|2v6UdU8fmWGmJ`dqZ<$75LUhzy6%$Ymn#qNWzO2o^Q)pau9_-F6n7$;&d;dVr6< z`X4B{f1b8vZ^Cxs2xR7MU3|C2r_M=f9b;VkCT!&zrBd+$yzT%4K<0}0x!ZiVgPov)MBreFl8=vxdC+ughCYYz7<>1RTPE{k* zSfkpTandY_C@r^xoEehus;m0AqCXEA)WE$V5c1YVEIzS>Rx_Eb68@i*Q!3YOQTXLS#v(o(5Tv})wTIc&%Gwu=tgK*jutMqexGop+}>CRGcwof`iQ z3Ot5+He06!3ZrT6WN%$b_2ZsZKUpJ9fnwUQ%9;ki>s?b}eJxLKl!bw`IIaDB=z@n8 zEj)ta%ogZ=H(a{|wMILt`0TWYOZWT2?P;hg_-J~Cd?fdv@)Fn2>bXas*2IMBQv0GP zIKpY=yiIDmY0o(1XSmNXq==Om!Lv;sNk1dQ#>TWk65c8`0cG3aQM=lgqNps3!$RW} z(l7n7FQ@&!jLA-N?oMKf$Wb^O&wEKi6MRW^a9HVOS)NYA8p;s1pibUJ%0^wD6)x3O z;20xHTcSjaWKp#!oXe}!mMtI3k&bc**~&7v>3}?afX>9|{iL+tnKX8YCIPZB<3ah}d2qJrZQD{Ckrv-JOCGFQA7G-J|E8U-l$dUse zt#^dP+%;r`E7VWIQH|)}0=l|YGr&gVH=}UrNEDqa6Dvi82*PlufNoJ)>f&pS57{R+ zy(|>CLlLiM^Pn2}UD+?a)#{pL7qeEatvhVyM2T@qhw7C=IHw5>ll=irJ6|=%{>cJ1 zv@BA#UYyMti^ZC1gFRi#V(%{8&vx@n_D>=f{CiH(0}Lik&S_B>wUcZK$x2KnE#>z-#b{I1Y`E&RtW|5pm_ z*PdP!+RMxc!1K%qCctxa)YHfFq(i{-m)?FG9kQBtAH1374Sl> z0$!+9z%#Y_^vf?RKNBoWfS0%iz@M1Q(}K^W%G1Y-7xIM&0X!2Szv;ujhIId%K75(B z{ZC!PXk`tD`Ce4l9i_8lIUB@yk<25vj@dv07Xr+vgQdnfC>+Sl>aVlm`aQP?mS5gM zW0I_P?`UuGj)>da+n?VFd>iA@U+saxKQWh-9p%nGvrb9`jPw<`pRl{#h464~{~6lA z&@$wDk_1vJ3m>C^9r<(a^eY&q{K$T-QN)hQ;3j&>o*s5V+*-u)m<#M<&Vcl#(~SNi ztXJEDr8v)*z70gnZ+R>h#)<%mRQ4n;j4lf$0zjecIfFV3u91mmFQsIcmr$Yv>PY6B zVcCSBdrLzjf5#VvT8# z!i6JJ>7!@EHRuAwQhetn8nDObc9=B-Uf~Ux#EUk>TBGEKUG0`vO9VU3>bY!V?%fOy z9h~o4$R`t+_U?DG4|O(iI47a9#HS+UYlyxs8YtL$ecb!GTGy!g^w_7aacG=3m2APT zfOhYV_Prmz+rh%3-lxhYKXTZeC1j3-*_0u$(aj-9+ywXq2Gd@%xIrj(PJefArhD9G zFIvA5Rs741x|9)khMyN3lkdyTkPT=nuOq(-G~~rI>ztjdh=h}-_2s#$2Umf(jU2sG z5RY%p5j@*yO(+={B(}PoS)6+?BiTN#$oIR$!hOww%>>8LdoHEzxAityy=bx+RN)(9 z?RQMIOn;o1BgRxy0rgC|pE5`ctsoR<-nQ8Pg~? z0c*Aa7N9R*2DIzYDf~REp1=+IlPh&oDJD`B*`cCa{6J+^F^8_itp~exI5uG)O9VQ)kXH%PX$Sd zrb4WOxM8Ry@@P8%KKD1zar4PPY`SI#@o1Z8-LKbzD}$7>dZ!#w=0;-lHx^|8Gp4!W znO!i7XZYRgz9^2p4Laec-H>NbaskvJu~MNfrSdLZDkI~|cvzai?^G!|r$pP(?>c)* z1_t?zw$HdFYVNG8q=J0XvrPbU`YL0k1TeZ^PCwC5<&VGN8IOW9OFu8J~(4=^8^@?!^ z=Ol*yV`@>+-udPn_g89h*qR*z#o;mC*JG%Z=sd8rUkxXrUklR&uDs;~N!GEsf z_PDEK>X41ZoK<@I#>Hc5E<0+wO0iXC^(=WBsJ>N7KZX*yD>_-=HocQ0{k58?;m$K( z1SkD2)?HSm1!mrJUpifEl58OHM-FZaUQxrQjJn_d=%Pr5g zR_sDmemHtWd-TPES7%Ji+5<>B0}|WL&EM_8x73Vi!ns=CKVS)Mm|a$AJF~;Z8t}*N z+0s_Z4`~+UXymB%!vQgf!Afn1n)&_uLQ#o-a!va8rjOSsrr zM?uQ$X&{BTOY7jo6bf|5GlAZ4u z82s5E0J*HC)b|uzhK8R2!3OC#VU48c7HSqX0oN*4(X9#CdLi)TgKR!0QPplRMuC+d?{MjT>o& z(7D)mizfMJOGadV6oUh3yf|;1=47*uNzB+%czXIcBEQ}+IM!id3*xRI4kEP%%1l>> zW+CT1kh^!=D+lSj4Jef4j17c74#Qw6CI7I|mY6V1u_g6Y3p90!%#ch?O0i;SML?|l zPMEkwUm)b@g=S3s4Y5!!w119?&YW6{=vBN~DHjrQ4CU#{0mwAI-21Ao>sp1A2uJWYtS0)5b8JZ^kvDU-2R z!fE`_bCL;FWBBo$C#ZD_F)WIsbFvjvF=YNA?F9B@V1@DLxDS>SGTC>OZWT;n9qIQb zV8L8;JfDz{5JabPv~~!DOey?h#Dl%A4aF7 zMiM(nLovsDBJT@1p^28Fy-9k^u9 z?Af1VE$U{0#cQi0;KX4T8kjBu6@k+z%5!BoKg}F zbEW}6fTTvQ0n7*Y&-N}mOT@b%B|d$mGOZ-2>Yq`S&6V#wb_?l#Gv)CP%~Bz0&}H+E8zwY%>4VV961#f2uZY4 zPhYd!gm_zYs2m9xpvL{=!2RawAT#NpVPxMcMTl{wuRJ!`wkdw$n~GcuRUw2bAz3t3 z5VRX7uc@_hbCLMeFWr0gtcHzi7KAiU?J9+1(8uI_AcPQaxf-kB`Djlqu_9Dr%m`rG zV@Y}FgC(f*a(i-h(0r76;%^K*4ZX12fWs%0n4_{7j#`P_`?=lnlj@xTklV|`G|7n> zz3w<)9FzG`nYu+1N`=|gps=rZXI*EYJJU_~$(iyOn3o2X5pr#1Wc4vVCNo9HjOFEp z@kDGJ@3Ei*19X zLJh7r>V*ki*)83XbpKwkvn;Gu2cPStYS`<)U>-lo41Rqd{n{6iBbK3~dw4o+!463n zH&jA>W>2{FF&C_(ohi3=Jg@?hV)*=g@-o*8t~;y^Q%ztuDScFZMh3-#W$f+1;~H^3 zh$sTAD>K6S>Jb5~hv3)A-FI7{9g_hT91Q>%oW^Yk$Jb3-a<;eo(NI zukf1+#WGt!D-S|dcUUIw%F@1;LuSru?^^nJpX{j*8w>QNFV{oK#O5;mi z&csOY+u;w8HDD*5x0_TpKic(qhNB3P5XlObVibf;I%AIxu4(E`tqO+oeECt%B;_#L z47b23s1$YRZsbtS5(4H(2}9He1TTa~>nz8Mb>iMn46_|%0+yaSa)-`gW5bbv$Nu%98~GPGD&etYd$9(PZFH2_xz?u#7U`CfXJ zWk@BQx~zSsG?L#ljB01L687o}WZ{;p3aHMl{_zrRR%gFzYP|Z0fs|C6P#-5IqX%XG zqeAWN`KnoBmsxOuXwaL`?=Boj?<)M8;7^j(-CeHaDgjZ(pJD+`jj9qcpT!x|t2>+9 zxTeZe(TH#@#{EE}m%1nY96B1IhEGz#zI^DOVD35Gwcp7*JE#wASb1zfAd;0{v$6^j zKv0qR@Wd5H0av*Io4c)>^pH66E^8mgS|6PAF*4O)EDM$9N7uBft3X`mP$G0pgA7^{h zF9k1=X)aG%Z#Yd_l|*w=`l40e#foAMp68=ir~VPHOwQlV_lJII_ng= zxY>~N1wMG1rm-iyo@wNC>Th({pNH}vNA;8Ldi%#O{Q3s|0!d#^@c&eI0bhumf7#jo zlmh=1n+tqqQh(E3e=hv8o&SM7`OiK5v4N)-OyEoW2=Eyvdiu%pShgoG!k@uqz!z)> z_=4>KU$7nEpV-cm?s~y?fG_bQz!z)>_?Lnia3cJaw7G}`b}j*!naJyc~1y$ zt&;6YT<-FKk$>a{cE|xhYs~nM$?dY7^goEwMh`cxq{hPXLk>go6NwyH)Rb2utp-Q>kZjMLMNqPPnq5Sh9kxG3hh; zeY(iubWegcXl-C4D5%SZ@_RFPe8#)#ee2us&C1?cC2ya!PHia3m`!}&I$`Ry{>Y<& zz5o*U)D=AmemxXp)^CivRo@F69lcgR1sq;K>|)=CCfy+^l;wWr12IX&0&={cfi9EU*_-;6^}ZF?Def8AJ=h*EJ(c z|BayNYu-mtGO7}*m|-muQHh}rfH@VBFhbRefc?Q;mR=>;JC4ql^J!;-kbK{;oCvvsHvrRBoXhoF9#%06n1LP1Gf+w& zC&GC(6)kWIi`B`XH6+YO%`z7&pfmI|MDSYRU>qVYwJtMhPV=+Nf7F%03^06c3b( z-pjI>?BQ%aKqZ&-pRGhv=a>zO9d7txwf%6=QM9x5A@;?2S04YiniahN7c1;4}+KJO2}f_V=|_&tWI^4vF^ zh@SOWOC?H#)rLuOx3-I4KhrwGGX3CkFEZdZyS=LX){nQDZb5sOg*rxhup&;BLc|v0e3# z;UEczK*~B^GZ>l>4zYyAA?&8=3bXb-g)0EevJ`q=fjX#T21{qpu;OUzwwLL%P4`VD zj1hL4s(vAHkkRaw*<3@6gw}UJ={m9z!&eRTlBcQtkWT8amYBWl1^6)Z1JS0P9w!Ac z(%lbh!ydD1{4`b9AC#u6Ei`5n3}0a&hN`heD?lln_YE1v_6HGC&6SOFrJs5j`q~4) zWzo<QdAHF-tO@y2<-$(SqhvI}uv(9KjNbWe~ z;CAn@vd02hhM3bbVa{qCV6sXGGV)~SYbWPR5`@vbcC-%;ZtkrRqJN>M|du^g(fvaGDbW$CU zQBweKV-=#=7Pe|sJd^DysHx20_&S6fhxW_eTo-shlLncwF9qty196A%ZkD^Rj`{B9 z-^bhV)#K8+p;$1hzirC&#ck*o*H}_5?LN?Rt4{W69Zjsa>zf?kj>H}J_ zZ^v^J#exiM5>A@(Izp*5eyw}ryJh9R>J`1Zqk1YPI1_5ScN&IKiXp@y4i zMeF26tQOo)S7V+{1q?_+HF`z*t=cB*W`6KF?<`zQ`gmUYYV|~QSQXN9+Qu|0Nhx@$ z);iAc-gyLlq+Q?nt(0!AQeRqn?E%7%2_J1*B79x0xr3rx#l%a;9kE0{pJP~3>u+fc z$7;iIr^x4Nm2)8UTzus;UcSSx%coRWU*`zT#z8cx=+j&)saR?ddEI=lg-EO}MegB( zS{#dP@EQD`>ovYIy5=2x=LxPDLhS*7X4Lx=vsE^MvX%SJw-4!sU?nkT;RQZ(g?Q%Q zCaB@+ID1>hQr^dBu30y_JsvO!-a&7KEMyWJwl-aFVNt4UCk=4 zN(J^)D$U0{631PMO#)6RPFA4nq!m~)oDr4kZu~b*Y;Y@01Y;}?R}ghijz|1g9-&4; z`t|-!B>u&)F*CCN%jO3BM~3b19#Mh+mtp%;+x&MT@&7Vxe=hvL6p6nxY`_t4Kv|MXItt)d~f@FXoSOLy0))|5{KZUmx0|!QZHjv#p1xN9 z!-Y*4NwM*xs4@REMk@N=BA%eQcS#$*3Vby~_^5sln=sqqrngr`o0~0&dZ_M~`l9bn z4^q#SW=LnMx=%{)tGvs9=`ZyEJYfVsv4I+%Hq@Ch>oWxjp}%8#ARs-u2(!iWg83 z&}Cft(?&kBAf1vqM+ISCxizS35^*e4#RVIg9WB0DT6DU+22icv@fGxBQdJ)VLfz^& z>Ozo3(nA}u6pRG4jAFm^Nwd@q#2=mYfPjSu&9qKgsU%KEZs~{dg!*Hdtt_~JspoZ1 zV)5%G7-bJ`ifzep7|GqJV;oQpWhlIoFa6oxLmdycZ-Q`5m6;|cN4%{2GodE*fT`nr zX{P5B2)AkhB@K(HjPZ}nOS^JMtFMg7onC-ZJEqi;zNdlHp4+|Fp&jBx(#`sAJG8DY zWCL3DH!@9RPB|lHHptbi_PoV{i+Pck4R-Y#%#8I6ol*StbX(h)mj{NHrSZ zmA*P8E~iss1vX+@2|;W|c9>)zHS)n22sMl-hcmJcCLlDh#>B>*q8IzntJOk4KH?14 zAZ;GKQ(`E1l*jCYjlRP4p|fCI3!g};jIE8-=F2HViuT{KGGqg1l1}Fg|1Q59ykACK z+=A`cNH6onl2s&C8`gsP_zd2qAb1@#*gj){k(MfW}+ltM`IPL^NTrT1b)I0GEu3`ucU4aW?`nujM_FyFdHBx z1XZnrM;(n^L=^TC<4&V1>@klom9Ns^8K>3QYISaQl4FNW$7|6@o${|}S=EV>*yaG| z%dsQ3RKUm=HisC*-4iYKEwAswH$^syG|du-3p=@|ZIlCX0{t4c{W;wqqtBdrPZm?U zfmChk{$slS>q_NN-jE&ZLvUn=rXiwOILE}#t&mjLkg&EZV}z|GQhN|s4jj-w=mnbj zQrMdWFt^Kclc7fpT6h9zFWJ5>o!>xm2o2an+>a-d_Ohz+s<0kZs+(uX(!5`)@=wR0 z=ddjW*w4D)Mx!VlytG{LssRpr{xY`pVOqvBZ<=P!BQ7G zaoOZ}$_(113MFeUJT7*n0JPIxRBc6X$dzDn(re)%+C8c~6VLN+i?cp)OCGR6`h&eT z>*gC;#`f0;E7LGUY{JX>n9mJ}&?c>R6(_5?Oo$?gap*Xc5YC~C+k^KR@B9I|d#aHG zN3~(@Vbm{v=E?d&AQ@+Cp+quWSa=^^?#M#E|LuO#LgC~bAZ}HPB zDp-MBKG@)8_Rs%pUElwrHx@nfOqioTIda+zPo@}<@(71 zoTTvkUb|@52?pUkmi{th&`g{=Kq&W(_ne=U)S33ELDoN9-(=^{uD$bzB?3asfLu-!Ja0}jV7 zujE#CwF|*L%R7&dG9ZfPs^1)$XU!Diqp1nZ*j+gEuBqnKwxT|;iOmHIg{h?Z_z3Qo zj7B3r&faPrzQ=SnGQzzZ!BmyUz5naJdo#ML}k;#bcHlh@AGATAWyREBStSB~TRvlYo!rL!qLPTkeTH_%n5H+-N#LJ(! zUBd^OI1nFi8`@L)6Qq>)*vb`(E73cK(F<%aP5*0DYeJ@YYf~FuB`Usuo%Y1d=3FQE z+LhyU#anZ2?A68@l&en5?Q;c2& zI9@CNTB7N|2@({X(|``XNy<-n)sqqGPu*i!6Y%?R@lBV=cnpYWtV-w$5#!aH>hRxgBw(xCb>sYhnE+A_rQh< zp{`3uU7PxqyBGl$Dv<+8{%MtTd_u8K!P%qk+hFLy^5S{}XSvghCb^5LZJ$p;75kiqU zSICc2uiM6-w=)kv_u##LLSwWTPdplj(1|wF3V5<)R7vNGtmW02d#i2y*qI^_t5TNP zHc^>g4jNZ4Fqm>=NKPB9-st~f5ug`ZL%=utG>Jb%x(Ra?TBqbg+|jLhy)bt;OG&DU zN{l77)p)}ibly;)d9&>mA_t*L+tGrS4i6iJr(od5)sOT5PDz5k43Xe?Pyu!9-VOde z*hzG1^#*KE{&-q@)nc8|M!o~x#hZ)xq;i@%u-nC+oC-7)|Hd@PMT%(Wn1%~d)?s3~ zSm>3{robb!?;f8m$oTQnw-9ehZB2@gAx#y|8K)hDx$ML~Q@<}Iro7C|RpZAKj3BmmFl1ir} zD$_kI*Z~bWq_QM4rB1J0#Dtd(JYD#hqcC%tI*vr0I_{5vU73hNoTOsKr@O4Z?55RI zz%B|FmHMGnP~~YM_{2u8eTfJABqug-V!DRtG}a{UCQ=xVI+bi?%}noxhYOfFJ7)v`%NwjZDj8c?$uHWGjkZB*i31_B87Tk3R2>SNs z+Delq$%li0@qlT)etZc+=`Fh?TJ#L|ylc+koAMpec)au0ASYI#K;6v=ybP-M?f`+weieEcGI0Nv#T9o3G_ApA49AM4x(M6-v2 zzh9%W;q*wRL9wiM?O=JTjb_74*;Ro(tw0tiyr*AyGmVVDQsp!AWo*N*{wyHNv&%+f zQl@}UfM3GW^o)h>1R~TOGK6iBG`Fd{OwWBEI_t~p%9(6Q6NCdOaVUE0MdDfe8`_5i zEY|!4I{6%@DKbxIoGFi}LtGl~Hu444+%U)@STRZ@S{44T0e(|GRc%K5H?ymf0{A4d zOS&F@kaw0G91@B+Wl#BfXr5#rxOiv6sl85jS?Dt2AwP#Pt+(rs#0lAj%iy(PGw#~7 zn`n!oMG6&KnV_G2?G{!uB{!l!UTV#795i>hnyB^vvG9RDHwv1tY)OIl@@=z!gA3}6 zJ<6Fc+q~rs0pY<$k6{!Nbwd8hBxyq7y%pE>-2sYWT0AaiC*Qg2&6VMz&(Z#>PQ${o zvnR2vYMrrRp=WL3k z?GdRddppN#9x^js4eyorQO{o4Rvwo%n({aovcSSFXl{6k_VS{vGY1JmTc~{s`igZH zA}a1NSxZ)c6#)h3s{ZJvHhhmHTh_OGe8sf3wtsYq_QnDNR1;M-b#47fEkcI`X*$+l zGE@)#72(KNs0~ExY|FkFhQ0h?s)U=G_oLr8k4lS;G^T|Ot{&;(yIpBT?;e#-26dDJ zuP}l8(0d#18}{=H*XjVyVI`}U@~H;nTGcSQC|S{0OV%eg`Qv+sD3nDWa+0Oss4E?pImoxMwE(Q1!mjZmY!~W~UWclTGecAELiOKS8 zG5yCAvrB13?g^)Mztk#^q`>}Z&12WVsmcLv5m^;qg{ZjTKMtisB=m(AM-;JcgQvwM z;``)PUQ)Vv7EXNR@ejAg6}LtgD56zF%H?s9QBeQ(_M@+DeQElP^qx}=?0%7~SGuZR zQKUW|4gQD$kYot&;1(M5hZtKQn*&;rNXFE8otPd33mg+@uMnHi<&P~OQp5OihcP~q7_gm;*(piT~o+6 z>cmdq{RB0)Hn@BXz4_eVHE|SuKAw0}*-8b6=ptp7C@qFU(}OJ2;ElOz&#a;C8t-+& z@{QrOA9FmJiMoedfYIqBVaIuf*KJmVFYINk{)U}Q1Hl$6veDNqm2U&=Xz$m^XVPZU zAh>&PploL85 zUr$Zu1Q?vu(xvwkxbJ{nTQwJ%IfZ#5H3>d-NlU-UKHkiS zJmMyM~ngxy^%ApaJ{OL z$a*=2wIx9<6gRmYh&)rG`r}6`D=FBTmta4Bx!@&}!ErgKoa7w^2! zq#r>Iq;)$lXxr@91J6JM|McE-obcGi8XjMbAd|a209!e+^gI{5GP@y9sBo zVc8@7)!U~b=dsJ%bRcVygraFue6N6ZFb&HW_ZW)XV|zWi$I|+ph(df zzxS{V_Ml-iO1H*wEP;l8T}CF8GJdhXxekAexz+Xk>mdV}7w&hNsNB_N0Hj5k8RHh( zk)g)|mr~;X7h#^o(@k3f-HFT=FaFc7dbSx{4g0biB}3x&`wc~jS#e!6`Mv{&G1riO zeRd5^p*=*_-Hn~>9S8t|o-n3Ck_BsyhU&fZ6Kq;Z5h{}=%alx5;e@)lC}fOXvs3re z9-F{@@9{A$_|82Sk%d?-KNo%}od1JT|L2~bOFher@t@`Y1X;2CZIBhqe;Q;}tY~hzz=HhnL(951 z7uyTKAM zSBIkTL6o&MQf<7@sf1zTKR2D1V#*RkO?oR@rpO9 zI^O6-SRwfc!b~ed6)RXNx#qv74xX16Roq1njUC`hBqOJR!&b_sGBQA%Sjyufl=8Re zIZ#M%%cR9dB(!>}?o5hER2gKz~SGscx@H1K*q z_72P=du+gE(y)at-w~v?xIH}REgB_XzfX%j$eU3IpLABlG+Dw<*@~x!pYON|b`Fo9 zzn+*US_^iLPArNOP~;sqie!Gn%Jo=QH@JP8c&dK^aV^+NHeUQXpDqi*U1hISrM`eq ziz3*dL^Ld3T7=&>Vj*2n0>C&nrKB;pk8KyN5c!n02O}qD)}ftXnYdr4p)@PXRj1ZZ z2Z%O@3drQtcYtpI7T7R%TwqCJ;U-J z&Yv7Wlu6b67yIzteG;1FRQrRc0Z1~bonTY}vK0cl!)iUUshwVot>H^D@`>RN#(hg6 zjg_A!B^*Xr1bTPvI9wq&Qn$NRI9Z(&Ug-rve_FZOl-=23B^iZ`!1v!vHr0)IBT-Q0 zSJ#eXzmu$X$1b_eR$y{9Gw&GClVUkEN10Gp3FRxQFD3m);bT`@&G%o|^sE$W2dnF0 z9pFPm<8pAjLL_todi-tSdr1Ri1O7|mvixIS&fnL)KbyAyR`>qQ%KIHHnp?seg8WlA6hp=@9FYK3Z#qr6G*pPLXCSVE1 z-#$t8@%Dc$1BrnB6xpdmPEiK2@bwO2B%=Htg@iBf{QCu#|A^jKPiQ}+_GY|2zd5K}gEk8D)|EW=Z2HPw0>)tL6(~qb-TyH2Jf7QkOjb+assl{`- zqu~#c+c`_#SlE+eiWNlP9?YTO0IauB(ly|vi0;d|LzC$;ZndkSj}GN1Cokf{K>_ zqd2FkdznLv{+{uiI53cyR3`61D-fnT0A0^*r1EBkv>Qm6E$c=RXiXM4z0Zfbf%^Z5 zyYfJrs_$Qk>Nb*yq^Ba|bDpctb5PiXWhJW&$;*a&sRNt-`RWZz1R3zpS7W3=OZT{duOutk5?@^ z>9T*ntNOp=>%Lua&GUC{>e{>3J9PjBcki)B4!P}j{>||xH@WZi-#==4-Q_QCzxjf{-Wf9g z%;cTdeRtEEk=t)NJ@Wp#+n=BC`}@CJ^M=%#v-8sVS5!Ra=2l-c?pL?*(4y;pJ!Eo= zTdGhBkv~@UgxyUF3Eq>454+rsplnxkD@57T=u06A9qM)j`7g%h{C_xt>81_Hn}9%;K)vC2+#ZzVKf6bcOcJBy$9xU3DJx@SWW1tQB!*%CNO-OIF_g(o09) zd)@iBO&+p*^L{1w&Ditjf&)Jr+PL}&9~}GZ!yB*t->9yyys)drpc}Tlcg#UGDgrP1 zLFXhp)~xhvrRk^MpQ`ox8wagDd`QRMH$VQ*-IebydU4sN&(B>jvj(_<^Qu<8VCR>Y zRjg3w^koN}Rei|Jxoi4fQ*rb&M{d7*RA<#$7`P6e8+EZx9W6Hhf8<7YS)|wY-dMffD~a1j-*I=3ZNHy>d9_NVqeu6|<#@wuzM>ksq2JGcFMW!cj+uYdBA z@sF47cxcvB&qQODzPtU>k+r|Ow(R2gmQ$xaZZG((-fz!cHQO#dboNJ;XD!&V=l*Al zj^F>9UJpHKwIA31p9)_bck;nUuc@+Mm-nXksdLCN=L~B0K=qfFJIe+hG^FujTzFITGOv_mp|vf`pSMs-0{QDqx*gT)F{qxbAl^>aY z@Wa!8nfk?)0h>3yzGKgoPj0yX=#ih?xBSzdPkhyKSMLvAKJ1XgTRePS?=cgi(PMjc z-+9%W?W=#<;ea}G?p^TCz3s++yZ^)kV;>hk(X4&T`HwIAXXo|jFF&dE>2;S!o6Q26-FG_c;~FEM_*g-qTN@$JMN>mwmsf6 z)#uD1o9_7jsARWm-}o`Q?yO#~S2On%U)AKE&i|a>rPRNDWLIyF{Zb#lcE&KcCG zY5yvzg%R?oWV*5urqCe4Zr zXtZ;`Z;z?F=b8~S#;^W*$a8D%U-`=R&CY=RdQ`Xx*tYKcp~-CxPFy!}<@(zjL{B*4 zjh^2&e5S&J;}=$YWzmA6Q_t%EL;wCieevPzoo60B_JY5Pch%nd=*UgCPkHZy+xu7T zd&}|74%sni_l_|)j6ZSt;;t2{R9jkM@2^*PT6$Q!)hkciZ{3~=bt%=Eo^O;gxZ!8d1{hh2PD?cC8&>Z z%PyY$VtmS_$#H$oTC!~LEkFHrbLHCgSFazl_3vwsJh}R-qdrM>Y*DSoq+2!@ZEV=9 z&*Fv8M1H^fw!?qB^^2}^J{o3Cn6!St|xV6@T6%RN3wD$o{yVtgC`Tm<1uiN^^ z?l1Oxta0<5KMdtT^c|2QJ-W%#-2=+*{BziWCEc4}^l@r>yB4!L9M=4{!@qpu<0r}{ z&vvMGJc)QglSDpIyF~eI<_@MW)N^LHf zQRnTheIFTG@$cu&x))73a^aW*Kkrp~|2>^swEFg((h-9PtiEcdS*7V$m(6&-&GFOY zR*#p@Kc`Q>n_u0($F%h$`h8Tb^I75y8f71TbDpyxMY2}1Y zuk^Zp#V3ik@4u@5%RBy@_D!Scz#Z-He>OUE^t(?Q-zDBO>vef@8=CMZS%^s(WM8p-ZXVn zhwbOg**fpp)Wwh7H6^z6^G%Hxjk>*g)9daVHu~PO#Exs8ZQJUWqgG5AHRk#ew?Fme zGZ$58l$hLbUBf?Ls&L9#n_ZkpTtrX!F2?86IwdH#>J#nsn0vj;brJ^#?_ zulxJ=Cf81Vv(?60FBIR|_RNOcp7`wZq5Wq6d|HDgGxpT_uFq#(_a1!U1?P@BXUO4) z@A0WJUt1CCxY4+QKrhlJs{ADYfY_Br8a=qT0 zYQ8^Y?V4S44_(#a#oPJ}e))mxTUOjNGuiK?YQ%{;nccju|@-8P*(|CQAb zJ#>>fuLt=<@hx^N7nPteIUp=fQe6);_UU>XiL|`~1b4 zZ%?j&%ukJ;uDNUCmAi*kTXFM4_r2b|L!)J*ORKf5bHvftEqLIwE*r0E5^cZspSRvz zJ^s0>kqPbI9UmDm_={Sn99#FFF_(W-`P<{nYg^pA{lRB`Z927Dn=2PAa@t)!t@r0E zp1W&H&+2F0STPZQEjQD!S z_>Xs%et75HH>1Z*sXFksha%fj*VH(?`@jD-XwYrltc%L-s`kpZPmEkY;<7=X{Q1|_kKIr_dgE2CNA~%$_|k{^ z9=83}9b>K^P;dNW(anQepEu-@XNLaPXutz6*Sh4ab7oz);l*wb?kT--!=XL9{IlVr zJBRe&8{72yAAkS8clznCe|z;`6Fxkz`s>@Pj`*hPrr}i@^@<$u>zbKY*ZcULe-9tC z_sI3%t=vpnBj${96J#cAmpNkIG^*8dnv+PIpV(vzCuUpoCaMHFvM}Al~__B$WCyZ`- z%GcKG&7GglxvNvPqbJup^qSO+>C>mAW_{eC!```Tr#!#A#~Y7s+x^;y=Y8>NjmeYm z7|`;{$hS>u+_-D{NoUWffAy5b-TKY>W%?KWYtH2j!Qb5#o%Vk6zKX0Nc*UnD&YELv zeBiKElRo*exjDIM`?T*G&UQZ87&)r_yz?qHoWA_- zj*%6^t9(=Sx$_o>~Y+gtv5>(oUD-@5+Vsb38L zdU%WZ(RcnBv7yaP+Z*1$y~@|;uIyZPLzUX?A1bN&LCY7$HmNaU#Vw~)>^p4q=lAp& zbz7rN{Xbmt)`}-vzqQ}50G4Qa<=;2&ePreqyXn}acP*=W(pjHAeAV7{YkJ&!$fj<) zr<^vp#pkat8}Rl(``X+04F2@ODp$YoXOp+iTw1-~wx9c7Skj};Cxe?EJUmhVkFgu3y!Y4dC#=2phl^&n z{Pwm8f<%ceYeJfOw5jNdWkw}l-my>&wK7b-MqcSyZj zg9nd1u>T#yOMj~~>7%x-ho1J(gOS%R`o3}fXPWN)?4HQwiB6-M%sgXac{7;tzP`z<1f!Bdu{&~CoF#Hv)ex3w(6v|O`3dj z$pfV~A6ooCy_Z`5cxvA!2X^kbq3`QQHtl{w#XE=AoN(;osUJMtYwgs5Crmzl)J=cg z_G0q;imQ#{4V^#u|HQ6vf=4npZ_)BsDJv8__$ri8I#72sPg&M1K)jQy#42q zXFPxWZSP&Msoi(|&iLlzF&odAIC0TE&DNa}pWNrB#CFxvl@Py*{XU zU9&s-%-Ht#$0t8ItGNHhQ`c2~$yvPgwHBk+KK|K;RS&)}c*jG@HI1$~Wais*dyV|Q z*UIai#ZaU??zCs$dGn%a6*oTj+LF!36@B^0K`#%u;>e$VH0plbBC)vL4OR9GDQ)xg z3FDTVYepS?RlC-AFTeJzqhGi&@$nA(ysgQ9Ds4OW+9R(xUpO%Lmr^>g69Q&F@?G+EZI@Y%=4w zv!>ns{Eid)Pd#M(7W3OtGe7S#=(}$39aFmPfn=-wKK|zIIW?O1ocHmy+v+{t_V(=! zdwgVmc#$U{SD7lOB8?7tmVQZrp@d9@zN#n z`1uz{j_dvA;umTkFn`9LtB(G9(u(z;rmmg3>#OlYzCXOuu%ebNiq}nl?7gLRr%v3k zz`pC-Q4^1Rx-AT3ojGOeHM?6FhwOiAWW?xAwKwe;`S-e04!`fMo^QNuME5Jb>#I6T zPqucJefwuJveQ2O*Ygfv7p?uqn0XJZI_#ZorAKw#I(yW8Z=N~)lGj&O8+pL^9=9L& z$-`H+zN2i-W3Nqrdt{~G_Fn$?p0?vDbDD0-nHz?OJWBf+Vl6<5AOBbsb9X>sMR?SFMVWn&*r^b zwV%{yXQg>HC+@#w^!D*}pI@?|?}x4Ww?6iu>nohJ>FL-%$IN`Q;-8x~uNXY~a%0(z zi^qR^`Iwg<828O&=LBH=s5a@g8YiZ{i!ARy|A-^H&ffc1|ASUFZnC2J&7G^iam6bg zw#4dhZG7X!58QL{^twYvxBRf)!VQ!Do^;xXCn|KDF~azcRZNFIUF(c9x6j+Srs(<~ zSNyo3mUG;Tza6ylq~T*9x^4O?@7zD+$Kww=cwx^|Yu~*>vqmHXU67TcG3?mc5lAb?D$8WS7*F&@~y+VRo!*mtGgDh zyx`8G&+FN&<+n@Lx2Sdh#bc(w(fIxL_KEjYzV_X5RT>QJ*R%Sl=6g+n>1d%HB_voOj}3e;Wg)Z(C6Pyit!o)MU>|Yf4W1dNv?a zUpS-aoc5h+|2U#cYJKNq?KhY8db!Q{9q&20SL)eLBf9?fY}Z$3JUY1j%dx{2cOEh9 zx&hxjc=qJ;r^V)Ndi1rsA3b6Fkas_Dnx1%2o$ub?zfL@osQvo!=Ug+jbKTlEjjZaB|({5*2r!*}fHxb@~Pp{ijW6P&iHr~?tqCeN3QK9uGQ@Wjg@Moh|H+ZbuyN?}{ z{N3U_xqR_YKQC&ywe9R*=5_q%)Gq5A?c9CG z=mjm7J^KCp+ZHw&+IQWIV}2j!9C~%)rRL4=d#3NujYn6CHmKZnc)g#09JP92-F5xm zFh{)6V*dC8rhPQ$-IA|*S&tOm8QJyhnL96U zzWn{B3panXf1e-v_vz8=pnskjyzsGKU%q`#?7^-dCVTCE|IJ#1p1EP}(#N|_C|Ni5 zts&06;}861-LmWV95=l063%&4S~je>*T76T!rwQ0^(o60r0{NS)CJ`ZQbZ%SyJ;yQ zyS@DcDY6UP9i;GIjLZ4|2vYc(*aBFPg5Bu}*`4mvp}adC7n;_gvQ>SbI`p2dI|gk? z9Q)5tkNtUm&o<*%SAF`#lAc!{|Jc2Y>OXmMq*B?&BfEC({q*xwJN+{De=T;m-2HX? znj03)xOn0f?``>NMC#kSHf=s&$^OgFxVU25VY}+?Ipmm=ADP(tll?BO-C_UtjvF&% z!`MzIS3LWbDR;j$wrjPOOMckcxEHhvJ(jwt_q~5?ywLji5=fanc*f*!kMDb3g_WBR z{N?)17uLS!wBiYe_8-0Br0C^$wklojRCr}+Nzb-#A9g{lvYSqH7XEL|!ezfqIq#ff zS6{g58t17M#$iA0?7M2&;#Dt?`(W+B9-mo%?itYK$b~CUdB5^c)h>i)p;4cndK4SP zx}aA3Gq+#%{C@GJqlUk5G#Ob?5v0ly-lx_^#DUme=SsvE5;p9C!NS_)QNz z{m-HYU%GnSknzPld#`^!>=b6d%r%ygJc7MxCjc=`+y8D2-3qM@hVbbR7>o%x= z!HU{HjJRp|v-SV^V?pn;n|Jv7x+kAXR{Olg7n2@)_FN;@dq$fd2lO8?^`_z zquU?t@=ekDTR-1;_~ftaTzgRaxpm(gyl432SC?AXJ-8*_wq@OTHFf9rz{?P z-JrKRC1xEky3US^8=OD1b*Ekj{PWM(w;s~H!;1Cy{#5yxMsMHTwa)QJHa%@m%~$%| zGw-iCZMv;)`@^3nRGZZxa_{iY%i3Lj?#vV38GBp3HTQjV+cif%Reke{3wIrH!vk+# zd2r8Tjy5OGx#0E>%DSw3h=xL8UHSxJu7tH;4+L9U1FBtIEXZQa4+_*ifMx9)**Rj`>)cXx1xE_?p8xsU$uh06Az zPN(14b@x}d&m4Tofb}IC=Ulve$f7gfTs7;FpCa4#JN$@&6S^+i(%_Wl(~HMedgJib zHQQZK*6#S&_}Jn9bNcqL^~pec$fYk_cJy^W&ad6B!bc}J>vPRphdp1iw9keb9aEnd z*F5T{`zqbIyVIsAn_pZU>w8Pz?a_C-P5Zaj`AY`1-C6zb)pyQYb73EQYo%d7-TL&Y z|8Dv5@)@r*xxv``N86vP|GR&kPdy>Y&o#ByO7k-qd zuPv?o3=|#^RI~H9f!AFqjt$dY5<7@}Ah?@bAf@ zrtM3vC@s3QNVbk9;1_f%!j361Q6XYV*t;b;fh}3Yu_Q%Z57#JJEA2b02Uo5&$cVy{ zXJ!dXk%%RGRV~>lYRO3tw!~=Kay{;K*gOlCn=EXZ;=`HYatv(*C$8DD3l^tR*c@Tn zsR)UY-RltKf4Qv#nGFST4*Y&$G$V0IwStYhq9G9eUY;9bSKaGKaKKY;>p(OYmi%Rg zF@lasLf*%O9J-c}O@ay8=-^(5lLeJlp8MfQjH)Bg+Yz(uBaZZT9NFFN$ZlsxHXu9l zzPQ(sRM4&d-bzxwgQUbvBt-%>l9Y-gDer4iPCrS?9+8x!(sr*Sm9Dq?e=jL1hm=h8 zrbIj%Ny$f^lIM|sU>hK(kMMAWz?V;JRm!r2yN$Tw^N*(TR%h9UafBZh&KXAB%2V@m3F1Nf@M ze{4!>Qe{D=l0MIJXR`iWnv~<(t?{Z=!@ul#nF>i^_s5e&G9f z)Y%qfVH(3iUMNen@J9>_J7sNo)1h}t3dY85*fLO279ozyixcE@iu^fOx#KM36i%m|rUD2iNWEnu$ zNtr!ZM9jotz`v*0f$j|WHT)1G>K+xI4)I>k=vCHhz~FvG;18Ddx&r>CNT>(!kopyu z!s$udsyKhlD?(AVhW|!Vf;2sS@USb2hk^IR78(Nq%l4Aig9Z&QD{9(#c<-`NLyBPr zeaWEyMNK;u_bG$_Hj#>^B4Dbb4zP0=Mk)&Y9S;1ESy65IjHV60+qtOeg`Lh8|5N|U zva%ti&6+kX8$5VGX|$xctZ#JiuzpR4T+z4bu)cj9Gj29;t9D_a@i(1WT*}q~TNN*- z=|vY`Qq;sQa-em7(C`5R*gnd)vxs+RMUE4TCLkf=B%{z<;v|hIqw7;Q#$$e54i~6A zgz|khcY(6c;CBNFlzU?Mo6-W6FaQ5Rpze*y<)GEa)tD(KnlgafrCetm$#Q$a5=6o6 zF_Tu55zm-0xX18E%8oKGSvzL;{Y?u=8W3ETWyhno1s-*vWwA$9WLZWu25b~y`=jw9 zuy4^6tW8@JyYqN({OSqErJOfD_p8S~gWnBw^>{r3e^WN87Va%`-xHU^1uCpm-UaIR zR)YzYcsyylj@Pts0_7k=MWBKsKJNl$pTX}25-7(*;%`ddP$c^=xV7P~D=vrMP?6=x zyFlG;Lm+`F^O4d5l_Qo4e?xi8lp;{sAeL|Q?M8hC6DVn!WUy0sf%0%%5vUAF@-0wz z3=l}5yp0%lfHxH5r-BPq&{cSsqxrj*69qr6Tz*H|nI~Od(~3egFZ~Y3vZFC*KvMh; z!K8sJ;z~#cdHFtKcbFCAh~qpHfjh_?u_n_(#SntS3zx$LD(MSz)9MhU7C|q3)H3Y| zWH#VW2t4Ca6Y^5ZIf}2~XFc+SkZi<=5z!GA568@yVKf~%a6oBspC&_kL3q}y->~8# zgNKzxuPhrlK#9-7nI8v7v>kHUtMi71qS%eXcyMw4|(9{9_9>3ax9lW!r;4>h&5pA;fG(vrc08g(fdSUh-mStDTSEG>a>x>p(ev=sik z1`$^fE^^z7fP%z~7R23FGU6CU)P!Kwg80EGa!fO7c!E_*pg={V6Or1osc%os(M=|mx- zHP{n^dX)BPat%H}&!~5Tmk5+C1o8H5D+$qg#5ByP3B@v)mJVUD$;^`{5a!8v{IHKp zkw_&BZ3}L==JDP;NmX(4706iIx zIuP+|$B?5NnSi{cwgvaQ;#&yfCE+ooV&LaNTF8MN!J~*7Q43U}wuc=3BxthG_u$U- zusxu9%*oROoGz@`j9LjRVmk@G0mF)#07y_bz`v5qd-iU?oe$v~2;z#c#mB*^5_>3; z&XrB?VV}MTVQPE#83L(n?H;**5#K@(XTdhXAq44Lkl410@D!3ZWVSu0;Fg`A*9_`f z+k&2`ME>5Kt`XD`q0SHigaQ}_8*-y@@DHJK6a&kkySNIUub(|Y`BdEluRr6Rkab(#;E(V$@C0pNu`#||k3Sys4@Fw6CVC7KmD>4({ z_(6#0*indEb<^O!!ah#HPT98hpxw`kZy|_%mQgAOVJ0jdQgCz>#-SQ`{(kn5G&OgX zPf5Vv#P%R@1Gz=o<*(-Q&)|1`?7&ma6$2meH>IO6MD`Urzb*!Z;c~b@gZK+ly4ujP47={TgPlsV?dSZr8@wElpQr~78A{w<5J%B16BkuwOW=+X}{nb z2;wamfDC}neLw|8Qw%Qv&Ksa5EeP_H9=rfYd*LJWvj>=vsJ#W*pMR zqe0t)+w^X2q9!~Bz~zF4C;~WYGzPGHZ42&{l&_1V`OX~RhO*#W2x6b@c#NfbA-xXLhbk4% zRwojW@YRbjj#6Ltu>(CLp#Y}&27*{;d5y5lX2l>FgD{e+vx(jX6rE=WvdmV}r!+de zPf4`+is4PK>swMh;EIw#P&XXjyR_*QP%bMTR>rGiFcDq+)#dJ=Mgjh**33A<8^6hS z8_1Ni8Y!HSos2^(47Y&^$X{4cNzks_e@6Xz+Nx&p9SMHUO-q+MsuQ0<5qpl+2S!&6 zfX3gHj_NeDWWiu)EVY?|gYml)BwHcfKm#(^j6OlS9|qi}I5cW#*T?NNctom}*t>LU z3RQNyX{R=4G$|fg(uDm>%!IPL>W3(uBz!Z0{18!HPsbHJiARPNjzW6v*-JjE>*w_b zHE}hljYjEd28~JvL3O?L`JS43nl?&?jEje?Zg{4d+ zObU%gl$2fB=?mO>P2Vpb`sk@Gx-!K_yl)1sDv?S%n0c>#cdj0XLpm zfau^M#e@0{8$5gn2bT;UG@xWqF*gm+=aotsDJPZEu07|OL@~2#&yjo3{TZZ&fbO@f zgTyKKTYLz=?AcL1?tMEZ893m91BVYP>BG%rqrQWO(HCE#<>Z11;)dWQJ0qW8gxakVOOxWj^bi|)WeGV^%`DwWz)gKOPZDqFB?3pq}PDbrfn`f z8;meV5-C6#-jIZ%=X(p{qhaAl7G3OAik0JOXCH#%jLFE)-rSxA-&+v(&4MDN4LS63 z_@8M)@vLdOuu)Q{4;b91|A>;(;zk@T)wg(9X;wW>0)rLT(?4 z7UK{J+eP5~K{`lFk2(s1-ruFr(sGI>;Cl$u;eJIOS)Wr3SUXQ>7G zKrUObY)9LIRw{tXCm7Ix?wNErF*`~-ocu$%>>-xW${mX9%8wz4CuIR79}?}5Z#Nm{ z3=%2ul5H~%uw30|R+O`kn*>b>TEQbd7YL&uZn|79c$!pKjX# z^ziuoijDNO2OXHJ#F_jU0y#zyv^dEKBzi!>La_j#9*~}cBq`K9v|wFvJ^X9|td9B` z_+UA_lY9$790t<@YXE>J6WUb)Xk)@wAT1iV%xFwU+Bizbou56}dd)^572$gb;xU+T zX^co*CaE#3Ad}RJ)9+^sp0X2ff>Mqlh>f=3?OJ9UECjhi))h_yZ5$;i^R)+CQ%ZV2 zm4gEpa1a@vBa zpMsu2l$0#FWgnPb1D%xK1JpejaRSw{;V5~n@*V>e^F2Gz4K&Xq=4u(r9{?!n%JQzC z3;>k+8wLQ9R;8%m0RZbh%47)mtq$TXNF9$*A*tg@s1O6KXTI!1q;Vsqx(fh`bNwd1 zhY<2B7{th$HXX$9T0kyO6BuOq%g-T*zaX@HdJCdN zmt_k%f?73jgsp%Y`33@c3j$h?5PYVWm#)XAcQSKLf6wdJG}}z;IXw0CYI402_R5!PK9FZlgKqBj+tZ zb`FLT1L8Icvp7Ma#-b2VCSp-XZ^3dDaEYHS0QsP5-avD!<_#INb8?<#-w8&;vhRcr zhZT&Ek3E=xO<* zKqFyKB^0(nNOYQ%e+Z@LV-Io1QR~~b(o>0d1DTvLIRxyT(KrD0g24s==m!iGSiUy^ z?hf%At(2g=pK;!BRmcsDt8&6tN>WWy+)aRWb*+fs*C1IdA41Aqwbk>m7iO9C4AGUX@n^ ziw5#)&_x5~Qu>gq_q8b+nqTiASv?>6!ks_~wgY$Elhzm7%_tad$W|g!F3=D{7?>i$ zX<=kaf?|ac10_&V`H-mhttp8Clqk+&2-B+ZBrs%HWS^gC&LaE##CQ|j9j*xp{&7ul z(5@hdFy22RhsFCRfy1oHHKUXGQ3p!K3e$in0QvrD6aXiFM``#tYw!D4(ur&joQFPF z(_ol@c}%;8tOJ<#3l*|^pX)yf!0?QAuL;OYa}t&OF5gU$tZey}8Rys!mCyNhz_m4!fX^`vKtgh^G_ckH)X}?Ypje)HSU#?Yx7MIkr}+kgxWgt; zils7zH-YS>i&lyk>thS-g{E(~mS$C;0(>)pTv6z6a#aywicB<-rHH5m6+SlO%@ixs z=lK=_6>|Z5nso8Li8L!o{Kf`Y|SsGiVScQ4VPk#oVYI zbdI;Mt6-VZI!6xCNt;$6W&~XxhG|7&FKJpSUN0zox=xGVlOS%XbgePpgQk^iWLJ_U zKJL2Lv?@?NzK0;5pfs%j_Dyk&AedGNyx15owIfkUSovIkTJd;ItAd^6n+f9U_%N;R zg167@s97)tz?$uv7tXXQD1RnUOt0sZSY+*cvSBrCTFHi0{*@fmhS#(zogaJyL7XiU z(Ch|KsY+aqEc(&79GD9ef+|;Uho{|xQz_@}=?G$2L>URrhAg|u(b@4DS5LbKCsWRO z57M~GRFx%-D>bQx)yG3P5#60vdi3}W3*zsfag|_qG_EAr9f&n@+K<<`Dy<)Bt)oqE zV4aFEfH91@LZb3-7YXA^7XEGCS&KS=%Qc7-R(@Tsl$!$Sa;1|an^X&@%RLxwIU6xI zFVKSYPTZvm1^;Ol<>9VD`Vc8}+iAgt2gEG~EaupMEfLU2JMdqWY>rad4Ymij8sZl_ zh{q)hT`WljBLjii7|7Ncc@41Ecv?%8in)(HI6BEX>kiDQ9=zImf2;{gEDmENf+f1e z24P2{{YOk*AiIBnhuXC&F4-XNpDjQQ2(*#}Ei}?ffm8U9EC4g~8?J*(INBsv^N6=Y zg2nK5kZ3Ul*5YpuD!9Z_Muf6+kn>@4)HI@$i}5Dzfg)NHA3~$yV-J>&cmq#TyGwFZ zcCbAlhy>D8vmDw&};oh_dY{@%ea(3PTcN2r=eQbQ^6fLCZ>nI&Jfh4Mg zzwAx~B6GLo7UXRdwJ7p3itnI447LTg;pSTilJmnY$ojwB0_lC`%AW4M*Gj62Zy`uY z6Sp9}Pq_us`BZuUgPnpqMC4lt=Lzi^S4NLAM}jEaVnQ77Hcx2(|^c!Q)#9 z;_J!hjD=d*NE}Q2OTqD8sEgL|MVhg}<3NtCRizL1i&M9ctISI^4fwv&%{o)p= zalhg_;cP*(&g>Ni--2X+m#09Hzl-mLvjxpMCwYw~pi;%27r6z99z0{j#CJm3f&%j7 z&p8C^oR9z#xdjSBl2mAw0zK!rW}TCPn9ikAMGjQC1qzxkz7x(VXx2H&xxa#5BsmIA zZh@MFCcYES7BuUe0|`QF=#0b%uHJZ;zuw4AuEi8fs}`0a2x2yd zK`{W`ESVNf=vR#Cqou@m@OJr-sHdh6%%HA&%I`!VZwk^Hj+yRV6HJR_QkG4>&O8tw zTkxi96zUwlg+Shv+=8S|m0O?&d?gj&POvTL7KG!Pa=u7%3v$-A+yXW0ngx7eoq}#b zIMgiY7NA?tNmF)OFws7t<0_I0q%GJfxJ?MZNP)a5c?y!TN1g&TFd?ZR3xjRJeV%*^ zfpQ&k3$nFLY=Im&o>ZWo!M32scN_*A@FJz>bqnrSYFu|xf!hY#f?K!oQwWsn5T}3^ zUY-JF;gh*?Y7}em z3L|>MQmT6INHP$mHl70Fk;E1#IFk5ID5s#bj=PSlO%7}2zm+#MiN_~>OtZ=;hchQo%x=EZUJ#T zd<$efD5=OU*eU4NnfEXS+yd5vgk2!(K}n@oD%ciu>&)MG&@IS%klX@Q4}!VAp($0} zItQcE7S9>$L2?UJJt(PY^I)gowlGYo2BOoJb|H>Kk;Ge|>OtZ=;XG&EItQcEmZu== zLGl!+dQehnkqvYTinqm2A%xf&IaA~o$a;{{vK(v+?pKvgs9+icvu0tl6B#k!PlC)D zMNecYkp!1&kZ`{ISt_NGJ(y74p>S5A7AzI1q~Ewe1C?svGPwHN#XJzQ!9AHX!MEOd zV(Ifhl8|o=P;D}Rg2FW&2Quvtfs=f|;zOY*JbfTYlJjooY&qo~WRAy@dmxiMxe|PP zLn?hBNYV_G0lBf)%VU%t%)G)v^ zE;b_7b{=RDrQ#-e5G|!W3>RhP!InUq0<>EdA?&~ee-TO>Bt;_3uy(nPxTyR{3c4kL z9s^5{l4YEn{|yZ<-c>=1jdFY@l>7`}wk#KLJ6pYq*A#HfHTI{GW3Hj4(6uK1E`lVj zeAr!g1tQq1aJ!V*&bnaLAydH-rkBIP=%8(|1PG7^2XMaSJLdnS$i*C|ECEtEPE&zr zW?hpFRASJsSgxD*-%FB>k%B&z?2BZ>4KpPOcJBgl{mZG%X+mdsM1WNw37y4mop2w@$t{dP%88S|cy69{)P++{3i&mE%7G$e zsQ4xl>G`qpDCr1UVKen1Qm++N_}Ao3O<=`KzGY5tngRx^K##`+hXmdQP6WcJjY4^t z3S>I{rZvj>LK1kJ3UMGm>mWDL6RHS)LFTc92r>v>*LP0`#5dy15!A-@w8!79^8S9RWF?N z;Z)ilW2iY9mSsibaj^V>R5w#sMHXZZfgV;;Siv}pXbi$gh2_vx>{41pd0CSnea)DY z?Bmu=?&qMSxT*<~Wf68Pf;lG(Iws&|kU6KMf=Kpp`8*_YY?gUO)v!Gv$~vdW5)dI# z2Zn{n0YB`Pz;z;rX^0P@^nC6i9SA{Zj5_@Uhy`66%63%>YT}h3nu}N_2Xhi}`AvMT z6J@*_O2I~Fm1)6Dyi(oc0%d2OtI9VF2W)yo(rFe+qCglc10WuasXD2s>Hv3Ev-e63 zm~SXZ>@Fbyl6t#H6rV}v?WAHneeB1>kYp;A`1BP)pdltm<7U)~L!2MnY9N<`OpsF8 zJxCwAQ~?)jkYcHH1=%#-1ZYjE+iVi-rT9udwng<%if}|RSF5ZB*>)IHevVkOG`7;l~ z9VM*;o1L2xcD6O;VWt&6hgdSoFat_v#ZY){vMnkB2|*_Xh4B#Hs3hl*iz3-M423T! z+oB3;8w`boiQ9%CNHZTV)O~4!*aiq+%5nV)mS%+Llw6LAn<}uc#AeB6o+v-%AQ?R$ zr|ec&fliqu5(Rbu*jL8n7VwnaA-H_WVt~ABH6cLKfujKm)kR2aY6B9B_1znw@&H|^ zsyu~aoE>tY8F+He1ePCV`iQgu;zK!@TTj{+h`?PJHLW3WrVQFQ(6uR;tsH{dW(5DB zt1(GWL1JN~4Y4LY&tS!6YyL=9+tzsY*5{odK9cs?vnnV}{PPEWVS&+p(bNujJ7TWn>n=6G5`AvTTD} zkY$@JdvF(9e5m<^VXB1mY!(D@D|mW3qgbKU0^GJx_@c2wEOLPsRUkGzuoaHB$oXoB zu_#y_lr>&-qR`JfuTIGur}i^P4dkIi6J<2BZ5p426LhSs7F#sC>qbtW>N`V?=bU;T9|Jkh=a`B zwRj|5QGprkP*0#MDvI6lu>}i!1MNz=T~f9M^qO)i$67Ei%}hiPyoqx#sA7ru5DL%7 z9^$ZvbX7>5a>E7zW)1}11KsHgCB?>^EvReZzP@CG^_V61MD2~$8>1BVhUB^79_k2{-) zX)|;T4lF^5@+|~$yKrin^c7$7Dm{`psQ|x&ZNc+219>=*@JzrK;13&+H$Xub1t>1%9wwDQ#>W=GNz*jA0<`3( z5X8ZelZ?`-g9J2Jz#gd7K~f1PgY7{NOmWsrZdM4)zLnwXvmoGefyldNf7w9kV)P+iPx%%emCIqzD%3&& z2MiB#4?C@MdVO-(IFf6bKy?PiFq&OUnJ?x~q?(sP!u5dX00a$BPip1_w2VTEDTvK8OK5BoR;uMf-HLh(1D@i9^5qo#x;I>34YYD!k7fsPb( zUgT>hofku)e#K=O$mT7V6~3b(9BbOqPzZJTVHYN^rf_2TJ8R7sC2_kRp%%>1P-x$| zKm+B~cq?**nPUekz_Rng0p$$&J2kJSFs1mAsBJo_w4V>?W;LQ!i1_(-0z0DE7bBw< zx-UkdqRD+X`d!hB4IILhtC8fCPLXt13{3g}m>Jy`qd=v?+m0TFA;(uvPOV`A@m&PV z&WvCRK%{j3Q^Bs~z8x*|;NkJ(sJ&cIN0v&{xll2So8}@^;DI^sM}0X8RGGh=AcaOT zJ2*2$8s;upMdS?WqyoUqbu;m(_H-1KGv7`S=S(;@Sc;d94PA;?K(0QvlSstl+KIR` zq-cI)_-3T}Wk9cxDYs;1i^dZvFlR2;APyss^wR^TMQxp&>6=vW%znh`uB~gvLgDy> zy`&JWos&M2X>?$|1KVd6*>GnVaSTQ0Ep?X$CvW*)QHK3ti z;k@)3ur-mb28K=znBqUkRc)6$S0z4^!bz9kiO4e;-ibs|fk`_~iLLtJg0Jypkok1uH*9&eQNe;+Z9Wqb_u5W_)+T{L}kN59RwfNY9=l#b> zDnZ6l=1!~@?)@uCcz!2>WK=CMm`OX0iIPBe5)+kDoDm-)O<1-{nh{g#G-++YEB0U+ zq^}hRTrS+sfEgIBE-)1jI6}}0WF|hQRKmDigQR(VTvWH;=|if-MRh;5Y*H=UMO82) zxExbsxeJFszyS6&2uCs4mf2h~*p4aa99))>mkPEB%Zga6nSd(~eP+x@CF8P~0(|1v zFGxTrGxX)a$)uFf&By&?C_yEHiYaKgv}E&;z?5=D{!=MelK(Wu(U$1Fak&PGOr>1Y z2y1~qq5#$@CBpY1S9sT&Too)T%2jvk!--5~UlT%PWkQ}b2QdZZ%Ox8mHZ=eaXeT4K zoC_tX;cR#^5E-k<%#@M}@*!H5%v7(e0=q`h>Tp&LZ%PHFbr2gYO5;j^*9EQ!S-gZG z+U2_9vUD94MkzoxHl@dQi%K3fwmYUY4B(n#9C@3u*jGsX@)rW#_u%Om2}0IiLpba@%v3u?7+M0gT@)sb z(4nP5SAjy(R~3%4$b&pshG-81_$?HN=_GfI977nLVfDVK$ld2OJyhWkLVHO3VBOFXyxS2*?Yv5FX~Ou6_>uE=PdQ?w+|r;#*m4Pedz z>r5g|#D{W-m!O)cVu50cKID4{0vZ*j1h-%aQ<7y1Zv38)t6`^Xm9izK7^VDP4GUWL z!E;0#ZJW^bo~4;0rnr#lWoeFIIF5tpcLqQ_Tp4g0m^TULfJA1;6i{n=S(<`!0B7tl zPQu^{3hDmELn7J!V@gz*bxk2Kf$^tZo@}L(kl@cENPt}aNKUchK3;{_JyC{1XO(He zvXu&+OfDzIYb6=87)Cx?j*Lwg7e%8j>A1uc+yp=5AmO?Xz=qP6;p3F!sko-T74!<< zLXZI2$2|=Vu%STC@M0iZScb=i@}gM>iHbtv#OAq1C2}YLBL3xkC?IX~LjF?j~#fl zo`O+hXMhl)oI)|lVt|kp>E@G=74acF{Xkowo~7m?F+PQmXXFS?XA?7WQoJ1ydqEe8l$5Jak!Cd{Ir~85=g3NfsAUjp5=$D8|sXLrhwSG$r76MU_xfBOtWS$Ib;WpqIl`F#iPiP?vkr?_xMz{p}A*G<@ z`0)k_XxR0Xo-|(1ES@y`N{)V#ay=Df9a}#iQJSl(Cwj@!H4TAoqH7w;C{!Q!(-Y2k zaCn??F4r9hWLN~JV%nm8o^ts?RPHFm#})w6sgz|PQ|UTQbcnQD9S%B`8v?9-2-pNw z`ba4>xpGBG?yqFtSvbg4c3Buf5whTkc4vx6CZ!PHqGzF_UcwoiZo`ZxFx%)MBYzL! zA*U27+bo9{x;HDQK2hX0DL%IybpwCWfkROTIw~D-4h$GE4)(x-YXmKwTBM-h;e0#? zkHt}FX|qYRQ0}bq$Z;_k4am}&~xtR|XKQrg+&0WyFF!H7joO*5<6d;U0s zh5>3kkU=n^{W4jEykQclLh**F*fA-f zB%g(vSyMQaEf)Cn&^d%m`!JI=PBQJQFhM*s-jbVH5*s#9K)b16&~Qn4d736fjF1Pz zo=*!BtpMtvdOih~S2O00mT`%LxFPN-XGojPzhJ$+ko|yJR*VwT7C>$?=R%>xL_2ln z?0k(v`Ohdjj?;#Pa~UV54XZr{cy!urDyWmKK*P9B4U&-ep;O(t>L3meg(K6?xL^*C zGPYSBuRh(2SWG@De*j!My&<@CZ46+gXncq>ki3U8AN8KYcMevnmy z{jVqiw-dYvxJz(LAv2JSf)AnOTU5SFZh;C@wN%=416ZJ>!akbioC{@vR1$73%^)p7 zVjdrkGL|Zx>@Q3y$u1w_RO;(MGT_eYCn;tx-JXVFyQDcyPql=uKsZ%umW|3akEe}; zj`CORM4e1jJ5>sQxg-NPy_5oMI57&&S%zjN2Bl`H5`bY*Sq*hILSA)^B$vV{MNGS( zf|Mj^qD;Aozm6b9OiqyzK@}$rIi=&E#FAqbQ+XW#Q&nGwJ1@a^6QpM6!*|cIXs0L0 zlL`hg%Udd#=2W0bcwaR%PdHdSS(HI&g#?mOZmBw74i}Y|ikb}dMcy6;_d!YnGzgWq zR|oguv2nOe#(mEN-s8Vipn@D?Pl4CiU^zLVE6V>Alv17#tLJ$N6zN@3lWD=80tGWl z@(R1_fWwES!q6H>#goQ_f*2+9Vs!OwUP1_CNerEaX-e6kN2Mu~igC|!YI&d^5VQ^yUM@*tbaXgVI6R|VQKSKI zqM|_o;F3yYBZZ_R8N)axlW(A$b4Rc{oaf-bNVwfo)ntsRyGt3lI zTaxq6WFnn>F!u=Wl%noQD!o>CgR@}#DIomA@Zr;$VJRhu4q%{>s=X*|nZ6{JLboAq zE!VkaEB*@Gj00B0iYR`)q|$0DuTma;JPeuq;eZ(AnwY9@Z2yL20E<7mO%`&!cdbnw=~p~g?@E3#A2f$M~W(&Gbo7)p;%RwU-{ILdHXX{plOC)iLD3T7S2 zF=0GNiZPc$Q7{X|i$h}1v<9?rJZ*{QLEah3R!i~DKn(42+R&zJiX(h4lAjz^c5xIe z8FwNJ1BO%j(S1Bmy^ifDY&kEWrWaKq2wv#EOV|h zGU5+22(FTSHBzo-WM$%0kcX)-)@3>5Lb;F*M{eln%P}4q=O~mu34Xj->yz$U5*qa( zb*el{3jCEHdxAQ(Uy73f^Bn}c?OhNJMolkuw1X~~2BW}Vj_z87KrqF07g$5=*tW#{N6;TBxc7<* z^AJpN1Wc~6oev8Y5*pZ{kYZpX!R}^^Lcz{gvAd{MwBdmTwNQ$OE*E3~)LjuzIQ4qb zV2kNT8PieeM@Nb3<)Wa1sIDm#-7ChRBj*`_L}O8)x*%gZp4`)&3+?DakLiT#NXB$Z zf}XjK99x(Fhp9mLf+$|4^q=J|6zpM=NE+?Rg_H4?hq1`I5E8)80Kw)iGvaZ zCc-5TF_j2+C@4)nG=&j^>4cdj88H128sTjN{6;PGrI0iUu01ktS2)=pCLhQSqhkTR zZER37QPy-QD2<*{4=2elhFzrDM5MMLlTzhD(}Yw16;tehS{w#JGl~;I-bfLOI3%7X z4=PON)DbKq2`4D9h(keY@(~&-TJ=BtF9lG;wQ6AylS-RO5Uon0Z}eERP+FCujmi%v zh19C7yb17p(5`^FBWJzC)G|=&y2r19%EGC&@;iqfp-id6@D59BWpJp`g?Ul)Ng%XL z8y+nEQfi(8)abR4P`LOwcP{8Gm|6i5y#Xvp49W>8MH2!cC1XJXYP4euhcb#E8%{|V z$3_iFcN91cR~11pT&4vicF9O04k;Xn5aj735hi$B~6_;4aYVUMw) zJu40%q0HUGcYv5RymX;Jt60hi9R%zbur26ORc~B^8l-i`E+h0Egn6P|hXh9} z;a%kBsW8X{g(Mpb1A@iYWSUZddgSs~bfq;AsadLUV z&x+B-Xhte5&!Xem`9Wim}|F+p(hmHxLnn=K8XiN9!mC`5Itg=9u zZ_Vml1nq+J+hG_W zDEcCt-wvulC+dKUcM*WM?In3x&afP(#Df zh7jY4LLa&#hMkJSbs{alGF1(2lqsw5*pFx$Q3wVDQrgI&0S?E`!HlbOs>*8ZZJFNtcYc3Jy~5W?wB| z{Y2@I3GxOMY)_U$F4!C3c{Fq!vMeT?^JN2_Az`s^XiS2F>%_4$1S=?P`dJFX!$9v+ zMuG_ZkU%Zio&qt{9*1J37k;SI{aYwanC?7XkcUZO zdprUyZ#n%xiO?K2=M!wbm;&tpXIxx2^)Yv_Egxonax_fmf?ta%KgnQ6ruRUWov!XUav>8@6ioNh_ymr8ajhmaua}MffR0&&w;51_w&7!LSV^KhyddF# znDN$`HZ+5#c@wE>Vw?mV1EO9l^W0^^I_IK?+GyI7v_L*UT%i=<(v%sI zoSu&lkIHT4gGdtu<MdeU4k zKd*}tE=rokSt1oE&QSmsc^3sUjx^=SAV1J_7|ex4bTWASFruSKqLV4qPF%rH@b?)c z!)zukn93VLxR9i4ralZR#%+0hywJEEQ$`H3aWomD!?#eF1fc@M^sxbtilN{c`l~LO z)#ODO?LixC7P#Anhr^94nY7a|AHY6bS8L+)LXVe$@hmyiH}3DUHJDHu5&5(5^QLLws;1#ZD# zXJN909K?$EILPay&>8yGE7QX({G7!6b#GHV*`f z^WIhIgKomXe;Xz^BSp4jK_VQ6GIL0c4Ku7Epi)x`3gkj6Ru38~96{kI2W) zHLjp2xEc$R2^Ua_2o_nSFH3cHf}iA8>pm2joix>KmIAuqTaYAJIR%qGLNT|c^sxbt zpF}}4^haGVO{|claA^j~d}rMg(Xr;QV!i_WyF|DhLeJ5=+1dK zmIM*OvgOc(!8^diOwqi=DU?QjOfFzy0Q;E6DuL*sZHK}t#XUF;P9hCADc|bF9qjvf z4j%PF;WYHSS1|QLVj{G9Q#hpu>F7xY6A1}6O$%_!TE&TiMC9oy2mmfdh9+c5$gq%t zL_oPnQ5S`+$|;}-3=8v;55utrvVj0w(1`|uI0baj#T163=Qk~mXDJw@4HlZPnb`OQ%3S^!WDXeASRq* zN%07@K-FBB?C6TExio{6zBrOSlExG>ln``{5hKds)Yy0MTKN#C9x>VqoC6(7Sx&i7Q3xdVy{{lIEqzj3Ah;PS zq0l7=OQ}rKPK@{<48Kg`h2q_|BA`+t% zaiWkprt|?^A$N@E<6-LjrnZ7V;#&w(8;~PRk!DSfFr|3bU<>Y;!^akMf>T>TEAcH9 z28ycS?0sy&<4Y;PCRC2v<1d(UqzEv8Mnus>EKy(s;Imwh>_0UTzI&mG$Y;+zb+mmb zoqR0F0{slxGOYXBg0GEy_zLofU%4Q7T_5&1q^zJ#ft2)9F4&c$*fMlthj1$UG7E<_ zB9^SD3PiTTl_Bq3p6FRc+dr;IlFgBeQur@g7lGR^0hxJCdCAY<0}P_)ctnQ32wyUX zb!42Jf?YZayJ7R1Ze}qSyi9<|hteelGfk4PSE1+t>I$UgYWdbya2qI|ysSu8xslKb zF>ypa1~k3b>C&6*!yzK@0QDRgu|nbWYzkbVJImoXf|Bvai315r9O`-;^+~ZD0)WXS zC;m8tWKhd%5#Vv=m_YS7Ar$?E{vHbUI7uXfR#FZlPhrBe9EiwdI!6u~BGWk(j3G}+ z0SWcOo><~WA)5ODsH%rO}WAcTRT6hw+ifhO>WAbrPtTPb%09mGme z<6&&Al2EXRpkOPwG+FKfEL2X?#PmC~r^LYiVfvk@%z$EXCrNzB6KKyBQYTx1V(={l zNp+XIwgfwH!&rUn0PtGM3=o@wZWuA%_@D6=!f{affDjmM0_05)`&(dmU>8)0$EMI5 zKo+9JX?bWf5ZMr_%>Xi@E)$4Ob1VI(rTnjrJ*(IEh3X;L61gpaN5IU`Z%fHZdEGoy_bYQxMDSP&7d{#UkTr+3;0q z$+jZd+F}_gV9Een4tK$T(Q*JN*I*(RMIvLwpc>+?{s0S8G!CIjFfoa26Rt^;Owx;T zKZLYG$RCP3+Ewl=g5ZA zdmK2$r0WmOG|bgko`J&gBE{q}p5Y|T1>G6)Y4Hp**cOEyuKBY(OG{;C~7Aqrm_0D08mn zsJVM^@m&`JU6ynjSf(JtxS_&5Ks=Ll56Bpnfj1|bz~MkALk}CW6uK7v zN`ur0eONSiAs~p(qnKSRjmE2BPL$D=E9a_JOO6!cs;pu*+36w1(gKoh)h z2@A=$Q|i7~L8M86CSV#2+>!qeX}aAOd9-?{>np{d4Z0M1u9WXoAt?UwDL*dbf0UZ_%L2;RwrTT%OC0)DNsCsj4_*NB(kLb$J@j|&lV5(?_hQs8@1o;u}QIJE|LS&9%&mK~rE z@NfN<1@U=><^t-Cs0~|1%@s>V*(Tj(7m%r2;rNiLNB43Z2wrx6EtoRSRrkt-4pQsN zgboGShb_3ZzK<;c0fnvT2?|mI?vOl*5ED8AEL)UFga1=e2j=X8iv?QUeLfb2Dba6g z7||KOr8#B1^b4g*O@*pMF2(@!rI`D`tFaxp zUWp=>6L0{*&oT_)(Zhr$03)c(5P$YP8I|kdLrb=1B(BX{cR>a znU;_~NjzwJPu?P&6DV&HILk3@0-i~7%7`xP%&iWU4PP!wPIM;MM5%0jO~KWXcxkfb z2yYS9azx>s49|K(xJ6tSrRnM9J0n-*PTQn!5id(+PAx160Wd7ZxbNjHiUS2cR6*U2 zCUh%Ow_00IIDGR$MsiK4i^w&R`Z{#?NZcaHTZ1wpIQL8n=L9W6s=F44f%Z}G${Ckv&18rwoA5omvrow-O z`cYpGl!2lN2l7P0{YNC(mNKH(MCokkSA^3HT}G?|U}bbRR7j%Wo2WXQ0_Ek4N`(2? zxF(blxh6_yTRedozKkUNTD~<3ehuwn#9Q;MCj`rn1v|N>|LA`)DVQA$hLQO)%rlkO zgk-BA3J+4u@XEY3U{6fxiB)J&@OrfRdcyG_QA(Bp0A6sjMMz8a{F=aTN~}ubN(@PB zi$T*FDG`&VA>mn$^@P(e%e6MH311tzCQ57Te3D_wNJ2O8wGq$_Apa|mjQmi1Wpj{3 zY6LUuazY)sBuZ)LOGJR&NmL#DyU%e#q> z314YLb{WubX&FHZNaxNs=^1|7dSK>8)N~;672NozQ5GU#qfi#Wph(FmKVd(T+sda# zk)fE>Kt4EQiJL$p5`tk9$_Uw6goNl7kqTQ2k3v#L*yMx~BG?2>iu)~3lOVZ_a82^1 zQH@v7Y((9nJjnAfBr(kTw9GIHOhQ6(MJOS1MWnt)AP3&-aGaww zq#9PoKu+*lV!Cdm(8~r4?t^p8=tjoX01XQP`+|jxBwa1ah+Gq?ueCLW_0r`0M7%}B z{6v!iE_-AYj=mOlA89*;-KSVPOa;1}wzGMX5q5IRdqi|{vxi8yv)X#XnPnxNY+J5M zI@vb4DpF@_s|xETVKcOp644E9QbdKGn-qpVLK(?5p^V5ikvcoJd@`+Vp^8HQ?BFl#=AhLMf4}qExqmbM`{MN)qaf&y8q1GfApXeLdl*ZaAw%5sZ}v zmOp?1im;7@UsFtOWy6~!SA{o;TovVV z>#GV&N;u_7N+|&;8|dazr9|bm_4OoTkdI=6&_dBla#biLa#fV-)>jpllweMI0@A_w z-T>tm*GM1$zd<x++RC-j>QqtCAg~B%{G`C!n)ZFx%D3@EO%M9rz zC1TKY!pLc)j0}M7>zZ5XflA9ro2S7!r&$RqkWLb509=$v13(7}u_j7y$MREaL$o@| z$b@u~uD2E1Sd@{zo=~kDu1b>Gk{Ei53f9n971m9X%q}P;g1pC6nvSwbDO7I~`DRc` z10y>wlQ4up ze+OiysZhkc=zqxN_`RRVZM*D6NGxFq?~s1`vsyWOLWQoN%fddyf(V zH4Qvfhzz6}6=CeH_t=u0k0*Qy$?iLp5YZE8B#3mNzMOE{Z&613ZV6#gm(B~YMR zG}kDpEhk(V$wi@zh(1N&vm#`~Pc!^`BpdLgKB5}%463;-sVyhOI3K$du~Uf{*$%=Z zB*a1Rp-Twc?@&TSOQ1o`$4F|+2`4KnUm7k-=B}xpK!fTqOL9i+@MW~`mOz6VjiE0m zRE=$8SD=`-Z4+IAz}4ivM^4x!8QFp(8}RTRfg7kd1sYUynZBHGa*lY9uq#l?h+Gt* zvGqlTq_MI84rN3%1sXj0p=oS=IUxcjaY-_(M)d>&4^iQ6k!-$02@%bAhE4d`x`f#O zKR68#-Xggu)JD{zsO~a-QDNPpeK!Oe7U5%imJ?p2h67tsLPS3x&?ytQh^PF+*VnS~ z4sQ|BcxO;8WsbI-aBW(;8_lW?184yU3_Pv~NNXhV}rF#K| zjED|J){9>-8ST3j(6ERMwti&c*nOVDPP3DX6gJffXi!aM4o?Aw{?f4LPQEm%=gzPw zeXT9W1e9J3G(@?IAlay!LLoG5W+PPE=YQg^nxf|&5d06nrPqMf53=99GA8n8)(C!k29oZ+jR4v zcxFWNpFuT}F*tr6vTE38htG`YvxDa2eak3}JA{fT-TEgaL@kMGB6IRnDZ(B*ln~Kl z2bY78kiMJ{d~5N_aE_9+_vC^oMXiD6!qP(f?)fv6iCCzGwB>};&W3jgoBo8Rrkef? zs)5YW78R0=u)R*+A*#R5pgPC&<%CKdiHnj>5xpo%TjxneMgmHEnZ8J(=g&|k4rjSX zA!?#>H2lGR^FDpnW1a_Hjh%IPi-^uTLz!SMC8RGW+@?s`gow9@S`^herY|b2m$vV2 zKZ9x-)0Y!se3tI@6Z)E(4QKoxZEpcq)wcbAs~{+dNOwp`cem2r-QC@d2+}3eDJ9a~ z-3`*+-QD$~!U@j4ulIj{oClu8+JpM8vBw;9uDRxCj-LNAYt9 z?cd0G(7XIUSijqR4S+wS_?dQoBkG}|=$5OyYiR&bKMFvl&+flv53~E|ZJ&0>AKtYz zfT958>-!bLAIe9+NBt=NR?6%y8{J#J`=ErwT_LbL9|0i!ZV-0>8T+27hw{VOg!cEGA0TAxzNUwA(ao6lHksz)<4)6^hX82;FcCug^KsvU-{{}Lbax&C zz|#F3k=^e+?x}g09_{Bn+MS}?nGis9-_7P-e@4(xxbJq^%)bry{qoreI>e1Nzx+Iwn#{Q83p3GOuAc?ggu zK*N-KnjR{KeoylODnfSOLk~Ea^asrMD+d9B`2fLTG!NaU{_4m63ziQM7j|FG18y5} zr|32ZQ38PZ0HI;`6+Kk^{GR0lgoWK#^DrFN&m2wpdz23l8FpXML;2_TBp)Co>>p|# z;GxJZrFJXm4@wOH$p?rEyV*?tX1Mn78T$7iA0Qy?zM6;a!|p2l-DV&F93LPmjOOQB z--q|m?=e0=CCBcod6*j7w>SQOa(q7@Jp$tR06}5*H9b@tyNTuBRQCf^aO}RChk0o9 zD+k>wy3NsmAU;4q7|p*^mgNqYyB!4qs_X}d1-q~2VcOVF|J*6M^A7-k4-gJ^vn}|) zk%RsVeh&}_c3%zkO=bC;N!tJ8CS$++bBFK!dK3Z(-vfk#-7MewSN!vPdJj;IvHNNs za13-;WAHA10Puu>FtGcI9w`6ZL2`HP3jibsi2wSBng{6Pe?`wNNqFZSKzabob5Q@B zKYsB3>#kznop%7>ae&CLf2esd@7&>fw`~iM9srA-sDIvcJiK>)PwN3t-GMzmj`?PwD|8zV54e5JLZ#d+sQ`TlWB<^Z+qm_XRyv>iIpS z2MG4MujXNx;a@ixcaYxQ4F(`^5D@Wo!wLQy0d&X1-KHNvJRBg@>%N-*r%jFj(<8t4 zuY}L<**rju*L^h)bNBVDf!);*{G);0^o;j;K|r9_eN7LPg6=T5y9fe6<^dwS?yGsg zI>vX3?phciMF8fY?<;yJ|NNfI1H^azL(M~tfBq=aciX-IAbEfQFRFX!`$Ku>_e35b zvg^K@AHny3pP~Pif9?wO-TDUrkOv6vqWX8{$Nr1*e1NE~f2et|=($7jZoLD54+MmD zQT-gmK78->dlC;2(e)2C58$2K+7|F;i&8;jfwX{g}v@Eqr zP0V%34D~d$j4iZC^o+Gl2ryY$!L*Due=NWHTOF|FFQWVDaN3OiTHuD4O2S$zv=($6v|Kix6=dvlm7pH zOZLB|`~OzGxwgjrE!hE~Wk2bh2TVc#<*Kc}!5lwbd3&exKe_T}KLvKTS0&X=1*@BG zf)cwEYQU)}_Qjg1ZIjqgabH1#Y^%zwu7k2od+;LpF|A3rbR$FjcvIi7DC zKP@HL9~1XC%PMc?djU<8-(1CC*AM>VD*i(}-(0~>X+H{V3oQ*xJriRR%D=_-jqsI+nUW+UG5VAZcf+g-Ir)WvQmAW~uhaOaLaCtdJlk8MmIHrIz{4S3@;RElw>B z6HTojjo!l2TuaRe%=U4tWLH2m`K#(}3E6`_^jZ*FJ`Jd3{N8M7qh#G^j+_7r6iK%< z3W%~zjsv8lx+6aQXku5x^2@xpA9fw5>8Q{}24>!nS$ut_YpyI=8p(hIrQmT`Lcswl zi*RVP&q?iP=P+F+!8M9t0A*endgo% zrh?J2vJMQ8u;aiYVc5QNCtY0ImZky`k1Ga;!zhrqDH1EK8lTo*o}IK#;@P5=~!{~!q_HJ zd=n8!0@5G5-?7w!fqk5NyNhYuE=x+J|Ae3Cm@;xcLq)DRqTxqpavNHA-O8^x{Sm!1 z43u}B&0l-1U-8RrVr==B*p)Q5()#rcb~OvFU*8lLWtWv?C*m-%GS~YV(l@_yvzkl8 zT+j3-8Gzlih+7bs%lIbD^^A3HT7{a1xQUUP@t+oMJ#!074qY{K%$sCGCa8A%;zv0) zG8sKhOI-_01u)v5OaFfz$uWO?{nK*y>c9N@^Kbo^Kl{gj`VT+I-Q?(h1hAo+ zj>XS7{P8pF>?XFD3M4=1021n(=c+fX*G-{yat3lGFfu+%wVQF4L(P;|OHW7F5|iep#0!~(rIwL2=FQgcWU{}^VAMCu z0DhWzZ$@K38i?)BsK=zg8UOv1dedQOQU2&&e%&AbWq|n0ba%0T_Cr5_i<^G+mw^KF z=BevV*ZA9xXRzwIqrzh3wu>@fm`z9V0#}&UW;CwCR5-oaT;nLmmmm;4!{-9D*6i%{ z!URFUPmJ_lXL1m@*QQVt!$5n&>vqsefRs`_?Q=D+c$>h1WGtW;^``xjbs3c}fYKs0 zp7Ofq+CtBz*CAl>IA(vNXPd^ zzz9P>>TC^Zo$LsuWle%rL0U0)PjP-kysKw8n^`C6^t{P&d+%I2w&+4|etGO2>$$X9 zJ-sxGqlZka!!F4D$eEP$b*Du)Tf;@!C2BXD)z%)w(qWMe{ZV|$#YBPMw##9(xntd_ z0U2#Tj#EI7OW90fLFaY`MSw{^>@}&F$}oSCKoQm@Z+|;=SEAfJb538+PApT+k>0Uh zK%0x?7PRxb{%Q;V-P30DbqG*rOVi4QsNhrvBRZWKX$JhZ9o%!H8|#cG?(l+h;x+3v z@o1JFpHa-dbXTzCd{h@AbBAg*EuVnFyR5kFP@MS1ZsGnVJ&8ieEk7j`hB>CO0zpeu zm8cVguq5ZM^e%?I;pQUsY?+t^?F*_L*-83#IEA9DG2AhhvCWHf#1_}ik5SrH3I_R; zIZvtmbG|NZ4{fygvt$o+30P+|3~vr`S2@_)uj{22Zw^VeZ^xI}Tc*l4yW%!^4?V&* zXlFA>rJ-Cu$@>m{4SyY0renO@vzH_9ydW7&Pd$6J%qMCyOT4*%_i^rk>We>TGpIdU4TyF@^=YZDr#Y7#4u~Fqr7GO zw%2Qz+8N5aElnR)8aN!9dh~FqzMero>k(d(s3vM*dt^Szp!!Pk{i6ub1?5GHXA@5r zj=#PqRDJAQ_oTL}tw7hD%n|*{eHY?wcHWCtW^CCY{7?>rt{Z;(R|wZO-K$($sc4UGkzsA!q%lFG zFj^F;IN$o7VR+pZUGO3ev1>BD76Uw8-$Jr|1>ZXFnd{i=Ml1v7L!3?h!x*iq#pNNn zp%Npraq;oRVo2X~UuRvD{A+>TBwTQQk`y)~6#VV9Tp7nK@eI^#MU_*@L7@Pl!SG?? zY^8;m1>|o9!zFaUD8|My3vpY{d8@2F=}W6!HO6K|7gIHFzC)jA?Ua3|niavq;EDIv z%NrVbnwQgD6IEJ$xVMlmDYs@|G(JYAdu?aW)#4MN6w9_cHs~bLQ#{pjCV#$-j)*=0 zG97uayO7=n`t`!Jy~hK^GmwK(TGO~73*Y|HbAlml4cb0LOi^+FEJw6fvqN-e#RDWB zKf|-u_8x)h-Xn8-Xl;S5#r_YDqrqsg-BE9aa71Bb)%ss%7umn8jxWn9)xX{*%g1x) zho{kI87|M(B``-|?{5CYizB%~|9Qw%BRQVn&p23fBTg}`yTuWi!&Rem2pJlq!Xq>}3 z>AwAXUWBCYNR}?P+6t(o-^xd-EJ&rsR8=ZNR@+M;ipwXhtm6~MDwIA%De7+)uu3>{FL2)}u zgp+sZFiWu|(xSvXll~x33o&E8^U4;v5_sy#nS@jAjD;%0Q&+P=8=J$!BTkNY6;}17 zy0TU_gTrMTO<=FVxId*i>xjgg1!%0h=cSy7oPrCp+SOf1urk`YzJQN0lvCWut#E=U z@(c@=97923KAahpVLhppk}bn2%gcWnNC8o9VFc)|q3@7JJJL0j&Zm4UtKjEJy|MUSb|7Xoo8 zqo_G+T3R|{{R0HK;Y)ShzV@;-vRFpy)m4=6Nr#J#{fc47q_)(M90qE=Dbos)HpvqtPo`0>JoE{NiBo-a}6AZg{Yo!=U=G8$;ZLEQI=cmgQ7~gHt2U2t3X?z!x6RL~5wOrSkqsuJ{ zt(%j(v%fZFuhlxn>9hsUN+b?ucUEP0d7n$J4rN?ho*v+>D{Xxm9j6T3w4>kuoD=J8 z{+W~9+~T^e`+@NmXKUaw2c2tv(BQv_Yl-S>Xt3_EWf* z27Di_)_OJq=CFA5$_q;q4X>~E_JV^g(_-?j4bzMA2gGFxYZr56hX_@;gq~h6mnHPB za(*^2qcXSGphL#zRE$PN%#qg124dw6 z1?;z8uKW^^Wx{f_ZxopcTB(doBVk}#)o_r-g<8gtMOG(+YeR3zjqTzVu;WHih0T_N zOKVb;vLI8%)jqXZP_g9}r#ABW@X<57z&A}X261~04jHDs$ zj*INlymom-%8c@{MC1?MK7EO6a~y2e1M-r1j z<^ohLeS-0VOkF+g>juBiYJroOdcA<93FPl6e&k?OuC#L4<*a{O}kj4R(*N| z94L6@_kAXg40U(r6HO)7Dyb|4?T4;qh>Je!!%Zv+wkVL`NDxByaW6%*8n!*0{Zt%Z z<)<+XJuqZ0GuMTl$cFUn+p9AfMB^;@dM~dp(2k$QJ)!hQWMvxW9VErgNn^12;vACO0Gsv1$TW5@#S%k%IiV1QjD;n)w4gAxfbg^(rY4}t1cQyPW!=X&;+X{V9 zff)8U=-aaSlS6uW?v?(tee;<^q8%#A2h9x^2*>i%@BR2I;?FPPD0{Q2s>09nlW0%G zcA2zxa)~<{C~ajyAUkH%keG?xJa0@d(8KC}8Tq)q2`-sUd@4kCO(}(`grn!DP-STB z)VpTV!xhp7$_VZFI3cAqLsZ?FkEkLbgn^0baA!bFnQWgiLnuEnc}x{7SUc_Btr9CJ zA*E8I&H1^>*3>Z?IN<9Of(nkO`R$T;q@1CL)@;j%M;R&Mr19mnnmK-jo(kzP{nakb zN5_hd1_B!SMhk6AOXxJ~3OXvop9#&nc_BQ3x@XHwrY`3@r^t$O zM2b@qr9BT3RlHgj-`Su!_mpEqeTMai;ixD$;y1Ul8B#7wC`Iw90CdQcG2RG80m8l- z$V#ztkv9n7ZRlfUyIw-A%LOwP4Oi?HY(uQDa4ZDJNaN=H9yzzm&WN$;IR0rd4rq&E z2H*j^g9UT~J2i+I6GaCpFZ~ltqgT1T!`Vky5D^GxWP7!u%nI#m@NwtxgTae~1%c`! z^EAAo6^7A4pVx~(-m9whjp)P&ztP#uZ;0}GCOfGB=Eop_O zbh}g|S-2y?(UVJ(47zuszDK}}dwfuB@ZJkX4g{YNdq<74KI=5Xzx!m4pBQA$=g@Jo zvCyA`vF-YXy}(d8bPlg-Zm{WsUlC_YspJuin@zS?Bs`JMB63POOHdh5C&4hZe=vQ* z=N{5LD4G<0gw@xBzLoQguLHI)kk<}HpGT`L@j?^gO2z7N5YI=hha_Pm`Y6TdSQ&8G!pb%;zz z!Kb8?#GP^ACWM`d^YCF!6gD70z;I)}3Y@bcE*AMl?NqKHuvcn@**1w{m->!FiF-Yl z!AdS#!V_d5;jxwOTQp?Rf1H+grMU_3JmoJTFPQDgE%lRN;Ojcyt>R6CJI9pEgdIUa z@;;*0muBgQCUZ}vFccx_Q}8_)!m29bHMxJv&)%iscr%3iB9NmeWytWj3%AIeyS~71 zt#%S(=9W7)IGF?gFq1+Upg+|+>1KA)>u7&bkqctvlC}3)TfZff+^Jb7!pGH^3Y7~ zUDLa*bV&L-33<+;p2EK`C5P)U6*(LI}iihC(hK`4;9K7|@@63){#v}2FmT)zE zMj>VR0Z9TuVJK9R!9cqLIa9ibL8HKfqwc-9mizNC6FU~I#q8$vwRC8nNRminaF335 zLxE_8i8xg*V$5UX#i{86Z9iLafeZ_41GIXk?{$+UX2(XnD1+d$f*5XcsuZ%1huqfd zj8vIMo+<;IJfTYLlrFVZUh1pC4W9a%#nIZL}_|^hju;?vNU3q~;;V!!??W>@t2Q z^X5>Q*s={DB1#k{W0uS8aOJD9(diA|i^);Q>MJ%3`CXfl2${~46ftn3p>K`Nr4U=C zdDEQa+!NQsJmib1i00~zWN?ghjp|4fV;mn0+k+sH_Fi+aYPNd5I1PG@LAk);;n@1i zSe^Oew3Xj+B4`$hFoMcNrnB4&y<|qsd$1`R7%8tY(E%s}eqg)lF`kkf_C2sOght92t^ON*7SrpRb6{kpZKi|t2Hg*f^W1r+e8tcPXY_q{@+`DC+}lE0TgN2PpIk z`F?^?$O%>yaX77^BjQF#&}drsu*nLQY-JoC+Rh}WMQ9|5qB@KvBkizDM*}_~6*`5ao zx05C+1=w03`oN${N#nIzQSuk#ELK9JgKB9xjzt`axlBTWB4Hc)5SliaSevQfJPTIv z0qGEVf`p5&<@{;f4n}2JnG@v(vynQe4jV|R>(!c)57|TXPFR zCbL49)K??gLB7N2KcncVNYibVfjdBRoYrQu)AR0H+vUt~Oweu5r`VTON=(C5ToN@1 zEh@?&GIom#U3`~=Hyd24@v*$SxV{_=Vd>r6UJ+q~n6F;)*Uu1EmN}LSz1HhAB(85e z>M|T%-<#Br%msX`7sG1%#z@YYZPD9|3Tw_TI@I1+*Yk8r>Xha3Ooh<$&3Azj~-l$9ef^Yrs5j&QO0j&Unb%Rw_XSxIn~VO;f!I}#PV zlblnHNohj6Q(p>Rj{m?oN|<${$hOK(0~48p?Ub1-CpCVtsKfD&(U6R9Gw+1QpcgEX z(OJP(3Sop2RO8XUl*Q8o#3{Ij3UynV7u!CwN)(ku9yU!o**}eQnc)P@9kA=l+{7Bg z>-LpYt;BAflzzQ(S-;!P(@|ZAoEk0hL@p;yopmFv=@*H| zCT2Tct7=PG2xxt&$ZnH!8g4dm8V*Jh0}o+b4moY{U6XyOGqa*le4fDVsSZ&9gT-4< zQf61o2nRJikjgb91*{D4@O|jn7;>Aw0kDz(${0UWF#J0Op5(oWPFQ25nwOkQ^9GJ3l zBemmlllv4l=mbW~K?sje`b|3BXf2?`__R5sdx3IsGf*l6MgMHBH?A`kJb3k!M3|1D zZ@9=dZ+F*)oxH)iCSL5bB3Ie^Wq&lmlATonPN#UsIp22tp(oK4(!MkM@+XSyvxNd5 zHB+WRlsMrM)n$?DLZ>O|A9b!2LT5|Xhw_(-KbIk#<;PV_b0-QlP5EVXS1z9;Ec#~b zYve&vOo4C-&7EwZL5gBA8qv|LNV}9z5$<1R*r|1E@`~eE!QkPtuwd6p;ve`tuOvzj z6H{9u&md-y%U?Jp`#hKv#O0YA&eXRd9(DJL+|=iiRAI?qt;a!5m)h+wB5b?hQjx&&$G&=o`WXy#knOsohes!- zzRAue{Y7RB|EU$m~lCIoH1&0t_A`9DsuOxIFKO0wPY=7T6BN&Z#g|g1 zzG+EUx;ci-nNGeP9s|%|W;k*&crQc0$x!WuTaV0!#Juawf+?b9Iez5Ep!y>72}HNg;bJij^O;s~FJ^5I zx4$2r#e6NEl1;SWirT4Q6IsN`cILpdqu*5`8f{;s1NCgQhtl(jYdVs5SlE@V3*=I{ zyi_{Xr59na!`ZA`vDJ#f$1w4IPKC|ZB3*-sUIdk6cnBCGJS(|09PbkW>b|)CEO;od zu;))^!(pzT;}I@$E&_8ziogqXMD|?CHO|_5K+Bhf#bc+9ti_Oh++X4!0pyC9@D~|R zTyO`E7@NguX!O}*mKA}Paa!rt?D4I@o9R##2P3#62VZj}cxEagXEZQ+CapnV4}|qh zU(bk^7i>)x^Wrd+72BID;Dl8kPqJm4gx64CXL%~68WqV;pHAYP6HALN$od{Kx-dhx zGe@5uuRB-S&Ki1$f1h6+8GC2Tz(o?;UJ8Lt9ODNi*PB-VjCCclXx7FGvc9UsVjFtV za#yZr`O7H-O-qvg?Ax11HW5c;IYXz1Ni2vQ6t07B79pxDAhN8J9({YC`2>9!67e(WYt{X%s{w5!nCQ<}Na0QUrnebqEP@l| zmoXnC1}8}fSD68fDhOvc=~n)QRT`P9UYw9`JD$axu{xX{>@g7FJznzBjhk)4f^PBv^;6TWDD`?p zoZ%=X`1+&Es-NycW}e?zYqB#3?8Q!hJls>Z#2Fsg3$G6tVmsJi&YX3D_$N?*G1f9; zg3)xsYde>1A`S<6*+X^?>|bDGqjhk~=u>N7v~=djARG95xFP9~qY8k26;=v*0h+b` zAwe{^Xz@FlB%j5|_91XXSLCGv(5xs9w9iRaxr|UFCDKH$X_}s~Od(A_+-B5zSEoB& zlBbJW#o!UPQSexGcry!=UfDq5+z$5`RZ|B3iQ2&Aitat4`*X zFU-iUN)cQpeA%{|FplQ}9d=uOnN5a%y<}(>{A_W-+`i`VF@E+rVVnw3xHBTx0>O(g zR>Y=DF9@FfMhQH%Wc;A)em=xV_fP-vkm!-mUkLh`GRqP{y1c_{2?Vk8v4$$#$Rk=; zBUR852xSlxpu9dTT_DQ9OmRq6D3IJ|K(g+SiR&N1t3C21f6s+ji}m7{S?-$oy$kyH zON-=VuUeus{bYAvwxj5)k(E9w%M0FPBPUfC-x7jEYuYlPtWdZjH?|7QeL3y$C^J>d z{mOh%i!lVlO+9=9A49plg=maxThsZZ_!FSsrMH-UF{dTmB1&?(93;}hS0(k!2KtEf z1`;}kQ3V?#O_3bi7livrV#No9dIyBPJt_7t0!ca`9Q23^*UODh6njm*t+n8UnL}1e z{YCO()#-3;R$P_UE*;R$jvM!U8-?a2=$YGfAmc5}%`HOEbfL>$3$$xd@G(4Au)rHz z;2d{{vFleW-*((>V5X3B*2-5?WM|&#d@^rdsJ3g7y2mMJ@twrxCC&o8!4RnSdog1A z$0e2mk>HeesfHB7MCF-6DDg}hS00}fc2OQ%wfQ9k42@nh_NvsFDdw9vN86iN<`x)j zq!gPb7v<#Ho2OgV8L03dc5Ze87oTB2SDA7;hcK3jgL0}bc-0$)z zK4Zp0Q~|HZGzkwPVVI~Af<4LOxu#Xeb4Sg9om1sU`!f6sD7f61xV}!@5g|nb5HS?y z@NN;W+P`gs1RUAQ*$>E?FujRnUix0rKdZYIp>k#J>_9!H%>aQnOnV)!P-SLEKBhn( z^huOHW(TcOaN$^S&tJhVyD$^CjWM)&Owxbr6Jt6qOYp}NmIYsE8W4tH>n40QK5}7l z_=b3ubBaZR>zS|2iy{jdv(Rjs7>XMaBcsTe>tO^0tzL_~Y7FAC0kC1%AOCqOIb@!k zi+WKEtC^psfXRbZMJViU0zbU+snM9XBOTZ)OmOLVUm`&^I)w!7Wy+G$enW|flcw0W zDHP)uVIzjt4wo{*Q|4t^v;wsp!m^>Po( zGB#-m+4`OOd$W8JRO(^{tX*&EclHSR((iOhrRzlWk%LUMGVmxun6zb&-+@@vLFXbH z0!P=O&8X#qRz5j$4XllKT~K?+I)0eWMI&W&f(j{vJf;)UE* zPU6cXI|1}eS%U#GGIrb|dI|*Y_jJO;Qb(!k)QzeK%l=%&9_E~=t`G^+2JB5-nLdfR z+ntmujKL{rRMMGSux=C~cJB6iV{AraABIFlNznD-e0Mi}&bnbCs`FIoS;`14n!Jy% z1IN^yt2?)?LbAPts?3kL{ou@P<~bom&iN z5h;9Ret`wKj16hFDa~QR*@G|jMd~?vZxePoJxA7rpOu0Isvi6vtqA!Wo~JJs_x#P> z9+$ssPu1hjC*k{e0LDv+2_+9pUuNjnQwp1L@DwK}Sa;)ETm8w4QV0b}fQr-Z!kGK%rc zArsQl!J$+o2cez(y3(0aWxXSaL}Zyrj0Mf{_K8}93R1C~kS9RiS8GciQ0YRE(}Mb& zQL&j0y`T7tzNRau(YRl5JemDuZ(`+Q@csI&gHFoXh`HDZ1{h}*DA``~^T_$+Ts#c> z>d-V|2}oAV`mBO2Z4>HOdmT@BXLeGHyLJqD&j)g~#2H|5yT&OSG+;k9SaP6_`o12G zZe*HdJ_Pe^r!b2v?)Law_@#^iia<_sbr%#;n5qYMUIu=Na4 z40Kfknqwe`21CmW=XwkNvqrH60to&j+8J#SNN5J$!W>LP3w}|lgR+W)`PO4tT#5xt zGUPRMzY1Zaz|z61k&Wu^A*AamsF%2)M9i_V5OEwB(T}}tb=RIh7Es!oi5)r%LQiSD z1U0BUT-+Qo*~N!9nwnJ^$;S2k`~w!x$8;zz&4pH&=hbcn*|42RN-Y`NWVXvu{T8-d1>v6P#0ahA)1EW|H+!BA#m|L zG!P7AFUu*|G_sBTDQV4egQ6IH5{2#lkf}#mIsF4;*U&q)^ zW7p^pGSGV1YUXEk;!9BDPjU8*ie5l2h^K09F-ay2kh!D{%&J`Xt?1;NY9+l9{Qzn! zwI_sU_TF-Qw2c`z`m?NxLoD%DNeulXK~ng-Cah9fUSpl8U?7K=BTzs923#1pM!T`;K4 zNUB_NH0Kunw0=&7;~jRS%hGCU3suglc|60H4i*Q*LF05B+vlrsVfx+!2w5I4lM~QC zVsn7@iIPDa0y@ajxZCH$%BDNO$gvU(Izb3&obj&t=MVM_c#{QP3Je;G5+?>mhrf9} z=@p5@K1H$^ecojvs0qVm;ZF=q%U8lS*`z`)56{ChI_fz0r8-W#HIO)>w2Y5WyxLaj zk#Bo3d0LXd5xPDua{u`=sMpxS4NVF9J0@FKtfxR)I_ox?M^$tBvH&<3N>9cHJN^PFli9Y4%m5yyYWD!%iiZ<~udQ z2K^ycdHdF*DV&6PJ})N^__v?!ABT`w>{K075_`+G zC#*c5lPA<}&pL1ApIj@CDEg?&)^<2oP6lm2&azAcl$huG9N7Tgbss%Pq`DUmy+?F5 zt=tf}G~ko$5>ahpwZ|H=KC!PF`sJ7ejjyv2-LSBQv`SMaVacFoV5EeQUzLBXcBU82 zf`$7EMtM9fYmkQBquOLXO}W=amjUAr+NcGG?O)yZLu((s zKKdA|S8-UDpz&2%XJ50MVZF0J8nqH1L4SGz6&$20q1eRPZqv<~r;=eGMui$SQllj8 zdoa4;5Q*pY=ulU63lhAh6>MvIExt%_m+E;!$yV3duJ)yEfpeuC+f*io$xtBOdQ0@- z{H^&a^XE&HsTb+3#=Fi-R^+r*TL>H7=u6!=R0cyHR%h!KzFCUez$?YG_`YaR)&(P^ z?>3(ahKXu(_AStGGH32+#!y{~s-hS`M%sc9KJR;L z=09D$`H_N@3-g<^g>(;$)wUyxUtx7&J^6c;|`t`_|_Txfcb)C4KeT`@Wfxb9#i7>a<^kfqLeA zozfy3<(Bp(ix7S5P+ICKDVz2DCW5l#v<4GoCB^cRsKU2`>xQv_lR3@H%%|aT)*JbR z;m6fE)4S!v%ULFCj!DF@(Zlk?gC%OGAM*5N8aU{-Pk2@*DTWY98>OxE&%ae-;b*mK z5XI|xeQrK3S&PS(HbUwaimiXtJ2>$1g>XmQsmf@b(`en`*?TWnq!(Qjl~^cW&8{hR zd>KlT*ZLT+^0=yP+69^Sm5B+1k!je?4 zlWc212Kr^y*BqRQ$kUHS`p#I;M^YJfQ0-NbN=43aFL9TMqB$`}Q-BlpVUKhTKXxm% z=ZDL`$jB|{;iaUsXK&1(qcbN0;R}d9&{$3S+TIPcSb=c0el{X@3Bi*E5@94?tDtree(t~pNduil`5zE!0m>@zZQc9J-(TwGPWZ?BJ;Y7E|^rPu=%dzTsy1 zeBwwNIU6c9`%Bo)t37X}C6k9sA_H&Ma0!Clb1C)rPfc+Hx8Yo(=!)? zx6vEnqEwz`6|A08Sd@xY(1l=(9ut>4vceHe$&O=b8f_)zwdX2X_Y$3rEuZ@cwSfuz zZ1+iJbn?v_SPxrqz31q2dR()(AGWE=yUu2mFe=yGY@BwoA*T-ODp+lYLAU>6>@lU~ zda?F?N7ztNCCo-@C!4UNf{@bn|M#u`JGx_?nzd^kzs;cIG$i{twQFj+D?~=zfjm;@$ZwPKwgQy~D_? zh%a#aB9{l-(uQow_vJ+BX-LdunMP;}+_su~+;)eottzvI*mq-aFI1`bGm+K6&4{6W zK&V=@f``7az4}a;A|H2J5hgo<0S@fOF9!7rN#VVpl84{nH=6vW+LR7iCMZt=jjmQ% zD#{^0KN{@+a5^xmm7@*?WJvu%V?^t<865MG#nU8te7Ht*7IVlQmgj<2E>K)kiE?(n zSSDf%BtDf~pGxre;BQQ=3b21OA|QkY3d=^Wc4)a?9};S?bGje3DQ2C2d#Wn2wSzR; zNlx_k6u0F9+FxlJQ!Vqn4cb4C`Ll{&r?kBwP>3&hK0JX9`=D%U0X;*ow_oAcWha(> z%`VNb=VMbC{FVX0&e!3<;*=$;Q%~C#7S=kG&g5}--^-BXzVK1$m63%{$Gg}af@cvP z`FxtN6GtbK#}iZ}us9+xpsryhM2H137@UL>8Rb_1^&!&&iMC#LpYp7=72NWWGK0u& ztjRrsf@3=QNi$q8hF&>7t$CojIb`{`zqYUXkJEpAW{U}K{rQ<04Zj-lN!9<+d%G&` z?nPYPedt2F=VO(?M#>*($=*eWf9VqVzVyr7R#v&7sLY|c`C`qr`n4Cxb4R8zYOV=) zy%S%lFEyz$WQCR^>zy|KpXLsTthge@Kgr^Vw$+5SaeR&~@{AMBk%H|C049+lL}jv+ z$j|9DLMk4tRvs0Fdf7Op-VXP&75v zcf~o*l)eT;pV*zZYTyB?ZE>g6mQZU4*c;aI1GBG1Y9V3pCo)aBOJ%QCKpfNFq%3&@0KL)uws&DcZHXlH8DEWVcqXbEOr|vqIOsn zXE1fPpk6RK(p%fXL(18#>db;ATvVbEZ%h2>?ZlN%`rW``n=GI+Ykjz56y@c0v$fkH z0hm*jJ{&6yJMVPOiqDCN8NBe?)dIb^;0mB6Y(tj?CVrIV2Tl2{2Fx3sB*Lv^!*1-) zmr^}~3kcaN45vFh)7x0V`HKx9w7UNxb`dBXqzgdPRlUMWYzjAZM!`=Cqw-% zwmN-hv-_mVCow%JPRG5mB39i?DL}{6o}2oW#oB`-Do{SqIw_mm8+^%M>1>(kbb+D2Na zN!DoakL52~$Ep3ez*ga)8Zr@Ts5NVCGnV*S$sW_aAFtzhJN4)S+7>*5^2=+WR~}3V z3Q09oJoF}>`+H!hZ_ohT)+X~xxf%<|;`a2!(JQ5E<~8b?zUtAV#gbsn=U-IFu*e&L z^h8_9HB|6j zZpJgN-c7?ZDPJY%peb0=x;pM4(`1dLyQUXOB3>AyX3HryGow=n4_5;k%Y95RsxmW| zy-H6$1-Z;VuB2N5Ze)|NNm@AhvAE*9HNx1|Ph*+Ou@i_3aM;Av6N?m<9u5lvIu~{I zX2ObE1KIFFt1J+oVNC*MpeBPawh*v6u5&b^l{cohyU6&zjQ+xAO=mTqn7zAFz%ZSH z7E87HmJV54&6kLDs{eX`@f7WLTTXFBQ(;UcCz~I(*_Xsj8%VWMugukkc`*Q^aPYqcZivgsilo zyc2%MsuSbJ9IL{pc1%}&c*&bwKZ&xBcK#*Z<ItT|`l#9m7Fy}=&AXbTW1P_T{9JtYDkzoF$Jj;@Ol`sSq?|}9I~Z?UK2>oFp0b6?rTlC-Qbckm$wm5bRVsi(N6y%i*>TjW zV&GDxV%bNxpd=}wfCp~dL1qwr$Qpt(r)puH?zvFt(F=D(!aXP{MY1;2Nc5UE2j6r{ zBYd}fXh^@ zg(^1MLaDf96sFfEAVW!YHK$!R+)talz%_}p(ACY7t+6uVShI(+9Qaq^`ed4|)}r8o zDv48ZUkn@{M;*|WRPWRa+YJ+#q>htCg_YLE|pQf%vzBiv?@_x7U8q zn4)jh&`b3j;nCH>A$Lciw?7oPN+23`$-J z4t#Fjr8-Jd3+%p!Jouaft7Fb%wRc+Hd-WZ1{veW8q|N5h=CJc(f~O>ph3O0itx91z z(?VJ8c~?iOqs8bH8#*f6($FdT_U5kbaHqUM+vTw0VeGb|)gX}Pn6Z3#l#O6|L%6k} zz3KeMs*Cec8Hd~FrGYZ`gwl9C9^0OfC@^OY(ZTH|rV+lUoX9xx&@qdO)9*?Bvn~`=eVC}4rOBt`Y2lh5TyB#4=;G7D?QDfYhRq~$IwnUMjZobzmuZV zz@5NSD5ua+I&b6W_i{77Av+$%@!KAU6Uq&14 z*%R*>KWIZ9oogucxAmo!*sWpB`hGIunwM^7<>xTV_}!|}sajhkEfZIhmNJCsJ&tiN zaX>f>y5;R;bj=MgV^5jk1pWl4lv~?{PA~z#nA7OqxOl);pCmqknDiEY4;W*+V@~AFHsIjx@hKFGjaHU4ttOOvaapi}=`f)n z>nK;JKRZ@?^gy#NDjoOn5s7%o5zZS3)n|^e?@gRsX~ljF3qM(>Rg`e5{Z?R zwn}9~>vx=!3>);dD<`SkgcMxF7)9+o*u$^c?J)%YOC*m)GTHsH@_lM>zKnjYbqS;u z$rqx8EhS}9jo}d^nyDbcYk?V*&-|*}-K*dASwwUs>WNmdJG$Idq*t09b1#m4L7!(X zGrno8QHI$tbUw0v$K~pF*a<}5THac6l|0+t|5w;qfN5E6ecu*P43Lr$hLUEUex895 zi{>CoDBTR*9fAxf4T1uKgbGLqQX(lOrId<-NC=XONP~pT_uKQHb6)QCpZEGa=X$Pj zx%Rzl?bUnj0~^DmdyUMw==cZses%eJ)9$^i&S*C6V$XLIzMHmpPl;x;W)*JG_2~oU zi!G1MtCF+J!Psa1I*Er144*V*W@`V3o=>eYCi!sw7u%27eqd|muFZZJl6CKi&0l0J zJpV+w3HiFOo;u}PIAP-DlesUC?o_Kw)8_R*57nvkcgOcfjM~?x&B@$DJ0DIC9(k%; zi%^zPm(ESDQLFT(Rl~k}<+X5y=jQ!gY3zHy)+qmFv(VC#AnYi7Zp@;z6-*^bXY$?l)^&WX(PdX{)=`-rutw=JqVfAfg`59g^ewpC>JyDML7 zJ95*D2Z~1@I~QK~^6B{n^KUIxdd2Sgb^GnI2_=U+`axZ=NkqT$Ny)b3b!YQv- z2`xA3+zh7C}KQw;&?Z3Y0e70c9xNN<$jvCVH+#gj7W~eY^__!Co z>K4j9a`Sy97cSU-ve~$O*^h6%<}UA>## zuJ)hxe&5f>cdnheDer)}`P&Wbe(PMq+RnGW%sYQ} zr{uD=f9kYuURAvA?uOs3dZctO2P|=LK*sxyw7O6+9(boLqbu#1wfT>w$E)3bul>g#|FyOH ziQLVWJy+p;&k@63iJX7&Y&zlRd%xM2va|b-_1ZKVyDR&*w=*vLvD{y+i+y>Z=Dgxx zE(sM_-G0Hs9iM!1Xx;l$r*v8U)9NL|JC_=?_3ZeI(e=X)wq8Cy%Z73OEl*B7_-gw< z;xBDF^z-IoOG{tg+d4FU_s%heSGOuXEx0CYmrR>xK0f{0i20=#eD%et;ora*`I`L)2`1iYW zy;8U8uvW>l3tXJrx6R(>V>=zW@^{vU^VVFkbIOc9$7a0WeBXkTJxbkN@z1~;&n?Y* zZQ17qFBTd2{0}|LmQ9-1u;}Q2o$Q-bZ2IKzD=X);`u@zC{(ZZi{VKWe%hT(oPVV{1 zsJe6d&dQ%@)s&1g`>+17(u7FYa))l_EiyK==HsLvzZ+39viyav-@lw=%WuO^G@rSn zOtXuP`%S5SYfjaPQ}^d>9gD8Iu;#NB$usNi2#@do)2ckHPW+ZQc-+RKr#roSdO*th zHa~9YR(;u(r$_(t!`^2{@BQu5*-?MH>zn_a^YPoChueJd?C5>7zMm4R_g;l|KeVp) z^uo$B{|C6e=TnC#e!67#(#eHB+|}jMiXmkleydHP)SK%fOU7sE|81U$!?BmPZc2*o z{Ac!!toLX6?a_Z_=sfh~=EYN=SkW}Rkj>vo-Ky7*Y(4u4n9J>Z9TW_%%kKKE#( zZ_b}TD*AYdbIUI^i)*tc*U{JOe(hgyw(+k+JKk(Kb4ASz`!_YbSh`Hz>?1yEcIoxf zZ`LjHQi<&OvX6XXWcguR@^5bTWV0n1med(ir^Nq&7ngXdY28XkIT>ji&Q@-oiF?ss?<41kvtGM>N zmr@Vbthnx_3&US1_NK+u1hc%T=Lp zu>rL^b)WG3eJy^-UbRWBx>w)0_e701FE703cCK|*OCPLPKiFzupOcj*PdruU^mo5s z-`U`LFaI063M~A7dP3uz!)K@L3l9EqZlHS7;)H>-B<{&YrwcwqNk%&d=ul{@10#J0}e~Gi%>_HI^-D5S^F$OhRDS=_Rjste?GV zgT8+1gZxpUZn^wt7w!CMN!;wqS8EjL6|Qn5>%axO8!o$cXia3>OW*ICQQ^&V(-w4j zHQeyaJ{t!=zHfBTEm>9!P5NU~@q{Bws~panq5Ri_+6*0Yz2dB4fzpdo`;Gl9ZDl&;Rn*;D=xP{h3Lxv@2Sq^4AlVoT|Jj`AXhNO{eWxyt`$c(*Yc#`&K=A zw!u3+N_}1W-{AMJ?dH*bi!91=qI<55-QO$mZtkCaIlf;o zvh}|5p|km?^muaD>S4(Z%PlMQN!@>g!#m6GKJ&w_(VM@?Q>@wTEl=$FdhHMU&;3ww zeDc!|jJXubv-M(wQq4cFdh+dKMTVAH^3rQ7s}y>pdE@!J%hgR?cyGxKg$E67^jF~4 zH{<7bZ+5cMz+)-9zxny_-lDDNZ{65>|EOV0=WRIA{N^Wv=STCLpW5id11JCbSK+-K zKg%=tX0t2*bo%1ZhWf?dTzX%ZBKz`Qc>GNNGPnKx``y2;T8)+WbL;&2=t{q?To5>O z{!YmPcRJqp?C7lD#qRyTf7(pgJge%!Di4PyKJ#vcY%ji4a!$Uiue8`S{9mzg84CxF zm3wyT_90m(cg}w|aObx}-YfmkgY8>6p5TybT^+JE;y_Icx4Tbmu*SFDFG>&HFT z?pT_zVNJCElCAG9+}^YG(7q!oT;9Diup{Tlyd@WmzEyJQ)UKJnDm#7Xp;7&Jz8ZbH zA6a~lAGkbe^u{Tf^9(qar%Aq6Ul!l=(4ZFUhj$O;JpAMj+1lT(wf|<*oMop@*xs~Y z*PO%SD&!wJ?*IC;Vg5qz{!uai;JE8&|Mh;bVS`m8+qdca*MeF}mj{l#GB>un{lmMf z?tJZBw&V^qdu6G3dhOEAP1Y7Kb@sJok2HPeP?f^(ZaLerY`+Z|J}5P4<&yq04o{r0 z?8#LZ&BFAd3np~n;!<(zdC1P`TAu}rOsOM*~}hCkM67FiyO6~T*}(ZEBDXI zk#F#^fp<>7P$c_^8@;O^uO1v*?(aUnxM2gvJeqm@*^cGk`JwH-PiJUc=%?``mMr*Z z)u`H|GsK0z-kOfql9n|0^9LD%pLWlE{D()@?A(=WY`z(J+K+lT_wkKedoT9OpWl=$ z{^5XagAXm)I%@RA$&EIic{O^i%fl(Akj-6{HH)y#4GDs8)RrDnC|YoG1ds8_yQC6-paCr8SlCuUU7wfpsZ z8#n)UaH}D6sw{e@%B*ZdpWb?8YnQqE7R)(x;QA*wR}K8VX6^nJrx*Tk*p62DPd6T0 zZ|Jw@f7+AfUkxgaTfAb%-%TR1Cl@?0>8;!w&K=EnA=EqL+6M>Rf9HXn4dxDOFzosZ zM{nP5{mJc*Gv})Oeb;hJCnwy#uX^PvS3{e7w@U6=w0M((-}Vi)uh(;V?SJwteYoGJ zd2jCCP_WmUmR;X@rr)s(qY@{+Q+L_HU%tFnBKhqm{pv4x?&R;SS3KHdMx~O+#+6&J zW=EOm@Qbx3miu&b*Y(d;%UQGVb4M;!EY;ysmY0fVu6H7#RnPrHN(>*J``qu(pUZlu z;f_qHNjtU;+1RG0EDMAJ$4teDzw=-A(Hj9UMBm62Z+s?h+&<@rHv|7rPJhdxUV zN9t_(HFN#_4Qe)OmXPr67qu@*RGfAO6r+k8+96$=}yB<{()2C@|JpY zbn35n%HR4pCYSmes2E3*Ry}Ay!QH)r>(3AMeCzPhmKP#_Kk?_#w+emqaocGAJC~kSoX!LfL?=t+>O8ebtYgVo_Ts*aH|M1s|1#zHwUp!CH!hj}OSRt9 zcc;wgl`XDUj=1Yj#a+)HcRgF&^&D}JKNa_Q_PFZBVtI}?z<@g^Q;u(Wa9iQ*abvT^jhryO(w^D;Tei5% zW6l&VBHKpEm9%?Z2iqYvKMi?Rzh}zdHZ-r@i|t4Y|4dE3IwXr_x~m+ecd4R~<@PA8G#~ z4KMAlw6o4*2{LU)0B=)Zs+Gzn;%mnE(G5_dke9N%0A_+H|K@;JU^(u?bM=2bIN7{ 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 AIRF3v diff --git a/apps/emqx_coap/intergration_test/Makefile b/apps/emqx_coap/intergration_test/Makefile deleted file mode 100644 index 12a2081dd..000000000 --- a/apps/emqx_coap/intergration_test/Makefile +++ /dev/null @@ -1,129 +0,0 @@ -.PHONY: clean, clean_result, start_broker stop_broker case1 case2 case3 - -RELX_CONF = emqx-rel/relx.config -LIBCOAP_GIT = libcoap/README.md - -all: clean_result $(RELX_CONF) $(LIBCOAP_GIT) start_broker clean_result case1 case2 case3 case4 stop_broker - @echo " " - @echo " test complete" - @echo " " - -clean_result: - -rm -f case*.txt - - -start_broker: - -rm -f emqx-rel/_rel/emqx/log/* - -emqx-rel/_rel/emqx/bin/emqx stop - sleep 1 - emqx-rel/_rel/emqx/bin/emqx start - sleep 1 - emqx-rel/_rel/emqx/bin/emqx_ctl plugins load emqx_coap - -stop_broker: - -emqx-rel/_rel/emqx/bin/emqx stop - -case1: - libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret" > case1_output.txt & - sleep 1 - libcoap/examples/coap-client -m put -e w123G45 "coap://127.0.0.1/mqtt/topic1?c=client2&u=mike&p=pw12" - sleep 6 - python check_result.py case1 case1_output.txt==w123G45 - -case2: - # subscribe to topic="x/y" - libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/x%2Fy?c=client3&u=tom&p=secret" > case2_output1.txt & - # subscribe to topic="+/z" - libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/%2B%2Fz?c=client4&u=mike&p=pw12" > case2_output2.txt & - sleep 1 - # publish to topic="x/y" - libcoap/examples/coap-client -m put -e big9wolf "coap://127.0.0.1/mqtt/x%2Fy?c=client5&u=sun&p=pw3" - # publish to topic="p/z" - libcoap/examples/coap-client -m put -e black2ant "coap://127.0.0.1/mqtt/p%2Fz?c=client5&u=sun&p=pw3" - sleep 6 - python check_result.py case2 case2_output1.txt==big9wolf case2_output1.txt!=black2ant case2_output2.txt!=big9wolf case2_output2.txt==black2ant - -case3: - libcoap/examples/coap-client -m get -T tk12 -s 5 "coap://127.0.0.1/mqtt/a%2Fb?c=client3&u=tom&p=secret" > case3_output1.txt & - libcoap/examples/coap-client -m get -T tk34 -s 5 "coap://127.0.0.1/mqtt/c%2Fd?c=client3&u=tom&p=secret" > case3_output2.txt & - sleep 1 - libcoap/examples/coap-client -m put -e big9wolf "coap://127.0.0.1/mqtt/c%2Fd?c=client5&u=sun&p=pw3" - libcoap/examples/coap-client -m put -e black2ant "coap://127.0.0.1/mqtt/a%2Fb?c=client5&u=sun&p=pw3" - sleep 6 - python check_result.py case3 case3_output1.txt==black2ant case3_output2.txt==big9wolf case3_output2.txt!=black2ant - - - -case4: - # reload emqx_coap, does it work as expected? - sleep 1 - emqx-rel/_rel/emqx/bin/emqx_ctl plugins unload emqx_coap - sleep 1 - emqx-rel/_rel/emqx/bin/emqx_ctl plugins load emqx_coap - sleep 1 - libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret" > case4_output.txt & - sleep 1 - libcoap/examples/coap-client -m put -e w6J3G45 "coap://127.0.0.1/mqtt/topic1?c=client2&u=mike&p=pw12" - sleep 6 - python check_result.py case4 case4_output.txt==w6J3G45 - - - - -$(RELX_CONF): - git clone https://github.com/emqx/emqx-rel.git - git clone https://github.com/emqx/emq-coap.git - @echo "update emq-coap with this development code" - mv emq-coap emqx_coap - -rm -rf emqx_coap/etc - -rm -rf emqx_coap/include - -rm -rf emqx_coap/priv - -rm -rf emqx_coap/src - -rm -rf emqx_coap/Makefile - cp -rf ../etc emqx_coap/ - cp -rf ../include emqx_coap/ - cp -rf ../priv emqx_coap/ - cp -rf ../src emqx_coap/ - cp -rf ../Makefile emqx_coap/Makefile - -mkdir emqx-rel/deps - mv emqx_coap emqx-rel/deps/ - @echo "start building ..." - make -C emqx-rel -f Makefile - - -coap: $(LIBCOAP_GIT) - @echo "make coap" - -$(LIBCOAP_GIT): - git clone -b v4.1.2 http://github.com/obgm/libcoap - cd libcoap && ./autogen.sh && ./configure --enable-documentation=no --enable-tests=no - make -C libcoap -f Makefile - -r: rebuild_emq - # r short for rebuild_emq - @echo " rebuild complete " - -rebuild_emq: - -emqx-rel/_rel/emqx/bin/emqx stop - -rm -rf emqx-rel/deps/emqx_coap/etc - -rm -rf emqx-rel/deps/emqx_coap/include - -rm -rf emqx-rel/deps/emqx_coap/priv - -rm -rf emqx-rel/deps/emqx_coap/src - -rm -rf emqx-rel/deps/emqx_coap/Makefile - cp -rf ../etc emqx-rel/deps/emqx_coap/ - cp -rf ../include emqx-rel/deps/emqx_coap/ - cp -rf ../priv emqx-rel/deps/emqx_coap/ - cp -rf ../src emqx-rel/deps/emqx_coap/ - cp -rf ../Makefile emqx-rel/deps/emqx_coap/Makefile - make -C emqx-rel -f Makefile - -clean: clean_result - -rm -f client/*.exe - -rm -f client/*.o - -rm -rf emqx-rel - -rm -rf libcoap - -lazy: clean_result start_broker case2 stop_broker - # custom your command here - @echo "you are so lazy" - diff --git a/apps/emqx_coap/intergration_test/README.md b/apps/emqx_coap/intergration_test/README.md deleted file mode 100644 index eb3507923..000000000 --- a/apps/emqx_coap/intergration_test/README.md +++ /dev/null @@ -1,8 +0,0 @@ -Integration test for emq-coap -====== - -execute following command -``` -make -``` - diff --git a/apps/emqx_coap/intergration_test/check_result.py b/apps/emqx_coap/intergration_test/check_result.py deleted file mode 100644 index f9baaefae..000000000 --- a/apps/emqx_coap/intergration_test/check_result.py +++ /dev/null @@ -1,52 +0,0 @@ -import sys - - -def have_string(filename, text): - data = open(filename, "rb").read() - if data.find(text) > 0: - return True - else: - return False - - -def mark(case_number, result, description): - if result: - f = open(case_number+"_PASS.txt", "wb") - f.close() - print("\n\n"+case_number+" PASS\n\n") - else: - f = open(case_number+"_FAIL.txt", "wb") - f.write(description) - f.close() - print("\n\n"+case_number+" FAIL\n\n") - -def parse_condition(condition): - if condition.find("==") > 0: - r = condition.split("==") - return r[0], r[1], True - elif condition.find("!=") > 0: - r = condition.split("!=") - return r[0], r[1], False - else: - print("\ncondition syntax error\n\n\n") - sys.exit("condition syntax error") - - -def main(): - case_number = sys.argv[1] - description = "" - conclustion = True - for condition in sys.argv[2:]: - filename, text, result = parse_condition(condition) - if have_string(filename, text) == result: - pass - else: - conclustion = False - description = description + "\n" + condition + " failed\n" - - mark(case_number, conclustion, description) - - -if __name__ == "__main__": - main() - diff --git a/apps/emqx_coap/rebar.config b/apps/emqx_coap/rebar.config deleted file mode 100644 index 0f8759b8a..000000000 --- a/apps/emqx_coap/rebar.config +++ /dev/null @@ -1,4 +0,0 @@ -{deps, - [ - {gen_coap, {git, "https://github.com/emqx/gen_coap", {tag, "v0.3.2"}}} - ]}. diff --git a/apps/emqx_coap/test/emqx_coap_SUITE.erl b/apps/emqx_coap/test/emqx_coap_SUITE.erl deleted file mode 100644 index 9618425a3..000000000 --- a/apps/emqx_coap/test/emqx_coap_SUITE.erl +++ /dev/null @@ -1,319 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_coap_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("gen_coap/include/coap.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx.hrl"). - --define(LOGT(Format, Args), ct:pal(Format, Args)). - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_coap], fun set_special_cfg/1), - Config. - -set_special_cfg(emqx_coap) -> - Opts = application:get_env(emqx_coap, dtls_opts,[]), - Opts2 = [{keyfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/key.pem")}, - {certfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/cert.pem")}], - application:set_env(emqx_coap, dtls_opts, emqx_misc:merge_opts(Opts, Opts2)), - application:set_env(emqx_coap, enable_stats, true); -set_special_cfg(_) -> - ok. - -end_per_suite(Config) -> - emqx_ct_helpers:stop_apps([emqx_coap]), - Config. - -%%-------------------------------------------------------------------- -%% Test Cases -%%-------------------------------------------------------------------- - -t_publish(_Config) -> - Topic = <<"abc">>, Payload = <<"123">>, - TopicStr = binary_to_list(Topic), - URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", - - %% Sub topic first - emqx:subscribe(Topic), - - Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - {ok, changed, _} = Reply, - - receive - {deliver, Topic, Msg} -> - ?assertEqual(Topic, Msg#message.topic), - ?assertEqual(Payload, Msg#message.payload) - after - 500 -> - ?assert(false) - end. - -t_publish_acl_deny(_Config) -> - Topic = <<"abc">>, Payload = <<"123">>, - TopicStr = binary_to_list(Topic), - URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", - - %% Sub topic first - emqx:subscribe(Topic), - - ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, authorize, 3, deny), - Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?assertEqual({error,forbidden}, Reply), - ok = meck:unload(emqx_access_control), - receive - {deliver, Topic, Msg} -> ct:fail({unexpected, {Topic, Msg}}) - after - 500 -> ok - end. - -t_observe(_Config) -> - Topic = <<"abc">>, TopicStr = binary_to_list(Topic), - Payload = <<"123">>, - Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", - {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri), - ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]), - - [SubPid] = emqx:subscribers(Topic), - ?assert(is_pid(SubPid)), - - %% Publish a message - emqx:publish(emqx_message:make(Topic, Payload)), - - Notif = receive_notification(), - ?LOGT("observer get Notif=~p", [Notif]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif, - ?assertEqual(Payload, PayloadRecv), - - er_coap_observer:stop(Pid), - timer:sleep(100), - - [] = emqx:subscribers(Topic). - -t_observe_acl_deny(_Config) -> - Topic = <<"abc">>, TopicStr = binary_to_list(Topic), - Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", - ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]), - ok = meck:expect(emqx_access_control, authorize, 3, deny), - ?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)), - [] = emqx:subscribers(Topic), - ok = meck:unload(emqx_access_control). - -t_observe_wildcard(_Config) -> - Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)), - Payload = <<"123">>, - Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", - {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri), - ?LOGT("observer Uri=~p, Pid=~p, N=~p, Code=~p, Content=~p", [Uri, Pid, N, Code, Content]), - - [SubPid] = emqx:subscribers(Topic), - ?assert(is_pid(SubPid)), - - %% Publish a message - emqx:publish(emqx_message:make(<<"a/b">>, Payload)), - - Notif = receive_notification(), - ?LOGT("observer get Notif=~p", [Notif]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif, - ?assertEqual(Payload, PayloadRecv), - - er_coap_observer:stop(Pid), - timer:sleep(100), - - [] = emqx:subscribers(Topic). - -t_observe_pub(_Config) -> - Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)), - Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret", - {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri), - ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]), - - [SubPid] = emqx:subscribers(Topic), - ?assert(is_pid(SubPid)), - - Topic2 = <<"a/b">>, Payload2 = <<"UFO">>, - TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)), - URI2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret", - - Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = Payload2}), - {ok,changed, _} = Reply2, - - Notif2 = receive_notification(), - ?LOGT("observer get Notif2=~p", [Notif2]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2, - ?assertEqual(Payload2, PayloadRecv2), - - Topic3 = <<"j/b">>, Payload3 = <<"ET629">>, - TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)), - URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=mike&p=guess", - Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), - {ok,changed, _} = Reply3, - - Notif3 = receive_notification(), - ?LOGT("observer get Notif3=~p", [Notif3]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv3}} = Notif3, - ?assertEqual(Payload3, PayloadRecv3), - - er_coap_observer:stop(Pid). - -t_one_clientid_sub_2_topics(_Config) -> - Topic1 = <<"abc">>, TopicStr1 = binary_to_list(Topic1), - Payload1 = <<"123">>, - Uri1 = "coap://127.0.0.1/mqtt/"++TopicStr1++"?c=client1&u=tom&p=secret", - {ok, Pid1, N1, Code1, Content1} = er_coap_observer:observe(Uri1), - ?LOGT("observer 1 Pid=~p, N=~p, Code=~p, Content=~p", [Pid1, N1, Code1, Content1]), - - [SubPid] = emqx:subscribers(Topic1), - ?assert(is_pid(SubPid)), - - Topic2 = <<"x/y">>, TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)), - Payload2 = <<"456">>, - Uri2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret", - {ok, Pid2, N2, Code2, Content2} = er_coap_observer:observe(Uri2), - ?LOGT("observer 2 Pid=~p, N=~p, Code=~p, Content=~p", [Pid2, N2, Code2, Content2]), - - [SubPid] = emqx:subscribers(Topic2), - ?assert(is_pid(SubPid)), - - emqx:publish(emqx_message:make(Topic1, Payload1)), - - Notif1 = receive_notification(), - ?LOGT("observer 1 get Notif=~p", [Notif1]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv1}} = Notif1, - ?assertEqual(Payload1, PayloadRecv1), - - emqx:publish(emqx_message:make(Topic2, Payload2)), - - Notif2 = receive_notification(), - ?LOGT("observer 2 get Notif=~p", [Notif2]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2, - ?assertEqual(Payload2, PayloadRecv2), - - er_coap_observer:stop(Pid1), - er_coap_observer:stop(Pid2). - -t_invalid_parameter(_Config) -> - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% "cid=client2" is invaid - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - Topic3 = <<"a/b">>, Payload3 = <<"ET629">>, - TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)), - URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?cid=client2&u=tom&p=simple", - Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), - ?assertMatch({error,bad_request}, Reply3), - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% "what=hello" is invaid - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - URI4 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?what=hello", - Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), - ?assertMatch({error, bad_request}, Reply4). - -t_invalid_topic(_Config) -> - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% "a/b" is a valid topic string - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - Topic3 = <<"a/b">>, Payload3 = <<"ET629">>, - TopicStr3 = binary_to_list(Topic3), - URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=tom&p=simple", - Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), - ?assertMatch({ok,changed,_Content}, Reply3), - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% "+?#" is invaid topic string - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - URI4 = "coap://127.0.0.1/mqtt/"++"+?#"++"?what=hello", - Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}), - ?assertMatch({error,bad_request}, Reply4). - -% mqtt connection kicked by coap with same client id -t_kick_1(_Config) -> - URI = "coap://127.0.0.1/mqtt/abc?c=clientid&u=tom&p=secret", - % workaround: emqx:subscribe does not kick same client id. - spawn_monitor(fun() -> - {ok, C} = emqtt:start_link([{host, "localhost"}, - {clientid, <<"clientid">>}, - {username, <<"plain">>}, - {password, <<"plain">>}]), - {ok, _} = emqtt:connect(C) end), - er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, - payload = <<"123">>}), - receive - {'DOWN', _, _, _, _} -> ok - after 2000 -> - ?assert(false) - end. - -% mqtt connection kicked by coap with same client id -t_acl(Config) -> - OldPath = emqx:get_env(plugins_etc_dir), - application:set_env(emqx, plugins_etc_dir, - emqx_ct_helpers:deps_path(emqx_authz, "test")), - Conf = #{<<"authz">> => - #{<<"rules">> => - [#{<<"principal">> =>#{<<"username">> => <<"coap">>}, - <<"permission">> => deny, - <<"topics">> => [<<"abc">>], - <<"action">> => <<"publish">>} - ]}}, - ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)), - application:ensure_all_started(emqx_authz), - - emqx:subscribe(<<"abc">>), - URI = "coap://127.0.0.1/mqtt/adbc?c=client1&u=coap&p=secret", - er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, - payload = <<"123">>}), - receive - _Something -> ?assert(false) - after 2000 -> - ok - end, - - ok = emqx_hooks:del('client.authorize', {emqx_authz, authorize}), - file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')), - application:set_env(emqx, plugins_etc_dir, OldPath), - application:stop(emqx_authz). - -t_stats(_) -> - ok. - -t_auth_failure(_) -> - ok. - -t_qos_supprot(_) -> - ok. - -%%-------------------------------------------------------------------- -%% Helpers - -receive_notification() -> - receive - {coap_notify, Pid, N2, Code2, Content2} -> - {coap_notify, Pid, N2, Code2, Content2} - after 2000 -> - receive_notification_timeout - end. - -testdir(DataPath) -> - Ls = filename:split(DataPath), - filename:join(lists:sublist(Ls, 1, length(Ls) - 1)). diff --git a/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl b/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl deleted file mode 100644 index c018b9165..000000000 --- a/apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl +++ /dev/null @@ -1,677 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_coap_pubsub_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("gen_coap/include/coap.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx.hrl"). - --define(LOGT(Format, Args), ct:pal(Format, Args)). - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_coap], fun set_special_cfg/1), - Config. - -set_special_cfg(emqx_coap) -> - application:set_env(emqx_coap, enable_stats, true); -set_special_cfg(_) -> - ok. - -end_per_suite(Config) -> - emqx_ct_helpers:stop_apps([emqx_coap]), - Config. - -%%-------------------------------------------------------------------- -%% Test Cases -%%-------------------------------------------------------------------- - -t_update_max_age(_Config) -> - TopicInPayload = <<"topic1">>, - Payload = <<";ct=42">>, - Payload1 = <<";ct=50">>, - URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", - URI2 = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - timer:sleep(50), - - %% post to create the same topic but with different max age and ct value in payload - Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 70, format = <<"application/link-format">>, payload = Payload1}), - {ok,created, #coap_content{location_path = LocPath}} = Reply1, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{TopicInPayload, MaxAge2, CT2, _ResPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload), - ?assertEqual(70, MaxAge2), - ?assertEqual(<<"50">>, CT2), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2). - -t_create_subtopic(_Config) -> - TopicInPayload = <<"topic1">>, - TopicInPayloadStr = "topic1", - Payload = <<";ct=42">>, - URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", - RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret", - - Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - timer:sleep(50), - - %% post to create the a sub topic - SubPayload = <<";ct=42">>, - SubTopicInPayloadStr = "subtopic", - SubURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"?c=client1&u=tom&p=secret", - SubRealURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"/"++SubTopicInPayloadStr++"?c=client1&u=tom&p=secret", - FullTopic = list_to_binary(TopicInPayloadStr++"/"++SubTopicInPayloadStr), - Reply1 = er_coap_client:request(post, SubURI, #coap_content{format = <<"application/link-format">>, payload = SubPayload}), - ?LOGT("Reply =~p", [Reply1]), - {ok,created, #coap_content{location_path = LocPath1}} = Reply1, - ?assertEqual([<<"/ps/topic1/subtopic">>] ,LocPath1), - [{FullTopic, MaxAge2, CT2, _ResPayload, _}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic), - ?assertEqual(60, MaxAge2), - ?assertEqual(<<"42">>, CT2), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, SubRealURI), - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI). - -t_over_max_age(_Config) -> - TopicInPayload = <<"topic1">>, - Payload = <<";ct=42">>, - URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{max_age = 2, format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(2, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - timer:sleep(3000), - ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)). - -t_refreash_max_age(_Config) -> - TopicInPayload = <<"topic1">>, - Payload = <<";ct=42">>, - Payload1 = <<";ct=50">>, - URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", - RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?LOGT("TimeStamp=~p", [TimeStamp]), - ?assertEqual(5, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - timer:sleep(3000), - - %% post to create the same topic, the max age timer will be restarted with the new max age value - Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload1}), - {ok,created, #coap_content{location_path = LocPath}} = Reply1, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{TopicInPayload, MaxAge2, CT2, _ResPayload, TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload), - ?LOGT("TimeStamp1=~p", [TimeStamp1]), - ?assertEqual(5, MaxAge2), - ?assertEqual(<<"50">>, CT2), - - timer:sleep(3000), - ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI). - -t_case01_publish_post(_Config) -> - timer:sleep(100), - MainTopic = <<"maintopic">>, - TopicInPayload = <<"topic1">>, - Payload = <<";ct=42">>, - MainTopicStr = binary_to_list(MainTopic), - - %% post to create topic maintopic/topic1 - URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret", - FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)), - Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply1]), - {ok,created, #coap_content{location_path = LocPath1}} = Reply1, - ?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1), - [{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic), - ?assertEqual(60, MaxAge), - ?assertEqual(<<"42">>, CT2), - - %% post to publish message to topic maintopic/topic1 - FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)), - URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret", - PubPayload = <<"PUBLISH">>, - - %% Sub topic first - emqx:subscribe(FullTopic), - - Reply2 = er_coap_client:request(post, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}), - ?LOGT("Reply =~p", [Reply2]), - {ok,changed, _} = Reply2, - TopicInfo = [{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic), - ?LOGT("the topic info =~p", [TopicInfo]), - - assert_recv(FullTopic, PubPayload), - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2). - -t_case02_publish_post(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"payload">>, - - %% Sub topic first - emqx:subscribe(Topic), - - %% post to publish a new topic "topic1", and the topic is created - URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(60, MaxAge), - ?assertEqual(<<"42">>, CT), - - assert_recv(Topic, Payload), - - %% post to publish a new message to the same topic "topic1" with different payload - NewPayload = <<"newpayload">>, - Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}), - ?LOGT("Reply =~p", [Reply1]), - {ok,changed, _} = Reply1, - [{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - - assert_recv(Topic, NewPayload), - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case03_publish_post(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"payload">>, - - %% Sub topic first - emqx:subscribe(Topic), - - %% post to publish a new topic "topic1", and the topic is created - URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(60, MaxAge), - ?assertEqual(<<"42">>, CT), - - assert_recv(Topic, Payload), - - %% post to publish a new message to the same topic "topic1", but the ct is not same as created - NewPayload = <<"newpayload">>, - Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}), - ?LOGT("Reply =~p", [Reply1]), - ?assertEqual({error,bad_request}, Reply1), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case04_publish_post(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"payload">>, - - %% post to publish a new topic "topic1", and the topic is created - URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(5, MaxAge), - ?assertEqual(<<"42">>, CT), - - %% after max age timeout, the topic still exists but the status is timeout - timer:sleep(6000), - ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case01_publish_put(_Config) -> - MainTopic = <<"maintopic">>, - TopicInPayload = <<"topic1">>, - Payload = <<";ct=42">>, - MainTopicStr = binary_to_list(MainTopic), - - %% post to create topic maintopic/topic1 - URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret", - FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)), - Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply1]), - {ok,created, #coap_content{location_path = LocPath1}} = Reply1, - ?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1), - [{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic), - ?assertEqual(60, MaxAge), - ?assertEqual(<<"42">>, CT2), - - %% put to publish message to topic maintopic/topic1 - FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)), - URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret", - PubPayload = <<"PUBLISH">>, - - %% Sub topic first - emqx:subscribe(FullTopic), - - Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}), - ?LOGT("Reply =~p", [Reply2]), - {ok,changed, _} = Reply2, - [{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic), - - assert_recv(FullTopic, PubPayload), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2). - -t_case02_publish_put(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"payload">>, - - %% Sub topic first - emqx:subscribe(Topic), - - %% put to publish a new topic "topic1", and the topic is created - URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(60, MaxAge), - ?assertEqual(<<"42">>, CT), - - assert_recv(Topic, Payload), - - %% put to publish a new message to the same topic "topic1" with different payload - NewPayload = <<"newpayload">>, - Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}), - ?LOGT("Reply =~p", [Reply1]), - {ok,changed, _} = Reply1, - [{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - - assert_recv(Topic, NewPayload), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case03_publish_put(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"payload">>, - - %% Sub topic first - emqx:subscribe(Topic), - - %% put to publish a new topic "topic1", and the topic is created - URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(60, MaxAge), - ?assertEqual(<<"42">>, CT), - - assert_recv(Topic, Payload), - - %% put to publish a new message to the same topic "topic1", but the ct is not same as created - NewPayload = <<"newpayload">>, - Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}), - ?LOGT("Reply =~p", [Reply1]), - ?assertEqual({error,bad_request}, Reply1), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case04_publish_put(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"payload">>, - - %% put to publish a new topic "topic1", and the topic is created - URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(put, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/topic1">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(5, MaxAge), - ?assertEqual(<<"42">>, CT), - - %% after max age timeout, no publish message to the same topic, the topic info will be deleted - %%%%%%%%%%%%%%%%%%%%%%%%%% - % but there is one thing to do is we don't count in the publish message received from emqx(from other node).TBD!!!!!!!!!!!!! - %%%%%%%%%%%%%%%%%%%%%%%%%% - timer:sleep(6000), - ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)), - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case01_subscribe(_Config) -> - Topic = <<"topic1">>, - Payload1 = <<";ct=42">>, - timer:sleep(100), - - %% First post to create a topic "topic1" - Uri = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/link-format">>, payload = Payload1}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = [LocPath]}} = Reply, - ?assertEqual(<<"/ps/topic1">> ,LocPath), - TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - %% Subscribe the topic - Uri1 = "coap://127.0.0.1"++binary_to_list(LocPath)++"?c=client1&u=tom&p=secret", - {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri1), - ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]), - - [SubPid] = emqx:subscribers(Topic), - ?assert(is_pid(SubPid)), - - %% Publish a message - Payload = <<"123">>, - emqx:publish(emqx_message:make(Topic, Payload)), - - Notif = receive_notification(), - ?LOGT("observer get Notif=~p", [Notif]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif, - - ?assertEqual(Payload, PayloadRecv), - - %% GET to read the publish message of the topic - Reply1 = er_coap_client:request(get, Uri1), - ?LOGT("Reply=~p", [Reply1]), - {ok,content, #coap_content{payload = <<"123">>}} = Reply1, - - er_coap_observer:stop(Pid), - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri1). - -t_case02_subscribe(_Config) -> - Topic = <<"a/b">>, - TopicStr = binary_to_list(Topic), - PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), - Payload = <<"payload">>, - - %% post to publish a new topic "a/b", and the topic is created - URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/a/b">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(5, MaxAge), - ?assertEqual(<<"42">>, CT), - - %% Wait for the max age of the timer expires - timer:sleep(6000), - ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)), - - %% Subscribe to the timeout topic "a/b", still successfully,got {ok, nocontent} Method - Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - Reply1 = {ok, Pid, _N, nocontent, _} = er_coap_observer:observe(Uri), - ?LOGT("Subscribe Reply=~p", [Reply1]), - - [SubPid] = emqx:subscribers(Topic), - ?assert(is_pid(SubPid)), - - %% put to publish to topic "a/b" - Reply2 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - {ok,changed, #coap_content{}} = Reply2, - [{Topic, MaxAge1, CT, Payload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT), - ?assertEqual(false, TimeStamp =:= timeout), - - %% Publish a message - emqx:publish(emqx_message:make(Topic, Payload)), - - Notif = receive_notification(), - ?LOGT("observer get Notif=~p", [Notif]), - {coap_notify, _, _, {ok,content}, #coap_content{payload = Payload}} = Notif, - - er_coap_observer:stop(Pid), - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case03_subscribe(_Config) -> - %% Subscribe to the unexisted topic "a/b", got not_found - Topic = <<"a/b">>, - TopicStr = binary_to_list(Topic), - PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), - Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - {error, not_found} = er_coap_observer:observe(Uri), - - [] = emqx:subscribers(Topic). - -t_case04_subscribe(_Config) -> - %% Subscribe to the wildcad topic "+/b", got bad_request - Topic = <<"+/b">>, - TopicStr = binary_to_list(Topic), - PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), - Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - {error, bad_request} = er_coap_observer:observe(Uri), - - [] = emqx:subscribers(Topic). - -t_case01_read(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"PubPayload">>, - timer:sleep(100), - - %% First post to create a topic "topic1" - Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = [LocPath]}} = Reply, - ?assertEqual(<<"/ps/topic1">> ,LocPath), - TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - %% GET to read the publish message of the topic - timer:sleep(1000), - Reply1 = er_coap_client:request(get, Uri), - ?LOGT("Reply=~p", [Reply1]), - {ok,content, #coap_content{payload = Payload}} = Reply1, - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri). - -t_case02_read(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"PubPayload">>, - timer:sleep(100), - - %% First post to publish a topic "topic1" - Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = [LocPath]}} = Reply, - ?assertEqual(<<"/ps/topic1">> ,LocPath), - TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - %% GET to read the publish message of unmatched format, got bad_request - Reply1 = er_coap_client:request(get, Uri, #coap_content{format = <<"application/json">>}), - ?LOGT("Reply=~p", [Reply1]), - {error, bad_request} = Reply1, - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri). - -t_case03_read(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - timer:sleep(100), - - %% GET to read the nexisted topic "topic1", got not_found - Reply = er_coap_client:request(get, Uri), - ?LOGT("Reply=~p", [Reply]), - {error, not_found} = Reply. - -t_case04_read(_Config) -> - Topic = <<"topic1">>, - TopicStr = binary_to_list(Topic), - Payload = <<"PubPayload">>, - timer:sleep(100), - - %% First post to publish a topic "topic1" - Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = [LocPath]}} = Reply, - ?assertEqual(<<"/ps/topic1">> ,LocPath), - TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?LOGT("lookup topic info=~p", [TopicInfo]), - ?assertEqual(60, MaxAge1), - ?assertEqual(<<"42">>, CT1), - - %% GET to read the publish message of wildcard topic, got bad_request - WildTopic = binary_to_list(<<"+/topic1">>), - Uri1 = "coap://127.0.0.1/ps/"++WildTopic++"?c=client1&u=tom&p=secret", - Reply1 = er_coap_client:request(get, Uri1, #coap_content{format = <<"application/json">>}), - ?LOGT("Reply=~p", [Reply1]), - {error, bad_request} = Reply1, - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri). - -t_case05_read(_Config) -> - Topic = <<"a/b">>, - TopicStr = binary_to_list(Topic), - PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), - Payload = <<"payload">>, - - %% post to publish a new topic "a/b", and the topic is created - URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/a/b">>] ,LocPath), - [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic), - ?assertEqual(5, MaxAge), - ?assertEqual(<<"42">>, CT), - - %% Wait for the max age of the timer expires - timer:sleep(6000), - ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)), - - %% GET to read the expired publish message, supposed to get {ok, nocontent}, but now got {ok, content} - Reply1 = er_coap_client:request(get, URI), - ?LOGT("Reply=~p", [Reply1]), - {ok, content, #coap_content{payload = <<>>}}= Reply1, - - {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI). - -t_case01_delete(_Config) -> - TopicInPayload = <<"a/b">>, - TopicStr = binary_to_list(TopicInPayload), - PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), - Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"), - URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret", - - %% Client post to CREATE topic "a/b" - Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}), - ?LOGT("Reply =~p", [Reply]), - {ok,created, #coap_content{location_path = LocPath}} = Reply, - ?assertEqual([<<"/ps/a/b">>] ,LocPath), - - %% Client post to CREATE topic "a/b/c" - TopicInPayload1 = <<"a/b/c">>, - PercentEncodedTopic1 = emqx_http_lib:uri_encode(binary_to_list(TopicInPayload1)), - Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"), - Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}), - ?LOGT("Reply =~p", [Reply1]), - {ok,created, #coap_content{location_path = LocPath1}} = Reply1, - ?assertEqual([<<"/ps/a/b/c">>] ,LocPath1), - - timer:sleep(50), - - %% DELETE the topic "a/b" - UriD = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - ReplyD = er_coap_client:request(delete, UriD), - ?LOGT("Reply=~p", [ReplyD]), - {ok, deleted, #coap_content{}}= ReplyD, - - timer:sleep(300), %% Waiting gen_server:cast/2 for deleting operation - ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload)), - ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload1)). - -t_case02_delete(_Config) -> - TopicInPayload = <<"a/b">>, - TopicStr = binary_to_list(TopicInPayload), - PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr), - - %% DELETE the unexisted topic "a/b" - Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret", - Reply1 = er_coap_client:request(delete, Uri1), - ?LOGT("Reply=~p", [Reply1]), - {error, not_found} = Reply1. - -t_case13_emit_stats_test(_Config) -> - ok. - -%%-------------------------------------------------------------------- -%% Internal functions - -receive_notification() -> - receive - {coap_notify, Pid, N2, Code2, Content2} -> - {coap_notify, Pid, N2, Code2, Content2} - after 2000 -> - receive_notification_timeout - end. - -assert_recv(Topic, Payload) -> - receive - {deliver, _, Msg} -> - ?assertEqual(Topic, Msg#message.topic), - ?assertEqual(Payload, Msg#message.payload) - after - 500 -> - ?assert(false) - end. - diff --git a/apps/emqx_exhook/.gitignore b/apps/emqx_exhook/.gitignore deleted file mode 100644 index da1f0db23..000000000 --- a/apps/emqx_exhook/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -.rebar3 -_* -.eunit -*.o -*.beam -*.plt -*.swp -*.swo -.erlang.cookie -ebin -log -erl_crash.dump -.rebar -logs -_build -.idea -*.iml -rebar3.crashdump -*~ -rebar.lock -data/ -*.conf.rendered -*.pyc -.DS_Store -*.class -Mnesia.nonode@nohost/ -src/emqx_exhook_pb.erl -src/emqx_exhook_v_1_hook_provider_client.erl -src/emqx_exhook_v_1_hook_provider_bhvr.erl diff --git a/apps/emqx_exhook/docs/design-cn.md b/apps/emqx_exhook/docs/design-cn.md deleted file mode 100644 index 6686e96e3..000000000 --- a/apps/emqx_exhook/docs/design-cn.md +++ /dev/null @@ -1,116 +0,0 @@ -# 设计 - -## 动机 - -在 EMQ X Broker v4.1-v4.2 中,我们发布了 2 个插件来扩展 emqx 的编程能力: - -1. `emqx-extension-hook` 提供了使用 Java, Python 向 Broker 挂载钩子的功能 -2. `emqx-exproto` 提供了使用 Java,Python 编写用户自定义协议接入插件的功能 - -但在后续的支持中发现许多难以处理的问题: - -1. 有大量的编程语言需要支持,需要编写和维护如 Go, JavaScript, Lua.. 等语言的驱动。 -2. `erlport` 使用的操作系统的管道进行通信,这让用户代码只能部署在和 emqx 同一个操作系统上。部署方式受到了极大的限制。 -3. 用户程序的启动参数直接打包到 Broker 中,导致用户开发无法实时的进行调试,单步跟踪等。 -4. `erlport` 会占用 `stdin` `stdout`。 - -因此,我们计划重构这部分的实现,其中主要的内容是: -1. 使用 `gRPC` 替换 `erlport`。 -2. 将 `emqx-extension-hook` 重命名为 `emqx-exhook` - - -旧版本的设计:[emqx-extension-hook design in v4.2.0](https://github.com/emqx/emqx-exhook/blob/v4.2.0/docs/design.md) - -## 设计 - -架构如下: - -``` - EMQ X -+========================+ +========+==========+ -| ExHook | | | | -| +----------------+ | gRPC | gRPC | User's | -| | gRPC Client | ------------------> | Server | Codes | -| +----------------+ | (HTTP/2) | | | -| | | | | -+========================+ +========+==========+ -``` - -`emqx-exhook` 通过 gRPC 的方式向用户部署的 gRPC 服务发送钩子的请求,并处理其返回的值。 - - -和 emqx 原生的钩子一致,emqx-exhook 也按照链式的方式执行: - - - -### gRPC 服务示例 - -用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中: - -```protobuff -syntax = "proto3"; - -package emqx.exhook.v1; - -service HookProvider { - - rpc OnProviderLoaded(ProviderLoadedRequest) returns (LoadedResponse) {}; - - rpc OnProviderUnloaded(ProviderUnloadedRequest) returns (EmptySuccess) {}; - - rpc OnClientConnect(ClientConnectRequest) returns (EmptySuccess) {}; - - rpc OnClientConnack(ClientConnackRequest) returns (EmptySuccess) {}; - - rpc OnClientConnected(ClientConnectedRequest) returns (EmptySuccess) {}; - - rpc OnClientDisconnected(ClientDisconnectedRequest) returns (EmptySuccess) {}; - - rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {}; - - rpc OnClientCheckAcl(ClientCheckAclRequest) returns (ValuedResponse) {}; - - rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {}; - - rpc OnClientUnsubscribe(ClientUnsubscribeRequest) returns (EmptySuccess) {}; - - rpc OnSessionCreated(SessionCreatedRequest) returns (EmptySuccess) {}; - - rpc OnSessionSubscribed(SessionSubscribedRequest) returns (EmptySuccess) {}; - - rpc OnSessionUnsubscribed(SessionUnsubscribedRequest) returns (EmptySuccess) {}; - - rpc OnSessionResumed(SessionResumedRequest) returns (EmptySuccess) {}; - - rpc OnSessionDiscarded(SessionDiscardedRequest) returns (EmptySuccess) {}; - - rpc OnSessionTakeovered(SessionTakeoveredRequest) returns (EmptySuccess) {}; - - rpc OnSessionTerminated(SessionTerminatedRequest) returns (EmptySuccess) {}; - - rpc OnMessagePublish(MessagePublishRequest) returns (ValuedResponse) {}; - - rpc OnMessageDelivered(MessageDeliveredRequest) returns (EmptySuccess) {}; - - rpc OnMessageDropped(MessageDroppedRequest) returns (EmptySuccess) {}; - - rpc OnMessageAcked(MessageAckedRequest) returns (EmptySuccess) {}; -} -``` - -### 配置文件示例 - -``` -## 配置 gRPC 服务地址 (HTTP) -## -## s1 为服务器的名称 -exhook.server.s1.url = http://127.0.0.1:9001 - -## 配置 gRPC 服务地址 (HTTPS) -## -## s2 为服务器名称 -exhook.server.s2.url = https://127.0.0.1:9002 -exhook.server.s2.cacertfile = ca.pem -exhook.server.s2.certfile = cert.pem -exhook.server.s2.keyfile = key.pem -``` diff --git a/apps/emqx_exhook/rebar.config b/apps/emqx_exhook/rebar.config deleted file mode 100644 index eafa20d85..000000000 --- a/apps/emqx_exhook/rebar.config +++ /dev/null @@ -1,48 +0,0 @@ -%%-*- mode: erlang -*- -{plugins, - [rebar3_proper, - {grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}} -]}. - -{deps, - [{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.2"}}} -]}. - -{grpc, - [{protos, ["priv/protos"]}, - {gpb_opts, [{module_name_prefix, "emqx_"}, - {module_name_suffix, "_pb"}]} -]}. - -{provider_hooks, - [{pre, [{compile, {grpc, gen}}, - {clean, {grpc, clean}}]} -]}. - -{edoc_opts, [{preprocess, true}]}. - -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - debug_info, - {parse_transform}]}. - -{xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - warnings_as_errors, deprecated_functions]}. -{xref_ignores, [emqx_exhook_pb]}. - -{cover_enabled, true}. -{cover_opts, [verbose]}. -{cover_export_enabled, true}. -{cover_excl_mods, [emqx_exhook_pb, - emqx_exhook_v_1_hook_provider_bhvr, - emqx_exhook_v_1_hook_provider_client]}. - -{profiles, - [{test, - [{deps, - []} - ]} -]}. diff --git a/apps/emqx_exhook/src/emqx_exhook.appup.src b/apps/emqx_exhook/src/emqx_exhook.appup.src deleted file mode 100644 index 26e84d88f..000000000 --- a/apps/emqx_exhook/src/emqx_exhook.appup.src +++ /dev/null @@ -1,23 +0,0 @@ -%% -*-: erlang -*- -{VSN, - [ - {"4.3.1", [ - {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} - ]}, - {"4.3.0", [ - {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []}, - {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ], - [ - {"4.3.1", [ - {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} - ]}, - {"4.3.0", [ - {load_module, emqx_exhook_pb, brutal_purge, soft_purge, []}, - {load_module, emqx_exhook_server, brutal_purge, soft_purge, []} - ]}, - {<<".*">>, []} - ] -}. diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl deleted file mode 100644 index 5d5a396a5..000000000 --- a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl +++ /dev/null @@ -1,96 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_exhook_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - -%%-------------------------------------------------------------------- -%% Setups -%%-------------------------------------------------------------------- - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Cfg) -> - _ = emqx_exhook_demo_svr:start(), - emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1), - Cfg. - -end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_exhook]), - emqx_exhook_demo_svr:stop(). - -set_special_cfgs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, plugins_loaded_file, undefined), - application:set_env(emqx, modules_loaded_file, undefined); -set_special_cfgs(emqx_exhook) -> - ok. - -%%-------------------------------------------------------------------- -%% Test cases -%%-------------------------------------------------------------------- - -t_noserver_nohook(_) -> - emqx_exhook:disable(default), - ?assertEqual([], ets:tab2list(emqx_hooks)), - - Opts = proplists:get_value( - default, - application:get_env(emqx_exhook, servers, []) - ), - ok = emqx_exhook:enable(default, Opts), - ?assertNotEqual([], ets:tab2list(emqx_hooks)). - -t_cli_list(_) -> - meck_print(), - ?assertEqual( [[emqx_exhook_server:format(Svr) || Svr <- emqx_exhook:list()]] - , emqx_exhook_cli:cli(["server", "list"]) - ), - unmeck_print(). - -t_cli_enable_disable(_) -> - meck_print(), - ?assertEqual([already_started], emqx_exhook_cli:cli(["server", "enable", "default"])), - ?assertEqual(ok, emqx_exhook_cli:cli(["server", "disable", "default"])), - ?assertEqual([], emqx_exhook_cli:cli(["server", "list"])), - - ?assertEqual([not_running], emqx_exhook_cli:cli(["server", "disable", "default"])), - ?assertEqual(ok, emqx_exhook_cli:cli(["server", "enable", "default"])), - unmeck_print(). - -t_cli_stats(_) -> - meck_print(), - _ = emqx_exhook_cli:cli(["server", "stats"]), - _ = emqx_exhook_cli:cli(x), - unmeck_print(). - -%%-------------------------------------------------------------------- -%% Utils -%%-------------------------------------------------------------------- - -meck_print() -> - meck:new(emqx_ctl, [passthrough, no_history, no_link]), - meck:expect(emqx_ctl, print, fun(_) -> ok end), - meck:expect(emqx_ctl, print, fun(_, Args) -> Args end). - -unmeck_print() -> - meck:unload(emqx_ctl). diff --git a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl b/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl deleted file mode 100644 index 656788b5e..000000000 --- a/apps/emqx_exhook/test/emqx_exhook_demo_svr.erl +++ /dev/null @@ -1,339 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_exhook_demo_svr). - --behavior(emqx_exhook_v_1_hook_provider_bhvr). - -%% --export([ start/0 - , stop/0 - , take/0 - , in/1 - ]). - -%% gRPC server HookProvider callbacks --export([ on_provider_loaded/2 - , on_provider_unloaded/2 - , on_client_connect/2 - , on_client_connack/2 - , on_client_connected/2 - , on_client_disconnected/2 - , on_client_authenticate/2 - , on_client_authorize/2 - , on_client_subscribe/2 - , on_client_unsubscribe/2 - , on_session_created/2 - , on_session_subscribed/2 - , on_session_unsubscribed/2 - , on_session_resumed/2 - , on_session_discarded/2 - , on_session_takeovered/2 - , on_session_terminated/2 - , on_message_publish/2 - , on_message_delivered/2 - , on_message_dropped/2 - , on_message_acked/2 - ]). - --define(PORT, 9000). --define(NAME, ?MODULE). - -%%-------------------------------------------------------------------- -%% Server APIs -%%-------------------------------------------------------------------- - -start() -> - Pid = spawn(fun mngr_main/0), - register(?MODULE, Pid), - {ok, Pid}. - -stop() -> - grpc:stop_server(?NAME), - ?MODULE ! stop. - -take() -> - ?MODULE ! {take, self()}, - receive {value, V} -> V - after 5000 -> error(timeout) end. - -in({FunName, Req}) -> - ?MODULE ! {in, FunName, Req}. - -mngr_main() -> - application:ensure_all_started(grpc), - Services = #{protos => [emqx_exhook_pb], - services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr} - }, - Options = [], - Svr = grpc:start_server(?NAME, ?PORT, Services, Options), - mngr_loop([Svr, queue:new(), queue:new()]). - -mngr_loop([Svr, Q, Takes]) -> - receive - {in, FunName, Req} -> - {NQ1, NQ2} = reply(queue:in({FunName, Req}, Q), Takes), - mngr_loop([Svr, NQ1, NQ2]); - {take, From} -> - {NQ1, NQ2} = reply(Q, queue:in(From, Takes)), - mngr_loop([Svr, NQ1, NQ2]); - stop -> - exit(normal) - end. - -reply(Q1, Q2) -> - case queue:len(Q1) =:= 0 orelse - queue:len(Q2) =:= 0 of - true -> {Q1, Q2}; - _ -> - {{value, {Name, V}}, NQ1} = queue:out(Q1), - {{value, From}, NQ2} = queue:out(Q2), - From ! {value, {Name, V}}, - {NQ1, NQ2} - end. - -%%-------------------------------------------------------------------- -%% callbacks -%%-------------------------------------------------------------------- - --spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. - -on_provider_loaded(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{hooks => [ - #{name => <<"client.connect">>}, - #{name => <<"client.connack">>}, - #{name => <<"client.connected">>}, - #{name => <<"client.disconnected">>}, - #{name => <<"client.authenticate">>}, - #{name => <<"client.authorize">>}, - #{name => <<"client.subscribe">>}, - #{name => <<"client.unsubscribe">>}, - #{name => <<"session.created">>}, - #{name => <<"session.subscribed">>}, - #{name => <<"session.unsubscribed">>}, - #{name => <<"session.resumed">>}, - #{name => <<"session.discarded">>}, - #{name => <<"session.takeovered">>}, - #{name => <<"session.terminated">>}, - #{name => <<"message.publish">>}, - #{name => <<"message.delivered">>}, - #{name => <<"message.acked">>}, - #{name => <<"message.dropped">>}]}, Md}. --spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_provider_unloaded(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_connect(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_connack(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_connected(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_disconnected(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - %% some cases for testing - case Username of - <<"baduser">> -> - {ok, #{type => 'STOP_AND_RETURN', - value => {bool_result, false}}, Md}; - <<"gooduser">> -> - {ok, #{type => 'STOP_AND_RETURN', - value => {bool_result, true}}, Md}; - <<"normaluser">> -> - {ok, #{type => 'CONTINUE', - value => {bool_result, true}}, Md}; - _ -> - {ok, #{type => 'IGNORE'}, Md} - end. - --spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - %% some cases for testing - case Username of - <<"baduser">> -> - {ok, #{type => 'STOP_AND_RETURN', - value => {bool_result, false}}, Md}; - <<"gooduser">> -> - {ok, #{type => 'STOP_AND_RETURN', - value => {bool_result, true}}, Md}; - <<"normaluser">> -> - {ok, #{type => 'CONTINUE', - value => {bool_result, true}}, Md}; - _ -> - {ok, #{type => 'IGNORE'}, Md} - end. - --spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_subscribe(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_client_unsubscribe(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_created(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_subscribed(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_unsubscribed(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_resumed(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_discarded(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_takeovered(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_session_terminated(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_message_publish(#{message := #{from := From} = Msg} = Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - %% some cases for testing - case From of - <<"baduser">> -> - NMsg = Msg#{qos => 0, - topic => <<"">>, - payload => <<"">> - }, - {ok, #{type => 'STOP_AND_RETURN', - value => {message, NMsg}}, Md}; - <<"gooduser">> -> - NMsg = Msg#{topic => From, - payload => From}, - {ok, #{type => 'STOP_AND_RETURN', - value => {message, NMsg}}, Md}; - _ -> - {ok, #{type => 'IGNORE'}, Md} - end. - --spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_message_delivered(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_message_dropped(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. - --spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata()) - -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} - | {error, grpc_cowboy_h:error_response()}. -on_message_acked(Req, Md) -> - ?MODULE:in({?FUNCTION_NAME, Req}), - %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), - {ok, #{}, Md}. diff --git a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl deleted file mode 100644 index 12f54eef6..000000000 --- a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl +++ /dev/null @@ -1,531 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(prop_exhook_hooks). - --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). - --import(emqx_ct_proper_types, - [ conninfo/0 - , clientinfo/0 - , sessioninfo/0 - , message/0 - , connack_return_code/0 - , topictab/0 - , topic/0 - , subopts/0 - ]). - --define(ALL(Vars, Types, Exprs), - ?SETUP(fun() -> - State = do_setup(), - fun() -> do_teardown(State) end - end, ?FORALL(Vars, Types, Exprs))). - -%%-------------------------------------------------------------------- -%% Properties -%%-------------------------------------------------------------------- - -prop_client_connect() -> - ?ALL({ConnInfo, ConnProps}, - {conninfo(), conn_properties()}, - begin - ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]), - {'on_client_connect', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{props => properties(ConnProps), - conninfo => from_conninfo(ConnInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_connack() -> - ?ALL({ConnInfo, Rc, AckProps}, - {conninfo(), connack_return_code(), ack_properties()}, - begin - ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]), - {'on_client_connack', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{props => properties(AckProps), - result_code => atom_to_binary(Rc, utf8), - conninfo => from_conninfo(ConnInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_authenticate() -> - ?ALL({ClientInfo0, AuthResult}, - {clientinfo(), authresult()}, - begin - ClientInfo = inject_magic_into(username, ClientInfo0), - OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult), - ExpectedAuthResult = case maps:get(username, ClientInfo) of - <<"baduser">> -> - AuthResult#{ - auth_result => not_authorized, - anonymous => false}; - <<"gooduser">> -> - AuthResult#{ - auth_result => success, - anonymous => false}; - <<"normaluser">> -> - AuthResult#{ - auth_result => success, - anonymous => false}; - _ -> - case maps:get(auth_result, AuthResult) of - success -> - #{auth_result => success, - anonymous => false}; - _ -> - #{auth_result => not_authorized, - anonymous => false} - end - end, - ?assertEqual(ExpectedAuthResult, OutAuthResult), - - {'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{result => authresult_to_bool(AuthResult), - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_authorize() -> - ?ALL({ClientInfo0, PubSub, Topic, Result}, - {clientinfo(), oneof([publish, subscribe]), - topic(), oneof([allow, deny])}, - begin - ClientInfo = inject_magic_into(username, ClientInfo0), - OutResult = emqx_hooks:run_fold( - 'client.authorize', - [ClientInfo, PubSub, Topic], - Result), - ExpectedOutResult = case maps:get(username, ClientInfo) of - <<"baduser">> -> deny; - <<"gooduser">> -> allow; - <<"normaluser">> -> allow; - _ -> Result - end, - ?assertEqual(ExpectedOutResult, OutResult), - - {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{result => aclresult_to_bool(Result), - type => pubsub_to_enum(PubSub), - topic => Topic, - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_connected() -> - ?ALL({ClientInfo, ConnInfo}, - {clientinfo(), conninfo()}, - begin - ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]), - {'on_client_connected', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_disconnected() -> - ?ALL({ClientInfo, Reason, ConnInfo}, - {clientinfo(), shutdown_reason(), conninfo()}, - begin - ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]), - {'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{reason => stringfy(Reason), - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_subscribe() -> - ?ALL({ClientInfo, SubProps, TopicTab}, - {clientinfo(), sub_properties(), topictab()}, - begin - ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]), - {'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{props => properties(SubProps), - topic_filters => topicfilters(TopicTab), - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_client_unsubscribe() -> - ?ALL({ClientInfo, UnSubProps, TopicTab}, - {clientinfo(), unsub_properties(), topictab()}, - begin - ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]), - {'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{props => properties(UnSubProps), - topic_filters => topicfilters(TopicTab), - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_created() -> - ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, - begin - ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]), - {'on_session_created', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_subscribed() -> - ?ALL({ClientInfo, Topic, SubOpts}, - {clientinfo(), topic(), subopts()}, - begin - ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]), - {'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{topic => Topic, - subopts => subopts(SubOpts), - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_unsubscribed() -> - ?ALL({ClientInfo, Topic, SubOpts}, - {clientinfo(), topic(), subopts()}, - begin - ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]), - {'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{topic => Topic, - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_resumed() -> - ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, - begin - ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]), - {'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_discared() -> - ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, - begin - ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]), - {'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_takeovered() -> - ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, - begin - ok = emqx_hooks:run('session.takeovered', [ClientInfo, SessInfo]), - {'on_session_takeovered', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_session_terminated() -> - ?ALL({ClientInfo, Reason, SessInfo}, - {clientinfo(), shutdown_reason(), sessioninfo()}, - begin - ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]), - {'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{reason => stringfy(Reason), - clientinfo => from_clientinfo(ClientInfo) - }, - ?assertEqual(Expected, Resp), - true - end). - -prop_message_publish() -> - ?ALL(Msg0, message(), - begin - Msg = emqx_message:from_map( - inject_magic_into(from, emqx_message:to_map(Msg0))), - OutMsg= emqx_hooks:run_fold('message.publish', [], Msg), - case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of - true -> - ?assertEqual(Msg, OutMsg), - skip; - _ -> - ExpectedOutMsg = case emqx_message:from(Msg) of - <<"baduser">> -> - MsgMap = emqx_message:to_map(Msg), - emqx_message:from_map( - MsgMap#{qos => 0, - topic => <<"">>, - payload => <<"">> - }); - <<"gooduser">> = From -> - MsgMap = emqx_message:to_map(Msg), - emqx_message:from_map( - MsgMap#{topic => From, - payload => From - }); - _ -> Msg - end, - ?assertEqual(ExpectedOutMsg, OutMsg), - - {'on_message_publish', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{message => from_message(Msg) - }, - ?assertEqual(Expected, Resp) - end, - true - end). - -prop_message_dropped() -> - ?ALL({Msg, By, Reason}, {message(), hardcoded, shutdown_reason()}, - begin - ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]), - case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of - true -> skip; - _ -> - {'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{reason => stringfy(Reason), - message => from_message(Msg) - }, - ?assertEqual(Expected, Resp) - end, - true - end). - -prop_message_delivered() -> - ?ALL({ClientInfo, Msg}, {clientinfo(), message()}, - begin - ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]), - case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of - true -> skip; - _ -> - {'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo), - message => from_message(Msg) - }, - ?assertEqual(Expected, Resp) - end, - true - end). - -prop_message_acked() -> - ?ALL({ClientInfo, Msg}, {clientinfo(), message()}, - begin - ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]), - case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of - true -> skip; - _ -> - {'on_message_acked', Resp} = emqx_exhook_demo_svr:take(), - Expected = - #{clientinfo => from_clientinfo(ClientInfo), - message => from_message(Msg) - }, - ?assertEqual(Expected, Resp) - end, - true - end). - -nodestr() -> - stringfy(node()). - -peerhost(#{peername := {Host, _}}) -> - ntoa(Host). - -sockport(#{sockname := {_, Port}}) -> - Port. - -%% copied from emqx_exhook - -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256})); -ntoa(IP) -> - list_to_binary(inet_parse:ntoa(IP)). - -maybe(undefined) -> <<>>; -maybe(B) -> B. - -properties(undefined) -> []; -properties(M) when is_map(M) -> - maps:fold(fun(K, V, Acc) -> - [#{name => stringfy(K), - value => stringfy(V)} | Acc] - end, [], M). - -topicfilters(Tfs) when is_list(Tfs) -> - [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs]. - -%% @private -stringfy(Term) when is_binary(Term) -> - Term; -stringfy(Term) when is_integer(Term) -> - integer_to_binary(Term); -stringfy(Term) when is_atom(Term) -> - atom_to_binary(Term, utf8); -stringfy(Term) -> - unicode:characters_to_binary((io_lib:format("~0p", [Term]))). - -subopts(SubOpts) -> - #{qos => maps:get(qos, SubOpts, 0), - rh => maps:get(rh, SubOpts, 0), - rap => maps:get(rap, SubOpts, 0), - nl => maps:get(nl, SubOpts, 0), - share => maps:get(share, SubOpts, <<>>) - }. - -authresult_to_bool(AuthResult) -> - maps:get(auth_result, AuthResult, undefined) == success. - -aclresult_to_bool(Result) -> - Result == allow. - -pubsub_to_enum(publish) -> 'PUBLISH'; -pubsub_to_enum(subscribe) -> 'SUBSCRIBE'. - -from_conninfo(ConnInfo) -> - #{node => nodestr(), - clientid => maps:get(clientid, ConnInfo), - username => maybe(maps:get(username, ConnInfo, <<>>)), - peerhost => peerhost(ConnInfo), - sockport => sockport(ConnInfo), - proto_name => maps:get(proto_name, ConnInfo), - proto_ver => stringfy(maps:get(proto_ver, ConnInfo)), - keepalive => maps:get(keepalive, ConnInfo) - }. - -from_clientinfo(ClientInfo) -> - #{node => nodestr(), - clientid => maps:get(clientid, ClientInfo), - username => maybe(maps:get(username, ClientInfo, <<>>)), - password => maybe(maps:get(password, ClientInfo, <<>>)), - peerhost => ntoa(maps:get(peerhost, ClientInfo)), - sockport => maps:get(sockport, ClientInfo), - protocol => stringfy(maps:get(protocol, ClientInfo)), - mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)), - is_superuser => maps:get(is_superuser, ClientInfo, false), - anonymous => maps:get(anonymous, ClientInfo, true), - cn => maybe(maps:get(cn, ClientInfo, <<>>)), - dn => maybe(maps:get(dn, ClientInfo, <<>>)) - }. - -from_message(Msg) -> - #{node => nodestr(), - id => emqx_guid:to_hexstr(emqx_message:id(Msg)), - qos => emqx_message:qos(Msg), - from => stringfy(emqx_message:from(Msg)), - topic => emqx_message:topic(Msg), - payload => emqx_message:payload(Msg), - timestamp => emqx_message:timestamp(Msg) - }. - -%%-------------------------------------------------------------------- -%% Helper -%%-------------------------------------------------------------------- - -do_setup() -> - logger:set_primary_config(#{level => warning}), - _ = emqx_exhook_demo_svr:start(), - emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1), - %% waiting first loaded event - {'on_provider_loaded', _} = emqx_exhook_demo_svr:take(), - ok. - -do_teardown(_) -> - emqx_ct_helpers:stop_apps([emqx_exhook]), - %% waiting last unloaded event - {'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(), - _ = emqx_exhook_demo_svr:stop(), - logger:set_primary_config(#{level => notice}), - timer:sleep(2000), - ok. - -set_special_cfgs(emqx) -> - application:set_env(emqx, allow_anonymous, false), - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, modules_loaded_file, undefined), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins")); -set_special_cfgs(emqx_exhook) -> - ok. - -%%-------------------------------------------------------------------- -%% Generators -%%-------------------------------------------------------------------- - -conn_properties() -> - #{}. - -ack_properties() -> - #{}. - -sub_properties() -> - #{}. - -unsub_properties() -> - #{}. - -shutdown_reason() -> - oneof([utf8(), {shutdown, emqx_ct_proper_types:limited_atom()}]). - -authresult() -> - ?LET(RC, connack_return_code(), #{auth_result => RC}). - -inject_magic_into(Key, Object) -> - case castspell() of - muggles -> Object; - Spell -> - Object#{Key => Spell} - end. - -castspell() -> - L = [<<"baduser">>, <<"gooduser">>, <<"normaluser">>, muggles], - lists:nth(rand:uniform(length(L)), L). diff --git a/apps/emqx_exproto/.gitignore b/apps/emqx_exproto/.gitignore deleted file mode 100644 index 384f2255a..000000000 --- a/apps/emqx_exproto/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -.eunit -deps -!deps/.placeholder -*.o -*.beam -*.plt -erl_crash.dump -ebin -!ebin/.placeholder -.concrete/DEV_MODE -.rebar -test/ebin/*.beam -.exrc -plugins/*/ebin -log/ -*.swp -*.so -.erlang.mk/ -cover/ -emqx.d -eunit.coverdata -test/ct.cover.spec -logs -ct.coverdata -.idea/ -emqx.iml -_rel/ -data/ -_build -.rebar3 -rebar3.crashdump -.DS_Store -emqx.iml -bbmustache/ -etc/gen.emqx.conf -compile_commands.json -cuttlefish -rebar.lock -xrefr -erlang.mk -*.coverdata -etc/emqx_exproto.conf.rendered -Mnesia.*/ -src/emqx_exproto_pb.erl -src/emqx_exproto_v_1_connection_adapter_bhvr.erl -src/emqx_exproto_v_1_connection_adapter_client.erl -src/emqx_exproto_v_1_connection_handler_bhvr.erl -src/emqx_exproto_v_1_connection_handler_client.erl diff --git a/apps/emqx_exproto/docs/design-cn.md b/apps/emqx_exproto/docs/design-cn.md deleted file mode 100644 index 7af7dbdb3..000000000 --- a/apps/emqx_exproto/docs/design-cn.md +++ /dev/null @@ -1,127 +0,0 @@ -# 多语言 - 协议接入 - -`emqx-exproto` 插件用于协议解析的多语言支持。它能够允许其他编程语言(例如:Python,Java 等)直接处理数据流实现协议的解析,并提供 Pub/Sub 接口以实现与系统其它组件的通信。 - -该插件给 EMQ X 带来的扩展性十分的强大,它能以你熟悉语言处理任何的私有协议,并享受由 EMQ X 系统带来的高连接,和高并发的优点。 - -## 特性 - -- 极强的扩展能力。使用 gRPC 作为 RPC 通信框架,支持各个主流编程语言 -- 高吞吐。连接层以完全的异步非阻塞式 I/O 的方式实现 -- 连接层透明。完全的支持 TCP\TLS UDP\DTLS 类型的连接管理,并对上层提供统一的 API 接口 -- 连接层的管理能力。例如,最大连接数,连接和吞吐的速率限制,IP 黑名单 等 - -## 架构 - -![Extension-Protocol Arch](images/exproto-arch.jpg) - -该插件主要需要处理的内容包括: - -1. **连接层:** 该部分主要 **维持 Socket 的生命周期,和数据的收发**。它的功能要求包括: - - 监听某个端口。当有新的 TCP/UDP 连接到达后,启动一个连接进程,来维持连接的状态。 - - 调用 `OnSocketCreated` 回调。用于通知外部模块**已新建立了一个连接**。 - - 调用 `OnScoektClosed` 回调。用于通知外部模块连接**已关闭**。 - - 调用 `OnReceivedBytes` 回调。用于通知外部模块**该连接新收到的数据包**。 - - 提供 `Send` 接口。供外部模块调用,**用于发送数据包**。 - - 提供 `Close` 接口。供外部模块调用,**用于主动关闭连接**。 - -2. **协议/会话层:**该部分主要**提供 PUB/SUB 接口**,以实现与 EMQ X Broker 系统的消息互通。包括: - - - 提供 `Authenticate` 接口。供外部模块调用,用于向集群注册客户端。 - - 提供 `StartTimer` 接口。供外部模块调用,用于为该连接进程启动心跳等定时器。 - - 提供 `Publish` 接口。供外部模块调用,用于发布消息 EMQ X Broker 中。 - - 提供 `Subscribe` 接口。供外部模块调用,用于订阅某主题,以实现从 EMQ X Broker 中接收某些下行消息。 - - 提供 `Unsubscribe` 接口。供外部模块调用,用于取消订阅某主题。 - - 调用 `OnTimerTimeout` 回调。用于处理定时器超时的事件。 - - 调用 `OnReceivedMessages` 回调。用于接收下行消息(在订阅主题成功后,如果主题上有消息,便会回调该方法) - -## 接口设计 - -从 gRPC 上的逻辑来说,emqx-exproto 会作为客户端向用户的 `ConnectionHandler` 服务发送回调请求。同时,它也会作为服务端向用户提供 `ConnectionAdapter` 服务,以提供 emqx-exproto 各个接口的访问。如图: - -![Extension Protocol gRPC Arch](images/exproto-grpc-arch.jpg) - - -详情参见:`priv/protos/exproto.proto`,例如接口的定义有: - -```protobuff -syntax = "proto3"; - -package emqx.exproto.v1; - -// The Broker side serivce. It provides a set of APIs to -// handle a protcol access -service ConnectionAdapter { - - // -- socket layer - - rpc Send(SendBytesRequest) returns (CodeResponse) {}; - - rpc Close(CloseSocketRequest) returns (CodeResponse) {}; - - // -- protocol layer - - rpc Authenticate(AuthenticateRequest) returns (CodeResponse) {}; - - rpc StartTimer(TimerRequest) returns (CodeResponse) {}; - - // -- pub/sub layer - - rpc Publish(PublishRequest) returns (CodeResponse) {}; - - rpc Subscribe(SubscribeRequest) returns (CodeResponse) {}; - - rpc Unsubscribe(UnsubscribeRequest) returns (CodeResponse) {}; -} - -service ConnectionHandler { - - // -- socket layer - - rpc OnSocketCreated(stream SocketCreatedRequest) returns (EmptySuccess) {}; - - rpc OnSocketClosed(stream SocketClosedRequest) returns (EmptySuccess) {}; - - rpc OnReceivedBytes(stream ReceivedBytesRequest) returns (EmptySuccess) {}; - - // -- pub/sub layer - - rpc OnTimerTimeout(stream TimerTimeoutRequest) returns (EmptySuccess) {}; - - rpc OnReceivedMessages(stream ReceivedMessagesRequest) returns (EmptySuccess) {}; -} -``` - -## 配置项设计 - -1. 以 **监听器(Listener)** 为基础,提供 TCP/UDP 的监听。 - - Listener 目前仅支持:TCP、TLS、UDP、DTLS。(ws、wss、quic 暂不支持) -2. 每个监听器,会指定一个 `ConnectionHandler` 的服务地址,用于调用外部模块的接口。 -3. emqx-exproto 还会监听一个 gRPC 端口用于提供对 `ConnectionAdapter` 服务的访问。 - -例如: - -``` properties -## gRPC 服务监听地址 (HTTP) -## -exproto.server.http.url = http://127.0.0.1:9002 - -## gRPC 服务监听地址 (HTTPS) -## -exproto.server.https.url = https://127.0.0.1:9002 -exproto.server.https.cacertfile = ca.pem -exproto.server.https.certfile = cert.pem -exproto.server.https.keyfile = key.pem - -## Listener 配置 -## 例如,名称为 protoname 协议的 TCP 监听器配置 -exproto.listener.protoname = tcp://0.0.0.0:7993 - -## ConnectionHandler 服务地址及 https 的证书配置 -exproto.listener.protoname.connection_handler_url = http://127.0.0.1:9001 -#exproto.listener.protoname.connection_handler_certfile = -#exproto.listener.protoname.connection_handler_cacertfile = -#exproto.listener.protoname.connection_handler_keyfile = - -# ... -``` diff --git a/apps/emqx_exproto/docs/images/exproto-arch.jpg b/apps/emqx_exproto/docs/images/exproto-arch.jpg deleted file mode 100644 index dddf7996b98c61bf55f911a35e0f51e76c8e8728..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72633 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