From aafdf29cd803b2b5d6fb29bd6df98594f25fd3ba Mon Sep 17 00:00:00 2001 From: lafirest Date: Fri, 6 Aug 2021 19:08:20 +0800 Subject: [PATCH 001/306] refactor(emqx_gateway): refactor the emqx_coap --- apps/emqx_gateway/etc/emqx_gateway.conf | 30 +- .../src/bhvrs/emqx_gateway_channel.erl | 1 - apps/emqx_gateway/src/coap/README.md | 505 +++++++++++++----- apps/emqx_gateway/src/coap/doc/flow.png | Bin 0 -> 111145 bytes .../src/coap/doc/shared_state.png | Bin 0 -> 31330 bytes apps/emqx_gateway/src/coap/doc/transport.png | Bin 0 -> 151279 bytes .../src/coap/emqx_coap_channel.erl | 275 ++++++---- .../emqx_gateway/src/coap/emqx_coap_frame.erl | 2 - apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 25 +- .../src/coap/emqx_coap_message.erl | 36 +- .../src/coap/emqx_coap_observe_res.erl | 20 +- .../src/coap/emqx_coap_session.erl | 247 ++++++--- apps/emqx_gateway/src/coap/emqx_coap_tm.erl | 40 +- .../src/coap/emqx_coap_transport.erl | 59 +- .../coap/handler/emqx_coap_mqtt_handler.erl | 40 ++ .../coap/handler/emqx_coap_pubsub_handler.erl | 155 ++++++ .../src/coap/include/emqx_coap.hrl | 7 +- .../resources/emqx_coap_mqtt_resource.erl | 153 ------ .../resources/emqx_coap_pubsub_resource.erl | 219 -------- .../resources/emqx_coap_pubsub_topics.erl | 185 ------- apps/emqx_gateway/src/emqx_gateway_cm.erl | 29 +- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 14 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 3 +- 23 files changed, 1044 insertions(+), 1001 deletions(-) create mode 100644 apps/emqx_gateway/src/coap/doc/flow.png create mode 100644 apps/emqx_gateway/src/coap/doc/shared_state.png create mode 100644 apps/emqx_gateway/src/coap/doc/transport.png create mode 100644 apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl create mode 100644 apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl delete mode 100644 apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl delete mode 100644 apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl delete mode 100644 apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 16315b012..6c7928174 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -39,9 +39,22 @@ gateway: { coap.1: { enable_stats: false - authentication.enable: false + + authentication: { + enable: true + authenticators: [ + { + name: "authenticator1" + mechanism: password-based + server_type: built-in-database + user_id_type: clientid + } + ] + } + + #authentication.enable: false + heartbeat: 30s - resource: mqtt notify_type: qos subscribe_qos: qos0 publish_qos: qos1 @@ -50,19 +63,6 @@ gateway: { } } - coap.2: { - enable_stats: false - authentication.enable:false - heartbeat: 30s - resource: pubsub - notify_type: non - subscribe_qos: qos2 - publish_qos: coap - listener.udp.1: { - bind: 5687 - } - } - mqttsn.1: { ## The MQTT-SN Gateway ID in ADVERTISE message. gateway_id: 1 diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl index 1a032a017..abd7391bd 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl @@ -94,4 +94,3 @@ %% @doc The callback for process terminated -callback terminate(any(), channel()) -> ok. - diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index f71938c92..c451d6533 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -1,190 +1,401 @@ # Table of Contents -1. [EMQX 5.0 CoAP Gateway](#org6feb6de) -2. [CoAP Message Processing Flow](#org8458c1a) - 1. [Request Timing Diagram](#orgeaa4f53) - 1. [Transport && Transport Manager](#org88207b8) - 2. [Resource](#orgb32ce94) -3. [Resource](#org8956f90) - 1. [MQTT Resource](#orge8c21b1) - 2. [PubSub Resource](#org68ddce7) -4. [Heartbeat](#orgffdfecd) -5. [Command](#org43004c2) -6. [MQTT QOS <=> CoAP non/con](#org0157b5c) +1. [EMQX 5.0 CoAP Gateway](#org61e5bb8) + 1. [Features](#orgeddbc94) + 1. [PubSub Handler](#orgfc7be2d) + 2. [MQTT Handler](#org55be508) + 3. [Heartbeat](#org3d1a32e) + 4. [Query String](#org9a6b996) + 2. [Implementation](#org9985dfe) + 1. [Request/Response flow](#orge94210c) - + # EMQX 5.0 CoAP Gateway -emqx-coap is a CoAP Gateway for EMQ X Broker. -It translates CoAP messages into MQTT messages and make it possible to communiate between CoAP clients and MQTT clients. +emqx-coap is a CoAP Gateway for EMQ X Broker. It translates CoAP messages into MQTT messages and make it possible to communiate between CoAP clients and MQTT clients. - + -# CoAP Message Processing Flow +## Features + +- Partially achieves [Publish-Subscribe Broker for the Constrained Application Protocol (CoAP)](https://datatracker.ietf.org/doc/html/draft-ietf-core-coap-pubsub-09) + we called this as ps handler, include following functions: + - Publish + - Subscribe + - UnSubscribe +- Long connection and authorization verification called as MQTT handler - + -## Request Timing Diagram +### PubSub Handler +1. Publish - ,------. ,------------. ,-----------------. ,---------. ,--------. - |client| |coap_gateway| |transport_manager| |transport| |resource| - `--+---' `-----+------' `--------+--------' `----+----' `---+----' - | | | | | - | -------------------> | | | - | | | | | - | | | | | - | | ------------------------>| | | - | | | | | - | | | | | - | | |----------------------->| | - | | | | | - | | | | | - | | | |------------------>| - | | | | | - | | | | | - | | | |<------------------| - | | | | | - | | | | | - | | |<-----------------------| | - | | | | | - | | | | | - | | <------------------------| | | - | | | | | - | | | | | - | <------------------- | | | - ,--+---. ,-----+------. ,--------+--------. ,----+----. ,---+----. - |client| |coap_gateway| |transport_manager| |transport| |resource| - `------' `------------' `-----------------' `---------' `--------' + Method: POST\ + URI Schema: ps/{+topic}{?q\*}\ + q\*: [Shared Options](#orgc50043b)\ + Response: + - 2.04 "Changed" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" when with wrong auth uri query - +2. Subscribe -### Transport && Transport Manager + Method: GET + Options: -Transport is a module that manages the life cycle and behaviour of CoAP messages\ -And the transport manager is to manage all transport which in this gateway + - Observer = 0 + URI Schema: ps/{+topic}{?q\*}\ + q\*: see [Shared Options](#orgc50043b)\ + Response: - + - 2.05 "Content" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" when with wrong auth uri query -### Resource - -The Resource is a behaviour that must implement GET/PUT/POST/DELETE method\ -Different Resources can have different implementations of this four method\ -Each gateway can only use one Resource module to process CoAP Request Message - - - - -# Resource - - - - -## MQTT Resource - -The MQTT Resource is a simple CoAP to MQTT adapter, the implementation of each method is as follows: - -- use uri path as topic -- GET: subscribe the topic -- PUT: publish message to this topic -- POST: like PUT -- DELETE: unsubscribe the topic - - - - -## PubSub Resource - -The PubSub Resource like the MQTT Resource, but has a retained topic's message database\ -This Resource is shared, only can has one instance. The implementation: - -- use uri path as topic -- GET: - - GET with observe = 0: subscribe the topic - - GET with observe = 1: unsubscribe the topic - - GET without observe: read lastest message from the message database, key is the topic -- PUT: - insert message into the message database, key is the topic -- POST: - like PUT, but will publish the message -- DELETE: - delete message from the database, key is topic - - - - -# Heartbeat - -At present, the CoAP gateway only supports UDP/DTLS connection, don't support UDP over TCP and UDP over WebSocket. -Because UDP is connectionless, so the client needs to send heartbeat ping to the server interval. Otherwise, the server will close related resources -Use ****POST with empty uri path**** as a heartbeat ping - -example: ``` -coap-client -m post coap://127.0.0.1 + Client1 Client2 Broker + | | Subscribe | + | | ----- GET /ps/topic1 Observe:0 Token:XX ----> | + | | | + | | <---------- 2.05 Content Observe:10---------- | + | | | + | | | + | | Publish | + | ---------|----------- PUT /ps/topic1 "1033.3" --------> | + | | Notify | + | | <---------- 2.05 Content Observe:11 --------- | + | | | ``` - +3. UnSubscribe -# Command + Method : GET + Options: -Command is means the operation which outside the CoAP protocol, like authorization -The Command format: + - Observe = 1 -1. use ****POST**** method -2. uri path is empty -3. query string is like ****action=comandX&argX=valuex&argY=valueY**** + URI Schema: ps/{+topic}{?q\*}\ + q\*: see [Shared Options](#orgc50043b)\ + Response: -example: -1. connect: -``` -coap-client -m post coap://127.0.0.1?action=connect&clientid=XXX&username=XXX&password=XXX -``` -2. disconnect: -``` -coap-client -m post coap://127.0.0.1?action=disconnect -``` + - 2.07 "No Content" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" when with wrong auth uri query - -# MQTT QOS <=> CoAP non/con + -CoAP gateway uses some options to control the conversion between MQTT qos and coap non/con: +### MQTT Handler -1.notify_type -Control the type of notify messages when the observed object has changed.Can be: + Establishing a connection is optional. If the CoAP client needs to use connection-based operations, it must first establish a connection. +At the same time, the connectionless mode and the connected mode cannot be mixed. +In connection mode, the Publish/Subscribe/UnSubscribe sent by the client must be has Token and ClientId in query string. +If the Token and Clientid is wrong/miss, EMQ X will reset the request. +The communication token is the data carried in the response payload after the client successfully establishes a connection. +After obtaining the token, the client's subsequent request must attach "token=Token" to the Query String +ClientId is necessary when there is a connection, and is a unique identifier defined by the client. +The server manages the client through the ClientId. If the ClientId is wrong, EMQ X will reset the request. -- non -- con -- qos - in this value, MQTT QOS0 -> non, QOS1/QOS2 -> con +1. Create a Connection -2.subscribe_qos -Control the qos of subscribe.Can be: + Method: POST + URI Schema: mqtt/{+topic}{?q\*} + q\*: -- qos0 -- qos1 -- qos2 -- coap - in this value, CoAP non -> qos0, con -> qos1 + - clientId := client uid + - username + - password -3.publish_qos -like subscribe_qos, but control the qos of the publish MQTT message + Response: -License -------- + - 2.01 "Created" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" wrong username or password -Apache License Version 2.0 + Payload: Token if success -Author ------- +2. Close a Connection -EMQ X Team. + Method : DELETE + URI Schema: mqtt/{+topic}{?q\*} + q\*: + + - clientId := client uid + - token + + Resonse: + + - 2.01 "Deleted" when success + - 4.00 "Bad Request" when error + - 4.01 "Unauthorized" wrong clientid or token + + + + +### Heartbeat + +The Coap client can maintain the "connection" with the server through the heartbeat (regardless of whether it is authenticated or not), so that the server will not release related resources +Method : PUT +URI Schema: mqtt/{+topic}{?q\*} +q\*: + +- clientId if authenticated +- token if authenticated + +Response: + +- 2.01 "Changed" when success +- 4.00 "Bad Request" when error +- 4.01 "Unauthorized" wrong clientid or token + + + + +### Query String + +CoAP gateway uses some options in query string to conversion between MQTT CoAP. + +1. Shared Options + + - clientId + - token + +2. Connect Options + + - username + - password + +3. Publish + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
optionvalue typedefault
retainbooleanfalse
qosMQTT QOSSee here
expiryMessage Expiry Interval0(Never expiry)
+ +4. Subscribe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
optionvalue typedefault
qosMQTT QOSSee here
nlMQTT Subscribe No Local0
rhMQTT Subscribe Retain Handing0
+ +5. MQTT QOS <=> CoAP non/con + + 1.notif_type + Control the type of notify messages when the observed object has changed.Can be: + + - non + - con + - qos + in this value, MQTT QOS0 -> non, QOS1/QOS2 -> con + + 2.subscribe_qos + Control the qos of subscribe.Can be: + + - qos0 + - qos1 + - qos2 + - coap + in this value, CoAP non -> qos0, con -> qos1 + + 3.publish_qos + like subscribe_qos, but control the qos of the publish MQTT message + + + + +## Implementation + + + + +### Request/Response flow + +![img](./doc/flow.png) + +1. Authorization check + + Check whether the clientid and token in the query string match the current connection + +2. Session + + Manager the "Transport Mnager" "Observe Resouces Manger" and next message id + +3. Transport Mnager + + Manager "Transport" create/close/dispatch + +4. Observe resources Mnager + + Mnager observe topic and token + +5. Transport + + ![img](./doc/transport.png) + + 1. Shared State + + ![img](./doc/shared_state.png) + +6. Handler + + 1. pubsub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodObserveAction
GET0subscribe and reply result
GET1unsubscribe and reply result
POSTXpublish and reply result
+ + 2. mqtt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodAction
PUTreply result
POSTreturn create connection action
DELETEreturn close connection action
diff --git a/apps/emqx_gateway/src/coap/doc/flow.png b/apps/emqx_gateway/src/coap/doc/flow.png new file mode 100644 index 0000000000000000000000000000000000000000..5c72883487b21f467864698eaeca248bfbefd839 GIT binary patch literal 111145 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 z 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

#L>xcc87a}!?W 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 literal 0 HcmV?d00001 diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index c208d00f8..7612a6142 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -27,10 +27,11 @@ -export([ info/1 , info/2 , stats/1 - , auth_publish/2 - , auth_subscribe/2 - , reply/4 - , ack/4 + , validator/3 + , get_clientinfo/1 + , get_config/2 + , get_config/3 + , result_keys/0 , transfer_result/3]). -export([ init/2 @@ -60,9 +61,16 @@ keepalive :: emqx_keepalive:keepalive() | undefined, %% Timer timers :: #{atom() => disable | undefined | reference()}, + token :: binary() | undefined, config :: hocon:config() }). +%% the execuate context for session call +-record(exec_ctx, { config :: hocon:config(), + ctx :: emqx_gateway_ctx:context(), + clientinfo :: emqx_types:clientinfo() + }). + -type channel() :: #channel{}. -define(DISCONNECT_WAIT_TIME, timer:seconds(10)). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). @@ -98,13 +106,18 @@ init(ConnInfo = #{peername := {PeerHost, _}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), Mountpoint = maps:get(mountpoint, Config, undefined), + EnableAuth = maps:get(enable, maps:get(authentication, Config)), ClientInfo = set_peercert_infos( Peercert, #{ zone => default , protocol => 'coap' , peerhost => PeerHost , sockport => SockPort - , clientid => emqx_guid:to_base62(emqx_guid:gen()) + , clientid => if EnableAuth -> + undefined; + true -> + emqx_guid:to_base62(emqx_guid:gen()) + end , username => undefined , is_bridge => false , is_superuser => false @@ -116,48 +129,52 @@ init(ConnInfo = #{peername := {PeerHost, _}, , conninfo = ConnInfo , clientinfo = ClientInfo , timers = #{} + , config = Config , session = emqx_coap_session:new() - , config = Config#{clientinfo => ClientInfo, - ctx => Ctx} , keepalive = emqx_keepalive:init(maps:get(heartbeat, Config)) }. -auth_publish(Topic, - #{ctx := Ctx, - clientinfo := ClientInfo}) -> - emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic). +validator(Type, Topic, #exec_ctx{ctx = Ctx, + clientinfo = ClientInfo}) -> + emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). -auth_subscribe(Topic, - #{ctx := Ctx, - clientinfo := ClientInfo}) -> - emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic). +get_clientinfo(#exec_ctx{clientinfo = ClientInfo}) -> + ClientInfo. + +get_config(Key, Ctx) -> + get_config(Key, Ctx, undefined). + +get_config(Key, #exec_ctx{config = Cfg}, Def) -> + maps:get(Key, Cfg, Def). + +result_keys() -> + [out, reply, connection]. transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT([out], From, Value, Result). + ?TRANSFER_RESULT(From, Value, Result). %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- -%% treat post to root path as a heartbeat -%% treat post to root path with query string as a command -handle_in(#coap_message{method = post, - options = Options} = Msg, ChannelT) -> - Channel = ensure_keepalive_timer(ChannelT), - case maps:get(uri_path, Options, <<>>) of - <<>> -> - handle_command(Msg, Channel); +handle_in(Msg, ChannleT) -> + Channel = ensure_keepalive_timer(ChannleT), + case convert_queries(Msg) of + {ok, Msg2} -> + case emqx_coap_message:is_request(Msg2) of + true -> + check_auth_state(Msg2, Channel); + _ -> + call_session(handle_response, Msg2, Channel) + end; _ -> - call_session(received, [Msg], Channel) - end; - -handle_in(Msg, Channel) -> - call_session(received, [Msg], ensure_keepalive_timer(Channel)). + response({error, bad_request}, <<"bad uri_query">>, Msg, Channel) + end. %%-------------------------------------------------------------------- %% Handle Delivers from broker to client %%-------------------------------------------------------------------- handle_deliver(Delivers, Channel) -> - call_session(deliver, [Delivers], Channel). + call_session(deliver, Delivers, Channel). %%-------------------------------------------------------------------- %% Handle timeout @@ -172,7 +189,7 @@ handle_timeout(_, {keepalive, NewVal}, #channel{keepalive = KeepAlive} = Channel end; handle_timeout(_, {transport, Msg}, Channel) -> - call_session(timeout, [Msg], Channel); + call_session(timeout, Msg, Channel); handle_timeout(_, disconnect, Channel) -> {shutdown, normal, Channel}; @@ -238,48 +255,123 @@ ensure_keepalive_timer(Fun, #channel{config = Cfg} = Channel) -> Interval = maps:get(heartbeat, Cfg), Fun(keepalive, Interval, keepalive, Channel). -handle_command(#coap_message{options = Options} = Msg, Channel) -> - case maps:get(uri_query, Options, []) of - [] -> - %% heartbeat - ack(Channel, {ok, valid}, <<>>, Msg); - QueryPairs -> - Queries = lists:foldl(fun(Pair, Acc) -> - [{K, V}] = cow_qs:parse_qs(Pair), - Acc#{K => V} - end, - #{}, - QueryPairs), - case maps:get(<<"action">>, Queries, undefined) of - undefined -> - ack(Channel, {error, bad_request}, <<"command without actions">>, Msg); - Action -> - handle_command(Action, Queries, Msg, Channel) - end +call_session(Fun, + Msg, + #channel{session = Session} = Channel) -> + Ctx = new_exec_ctx(Channel), + Result = erlang:apply(emqx_coap_session, Fun, [Msg, Ctx, Session]), + process_result([session, connection, out], Result, Msg, Channel). + +process_result([Key | T], Result, Msg, Channel) -> + case handle_result(Key, Result, Msg, Channel) of + {ok, Channel2} -> + process_result(T, Result, Msg, Channel2); + Other -> + Other + end; + +process_result(_, _, _, Channel) -> + {ok, Channel}. + +handle_result(session, #{session := Session}, _, Channel) -> + {ok, Channel#channel{session = Session}}; + +handle_result(connection, #{connection := open}, Msg, Channel) -> + do_connect(Msg, Channel); + +handle_result(connection, #{connection := close}, Msg, Channel) -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, {outgoing, Reply}, Channel}; + +handle_result(out, #{out := Out}, _, Channel) -> + {ok, {outgoing, Out}, Channel}; + +handle_result(_, _, _, Channel) -> + {ok, Channel}. + +check_auth_state(Method, #channel{config = Cfg} = Channel) -> + #{authentication := #{enable := Enable}} = Cfg, + check_token(Enable, Method, Channel). + +check_token(true, + #coap_message{options = Options} = Msg, + #channel{token = Token, + clientinfo = ClientInfo} = Channel) -> + #{clientid := ClientId} = ClientInfo, + case maps:get(uri_query, Options, undefined) of + #{<<"clientid">> := ClientId, + <<"token">> := Token} -> + call_session(handle_request, Msg, Channel); + #{<<"clientid">> := DesireId} -> + try_takeover(ClientId, DesireId, Msg, Channel); + _ -> + response({error, unauthorized}, Msg, Channel) + end; + +check_token(false, + #coap_message{options = Options} = Msg, + Channel) -> + case maps:get(uri_query, Options, undefined) of + #{<<"clientid">> := _} -> + response({error, unauthorized}, Msg, Channel); + #{<<"token">> := _} -> + response({error, unauthorized}, Msg, Channel); + _ -> + call_session(handle_request, Msg, Channel) end. -handle_command(<<"connect">>, Queries, Msg, Channel) -> +response(Method, Req, Channel) -> + response(Method, <<>>, Req, Channel). + +response(Method, Payload, Req, Channel) -> + Reply = emqx_coap_message:piggyback(Method, Payload, Req), + call_session(handle_out, Reply, Channel). + +try_takeover(undefined, + DesireId, + #coap_message{options = Opts} = Msg, + Channel) -> + case maps:get(uri_path, Opts, []) of + [<<"mqtt">>, <<"connection">> | _] -> + %% may be is a connect request + %% TODO need check repeat connect, unless we implement the + %% udp connection baseon the clientid + call_session(handle_request, Msg, Channel); + _ -> + do_takeover(DesireId, Msg, Channel) + end; + +try_takeover(_, DesireId, Msg, Channel) -> + do_takeover(DesireId, Msg, Channel). + +do_takeover(_DesireId, Msg, Channel) -> + %% TODO completed the takeover, now only reset the message + Reset = emqx_coap_message:reset(Msg), + call_session(handle_out, Reset, Channel). + +new_exec_ctx(#channel{config = Cfg, + ctx = Ctx, + clientinfo = ClientInfo}) -> + #exec_ctx{config = Cfg, + ctx = Ctx, + clientinfo = ClientInfo}. + +do_connect(#coap_message{options = Opts} = Req, Channel) -> + Queries = maps:get(uri_query, Opts), case emqx_misc:pipeline( [ fun run_conn_hooks/2 , fun enrich_clientinfo/2 , fun set_log_meta/2 , fun auth_connect/2 ], - {Queries, Msg}, + {Queries, Req}, Channel) of {ok, _Input, NChannel} -> - process_connect(ensure_connected(NChannel), Msg); + process_connect(ensure_connected(NChannel), Req); {error, ReasonCode, NChannel} -> ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), - ack(NChannel, {error, bad_request}, ErrMsg, Msg) - end; - -handle_command(<<"disconnect">>, _, Msg, Channel) -> - Channel2 = ensure_timer(disconnect, ?DISCONNECT_WAIT_TIME, disconnect, Channel), - ack(Channel2, {ok, deleted}, <<>>, Msg); - -handle_command(_, _, Msg, Channel) -> - ack(Channel, {error, bad_request}, <<"invalid action">>, Msg). + response({error, bad_request}, ErrMsg, Req, NChannel) + end. run_conn_hooks(Input, Channel = #channel{ctx = Ctx, conninfo = ConnInfo}) -> @@ -291,8 +383,7 @@ run_conn_hooks(Input, Channel = #channel{ctx = Ctx, end. enrich_clientinfo({Queries, Msg}, - Channel = #channel{clientinfo = ClientInfo0, - config = Cfg}) -> + Channel = #channel{clientinfo = ClientInfo0}) -> case Queries of #{<<"username">> := UserName, <<"password">> := Password, @@ -301,8 +392,7 @@ enrich_clientinfo({Queries, Msg}, password => Password, clientid => ClientId}, {ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo), - {ok, Channel#channel{clientinfo = NClientInfo, - config = Cfg#{clientinfo := NClientInfo}}}; + {ok, Channel#channel{clientinfo = NClientInfo}}; _ -> {error, "invalid queries", Channel} end. @@ -324,7 +414,8 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, {error, Reason} end. -fix_mountpoint(_Packet, #{mountpoint := undefined}) -> ok; +fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> + {ok, ClientInfo}; fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> %% TODO: Enrich the varibale replacement???? %% i.e: ${ClientInfo.auth_result.productKey} @@ -334,27 +425,33 @@ fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> ensure_connected(Channel = #channel{ctx = Ctx, conninfo = ConnInfo, clientinfo = ClientInfo}) -> - NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)}, + NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) + , proto_name => <<"COAP">> + , proto_ver => <<"1">> + }, ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), Channel#channel{conninfo = NConnInfo}. process_connect(Channel = #channel{ctx = Ctx, + session = Session, conninfo = ConnInfo, clientinfo = ClientInfo}, Msg) -> - SessFun = fun(_,_) -> emqx_coap_session:new() end, + %% inherit the old session + SessFun = fun(_,_) -> Session end, case emqx_gateway_ctx:open_session( Ctx, true, ClientInfo, ConnInfo, - SessFun + SessFun, + emqx_coap_session ) of {ok, _Sess} -> - ack(Channel, {ok, created}, <<"connected">>, Msg); + response({ok, created}, <<"connected">>, Msg, Channel); {error, Reason} -> ?LOG(error, "Failed to open session du to ~p", [Reason]), - ack(Channel, {error, bad_request}, <<>>, Msg) + response({error, bad_request}, Msg, Channel) end. run_hooks(Ctx, Name, Args) -> @@ -365,24 +462,20 @@ run_hooks(Ctx, Name, Args, Acc) -> emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_hooks:run_fold(Name, Args, Acc). -reply(Channel, Method, Payload, Req) -> - call_session(reply, [Req, Method, Payload], Channel). - -ack(Channel, Method, Payload, Req) -> - call_session(piggyback, [Req, Method, Payload], Channel). - -call_session(F, - A, - #channel{session = Session, - config = Cfg} = Channel) -> - case erlang:apply(emqx_coap_session, F, A ++ [Cfg, Session]) of - #{out := Out, - session := Session2} -> - {ok, {outgoing, Out}, Channel#channel{session = Session2}}; - #{out := Out} -> - {ok, {outgoing, Out}, Channel}; - #{session := Session2} -> - {ok, Channel#channel{session = Session2}}; - _ -> - {ok, Channel} +convert_queries(#coap_message{options = Opts} = Msg) -> + case maps:get(uri_query, Opts, undefined) of + undefined -> + {ok, Msg#coap_message{options = Opts#{uri_query => #{}}}}; + Queries -> + convert_queries(Queries, #{}, Msg) end. + +convert_queries([H | T], Queries, Msg) -> + case re:split(H, "=") of + [Key, Val] -> + convert_queries(T, Queries#{Key => Val}, Msg); + _ -> + error + end; +convert_queries([], Queries, #coap_message{options = Opts} = Msg) -> + {ok, Msg#coap_message{options = Opts#{uri_query => Queries}}}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl index 9a53f3e01..c1bc08928 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl @@ -161,9 +161,7 @@ encode_option(location_query, OptVal) -> {?OPTION_LOCATION_QUERY, OptVal}; encode_option(proxy_uri, OptVal) -> {?OPTION_PROXY_URI, OptVal}; encode_option(proxy_scheme, OptVal) -> {?OPTION_PROXY_SCHEME, OptVal}; encode_option(size1, OptVal) -> {?OPTION_SIZE1, binary:encode_unsigned(OptVal)}; -%% draft-ietf-ore-observe-16 encode_option(observe, OptVal) -> {?OPTION_OBSERVE, binary:encode_unsigned(OptVal)}; -%% draft-ietf-ore-block-17 encode_option(block2, OptVal) -> {?OPTION_BLOCK2, encode_block(OptVal)}; encode_option(block1, OptVal) -> {?OPTION_BLOCK1, encode_block(OptVal)}; %% unknown opton diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index 6d27cd85a..09426a13d 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -57,17 +57,14 @@ init([]) -> %%-------------------------------------------------------------------- on_insta_create(_Insta = #{id := InstaId, - rawconf := #{resource := Resource} = RawConf + rawconf := RawConf }, Ctx, _GwState) -> - ResourceMod = get_resource_mod(Resource), Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, ResourceMod, Lis) + start_listener(InstaId, Ctx, Lis) end, Listeners), - {ok, ResCtx} = ResourceMod:init(RawConf), - {ok, ListenerPids, #{ctx => Ctx, - res_ctx => ResCtx}}. + {ok, ListenerPids, #{ctx => Ctx}}. on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> InstaId = maps:get(id, NewInsta), @@ -85,12 +82,10 @@ on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> end. on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := #{resource := Resource} = RawConf + rawconf := RawConf }, - #{res_ctx := ResCtx} = _GwInstaState, + _GwInstaState, _GWState) -> - ResourceMod = get_resource_mod(Resource), - ok = ResourceMod:stop(ResCtx), Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> stop_listener(InstaId, Lis) @@ -100,10 +95,9 @@ on_insta_destroy(_Insta = #{ id := InstaId, %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, ResourceMod, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - Cfg2 = Cfg#{resource => ResourceMod}, - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg2) of + case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> ?ULOG("Start coap ~s:~s listener on ~s successfully.~n", [InstaId, Type, ListenOnStr]), @@ -148,8 +142,3 @@ stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> Name = name(InstaId, Type), esockd:close(Name, ListenOn). - -get_resource_mod(mqtt) -> - emqx_coap_mqtt_resource; -get_resource_mod(pubsub) -> - emqx_coap_pubsub_resource. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_message.erl b/apps/emqx_gateway/src/coap/emqx_coap_message.erl index 52a03c418..2e9fb144e 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_message.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_message.erl @@ -24,7 +24,13 @@ %% convenience functions for message construction -module(emqx_coap_message). --export([request/2, request/3, request/4, ack/1, response/1, response/2, response/3]). +-export([ request/2, request/3, request/4 + , ack/1, response/1, response/2 + , reset/1, piggyback/2, piggyback/3 + , response/3]). + +-export([is_request/1]). + -export([set/3, set_payload/2, get_content/1, set_content/2, set_content/3, get_option/2]). -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). @@ -42,10 +48,13 @@ request(Type, Method, Content=#coap_content{}, Options) -> set_content(Content, #coap_message{type = Type, method = Method, options = Options}). -ack(Request = #coap_message{}) -> - #coap_message{type = ack, - id = Request#coap_message.id}. +ack(#coap_message{id = Id}) -> + #coap_message{type = ack, id = Id}. +reset(#coap_message{id = Id}) -> + #coap_message{type = reset, id = Id}. + +%% just make a response response(#coap_message{type = Type, id = Id, token = Token}) -> @@ -61,6 +70,19 @@ response(Method, Payload, Request) -> set_payload(Payload, response(Request))). +%% make a response which maybe is a piggyback ack +piggyback(Method, Request) -> + piggyback(Method, <<>>, Request). + +piggyback(Method, Payload, Request) -> + Reply = response(Method, Payload, Request), + case Reply of + #coap_message{type = con} -> + Reply#coap_message{type = ack}; + _ -> + Reply + end. + %% omit option for its default value set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg; @@ -144,3 +166,9 @@ set_payload_block(Content, BlockId, {Num, _, Size}, Msg) -> set(BlockId, {Num, false, Size}, set_payload(binary:part(Content, OffsetBegin, ContentSize - OffsetBegin), Msg)) end. + +is_request(#coap_message{method = Method}) when is_atom(Method) -> + Method =/= undefined; + +is_request(_) -> + false. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl b/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl index 3cf925448..20473322e 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_observe_res.erl @@ -18,7 +18,7 @@ %% API -export([ new_manager/0, insert/3, remove/2 - , res_changed/2, foreach/2]). + , res_changed/2, foreach/2, subscriptions/1]). -export_type([manager/0]). -define(MAX_SEQ_ID, 16777215). @@ -40,14 +40,15 @@ new_manager() -> #{}. --spec insert(topic(), token(), manager()) -> manager(). +-spec insert(topic(), token(), manager()) -> {seq_id(), manager()}. insert(Topic, Token, Manager) -> - case maps:get(Topic, Manager, undefined) of - undefined -> - Manager#{Topic => new_res(Token)}; - _ -> - Manager - end. + Res = case maps:get(Topic, Manager, undefined) of + undefined -> + new_res(Token); + Any -> + Any + end, + {maps:get(seq_id, Res), Manager#{Topic => Res}}. -spec remove(topic(), manager()) -> manager(). remove(Topic, Manager) -> @@ -72,6 +73,9 @@ foreach(F, Manager) -> Manager), ok. +subscriptions(Manager) -> + maps:keys(Manager). + %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index 8b9eed14c..98e24f05c 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -23,11 +23,14 @@ %% API -export([new/0, transfer_result/3]). --export([ received/3 - , reply/4 - , reply/5 - , ack/3 - , piggyback/4 +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ handle_request/3 + , handle_response/3 + , handle_out/3 , deliver/3 , timeout/3]). @@ -36,10 +39,31 @@ -record(session, { transport_manager :: emqx_coap_tm:manager() , observe_manager :: emqx_coap_observe_res:manager() , next_msg_id :: coap_message_id() + , created_at :: pos_integer() }). -type session() :: #session{}. +%% steal from emqx_session +-define(INFO_KEYS, [subscriptions, + upgrade_qos, + retry_interval, + await_rel_timeout, + created_at + ]). + +-define(STATS_KEYS, [subscriptions_cnt, + subscriptions_max, + inflight_cnt, + inflight_max, + mqueue_len, + mqueue_max, + mqueue_dropped, + next_pkt_id, + awaiting_rel_cnt, + awaiting_rel_max + ]). + %%%------------------------------------------------------------------- %%% API %%%------------------------------------------------------------------- @@ -48,125 +72,163 @@ new() -> _ = emqx_misc:rand_seed(), #session{ transport_manager = emqx_coap_tm:new() , observe_manager = emqx_coap_observe_res:new_manager() - , next_msg_id = rand:uniform(?MAX_MESSAGE_ID)}. + , next_msg_id = rand:uniform(?MAX_MESSAGE_ID) + , created_at = erlang:system_time(millisecond)}. + +%%-------------------------------------------------------------------- +%% Info, Stats +%%-------------------------------------------------------------------- +%% @doc Compatible with emqx_session +%% do we need use inflight and mqueue in here? +-spec(info(session()) -> emqx_types:infos()). +info(Session) -> + maps:from_list(info(?INFO_KEYS, Session)). + +info(Keys, Session) when is_list(Keys) -> + [{Key, info(Key, Session)} || Key <- Keys]; +info(subscriptions, #session{observe_manager = OM}) -> + emqx_coap_observe_res:subscriptions(OM); +info(subscriptions_cnt, #session{observe_manager = OM}) -> + erlang:length(emqx_coap_observe_res:subscriptions(OM)); +info(subscriptions_max, _) -> + infinity; +info(upgrade_qos, _) -> + ?QOS_0; +info(inflight, _) -> + emqx_inflight:new(); +info(inflight_cnt, _) -> + 0; +info(inflight_max, _) -> + 0; +info(retry_interval, _) -> + infinity; +info(mqueue, _) -> + emqx_mqueue:init(#{max_len => 0, store_qos0 => false}); +info(mqueue_len, #session{transport_manager = TM}) -> + maps:size(TM); +info(mqueue_max, _) -> + 0; +info(mqueue_dropped, _) -> + 0; +info(next_pkt_id, #session{next_msg_id = PacketId}) -> + PacketId; +info(awaiting_rel, _) -> + #{}; +info(awaiting_rel_cnt, _) -> + 0; +info(awaiting_rel_max, _) -> + infinity; +info(await_rel_timeout, _) -> + infinity; +info(created_at, #session{created_at = CreatedAt}) -> + CreatedAt. + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(Session) -> info(?STATS_KEYS, Session). %%%------------------------------------------------------------------- %%% Process Message %%%------------------------------------------------------------------- -received(#coap_message{type = ack} = Msg, Cfg, Session) -> - handle_response(Msg, Cfg, Session); +handle_request(Msg, Ctx, Session) -> + call_transport_manager(?FUNCTION_NAME, + Msg, + Ctx, + [fun process_tm/3, fun process_subscribe/3], + Session). -received(#coap_message{type = reset} = Msg, Cfg, Session) -> - handle_response(Msg, Cfg, Session); +handle_response(Msg, Ctx, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session). -received(#coap_message{method = Method} = Msg, Cfg, Session) when is_atom(Method) -> - handle_request(Msg, Cfg, Session); +handle_out(Msg, Ctx, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session). -received(Msg, Cfg, Session) -> - handle_response(Msg, Cfg, Session). - -reply(Req, Method, Cfg, Session) -> - reply(Req, Method, <<>>, Cfg, Session). - -reply(Req, Method, Payload, Cfg, Session) -> - Response = emqx_coap_message:response(Method, Payload, Req), - handle_out(Response, Cfg, Session). - -ack(Req, Cfg, Session) -> - piggyback(Req, <<>>, Cfg, Session). - -piggyback(Req, Payload, Cfg, Session) -> - Response = emqx_coap_message:ack(Req), - Response2 = emqx_coap_message:set_payload(Payload, Response), - handle_out(Response2, Cfg, Session). - -deliver(Delivers, Cfg, Session) -> +deliver(Delivers, Ctx, Session) -> Fun = fun({_, Topic, Message}, #{out := OutAcc, session := #session{observe_manager = OM, - next_msg_id = MsgId} = SAcc} = Acc) -> + next_msg_id = MsgId, + transport_manager = TM} = SAcc} = Acc) -> case emqx_coap_observe_res:res_changed(Topic, OM) of undefined -> Acc; {Token, SeqId, OM2} -> - Msg = mqtt_to_coap(Message, MsgId, Token, SeqId, Cfg), - SAcc2 = SAcc#session{next_msg_id = next_msg_id(MsgId), + Msg = mqtt_to_coap(Message, MsgId, Token, SeqId, Ctx), + SAcc2 = SAcc#session{next_msg_id = next_msg_id(MsgId, TM), observe_manager = OM2}, - #{out := Out} = Result = call_transport_manager(handle_out, Msg, Cfg, SAcc2), + #{out := Out} = Result = handle_out(Msg, Ctx, SAcc2), Result#{out := [Out | OutAcc]} end end, lists:foldl(Fun, - #{out => [], - session => Session}, + #{out => [], session => Session}, Delivers). -timeout(Timer, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Timer, Cfg, Session). +timeout(Timer, Ctx, Session) -> + call_transport_manager(?FUNCTION_NAME, Timer, Ctx, [fun process_tm/3], Session). + +result_keys() -> + [tm, subscribe] ++ emqx_coap_channel:result_keys(). transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT([out, subscribe], From, Value, Result). + ?TRANSFER_RESULT(From, Value, Result). %%%------------------------------------------------------------------- %%% Internal functions %%%------------------------------------------------------------------- -handle_request(Msg, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Cfg, Session). - -handle_response(Msg, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Cfg, Session). - -handle_out(Msg, Cfg, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Cfg, Session). - call_transport_manager(Fun, Msg, - Cfg, + Ctx, + Processor, #session{transport_manager = TM} = Session) -> try - Result = emqx_coap_tm:Fun(Msg, Cfg, TM), - {ok, _, Session2} = emqx_misc:pipeline([fun process_tm/2, - fun process_subscribe/2], - Result, - Session), - emqx_coap_channel:transfer_result(session, Session2, Result) + Result = emqx_coap_tm:Fun(Msg, Ctx, TM), + {ok, Result2, Session2} = pipeline(Processor, + Result, + Msg, + Session), + emqx_coap_channel:transfer_result(session, Session2, Result2) catch Type:Reason:Stack -> ?ERROR("process transmission with, message:~p failed~n Type:~p,Reason:~p~n,StackTrace:~p~n", [Msg, Type, Reason, Stack]), - #{out => emqx_coap_message:response({error, internal_server_error}, Msg)} + ?REPLY({error, internal_server_error}, Msg) end. -process_tm(#{tm := TM}, Session) -> +process_tm(#{tm := TM}, _, Session) -> {ok, Session#session{transport_manager = TM}}; -process_tm(_, Session) -> +process_tm(_, _, Session) -> {ok, Session}. -process_subscribe(#{subscribe := Sub}, #session{observe_manager = OM} = Session) -> +process_subscribe(#{subscribe := Sub} = Result, + Msg, + #session{observe_manager = OM} = Session) -> case Sub of undefined -> - {ok, Session}; + {ok, Result, Session}; {Topic, Token} -> - OM2 = emqx_coap_observe_res:insert(Topic, Token, OM), - {ok, Session#session{observe_manager = OM2}}; + {SeqId, OM2} = emqx_coap_observe_res:insert(Topic, Token, OM), + Replay = emqx_coap_message:piggyback({ok, content}, Msg), + Replay2 = Replay#coap_message{options = #{observe => SeqId}}, + {ok, Result#{reply => Replay2}, Session#session{observe_manager = OM2}}; Topic -> OM2 = emqx_coap_observe_res:remove(Topic, OM), - {ok, Session#session{observe_manager = OM2}} + Replay = emqx_coap_message:piggyback({ok, nocontent}, Msg), + {ok, Result#{reply => Replay}, Session#session{observe_manager = OM2}} end; -process_subscribe(_, Session) -> - {ok, Session}. +process_subscribe(Result, _, Session) -> + {ok, Result, Session}. -mqtt_to_coap(MQTT, MsgId, Token, SeqId, Cfg) -> +mqtt_to_coap(MQTT, MsgId, Token, SeqId, Ctx) -> #message{payload = Payload} = MQTT, - #coap_message{type = get_notify_type(MQTT, Cfg), + #coap_message{type = get_notify_type(MQTT, Ctx), method = {ok, content}, id = MsgId, token = Token, payload = Payload, - options = #{observe => SeqId, - max_age => get_max_age(MQTT)}}. + options = #{observe => SeqId}}. -get_notify_type(#message{qos = Qos}, #{notify_type := Type}) -> - case Type of +get_notify_type(#message{qos = Qos}, Ctx) -> + case emqx_coap_channel:get_config(notify_type, Ctx) of qos -> case Qos of ?QOS_0 -> @@ -178,18 +240,31 @@ get_notify_type(#message{qos = Qos}, #{notify_type := Type}) -> Other end. --spec get_max_age(#message{}) -> max_age(). -get_max_age(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}) -> - ?MAXIMUM_MAX_AGE; -get_max_age(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, - timestamp = Ts}) -> - Now = erlang:system_time(millisecond), - Diff = (Now - Ts + Interval * 1000) / 1000, - erlang:max(1, erlang:floor(Diff)); -get_max_age(_) -> - ?DEFAULT_MAX_AGE. +next_msg_id(MsgId, TM) -> + next_msg_id(MsgId + 1, MsgId, TM). -next_msg_id(MsgId) when MsgId >= ?MAX_MESSAGE_ID -> - 1; -next_msg_id(MsgId) -> - MsgId + 1. +next_msg_id(MsgId, MsgId, _) -> + erlang:throw("too many message in delivering"); +next_msg_id(MsgId, BeginId, TM) when MsgId >= ?MAX_MESSAGE_ID -> + check_is_inused(1, BeginId, TM); +next_msg_id(MsgId, BeginId, TM) -> + check_is_inused(MsgId, BeginId, TM). + +check_is_inused(NewMsgId, BeginId, TM) -> + case emqx_coap_tm:is_inused(out, NewMsgId, TM) of + false -> + NewMsgId; + _ -> + next_msg_id(NewMsgId + 1, BeginId, TM) + end. + +pipeline([Fun | T], Result, Msg, Session) -> + case Fun(Result, Msg, Session) of + {ok, Session2} -> + pipeline(T, Result, Msg, Session2); + {ok, Result2, Session2} -> + pipeline(T, Result2, Msg, Session2) + end; + +pipeline([], Result, _, Session) -> + {ok, Result, Session}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl index 8830d7447..5a664b0f2 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl @@ -21,7 +21,8 @@ , handle_request/3 , handle_response/3 , handle_out/3 - , timeout/3]). + , timeout/3 + , is_inused/3]). -export_type([manager/0, event_result/1]). @@ -60,18 +61,18 @@ new() -> #{}. -handle_request(#coap_message{id = MsgId} = Msg, Cfg, TM) -> +handle_request(#coap_message{id = MsgId} = Msg, Ctx, TM) -> Id = {in, MsgId}, case maps:get(Id, TM, undefined) of undefined -> Transport = emqx_coap_transport:new(), Machine = new_state_machine(Id, Transport), - process_event(in, Msg, TM, Machine, Cfg); + process_event(in, Msg, TM, Ctx, Machine); Machine -> - process_event(in, Msg, TM, Machine, Cfg) + process_event(in, Msg, TM, Ctx, Machine) end. -handle_response(#coap_message{type = Type, id = MsgId} = Msg, Cfg, TM) -> +handle_response(#coap_message{type = Type, id = MsgId} = Msg, Ctx, TM) -> Id = {out, MsgId}, case maps:get(Id, TM, undefined) of undefined -> @@ -79,26 +80,25 @@ handle_response(#coap_message{type = Type, id = MsgId} = Msg, Cfg, TM) -> reset -> ?EMPTY_RESULT; _ -> - #{out => #coap_message{type = reset, - id = MsgId}} + ?RESET(Msg) end; Machine -> - process_event(in, Msg, TM, Machine, Cfg) + process_event(in, Msg, TM, Ctx, Machine) end. -handle_out(#coap_message{id = MsgId} = Msg, Cfg, TM) -> +handle_out(#coap_message{id = MsgId} = Msg, Ctx, TM) -> Id = {out, MsgId}, case maps:get(Id, TM, undefined) of undefined -> Transport = emqx_coap_transport:new(), Machine = new_state_machine(Id, Transport), - process_event(out, Msg, TM, Machine, Cfg); + process_event(out, Msg, TM, Ctx, Machine); _ -> - ?WARN("Repeat sending message with id:~p~n", [Id]), + %% ignore repeat send ?EMPTY_RESULT end. -timeout({Id, Type, Msg}, Cfg, TM) -> +timeout({Id, Type, Msg}, Ctx, TM) -> case maps:get(Id, TM, undefined) of undefined -> ?EMPTY_RESULT; @@ -106,12 +106,16 @@ timeout({Id, Type, Msg}, Cfg, TM) -> %% maybe timer has been canceled case maps:is_key(Type, Timers) of true -> - process_event(Type, Msg, TM, Machine, Cfg); + process_event(Type, Msg, TM, Ctx, Machine); _ -> ?EMPTY_RESULT end end. +-spec is_inused(direction(), message_id(), manager()) -> boolean(). +is_inused(Dir, Msg, Manager) -> + maps:is_key({Dir, Msg}, Manager). + %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- @@ -124,9 +128,9 @@ new_state_machine(Id, Transport) -> process_event(stop_timeout, _, TM, + _, #state_machine{id = Id, - timers = Timers}, - _) -> + timers = Timers}) -> lists:foreach(fun({_, Ref}) -> emqx_misc:cancel_timer(Ref) end, @@ -136,11 +140,11 @@ process_event(stop_timeout, process_event(Event, Msg, TM, + Ctx, #state_machine{id = Id, state = State, - transport = Transport} = Machine, - Cfg) -> - Result = emqx_coap_transport:State(Event, Msg, Transport, Cfg), + transport = Transport} = Machine) -> + Result = emqx_coap_transport:State(Event, Msg, Ctx, Transport), {ok, _, Machine2} = emqx_misc:pipeline([fun process_state_change/2, fun process_transport_change/2, fun process_timeouts/2], diff --git a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl index b4c8ae333..2c2aaab2e 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl @@ -21,6 +21,8 @@ -export_type([transport/0]). +-import(emqx_coap_message, [reset/1]). + -spec new() -> transport(). new() -> #transport{cache = undefined, @@ -28,54 +30,33 @@ new() -> retry_count = 0}. idle(in, - #coap_message{type = non, id = MsgId, method = Method} = Msg, - _, - #{resource := Resource} = Cfg) -> + #coap_message{type = non, method = Method} = Msg, + Ctx, + _) -> Ret = #{next => until_stop, timeouts => [{stop_timeout, ?NON_LIFETIME}]}, case Method of undefined -> - Ret#{out => #coap_message{type = reset, id = MsgId}}; + ?RESET(Msg); _ -> - case erlang:apply(Resource, Method, [Msg, Cfg]) of - #coap_message{} = Result -> - Ret#{out => Result}; - {has_sub, Result, Sub} -> - Ret#{out => Result, subscribe => Sub}; - error -> - Ret#{out => - emqx_coap_message:response({error, internal_server_error}, Msg)} - end + Result = call_handler(Msg, Ctx), + maps:merge(Ret, Result) end; idle(in, - #coap_message{id = MsgId, - type = con, - method = Method} = Msg, - Transport, - #{resource := Resource} = Cfg) -> + #coap_message{type = con, method = Method} = Msg, + Ctx, + Transport) -> Ret = #{next => maybe_resend, timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}, case Method of undefined -> - ResetMsg = #coap_message{type = reset, id = MsgId}, + ResetMsg = reset(Msg), Ret#{transport => Transport#transport{cache = ResetMsg}, out => ResetMsg}; _ -> - {RetMsg, SubInfo} = - case erlang:apply(Resource, Method, [Msg, Cfg]) of - #coap_message{} = Result -> - {Result, undefined}; - {has_sub, Result, Sub} -> - {Result, Sub}; - error -> - {emqx_coap_message:response({error, internal_server_error}, Msg), - undefined} - end, - RetMsg2 = RetMsg#coap_message{type = ack}, - Ret#{out => RetMsg2, - transport => Transport#transport{cache = RetMsg2}, - subscribe => SubInfo} + Result = call_handler(Msg, Ctx), + maps:merge(Ret, Result) end; idle(out, #coap_message{type = non} = Msg, _, _) -> @@ -83,7 +64,7 @@ idle(out, #coap_message{type = non} = Msg, _, _) -> out => Msg, timeouts => [{stop_timeout, ?NON_LIFETIME}]}; -idle(out, Msg, Transport, _) -> +idle(out, Msg, _, Transport) -> _ = emqx_misc:rand_seed(), Timeout = ?ACK_TIMEOUT + rand:uniform(?ACK_RANDOM_FACTOR), #{next => wait_ack, @@ -133,3 +114,13 @@ wait_ack(state_timeout, until_stop(_, _, _, _) -> ?EMPTY_RESULT. + +call_handler(#coap_message{options = Opts} = Msg, Ctx) -> + case maps:get(uri_path, Opts, undefined) of + [<<"ps">> | RestPath] -> + emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx); + [<<"mqtt">> | RestPath] -> + emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx); + _ -> + ?REPLY({error, bad_request}, Msg) + end. diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl new file mode 100644 index 000000000..88a4a2310 --- /dev/null +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl @@ -0,0 +1,40 @@ +%%-------------------------------------------------------------------- +%% 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_mqtt_handler). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). + +-export([handle_request/3]). +-import(emqx_coap_message, [response/2, response/3]). + +handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _) -> + handle_method(Method, Msg); + +handle_request(_, Msg, _) -> + ?REPLY({error, bad_request}, Msg). + +handle_method(put, Msg) -> + ?REPLY({ok, changed}, Msg); + +handle_method(post, _) -> + #{connection => open}; + +handle_method(delete, _) -> + #{connection => close}; + +handle_method(_, Msg) -> + ?REPLY({error, method_not_allowed}, Msg). diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl new file mode 100644 index 000000000..e6886a559 --- /dev/null +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl @@ -0,0 +1,155 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +%% a coap to mqtt adapter with a retained topic message database +-module(emqx_coap_pubsub_handler). + +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). + +-export([handle_request/3]). + +-import(emqx_coap_message, [response/2, response/3]). + +-define(UNSUB(Topic), #{subscribe => Topic}). +-define(SUB(Topic, Token), #{subscribe => {Topic, Token}}). +-define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}). + +handle_request(Path, #coap_message{method = Method} = Msg, Ctx) -> + case check_topic(Path) of + {ok, Topic} -> + handle_method(Method, Topic, Msg, Ctx); + _ -> + ?REPLY({error, bad_request}, <<"invalid topic">>, Msg) + end. + +handle_method(get, Topic, #coap_message{options = Opts} = Msg, Ctx) -> + case maps:get(observe, Opts, undefined) of + 0 -> + subscribe(Msg, Topic, Ctx); + 1 -> + unsubscribe(Topic, Ctx); + _ -> + ?REPLY({error, bad_request}, <<"invalid observe value">>, Msg) + end; + +handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx) -> + case emqx_coap_channel:validator(publish, Topic, Ctx) of + allow -> + ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), + #{clientid := ClientId} = ClientInfo, + QOS = get_publish_qos(Msg, Ctx), + MQTTMsg = emqx_message:make(ClientId, QOS, Topic, Payload), + MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), + _ = emqx_broker:publish(MQTTMsg2), + ?REPLY({ok, changed}, Msg); + _ -> + ?REPLY({error, unauthorized}, Msg) + end; + +handle_method(_, _, Msg, _) -> + ?REPLY({error, method_not_allowed}, Msg). + +check_topic([]) -> + error; + +check_topic(Path) -> + Sep = <<"/">>, + {ok, + emqx_http_lib:uri_decode( + lists:foldl(fun(Part, Acc) -> + <> + end, + <<>>, + Path))}. + +get_sub_opts(#coap_message{options = Opts} = Msg, Ctx) -> + SubOpts = maps:fold(fun parse_sub_opts/3, #{}, Opts), + case SubOpts of + #{qos := _} -> + maps:merge(SubOpts, ?SUBOPTS); + _ -> + CfgType = emqx_coap_channel:get_config(subscribe_qos, Ctx), + maps:merge(SubOpts, ?SUBOPTS#{qos => type_to_qos(CfgType, Msg)}) + end. + +parse_sub_opts(<<"qos">>, V, Opts) -> + Opts#{qos => erlang:binary_to_integer(V)}; +parse_sub_opts(<<"nl">>, V, Opts) -> + Opts#{nl => erlang:binary_to_integer(V)}; +parse_sub_opts(<<"rh">>, V, Opts) -> + Opts#{rh => erlang:binary_to_integer(V)}; +parse_sub_opts(_, _, Opts) -> + Opts. + +type_to_qos(qos0, _) -> ?QOS_0; +type_to_qos(qos1, _) -> ?QOS_1; +type_to_qos(qos2, _) -> ?QOS_2; +type_to_qos(coap, #coap_message{type = Type}) -> + case Type of + non -> + ?QOS_0; + _ -> + ?QOS_1 + end. + +get_publish_qos(#coap_message{options = Opts} = Msg, Ctx) -> + case maps:get(uri_query, Opts) of + #{<<"qos">> := QOS} -> + erlang:binary_to_integer(QOS); + _ -> + CfgType = emqx_coap_channel:get_config(publish_qos, Ctx), + type_to_qos(CfgType, Msg) + end. + +apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) -> + maps:fold(fun(<<"retain">>, V, Acc) -> + Val = erlang:binary_to_atom(V), + emqx_message:set_flag(retain, Val, Acc); + (<<"expiry">>, V, Acc) -> + Val = erlang:binary_to_integer(V), + Props = emqx_message:get_header(properties, Acc), + emqx_message:set_header(properties, + Props#{'Message-Expiry-Interval' => Val}, + Acc); + (_, _, Acc) -> + Acc + end, + MQTTMsg, + maps:get(uri_query, Opts)). + +subscribe(#coap_message{token = <<>>} = Msg, _, _) -> + ?REPLY({error, bad_request}, <<"observe without token">>, Msg); + +subscribe(#coap_message{token = Token} = Msg, Topic, Ctx) -> + case emqx_coap_channel:validator(subscribe, Topic, Ctx) of + allow -> + ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), + #{clientid := ClientId} = ClientInfo, + SubOpts = get_sub_opts(Msg, Ctx), + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', + [ClientInfo, Topic, SubOpts]), + ?SUB(Topic, Token); + _ -> + ?REPLY({error, unauthorized}, Msg) + end. + +unsubscribe(Topic, Ctx) -> + ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), + emqx_broker:unsubscribe(Topic), + emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, ?SUBOPTS]), + ?UNSUB(Topic). diff --git a/apps/emqx_gateway/src/coap/include/emqx_coap.hrl b/apps/emqx_gateway/src/coap/include/emqx_coap.hrl index 911d10a22..3b0268abb 100644 --- a/apps/emqx_gateway/src/coap/include/emqx_coap.hrl +++ b/apps/emqx_gateway/src/coap/include/emqx_coap.hrl @@ -23,12 +23,17 @@ -define(MAXIMUM_MAX_AGE, 4294967295). -define(EMPTY_RESULT, #{}). --define(TRANSFER_RESULT(Keys, From, Value, R1), +-define(TRANSFER_RESULT(From, Value, R1), begin + Keys = result_keys(), R2 = maps:with(Keys, R1), R2#{From => Value} end). +-define(RESET(Msg), #{out => emqx_coap_message:reset(Msg)}). +-define(REPLY(Resp, Payload, Msg), #{out => emqx_coap_message:piggyback(Resp, Payload, Msg)}). +-define(REPLY(Resp, Msg), ?REPLY(Resp, <<>>, Msg)). + -type coap_message_id() :: 1 .. ?MAX_MESSAGE_ID. -type message_type() :: con | non | ack | reset. -type max_age() :: 1 .. ?MAXIMUM_MAX_AGE. diff --git a/apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl b/apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl deleted file mode 100644 index 1fd3d7b8e..000000000 --- a/apps/emqx_gateway/src/coap/resources/emqx_coap_mqtt_resource.erl +++ /dev/null @@ -1,153 +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. -%%-------------------------------------------------------------------- - -%% a coap to mqtt adapter --module(emqx_coap_mqtt_resource). - --behaviour(emqx_coap_resource). - --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). - - --export([ init/1 - , stop/1 - , get/2 - , put/2 - , post/2 - , delete/2 - ]). - --export([ check_topic/1 - , publish/3 - , subscribe/3 - , unsubscribe/3]). - --define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, is_new => false}). - -init(_) -> - {ok, undefined}. - -stop(_) -> - ok. - -%% get: subscribe, ignore observe option -get(#coap_message{token = Token} = Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - case Token of - <<>> -> - emqx_coap_message:response({error, bad_request}, <<"observer without token">>, Msg); - _ -> - Ret = subscribe(Msg, Topic, Cfg), - RetMsg = emqx_coap_message:response(Ret, Msg), - case Ret of - {ok, _} -> - {has_sub, RetMsg, {Topic, Token}}; - _ -> - RetMsg - end - end; - Any -> - Any - end. - -%% put: equal post -put(Msg, Cfg) -> - post(Msg, Cfg). - -%% post: publish a message -post(Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - emqx_coap_message:response(publish(Msg, Topic, Cfg), Msg); - Any -> - Any - end. - -%% delete: ubsubscribe -delete(Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - unsubscribe(Msg, Topic, Cfg), - {has_sub, emqx_coap_message:response({ok, deleted}, Msg), Topic}; - Any -> - Any - end. - -check_topic(#coap_message{options = Options} = Msg) -> - case maps:get(uri_path, Options, []) of - [] -> - emqx_coap_message:response({error, bad_request}, <<"invalid topic">> , Msg); - UriPath -> - Sep = <<"/">>, - {ok, lists:foldl(fun(Part, Acc) -> - <> - end, - <<>>, - UriPath)} - end. - -publish(#coap_message{payload = Payload} = Msg, - Topic, - #{clientinfo := ClientInfo, - publish_qos := QOS} = Cfg) -> - case emqx_coap_channel:auth_publish(Topic, Cfg) of - allow -> - #{clientid := ClientId} = ClientInfo, - MQTTMsg = emqx_message:make(ClientId, type_to_qos(QOS, Msg), Topic, Payload), - MQTTMsg2 = emqx_message:set_flag(retain, false, MQTTMsg), - _ = emqx_broker:publish(MQTTMsg2), - {ok, changed}; - _ -> - {error, unauthorized} - end. - -subscribe(Msg, Topic, #{clientinfo := ClientInfo}= Cfg) -> - case emqx_topic:wildcard(Topic) of - false -> - case emqx_coap_channel:auth_subscribe(Topic, Cfg) of - allow -> - #{clientid := ClientId} = ClientInfo, - SubOpts = get_sub_opts(Msg, Cfg), - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]), - {ok, created}; - _ -> - {error, unauthorized} - end; - _ -> - %% now, we don't support wildcard in subscribe topic - {error, bad_request, <<"">>} - end. - -unsubscribe(Msg, Topic, #{clientinfo := ClientInfo} = Cfg) -> - emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, get_sub_opts(Msg, Cfg)]). - -get_sub_opts(Msg, #{subscribe_qos := Type}) -> - ?SUBOPTS#{qos => type_to_qos(Type, Msg)}. - -type_to_qos(qos0, _) -> ?QOS_0; -type_to_qos(qos1, _) -> ?QOS_1; -type_to_qos(qos2, _) -> ?QOS_2; -type_to_qos(coap, #coap_message{type = Type}) -> - case Type of - non -> - ?QOS_0; - _ -> - ?QOS_1 - end. diff --git a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl b/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl deleted file mode 100644 index c750f66dd..000000000 --- a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_resource.erl +++ /dev/null @@ -1,219 +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. -%%-------------------------------------------------------------------- - -%% a coap to mqtt adapter with a retained topic message database --module(emqx_coap_pubsub_resource). - --behaviour(emqx_coap_resource). - --include_lib("emqx/include/logger.hrl"). --include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). - - --export([ init/1 - , stop/1 - , get/2 - , put/2 - , post/2 - , delete/2 - ]). --import(emqx_coap_mqtt_resource, [ check_topic/1, subscribe/3, unsubscribe/3 - , publish/3]). - --import(emqx_coap_message, [response/2, response/3, set_content/2]). -%%-------------------------------------------------------------------- -%% Resource Callbacks -%%-------------------------------------------------------------------- -init(_) -> - emqx_coap_pubsub_topics:start_link(). - -stop(Pid) -> - emqx_coap_pubsub_topics:stop(Pid). - -%% get: read last publish message -%% get with observe 0: subscribe -%% get with observe 1: unsubscribe -get(#coap_message{token = Token} = Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - case emqx_coap_message:get_option(observe, Msg) of - undefined -> - Content = emqx_coap_message:get_content(Msg), - read_last_publish_message(emqx_topic:wildcard(Topic), Msg, Topic, Content); - 0 -> - case Token of - <<>> -> - response({error, bad_reuqest}, <<"observe without token">>, Msg); - _ -> - Ret = subscribe(Msg, Topic, Cfg), - RetMsg = response(Ret, Msg), - case Ret of - {ok, _} -> - {has_sub, RetMsg, {Topic, Token}}; - _ -> - RetMsg - end - end; - 1 -> - unsubscribe(Msg, Topic, Cfg), - {has_sub, response({ok, deleted}, Msg), Topic} - end; - Any -> - Any - end. - -%% put: insert a message into topic database -put(Msg, _) -> - case check_topic(Msg) of - {ok, Topic} -> - Content = emqx_coap_message:get_content(Msg), - #coap_content{payload = Payload, - format = Format, - max_age = MaxAge} = Content, - handle_received_create(Msg, Topic, MaxAge, Format, Payload); - Any -> - Any - end. - -%% post: like put, but will publish the inserted message -post(Msg, Cfg) -> - case check_topic(Msg) of - {ok, Topic} -> - Content = emqx_coap_message:get_content(Msg), - #coap_content{max_age = MaxAge, - format = Format, - payload = Payload} = Content, - handle_received_publish(Msg, Topic, MaxAge, Format, Payload, Cfg); - Any -> - Any - end. - -%% delete: delete a message from topic database -delete(Msg, _) -> - case check_topic(Msg) of - {ok, Topic} -> - delete_topic_info(Msg, Topic); - Any -> - Any - end. - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- -add_topic_info(Topic, MaxAge, Format, Payload) when is_binary(Topic), Topic =/= <<>> -> - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [{_, StoredMaxAge, StoredCT, _, _}] -> - ?LOG(debug, "publish topic=~p already exists, need reset the topic info", [Topic]), - %% check whether the ct value stored matches the ct option in this POST message - case Format =:= StoredCT of - true -> - {ok, Ret} = - case StoredMaxAge =:= MaxAge of - true -> - emqx_coap_pubsub_topics:reset_topic_info(Topic, Payload); - false -> - emqx_coap_pubsub_topics:reset_topic_info(Topic, MaxAge, Payload) - end, - {changed, Ret}; - false -> - ?LOG(debug, "ct values of topic=~p do not match, stored ct=~p, new ct=~p, ignore the PUBLISH", [Topic, StoredCT, Format]), - {changed, false} - end; - [] -> - ?LOG(debug, "publish topic=~p will be created", [Topic]), - {ok, Ret} = emqx_coap_pubsub_topics:add_topic_info(Topic, MaxAge, Format, Payload), - {created, Ret} - end; - -add_topic_info(Topic, _MaxAge, _Format, _Payload) -> - ?LOG(debug, "create topic=~p info failed", [Topic]), - {badarg, false}. - -format_string_to_int(<<"application/octet-stream">>) -> - <<"42">>; -format_string_to_int(<<"application/exi">>) -> - <<"47">>; -format_string_to_int(<<"application/json">>) -> - <<"50">>; -format_string_to_int(_) -> - <<"42">>. - -handle_received_publish(Msg, Topic, MaxAge, Format, Payload, Cfg) -> - case add_topic_info(Topic, MaxAge, format_string_to_int(Format), Payload) of - {_, true} -> - response(publish(Msg, Topic, Cfg), Msg); - {_, false} -> - ?LOG(debug, "add_topic_info failed, will return bad_request", []), - response({error, bad_request}, Msg) - end. - -handle_received_create(Msg, Topic, MaxAge, Format, Payload) -> - case add_topic_info(Topic, MaxAge, format_string_to_int(Format), Payload) of - {Ret, true} -> - response({ok, Ret}, Msg); - {_, false} -> - ?LOG(debug, "add_topic_info failed, will return bad_request", []), - response({error, bad_request}, Msg) - end. - -return_resource(Msg, Topic, Payload, MaxAge, TimeStamp, Content) -> - TimeElapsed = trunc((erlang:system_time(millisecond) - TimeStamp) / 1000), - case TimeElapsed < MaxAge of - true -> - LeftTime = (MaxAge - TimeElapsed), - ?LOG(debug, "topic=~p has max age left time is ~p", [Topic, LeftTime]), - set_content(Content#coap_content{max_age = LeftTime, payload = Payload}, - response({ok, content}, Msg)); - false -> - ?LOG(debug, "topic=~p has been timeout, will return empty content", [Topic]), - response({ok, nocontent}, Msg) - end. - -read_last_publish_message(false, Msg, Topic, Content=#coap_content{format = QueryFormat}) when is_binary(QueryFormat)-> - ?LOG(debug, "the QueryFormat=~p", [QueryFormat]), - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [] -> - response({error, not_found}, Msg); - [{_, MaxAge, CT, Payload, TimeStamp}] -> - case CT =:= format_string_to_int(QueryFormat) of - true -> - return_resource(Msg, Topic, Payload, MaxAge, TimeStamp, Content); - false -> - ?LOG(debug, "format value does not match, the queried format=~p, the stored format=~p", [QueryFormat, CT]), - response({error, bad_request}, Msg) - end - end; - -read_last_publish_message(false, Msg, Topic, Content) -> - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [] -> - response({error, not_found}, Msg); - [{_, MaxAge, _, Payload, TimeStamp}] -> - return_resource(Msg, Topic, Payload, MaxAge, TimeStamp, Content) - end; - -read_last_publish_message(true, Msg, Topic, _Content) -> - ?LOG(debug, "the topic=~p is illegal wildcard topic", [Topic]), - response({error, bad_request}, Msg). - -delete_topic_info(Msg, Topic) -> - case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of - [] -> - response({error, not_found}, Msg); - [{_, _, _, _, _}] -> - emqx_coap_pubsub_topics:delete_sub_topics(Topic), - response({ok, deleted}, Msg) - end. diff --git a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl b/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl deleted file mode 100644 index 328d1df04..000000000 --- a/apps/emqx_gateway/src/coap/resources/emqx_coap_pubsub_topics.erl +++ /dev/null @@ -1,185 +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_topics). - --behaviour(gen_server). - --include_lib("emqx/include/logger.hrl"). --include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). - - --export([ start_link/0 - , stop/1 - ]). - --export([ add_topic_info/4 - , delete_topic_info/1 - , delete_sub_topics/1 - , is_topic_existed/1 - , is_topic_timeout/1 - , reset_topic_info/2 - , reset_topic_info/3 - , reset_topic_info/4 - , lookup_topic_info/1 - , lookup_topic_payload/1 - ]). - -%% gen_server. --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --record(state, {}). - --define(COAP_TOPIC_TABLE, coap_topic). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -stop(Pid) -> - gen_server:stop(Pid). - -add_topic_info(Topic, MaxAge, CT, Payload) when is_binary(Topic), is_integer(MaxAge), is_binary(CT), is_binary(Payload) -> - gen_server:call(?MODULE, {add_topic, {Topic, MaxAge, CT, Payload}}). - -delete_topic_info(Topic) when is_binary(Topic) -> - gen_server:call(?MODULE, {remove_topic, Topic}). - -delete_sub_topics(Topic) when is_binary(Topic) -> - gen_server:cast(?MODULE, {remove_sub_topics, Topic}). - -reset_topic_info(Topic, Payload) -> - gen_server:call(?MODULE, {reset_topic, {Topic, Payload}}). - -reset_topic_info(Topic, MaxAge, Payload) -> - gen_server:call(?MODULE, {reset_topic, {Topic, MaxAge, Payload}}). - -reset_topic_info(Topic, MaxAge, CT, Payload) -> - gen_server:call(?MODULE, {reset_topic, {Topic, MaxAge, CT, Payload}}). - -is_topic_existed(Topic) -> - ets:member(?COAP_TOPIC_TABLE, Topic). - -is_topic_timeout(Topic) when is_binary(Topic) -> - [{Topic, MaxAge, _, _, TimeStamp}] = ets:lookup(?COAP_TOPIC_TABLE, Topic), - %% MaxAge: x seconds - MaxAge < ((erlang:system_time(millisecond) - TimeStamp) / 1000). - -lookup_topic_info(Topic) -> - ets:lookup(?COAP_TOPIC_TABLE, Topic). - -lookup_topic_payload(Topic) -> - try ets:lookup_element(?COAP_TOPIC_TABLE, Topic, 4) - catch - error:badarg -> undefined - end. - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([]) -> - _ = ets:new(?COAP_TOPIC_TABLE, [set, named_table, protected]), - ?LOG(debug, "Create the coap_topic table", []), - {ok, #state{}}. - -handle_call({add_topic, {Topic, MaxAge, CT, Payload}}, _From, State) -> - Ret = create_table_element(Topic, MaxAge, CT, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({reset_topic, {Topic, Payload}}, _From, State) -> - Ret = update_table_element(Topic, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({reset_topic, {Topic, MaxAge, Payload}}, _From, State) -> - Ret = update_table_element(Topic, MaxAge, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({reset_topic, {Topic, MaxAge, CT, Payload}}, _From, State) -> - Ret = update_table_element(Topic, MaxAge, CT, Payload), - {reply, {ok, Ret}, State, hibernate}; - -handle_call({remove_topic, {Topic, _Content}}, _From, State) -> - ets:delete(?COAP_TOPIC_TABLE, Topic), - ?LOG(debug, "Remove topic ~p in the coap_topic table", [Topic]), - {reply, ok, State, hibernate}; - -handle_call(Request, _From, State) -> - ?LOG(error, "adapter unexpected call ~p", [Request]), - {reply, ignored, State, hibernate}. - -handle_cast({remove_sub_topics, TopicPrefix}, State) -> - DeletedTopicNum = ets:foldl(fun ({Topic, _, _, _, _}, AccIn) -> - case binary:match(Topic, TopicPrefix) =/= nomatch of - true -> - ?LOG(debug, "Remove topic ~p in the coap_topic table", [Topic]), - ets:delete(?COAP_TOPIC_TABLE, Topic), - AccIn + 1; - false -> - AccIn - end - end, 0, ?COAP_TOPIC_TABLE), - ?LOG(debug, "Remove number of ~p topics with prefix=~p in the coap_topic table", [DeletedTopicNum, TopicPrefix]), - {noreply, State, hibernate}; - -handle_cast(Msg, State) -> - ?LOG(error, "broker_api unexpected cast ~p", [Msg]), - {noreply, State, hibernate}. - -handle_info(Info, State) -> - ?LOG(error, "adapter unexpected info ~p", [Info]), - {noreply, State, hibernate}. - -terminate(Reason, #state{}) -> - ets:delete(?COAP_TOPIC_TABLE), - Level = case Reason =:= normal orelse Reason =:= shutdown of - true -> debug; - false -> error - end, - ?SLOG(Level, #{terminate_reason => Reason}). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- -create_table_element(Topic, MaxAge, CT, Payload) -> - TopicInfo = {Topic, MaxAge, CT, Payload, erlang:system_time(millisecond)}, - ?LOG(debug, "Insert ~p in the coap_topic table", [TopicInfo]), - ets:insert_new(?COAP_TOPIC_TABLE, TopicInfo). - -update_table_element(Topic, Payload) -> - ?LOG(debug, "Update the topic=~p only with Payload", [Topic]), - ets:update_element(?COAP_TOPIC_TABLE, Topic, [{4, Payload}, {5, erlang:system_time(millisecond)}]). - -update_table_element(Topic, MaxAge, Payload) -> - ?LOG(debug, "Update the topic=~p info of MaxAge=~p and Payload", [Topic, MaxAge]), - ets:update_element(?COAP_TOPIC_TABLE, Topic, [{2, MaxAge}, {4, Payload}, {5, erlang:system_time(millisecond)}]). - -update_table_element(Topic, MaxAge, CT, <<>>) -> - ?LOG(debug, "Update the topic=~p info of MaxAge=~p, CT=~p, payload=<<>>", [Topic, MaxAge, CT]), - ets:update_element(?COAP_TOPIC_TABLE, Topic, [{2, MaxAge}, {3, CT}, {5, erlang:system_time(millisecond)}]). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 546640a90..83142abb1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -31,6 +31,7 @@ -export([start_link/1]). -export([ open_session/5 + , open_session/6 , kick_session/2 , kick_session/3 , register_channel/4 @@ -225,28 +226,32 @@ connection_closed(Type, ClientId) -> }} | {error, any()}. -open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> +open_session(Type, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + open_session(Type, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, emqx_session). + +open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> 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}} + ok = discard_session(Type, ClientId), + Session = create_session(Type, + ClientInfo, + ConnInfo, + CreateSessionFun, + SessionMod + ), + 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) -> + _ClientInfo, _ConnInfo, _CreateSessionFun, _SessionMod) -> %% TODO: {error, not_supported_now}. %% @private -create_session(Type, ClientInfo, ConnInfo, CreateSessionFun) -> +create_session(Type, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> try Session = emqx_gateway_utils:apply( CreateSessionFun, @@ -255,7 +260,7 @@ create_session(Type, ClientInfo, ConnInfo, CreateSessionFun) -> ok = emqx_gateway_metrics:inc(Type, 'session.created'), SessionInfo = case is_tuple(Session) andalso element(1, Session) == session of - true -> emqx_session:info(Session); + true -> SessionMod:info(Session); _ -> case is_map(Session) of false -> diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 406de7767..d9517b53f 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -40,6 +40,7 @@ %% Authentication circle -export([ authenticate/2 , open_session/5 + , open_session/6 , insert_channel_info/4 , set_chan_info/3 , set_chan_stats/3 @@ -96,15 +97,18 @@ authenticate(_Ctx, ClientInfo) -> pendings => list() }} | {error, any()}. -open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun) -> +open_session(Ctx, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + open_session(Ctx, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, emqx_session). + +open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> logger:warning("clean_start=false is not supported now, " "fallback to clean_start mode"), - open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun); + open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun, SessionMod); open_session(_Ctx = #{type := Type}, - CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> emqx_gateway_cm:open_session(Type, CleanStart, - ClientInfo, ConnInfo, CreateSessionFun). + ClientInfo, ConnInfo, CreateSessionFun, SessionMod). -spec insert_channel_info(context(), emqx_types:clientid(), @@ -132,7 +136,7 @@ connection_closed(_Ctx = #{type := Type}, ClientId) -> -spec authorize(context(), emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) - -> allow | deny. + -> allow | deny. authorize(_Ctx, ClientInfo, PubSub, Topic) -> emqx_access_control:authorize(ClientInfo, PubSub, Topic). diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 5cb958701..5c98e1f34 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -215,8 +215,7 @@ fields(coap) -> fields(coap_structs) -> [ {enable_stats, t(boolean(), undefined, true)} , {authentication, t(ref(authentication))} - , {heartbeat, t(duration(), undefined, "15s")} - , {resource, t(union([mqtt, pubsub]), undefined, mqtt)} + , {heartbeat, t(duration(), undefined, "30s")} , {notify_type, t(union([non, con, qos]), undefined, qos)} , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} From e3415ae361e7224624b7affa8458a374b8ec5931 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 12 Aug 2021 12:24:11 +0800 Subject: [PATCH 002/306] fix: depart api path & add parameter --- .../src/emqx_dashboard_monitor_api.erl | 144 +++++++++++++----- 1 file changed, 109 insertions(+), 35 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 277b0b1fd..1521e63a2 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -10,7 +10,12 @@ -export([api_spec/0]). --export([counters/2, current_counters/2]). +-export([ monitor/2 + , counters/2 + , monitor_nodes/2 + , monitor_nodes_counters/2 + , current_counters/2 + ]). -define(COUNTERS, [ connection , route @@ -20,7 +25,14 @@ , dropped]). api_spec() -> - {[monitor_api(), monitor_current_api()], [counters_schema()]}. + { + [ monitor_api() + , monitor_nodes_api() + , monitor_nodes_counters_api() + , monitor_counters_api() + , monitor_current_api()], + [] + }. monitor_api() -> Metadata = #{ @@ -28,21 +40,48 @@ monitor_api() -> description => <<"List monitor data">>, parameters => [ #{ - name => node, + name => aggregate, in => query, required => false, - schema => #{type => string} - }, - #{ - name => counter, - in => query, - required => false, - schema => #{type => string, enum => ?COUNTERS} + schema => #{type => boolean} } ], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data">>, counters)}}}, - {"/monitor", Metadata, counters}. + <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data">>, counters_schema())}}}, + {"/monitor", Metadata, monitor}. + +monitor_nodes_api() -> + Metadata = #{ + get => #{ + description => <<"List monitor data">>, + parameters => [path_param_node()], + responses => #{ + <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data in node">>, counters_schema())}}}, + {"/monitor/nodes/:node", Metadata, monitor_nodes}. + +monitor_nodes_counters_api() -> + Metadata = #{ + get => #{ + description => <<"List monitor data">>, + parameters => [ + path_param_node(), + path_param_counter() + ], + responses => #{ + <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor single count data in node">>, counter_schema())}}}, + {"/monitor/nodes/:node/counters/:counter", Metadata, monitor_nodes_counters}. + +monitor_counters_api() -> + Metadata = #{ + get => #{ + description => <<"List monitor data">>, + parameters => [ + path_param_counter() + ], + responses => #{ + <<"200">> => + emqx_mgmt_util:response_schema(<<"Monitor single count data">>, counter_schema())}}}, + {"/monitor/counters/:counter", Metadata, counters}. monitor_current_api() -> Metadata = #{ get => #{ @@ -52,6 +91,24 @@ monitor_current_api() -> current_counters_schema())}}}, {"/monitor/current", Metadata, current_counters}. +path_param_node() -> + #{ + name => node, + in => path, + required => true, + schema => #{type => string}, + example => node() + }. + +path_param_counter() -> + #{ + name => counter, + in => path, + required => true, + schema => #{type => string, enum => ?COUNTERS}, + example => hd(?COUNTERS) + }. + current_counters_schema() -> #{ type => object, @@ -69,13 +126,14 @@ counters_schema() -> end, Properties = lists:foldl(Fun, #{}, ?COUNTERS), #{ - counters => #{ - type => object, - properties => Properties} + type => object, + properties => Properties }. counters_schema(Name) -> - #{Name => #{ + #{Name => counter_schema()}. +counter_schema() -> + #{ type => array, items => #{ type => object, @@ -83,16 +141,25 @@ counters_schema(Name) -> timestamp => #{ type => integer}, count => #{ - type => integer}}}}}. + type => integer}}}}. %%%============================================================================================== %% parameters trans +monitor(get, Request) -> + Aggregate = proplists:get_value(<<"aggregate">>, cowboy_req:parse_qs(Request), <<"false">>), + {200, list_collect(Aggregate)}. + +monitor_nodes(get, Request) -> + Node = cowboy_req:binding(node, Request), + lookup([{<<"node">>, Node}]). + +monitor_nodes_counters(get, Request) -> + Node = cowboy_req:binding(node, Request), + Counter = cowboy_req:binding(counter, Request), + lookup([{<<"node">>, Node}, {<<"counter">>, Counter}]). + counters(get, Request) -> - case cowboy_req:parse_qs(Request) of - [] -> - {200, get_collect()}; - Params -> - lookup(Params) - end. + Counter = cowboy_req:binding(counter, Request), + lookup([{<<"counter">>, Counter}]). current_counters(get, _) -> Data = [get_collect(Node) || Node <- ekka_mnesia:running_nodes()], @@ -107,7 +174,15 @@ current_counters(get, _) -> }, {200, Response}. - %%%============================================================================================== +format_current_metrics(Collects) -> + format_current_metrics(Collects, {0,0,0,0}). +format_current_metrics([], Acc) -> + Acc; +format_current_metrics([{Received, Sent, Sub, Conn} | Collects], {Received1, Sent1, Sub1, Conn1}) -> + format_current_metrics(Collects, {Received1 + Received, Sent1 + Sent, Sub1 + Sub, Conn1 + Conn}). + + +%%%============================================================================================== %% api apply lookup(Params) -> @@ -122,19 +197,18 @@ lookup_(#{node := Node, counter := Counter}) -> lookup_(#{node := Node}) -> {200, sampling(Node)}; lookup_(#{counter := Counter}) -> - Data = [sampling(Node, Counter) || Node <- ekka_mnesia:running_nodes()], + CounterData = merger_counters([sampling(Node, Counter) || Node <- ekka_mnesia:running_nodes()]), + Data = hd(maps:values(CounterData)), {200, Data}. -format_current_metrics(Collects) -> - format_current_metrics(Collects, {0,0,0,0}). -format_current_metrics([], Acc) -> - Acc; -format_current_metrics([{Received, Sent, Sub, Conn} | Collects], {Received1, Sent1, Sub1, Conn1}) -> - format_current_metrics(Collects, {Received1 + Received, Sent1 + Sent, Sub1 + Sub, Conn1 + Conn}). - -get_collect() -> - Counters = [sampling(Node) || Node <- ekka_mnesia:running_nodes()], - merger_counters(Counters). +list_collect(Aggregate) -> + case Aggregate of + <<"true">> -> + [maps:put(node, Node, sampling(Node)) || Node <- ekka_mnesia:running_nodes()]; + _ -> + Counters = [sampling(Node) || Node <- ekka_mnesia:running_nodes()], + merger_counters(Counters) + end. get_collect(Node) when Node =:= node() -> emqx_dashboard_collection:get_collect(); From 43f0ef40e4e24e40ead2b653b88df0d415261896 Mon Sep 17 00:00:00 2001 From: Parham Alvani Date: Wed, 11 Aug 2021 18:41:53 +0430 Subject: [PATCH 003/306] feat: Expose Internal MQTT Service --- deploy/charts/emqx/templates/StatefulSet.yaml | 6 +++++- deploy/charts/emqx/templates/service.yaml | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 410fbd09a..040cd531b 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -102,8 +102,12 @@ spec: containerPort: {{ .Values.emqxConfig.EMQX_LISTENER__WSS__EXTERNAL | default 8084 }} - name: dashboard containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} - {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS) }} + {{- if not (empty .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL) }} - name: dashboardtls + containerPort: {{ .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL }} + {{- end }} + {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS) }} + - name: internalmqtt containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS }} {{- end }} - name: ekka diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 6e31a97c3..92ba0b47e 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -35,6 +35,17 @@ spec: {{- else if eq .Values.service.type "ClusterIP" }} nodePort: null {{- end }} + {{- if not (empty .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL) }} + - name: internalmqtt + port: {{ .Values.service.internalmqtt | default 11883 }} + protocol: TCP + targetPort: internalmqtt + {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePorts.internalmqtt)) }} + nodePort: {{ .Values.service.nodePorts.internalmqtt }} + {{- else if eq .Values.service.type "ClusterIP" }} + nodePort: null + {{- end }} + {{ end }} - name: mqttssl port: {{ .Values.service.mqttssl | default 8883 }} protocol: TCP @@ -115,6 +126,12 @@ spec: port: {{ .Values.service.mqtt | default 1883 }} protocol: TCP targetPort: mqtt + {{- if not (empty .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL) }} + - name: mqtt + port: {{ .Values.service.internalmqtt | default 11883 }} + protocol: TCP + targetPort: internalmqtt + {{ end }} - name: mqttssl port: {{ .Values.service.mqttssl | default 8883 }} protocol: TCP From 4f72d2d0c5b29820fdd1a4d1763c7d665b3569f0 Mon Sep 17 00:00:00 2001 From: Parham Alvani Date: Thu, 12 Aug 2021 08:15:00 +0430 Subject: [PATCH 004/306] fix: Correct Issues --- deploy/charts/emqx/templates/StatefulSet.yaml | 4 ++-- deploy/charts/emqx/templates/service.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 040cd531b..12fd4c8d8 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -103,11 +103,11 @@ spec: - name: dashboard containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} {{- if not (empty .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL) }} - - name: dashboardtls + - name: internalmqtt containerPort: {{ .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL }} {{- end }} {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS) }} - - name: internalmqtt + - name: dashboardtls containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS }} {{- end }} - name: ekka diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 92ba0b47e..f019c5614 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -127,7 +127,7 @@ spec: protocol: TCP targetPort: mqtt {{- if not (empty .Values.emqxConfig.EMQX_LISTENER__TCP__INTERNAL) }} - - name: mqtt + - name: internalmqtt port: {{ .Values.service.internalmqtt | default 11883 }} protocol: TCP targetPort: internalmqtt From 7e5be6ed6c62b8de39518bb910ab0415c371e7d2 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 10 Aug 2021 16:37:31 +0800 Subject: [PATCH 005/306] refactor(exhook): add mechanism to reload the failure server --- apps/emqx_exhook/etc/emqx_exhook.conf | 16 -- apps/emqx_exhook/src/emqx_exhook_app.erl | 51 ------- apps/emqx_exhook/src/emqx_exhook_mngr.erl | 171 ++++++++++++++++++++++ apps/emqx_exhook/src/emqx_exhook_sup.erl | 16 +- 4 files changed, 186 insertions(+), 68 deletions(-) delete mode 100644 apps/emqx_exhook/etc/emqx_exhook.conf create mode 100644 apps/emqx_exhook/src/emqx_exhook_mngr.erl diff --git a/apps/emqx_exhook/etc/emqx_exhook.conf b/apps/emqx_exhook/etc/emqx_exhook.conf deleted file mode 100644 index 648eb554f..000000000 --- a/apps/emqx_exhook/etc/emqx_exhook.conf +++ /dev/null @@ -1,16 +0,0 @@ -##==================================================================== -## EMQ X Hooks -##==================================================================== - -exhook: { - servers: [ - # { name: "default" - # url: "http://127.0.0.1:9000" - # #ssl: { - # # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" - # # certfile: "{{ platform_etc_dir }}/certs/cert.pem" - # # keyfile: "{{ platform_etc_dir }}/certs/key.pem" - # #} - # } - ] -} diff --git a/apps/emqx_exhook/src/emqx_exhook_app.erl b/apps/emqx_exhook/src/emqx_exhook_app.erl index c97b26677..80dc24b70 100644 --- a/apps/emqx_exhook/src/emqx_exhook_app.erl +++ b/apps/emqx_exhook/src/emqx_exhook_app.erl @@ -20,41 +20,22 @@ -include("emqx_exhook.hrl"). --define(CNTER, emqx_exhook_counter). - -export([ start/2 , stop/1 , prep_stop/1 ]). -%% Internal export --export([ load_server/2 - , unload_server/1 - , unload_exhooks/0 - , init_hooks_cnter/0 - ]). - %%-------------------------------------------------------------------- %% Application callbacks %%-------------------------------------------------------------------- start(_StartType, _StartArgs) -> {ok, Sup} = emqx_exhook_sup:start_link(), - - %% Init counter - init_hooks_cnter(), - - %% Load all dirvers - load_all_servers(), - - %% Register CLI emqx_ctl:register_command(exhook, {emqx_exhook_cli, cli}, []), {ok, Sup}. prep_stop(State) -> emqx_ctl:unregister_command(exhook), - _ = unload_exhooks(), - ok = unload_all_servers(), State. stop(_State) -> @@ -63,35 +44,3 @@ stop(_State) -> %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- - -load_all_servers() -> - try - lists:foreach(fun(#{name := Name} = Options) -> - load_server(Name, maps:remove(name, Options)) - end, emqx_config:get([exhook, servers])) - catch - _Class : _Reason -> - ok - end, ok. - -unload_all_servers() -> - emqx_exhook:disable_all(). - -load_server(Name, Options) -> - emqx_exhook:enable(Name, Options). - -unload_server(Name) -> - emqx_exhook:disable(Name). - -unload_exhooks() -> - [emqx:unhook(Name, {M, F}) || - {Name, {M, F, _A}} <- ?ENABLED_HOOKS]. - -init_hooks_cnter() -> - try - _ = ets:new(?CNTER, [named_table, public]), ok - catch - error:badarg:_ -> - ok - end. - diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl new file mode 100644 index 000000000..8e3dfaf82 --- /dev/null +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -0,0 +1,171 @@ +%%-------------------------------------------------------------------- +%% 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 Manage the server status and reload strategy +-module(emqx_exhook_mngr). + +-behaviour(gen_server). + +-include("emqx_exhook.hrl"). +-include_lib("emqx/include/logger.hrl"). + +%% APIs +-export([start_link/2]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-record(state, { + %% Running servers + running :: map(), + %% Wait to reload servers + waiting :: map(), + %% Marked stopped servers + stopped :: map(), + %% Auto reconnect timer interval + auto_reconnect :: false | non_neg_integer(), + %% Timer references + trefs :: map() + }). + +-type servers() :: [{Name :: atom(), server_options()}]. + +-type server_options() :: [ {scheme, http | https} + | {host, string()} + | {port, inet:port_number()} + ]. + +-define(CNTER, emqx_exhook_counter). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +-spec start_link(servers(), false | non_neg_integer()) + ->ignore + | {ok, pid()} + | {error, any()}. +start_link(Servers, AutoReconnect) -> + gen_server:start_link(?MODULE, [Servers, AutoReconnect], []). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Servers, AutoReconnect]) -> + %% XXX: Due to the ExHook Module in the enterprise, + %% this process may start multiple times and they will share this table + try + _ = ets:new(?CNTER, [named_table, public]), ok + catch + error:badarg:_ -> + ok + end, + + %% Load the hook servers + {Waiting, Running} = load_all_servers(Servers), + {ok, ensure_reload_timer( + #state{waiting = Waiting, + running = Running, + stopped = #{}, + auto_reconnect = AutoReconnect, + trefs = #{} + } + )}. + +%% @private +load_all_servers(Servers) -> + load_all_servers(Servers, #{}, #{}). +load_all_servers([], Waiting, Running) -> + {Waiting, Running}; +load_all_servers([{Name, Options}|More], Waiting, Running) -> + {NWaiting, NRunning} = case emqx_exhook:enable(Name, Options) of + ok -> + {Waiting, Running#{Name => Options}}; + {error, _} -> + {Waiting#{Name => Options}, Running} + end, + load_all_servers(More, NWaiting, NRunning). + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({timeout, _Ref, {reload, Name}}, + State0 = #state{waiting = Waiting, + running = Running, + trefs = TRefs}) -> + State = State0#state{trefs = maps:remove(Name, TRefs)}, + case maps:get(Name, Waiting, undefined) of + undefined -> + {noreply, State}; + Options -> + case emqx_exhook:enable(Name, Options) of + ok -> + ?LOG(warning, "Reconnect to exhook callback server " + "\"~s\" successfully!", [Name]), + {noreply, State#state{ + running = maps:put(Name, Options, Running), + waiting = maps:remove(Name, Waiting)} + }; + {error, _} -> + {noreply, ensure_reload_timer(State)} + end + end; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + _ = emqx_exhook:disable_all(), + _ = unload_exhooks(), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal funcs +%%-------------------------------------------------------------------- + +unload_exhooks() -> + [emqx:unhook(Name, {M, F}) || + {Name, {M, F, _A}} <- ?ENABLED_HOOKS]. + +ensure_reload_timer(State = #state{auto_reconnect = false}) -> + State; +ensure_reload_timer(State = #state{waiting = Waiting, + trefs = TRefs, + auto_reconnect = Intv}) -> + NRefs = maps:fold(fun(Name, _, AccIn) -> + case maps:get(Name, AccIn, undefined) of + undefined -> + Ref = erlang:start_timer(Intv, self(), {reload, Name}), + AccIn#{Name => Ref}; + _HasRef -> + AccIn + end + end, TRefs, Waiting), + State#state{trefs = NRefs}. diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index c3ca811bd..96509181a 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -26,6 +26,13 @@ , stop_grpc_client_channel/1 ]). +-define(CHILD(Mod, Type, Args), + #{ id => Mod + , start => {Mod, start_link, Args} + , type => Type + } + ). + %%-------------------------------------------------------------------- %% Supervisor APIs & Callbacks %%-------------------------------------------------------------------- @@ -34,7 +41,14 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 10, 100}, []}}. + Mngr = ?CHILD(emqx_exhook_mngr, worker, [servers(), auto_reconnect()]), + {ok, {{one_for_one, 10, 100}, [Mngr]}}. + +servers() -> + application:get_env(emqx_exhook, servers, []). + +auto_reconnect() -> + application:get_env(emqx_exhook, auto_reconnect, 60000). %%-------------------------------------------------------------------- %% APIs From f5acf5fd0ba9c7f11ff97e83e25a57844bf8398d Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 11 Aug 2021 17:31:21 +0800 Subject: [PATCH 006/306] refactor(exhook): move all manager code into mngr module --- apps/emqx_exhook/src/emqx_exhook.erl | 89 +++------ apps/emqx_exhook/src/emqx_exhook_cli.erl | 28 +-- apps/emqx_exhook/src/emqx_exhook_mngr.erl | 205 ++++++++++++++++---- apps/emqx_exhook/src/emqx_exhook_server.erl | 39 ++-- apps/emqx_exhook/src/emqx_exhook_sup.erl | 6 +- 5 files changed, 230 insertions(+), 137 deletions(-) diff --git a/apps/emqx_exhook/src/emqx_exhook.erl b/apps/emqx_exhook/src/emqx_exhook.erl index b370c6e27..008c3d241 100644 --- a/apps/emqx_exhook/src/emqx_exhook.erl +++ b/apps/emqx_exhook/src/emqx_exhook.erl @@ -20,10 +20,8 @@ -include_lib("emqx/include/logger.hrl"). -%% Mgmt APIs --export([ enable/2 +-export([ enable/1 , disable/1 - , disable_all/0 , list/0 ]). @@ -35,64 +33,54 @@ %% Mgmt APIs %%-------------------------------------------------------------------- -%% XXX: Only return the running servers --spec list() -> [emqx_exhook_server:server()]. -list() -> - [server(Name) || Name <- running()]. - --spec enable(binary(), map()) -> ok | {error, term()}. -enable(Name, Options) -> - case lists:member(Name, running()) of - true -> - {error, already_started}; - _ -> - case emqx_exhook_server:load(Name, Options) of - {ok, ServiceState} -> - save(Name, ServiceState); - {error, Reason} -> - ?LOG(error, "Load server ~p failed: ~p", [Name, Reason]), - {error, Reason} - end - end. +-spec enable(atom()|string()) -> ok | {error, term()}. +enable(Name) -> + with_mngr(fun(Pid) -> emqx_exhook_mngr:enable(Pid, Name) end). -spec disable(binary()) -> ok | {error, term()}. disable(Name) -> - case server(Name) of - undefined -> {error, not_running}; - Service -> - ok = emqx_exhook_server:unload(Service), - unsave(Name) + with_mngr(fun(Pid) -> emqx_exhook_mngr:disable(Pid, Name) end). + +-spec list() -> [atom() | string()]. +list() -> + with_mngr(fun(Pid) -> emqx_exhook_mngr:list(Pid) end). + +with_mngr(Fun) -> + case lists:keyfind(emqx_exhook_mngr, 1, + supervisor:which_children(emqx_exhook_sup)) of + {_, Pid, _, _} -> + Fun(Pid); + _ -> + {error, no_manager_svr} end. --spec disable_all() -> ok. -disable_all() -> - lists:foreach(fun disable/1, running()). - -%%---------------------------------------------------------- +%%-------------------------------------------------------------------- %% Dispatch APIs -%%---------------------------------------------------------- +%%-------------------------------------------------------------------- -spec cast(atom(), map()) -> ok. cast(Hookpoint, Req) -> - cast(Hookpoint, Req, running()). + cast(Hookpoint, Req, emqx_exhook_mngr:running()). cast(_, _, []) -> ok; cast(Hookpoint, Req, [ServiceName|More]) -> %% XXX: Need a real asynchronous running - _ = emqx_exhook_server:call(Hookpoint, Req, server(ServiceName)), + _ = emqx_exhook_server:call(Hookpoint, Req, + emqx_exhook_mngr:server(ServiceName)), cast(Hookpoint, Req, More). -spec call_fold(atom(), term(), function()) -> {ok, term()} | {stop, term()}. call_fold(Hookpoint, Req, AccFun) -> - call_fold(Hookpoint, Req, AccFun, running()). + call_fold(Hookpoint, Req, AccFun, emqx_exhook_mngr:running()). call_fold(_, Req, _, []) -> {ok, Req}; call_fold(Hookpoint, Req, AccFun, [ServiceName|More]) -> - case emqx_exhook_server:call(Hookpoint, Req, server(ServiceName)) of + case emqx_exhook_server:call(Hookpoint, Req, + emqx_exhook_mngr:server(ServiceName)) of {ok, Resp} -> case AccFun(Req, Resp) of {stop, NReq} -> {stop, NReq}; @@ -102,30 +90,3 @@ call_fold(Hookpoint, Req, AccFun, [ServiceName|More]) -> _ -> call_fold(Hookpoint, Req, AccFun, More) end. - -%%---------------------------------------------------------- -%% Storage - -save(Name, ServiceState) -> - Saved = persistent_term:get(?APP, []), - persistent_term:put(?APP, lists:reverse([Name | Saved])), - persistent_term:put({?APP, Name}, ServiceState). - -unsave(Name) -> - case persistent_term:get(?APP, []) of - [] -> - persistent_term:erase(?APP); - Saved -> - persistent_term:put(?APP, lists:delete(Name, Saved)) - end, - persistent_term:erase({?APP, Name}), - ok. - -running() -> - persistent_term:get(?APP, []). - -server(Name) -> - case catch persistent_term:get({?APP, Name}) of - {'EXIT', {badarg,_}} -> undefined; - Service -> Service - end. diff --git a/apps/emqx_exhook/src/emqx_exhook_cli.erl b/apps/emqx_exhook/src/emqx_exhook_cli.erl index 0290d00ea..40c6e2f9f 100644 --- a/apps/emqx_exhook/src/emqx_exhook_cli.erl +++ b/apps/emqx_exhook/src/emqx_exhook_cli.erl @@ -22,25 +22,18 @@ cli(["server", "list"]) -> if_enabled(fun() -> - Services = emqx_exhook:list(), - [emqx_ctl:print("HookServer(~s)~n", - [emqx_exhook_server:format(Service)]) || Service <- Services] + ServerNames = emqx_exhook:list(), + [emqx_ctl:print("Server(~s)~n", [format(Name)]) || Name <- ServerNames] end); -cli(["server", "enable", Name0]) -> +cli(["server", "enable", Name]) -> if_enabled(fun() -> - Name = iolist_to_binary(Name0), - case find_server_options(Name) of - undefined -> - emqx_ctl:print("not_found~n"); - Opts -> - print(emqx_exhook:enable(Name, Opts)) - end + print(emqx_exhook:enable(list_to_existing_atom(Name))) end); cli(["server", "disable", Name]) -> if_enabled(fun() -> - print(emqx_exhook:disable(iolist_to_binary(Name))) + print(emqx_exhook:disable(list_to_existing_atom(Name))) end); cli(["server", "stats"]) -> @@ -73,7 +66,8 @@ find_server_options(Name) -> if_enabled(Fun) -> case lists:keymember(?APP, 1, application:which_applications()) of - true -> Fun(); + true -> + Fun(); _ -> hint() end. @@ -87,3 +81,11 @@ stats() -> _ -> Acc end end, [], emqx_metrics:all())). + +format(Name) -> + case emqx_exhook_mngr:server(Name) of + undefined -> + io_lib:format("name=~s, hooks=#{}, active=false", [Name]); + Server -> + emqx_exhook_server:format(Server) + end. diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl index 8e3dfaf82..ce375825f 100644 --- a/apps/emqx_exhook/src/emqx_exhook_mngr.erl +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -23,7 +23,18 @@ -include_lib("emqx/include/logger.hrl"). %% APIs --export([start_link/2]). +-export([start_link/3]). + +%% Mgmt API +-export([ enable/2 + , disable/2 + , list/1 + ]). + +%% Helper funcs +-export([ running/0 + , server/1 + ]). %% gen_server callbacks -export([ init/1 @@ -36,13 +47,15 @@ -record(state, { %% Running servers - running :: map(), + running :: map(), %% XXX: server order? %% Wait to reload servers waiting :: map(), %% Marked stopped servers stopped :: map(), %% Auto reconnect timer interval auto_reconnect :: false | non_neg_integer(), + %% Request options + request_options :: grpc_client:options(), %% Timer references trefs :: map() }). @@ -54,24 +67,40 @@ | {port, inet:port_number()} ]. +-define(DEFAULT_TIMEOUT, 60000). + -define(CNTER, emqx_exhook_counter). %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- --spec start_link(servers(), false | non_neg_integer()) +-spec start_link(servers(), false | non_neg_integer(), grpc_client:options()) ->ignore | {ok, pid()} | {error, any()}. -start_link(Servers, AutoReconnect) -> - gen_server:start_link(?MODULE, [Servers, AutoReconnect], []). +start_link(Servers, AutoReconnect, ReqOpts) -> + gen_server:start_link(?MODULE, [Servers, AutoReconnect, ReqOpts], []). + +-spec enable(pid(), atom()|string()) -> ok | {error, term()}. +enable(Pid, Name) -> + call(Pid, {load, Name}). + +-spec disable(pid(), atom()|string()) -> ok | {error, term()}. +disable(Pid, Name) -> + call(Pid, {unload, Name}). + +list(Pid) -> + call(Pid, list). + +call(Pid, Req) -> + gen_server:call(Pid, Req, ?DEFAULT_TIMEOUT). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([Servers, AutoReconnect]) -> +init([Servers, AutoReconnect, ReqOpts]) -> %% XXX: Due to the ExHook Module in the enterprise, %% this process may start multiple times and they will share this table try @@ -82,29 +111,53 @@ init([Servers, AutoReconnect]) -> end, %% Load the hook servers - {Waiting, Running} = load_all_servers(Servers), + {Waiting, Running} = load_all_servers(Servers, ReqOpts), {ok, ensure_reload_timer( #state{waiting = Waiting, running = Running, stopped = #{}, + request_options = ReqOpts, auto_reconnect = AutoReconnect, trefs = #{} } )}. %% @private -load_all_servers(Servers) -> - load_all_servers(Servers, #{}, #{}). -load_all_servers([], Waiting, Running) -> +load_all_servers(Servers, ReqOpts) -> + load_all_servers(Servers, ReqOpts, #{}, #{}). +load_all_servers([], _Request, Waiting, Running) -> {Waiting, Running}; -load_all_servers([{Name, Options}|More], Waiting, Running) -> - {NWaiting, NRunning} = case emqx_exhook:enable(Name, Options) of - ok -> - {Waiting, Running#{Name => Options}}; - {error, _} -> - {Waiting#{Name => Options}, Running} - end, - load_all_servers(More, NWaiting, NRunning). +load_all_servers([{Name, Options}|More], ReqOpts, Waiting, Running) -> + {NWaiting, NRunning} = + case emqx_exhook_server:load(Name, Options, ReqOpts) of + {ok, ServerState} -> + save(Name, ServerState), + {Waiting, Running#{Name => Options}}; + {error, _} -> + {Waiting#{Name => Options}, Running} + end, + load_all_servers(More, ReqOpts, NWaiting, NRunning). + +handle_call({load, Name}, _From, State) -> + {Result, NState} = do_load_server(Name, State), + {reply, Result, NState}; + +handle_call({unload, Name}, _From, State) -> + case do_unload_server(Name, State) of + {error, Reason} -> + {reply, {error, Reason}, State}; + {ok, NState} -> + {reply, ok, NState} + end; + +handle_call(list, _From, State = #state{ + running = Running, + waiting = Waiting, + stopped = Stopped}) -> + ServerNames = maps:keys(Running) + ++ maps:keys(Waiting) + ++ maps:keys(Stopped), + {reply, ServerNames, State}; handle_call(_Request, _From, State) -> Reply = ok, @@ -113,33 +166,27 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({timeout, _Ref, {reload, Name}}, - State0 = #state{waiting = Waiting, - running = Running, - trefs = TRefs}) -> - State = State0#state{trefs = maps:remove(Name, TRefs)}, - case maps:get(Name, Waiting, undefined) of - undefined -> - {noreply, State}; - Options -> - case emqx_exhook:enable(Name, Options) of - ok -> - ?LOG(warning, "Reconnect to exhook callback server " - "\"~s\" successfully!", [Name]), - {noreply, State#state{ - running = maps:put(Name, Options, Running), - waiting = maps:remove(Name, Waiting)} - }; - {error, _} -> - {noreply, ensure_reload_timer(State)} - end +handle_info({timeout, _Ref, {reload, Name}}, State) -> + {Result, NState} = do_load_server(Name, State), + case Result of + ok -> + {noreply, NState}; + {error, not_found} -> + {noreply, NState}; + {error, Reason} -> + ?LOG(warning, "Failed to reload exhook callback server \"~s\", " + "Reason: ~0p", [Name, Reason]), + {noreply, ensure_reload_timer(NState)} end; handle_info(_Info, State) -> {noreply, State}. -terminate(_Reason, _State) -> - _ = emqx_exhook:disable_all(), +terminate(_Reason, State = #state{stopped = Stopped}) -> + _ = maps:fold(fun(Name, _, AccIn) -> + {ok, NAccIn} = do_unload_server(Name, AccIn), + NAccIn + end, State, Stopped), _ = unload_exhooks(), ok. @@ -154,6 +201,49 @@ unload_exhooks() -> [emqx:unhook(Name, {M, F}) || {Name, {M, F, _A}} <- ?ENABLED_HOOKS]. +do_load_server(Name, State0 = #state{ + waiting = Waiting, + running = Running, + stopped = Stopped, + request_options = ReqOpts}) -> + State = clean_reload_timer(Name, State0), + case maps:get(Name, Running, undefined) of + undefined -> + case maps:get(Name, Stopped, + maps:get(Name, Waiting, undefined)) of + undefined -> + {{error, not_found}, State}; + Options -> + case emqx_exhook_server:load(Name, Options, ReqOpts) of + {ok, ServerState} -> + save(Name, ServerState), + ?LOG(info, "Load exhook callback server " + "\"~s\" successfully!", [Name]), + {ok, State#state{ + running = maps:put(Name, Options, Running), + waiting = maps:remove(Name, Waiting), + stopped = maps:remove(Name, Stopped) + } + }; + {error, Reason} -> + {{error, Reason}, State} + end + end; + _ -> + {{error, already_started}, State} + end. + +do_unload_server(Name, State = #state{running = Running, stopped = Stopped}) -> + case maps:take(Name, Running) of + error -> {error, not_running}; + {Options, NRunning} -> + ok = emqx_exhook_server:unload(server(Name)), + ok = unsave(Name), + {ok, State#state{running = NRunning, + stopped = maps:put(Name, Options, Stopped) + }} + end. + ensure_reload_timer(State = #state{auto_reconnect = false}) -> State; ensure_reload_timer(State = #state{waiting = Waiting, @@ -169,3 +259,38 @@ ensure_reload_timer(State = #state{waiting = Waiting, end end, TRefs, Waiting), State#state{trefs = NRefs}. + +clean_reload_timer(Name, State = #state{trefs = TRefs}) -> + case maps:take(Name, TRefs) of + error -> State; + {TRef, NTRefs} -> + _ = erlang:cancel_timer(TRef), + State#state{trefs = NTRefs} + end. + +%%-------------------------------------------------------------------- +%% Server state persistent + +save(Name, ServerState) -> + Saved = persistent_term:get(?APP, []), + persistent_term:put(?APP, lists:reverse([Name | Saved])), + persistent_term:put({?APP, Name}, ServerState). + +unsave(Name) -> + case persistent_term:get(?APP, []) of + [] -> + persistent_term:erase(?APP); + Saved -> + persistent_term:put(?APP, lists:delete(Name, Saved)) + end, + persistent_term:erase({?APP, Name}), + ok. + +running() -> + persistent_term:get(?APP, []). + +server(Name) -> + case catch persistent_term:get({?APP, Name}) of + {'EXIT', {badarg,_}} -> undefined; + Service -> Service + end. diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl index 897a2858d..9bf9cbad3 100644 --- a/apps/emqx_exhook/src/emqx_exhook_server.erl +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -24,7 +24,7 @@ -define(PB_CLIENT_MOD, emqx_exhook_v_1_hook_provider_client). %% Load/Unload --export([ load/2 +-export([ load/3 , unload/1 ]). @@ -39,8 +39,8 @@ -record(server, { %% Server name (equal to grpc client channel name) name :: server_name(), - %% The server started options - options :: options(), + %% The function options + options :: map(), %% gRPC channel pid channel :: pid(), %% Registered hook names and options @@ -84,8 +84,8 @@ %% Load/Unload APIs %%-------------------------------------------------------------------- --spec load(binary(), options()) -> {ok, server()} | {error, term()} . -load(Name0, Opts0) -> +-spec load(atom(), options(), map()) -> {ok, server()} | {error, term()} . +load(Name0, Opts0, ReqOpts) -> Name = to_list(Name0), {SvrAddr, ClientOpts} = channel_opts(Opts0), case emqx_exhook_sup:start_grpc_client_channel( @@ -93,7 +93,7 @@ load(Name0, Opts0) -> SvrAddr, ClientOpts) of {ok, _ChannPoolPid} -> - case do_init(Name) of + case do_init(Name, ReqOpts) of {ok, HookSpecs} -> %% Reigster metrics Prefix = lists:flatten( @@ -102,7 +102,7 @@ load(Name0, Opts0) -> %% Ensure hooks ensure_hooks(HookSpecs), {ok, #server{name = Name, - options = Opts0, + options = ReqOpts, channel = _ChannPoolPid, hookspec = HookSpecs, prefix = Prefix }}; @@ -149,22 +149,22 @@ filter(Ls) -> [ E || E <- Ls, E /= undefined]. -spec unload(server()) -> ok. -unload(#server{name = Name, hookspec = HookSpecs}) -> - _ = do_deinit(Name), +unload(#server{name = Name, options = ReqOpts, hookspec = HookSpecs}) -> + _ = do_deinit(Name, ReqOpts), _ = may_unload_hooks(HookSpecs), _ = emqx_exhook_sup:stop_grpc_client_channel(Name), ok. -do_deinit(Name) -> - _ = do_call(Name, 'on_provider_unloaded', #{}), +do_deinit(Name, ReqOpts) -> + _ = do_call(Name, 'on_provider_unloaded', #{}, ReqOpts), ok. -do_init(ChannName) -> +do_init(ChannName, ReqOpts) -> %% BrokerInfo defined at: exhook.protos BrokerInfo = maps:with([version, sysdescr, uptime, datetime], maps:from_list(emqx_sys:info())), Req = #{broker => BrokerInfo}, - case do_call(ChannName, 'on_provider_loaded', Req) of + case do_call(ChannName, 'on_provider_loaded', Req, ReqOpts) of {ok, InitialResp} -> try {ok, resovle_hookspec(maps:get(hooks, InitialResp, []))} @@ -230,7 +230,7 @@ may_unload_hooks(HookSpecs) -> end, maps:keys(HookSpecs)). format(#server{name = Name, hookspec = Hooks}) -> - io_lib:format("name=~p, hooks=~0p", [Name, Hooks]). + io_lib:format("name=~s, hooks=~0p, active=true", [Name, Hooks]). %%-------------------------------------------------------------------- %% APIs @@ -243,7 +243,8 @@ name(#server{name = Name}) -> -> ignore | {ok, Resp :: term()} | {error, term()}. -call(Hookpoint, Req, #server{name = ChannName, hookspec = Hooks, prefix = Prefix}) -> +call(Hookpoint, Req, #server{name = ChannName, options = ReqOpts, + hookspec = Hooks, prefix = Prefix}) -> GrpcFunc = hk2func(Hookpoint), case maps:get(Hookpoint, Hooks, undefined) of undefined -> ignore; @@ -258,7 +259,7 @@ call(Hookpoint, Req, #server{name = ChannName, hookspec = Hooks, prefix = Prefix false -> ignore; _ -> inc_metrics(Prefix, Hookpoint), - do_call(ChannName, GrpcFunc, Req) + do_call(ChannName, GrpcFunc, Req, ReqOpts) end end. @@ -276,9 +277,9 @@ match_topic_filter(_, []) -> match_topic_filter(TopicName, TopicFilter) -> lists:any(fun(F) -> emqx_topic:match(TopicName, F) end, TopicFilter). --spec do_call(string(), atom(), map()) -> {ok, map()} | {error, term()}. -do_call(ChannName, Fun, Req) -> - Options = #{channel => ChannName}, +-spec do_call(string(), atom(), map(), map()) -> {ok, map()} | {error, term()}. +do_call(ChannName, Fun, Req, ReqOpts) -> + Options = ReqOpts#{channel => ChannName}, ?LOG(debug, "Call ~0p:~0p(~0p, ~0p)", [?PB_CLIENT_MOD, Fun, Req, Options]), case catch apply(?PB_CLIENT_MOD, Fun, [Req, Options]) of {ok, Resp, _Metadata} -> diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index 96509181a..cb6cea635 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -41,7 +41,8 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Mngr = ?CHILD(emqx_exhook_mngr, worker, [servers(), auto_reconnect()]), + Mngr = ?CHILD(emqx_exhook_mngr, worker, + [servers(), auto_reconnect(), request_options()]), {ok, {{one_for_one, 10, 100}, [Mngr]}}. servers() -> @@ -50,6 +51,9 @@ servers() -> auto_reconnect() -> application:get_env(emqx_exhook, auto_reconnect, 60000). +request_options() -> + #{timeout => application:get_env(emqx_exhook, request_timeout, 5000)}. + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- From 6574fc4f14d2583a23eb66ba80aeb962d2f0af4c Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 11 Aug 2021 18:21:16 +0800 Subject: [PATCH 007/306] fix(exhook): set trap_exit flag --- apps/emqx_exhook/src/emqx_exhook_mngr.erl | 5 +++-- apps/emqx_exhook/src/emqx_exhook_sup.erl | 1 + apps/emqx_exhook/test/emqx_exhook_SUITE.erl | 13 ++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl index ce375825f..e5796cd7e 100644 --- a/apps/emqx_exhook/src/emqx_exhook_mngr.erl +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -101,6 +101,7 @@ call(Pid, Req) -> %%-------------------------------------------------------------------- init([Servers, AutoReconnect, ReqOpts]) -> + process_flag(trap_exit, true), %% XXX: Due to the ExHook Module in the enterprise, %% this process may start multiple times and they will share this table try @@ -182,11 +183,11 @@ handle_info({timeout, _Ref, {reload, Name}}, State) -> handle_info(_Info, State) -> {noreply, State}. -terminate(_Reason, State = #state{stopped = Stopped}) -> +terminate(_Reason, State = #state{running = Running}) -> _ = maps:fold(fun(Name, _, AccIn) -> {ok, NAccIn} = do_unload_server(Name, AccIn), NAccIn - end, State, Stopped), + end, State, Running), _ = unload_exhooks(), ok. diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index cb6cea635..6afed3d80 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -30,6 +30,7 @@ #{ id => Mod , start => {Mod, start_link, Args} , type => Type + , shutdown => 15000 } ). diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl index bf5d6ac1f..52664f57a 100644 --- a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl +++ b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl @@ -53,15 +53,14 @@ end_per_suite(_Cfg) -> %%-------------------------------------------------------------------- t_noserver_nohook(_) -> - emqx_exhook:disable(<<"default">>), - ?assertEqual([], loaded_exhook_hookpoints()), - [#{name := Name} = Opts] = emqx_config:get([exhook, servers]), - ok = emqx_exhook:enable(Name, Opts), - ?assertNotEqual([], loaded_exhook_hookpoints()). + emqx_exhook:disable(default), + ?assertEqual([], ets:tab2list(emqx_hooks)), + ok = emqx_exhook:enable(default), + ?assertNotEqual([], ets:tab2list(emqx_hooks)). t_cli_list(_) -> meck_print(), - ?assertEqual( [[emqx_exhook_server:format(Svr) || Svr <- emqx_exhook:list()]] + ?assertEqual( [[emqx_exhook_server:format(emqx_exhook_mngr:server(Name)) || Name <- emqx_exhook:list()]] , emqx_exhook_cli:cli(["server", "list"]) ), unmeck_print(). @@ -70,7 +69,7 @@ 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([["name=default, hooks=#{}, active=false"]], 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"])), From 3cef377b33d8cd1523e93a4c9fa1f5cb5fa1d606 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 11 Aug 2021 19:41:07 +0800 Subject: [PATCH 008/306] feat(exhook): make request_failed_action working --- apps/emqx_exhook/etc/emqx_exhook.conf | 38 +++++++++++++++++ apps/emqx_exhook/src/emqx_exhook.erl | 47 ++++++++++++++++----- apps/emqx_exhook/src/emqx_exhook_mngr.erl | 16 ++++++- apps/emqx_exhook/src/emqx_exhook_sup.erl | 11 +++-- apps/emqx_exhook/test/emqx_exhook_SUITE.erl | 20 +++++++++ 5 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 apps/emqx_exhook/etc/emqx_exhook.conf diff --git a/apps/emqx_exhook/etc/emqx_exhook.conf b/apps/emqx_exhook/etc/emqx_exhook.conf new file mode 100644 index 000000000..4df798fed --- /dev/null +++ b/apps/emqx_exhook/etc/emqx_exhook.conf @@ -0,0 +1,38 @@ +##==================================================================== +## EMQ X Hooks +##==================================================================== + +exhook: { + ## The default value or action will be returned, while the request to + ## the gRPC server failed or no available grpc server running. + ## + ## Default: deny + ## Value: ignore | deny + request_failed_action: deny + + ## The timeout to request grpc server + ## + ## Default: 5s + ## Value: Duration + request_timeout: 5s + + ## Whether to automatically reconnect (initialize) the gRPC server + ## + ## When gRPC is not available, exhook tries to request the gRPC service at + ## that interval and reinitialize the list of mounted hooks. + ## + ## Default: false + ## Value: false | Duration + auto_reconnect: 60s + + servers: [ + # { name: "default" + # url: "http://127.0.0.1:9000" + # #ssl: { + # # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + # # certfile: "{{ platform_etc_dir }}/certs/cert.pem" + # # keyfile: "{{ platform_etc_dir }}/certs/key.pem" + # #} + # } + ] +} diff --git a/apps/emqx_exhook/src/emqx_exhook.erl b/apps/emqx_exhook/src/emqx_exhook.erl index 008c3d241..8558a91ad 100644 --- a/apps/emqx_exhook/src/emqx_exhook.erl +++ b/apps/emqx_exhook/src/emqx_exhook.erl @@ -64,29 +64,54 @@ cast(Hookpoint, Req) -> cast(_, _, []) -> ok; -cast(Hookpoint, Req, [ServiceName|More]) -> +cast(Hookpoint, Req, [ServerName|More]) -> %% XXX: Need a real asynchronous running _ = emqx_exhook_server:call(Hookpoint, Req, - emqx_exhook_mngr:server(ServiceName)), + emqx_exhook_mngr:server(ServerName)), cast(Hookpoint, Req, More). -spec call_fold(atom(), term(), function()) -> {ok, term()} | {stop, term()}. call_fold(Hookpoint, Req, AccFun) -> - call_fold(Hookpoint, Req, AccFun, emqx_exhook_mngr:running()). + FailedAction = emqx_exhook_mngr:get_request_failed_action(), + ServerNames = emqx_exhook_mngr:running(), + case ServerNames == [] andalso FailedAction == deny of + true -> + {stop, deny_action_result(Hookpoint, Req)}; + _ -> + call_fold(Hookpoint, Req, FailedAction, AccFun, ServerNames) + end. -call_fold(_, Req, _, []) -> +call_fold(_, Req, _, _, []) -> {ok, Req}; -call_fold(Hookpoint, Req, AccFun, [ServiceName|More]) -> - case emqx_exhook_server:call(Hookpoint, Req, - emqx_exhook_mngr:server(ServiceName)) of +call_fold(Hookpoint, Req, FailedAction, AccFun, [ServerName|More]) -> + Server = emqx_exhook_mngr:server(ServerName), + case emqx_exhook_server:call(Hookpoint, Req, Server) of {ok, Resp} -> case AccFun(Req, Resp) of - {stop, NReq} -> {stop, NReq}; - {ok, NReq} -> call_fold(Hookpoint, NReq, AccFun, More); - _ -> call_fold(Hookpoint, Req, AccFun, More) + {stop, NReq} -> + {stop, NReq}; + {ok, NReq} -> + call_fold(Hookpoint, NReq, FailedAction, AccFun, More); + _ -> + call_fold(Hookpoint, Req, FailedAction, AccFun, More) end; _ -> - call_fold(Hookpoint, Req, AccFun, More) + case FailedAction of + deny -> + {stop, deny_action_result(Hookpoint, Req)}; + _ -> + call_fold(Hookpoint, Req, FailedAction, AccFun, More) + end end. + +%% XXX: Hard-coded the deny response +deny_action_result('client.authenticate', _) -> + #{result => false}; +deny_action_result('client.check_acl', _) -> + #{result => false}; +deny_action_result('message.publish', Msg) -> + %% TODO: Not support to deny a message + %% maybe we can put the 'allow_publish' into message header + Msg. diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl index e5796cd7e..cadd5eb37 100644 --- a/apps/emqx_exhook/src/emqx_exhook_mngr.erl +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -34,6 +34,8 @@ %% Helper funcs -export([ running/0 , server/1 + , put_request_failed_action/1 + , get_request_failed_action/0 ]). %% gen_server callbacks @@ -100,7 +102,7 @@ call(Pid, Req) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([Servers, AutoReconnect, ReqOpts]) -> +init([Servers, AutoReconnect, ReqOpts0]) -> process_flag(trap_exit, true), %% XXX: Due to the ExHook Module in the enterprise, %% this process may start multiple times and they will share this table @@ -111,7 +113,13 @@ init([Servers, AutoReconnect, ReqOpts]) -> ok end, + %% put the global option + put_request_failed_action( + maps:get(request_failed_action, ReqOpts0, deny) + ), + %% Load the hook servers + ReqOpts = maps:without([request_failed_action], ReqOpts0), {Waiting, Running} = load_all_servers(Servers, ReqOpts), {ok, ensure_reload_timer( #state{waiting = Waiting, @@ -272,6 +280,12 @@ clean_reload_timer(Name, State = #state{trefs = TRefs}) -> %%-------------------------------------------------------------------- %% Server state persistent +put_request_failed_action(Val) -> + persistent_term:put({?APP, request_failed_action}, Val). + +get_request_failed_action() -> + persistent_term:get({?APP, request_failed_action}). + save(Name, ServerState) -> Saved = persistent_term:get(?APP, []), persistent_term:put(?APP, lists:reverse([Name | Saved])), diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index 6afed3d80..e9c405de0 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -47,13 +47,18 @@ init([]) -> {ok, {{one_for_one, 10, 100}, [Mngr]}}. servers() -> - application:get_env(emqx_exhook, servers, []). + env(servers, []). auto_reconnect() -> - application:get_env(emqx_exhook, auto_reconnect, 60000). + env(auto_reconnect, 60000). request_options() -> - #{timeout => application:get_env(emqx_exhook, request_timeout, 5000)}. + #{timeout => env(request_timeout, 5000), + request_failed_action => env(request_failed_action, deny) + }. + +env(Key, Def) -> + application:get_env(emqx_exhook, Key, Def). %%-------------------------------------------------------------------- %% APIs diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl index 52664f57a..88964487c 100644 --- a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl +++ b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl @@ -58,6 +58,26 @@ t_noserver_nohook(_) -> ok = emqx_exhook:enable(default), ?assertNotEqual([], ets:tab2list(emqx_hooks)). +t_access_failed_if_no_server_running(_) -> + emqx_exhook:disable(default), + ClientInfo = #{clientid => <<"user-id-1">>, + username => <<"usera">>, + peerhost => {127,0,0,1}, + sockport => 1883, + protocol => mqtt, + mountpoint => undefined + }, + ?assertMatch({stop, #{auth_result := not_authorized}}, + emqx_exhook_handler:on_client_authenticate(ClientInfo, #{auth_result => success})), + + ?assertMatch({stop, deny}, + emqx_exhook_handler:on_client_check_acl(ClientInfo, publish, <<"t/1">>, allow)), + + Message = emqx_message:make(<<"t/1">>, <<"abc">>), + ?assertMatch({stop, Message}, + emqx_exhook_handler:on_message_publish(Message)), + emqx_exhook:enable(default). + t_cli_list(_) -> meck_print(), ?assertEqual( [[emqx_exhook_server:format(emqx_exhook_mngr:server(Name)) || Name <- emqx_exhook:list()]] From b3fac24c5e9836cd6dfabeac2a98869e4ea72c93 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 12 Aug 2021 14:49:26 +0800 Subject: [PATCH 009/306] chore(schmea): include the emqx_exhook_schema --- apps/emqx_machine/src/emqx_machine_schema.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index bf695bb19..f0de9d443 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -54,6 +54,7 @@ , emqx_dashboard_schema , emqx_gateway_schema , emqx_prometheus_schema + , emqx_exhook_schema ]). %% TODO: add a test case to ensure the list elements are unique From 4921c00a195d0070694a5f936d4e04cb920c49ca Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 12 Aug 2021 14:50:26 +0800 Subject: [PATCH 010/306] chore(exhook): change the name to binary type --- apps/emqx_exhook/src/emqx_exhook.erl | 4 ++-- apps/emqx_exhook/src/emqx_exhook_cli.erl | 15 ++++---------- apps/emqx_exhook/src/emqx_exhook_mngr.erl | 8 ++++--- apps/emqx_exhook/src/emqx_exhook_schema.erl | 16 +++++++++++++- apps/emqx_exhook/src/emqx_exhook_server.erl | 23 +++++++-------------- apps/emqx_exhook/src/emqx_exhook_sup.erl | 2 +- apps/emqx_exhook/test/emqx_exhook_SUITE.erl | 12 +++++------ 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/apps/emqx_exhook/src/emqx_exhook.erl b/apps/emqx_exhook/src/emqx_exhook.erl index 8558a91ad..c6b02e716 100644 --- a/apps/emqx_exhook/src/emqx_exhook.erl +++ b/apps/emqx_exhook/src/emqx_exhook.erl @@ -33,7 +33,7 @@ %% Mgmt APIs %%-------------------------------------------------------------------- --spec enable(atom()|string()) -> ok | {error, term()}. +-spec enable(binary()) -> ok | {error, term()}. enable(Name) -> with_mngr(fun(Pid) -> emqx_exhook_mngr:enable(Pid, Name) end). @@ -109,7 +109,7 @@ call_fold(Hookpoint, Req, FailedAction, AccFun, [ServerName|More]) -> %% XXX: Hard-coded the deny response deny_action_result('client.authenticate', _) -> #{result => false}; -deny_action_result('client.check_acl', _) -> +deny_action_result('client.authorize', _) -> #{result => false}; deny_action_result('message.publish', Msg) -> %% TODO: Not support to deny a message diff --git a/apps/emqx_exhook/src/emqx_exhook_cli.erl b/apps/emqx_exhook/src/emqx_exhook_cli.erl index 40c6e2f9f..860499698 100644 --- a/apps/emqx_exhook/src/emqx_exhook_cli.erl +++ b/apps/emqx_exhook/src/emqx_exhook_cli.erl @@ -28,12 +28,12 @@ cli(["server", "list"]) -> cli(["server", "enable", Name]) -> if_enabled(fun() -> - print(emqx_exhook:enable(list_to_existing_atom(Name))) + print(emqx_exhook:enable(iolist_to_binary(Name))) end); cli(["server", "disable", Name]) -> if_enabled(fun() -> - print(emqx_exhook:disable(list_to_existing_atom(Name))) + print(emqx_exhook:disable(iolist_to_binary(Name))) end); cli(["server", "stats"]) -> @@ -52,14 +52,6 @@ print(ok) -> print({error, Reason}) -> emqx_ctl:print("~p~n", [Reason]). -find_server_options(Name) -> - Ls = emqx_config:get([exhook, servers]), - case [ E || E = #{name := N} <- Ls, N =:= Name] of - [] -> undefined; - [Options] -> - maps:remove(name, Options) - end. - %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- @@ -85,7 +77,8 @@ stats() -> format(Name) -> case emqx_exhook_mngr:server(Name) of undefined -> - io_lib:format("name=~s, hooks=#{}, active=false", [Name]); + lists:flatten( + io_lib:format("name=~s, hooks=#{}, active=false", [Name])); Server -> emqx_exhook_server:format(Server) end. diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl index cadd5eb37..8c15fc330 100644 --- a/apps/emqx_exhook/src/emqx_exhook_mngr.erl +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -84,11 +84,11 @@ start_link(Servers, AutoReconnect, ReqOpts) -> gen_server:start_link(?MODULE, [Servers, AutoReconnect, ReqOpts], []). --spec enable(pid(), atom()|string()) -> ok | {error, term()}. +-spec enable(pid(), binary()) -> ok | {error, term()}. enable(Pid, Name) -> call(Pid, {load, Name}). --spec disable(pid(), atom()|string()) -> ok | {error, term()}. +-spec disable(pid(), binary()) -> ok | {error, term()}. disable(Pid, Name) -> call(Pid, {unload, Name}). @@ -136,7 +136,9 @@ load_all_servers(Servers, ReqOpts) -> load_all_servers(Servers, ReqOpts, #{}, #{}). load_all_servers([], _Request, Waiting, Running) -> {Waiting, Running}; -load_all_servers([{Name, Options}|More], ReqOpts, Waiting, Running) -> +load_all_servers([#{name := Name0} = Options0|More], ReqOpts, Waiting, Running) -> + Name = iolist_to_binary(Name0), + Options = Options0#{name => Name}, {NWaiting, NRunning} = case emqx_exhook_server:load(Name, Options, ReqOpts) of {ok, ServerState} -> diff --git a/apps/emqx_exhook/src/emqx_exhook_schema.erl b/apps/emqx_exhook/src/emqx_exhook_schema.erl index 68ffb5735..852a210fe 100644 --- a/apps/emqx_exhook/src/emqx_exhook_schema.erl +++ b/apps/emqx_exhook/src/emqx_exhook_schema.erl @@ -26,10 +26,24 @@ -behaviour(hocon_schema). +-type duration() :: integer(). + +-typerefl_from_string({duration/0, emqx_schema, to_duration}). + +-reflect_type([duration/0]). + -export([structs/0, fields/1]). + -export([t/1, t/3, t/4, ref/1]). -structs() -> [servers]. +structs() -> [exhook]. + +fields(exhook) -> + [ {request_failed_action, t(union([deny, ignore]), undefined, deny)} + , {request_timeout, t(duration(), undefined, "5s")} + , {auto_reconnect, t(union([false, duration()]), undefined, "60s")} + , {servers, t(hoconsc:array(ref(servers)), undefined, [])} + ]; fields(servers) -> [ {name, string()} diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl index 9bf9cbad3..94ac4df61 100644 --- a/apps/emqx_exhook/src/emqx_exhook_server.erl +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -38,7 +38,7 @@ -record(server, { %% Server name (equal to grpc client channel name) - name :: server_name(), + name :: binary(), %% The function options options :: map(), %% gRPC channel pid @@ -49,7 +49,6 @@ prefix :: list() }). --type server_name() :: string(). -type server() :: #server{}. -type hookpoint() :: 'client.connect' @@ -84,9 +83,8 @@ %% Load/Unload APIs %%-------------------------------------------------------------------- --spec load(atom(), options(), map()) -> {ok, server()} | {error, term()} . -load(Name0, Opts0, ReqOpts) -> - Name = to_list(Name0), +-spec load(binary(), options(), map()) -> {ok, server()} | {error, term()} . +load(Name, Opts0, ReqOpts) -> {SvrAddr, ClientOpts} = channel_opts(Opts0), case emqx_exhook_sup:start_grpc_client_channel( Name, @@ -112,20 +110,12 @@ load(Name0, Opts0, ReqOpts) -> {error, _} = E -> E end. -%% @private -to_list(Name) when is_atom(Name) -> - atom_to_list(Name); -to_list(Name) when is_binary(Name) -> - binary_to_list(Name); -to_list(Name) when is_list(Name) -> - Name. - %% @private channel_opts(Opts = #{url := URL}) -> case uri_string:parse(URL) of - #{scheme := <<"http">>, host := Host, port := Port} -> + #{scheme := "http", host := Host, port := Port} -> {format_http_uri("http", Host, Port), #{}}; - #{scheme := <<"https">>, host := Host, port := Port} -> + #{scheme := "https", host := Host, port := Port} -> SslOpts = case maps:get(ssl, Opts, undefined) of undefined -> []; @@ -230,7 +220,8 @@ may_unload_hooks(HookSpecs) -> end, maps:keys(HookSpecs)). format(#server{name = Name, hookspec = Hooks}) -> - io_lib:format("name=~s, hooks=~0p, active=true", [Name, Hooks]). + lists:flatten( + io_lib:format("name=~s, hooks=~0p, active=true", [Name, Hooks])). %%-------------------------------------------------------------------- %% APIs diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index e9c405de0..ba92e3f96 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -58,7 +58,7 @@ request_options() -> }. env(Key, Def) -> - application:get_env(emqx_exhook, Key, Def). + emqx_config:get([exhook, Key], Def). %%-------------------------------------------------------------------- %% APIs diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl index 88964487c..d2cc78b47 100644 --- a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl +++ b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl @@ -53,13 +53,13 @@ end_per_suite(_Cfg) -> %%-------------------------------------------------------------------- t_noserver_nohook(_) -> - emqx_exhook:disable(default), + emqx_exhook:disable(<<"default">>), ?assertEqual([], ets:tab2list(emqx_hooks)), - ok = emqx_exhook:enable(default), + ok = emqx_exhook:enable(<<"default">>), ?assertNotEqual([], ets:tab2list(emqx_hooks)). t_access_failed_if_no_server_running(_) -> - emqx_exhook:disable(default), + emqx_exhook:disable(<<"default">>), ClientInfo = #{clientid => <<"user-id-1">>, username => <<"usera">>, peerhost => {127,0,0,1}, @@ -67,16 +67,16 @@ t_access_failed_if_no_server_running(_) -> protocol => mqtt, mountpoint => undefined }, - ?assertMatch({stop, #{auth_result := not_authorized}}, + ?assertMatch({stop, {error, not_authorized}}, emqx_exhook_handler:on_client_authenticate(ClientInfo, #{auth_result => success})), ?assertMatch({stop, deny}, - emqx_exhook_handler:on_client_check_acl(ClientInfo, publish, <<"t/1">>, allow)), + emqx_exhook_handler:on_client_authorize(ClientInfo, publish, <<"t/1">>, allow)), Message = emqx_message:make(<<"t/1">>, <<"abc">>), ?assertMatch({stop, Message}, emqx_exhook_handler:on_message_publish(Message)), - emqx_exhook:enable(default). + emqx_exhook:enable(<<"default">>). t_cli_list(_) -> meck_print(), From a4d29ec8deb005415db77f5093645de4b78a0a9e Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 12 Aug 2021 16:16:10 +0800 Subject: [PATCH 011/306] chore(exhook): fix diaylzer warnings --- apps/emqx_exhook/src/emqx_exhook_mngr.erl | 6 ++++-- apps/emqx_exhook/src/emqx_exhook_server.erl | 8 ++------ apps/emqx_exhook/src/emqx_exhook_sup.erl | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/emqx_exhook/src/emqx_exhook_mngr.erl b/apps/emqx_exhook/src/emqx_exhook_mngr.erl index 8c15fc330..1a6e10bf0 100644 --- a/apps/emqx_exhook/src/emqx_exhook_mngr.erl +++ b/apps/emqx_exhook/src/emqx_exhook_mngr.erl @@ -195,8 +195,10 @@ handle_info(_Info, State) -> terminate(_Reason, State = #state{running = Running}) -> _ = maps:fold(fun(Name, _, AccIn) -> - {ok, NAccIn} = do_unload_server(Name, AccIn), - NAccIn + case do_unload_server(Name, AccIn) of + {ok, NAccIn} -> NAccIn; + _ -> AccIn + end end, State, Running), _ = unload_exhooks(), ok. diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl index 94ac4df61..924e5d7ba 100644 --- a/apps/emqx_exhook/src/emqx_exhook_server.erl +++ b/apps/emqx_exhook/src/emqx_exhook_server.erl @@ -73,17 +73,13 @@ -export_type([server/0]). --type options() :: #{ url := uri_string:uri_string() - , ssl => map() - }. - -dialyzer({nowarn_function, [inc_metrics/2]}). %%-------------------------------------------------------------------- %% Load/Unload APIs %%-------------------------------------------------------------------- --spec load(binary(), options(), map()) -> {ok, server()} | {error, term()} . +-spec load(binary(), map(), map()) -> {ok, server()} | {error, term()} . load(Name, Opts0, ReqOpts) -> {SvrAddr, ClientOpts} = channel_opts(Opts0), case emqx_exhook_sup:start_grpc_client_channel( @@ -268,7 +264,7 @@ match_topic_filter(_, []) -> match_topic_filter(TopicName, TopicFilter) -> lists:any(fun(F) -> emqx_topic:match(TopicName, F) end, TopicFilter). --spec do_call(string(), atom(), map(), map()) -> {ok, map()} | {error, term()}. +-spec do_call(binary(), atom(), map(), map()) -> {ok, map()} | {error, term()}. do_call(ChannName, Fun, Req, ReqOpts) -> Options = ReqOpts#{channel => ChannName}, ?LOG(debug, "Call ~0p:~0p(~0p, ~0p)", [?PB_CLIENT_MOD, Fun, Req, Options]), diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index ba92e3f96..32f8fa472 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -65,13 +65,13 @@ env(Key, Def) -> %%-------------------------------------------------------------------- -spec start_grpc_client_channel( - string(), + binary(), uri_string:uri_string(), grpc_client:options()) -> {ok, pid()} | {error, term()}. start_grpc_client_channel(Name, SvrAddr, Options) -> grpc_client_sup:create_channel_pool(Name, SvrAddr, Options). --spec stop_grpc_client_channel(string()) -> ok. +-spec stop_grpc_client_channel(binary()) -> ok. stop_grpc_client_channel(Name) -> %% Avoid crash due to hot-upgrade had unloaded %% grpc application From 1c86bd6199ebd019a2ad7b35d2e6313df25ef4bd Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 11 Aug 2021 21:29:03 +0800 Subject: [PATCH 012/306] feat(emqx_config): add emqx_config:fill_defaults/1 --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_config.erl | 128 +++++++++++++++--- apps/emqx/src/emqx_map_lib.erl | 45 ++++-- apps/emqx_machine/src/emqx_machine_schema.erl | 1 + rebar.config | 2 +- 5 files changed, 147 insertions(+), 31 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index ebe46559b..f34173fae 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -15,7 +15,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.4"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.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.14.1"}}} diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 151cf8e8e..dc1c6d7c5 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -20,14 +20,20 @@ -export([ init_load/2 , read_override_conf/0 , check_config/2 + , fill_defaults/1 + , fill_defaults/2 , save_configs/4 , save_to_app_env/1 , save_to_config_map/2 , save_to_override_conf/1 ]). --export([get_root/1, - get_root_raw/1]). +-export([ get_root/1 + , get_root_raw/1 + ]). + +-export([ get_default_value/1 + ]). -export([ get/1 , get/2 @@ -37,6 +43,12 @@ , put/2 ]). +-export([ save_schema_mod/1 + , get_schema_mod/0 + , get_schema_mod/1 + , get_root_names/0 + ]). + -export([ get_zone_conf/2 , get_zone_conf/3 , put_zone_conf/3 @@ -53,6 +65,7 @@ , update/3 , remove/1 , remove/2 + , reset/1 ]). -export([ get_raw/1 @@ -63,6 +76,7 @@ -define(CONF, conf). -define(RAW_CONF, raw_conf). +-define(PERSIS_MOD_ROOTNAMES, {?MODULE, default_conf}). -define(PERSIS_KEY(TYPE, ROOT), {?MODULE, TYPE, ROOT}). -define(ZONE_CONF_PATH(ZONE, PATH), [zones, ZONE | PATH]). -define(LISTENER_CONF_PATH(ZONE, LISTENER, PATH), [zones, ZONE, listeners, LISTENER | PATH]). @@ -170,20 +184,49 @@ put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config). -spec update(emqx_map_lib:config_key_path(), update_request()) -> ok | {error, term()}. -update(KeyPath, UpdateReq) -> - update(emqx_schema, KeyPath, UpdateReq). +update([RootName | _] = KeyPath, UpdateReq) -> + update(get_schema_mod(RootName), KeyPath, UpdateReq). -spec update(module(), emqx_map_lib:config_key_path(), update_request()) -> ok | {error, term()}. -update(SchemaModule, KeyPath, UpdateReq) -> - emqx_config_handler:update_config(SchemaModule, KeyPath, {update, UpdateReq}). +update(SchemaMod, KeyPath, UpdateReq) -> + emqx_config_handler:update_config(SchemaMod, KeyPath, {update, UpdateReq}). -spec remove(emqx_map_lib:config_key_path()) -> ok | {error, term()}. -remove(KeyPath) -> - remove(emqx_schema, KeyPath). +remove([RootName | _] = KeyPath) -> + remove(get_schema_mod(RootName), KeyPath). -remove(SchemaModule, KeyPath) -> - emqx_config_handler:update_config(SchemaModule, KeyPath, remove). +remove(SchemaMod, KeyPath) -> + emqx_config_handler:update_config(SchemaMod, KeyPath, remove). + +-spec reset(emqx_map_lib:config_key_path()) -> ok | {error, term()}. +reset([RootName | _] = KeyPath) -> + case get_default_value(KeyPath) of + {ok, Default} -> + emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, + {update, Default}); + {error, _} = Error -> + Error + end. + +-spec get_default_value(emqx_map_lib:config_key_path()) -> ok | {error, term()}. +get_default_value([RootName | _] = KeyPath) -> + BinKeyPath = [bin(Key) || Key <- KeyPath], + case find_raw([RootName]) of + {ok, RawConf} -> + RawConf1 = emqx_map_lib:deep_remove(BinKeyPath, RawConf), + SchemaMod = get_schema_mod(RootName), + try fill_defaults(SchemaMod, RawConf1) of FullConf -> + case emqx_map_lib:deep_find(BinKeyPath, FullConf) of + {not_found, _, _} -> {error, no_default_value}; + {ok, Val} -> {ok, Val} + end + catch error:_ -> + {error, required_conf} + end; + {not_found, _, _} -> + {error, {rootname_not_found, RootName}} + end. -spec get_raw(emqx_map_lib:config_key_path()) -> term(). get_raw(KeyPath) -> do_get(?RAW_CONF, KeyPath). @@ -208,7 +251,7 @@ put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config). %% NOTE: The order of the files is significant, configs from files orderd %% in the rear of the list overrides prior values. -spec init_load(module(), [string()] | binary() | hocon:config()) -> ok. -init_load(SchemaModule, Conf) when is_list(Conf) orelse is_binary(Conf) -> +init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> ParseOptions = #{format => richmap}, Parser = case is_binary(Conf) of true -> fun hocon:binary/2; @@ -216,39 +259,78 @@ init_load(SchemaModule, Conf) when is_list(Conf) orelse is_binary(Conf) -> end, case Parser(Conf, ParseOptions) of {ok, RawRichConf} -> - init_load(SchemaModule, RawRichConf); + init_load(SchemaMod, RawRichConf); {error, Reason} -> logger:error(#{msg => failed_to_load_hocon_conf, reason => Reason }), error(failed_to_load_hocon_conf) end; -init_load(SchemaModule, RawRichConf) when is_map(RawRichConf) -> +init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) -> %% check with richmap for line numbers in error reports (future enhancement) Opts = #{return_plain => true, nullable => true }, %% this call throws exception in case of check failure - {_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaModule, RawRichConf, Opts), - ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(CheckedConf), - hocon_schema:richmap_to_map(RawRichConf)). + {_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaMod, RawRichConf, Opts), + ok = save_schema_mod(SchemaMod), + ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(normalize_conf(CheckedConf)), + normalize_conf(hocon_schema:richmap_to_map(RawRichConf))). + +normalize_conf(Conf) -> + maps:with(get_root_names(), Conf). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). -check_config(SchemaModule, RawConf) -> +check_config(SchemaMod, RawConf) -> Opts = #{return_plain => true, nullable => true, format => map }, {AppEnvs, CheckedConf} = - hocon_schema:map_translate(SchemaModule, RawConf, Opts), + hocon_schema:map_translate(SchemaMod, RawConf, Opts), Conf = maps:with(maps:keys(RawConf), CheckedConf), {AppEnvs, emqx_map_lib:unsafe_atom_key_map(Conf)}. +-spec fill_defaults(raw_config()) -> map(). +fill_defaults(RawConf) -> + RootNames = get_root_names(), + maps:fold(fun(Key, Conf, Acc) -> + SubMap = #{Key => Conf}, + WithDefaults = case lists:member(Key, RootNames) of + true -> fill_defaults(get_schema_mod(Key), SubMap); + false -> SubMap + end, + maps:merge(Acc, WithDefaults) + end, #{}, RawConf). + +-spec fill_defaults(module(), raw_config()) -> map(). +fill_defaults(SchemaMod, RawConf) -> + hocon_schema:check_plain(SchemaMod, RawConf, + #{nullable => true, no_conversion => true}, [str(K) || K <- maps:keys(RawConf)]). + -spec read_override_conf() -> raw_config(). read_override_conf() -> load_hocon_file(emqx_override_conf_name(), map). +-spec save_schema_mod(module()) -> ok. +save_schema_mod(SchemaMod) -> + OldMods = get_schema_mod(), + NewMods = maps:from_list([{bin(RootName), SchemaMod} || RootName <- SchemaMod:structs()]), + persistent_term:put(?PERSIS_MOD_ROOTNAMES, maps:merge(OldMods, NewMods)). + +-spec get_schema_mod() -> #{binary() => atom()}. +get_schema_mod() -> + persistent_term:get(?PERSIS_MOD_ROOTNAMES, #{}). + +-spec get_schema_mod(atom() | binary()) -> [module()]. +get_schema_mod(RootName) -> + maps:get(bin(RootName), get_schema_mod()). + +-spec get_root_names() -> [binary()]. +get_root_names() -> + maps:keys(get_schema_mod()). + -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. save_configs(_AppEnvs, Conf, RawConf, OverrideConf) -> %% We may need also support hot config update for the apps that use application envs. @@ -338,10 +420,20 @@ do_deep_put(?RAW_CONF, KeyPath, Map, Value) -> atom(Bin) when is_binary(Bin) -> binary_to_existing_atom(Bin, latin1); +atom(Str) when is_list(Str) -> + list_to_existing_atom(Str); atom(Atom) when is_atom(Atom) -> Atom. +str(Bin) when is_binary(Bin) -> + binary_to_list(Bin); +str(Str) when is_list(Str) -> + Str; +str(Atom) when is_atom(Atom) -> + atom_to_list(Atom). + bin(Bin) when is_binary(Bin) -> Bin; +bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). conf_key(?CONF, RootName) -> diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index d720e771e..cda4f3a85 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -23,6 +23,8 @@ , deep_merge/2 , safe_atom_key_map/1 , unsafe_atom_key_map/1 + , jsonable_map/1 + , deep_convert/2 ]). -export_type([config_key/0, config_key_path/0]). @@ -97,21 +99,42 @@ deep_merge(BaseMap, NewMap) -> end, #{}, BaseMap), maps:merge(MergedBase, maps:with(NewKeys, NewMap)). +-spec deep_convert(map(), fun((K::any(), V::any()) -> {K1::any(), V1::any()})) -> map(). +deep_convert(Map, ConvFun) when is_map(Map) -> + maps:fold(fun(K, V, Acc) -> + {K1, V1} = ConvFun(K, deep_convert(V, ConvFun)), + Acc#{K1 => V1} + end, #{}, Map); +deep_convert(ListV, ConvFun) when is_list(ListV) -> + [deep_convert(V, ConvFun) || V <- ListV]; +deep_convert(Val, _) -> Val. + +-spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. unsafe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). +-spec safe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. safe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). +-spec jsonable_map(map()) -> map(). +jsonable_map(Map) -> + deep_convert(Map, fun(K, V) -> + {jsonable_value(K), jsonable_value(V)} + end). + +jsonable_value([]) -> []; +jsonable_value(Val) when is_list(Val) -> + case io_lib:printable_unicode_list(Val) of + true -> unicode:characters_to_binary(Val); + false -> Val + end; +jsonable_value(Val) -> + Val. + %%--------------------------------------------------------------------------- -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. +covert_keys_to_atom(BinKeyMap, Conv) -> + deep_convert(BinKeyMap, fun + (K, V) when is_atom(K) -> {K, V}; + (K, V) when is_binary(K) -> {Conv(K), V} + end). diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index f0de9d443..cef5e525f 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -54,6 +54,7 @@ , emqx_dashboard_schema , emqx_gateway_schema , emqx_prometheus_schema + , emqx_rule_engine_schema , emqx_exhook_schema ]). diff --git a/rebar.config b/rebar.config index b59909dbe..1abaef868 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.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}} ]}. From 8dbb14b6687eb97ce62e925e0b3f814abb8d0cf8 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 11 Aug 2021 21:30:05 +0800 Subject: [PATCH 013/306] feat(config): improve the API for resetting configs --- .../src/emqx_mgmt_api_configs.erl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 15bc8af11..2c37ee1a2 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -89,7 +89,7 @@ config_reset_api() -> - For a config entry that has no default value, an error 400 will be returned">>, parameters => ?PARAM_CONF_PATH, responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Remove configs successfully">>), + <<"200">> => emqx_mgmt_util:response_schema(<<"Reset configs successfully">>), <<"400">> => emqx_mgmt_util:response_error_schema( <<"It's not able to reset the config">>, ['INVALID_OPERATION']) } @@ -100,7 +100,8 @@ config_reset_api() -> %%%============================================================================================== %% parameters trans config(get, Req) -> - case emqx_config:find_raw(conf_path(Req)) of + Path = conf_path(Req), + case emqx_map_lib:deep_find(Path, get_full_config()) of {ok, Conf} -> {200, Conf}; {not_found, _, _} -> @@ -110,17 +111,21 @@ config(get, Req) -> config(put, Req) -> Path = conf_path(Req), ok = emqx_config:update(Path, http_body(Req)), - {200, emqx_config:get_raw(Path)}. + {200, emqx_map_lib:deep_get(Path, get_full_config())}. config_reset(post, Req) -> %% reset the config specified by the query string param 'conf_path' - Path = conf_path_reset(Req), - case emqx_config:remove(Path ++ conf_path_from_querystr(Req)) of - ok -> {200, emqx_config:get_raw(Path)}; + Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req), + case emqx_config:reset(Path) of + ok -> {200}; {error, Reason} -> {400, ?ERR_MSG(Reason)} end. +get_full_config() -> + emqx_map_lib:jsonable_map( + emqx_config:fill_defaults(emqx_config:get_raw([]))). + conf_path_from_querystr(Req) -> case proplists:get_value(<<"conf_path">>, cowboy_req:parse_qs(Req)) of undefined -> []; From b652a64dbb31bec52d0ef3ccabdf69f5412b4cea Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 12 Aug 2021 14:38:10 +0800 Subject: [PATCH 014/306] fix(config): reset config failed due to rootname not found --- apps/emqx/src/emqx_config.erl | 8 +++----- apps/emqx/src/emqx_config_handler.erl | 4 ++-- apps/emqx_management/src/emqx_mgmt_api_configs.erl | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index dc1c6d7c5..529a09bb7 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -214,15 +214,13 @@ get_default_value([RootName | _] = KeyPath) -> BinKeyPath = [bin(Key) || Key <- KeyPath], case find_raw([RootName]) of {ok, RawConf} -> - RawConf1 = emqx_map_lib:deep_remove(BinKeyPath, RawConf), - SchemaMod = get_schema_mod(RootName), - try fill_defaults(SchemaMod, RawConf1) of FullConf -> + RawConf1 = emqx_map_lib:deep_remove(BinKeyPath, #{bin(RootName) => RawConf}), + try fill_defaults(get_schema_mod(RootName), RawConf1) of FullConf -> case emqx_map_lib:deep_find(BinKeyPath, FullConf) of {not_found, _, _} -> {error, no_default_value}; {ok, Val} -> {ok, Val} end - catch error:_ -> - {error, required_conf} + catch error : Reason -> {error, Reason} end; {not_found, _, _} -> {error, {rootname_not_found, RootName}} diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index e4285b503..2a1e70501 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -82,8 +82,8 @@ handle_call({add_child, ConfKeyPath, HandlerName}, _From, handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> - OldConf = emqx_config:get_root(ConfKeyPath), - OldRawConf = emqx_config:get_root_raw(ConfKeyPath), + OldConf = emqx_config:get([]), + OldRawConf = emqx_config:get_raw([]), Result = try {NewRawConf, OverrideConf} = process_upadate_request(ConfKeyPath, OldRawConf, Handlers, UpdateArgs), diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 2c37ee1a2..72fc179eb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -46,7 +46,7 @@ -define(MAX_DEPTH, 1). --define(ERR_MSG(MSG), io_lib:format("~p", [MSG])). +-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))). api_spec() -> {config_apis() ++ [config_reset_api()], []}. From cc1222ffea6d88f1f3529a3ca10fda20ac8f7c5b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 12 Aug 2021 15:49:47 +0800 Subject: [PATCH 015/306] fix(emqx_config): correct some function sepcs and return values --- apps/emqx/src/emqx_config.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 529a09bb7..24141ceb9 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -176,8 +176,8 @@ find_listener_conf(Zone, Listener, KeyPath) -> -spec put(map()) -> ok. put(Config) -> maps:fold(fun(RootName, RootValue, _) -> - ?MODULE:put([RootName], RootValue) - end, [], Config). + ?MODULE:put([RootName], RootValue) + end, ok, Config). -spec put(emqx_map_lib:config_key_path(), term()) -> ok. put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config). @@ -209,7 +209,7 @@ reset([RootName | _] = KeyPath) -> Error end. --spec get_default_value(emqx_map_lib:config_key_path()) -> ok | {error, term()}. +-spec get_default_value(emqx_map_lib:config_key_path()) -> {ok, term()} | {error, term()}. get_default_value([RootName | _] = KeyPath) -> BinKeyPath = [bin(Key) || Key <- KeyPath], case find_raw([RootName]) of @@ -235,8 +235,8 @@ get_raw(KeyPath, Default) -> do_get(?RAW_CONF, KeyPath, Default). -spec put_raw(map()) -> ok. put_raw(Config) -> maps:fold(fun(RootName, RootV, _) -> - ?MODULE:put_raw([RootName], RootV) - end, [], hocon_schema:get_value([], Config)). + ?MODULE:put_raw([RootName], RootV) + end, ok, hocon_schema:get_value([], Config)). -spec put_raw(emqx_map_lib:config_key_path(), term()) -> ok. put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config). @@ -321,7 +321,7 @@ save_schema_mod(SchemaMod) -> get_schema_mod() -> persistent_term:get(?PERSIS_MOD_ROOTNAMES, #{}). --spec get_schema_mod(atom() | binary()) -> [module()]. +-spec get_schema_mod(atom() | binary()) -> module(). get_schema_mod(RootName) -> maps:get(bin(RootName), get_schema_mod()). From 4bf1e83449cb9b8998319649abed1f0a12c28225 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 12 Aug 2021 16:48:48 +0800 Subject: [PATCH 016/306] fix(test): variable 'Id' shadowed in 'fun' --- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 3ed55e6e1..6af7f3528 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -147,8 +147,8 @@ t_api(_) -> <<"permission">> := <<"deny">> }, jsx:decode(Result4)), - lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id}}) -> - {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id)]), []) + lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id0}}) -> + {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id0)]), []) end, Rules), {ok, 200, Result5} = request(get, uri(["authorization"]), []), ?assertEqual([], get_rules(Result5)), From 20f9cf19cd0944e0991ba34d54c63afc10f40980 Mon Sep 17 00:00:00 2001 From: lafirest Date: Thu, 12 Aug 2021 18:52:34 +0800 Subject: [PATCH 017/306] fix(emqx_gateway): fix some error in README and code --- apps/emqx_gateway/etc/emqx_gateway.conf | 18 ++++----- apps/emqx_gateway/src/coap/README.md | 40 ++++++++++--------- .../src/coap/emqx_coap_channel.erl | 6 +-- .../src/coap/emqx_coap_session.erl | 6 +-- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 6c7928174..8c77fe652 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -38,7 +38,7 @@ gateway: { } coap.1: { - enable_stats: false + enable_stats: false authentication: { enable: true @@ -52,16 +52,16 @@ gateway: { ] } - #authentication.enable: false + #authentication.enable: false - heartbeat: 30s - notify_type: qos - subscribe_qos: qos0 - publish_qos: qos1 - listener.udp.1: { - bind: 5683 + heartbeat: 30s + notify_type: qos + subscribe_qos: qos0 + publish_qos: qos1 + listener.udp.1: { + bind: 5683 } - } + } mqttsn.1: { ## The MQTT-SN Gateway ID in ADVERTISE message. diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index c451d6533..12b5ac5b7 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -108,10 +108,10 @@ The server manages the client through the ClientId. If the ClientId is wrong, EM 1. Create a Connection Method: POST - URI Schema: mqtt/{+topic}{?q\*} + URI Schema: mqtt/connection{?q\*} q\*: - - clientId := client uid + - clientid := client uid - username - password @@ -126,10 +126,10 @@ The server manages the client through the ClientId. If the ClientId is wrong, EM 2. Close a Connection Method : DELETE - URI Schema: mqtt/{+topic}{?q\*} + URI Schema: mqtt/connection{?q\*} q\*: - - clientId := client uid + - clientid := client uid - token Resonse: @@ -143,12 +143,14 @@ The server manages the client through the ClientId. If the ClientId is wrong, EM ### Heartbeat -The Coap client can maintain the "connection" with the server through the heartbeat (regardless of whether it is authenticated or not), so that the server will not release related resources +The Coap client can maintain the "connection" with the server through the heartbeat, +regardless of whether it is authenticated or not, +so that the server will not release related resources Method : PUT -URI Schema: mqtt/{+topic}{?q\*} +URI Schema: mqtt/connection{?q\*} q\*: -- clientId if authenticated +- clientid if authenticated - token if authenticated Response: @@ -166,7 +168,7 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. 1. Shared Options - - clientId + - clientid - token 2. Connect Options @@ -188,9 +190,9 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. - option - value type - default + Option + Type + Default @@ -204,7 +206,7 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. qos - MQTT QOS + MQTT Qos See here @@ -231,16 +233,16 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. - option - value type - default + Option + Type + Default qos - MQTT QOS + MQTT Qos See here @@ -260,7 +262,7 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. -5. MQTT QOS <=> CoAP non/con +5. MQTT Qos <=> CoAP non/con 1.notif_type Control the type of notify messages when the observed object has changed.Can be: @@ -268,7 +270,7 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. - non - con - qos - in this value, MQTT QOS0 -> non, QOS1/QOS2 -> con + in this value, MQTT Qos0 -> non, Qos1/Qos2 -> con 2.subscribe_qos Control the qos of subscribe.Can be: @@ -300,7 +302,7 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. 2. Session - Manager the "Transport Mnager" "Observe Resouces Manger" and next message id + Manager the "Transport Manager" "Observe Resouces Manger" and next message id 3. Transport Mnager diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 7612a6142..760a832ae 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -148,7 +148,7 @@ get_config(Key, #exec_ctx{config = Cfg}, Def) -> maps:get(Key, Cfg, Def). result_keys() -> - [out, reply, connection]. + [out, connection]. transfer_result(From, Value, Result) -> ?TRANSFER_RESULT(From, Value, Result). @@ -289,9 +289,9 @@ handle_result(out, #{out := Out}, _, Channel) -> handle_result(_, _, _, Channel) -> {ok, Channel}. -check_auth_state(Method, #channel{config = Cfg} = Channel) -> +check_auth_state(Msg, #channel{config = Cfg} = Channel) -> #{authentication := #{enable := Enable}} = Cfg, - check_token(Enable, Method, Channel). + check_token(Enable, Msg, Channel). check_token(true, #coap_message{options = Options} = Msg, diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index 98e24f05c..50e91797b 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -162,7 +162,7 @@ deliver(Delivers, Ctx, Session) -> end, lists:foldl(Fun, #{out => [], session => Session}, - Delivers). + lists:reverse(Delivers)). timeout(Timer, Ctx, Session) -> call_transport_manager(?FUNCTION_NAME, Timer, Ctx, [fun process_tm/3], Session). @@ -189,8 +189,8 @@ call_transport_manager(Fun, Session), emqx_coap_channel:transfer_result(session, Session2, Result2) catch Type:Reason:Stack -> - ?ERROR("process transmission with, message:~p failed~n -Type:~p,Reason:~p~n,StackTrace:~p~n", [Msg, Type, Reason, Stack]), + ?ERROR("process transmission with, message:~p failed~nType:~p,Reason:~p~n,StackTrace:~p~n", + [Msg, Type, Reason, Stack]), ?REPLY({error, internal_server_error}, Msg) end. From 3b5232824401cd725028d89e3acb8b76ab1be4b1 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 12 Aug 2021 19:41:53 +0800 Subject: [PATCH 018/306] fix: monitor response data format --- apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 1521e63a2..652e0b986 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -193,7 +193,8 @@ lookup(Params) -> lookup_(lists:foldl(Fun, #{}, Params)). lookup_(#{node := Node, counter := Counter}) -> - {200, sampling(Node, Counter)}; + Data = hd(maps:values(sampling(Node, Counter))), + {200, Data}; lookup_(#{node := Node}) -> {200, sampling(Node)}; lookup_(#{counter := Counter}) -> From 618125b1da1ab7498d1c495d59f9b6aafc6a46bb Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 12 Aug 2021 15:37:13 +0800 Subject: [PATCH 019/306] fix(connector): fix schema error for mongo --- apps/emqx_connector/src/emqx_connector_mongo.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 4af339538..36ea01db2 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -62,7 +62,8 @@ fields(sharded) -> , {servers, fun servers/1} ] ++ mongo_fields(); fields(topology) -> - [ {max_overflow, fun emqx_connector_schema_lib:pool_size/1} + [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} + , {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} @@ -81,6 +82,8 @@ mongo_fields() -> , {auth_source, #{type => binary(), nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} + , {topology, #{type => hoconsc:ref(?MODULE, topology), + nullable => true}} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -170,9 +173,10 @@ do_start(InstId, Opts0, Config = #{mongo_type := Type, ]; false -> [{ssl, false}] end, + Topology= maps:get(topology, Config, #{}), Opts = Opts0 ++ [{pool_size, PoolSize}, - {options, init_topology_options(maps:to_list(Config), [])}, + {options, init_topology_options(maps:to_list(Topology), [])}, {worker_options, init_worker_options(maps:to_list(Config), SslOpts)}], %% test the connection TestOpts = case maps:is_key(server, Config) of From dfa6761e49200f874dabeffd95c03de7a608f212 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Thu, 12 Aug 2021 19:58:52 +0200 Subject: [PATCH 020/306] fix(rlog): Ensure the correct order of table initalization --- apps/emqx_authn/src/emqx_authn_app.erl | 2 +- apps/emqx_gateway/src/emqx_gateway.app.src | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 52e59b2a6..bd9ec9cfe 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -27,8 +27,8 @@ ]). start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_authn_sup:start_link(), ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), + {ok, Sup} = emqx_authn_sup:start_link(), initialize(), {ok, Sup}. diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index 2fc329711..e25b767cc 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {emqx_gateway_app, []}}, - {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx]}, + {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx, emqx_authn]}, {env, []}, {modules, []}, {licenses, ["Apache 2.0"]}, From d1faf93bbf260ba09ed8d60c6f6645f95c606ffd Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 12 Aug 2021 09:33:47 +0800 Subject: [PATCH 021/306] chore(CI): rename artifact for build packages workflows Signed-off-by: zhanghongtong --- .github/workflows/build_packages.yaml | 92 ++++++++++++++++----------- rebar.config.erl | 5 +- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 7388b4d05..f14063eaf 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -44,6 +44,11 @@ jobs: echo "::set-output name=old_vsns::$old_vsns" echo "::set-output name=profiles::[\"emqx\", \"emqx-edge\"]" fi + - name: get otp version + id: get_otp_version + run: | + otp="$(erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().' -noshell)" + echo "::set-output name=otp::$otp" - name: set get token if: endsWith(github.repository, 'enterprise') run: | @@ -54,12 +59,13 @@ jobs: run: | make ensure-rebar3 ./rebar3 as default get-deps + rm -rf rebar.lock - name: gen zip file - run: zip -ryq source.zip source/* source/.[^.]* + run: zip -ryq source-${{ steps.get_otp_version.outputs.otp }}.zip source/* source/.[^.]* - uses: actions/upload-artifact@v2 with: - name: source - path: source.zip + name: source-${{ steps.get_otp_version.outputs.otp }} + path: source-${{ steps.get_otp_version.outputs.otp }}.zip windows: runs-on: windows-2019 @@ -77,19 +83,21 @@ jobs: steps: - uses: actions/download-artifact@v2 with: - name: source + name: source-23.2.7.2-emqx-2 path: . - name: unzip source code - run: Expand-Archive -Path source.zip -DestinationPath ./ + run: Expand-Archive -Path source-23.2.7.2-emqx-2.zip -DestinationPath ./ - uses: ilammy/msvc-dev-cmd@v1 - - uses: gleam-lang/setup-erlang@v1.1.0 + - uses: gleam-lang/setup-erlang@v1.1.2 id: install_erlang + ## gleam-lang/setup-erlang does not yet support the installation of otp24 on windows with: - otp-version: 24.0.5 + otp-version: 23.2 - name: build env: PYTHON: python DIAGNOSTIC: 1 + working-directory: source run: | $env:PATH = "${{ steps.install_erlang.outputs.erlpath }}\bin;$env:PATH" @@ -101,9 +109,9 @@ jobs: else { $pkg_name = "${{ matrix.profile }}-windows-$($version -replace '/').zip" } - cd source - ## We do not build/release bcrypt for windows package + ## We do not build/release bcrypt and quic for windows package Remove-Item -Recurse -Force -Path _build/default/lib/bcrypt/ + Remove-Item -Recurse -Force -Path _build/default/lib/quicer/ if (Test-Path rebar.lock) { Remove-Item -Force -Path rebar.lock } @@ -118,8 +126,8 @@ jobs: Get-FileHash -Path "_packages/${{ matrix.profile }}/$pkg_name" | Format-List | grep 'Hash' | awk '{print $3}' > _packages/${{ matrix.profile }}/$pkg_name.sha256 - name: run emqx timeout-minutes: 1 + working-directory: source run: | - cd source ./_build/${{ matrix.profile }}/rel/emqx/bin/emqx start Start-Sleep -s 5 ./_build/${{ matrix.profile }}/rel/emqx/bin/emqx stop @@ -128,7 +136,7 @@ jobs: - uses: actions/upload-artifact@v1 if: startsWith(github.ref, 'refs/tags/') with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-23.2.7.2-emqx-2 path: source/_packages/${{ matrix.profile }}/. mac: @@ -140,7 +148,7 @@ jobs: fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} - erl_otp: + otp: - 24.0.5-emqx-1 exclude: - profile: emqx-edge @@ -148,10 +156,10 @@ jobs: steps: - uses: actions/download-artifact@v2 with: - name: source + name: source-${{ matrix.otp }} path: . - name: unzip source code - run: unzip -q source.zip + run: unzip -q source-${{ matrix.otp }}.zip - name: prepare run: | brew update @@ -162,7 +170,7 @@ jobs: id: cache with: path: ~/.kerl - key: erl${{ matrix.erl_otp }}-macos10.15 + key: erl${{ matrix.otp }}-macos10.15 - name: build erlang if: steps.cache.outputs.cache-hit != 'true' timeout-minutes: 60 @@ -171,18 +179,18 @@ jobs: OTP_GITHUB_URL: https://github.com/emqx/otp run: | kerl update releases - kerl build ${{ matrix.erl_otp }} - kerl install ${{ matrix.erl_otp }} $HOME/.kerl/${{ matrix.erl_otp }} + kerl build ${{ matrix.otp }} + kerl install ${{ matrix.otp }} $HOME/.kerl/${{ matrix.otp }} - name: build + working-directory: source run: | - . $HOME/.kerl/${{ matrix.erl_otp }}/activate - cd source + . $HOME/.kerl/${{ matrix.otp }}/activate make ensure-rebar3 sudo cp rebar3 /usr/local/bin/rebar3 make ${{ matrix.profile }}-zip - name: test + working-directory: source run: | - 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 @@ -207,7 +215,7 @@ jobs: - uses: actions/upload-artifact@v1 if: startsWith(github.ref, 'refs/tags/') with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-${{ matrix.otp }} path: source/_packages/${{ matrix.profile }}/. linux: @@ -219,12 +227,6 @@ jobs: fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} - erl_otp: - - 23.2.7.2-emqx-2 - - 24.0.5-emqx-1 - arch: - - amd64 - - arm64 os: - ubuntu20.04 - ubuntu18.04 @@ -237,6 +239,12 @@ jobs: - centos6 - raspbian10 # - raspbian9 + arch: + - amd64 + - arm64 + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 exclude: - os: centos6 arch: arm64 @@ -265,10 +273,10 @@ jobs: platforms: all - uses: actions/download-artifact@v2 with: - name: source + name: source-${{ matrix.otp }} path: . - name: unzip source code - run: unzip -q source.zip + run: unzip -q source-${{ matrix.otp }}.zip - name: downloads old emqx zip packages env: PROFILE: ${{ matrix.profile }} @@ -298,7 +306,7 @@ jobs: done - name: build emqx packages env: - ERL_OTP: erl${{ matrix.erl_otp }} + ERL_OTP: erl${{ matrix.otp }} PROFILE: ${{ matrix.profile }} ARCH: ${{ matrix.arch }} SYSTEM: ${{ matrix.os }} @@ -327,7 +335,7 @@ jobs: - uses: actions/upload-artifact@v1 if: startsWith(github.ref, 'refs/tags/') with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-${{ matrix.otp }} path: source/_packages/${{ matrix.profile }}/. docker: @@ -338,16 +346,16 @@ jobs: fail-fast: false matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} - erl_otp: + otp: - 24.0.5-emqx-1 steps: - uses: actions/download-artifact@v2 with: - name: source + name: source-${{ matrix.otp }} path: . - name: unzip source code - run: unzip -q source.zip + run: unzip -q source-${{ matrix.otp }}.zip - uses: docker/setup-buildx-action@v1 - uses: docker/setup-qemu-action@v1 with: @@ -356,7 +364,7 @@ jobs: - name: build emqx docker image if: github.event_name != 'release' env: - ERL_OTP: erl${{ matrix.erl_otp }} + ERL_OTP: erl${{ matrix.otp }} PROFILE: ${{ matrix.profile }} working-directory: source run: | @@ -377,7 +385,7 @@ jobs: - name: build emqx docker image if: github.event_name == 'release' env: - ERL_OTP: erl${{ matrix.erl_otp }} + ERL_OTP: erl${{ matrix.otp }} PROFILE: ${{ matrix.profile }} working-directory: source run: | @@ -393,12 +401,18 @@ jobs: --push . delete-artifact: + runs-on: ubuntu-20.04 + strategy: + matrix: + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 needs: [prepare, mac, linux, docker] steps: - uses: geekyeggo/delete-artifact@v1 with: - name: source + name: source-${{ matrix.otp }} upload: runs-on: ubuntu-20.04 @@ -410,6 +424,8 @@ jobs: strategy: matrix: profile: ${{fromJSON(needs.prepare.outputs.profiles)}} + otp: + - 24.0.5-emqx-1 steps: - uses: actions/checkout@v2 @@ -420,7 +436,7 @@ jobs: echo 'EOF' >> $GITHUB_ENV - uses: actions/download-artifact@v2 with: - name: ${{ matrix.profile }} + name: ${{ matrix.profile }}-${{ matrix.otp }} path: ./_packages/${{ matrix.profile }} - name: install dos2unix run: sudo apt-get update && sudo apt install -y dos2unix diff --git a/rebar.config.erl b/rebar.config.erl index cfc5ce226..ae2cbfe88 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -397,10 +397,7 @@ is_debug(VarName) -> end. provide_bcrypt_dep() -> - case os:type() of - {win32, _} -> false; - _ -> true - end. + not is_win32(). provide_bcrypt_release(ReleaseType) -> provide_bcrypt_dep() andalso ReleaseType =:= cloud. From 93dbdaa84a99e670449b6732bfacb08a2b5ff19f Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Tue, 10 Aug 2021 19:09:22 +0800 Subject: [PATCH 022/306] feat: dashboard api support jwt --- apps/emqx_dashboard/etc/emqx_dashboard.conf | 2 + .../emqx_dashboard/include/emqx_dashboard.hrl | 13 +- apps/emqx_dashboard/src/emqx_dashboard.erl | 23 +- .../src/emqx_dashboard_admin.erl | 60 ++---- .../emqx_dashboard/src/emqx_dashboard_api.erl | 143 +++++++++---- .../emqx_dashboard/src/emqx_dashboard_app.erl | 1 + .../emqx_dashboard/src/emqx_dashboard_jwt.erl | 202 ++++++++++++++++++ .../src/emqx_dashboard_schema.erl | 1 + .../emqx_dashboard/src/emqx_dashboard_sup.erl | 2 +- 9 files changed, 357 insertions(+), 90 deletions(-) create mode 100644 apps/emqx_dashboard/src/emqx_dashboard_jwt.erl diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf index c25c2802d..2bc84569c 100644 --- a/apps/emqx_dashboard/etc/emqx_dashboard.conf +++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf @@ -7,6 +7,8 @@ emqx_dashboard:{ default_password: "public" ## notice: sample_interval should be divisible by 60. sample_interval: 10s + ## api jwt timeout. default is 30 minute + jwt_exptime: 30m listeners: [ { num_acceptors: 4 diff --git a/apps/emqx_dashboard/include/emqx_dashboard.hrl b/apps/emqx_dashboard/include/emqx_dashboard.hrl index 65f1d6ff5..265552bf7 100644 --- a/apps/emqx_dashboard/include/emqx_dashboard.hrl +++ b/apps/emqx_dashboard/include/emqx_dashboard.hrl @@ -14,7 +14,18 @@ %% limitations under the License. %%-------------------------------------------------------------------- --record(mqtt_admin, {username, password, tags, role = undefined}). +-record(mqtt_admin, { + username :: binary(), + password :: binary(), + tags :: list() | binary(), + role = undefined :: atom() + }). + +-record(mqtt_admin_jwt, { + token :: binary(), + username :: binary(), + exptime :: integer() + }). -type(mqtt_admin() :: #mqtt_admin{}). diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 656283bf6..27fe4e77d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -104,20 +104,27 @@ listener_name(Proto) -> authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of - {basic, Username, Password} -> - case emqx_dashboard_admin:check(iolist_to_binary(Username), - iolist_to_binary(Password)) of + {bearer, Token} -> + case emqx_dashboard_admin:jwt_verify(Token) of ok -> ok; - {error, _} -> + {error, token_timeout} -> {401, #{<<"WWW-Authenticate">> => - <<"Basic Realm=\"minirest-server\"">>}, - <<"UNAUTHORIZED">>} + <<"Bearer Realm=\"minirest-server\"">>}, + #{code => <<"TOKEN_TIME_OUT">>, + message => <<"POST '/login', get your new token">>} + }; + {error, not_found} -> + {401, #{<<"WWW-Authenticate">> => + <<"Bearer Realm=\"minirest-server\"">>}, + #{code => <<"BAD_TOKEN">>, + message => <<"POST '/login'">>}} end; _ -> {401, #{<<"WWW-Authenticate">> => - <<"Basic Realm=\"minirest-server\"">>}, - <<"UNAUTHORIZED">>} + <<"Bearer Realm=\"minirest-server\"">>}, + #{code => <<"UNAUTHORIZED">>, + message => <<"POST '/login'">>}} end. format(Port) when is_integer(Port) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index fdec41b2b..6334bbba6 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -18,8 +18,6 @@ -module(emqx_dashboard_admin). --behaviour(gen_server). - -include("emqx_dashboard.hrl"). -rlog_shard({?DASHBOARD_SHARD, mqtt_admin}). @@ -30,9 +28,6 @@ %% Mnesia bootstrap -export([mnesia/1]). -%% API Function Exports --export([start_link/0]). - %% mqtt_admin api -export([ add_user/3 , force_add_user/3 @@ -45,15 +40,13 @@ , check/2 ]). -%% gen_server Function Exports --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 +-export([ jwt_sign/2 + , jwt_verify/1 + , jwt_destroy_by_username/1 ]). +-export([add_default_user/0]). + %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -73,10 +66,6 @@ mnesia(copy) -> %% API %%-------------------------------------------------------------------- --spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -spec(add_user(binary(), binary(), binary()) -> ok | {error, any()}). add_user(Username, Password, Tags) when is_binary(Username), is_binary(Password) -> Admin = #mqtt_admin{username = Username, password = hash(Password), tags = Tags}, @@ -170,35 +159,27 @@ check(Username, Password) -> [#mqtt_admin{password = <>}] -> case Hash =:= md5_hash(Salt, Password) of true -> ok; - false -> {error, <<"Password Error">>} + false -> {error, <<"PASSWORD_ERROR">>} end; [] -> - {error, <<"Username Not Found">>} + {error, <<"USERNAME_ERROR">>} end. %%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- +%% jwt +jwt_sign(Username, Password) -> + case check(Username, Password) of + ok -> + emqx_dashboard_jwt:sign(Username, Password); + Error -> + Error + end. -init([]) -> - %% Add default admin user - _ = add_default_user(binenv(default_username), binenv(default_password)), - {ok, state}. +jwt_verify(Token) -> + emqx_dashboard_jwt:verify(Token). -handle_call(_Req, _From, State) -> - {reply, error, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info(_Msg, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +jwt_destroy_by_username(Username) -> + emqx_dashboard_jwt:destroy_by_username(Username). %%-------------------------------------------------------------------- %% Internal functions @@ -216,6 +197,9 @@ salt() -> Salt = rand:uniform(16#ffffffff), <>. +add_default_user() -> + add_default_user(binenv(default_username), binenv(default_password)). + binenv(Key) -> iolist_to_binary(emqx_config:get([emqx_dashboard, Key], "")). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index a56df7ec3..48045b160 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -16,6 +16,16 @@ -module(emqx_dashboard_api). +-ifndef(EMQX_ENTERPRISE). + +-define(RELEASE, community). + +-else. + +-define(VERSION, enterprise). + +-endif. + -behaviour(minirest_api). -include("emqx_dashboard.hrl"). @@ -28,17 +38,27 @@ -export([api_spec/0]). --export([ auth/2 +-export([ login/2 + , logout/2 , users/2 , user/2 , change_pwd/2 ]). -api_spec() -> - {[auth_api(), users_api(), user_api(), change_pwd_api()], schemas()}. +-define(EMPTY(V), (V == undefined orelse V == <<>>)). -schemas() -> - [#{auth => #{ +api_spec() -> + { + [ login_api() + , logout_api() + , users_api() + , user_api() + , change_pwd_api() + ], + []}. + +login_api() -> + AuthSchema = #{ type => object, properties => #{ username => #{ @@ -46,10 +66,55 @@ schemas() -> description => <<"Username">>}, password => #{ type => string, - description => <<"password">>} + description => <<"Password">>}}}, + TokenSchema = #{ + type => object, + properties => #{ + token => #{ + type => string, + description => <<"JWT Token">>}, + license => #{ + type => object, + properties => #{ + edition => #{ + type => string, + enum => [community, enterprise]}}}, + version => #{ + type => string}}}, + + Metadata = #{ + post => #{ + description => <<"Dashboard Auth">>, + 'requestBody' => request_body_schema(AuthSchema), + responses => #{ + <<"200">> => + response_schema(<<"Dashboard Auth successfully">>, TokenSchema), + <<"401">> => unauthorized_request() + }, + security => [] } - }}, - #{show_user => #{ + }, + {"/login", Metadata, login}. +logout_api() -> + AuthSchema = #{ + type => object, + properties => #{ + username => #{ + type => string, + description => <<"Username">>}}}, + Metadata = #{ + post => #{ + description => <<"Dashboard Auth">>, + 'requestBody' => request_body_schema(AuthSchema), + responses => #{ + <<"200">> => + response_schema(<<"Dashboard Auth successfully">>)} + } + }, + {"/logout", Metadata, logout}. + +users_api() -> + ShowSchema = #{ type => object, properties => #{ username => #{ @@ -57,10 +122,8 @@ schemas() -> description => <<"Username">>}, tag => #{ type => string, - description => <<"Tag">>} - } - }}, - #{create_user => #{ + description => <<"Tag">>}}}, + CreateSchema = #{ type => object, properties => #{ username => #{ @@ -71,36 +134,17 @@ schemas() -> description => <<"Password">>}, tag => #{ type => string, - description => <<"Tag">>} - } - }}]. - -auth_api() -> - Metadata = #{ - post => #{ - description => <<"Dashboard Auth">>, - 'requestBody' => request_body_schema(auth), - responses => #{ - <<"200">> => - response_schema(<<"Dashboard Auth successfully">>), - <<"400">> => bad_request() - }, - security => [] - } - }, - {"/auth", Metadata, auth}. - -users_api() -> + description => <<"Tag">>}}}, Metadata = #{ get => #{ description => <<"Get dashboard users">>, responses => #{ - <<"200">> => response_array_schema(<<"">>, show_user) + <<"200">> => response_array_schema(<<"">>, ShowSchema) } }, post => #{ description => <<"Create dashboard users">>, - 'requestBody' => request_body_schema(create_user), + 'requestBody' => request_body_schema(CreateSchema), responses => #{ <<"200">> => response_schema(<<"Create Users successfully">>), <<"400">> => bad_request() @@ -171,20 +215,26 @@ path_param_username() -> example => <<"admin">> }. --define(EMPTY(V), (V == undefined orelse V == <<>>)). - -auth(post, Request) -> +login(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), - case emqx_dashboard_admin:check(Username, Password) of - ok -> - {200}; - {error, Reason} -> - {400, #{code => <<"AUTH_FAIL">>, message => Reason}} + case emqx_dashboard_admin:jwt_sign(Username, Password) of + {ok, Token} -> + Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), + {200, #{token => Token, version => Version, license => #{edition => ?RELEASE}}}; + {error, Code} -> + {401, #{code => Code, message => <<"Auth filed">>}} end. +logout(_, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + Params = emqx_json:decode(Body, [return_maps]), + Username = maps:get(<<"username">>, Params), + emqx_dashboard_admin:jwt_destroy_by_username(Username), + {200}. + users(get, _Request) -> {200, [row(User) || User <- emqx_dashboard_admin:all_users()]}; @@ -251,3 +301,12 @@ bad_request() -> code => #{type => string} } }). +unauthorized_request() -> + response_schema(<<"Unauthorized">>, + #{ + type => object, + properties => #{ + message => #{type => string}, + code => #{type => string, enum => ['PASSWORD_ERROR', 'USERNAME_ERROR']} + } + }). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl index 54202d806..edcc19d8b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl @@ -29,6 +29,7 @@ start(_StartType, _StartArgs) -> ok = ekka_rlog:wait_for_shards([?DASHBOARD_SHARD], infinity), emqx_dashboard:start_listeners(), emqx_dashboard_cli:load(), + ok = emqx_dashboard_admin:add_default_user(), {ok, Sup}. stop(_State) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_jwt.erl b/apps/emqx_dashboard/src/emqx_dashboard_jwt.erl new file mode 100644 index 000000000..291affd66 --- /dev/null +++ b/apps/emqx_dashboard/src/emqx_dashboard_jwt.erl @@ -0,0 +1,202 @@ +%%-------------------------------------------------------------------- +%% 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_dashboard_jwt). + +-include("emqx_dashboard.hrl"). + +-define(TAB, mqtt_admin_jwt). + +-export([ sign/2 + , verify/1 + , destroy/1 + , destroy_by_username/1 + ]). + +-rlog_shard({?DASHBOARD_SHARD, mqtt_admin_jwt}). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-export([mnesia/1]). + +-define(EXPTIME, 60 * 60 * 1000). + +-define(CLEAN_JWT_INTERVAL, 60 * 60 * 1000). + +%%-------------------------------------------------------------------- +%% gen server part +-behaviour(gen_server). + +-export([start_link/0]). + +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +%%-------------------------------------------------------------------- +%% jwt function +-spec(sign(Username :: binary(), Password :: binary()) -> + {ok, Token :: binary()} | {error, Reason :: term()}). +sign(Username, Password) -> + do_sign(Username, Password). + +-spec(verify(Token :: binary()) -> Result :: ok | {error, token_timeout | not_found}). +verify(Token) -> + do_verify(Token). + +-spec(destroy(KeyOrKeys :: list() | binary() | #mqtt_admin_jwt{}) -> ok). +destroy([]) -> + ok; +destroy(JWTorTokenList) when is_list(JWTorTokenList)-> + [destroy(JWTorToken) || JWTorToken <- JWTorTokenList], + ok; +destroy(#mqtt_admin_jwt{token = Token}) -> + destroy(Token); +destroy(Token) when is_binary(Token)-> + do_destroy(Token). + +-spec(destroy_by_username(Username :: binary()) -> ok). +destroy_by_username(Username) -> + do_destroy_by_username(Username). + +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {type, set}, + {disc_copies, [node()]}, + {record_name, mqtt_admin_jwt}, + {attributes, record_info(fields, mqtt_admin_jwt)}, + {storage_properties, [{ets, [{read_concurrency, true}, + {write_concurrency, true}]}]}]); +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB, disc_copies). + +%%-------------------------------------------------------------------- +%% jwt apply +do_sign(Username, Password) -> + ExpTime = jwt_expiration_time(), + Salt = salt(), + JWK = jwk(Username, Password, Salt), + JWS = #{ + <<"alg">> => <<"HS256">> + }, + JWT = #{ + <<"iss">> => <<"EMQ X">>, + <<"exp">> => ExpTime + }, + Signed = jose_jwt:sign(JWK, JWS, JWT), + {_, Token} = jose_jws:compact(Signed), + ok = ekka_mnesia:dirty_write(format(Token, Username, ExpTime)), + {ok, Token}. + +do_verify(Token)-> + case lookup(Token) of + {ok, JWT = #mqtt_admin_jwt{exptime = ExpTime}} -> + case ExpTime > erlang:system_time(millisecond) of + true -> + ekka_mnesia:dirty_write(JWT#mqtt_admin_jwt{exptime = jwt_expiration_time()}), + ok; + _ -> + {error, token_timeout} + end; + Error -> + Error + end. + +do_destroy(Token) -> + Fun = fun mnesia:delete/1, + ekka_mnesia:transaction(?DASHBOARD_SHARD, Fun, [{?TAB, Token}]). + +do_destroy_by_username(Username) -> + gen_server:cast(?MODULE, {destroy, Username}). + +%%-------------------------------------------------------------------- +%% jwt internal util function + +lookup(Token) -> + case mnesia:dirty_read(?TAB, Token) of + [JWT] -> {ok, JWT}; + [] -> {error, not_found} + end. + +lookup_by_username(Username) -> + Spec = [{{mqtt_admin_jwt, '_', Username, '_'}, [], ['$_']}], + mnesia:dirty_select(?TAB, Spec). + +jwk(Username, Password, Salt) -> + Key = erlang:md5(<>), + #{ + <<"kty">> => <<"oct">>, + <<"k">> => jose_base64url:encode(Key) + }. + +jwt_expiration_time() -> + ExpTime = emqx_config:get([emqx_dashboard, jwt_exptime], ?EXPTIME), + erlang:system_time(millisecond) + ExpTime. + +salt() -> + _ = emqx_misc:rand_seed(), + Salt = rand:uniform(16#ffffffff), + <>. + +format(Token, Username, ExpTime) -> + #mqtt_admin_jwt{ + token = Token, + username = Username, + exptime = ExpTime + }. + +%%-------------------------------------------------------------------- +%% gen server +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + timer_clean(self()), + {ok, state}. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast({destroy, Username}, State) -> + Tokens = lookup_by_username(Username), + destroy(Tokens), + {noreply, State}; +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(clean_jwt, State) -> + timer_clean(self()), + Now = erlang:system_time(millisecond), + Spec = [{{mqtt_admin_jwt, '_', '_', '$1'}, [{'<', '$1', Now}], ['$_']}], + JWTList = mnesia:dirty_select(?TAB, Spec), + destroy(JWTList), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +timer_clean(Pid) -> + erlang:send_after(?CLEAN_JWT_INTERVAL, Pid, clean_jwt). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 2dae5e7e4..a42428e50 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -28,6 +28,7 @@ fields("emqx_dashboard") -> , {default_username, fun default_username/1} , {default_password, fun default_password/1} , {sample_interval, emqx_schema:t(emqx_schema:duration_s(), undefined, "10s")} + , {jwt_exptime, emqx_schema:t(emqx_schema:duration(), undefined, "30m")} ]; fields("http") -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_sup.erl b/apps/emqx_dashboard/src/emqx_dashboard_sup.erl index 8ec161f11..f3ecd6128 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_sup.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_sup.erl @@ -29,4 +29,4 @@ start_link() -> init([]) -> {ok, {{one_for_all, 10, 100}, - [?CHILD(emqx_dashboard_admin), ?CHILD(emqx_dashboard_collection)]}}. + [?CHILD(emqx_dashboard_jwt), ?CHILD(emqx_dashboard_collection)]}}. From 96ee04bbd0da1190f33d861a3ee04e71742779bc Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 12 Aug 2021 14:22:53 +0800 Subject: [PATCH 023/306] chore(modules): remove start emqx-ee load_modules --- apps/emqx_machine/src/emqx_machine.erl | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apps/emqx_machine/src/emqx_machine.erl b/apps/emqx_machine/src/emqx_machine.erl index 76a51fc3b..bcfb1f501 100644 --- a/apps/emqx_machine/src/emqx_machine.erl +++ b/apps/emqx_machine/src/emqx_machine.erl @@ -40,9 +40,6 @@ start() -> ok = set_backtrace_depth(), ok = print_otp_version_warning(), - %% need to load some app envs - %% TODO delete it once emqx boot does not depend on modules envs - _ = load_modules(), ok = load_config_files(), ok = ensure_apps_started(), @@ -80,14 +77,6 @@ print_vsn() -> ?ULOG("~s ~s is running now!~n", [emqx_app:get_description(), emqx_app:get_release()]). -endif. % TEST --ifndef(EMQX_ENTERPRISE). -load_modules() -> - application:load(emqx_modules). --else. -load_modules() -> - ok. --endif. - load_config_files() -> %% the app env 'config_files' for 'emqx` app should be set %% in app.time.config by boot script before starting Erlang VM @@ -155,9 +144,7 @@ reboot_apps() -> , emqx_data_bridge , emqx_bridge_mqtt , emqx_plugin_libs - , emqx_config_helper , emqx_management - , emqx_release_helper , emqx_retainer , emqx_exhook , emqx_rule_actions From 22dd6e1a864ef1748e3319a8723ed53a3be91d4a Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Fri, 13 Aug 2021 14:19:31 +0800 Subject: [PATCH 024/306] fix: rename tab & params --- apps/emqx_dashboard/etc/emqx_dashboard.conf | 2 +- apps/emqx_dashboard/src/emqx_dashboard.erl | 2 +- .../src/emqx_dashboard_admin.erl | 20 +++++++++---------- .../emqx_dashboard/src/emqx_dashboard_api.erl | 4 ++-- .../src/emqx_dashboard_schema.erl | 2 +- .../emqx_dashboard/src/emqx_dashboard_sup.erl | 2 +- ...board_jwt.erl => emqx_dashboard_token.erl} | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) rename apps/emqx_dashboard/src/{emqx_dashboard_jwt.erl => emqx_dashboard_token.erl} (98%) diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf index 2bc84569c..629facf7e 100644 --- a/apps/emqx_dashboard/etc/emqx_dashboard.conf +++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf @@ -8,7 +8,7 @@ emqx_dashboard:{ ## notice: sample_interval should be divisible by 60. sample_interval: 10s ## api jwt timeout. default is 30 minute - jwt_exptime: 30m + token_expired_time: 60m listeners: [ { num_acceptors: 4 diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 27fe4e77d..fb4a88066 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -105,7 +105,7 @@ listener_name(Proto) -> authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of {bearer, Token} -> - case emqx_dashboard_admin:jwt_verify(Token) of + case emqx_dashboard_admin:verify_token(Token) of ok -> ok; {error, token_timeout} -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 6334bbba6..b32d3d346 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -40,9 +40,9 @@ , check/2 ]). --export([ jwt_sign/2 - , jwt_verify/1 - , jwt_destroy_by_username/1 +-export([ sign_token/2 + , verify_token/1 + , destroy_token_by_username/1 ]). -export([add_default_user/0]). @@ -166,20 +166,20 @@ check(Username, Password) -> end. %%-------------------------------------------------------------------- -%% jwt -jwt_sign(Username, Password) -> +%% token +sign_token(Username, Password) -> case check(Username, Password) of ok -> - emqx_dashboard_jwt:sign(Username, Password); + emqx_dashboard_token:sign(Username, Password); Error -> Error end. -jwt_verify(Token) -> - emqx_dashboard_jwt:verify(Token). +verify_token(Token) -> + emqx_dashboard_token:verify(Token). -jwt_destroy_by_username(Username) -> - emqx_dashboard_jwt:destroy_by_username(Username). +destroy_token_by_username(Username) -> + emqx_dashboard_token:destroy_by_username(Username). %%-------------------------------------------------------------------- %% Internal functions diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 48045b160..422d246f4 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -220,7 +220,7 @@ login(post, Request) -> Params = emqx_json:decode(Body, [return_maps]), Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), - case emqx_dashboard_admin:jwt_sign(Username, Password) of + case emqx_dashboard_admin:sign_token(Username, Password) of {ok, Token} -> Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), {200, #{token => Token, version => Version, license => #{edition => ?RELEASE}}}; @@ -232,7 +232,7 @@ logout(_, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Username = maps:get(<<"username">>, Params), - emqx_dashboard_admin:jwt_destroy_by_username(Username), + emqx_dashboard_admin:destroy_token_by_username(Username), {200}. users(get, _Request) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index a42428e50..6cca17390 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -28,7 +28,7 @@ fields("emqx_dashboard") -> , {default_username, fun default_username/1} , {default_password, fun default_password/1} , {sample_interval, emqx_schema:t(emqx_schema:duration_s(), undefined, "10s")} - , {jwt_exptime, emqx_schema:t(emqx_schema:duration(), undefined, "30m")} + , {token_expired_time, emqx_schema:t(emqx_schema:duration(), undefined, "30m")} ]; fields("http") -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_sup.erl b/apps/emqx_dashboard/src/emqx_dashboard_sup.erl index f3ecd6128..90e84fcef 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_sup.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_sup.erl @@ -29,4 +29,4 @@ start_link() -> init([]) -> {ok, {{one_for_all, 10, 100}, - [?CHILD(emqx_dashboard_jwt), ?CHILD(emqx_dashboard_collection)]}}. + [?CHILD(emqx_dashboard_token), ?CHILD(emqx_dashboard_collection)]}}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_jwt.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl similarity index 98% rename from apps/emqx_dashboard/src/emqx_dashboard_jwt.erl rename to apps/emqx_dashboard/src/emqx_dashboard_token.erl index 291affd66..fdba7fb7e 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_jwt.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_dashboard_jwt). +-module(emqx_dashboard_token). -include("emqx_dashboard.hrl"). @@ -148,7 +148,7 @@ jwk(Username, Password, Salt) -> }. jwt_expiration_time() -> - ExpTime = emqx_config:get([emqx_dashboard, jwt_exptime], ?EXPTIME), + ExpTime = emqx_config:get([emqx_dashboard, token_expired_time], ?EXPTIME), erlang:system_time(millisecond) + ExpTime. salt() -> From 23a8e28f77bdaaa323e32adaf65c3a20cab93ffe Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Fri, 13 Aug 2021 14:26:43 +0800 Subject: [PATCH 025/306] fix: monitor timestamp millsecond --- apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 652e0b986..58b95f093 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -139,7 +139,8 @@ counter_schema() -> type => object, properties => #{ timestamp => #{ - type => integer}, + type => integer, + description => <<"Millisecond">>}, count => #{ type => integer}}}}. %%%============================================================================================== @@ -298,7 +299,7 @@ format([#mqtt_collect{timestamp = Ts, collect = {C, R, S, Re, S1, D}} | Collects [[Ts, S1] | Sent], [[Ts, D] | Dropped]}). add_key(Collects) -> - lists:reverse([#{timestamp => Ts, count => C} || [Ts, C] <- Collects]). + lists:reverse([#{timestamp => Ts * 1000, count => C} || [Ts, C] <- Collects]). format_single(Collects, Counter) -> #{Counter => format_single(Collects, counter_index(Counter), [])}. @@ -306,7 +307,7 @@ format_single([], _Index, Acc) -> lists:reverse(Acc); format_single([#mqtt_collect{timestamp = Ts, collect = Collect} | Collects], Index, Acc) -> format_single(Collects, Index, - [#{timestamp => Ts, count => erlang:element(Index, Collect)} | Acc]). + [#{timestamp => Ts * 1000, count => erlang:element(Index, Collect)} | Acc]). counter_index(connection) -> 1; counter_index(route) -> 2; From c78945441c6a6688c12446e8e3b21201f32e9e22 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 13 Aug 2021 14:10:36 +0800 Subject: [PATCH 026/306] chore(CI): timed trigger code synchronization --- .github/workflows/git_sync.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/git_sync.yaml b/.github/workflows/git_sync.yaml index 50fa8c364..48e29a37d 100644 --- a/.github/workflows/git_sync.yaml +++ b/.github/workflows/git_sync.yaml @@ -1,9 +1,10 @@ name: Sync to enterprise on: + schedule: + - cron: '0 */6 * * *' push: branches: - - master - main-v* jobs: From c90fc9578e81c4f7713acecda029a934f70cf732 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 13 Aug 2021 11:32:05 +0800 Subject: [PATCH 027/306] chore(CI): using docker official actions for build packages workflow Signed-off-by: zhanghongtong --- .github/workflows/build_packages.yaml | 63 ++++++++++++++------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index f14063eaf..a96ef705e 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -356,49 +356,50 @@ jobs: path: . - name: unzip source code run: unzip -q source-${{ matrix.otp }}.zip + - name: get version + id: version + working-directory: source + run: echo "::set-output name=version::$(./pkg-vsn.sh)" - uses: docker/setup-buildx-action@v1 - uses: docker/setup-qemu-action@v1 with: image: tonistiigi/binfmt:latest platforms: all - - name: build emqx docker image + - uses: docker/build-push-action@v2 if: github.event_name != 'release' - env: - ERL_OTP: erl${{ matrix.otp }} - PROFILE: ${{ matrix.profile }} - working-directory: source - run: | - PKG_VSN="$(./pkg-vsn.sh)" - docker buildx build --no-cache \ - --platform=linux/amd64,linux/arm64 \ - --build-arg PKG_VSN=$PKG_VSN \ - --build-arg BUILD_FROM=emqx/build-env:$ERL_OTP-alpine \ - --build-arg RUN_FROM=alpine:3.14 \ - --build-arg EMQX_NAME=$PROFILE \ - --tag emqx/$PROFILE:$PKG_VSN \ - -f deploy/docker/Dockerfile . + with: + push: false + pull: true + no-cache: true + platforms: linux/amd64,linux/arm64 + tags: emqx/${{ matrix.profile }}:${{ steps.version.outputs.version }} + build-args: | + PKG_VSN=${{ steps.version.outputs.version }} + BUILD_FROM=emqx/build-env:erl${{ matrix.otp }}-alpine + RUN_FROM=alpine:3.14 + EMQX_NAME=${{ matrix.profile }} + file: source/deploy/docker/Dockerfile + context: source - uses: docker/login-action@v1 if: github.event_name == 'release' with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - - name: build emqx docker image + - uses: docker/build-push-action@v2 if: github.event_name == 'release' - env: - ERL_OTP: erl${{ matrix.otp }} - PROFILE: ${{ matrix.profile }} - working-directory: source - run: | - PKG_VSN="$(./pkg-vsn.sh)" - docker buildx build --no-cache \ - --platform=linux/amd64,linux/arm64 \ - --build-arg PKG_VSN=$PKG_VSN \ - --build-arg BUILD_FROM=emqx/build-env:$ERL_OTP-alpine \ - --build-arg RUN_FROM=alpine:3.14 \ - --build-arg EMQX_NAME=$PROFILE \ - --tag emqx/$PROFILE:$PKG_VSN \ - -f deploy/docker/Dockerfile \ - --push . + with: + push: true + pull: true + no-cache: true + platforms: linux/amd64,linux/arm64 + tags: emqx/${{ matrix.profile }}:${{ steps.version.outputs.version }} + build-args: | + PKG_VSN=${{ steps.version.outputs.version }} + BUILD_FROM=emqx/build-env:erl${{ matrix.otp }}-alpine + RUN_FROM=alpine:3.14 + EMQX_NAME=${{ matrix.profile }} + file: source/deploy/docker/Dockerfile + context: source delete-artifact: From 391bcdcf7fabd740c4456874c11b154f12beee39 Mon Sep 17 00:00:00 2001 From: Mohammad Yosefpor Date: Thu, 12 Aug 2021 15:52:16 +0430 Subject: [PATCH 028/306] fix: running on Openshift cluster --- deploy/charts/emqx/templates/StatefulSet.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 12fd4c8d8..f8f7fce52 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -63,6 +63,8 @@ spec: claimName: {{ tpl . $ }} {{- end }} {{- end }} + - name: emqx-config + emptyDir: {} {{- if .Values.emqxLicneseSecretName }} - name: emqx-license secret: @@ -137,6 +139,8 @@ spec: subPath: "emqx.lic" readOnly: true {{ end }} + - name: emqx-config + mountPath: /opt/emqx/data/configs/ readinessProbe: httpGet: path: /api/v5/status From 4bd2240c1a4a9d1dfc0daef394880f874af0fc6c Mon Sep 17 00:00:00 2001 From: Mohammad Yosefpor Date: Fri, 13 Aug 2021 01:12:12 +0430 Subject: [PATCH 029/306] fix: apply suggestions --- deploy/charts/emqx/templates/StatefulSet.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index f8f7fce52..fca9d1903 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -63,8 +63,6 @@ spec: claimName: {{ tpl . $ }} {{- end }} {{- end }} - - name: emqx-config - emptyDir: {} {{- if .Values.emqxLicneseSecretName }} - name: emqx-license secret: @@ -132,15 +130,13 @@ spec: {{ toYaml .Values.resources | indent 12 }} volumeMounts: - name: emqx-data - mountPath: "/opt/emqx/data/mnesia" + mountPath: "/opt/emqx/data" {{ if .Values.emqxLicneseSecretName }} - name: emqx-license mountPath: "/opt/emqx/etc/emqx.lic" subPath: "emqx.lic" readOnly: true {{ end }} - - name: emqx-config - mountPath: /opt/emqx/data/configs/ readinessProbe: httpGet: path: /api/v5/status From 4b3b4dd54ca2f0c8cf4eea1b84d7cf6e691db9be Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Fri, 13 Aug 2021 16:38:33 +0800 Subject: [PATCH 030/306] fix: dashboard 404 conflict --- apps/emqx_dashboard/src/emqx_dashboard.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index fb4a88066..adbdbc8e7 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -57,7 +57,8 @@ start_listener({Proto, Port, Options}) -> name => "authorization", in => header}}}}, Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, - {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}], + {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, + {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}], Minirest = #{ protocol => Proto, base_path => ?BASE_PATH, From 88eb4aee64e953dc330b30f53b5ad696cd5dbbb4 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 13 Aug 2021 16:18:02 +0800 Subject: [PATCH 031/306] chore(Dashboard): Update dashboard tag --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dc44d208b..d521ca179 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.4 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.5 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From 26c0754732f6837c76721f1a2fa574a25d4fffb4 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 13 Aug 2021 17:55:28 +0800 Subject: [PATCH 032/306] chore(release): update emqx release version Signed-off-by: zhanghongtong --- 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 61444224c..85ba805bc 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-alpha.3"}). +-define(EMQX_RELEASE, {opensource, "5.0-alpha.4"}). -else. From b381b5d2b91f143f9469f27ed36c4967dd9106fe Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 13 Aug 2021 18:48:45 +0800 Subject: [PATCH 033/306] feat(emqx_config): return config maps when emqx_config:update/2,3 --- apps/emqx/src/emqx_config.erl | 32 +++++++++++++++------------ apps/emqx/src/emqx_config_handler.erl | 30 ++++++++++++++++++------- apps/emqx_authz/src/emqx_authz.erl | 4 ++-- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 24141ceb9..64a19b2f8 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -65,7 +65,7 @@ , update/3 , remove/1 , remove/2 - , reset/1 + , reset/2 ]). -export([ get_raw/1 @@ -184,27 +184,31 @@ put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config). -spec update(emqx_map_lib:config_key_path(), update_request()) -> ok | {error, term()}. -update([RootName | _] = KeyPath, UpdateReq) -> - update(get_schema_mod(RootName), KeyPath, UpdateReq). +update(KeyPath, UpdateReq) -> + update(KeyPath, UpdateReq, #{}). --spec update(module(), emqx_map_lib:config_key_path(), update_request()) -> - ok | {error, term()}. -update(SchemaMod, KeyPath, UpdateReq) -> - emqx_config_handler:update_config(SchemaMod, KeyPath, {update, UpdateReq}). +-spec update(emqx_map_lib:config_key_path(), update_request(), + emqx_config_handler:update_opts()) -> ok | {error, term()}. +update([RootName | _] = KeyPath, UpdateReq, Opts) -> + emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, + {{update, UpdateReq}, Opts}). -spec remove(emqx_map_lib:config_key_path()) -> ok | {error, term()}. -remove([RootName | _] = KeyPath) -> - remove(get_schema_mod(RootName), KeyPath). +remove(KeyPath) -> + remove(KeyPath, #{}). -remove(SchemaMod, KeyPath) -> - emqx_config_handler:update_config(SchemaMod, KeyPath, remove). +-spec remove(emqx_map_lib:config_key_path(), emqx_config_handler:update_opts()) -> + ok | {error, term()}. +remove([RootName | _] = KeyPath, Opts) -> + emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, {remove, Opts}). --spec reset(emqx_map_lib:config_key_path()) -> ok | {error, term()}. -reset([RootName | _] = KeyPath) -> +-spec reset(emqx_map_lib:config_key_path(), emqx_config_handler:update_opts()) -> + ok | {error, term()}. +reset([RootName | _] = KeyPath, Opts) -> case get_default_value(KeyPath) of {ok, Default} -> emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, - {update, Default}); + {{update, Default}, Opts}); {error, _} = Error -> Error end. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index 2a1e70501..cae104945 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -38,9 +38,15 @@ -define(MOD, {mod}). +-export_type([update_opts/0, update_cmd/0, update_args/0]). -type handler_name() :: module(). -type handlers() :: #{emqx_config:config_key() => handlers(), ?MOD => handler_name()}. --type update_args() :: {update, emqx_config:update_request()} | remove. +-type update_cmd() :: {update, emqx_config:update_request()} | remove. +-type update_opts() :: #{ + %% fill the default values into the rawconf map + rawconf_with_defaults => boolean() + }. +-type update_args() :: {update_cmd(), Opts :: update_opts()}. -optional_callbacks([ pre_config_update/2 , post_config_update/3 @@ -61,7 +67,7 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []). -spec update_config(module(), emqx_config:config_key_path(), update_args()) -> - ok | {error, term()}. + {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, UpdateArgs}). @@ -80,7 +86,7 @@ handle_call({add_child, ConfKeyPath, HandlerName}, _From, {reply, ok, State#{handlers => emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; -handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, +handle_call({change_config, SchemaModule, ConfKeyPath, {_Cmd, Opts} = UpdateArgs}, _From, #{handlers := Handlers} = State) -> OldConf = emqx_config:get([]), OldRawConf = emqx_config:get_raw([]), @@ -89,7 +95,10 @@ handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, Handlers, UpdateArgs), {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf), _ = do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateArgs), - emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) + case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of + ok -> {ok, emqx_config:get([]), return_rawconf(Opts)}; + Err -> Err + end catch Error:Reason:ST -> ?LOG(error, "change_config failed: ~p", [{Error, Reason, ST}]), {error, Reason} @@ -112,12 +121,12 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -process_upadate_request(ConfKeyPath, OldRawConf, _Handlers, remove) -> +process_upadate_request(ConfKeyPath, OldRawConf, _Handlers, {remove, _Opts}) -> BinKeyPath = bin_path(ConfKeyPath), NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf), OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()), {NewRawConf, OverrideConf}; -process_upadate_request(ConfKeyPath, OldRawConf, Handlers, {update, UpdateReq}) -> +process_upadate_request(ConfKeyPath, OldRawConf, Handlers, {{update, UpdateReq}, _Opts}) -> NewRawConf = do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq), OverrideConf = update_override_config(NewRawConf), {NewRawConf, OverrideConf}. @@ -172,8 +181,13 @@ update_override_config(RawConf) -> OldConf = emqx_config:read_override_conf(), maps:merge(OldConf, RawConf). -up_req(remove) -> '$remove'; -up_req({update, Req}) -> Req. +up_req({remove, _Opts}) -> '$remove'; +up_req({{update, Req}, _Opts}) -> Req. + +return_rawconf(#{rawconf_with_defaults := true}) -> + emqx_config:fill_defaults(emqx_config:get_raw([])); +return_rawconf(_) -> + emqx_config:get_raw([]). bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath]. diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 5c46e7749..d4399b82d 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -61,10 +61,10 @@ lookup(Id) -> end. move(Id, Position) -> - emqx_config:update(emqx_authz_schema, ?CONF_KEY_PATH, {move, Id, Position}). + emqx_config:update(?CONF_KEY_PATH, {move, Id, Position}). update(Cmd, Rules) -> - emqx_config:update(emqx_authz_schema, ?CONF_KEY_PATH, {Cmd, Rules}). + emqx_config:update(?CONF_KEY_PATH, {Cmd, Rules}). pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_rule_by_id(Id), From 7f03cd0e8b79af026baf0d01bbe653899a14d382 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 16 Aug 2021 15:11:00 +0800 Subject: [PATCH 034/306] fix(config): update the calls to emqx_config:update/2,3 --- apps/emqx/src/emqx_config.erl | 50 +++++++++++++------ apps/emqx/test/emqx_alarm_SUITE.erl | 4 +- apps/emqx_authz/src/emqx_authz_api.erl | 10 ++-- apps/emqx_authz/test/emqx_authz_SUITE.erl | 28 +++++------ apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 8 +-- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 8 +-- .../test/emqx_authz_mongo_SUITE.erl | 8 +-- .../test/emqx_authz_mysql_SUITE.erl | 8 +-- .../test/emqx_authz_pgsql_SUITE.erl | 8 +-- .../test/emqx_authz_redis_SUITE.erl | 8 +-- .../src/emqx_data_bridge_api.erl | 4 +- .../src/emqx_mgmt_api_configs.erl | 12 +++-- .../src/emqx_prometheus_api.erl | 2 +- apps/emqx_statsd/src/emqx_statsd_api.erl | 2 +- 14 files changed, 92 insertions(+), 68 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 64a19b2f8..cff511352 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -43,7 +43,7 @@ , put/2 ]). --export([ save_schema_mod/1 +-export([ save_schema_mod_and_names/1 , get_schema_mod/0 , get_schema_mod/1 , get_root_names/0 @@ -76,7 +76,7 @@ -define(CONF, conf). -define(RAW_CONF, raw_conf). --define(PERSIS_MOD_ROOTNAMES, {?MODULE, default_conf}). +-define(PERSIS_SCHEMA_MODS, {?MODULE, schema_mods}). -define(PERSIS_KEY(TYPE, ROOT), {?MODULE, TYPE, ROOT}). -define(ZONE_CONF_PATH(ZONE, PATH), [zones, ZONE | PATH]). -define(LISTENER_CONF_PATH(ZONE, LISTENER, PATH), [zones, ZONE, listeners, LISTENER | PATH]). @@ -183,17 +183,18 @@ put(Config) -> put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config). -spec update(emqx_map_lib:config_key_path(), update_request()) -> - ok | {error, term()}. + {ok, config(), raw_config()} | {error, term()}. update(KeyPath, UpdateReq) -> update(KeyPath, UpdateReq, #{}). -spec update(emqx_map_lib:config_key_path(), update_request(), - emqx_config_handler:update_opts()) -> ok | {error, term()}. + emqx_config_handler:update_opts()) -> + {ok, config(), raw_config()} | {error, term()}. update([RootName | _] = KeyPath, UpdateReq, Opts) -> emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, {{update, UpdateReq}, Opts}). --spec remove(emqx_map_lib:config_key_path()) -> ok | {error, term()}. +-spec remove(emqx_map_lib:config_key_path()) -> {ok, config(), raw_config()} | {error, term()}. remove(KeyPath) -> remove(KeyPath, #{}). @@ -203,7 +204,7 @@ remove([RootName | _] = KeyPath, Opts) -> emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, {remove, Opts}). -spec reset(emqx_map_lib:config_key_path(), emqx_config_handler:update_opts()) -> - ok | {error, term()}. + {ok, config(), raw_config()} | {error, term()}. reset([RootName | _] = KeyPath, Opts) -> case get_default_value(KeyPath) of {ok, Default} -> @@ -275,12 +276,12 @@ init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) -> }, %% this call throws exception in case of check failure {_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaMod, RawRichConf, Opts), - ok = save_schema_mod(SchemaMod), + ok = save_schema_mod_and_names(SchemaMod), ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(normalize_conf(CheckedConf)), normalize_conf(hocon_schema:richmap_to_map(RawRichConf))). normalize_conf(Conf) -> - maps:with(get_root_names(), Conf). + maps:with(get_root_names(bin), Conf). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). @@ -296,7 +297,7 @@ check_config(SchemaMod, RawConf) -> -spec fill_defaults(raw_config()) -> map(). fill_defaults(RawConf) -> - RootNames = get_root_names(), + RootNames = get_root_names(bin), maps:fold(fun(Key, Conf, Acc) -> SubMap = #{Key => Conf}, WithDefaults = case lists:member(Key, RootNames) of @@ -309,21 +310,26 @@ fill_defaults(RawConf) -> -spec fill_defaults(module(), raw_config()) -> map(). fill_defaults(SchemaMod, RawConf) -> hocon_schema:check_plain(SchemaMod, RawConf, - #{nullable => true, no_conversion => true}, [str(K) || K <- maps:keys(RawConf)]). + #{nullable => true, no_conversion => true}, root_names_from_conf(RawConf)). -spec read_override_conf() -> raw_config(). read_override_conf() -> load_hocon_file(emqx_override_conf_name(), map). --spec save_schema_mod(module()) -> ok. -save_schema_mod(SchemaMod) -> +-spec save_schema_mod_and_names(module()) -> ok. +save_schema_mod_and_names(SchemaMod) -> + RootNames = SchemaMod:structs(), OldMods = get_schema_mod(), - NewMods = maps:from_list([{bin(RootName), SchemaMod} || RootName <- SchemaMod:structs()]), - persistent_term:put(?PERSIS_MOD_ROOTNAMES, maps:merge(OldMods, NewMods)). + OldNames = get_root_names(), + NewMods = maps:from_list([{bin(Name), SchemaMod} || Name <- RootNames]), + persistent_term:put(?PERSIS_SCHEMA_MODS, #{ + mods => maps:merge(OldMods, NewMods), + names => lists:usort(OldNames ++ RootNames) + }). -spec get_schema_mod() -> #{binary() => atom()}. get_schema_mod() -> - persistent_term:get(?PERSIS_MOD_ROOTNAMES, #{}). + maps:get(mods, persistent_term:get(?PERSIS_SCHEMA_MODS, #{mods => #{}})). -spec get_schema_mod(atom() | binary()) -> module(). get_schema_mod(RootName) -> @@ -331,6 +337,9 @@ get_schema_mod(RootName) -> -spec get_root_names() -> [binary()]. get_root_names() -> + maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). + +get_root_names(bin) -> maps:keys(get_schema_mod()). -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. @@ -420,6 +429,17 @@ do_deep_put(?CONF, KeyPath, Map, Value) -> do_deep_put(?RAW_CONF, KeyPath, Map, Value) -> emqx_map_lib:deep_put([bin(Key) || Key <- KeyPath], Map, Value). +root_names_from_conf(RawConf) -> + Keys = maps:keys(RawConf), + StrNames = [str(K) || K <- Keys], + AtomNames = lists:foldl(fun(K, Acc) -> + try [atom(K) | Acc] + catch error:badarg -> Acc + end + end, [], Keys), + PossibleNames = StrNames ++ AtomNames, + [Name || Name <- get_root_names(), lists:member(Name, PossibleNames)]. + atom(Bin) when is_binary(Bin) -> binary_to_existing_atom(Bin, latin1); atom(Str) when is_list(Str) -> diff --git a/apps/emqx/test/emqx_alarm_SUITE.erl b/apps/emqx/test/emqx_alarm_SUITE.erl index 1157f94bc..e797a91d2 100644 --- a/apps/emqx/test/emqx_alarm_SUITE.erl +++ b/apps/emqx/test/emqx_alarm_SUITE.erl @@ -28,14 +28,14 @@ all() -> emqx_ct:all(?MODULE). init_per_testcase(t_size_limit, Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:update([alarm], #{ + {ok, _, _} = emqx_config:update([alarm], #{ <<"size_limit">> => 2 }), Config; init_per_testcase(t_validity_period, Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:update([alarm], #{ + {ok, _, _} = emqx_config:update([alarm], #{ <<"validity_period">> => <<"1s">> }), Config; diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index e6d1732a6..21519153a 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -449,7 +449,7 @@ rules(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update(head, [RawConfig]) of - ok -> {204}; + {ok, _, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} @@ -458,7 +458,7 @@ rules(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update(replace, RawConfig) of - ok -> {204}; + {ok, _, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} @@ -486,7 +486,7 @@ rule(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update({replace_once, RuleId}, RawConfig) of - ok -> {204}; + {ok, _, _} -> {204}; {error, not_found_rule} -> {404, #{code => <<"NOT_FOUND">>, messgae => <<"rule ", RuleId/binary, " not found">>}}; @@ -497,7 +497,7 @@ rule(put, Request) -> rule(delete, Request) -> RuleId = cowboy_req:binding(id, Request), case emqx_authz:update({replace_once, RuleId}, #{}) of - ok -> {204}; + {ok, _, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} @@ -507,7 +507,7 @@ move_rule(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), #{<<"position">> := Position} = jsx:decode(Body, [return_maps]), case emqx_authz:move(RuleId, Position) of - ok -> {204}; + {ok, _, _} -> {204}; {error, not_found_rule} -> {404, #{code => <<"NOT_FOUND">>, messgae => <<"rule ", RuleId/binary, " not found">>}}; diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 6f88fe865..e509542e6 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -33,17 +33,17 @@ groups() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz]), ok. init_per_testcase(_, Config) -> - ok = emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), Config. -define(RULE1, #{<<"principal">> => <<"all">>, @@ -82,9 +82,9 @@ init_per_testcase(_, Config) -> %%------------------------------------------------------------------------------ t_update_rule(_) -> - ok = emqx_authz:update(replace, [?RULE2]), - ok = emqx_authz:update(head, [?RULE1]), - ok = emqx_authz:update(tail, [?RULE3]), + {ok, _, _} = emqx_authz:update(replace, [?RULE2]), + {ok, _, _} = emqx_authz:update(head, [?RULE1]), + {ok, _, _} = emqx_authz:update(tail, [?RULE3]), Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), ?assertMatch(Lists1, emqx_config:get([authorization, rules], [])), @@ -107,7 +107,7 @@ t_update_rule(_) -> } ] = emqx_authz:lookup(), - ok = emqx_authz:update({replace_once, Id3}, ?RULE4), + {ok, _, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), ?assertMatch(Lists2, emqx_config:get([authorization, rules], [])), @@ -132,38 +132,38 @@ t_update_rule(_) -> } ] = emqx_authz:lookup(), - ok = emqx_authz:update(replace, []). + {ok, _, _} = emqx_authz:update(replace, []). t_move_rule(_) -> - ok = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, _, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, #{annotations := #{id := Id4}} ] = emqx_authz:lookup(), - ok = emqx_authz:move(Id4, <<"top">>), + {ok, _, _} = emqx_authz:move(Id4, <<"top">>), ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}} ], emqx_authz:lookup()), - ok = emqx_authz:move(Id1, <<"bottom">>), + {ok, _, _} = emqx_authz:move(Id1, <<"bottom">>), ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), - ok = emqx_authz:move(Id3, #{<<"before">> => Id4}), + {ok, _, _} = emqx_authz:move(Id3, #{<<"before">> => Id4}), ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), - ok = emqx_authz:move(Id2, #{<<"after">> => Id1}), + {ok, _, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}), ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, #{annotations := #{id := Id1}}, diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 6af7f3528..b5d5902d7 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -76,13 +76,13 @@ init_per_suite(Config) -> ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_management]), ok. @@ -155,7 +155,7 @@ t_api(_) -> ok. t_move_rule(_) -> - ok = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, _, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index 3d6d918eb..ec430afec 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -34,8 +34,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, <<"headers">> => #{}, @@ -45,11 +45,11 @@ init_per_suite(Config) -> <<"principal">> => <<"all">>, <<"type">> => <<"http">>} ], - ok = emqx_authz:update(replace, Rules), + {ok, _, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index 67f9a3bfe..8b74d5ec0 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -34,8 +34,8 @@ init_per_suite(Config) -> meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"mongo_type">> => <<"single">>, <<"server">> => <<"127.0.0.1:27017">>, @@ -47,11 +47,11 @@ init_per_suite(Config) -> <<"find">> => #{<<"a">> => <<"b">>}, <<"type">> => <<"mongo">>} ], - ok = emqx_authz:update(replace, Rules), + {ok, _, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index a1120684e..cfe64e2fa 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -49,11 +49,11 @@ init_per_suite(Config) -> <<"principal">> => <<"all">>, <<"sql">> => <<"abcb">>, <<"type">> => <<"mysql">> }], - emqx_authz:update(replace, Rules), + {ok, _, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 61a719474..a6f62322c 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -48,11 +48,11 @@ init_per_suite(Config) -> }, <<"sql">> => <<"abcb">>, <<"type">> => <<"pgsql">> }], - emqx_authz:update(replace, Rules), + {ok, _, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 4a1765589..17947e7d9 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - ok = emqx_config:update([zones, default, authorization, cache, enable], false), - ok = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -47,11 +47,11 @@ init_per_suite(Config) -> }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">>, <<"type">> => <<"redis">> }], - emqx_authz:update(replace, Rules), + {ok, _, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - ok = emqx_authz:update(replace, []), + {ok, _, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl index 7b1b4981d..2039ab61f 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl @@ -124,7 +124,7 @@ format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := St update_config_and_reply(Name, BridgeType, Config, Data) -> case emqx_data_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of - ok -> + {ok, _, _} -> {200, #{code => 0, data => format_api_reply( emqx_resource_api:format_data(Data))}}; {error, Reason} -> @@ -133,7 +133,7 @@ update_config_and_reply(Name, BridgeType, Config, Data) -> delete_config_and_reply(Name) -> case emqx_data_bridge:update_config({delete, Name}) of - ok -> {200, #{code => 0, data => #{}}}; + {ok, _, _} -> {200, #{code => 0, data => #{}}}; {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 72fc179eb..f8aab5ddd 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -89,6 +89,9 @@ config_reset_api() -> - For a config entry that has no default value, an error 400 will be returned">>, parameters => ?PARAM_CONF_PATH, responses => #{ + %% We only return "200" rather than the new configs that has been changed, as + %% the schema of the changed configs is depends on the request parameter + %% `conf_path`, it cannot be defined here. <<"200">> => emqx_mgmt_util:response_schema(<<"Reset configs successfully">>), <<"400">> => emqx_mgmt_util:response_error_schema( <<"It's not able to reset the config">>, ['INVALID_OPERATION']) @@ -110,14 +113,15 @@ config(get, Req) -> config(put, Req) -> Path = conf_path(Req), - ok = emqx_config:update(Path, http_body(Req)), - {200, emqx_map_lib:deep_get(Path, get_full_config())}. + {ok, _, RawConf} = emqx_config:update(Path, http_body(Req), + #{rawconf_with_defaults => true}), + {200, emqx_map_lib:deep_get(Path, emqx_map_lib:jsonable_map(RawConf))}. config_reset(post, Req) -> %% reset the config specified by the query string param 'conf_path' Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req), - case emqx_config:reset(Path) of - ok -> {200}; + case emqx_config:reset(Path, #{}) of + {ok, _, _} -> {200}; {error, Reason} -> {400, ?ERR_MSG(Reason)} end. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 3b5d686d3..555311cdc 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -113,7 +113,7 @@ prometheus(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - ok = emqx_config:update([prometheus], Params), + {ok, _, _} = emqx_config:update([prometheus], Params), enable_prometheus(Enable). % stats(_Bindings, Params) -> diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index c16bc9a61..d749e26e4 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -91,7 +91,7 @@ statsd(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - ok = emqx_config:update([statsd], Params), + {ok, _, _} = emqx_config:update([statsd], Params), enable_statsd(Enable). enable_statsd(true) -> From 988d62042112ea9a901242df6267acaaa18c0749 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 16 Aug 2021 16:33:59 +0800 Subject: [PATCH 035/306] fix(test): mock emqx_resource:remove/1 in emqx_authz_http_SUITE --- apps/emqx_authz/test/emqx_authz_http_SUITE.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index ec430afec..59fdb1eb2 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -31,6 +31,7 @@ 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), + meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), From 4dc57721038f9a26b244bf47cad106f5c08c83d0 Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Tue, 17 Aug 2021 09:15:27 +0800 Subject: [PATCH 036/306] chore(rule-metrics): remove hot upgrade code change --- .../src/emqx_rule_metrics.erl | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_metrics.erl b/apps/emqx_rule_engine/src/emqx_rule_metrics.erl index bc2b04c07..8db444a7c 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_metrics.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_metrics.erl @@ -302,47 +302,6 @@ handle_info(ticking, State = #state{rule_speeds = RuleSpeeds0, handle_info(_Info, State) -> {noreply, State}. -code_change({down, Vsn}, State = #state{metric_ids = MIDs}, _Extra) - when Vsn =:= "4.2.0"; - Vsn =:= "4.2.1" -> - emqx_metrics:ensure('actions.failure'), - emqx_metrics:set('actions.failure', - emqx_metrics:val('actions.error') - + emqx_metrics:val('actions.exception')), - [begin - Matched = get_rules_matched(Id), - Succ = get_actions_success(Id), - Error = get_actions_error(Id), - Except = get_actions_exception(Id), - ok = delete_counters(Id), - ok = create_counters(Id), - inc_rules_matched(Id, Matched), - inc_actions_success(Id, Succ), - inc_actions_error(Id, Error + Except) - end || Id <- sets:to_list(MIDs)], - {ok, State}; - -code_change(Vsn, State = #state{metric_ids = MIDs}, _Extra) - when Vsn =:= "4.2.0"; - Vsn =:= "4.2.1" -> - [emqx_metrics:ensure(Name) - || Name <- - ['actions.error', 'actions.taken', - 'actions.exception', 'actions.retry' - ]], - emqx_metrics:set('actions.error', emqx_metrics:val('actions.failure')), - [begin - Matched = get_rules_matched(Id), - Succ = get_actions_success(Id), - Error = get_actions_error(Id), - ok = delete_counters(Id), - ok = create_counters(Id), - inc_rules_matched(Id, Matched), - inc_actions_success(Id, Succ), - inc_actions_error(Id, Error) - end || Id <- sets:to_list(MIDs)], - {ok, State}; - code_change(_OldVsn, State, _Extra) -> {ok, State}. From 7fcd925efff7b1432a6a8bb87cbef892cb9b890b Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 16 Aug 2021 19:54:15 +0800 Subject: [PATCH 037/306] feat(delayed): add delayed REST HTTP API --- apps/emqx_modules/src/emqx_delayed.erl | 9 +- apps/emqx_modules/src/emqx_delayed_api.erl | 182 +++++++++++++++++++++ 2 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 apps/emqx_modules/src/emqx_delayed_api.erl diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index c55a8ddab..5e1754f4b 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -179,13 +179,8 @@ handle_info(Info, State) -> terminate(_Reason, #{timer := TRef}) -> emqx_misc:cancel_timer(TRef). -code_change({down, Vsn}, State, _Extra) when Vsn =:= "4.3.0" -> - NState = maps:with([timer, publish_at], State), - {ok, NState}; - -code_change(Vsn, State, _Extra) when Vsn =:= "4.3.0" -> - NState = ensure_stats_event(State), - {ok, NState}. +code_change(_Vsn, State, _Extra) -> + {ok, State}. %%-------------------------------------------------------------------- %% Internal functions diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl new file mode 100644 index 000000000..06de3ab13 --- /dev/null +++ b/apps/emqx_modules/src/emqx_delayed_api.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_delayed_api). + +-behavior(minirest_api). + +-import(emqx_mgmt_util, [ response_schema/1 + , response_schema/2 + , request_body_schema/1 + ]). + +% -export([cli/1]). + +-export([ status/2 + , delayed_messages/2 + , delete_delayed_message/2 + ]). + +-export([enable_delayed/2]). + +-export([api_spec/0]). + +api_spec() -> + {[status(), delayed_messages(), delete_delayed_message()], + [delayed_message_schema()]}. + + +delayed_message_schema() -> + #{broker_info => #{ + type => object, + properties => #{ + msgid => #{ + type => string, + description => <<"Message Id">> + } + } + }}. + +status() -> + Metadata = #{ + get => #{ + description => "Get delayed status", + responses => #{ + <<"200">> => response_schema(<<"Bad Request">>, + #{ + type => object, + properties => #{enable => #{type => boolean}} + } + ) + } + }, + put => #{ + description => "Enable or disbale delayed", + 'requestBody' => request_body_schema(#{ + type => object, + properties => #{ + enable => #{ + type => boolean + } + } + }), + responses => #{ + <<"200">> => + response_schema(<<"Enable or disbale delayed successfully">>), + <<"400">> => + response_schema(<<"Bad Request">>, + #{ + type => object, + properties => #{ + message => #{type => string}, + code => #{type => string} + } + } + ) + } + } + }, + {"/delayed/status", Metadata, status}. + +delayed_messages() -> + Metadata = #{ + get => #{ + description => "Get delayed message list", + responses => #{ + <<"200">> => emqx_mgmt_util:response_array_schema(<<>>, delayed_message) + } + } + }, + {"/delayed/messages", Metadata, delayed_messages}. + +delete_delayed_message() -> + Metadata = #{ + delete => #{ + description => "Delete delayed message", + parameters => [#{ + name => msgid, + in => path, + schema => #{type => string}, + required => true + }], + responses => #{ + <<"200">> => response_schema(<<"Bad Request">>, + #{ + type => object, + properties => #{enable => #{type => boolean}} + } + ) + } + } + }, + {"/delayed/messages/:msgid", Metadata, delete_delayed_message}. + + +%%-------------------------------------------------------------------- +%% HTTP API +%%-------------------------------------------------------------------- +status(get, _Request) -> + {200, get_status()}; + +status(put, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + Params = emqx_json:decode(Body, [return_maps]), + Enable = maps:get(<<"enable">>, Params), + case Enable =:= get_status() of + true -> + Reason = case Enable of + true -> <<"Telemetry status is already enabled">>; + false -> <<"Telemetry status is already disable">> + end, + {400, #{code => "BAD_REQUEST", message => Reason}}; + false -> + enable_delayed(Enable), + {200} + end. + +delayed_messages(get, _Request) -> + {200, []}. + +delete_delayed_message(delete, _Request) -> + {200}. + +%%-------------------------------------------------------------------- +%% internal function +%%-------------------------------------------------------------------- +enable_delayed(Enable) -> + lists:foreach(fun(Node) -> + enable_delayed(Node, Enable) + end, ekka_mnesia:running_nodes()). + +enable_delayed(Node, Enable) when Node =:= node() -> + case Enable of + true -> + emqx_delayed:enable(); + false -> + emqx_delayed:disable() + end; + +enable_delayed(Node, Enable) -> + rpc_call(Node, ?MODULE, enable_delayed, [Node, Enable]). + +rpc_call(Node, Module, Fun, Args) -> + case rpc:call(Node, Module, Fun, Args) of + {badrpc, Reason} -> {error, Reason}; + Result -> Result + end. + +get_status() -> + emqx_config:get([delayed, enable], true). From 50ee840220e3d4cb03fb12d47b90d704c0f4ed49 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 17 Aug 2021 17:49:37 +0800 Subject: [PATCH 038/306] feat: add rewrite api (#5502) --- apps/emqx_modules/src/emqx_rewrite.erl | 32 ++++++++--- apps/emqx_modules/src/emqx_rewrite_api.erl | 66 ++++++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 apps/emqx_modules/src/emqx_rewrite_api.erl diff --git a/apps/emqx_modules/src/emqx_rewrite.erl b/apps/emqx_modules/src/emqx_rewrite.erl index 20e51ae16..a7e98bbea 100644 --- a/apps/emqx_modules/src/emqx_rewrite.erl +++ b/apps/emqx_modules/src/emqx_rewrite.erl @@ -35,12 +35,37 @@ , disable/0 ]). +-export([ list/0 + , update/1]). + %%-------------------------------------------------------------------- %% Load/Unload %%-------------------------------------------------------------------- enable() -> Rules = emqx_config:get([rewrite, rules], []), + register_hook(Rules). + +disable() -> + emqx_hooks:del('client.subscribe', {?MODULE, rewrite_subscribe}), + emqx_hooks:del('client.unsubscribe', {?MODULE, rewrite_unsubscribe}), + emqx_hooks:del('message.publish', {?MODULE, rewrite_publish}). + +list() -> + maps:get(<<"rules">>, emqx_config:get_raw([<<"rewrite">>], #{}), []). + +update(Rules0) -> + Rewrite = emqx_config:get_raw([<<"rewrite">>], #{}), + {ok, Config, _} = emqx_config:update([rewrite], maps:put(<<"rules">>, Rules0, Rewrite)), + Rules = maps:get(rules, maps:get(rewrite, Config, #{}), []), + case Rules of + [] -> + disable(); + _ -> + register_hook(Rules) + end. + +register_hook(Rules) -> case Rules =:= [] of true -> ok; false -> @@ -50,11 +75,6 @@ enable() -> emqx_hooks:put('message.publish', {?MODULE, rewrite_publish, [PubRules]}) end. -disable() -> - emqx_hooks:del('client.subscribe', {?MODULE, rewrite_subscribe}), - emqx_hooks:del('client.unsubscribe', {?MODULE, rewrite_unsubscribe}), - emqx_hooks:del('message.publish', {?MODULE, rewrite_publish}). - rewrite_subscribe(_ClientInfo, _Properties, TopicFilters, Rules) -> {ok, [{match_and_rewrite(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}. @@ -67,7 +87,6 @@ rewrite_publish(Message = #message{topic = Topic}, Rules) -> %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- - compile(Rules) -> lists:foldl(fun(#{source_topic := Topic, re := Re, @@ -102,4 +121,3 @@ rewrite(Topic, MP, Dest) -> end, Dest, Vars)); nomatch -> Topic end. - diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl new file mode 100644 index 000000000..2be28a081 --- /dev/null +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -0,0 +1,66 @@ +-module(emqx_rewrite_api). + +-behaviour(minirest_api). + +-export([api_spec/0]). + +-export([topic_rewrite/2]). + +-define(MAX_RULES_LIMIT, 20). + +-define(EXCEED_LIMIT, 'EXCEED_LIMIT'). + +api_spec() -> + {[rewrite_api()], []}. + +topic_rewrite_schema() -> + #{ + type => object, + properties => #{ + action => #{ + type => string, + description => <<"Node">>, + enum => [subscribe, publish]}, + source_topic => #{ + type => string, + description => <<"Topic">>}, + re => #{ + type => string, + description => <<"Regular expressions">>}, + dest_topic => #{ + type => string, + description => <<"Destination topic">>} + } + }. + +rewrite_api() -> + Path = "/mqtt/topic_rewrite", + Metadata = #{ + get => #{ + description => <<"List topic rewrite">>, + responses => #{ + <<"200">> => + emqx_mgmt_util:response_array_schema(<<"List all rewrite rules">>, topic_rewrite_schema())}}, + post => #{ + description => <<"Update topic rewrite">>, + 'requestBody' => emqx_mgmt_util:request_body_array_schema(topic_rewrite_schema()), + response => #{ + <<"200">> => + emqx_mgmt_util:response_schema(<<"Update topic rewrite success">>, topic_rewrite_schema()), + <<"413">> => emqx_mgmt_util:response_error_schema(<<"Rules count exceed max limit">>, [?EXCEED_LIMIT])}}}, + {Path, Metadata, topic_rewrite}. + +topic_rewrite(get, _Request) -> + {200, emqx_rewrite:list()}; + +topic_rewrite(post, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + Params = emqx_json:decode(Body, [return_maps]), + case length(Params) < ?MAX_RULES_LIMIT of + true -> + ok = emqx_rewrite:update(Params), + {200, emqx_rewrite:list()}; + _ -> + Message = list_to_binary(io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT])), + {413, #{code => ?EXCEED_LIMIT, message => Message}} + end. From be4d2495f0c3f411a364d837031583189217659a Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 13 Aug 2021 19:13:14 +0800 Subject: [PATCH 039/306] refactor(gw): single instance support only --- apps/emqx_gateway/etc/emqx_gateway.conf | 306 +++++++++--------- apps/emqx_gateway/include/emqx_gateway.hrl | 10 +- .../src/bhvrs/emqx_gateway_impl.erl | 28 +- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 94 +++--- apps/emqx_gateway/src/emqx_gateway.erl | 60 ++-- apps/emqx_gateway/src/emqx_gateway_app.erl | 55 ++-- apps/emqx_gateway/src/emqx_gateway_gw_sup.erl | 58 ++-- .../src/emqx_gateway_insta_sup.erl | 147 ++++----- .../src/emqx_gateway_registry.erl | 76 ++--- apps/emqx_gateway/src/emqx_gateway_schema.erl | 27 +- apps/emqx_gateway/src/emqx_gateway_sup.erl | 107 +++--- .../src/exproto/emqx_exproto_impl.erl | 114 +++---- .../src/lwm2m/emqx_lwm2m_impl.erl | 122 ++++--- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 91 +++--- .../src/stomp/emqx_stomp_impl.erl | 97 +++--- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 13 +- .../test/emqx_gateway_registry_SUITE.erl | 2 +- apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 4 +- .../test/emqx_sn_protocol_SUITE.erl | 35 +- .../test/emqx_sn_registry_SUITE.erl | 4 +- apps/emqx_gateway/test/emqx_stomp_SUITE.erl | 6 +- 21 files changed, 647 insertions(+), 809 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 8c77fe652..0a5b6065e 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -4,167 +4,169 @@ gateway: { - stomp.1: { - frame: { - max_headers: 10 - max_headers_length: 1024 - max_body_length: 8192 - } + stomp: { - clientinfo_override: { - username: "${Packet.headers.login}" - password: "${Packet.headers.passcode}" - } - - authentication: { - enable: true - authenticators: [ - { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid - } - ] - } - - listener.tcp.1: { - bind: 61613 - acceptors: 16 - max_connections: 1024000 - max_conn_rate: 1000 - active_n: 100 - } + frame: { + max_headers: 10 + max_headers_length: 1024 + max_body_length: 8192 } - coap.1: { - enable_stats: false - - authentication: { - enable: true - authenticators: [ - { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid - } - ] - } - - #authentication.enable: false - - heartbeat: 30s - notify_type: qos - subscribe_qos: qos0 - publish_qos: qos1 - listener.udp.1: { - bind: 5683 - } + clientinfo_override: { + username: "${Packet.headers.login}" + password: "${Packet.headers.passcode}" } - 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 - } + authentication: { + enable: true + authenticators: [ + { + name: "authenticator1" + mechanism: password-based + server_type: built-in-database + user_id_type: clientid + } + ] } + listener.tcp.1: { + bind: 61613 + acceptors: 16 + max_connections: 1024000 + max_conn_rate: 1000 + active_n: 100 + } + } + + coap: { + + enable_stats: false + + authentication: { + enable: true + authenticators: [ + { + name: "authenticator1" + mechanism: password-based + server_type: built-in-database + user_id_type: clientid + } + ] + } + + #authentication.enable: false + + heartbeat: 30s + notify_type: qos + subscribe_qos: qos0 + publish_qos: qos1 + listener.udp.1: { + bind: 5683 + } + } + + mqttsn: { + ## 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 + } + } + ## Extension Protocol Gateway - exproto.1: { - - ## The gRPC server to accept requests - server: { - bind: 9100 - #ssl.keyfile: - #ssl.certfile: - #ssl.cacertfile: - } - - handler: { - address: "http://127.0.0.1:9001" - #ssl.keyfile: - #ssl.certfile: - #ssl.cacertfile: - } - - authentication.enable: false - - listener.tcp.1: { - bind: 7993 - acceptors: 8 - max_connections: 10240 - max_conn_rate: 1000 - } - - #listener.ssl.1: {} - #listener.udp.1: {} - #listener.dtls.1: {} + exproto: { + ## The gRPC server to accept requests + server: { + bind: 9100 + #ssl.keyfile: + #ssl.certfile: + #ssl.cacertfile: } - lwm2m_xml_dir: "{{ platform_etc_dir }}/lwm2m_xml" - - lwm2m.1: { - - lifetime_min: 1s - - lifetime_max: 86400s - - qmode_time_windonw: 22 - - auto_observe: false - - mountpoint: "lwm2m/%e/" - - ## always | contains_object_list - update_msg_publish_condition: contains_object_list - - translators: { - command: "dn/#" - response: "up/resp" - notify: "up/notify" - register: "up/resp" - update: "up/resp" - } - - listener.udp.1 { - bind: 5783 - } + handler: { + address: "http://127.0.0.1:9001" + #ssl.keyfile: + #ssl.certfile: + #ssl.cacertfile: } + + authentication.enable: false + + listener.tcp.1: { + bind: 7993 + acceptors: 8 + max_connections: 10240 + max_conn_rate: 1000 + } + + #listener.ssl.1: {} + #listener.udp.1: {} + #listener.dtls.1: {} + } + + + lwm2m: { + + xml_dir: "{{ platform_etc_dir }}/lwm2m_xml" + + lifetime_min: 1s + + lifetime_max: 86400s + + qmode_time_windonw: 22 + + auto_observe: false + + mountpoint: "lwm2m/%e/" + + ## always | contains_object_list + update_msg_publish_condition: contains_object_list + + translators: { + command: "dn/#" + response: "up/resp" + notify: "up/notify" + register: "up/resp" + update: "up/resp" + } + + listener.udp.1 { + bind: 5783 + } + } } diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl index 35fad7f23..997d6bc72 100644 --- a/apps/emqx_gateway/include/emqx_gateway.hrl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -20,15 +20,13 @@ -type instance_id() :: atom(). -type gateway_type() :: atom(). -%% @doc The Gateway Instace defination --type instance() :: - #{ id := instance_id() - , type := gateway_type() - , name := binary() +%% @doc The Gateway defination +-type gateway() :: + #{ type := gateway_type() , descr => binary() | undefined %% Appears only in creating or detailed info , rawconf => map() - %% Appears only in getting instance status/info + %% Appears only in getting gateway status/info , status => stopped | running }. diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl index 8d413e49c..6906043d9 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl @@ -22,29 +22,21 @@ -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() - ) +-callback on_gateway_load(Gateway :: gateway(), + Ctx :: emqx_gateway_ctx:context()) -> {error, reason()} - | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} + | {ok, [ChildPid :: pid()], GwState :: state()} %% TODO: v0.2 The child spec is better for restarting child process - | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()}. + | {ok, [Childspec :: supervisor:child_spec()], GwState :: state()}. %% @doc --callback on_insta_update(NewInsta :: instance(), - OldInsta :: instance(), - GwInstaState :: state(), - GwState :: state()) +-callback on_gateway_update(NewGateway :: gateway(), + OldGateway :: gateway(), + GwState :: state()) -> ok - | {ok, [GwInstaPid :: pid()], GwInstaState :: state()} - | {ok, [Childspec :: supervisor:child_spec()], GwInstaState :: state()} + | {ok, [ChildPid :: pid()], NGwState :: state()} + | {ok, [Childspec :: supervisor:child_spec()], NGwState :: state()} | {error, reason()}. %% @doc --callback on_insta_destroy(Insta :: instance(), - GwInstaState :: state(), - GwState :: state()) -> ok. +-callback on_gateway_unload(Gateway :: gateway(), GwState :: state()) -> ok. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index 09426a13d..a27db934f 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -21,95 +21,85 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx/include/logger.hrl"). --dialyzer({nowarn_function, [load/0]}). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - Options = [], - emqx_gateway_registry:load(coap, RegistryOptions, Options). + emqx_gateway_registry:reg(coap, RegistryOptions). -unload() -> - emqx_gateway_registry:unload(coap). - -init([]) -> - GwState = #{}, - {ok, GwState}. +unreg() -> + emqx_gateway_registry:unreg(coap). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> +on_gateway_load(_Gateway = #{type := GwType, + rawconf := RawConf + }, Ctx) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwType, Ctx, Lis) end, Listeners), {ok, ListenerPids, #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> + GwType = maps:get(type, NewGateway), 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) + on_gateway_unload(OldGateway, GwState), + on_gateway_load(NewGateway, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update coap instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwType, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, - _GwInstaState, - _GWState) -> +on_gateway_unload(_Gateway = #{ type := GwType, + rawconf := RawConf + }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) - end, Listeners). + stop_listener(GwType, Lis) + end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start coap ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Start ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start coap ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwType, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_coap_frame, @@ -124,21 +114,21 @@ do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). +name(GwType, Type) -> + list_to_atom(lists:concat([GwType, ":", Type])). -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop coap ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop coap ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwType, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index f6c12ad53..2835b91ff 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -20,8 +20,8 @@ %% APIs -export([ registered_gateway/0 - , create/4 - , remove/1 + , load/2 + , unload/1 , lookup/1 , update/1 , start/1 @@ -37,48 +37,40 @@ registered_gateway() -> %%-------------------------------------------------------------------- %% Gateway Instace APIs --spec list() -> [instance()]. +-spec list() -> [gateway()]. list() -> - lists:append(lists:map( - fun({_, Insta}) -> Insta end, - emqx_gateway_sup:list_gateway_insta() - )). + emqx_gateway_sup:list_gateway_insta(). --spec create(gateway_type(), binary(), binary(), map()) +-spec load(gateway_type(), 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). +load(GwType, RawConf) -> + Gateway = #{ type => GwType + , descr => undefined + , rawconf => RawConf + }, + emqx_gateway_sup:load_gateway(Gateway). --spec remove(instance_id()) -> ok | {error, any()}. -remove(InstaId) -> - emqx_gateway_sup:remove_gateway_insta(InstaId). +-spec unload(gateway_type()) -> ok | {error, any()}. +unload(GwType) -> + emqx_gateway_sup:unload_gateway(GwType). --spec lookup(instance_id()) -> instance() | undefined. -lookup(InstaId) -> - emqx_gateway_sup:lookup_gateway_insta(InstaId). +-spec lookup(gateway_type()) -> gateway() | undefined. +lookup(GwType) -> + emqx_gateway_sup:lookup_gateway(GwType). --spec update(instance()) -> ok | {error, any()}. -update(NewInsta) -> - emqx_gateway_sup:update_gateway_insta(NewInsta). +-spec update(gateway()) -> ok | {error, any()}. +update(NewGateway) -> + emqx_gateway_sup:update_gateway(NewGateway). --spec start(instance_id()) -> ok | {error, any()}. -start(InstaId) -> - emqx_gateway_sup:start_gateway_insta(InstaId). +-spec start(gateway_type()) -> ok | {error, any()}. +start(GwType) -> + emqx_gateway_sup:start_gateway_insta(GwType). --spec stop(instance_id()) -> ok | {error, any()}. -stop(InstaId) -> - emqx_gateway_sup:stop_gateway_insta(InstaId). +-spec stop(gateway_type()) -> ok | {error, any()}. +stop(GwType) -> + emqx_gateway_sup:stop_gateway_insta(GwType). %%-------------------------------------------------------------------- %% 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 index f4918e75d..b27319eac 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -27,7 +27,7 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_gateway_sup:start_link(), emqx_gateway_cli:load(), load_default_gateway_applications(), - create_gateway_by_default(), + load_gateway_by_default(), {ok, Sup}. stop(_State) -> @@ -40,56 +40,43 @@ stop(_State) -> load_default_gateway_applications() -> Apps = gateway_type_searching(), ?LOG(info, "Starting the default gateway types: ~p", [Apps]), - lists:foreach(fun load/1, Apps). + lists:foreach(fun reg/1, Apps). gateway_type_searching() -> %% FIXME: Hardcoded apps [emqx_stomp_impl, emqx_sn_impl, emqx_exproto_impl, emqx_coap_impl, emqx_lwm2m_impl]. -load(Mod) -> +reg(Mod) -> try - Mod:load(), - ?LOG(info, "Load ~s gateway application successfully!", [Mod]) + Mod:reg(), + ?LOG(info, "Register ~s gateway application successfully!", [Mod]) catch - Class : Reason -> - ?LOG(error, "Load ~s gateway application failed: {~p, ~p}", - [Mod, Class, Reason]) + Class : Reason : Stk -> + ?LOG(error, "Failed to register ~s gateway application: {~p, ~p}\n" + "Stacktrace: ~0p", + [Mod, Class, Reason, Stk]) end. -create_gateway_by_default() -> - create_gateway_by_default(zipped_confs()). +load_gateway_by_default() -> + load_gateway_by_default(confs()). -create_gateway_by_default([]) -> +load_gateway_by_default([]) -> ok; -create_gateway_by_default([{Type, Name, Confs}|More]) -> +load_gateway_by_default([{Type, Confs}|More]) -> case emqx_gateway_registry:lookup(Type) of undefined -> - ?LOG(error, "Skip to start ~s#~s: not_registred_type", - [Type, Name]); + ?LOG(error, "Skip to load ~s gateway, because it is not registered", + [Type]); _ -> - case emqx_gateway:create(Type, - atom_to_binary(Name, utf8), - <<>>, - Confs) of + case emqx_gateway:load(Type, Confs) of {ok, _} -> - ?LOG(debug, "Start ~s#~s successfully!", [Type, Name]); + ?LOG(debug, "Load ~s gateway successfully!", [Type]); {error, Reason} -> - ?LOG(error, "Start ~s#~s failed: ~0p", - [Type, Name, Reason]) + ?LOG(error, "Failed to load ~s gateway: ~0p", [Type, Reason]) end end, - create_gateway_by_default(More). + load_gateway_by_default(More). -zipped_confs() -> - All = maps:to_list( - maps:without(exclude_options(), emqx_config:get([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)). - -exclude_options() -> - [lwm2m_xml_dir]. +confs() -> + maps:to_list(emqx_config:get([gateway], [])). diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl index 21ad30c0d..37bfd10a3 100644 --- a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -15,6 +15,10 @@ %%-------------------------------------------------------------------- %% @doc The Gateway Top supervisor. +%% +%% This supervisor has monitor a bunch of process/resources depended by +%% gateway runtime +%% -module(emqx_gateway_gw_sup). -behaviour(supervisor). @@ -41,64 +45,62 @@ 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 +-spec create_insta(pid(), gateway(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}. +create_insta(Sup, Gateway = #{type := GwType}, GwDscrptr) -> + case emqx_gateway_utils:find_sup_child(Sup, GwType) of {ok, _GwInstaPid} -> {error, alredy_existed}; false -> - %% XXX: More instances options to it? - %% - Ctx = ctx(Sup, InstaId), + Ctx = ctx(Sup, GwType), %% ChildSpec = emqx_gateway_utils:childspec( - InstaId, + GwType, worker, emqx_gateway_insta_sup, - [Insta, Ctx, GwDscrptr] + [Gateway, 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 +-spec remove_insta(pid(), GwType :: gateway_type()) -> ok | {error, any()}. +remove_insta(Sup, GwType) -> + case emqx_gateway_utils:find_sup_child(Sup, GwType) of false -> ok; {ok, _GwInstaPid} -> - ok = supervisor:terminate_child(Sup, InstaId), - ok = supervisor:delete_child(Sup, InstaId) + ok = supervisor:terminate_child(Sup, GwType), + ok = supervisor:delete_child(Sup, GwType) 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 +-spec update_insta(pid(), NewGateway :: gateway()) -> ok | {error, any()}. +update_insta(Sup, NewGateway = #{type := GwType}) -> + case emqx_gateway_utils:find_sup_child(Sup, GwType) of false -> {error, not_found}; {ok, GwInstaPid} -> - emqx_gateway_insta_sup:update(GwInstaPid, NewInsta) + emqx_gateway_insta_sup:update(GwInstaPid, NewGateway) end. --spec start_insta(pid(), atom()) -> ok | {error, any()}. -start_insta(Sup, InstaId) -> - case emqx_gateway_utils:find_sup_child(Sup, InstaId) of +-spec start_insta(pid(), gateway_type()) -> ok | {error, any()}. +start_insta(Sup, GwType) -> + case emqx_gateway_utils:find_sup_child(Sup, GwType) 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 +-spec stop_insta(pid(), gateway_type()) -> ok | {error, any()}. +stop_insta(Sup, GwType) -> + case emqx_gateway_utils:find_sup_child(Sup, GwType) of false -> {error, not_found}; {ok, GwInstaPid} -> emqx_gateway_insta_sup:disable(GwInstaPid) end. --spec list_insta(pid()) -> [instance()]. +-spec list_insta(pid()) -> [gateway()]. list_insta(Sup) -> lists:filtermap( - fun({InstaId, GwInstaPid, _Type, _Mods}) -> - is_gateway_insta_id(InstaId) + fun({GwType, GwInstaPid, _Type, _Mods}) -> + is_gateway_insta_id(GwType) andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)} end, supervisor:which_children(Sup)). @@ -119,10 +121,10 @@ init([Type]) -> %% Internal funcs %%-------------------------------------------------------------------- -ctx(Sup, InstaId) -> +ctx(Sup, GwType) -> {_, Type} = erlang:process_info(Sup, registered_name), {ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm), - #{ instid => InstaId + #{ instid => GwType , type => Type , cm => CM }. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 286be76e5..4fb909483 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc The gateway instance management +%% @doc The gateway runtime -module(emqx_gateway_insta_sup). -behaviour(gen_server). @@ -40,42 +40,42 @@ ]). -record(state, { - insta :: instance(), + gw :: gateway(), ctx :: emqx_gateway_ctx:context(), status :: stopped | running, child_pids :: [pid()], - insta_state :: emqx_gateway_impl:state() | undefined + gw_state :: emqx_gateway_impl:state() | undefined }). %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -start_link(Insta, Ctx, GwDscrptr) -> +start_link(Gateway, Ctx, GwDscrptr) -> gen_server:start_link( ?MODULE, - [Insta, Ctx, GwDscrptr], + [Gateway, Ctx, GwDscrptr], [] ). --spec info(pid()) -> instance(). +-spec info(pid()) -> gateway(). info(Pid) -> gen_server:call(Pid, info). -%% @doc Stop instance +%% @doc Stop gateway -spec disable(pid()) -> ok | {error, any()}. disable(Pid) -> call(Pid, disable). -%% @doc Start instance +%% @doc Start gateway -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}). +%% @doc Update the gateway configurations +-spec update(pid(), gateway()) -> ok | {error, any()}. +update(Pid, NewGateway) -> + call(Pid, {update, NewGateway}). call(Pid, Req) -> gen_server:call(Pid, Req, 5000). @@ -84,30 +84,29 @@ call(Pid, Req) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([Insta, Ctx0, _GwDscrptr]) -> +init([Gateway, Ctx0, _GwDscrptr]) -> process_flag(trap_exit, true), - #{id := InstaId, rawconf := RawConf} = Insta, - Ctx = do_init_context(InstaId, RawConf, Ctx0), + #{type := GwType, rawconf := RawConf} = Gateway, + Ctx = do_init_context(GwType, RawConf, Ctx0), State = #state{ - insta = Insta, + gw = Gateway, ctx = Ctx, child_pids = [], status = stopped }, - case cb_insta_create(State) of - {error, _Reason} -> + case cb_gateway_load(State) of + {error, Reason} -> do_deinit_context(Ctx), - %% XXX: Return Reason?? - {stop, create_gateway_instance_failed}; + {stop, {load_gateway_failure, Reason}}; {ok, NState} -> {ok, NState} end. -do_init_context(InstaId, RawConf, Ctx) -> +do_init_context(GwType, RawConf, Ctx) -> Auth = case maps:get(authentication, RawConf, #{enable => false}) of #{enable := true, authenticators := AuthCfgs} when is_list(AuthCfgs) -> - create_authenticators_for_gateway_insta(InstaId, AuthCfgs); + create_authenticators_for_gateway_insta(GwType, AuthCfgs); _ -> undefined end, @@ -117,13 +116,13 @@ do_deinit_context(Ctx) -> cleanup_authenticators_for_gateway_insta(maps:get(auth, Ctx)), ok. -handle_call(info, _From, State = #state{insta = Insta}) -> - {reply, Insta, State}; +handle_call(info, _From, State = #state{gw = Gateway}) -> + {reply, Gateway, State}; handle_call(disable, _From, State = #state{status = Status}) -> case Status of running -> - case cb_insta_destroy(State) of + case cb_gateway_unload(State) of {ok, NState} -> {reply, ok, NState}; {error, Reason} -> @@ -136,7 +135,7 @@ handle_call(disable, _From, State = #state{status = Status}) -> handle_call(enable, _From, State = #state{status = Status}) -> case Status of stopped -> - case cb_insta_create(State) of + case cb_gateway_load(State) of {error, Reason} -> {reply, {error, Reason}, State}; {ok, NState} -> @@ -147,28 +146,30 @@ handle_call(enable, _From, State = #state{status = Status}) -> end; %% Stopped -> update -handle_call({update, NewInsta}, _From, State = #state{insta = Insta, +handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, status = stopped}) -> - case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of + case maps:get(type, NewGateway, undefined) + == maps:get(type, Gateway, undefined) of true -> - {reply, ok, State#state{insta = NewInsta}}; + {reply, ok, State#state{gw = NewGateway}}; false -> - {reply, {error, bad_instan_id}, State} + {reply, {error, gateway_type_not_match}, State} end; %% Running -> update -handle_call({update, NewInsta}, _From, State = #state{insta = Insta, +handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, status = running}) -> - case maps:get(id, NewInsta, undefined) == maps:get(id, Insta, undefined) of + case maps:get(type, NewGateway, undefined) + == maps:get(type, Gateway, undefined) of true -> - case cb_insta_update(NewInsta, State) of + case cb_gateway_update(NewGateway, State) of {ok, NState} -> {reply, ok, NState}; {error, Reason} -> {reply, {error, Reason}, State} end; false -> - {reply, {error, bad_instan_id}, State} + {reply, {error, gateway_type_not_match}, State} end; handle_call(_Request, _From, State) -> @@ -187,7 +188,7 @@ handle_info({'EXIT', Pid, Reason}, State = #state{child_pids = Pids}) -> logger:error("All child process exited!"), {noreply, State#state{status = stopped, child_pids = [], - insta_state = undefined}}; + gw_state = undefined}}; RemainPids -> {noreply, State#state{child_pids = RemainPids}} end; @@ -201,10 +202,7 @@ handle_info(Info, State) -> {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 + Pids /= [] andalso (_ = cb_gateway_unload(State)), _ = do_deinit_context(Ctx), ok. @@ -217,8 +215,8 @@ code_change(_OldVsn, State, _Extra) -> %% @doc AuthCfgs is a array of authenticatior configurations, %% see: emqx_authn_schema:authenticators/1 -create_authenticators_for_gateway_insta(InstaId0, AuthCfgs) -> - ChainId = atom_to_binary(InstaId0, utf8), +create_authenticators_for_gateway_insta(GwType, AuthCfgs) -> + ChainId = atom_to_binary(GwType, utf8), case emqx_authn:create_chain(#{id => ChainId}) of {ok, _ChainInfo} -> Results = lists:map(fun(AuthCfg = #{name := Name}) -> @@ -245,88 +243,85 @@ cleanup_authenticators_for_gateway_insta(ChainId) -> case emqx_authn:delete_chain(ChainId) of ok -> ok; {error, {not_found, _}} -> - logger:warning("Failed clean authenticator chain: ~s, " + logger:warning("Failed to clean authenticator chain: ~s, " "reason: not_found", [ChainId]); {error, Reason} -> - logger:error("Failed clean authenticator chain: ~s, " + logger:error("Failed to clean authenticator chain: ~s, " "reason: ~p", [ChainId, Reason]) end. -cb_insta_destroy(State = #state{insta = Insta = #{type := Type}, - insta_state = InstaState}) -> +cb_gateway_unload(State = #state{gw = Gateway = #{type := GwType}, + gw_state = GwState}) -> try - #{cbkmod := CbMod, - state := GwState} = emqx_gateway_registry:lookup(Type), - CbMod:on_insta_destroy(Insta, InstaState, GwState), + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), + CbMod:on_gateway_unload(Gateway, GwState, GwState), {ok, State#state{child_pids = [], - insta_state = undefined, + gw_state = undefined, status = stopped}} catch Class : Reason : Stk -> - logger:error("Destroy instance (~0p, ~0p, _) crashed: " + logger:error("Failed to unload gateway (~0p, ~0p) crashed: " "{~p, ~p}, stacktrace: ~0p", - [Insta, InstaState, + [Gateway, GwState, Class, Reason, Stk]), {error, {Class, Reason, Stk}} end. -cb_insta_create(State = #state{insta = Insta = #{type := Type}, - ctx = Ctx}) -> +cb_gateway_load(State = #state{gw = Gateway = #{type := GwType}, + ctx = Ctx}) -> try - #{cbkmod := CbMod, - state := GwState} = emqx_gateway_registry:lookup(Type), - case CbMod:on_insta_create(Insta, Ctx, GwState) of + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), + case CbMod:on_gateway_load(Gateway, Ctx) of {error, Reason} -> throw({callback_return_error, Reason}); - {ok, InstaPidOrSpecs, InstaState} -> - ChildPids = start_child_process(InstaPidOrSpecs), + {ok, ChildPidOrSpecs, GwState} -> + ChildPids = start_child_process(ChildPidOrSpecs), {ok, State#state{ status = running, child_pids = ChildPids, - insta_state = InstaState + gw_state = GwState }} end catch Class : Reason1 : Stk -> - logger:error("Create instance (~0p, ~0p, _) crashed: " + logger:error("Failed to load ~s gateway (~0p, ~0p) crashed: " "{~p, ~p}, stacktrace: ~0p", - [Insta, Ctx, + [GwType, Gateway, Ctx, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. -cb_insta_update(NewInsta, - State = #state{insta = Insta = #{type := Type}, - ctx = Ctx, - insta_state = GwInstaState}) -> +cb_gateway_update(NewGateway, + State = #state{gw = Gateway = #{type := GwType}, + ctx = Ctx, + gw_state = GwState}) -> try - #{cbkmod := CbMod, - state := GwState} = emqx_gateway_registry:lookup(Type), - case CbMod:on_insta_update(NewInsta, Insta, GwInstaState, GwState) of + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), + case CbMod:on_gateway_update(NewGateway, Gateway, GwState) of {error, Reason} -> throw({callback_return_error, Reason}); - {ok, InstaPidOrSpecs, InstaState} -> + {ok, ChildPidOrSpecs, NGwState} -> %% XXX: Hot-upgrade ??? - ChildPids = start_child_process(InstaPidOrSpecs), + ChildPids = start_child_process(ChildPidOrSpecs), {ok, State#state{ status = running, child_pids = ChildPids, - insta_state = InstaState + gw_state = NGwState }} end catch Class : Reason1 : Stk -> - logger:error("Update instance (~0p, ~0p, ~0p, _) crashed: " + logger:error("Failed to update gateway (~0p, ~0p, ~0p) crashed: " "{~p, ~p}, stacktrace: ~0p", - [NewInsta, Insta, Ctx, + [NewGateway, Gateway, Ctx, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. -start_child_process([Indictor|_] = InstaPidOrSpecs) -> +start_child_process([Indictor|_] = ChildPidOrSpecs) -> case erlang:is_pid(Indictor) of true -> - InstaPidOrSpecs; + ChildPidOrSpecs; _ -> - do_start_child_process(InstaPidOrSpecs) + do_start_child_process(ChildPidOrSpecs) end. do_start_child_process(ChildSpecs) when is_list(ChildSpecs) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl index 07cc37bd8..cc46a0173 100644 --- a/apps/emqx_gateway/src/emqx_gateway_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -23,11 +23,9 @@ -behavior(gen_server). %% APIs for Impl. --export([ load/3 - , unload/1 - ]). - --export([ list/0 +-export([ reg/2 + , unreg/1 + , list/0 , lookup/1 ]). @@ -44,9 +42,17 @@ ]). -record(state, { - loaded = #{} :: #{ gateway_type() => descriptor() } + reged = #{} :: #{ gateway_type() => descriptor() } }). +-type registry_options() :: [registry_option()]. + +-type registry_option() :: {cbkmod, atom()}. + +-type descriptor() :: #{ cbkmod := atom() + , rgopts := registry_options() + }. + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -58,37 +64,24 @@ start_link() -> %% 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()) +-spec reg(gateway_type(), registry_options()) -> ok | {error, any()}. -load(Type, RgOpts, GwOpts) -> +reg(Type, RgOpts) -> CbMod = proplists:get_value(cbkmod, RgOpts, Type), Dscrptr = #{ cbkmod => CbMod , rgopts => RgOpts - , gwopts => GwOpts }, - call({load, Type, Dscrptr}). + call({reg, Type, Dscrptr}). --spec unload(gateway_type()) -> ok | {error, any()}. -unload(Type) -> +-spec unreg(gateway_type()) -> ok | {error, any()}. +unreg(Type) -> %% TODO: Checking ALL INSTACE HAS STOPPED - call({unload, Type}). + call({unreg, Type}). %% TODO: -%unload(Type, Force) -> -% call({unload, Type, Froce}). +%unreg(Type, Force) -> +% call({unreg, Type, Froce}). %% @doc Return all registered protocol gateway implementation -spec list() -> [{gateway_type(), descriptor()}]. @@ -109,41 +102,30 @@ call(Req) -> init([]) -> %% TODO: Metrics ??? process_flag(trap_exit, true), - {ok, #state{loaded = #{}}}. + {ok, #state{reged = #{}}}. -handle_call({load, Type, Dscrptr}, _From, State = #state{loaded = Gateways}) -> +handle_call({reg, Type, Dscrptr}, _From, State = #state{reged = 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; + NGateways = maps:put(Type, Dscrptr, Gateways), + {reply, ok, State#state{reged = NGateways}}; _ -> {reply, {error, already_existed}, State} end; -handle_call({unload, Type}, _From, State = #state{loaded = Gateways}) -> +handle_call({unreg, Type}, _From, State = #state{reged = 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)}} + emqx_gateway_sup:unload_gateway(Type), + {reply, ok, State#state{reged = maps:remove(Type, Gateways)}} end; -handle_call(all, _From, State = #state{loaded = Gateways}) -> +handle_call(all, _From, State = #state{reged = Gateways}) -> {reply, maps:to_list(Gateways), State}; -handle_call({lookup, Type}, _From, State = #state{loaded = Gateways}) -> +handle_call({lookup, Type}, _From, State = #state{reged = Gateways}) -> Reply = maps:get(Type, Gateways, undefined), {reply, Reply, State}; diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 5c98e1f34..85d57a51b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -32,17 +32,13 @@ structs() -> ["gateway"]. fields("gateway") -> - [{stomp, t(ref(stomp))}, - {mqttsn, t(ref(mqttsn))}, - {coap, t(ref(coap))}, - {lwm2m, t(ref(lwm2m))}, - {lwm2m_xml_dir, t(string())}, - {exproto, t(ref(exproto))} + [{stomp, t(ref(stomp_structs))}, + {mqttsn, t(ref(mqttsn_structs))}, + {coap, t(ref(coap_structs))}, + {lwm2m, t(ref(lwm2m_structs))}, + {exproto, t(ref(exproto_structs))} ]; -fields(stomp) -> - [{"$id", t(ref(stomp_structs))}]; - fields(stomp_structs) -> [ {frame, t(ref(stomp_frame))} , {clientinfo_override, t(ref(clientinfo_override))} @@ -56,9 +52,6 @@ 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())} @@ -76,12 +69,9 @@ fields(mqttsn_predefined) -> , {topic, t(string())} ]; -fields(lwm2m) -> - [{"$id", t(ref(lwm2m_structs))} - ]; - fields(lwm2m_structs) -> - [ {lifetime_min, t(duration())} + [ {xml_dir, t(string())} + , {lifetime_min, t(duration())} , {lifetime_max, t(duration())} , {qmode_time_windonw, t(integer())} , {auto_observe, t(boolean())} @@ -91,9 +81,6 @@ fields(lwm2m_structs) -> , {listener, t(ref(udp_listener_group))} ]; -fields(exproto) -> - [{"$id", t(ref(exproto_structs))}]; - fields(exproto_structs) -> [ {server, t(ref(exproto_grpc_server))} , {handler, t(ref(exproto_grpc_handler))} diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index c974060d0..4f340913e 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -22,22 +22,16 @@ -export([start_link/0]). -%% Gateway Instance APIs --export([ create_gateway_insta/1 - , remove_gateway_insta/1 - , lookup_gateway_insta/1 - , update_gateway_insta/1 +%% Gateway APIs +-export([ load_gateway/1 + , unload_gateway/1 + , lookup_gateway/1 + , update_gateway/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]). @@ -48,88 +42,71 @@ 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}}; + +-spec load_gateway(gateway()) -> {ok, pid()} | {error, any()}. +load_gateway(Gateway = #{type := GwType}) -> + case emqx_gateway_registry:lookup(GwType) of + undefined -> {error, {unknown_gateway_type, GwType}}; GwDscrptr -> - {ok, GwSup} = ensure_gateway_suptree_ready(gatewayid(Type)), - emqx_gateway_gw_sup:create_insta(GwSup, Insta, GwDscrptr) + {ok, GwSup} = ensure_gateway_suptree_ready(GwType), + emqx_gateway_gw_sup:create_insta(GwSup, Gateway, 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); +-spec unload_gateway(gateway_type()) -> ok | {error, not_found}. +unload_gateway(GwType) -> + case lists:keyfind(GwType, 1, supervisor:which_children(?MODULE)) of + false -> {error, not_found}; _ -> + _ = supervisor:terminate_child(?MODULE, GwType), + _ = supervisor:delete_child(?MODULE, GwType), ok end. --spec lookup_gateway_insta(instance_id()) -> instance() | undefined. -lookup_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of +-spec lookup_gateway(gateway_type()) -> gateway() | undefined. +lookup_gateway(GwType) -> + case search_gateway_insta_proc(GwType) of {ok, {_, GwInstaPid}} -> emqx_gateway_insta_sup:info(GwInstaPid); _ -> undefined end. --spec update_gateway_insta(instance()) +-spec update_gateway(gateway_type()) -> ok | {error, any()}. -update_gateway_insta(NewInsta = #{type := Type}) -> - case emqx_gateway_utils:find_sup_child(?MODULE, gatewayid(Type)) of +update_gateway(NewGateway = #{type := GwType}) -> + case emqx_gateway_utils:find_sup_child(?MODULE, GwType) of {ok, GwSup} -> - emqx_gateway_gw_sup:update_insta(GwSup, NewInsta); + emqx_gateway_gw_sup:update_insta(GwSup, NewGateway); _ -> {error, not_found} end. -start_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of +start_gateway_insta(GwType) -> + case search_gateway_insta_proc(GwType) of {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:start_insta(GwSup, InstaId); + emqx_gateway_gw_sup:start_insta(GwSup, GwType); _ -> {error, not_found} end. --spec stop_gateway_insta(instance_id()) -> ok | {error, any()}. -stop_gateway_insta(InstaId) -> - case search_gateway_insta_proc(InstaId) of +-spec stop_gateway_insta(gateway_type()) -> ok | {error, any()}. +stop_gateway_insta(GwType) -> + case search_gateway_insta_proc(GwType) of {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:stop_insta(GwSup, InstaId); + emqx_gateway_gw_sup:stop_insta(GwSup, GwType); _ -> {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()}]. +-spec list_gateway_insta() -> [gateway()]. list_gateway_insta() -> - lists:map( + lists:append(lists:map( fun(SupId) -> - Instas = emqx_gateway_gw_sup:list_insta(SupId), - {SupId, Instas} - end, list_started_gateway()). + emqx_gateway_gw_sup:list_insta(SupId) + 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([]) -> @@ -145,17 +122,14 @@ init([]) -> %% 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 +ensure_gateway_suptree_ready(GwType) -> + case lists:keyfind(GwType, 1, supervisor:which_children(?MODULE)) of false -> ChildSpec = emqx_gateway_utils:childspec( - Type, + GwType, supervisor, emqx_gateway_gw_sup, - [Type] + [GwType] ), emqx_gateway_utils:supervisor_ret( supervisor:start_child(?MODULE, ChildSpec) @@ -190,4 +164,3 @@ started_gateway_pid() -> is_a_gateway_id(Id) -> Id /= emqx_gateway_registry. - diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 6f47e25ff..363af9f16 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -20,16 +20,13 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([]). - --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx/include/logger.hrl"). @@ -38,24 +35,19 @@ %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(exproto, RegistryOptions, []). - -unload() -> - emqx_gateway_registry:unload(exproto). - -init(_) -> - GwState = #{}, - {ok, GwState}. + emqx_gateway_registry:reg(exproto, RegistryOptions). +unreg() -> + emqx_gateway_registry:unreg(exproto). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -start_grpc_server(InstaId, Options = #{bind := ListenOn}) -> +start_grpc_server(GwType, Options = #{bind := ListenOn}) -> Services = #{protos => [emqx_exproto_pb], services => #{ 'emqx.exproto.v1.ConnectionAdapter' => emqx_exproto_gsvr} @@ -65,10 +57,10 @@ start_grpc_server(InstaId, Options = #{bind := ListenOn}) -> SslOpts -> [{ssl_options, SslOpts}] end, - _ = grpc:start_server(InstaId, ListenOn, Services, SvrOptions), - ?ULOG("Start ~s gRPC server on ~p successfully.~n", [InstaId, ListenOn]). + _ = grpc:start_server(GwType, ListenOn, Services, SvrOptions), + ?ULOG("Start ~s gRPC server on ~p successfully.~n", [GwType, ListenOn]). -start_grpc_client_channel(InstaId, Options = #{address := UriStr}) -> +start_grpc_client_channel(GwType, Options = #{address := UriStr}) -> UriMap = uri_string:parse(UriStr), Scheme = maps:get(scheme, UriMap), Host = maps:get(host, UriMap), @@ -85,79 +77,79 @@ start_grpc_client_channel(InstaId, Options = #{address := UriStr}) -> transport_opts => SslOpts}}; _ -> #{} end, - grpc_client_sup:create_channel_pool(InstaId, SvrAddr, ClientOpts). + grpc_client_sup:create_channel_pool(GwType, SvrAddr, ClientOpts). -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> +on_gateway_load(_Gateway = #{ type := GwType, + rawconf := RawConf + }, Ctx) -> %% XXX: How to monitor it ? %% Start grpc client pool & client channel - PoolName = pool_name(InstaId), + PoolName = pool_name(GwType), PoolSize = emqx_vm:schedulers() * 2, {ok, _} = emqx_pool_sup:start_link(PoolName, hash, PoolSize, {emqx_exproto_gcli, start_link, []}), - _ = start_grpc_client_channel(InstaId, maps:get(handler, RawConf)), + _ = start_grpc_client_channel(GwType, maps:get(handler, RawConf)), %% XXX: How to monitor it ? - _ = start_grpc_server(InstaId, maps:get(server, RawConf)), + _ = start_grpc_server(GwType, maps:get(server, RawConf)), NRawConf = maps:without( [server, handler], RawConf#{pool_name => PoolName} ), Listeners = emqx_gateway_utils:normalize_rawconf( - NRawConf#{handler => InstaId} + NRawConf#{handler => GwType} ), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwType, Ctx, Lis) end, Listeners), - {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> + GwType = maps:get(type, NewGateway), 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) + on_gateway_unload(OldGateway, GwState), + on_gateway_load(NewGateway, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update exproto instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwType, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> +on_gateway_unload(_Gateway = #{ type := GwType, + rawconf := RawConf + }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwType, Lis) end, Listeners). -pool_name(InstaId) -> - list_to_atom(lists:concat([InstaId, "_gcli_pool"])). +pool_name(GwType) -> + list_to_atom(lists:concat([GwType, "_gcli_pool"])). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start exproto ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Start ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start exproto ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwType, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_exproto_frame, @@ -176,8 +168,8 @@ do_start_listener(udp, Name, ListenOn, Opts, MFA) -> do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). +name(GwType, Type) -> + list_to_atom(lists:concat([GwType, ":", Type])). merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> @@ -200,18 +192,18 @@ merge_default_by_type(Type, Options) when Type =:= udp; [{udp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop exproto ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop exproto ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwType, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index d94c9fa8b..cf1a1a017 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -20,36 +20,37 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([]). - --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). +-include_lib("emqx/include/logger.hrl"). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(lwm2m, RegistryOptions, []). + emqx_gateway_registry:reg(lwm2m, RegistryOptions). -unload() -> - %% XXX: - lwm2m_coap_server_registry:remove_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), - emqx_gateway_registry:unload(lwm2m). +unreg() -> + emqx_gateway_registry:unreg(lwm2m). + +%%-------------------------------------------------------------------- +%% emqx_gateway_registry callbacks +%%-------------------------------------------------------------------- + +on_gateway_load(_Gateway = #{ type := GwType, + rawconf := RawConf + }, Ctx) -> -init(_) -> %% Handler _ = lwm2m_coap_server:start_registry(), lwm2m_coap_server_registry:add_handler( @@ -57,75 +58,66 @@ init(_) -> emqx_lwm2m_coap_resource, undefined ), %% Xml registry - {ok, _} = emqx_lwm2m_xml_object_db:start_link( - emqx_config:get([gateway, lwm2m_xml_dir]) - ), + {ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, RawConf)), %% XXX: Self managed table? %% TODO: Improve it later {ok, _} = emqx_lwm2m_cm:start_link(), - GwState = #{}, - {ok, GwState}. - -%% TODO: deinit - -%%-------------------------------------------------------------------- -%% emqx_gateway_registry callbacks -%%-------------------------------------------------------------------- - -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwType, Ctx, Lis) end, Listeners), - {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. + {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> + GwType = maps:get(type, NewGateway), 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) + on_gateway_unload(OldGateway, GwState), + on_gateway_load(NewGateway, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update stomp instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwType, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> +on_gateway_unload(_Gateway = #{ type := GwType, + rawconf := RawConf + }, _GwState) -> + %% XXX: + lwm2m_coap_server_registry:remove_handler( + [<<"rd">>], + emqx_lwm2m_coap_resource, undefined + ), + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwType, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - io:format("Start lwm2m ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Start ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]), Pid; {error, Reason} -> - io:format(standard_error, - "Failed to start lwm2m ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, udp), +start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwType, udp), NCfg = Cfg#{ctx => Ctx}, NSocketOpts = merge_default(SocketOpts), Options = [{config, NCfg}|NSocketOpts], @@ -136,8 +128,8 @@ start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> lwm2m_coap_server:start_dtls(Name, ListenOn, Options) end. -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). +name(GwType, Type) -> + list_to_atom(lists:concat([GwType, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -149,22 +141,20 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> io:format("Stop lwm2m ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]); {error, Reason} -> - io:format(standard_error, - "Failed to stop lwm2m ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason] - ) + ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwType, Type), case Type of udp -> lwm2m_coap_server:stop_udp(Name, ListenOn); diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 57070206c..03eebfd22 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -20,16 +20,13 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([]). - --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx/include/logger.hrl"). @@ -38,25 +35,21 @@ %% APIs %%-------------------------------------------------------------------- -load() -> +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(mqttsn, RegistryOptions, []). + emqx_gateway_registry:reg(mqttsn, RegistryOptions). -unload() -> - emqx_gateway_registry:unload(mqttsn). - -init(_) -> - GwState = #{}, - {ok, GwState}. +unreg() -> + emqx_gateway_registry:unreg(mqttsn). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> +on_gateway_load(_Gateway = #{ type := GwType, + rawconf := RawConf + }, Ctx) -> %% We Also need to start `emqx_sn_broadcast` & %% `emqx_sn_registry` process @@ -71,7 +64,7 @@ on_insta_create(_Insta = #{ id := InstaId, end, PredefTopics = maps:get(predefined, RawConf), - {ok, RegistrySvr} = emqx_sn_registry:start_link(InstaId, PredefTopics), + {ok, RegistrySvr} = emqx_sn_registry:start_link(GwType, PredefTopics), NRawConf = maps:without( [broadcast, predefined], @@ -80,52 +73,52 @@ on_insta_create(_Insta = #{ id := InstaId, Listeners = emqx_gateway_utils:normalize_rawconf(NRawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(InstaId, Ctx, Lis) + start_listener(GwType, Ctx, Lis) end, Listeners), {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(NewGateway = #{type := GwType}, OldGateway, + GwState = #{ctx := Ctx}) -> 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) + on_gateway_unload(OldGateway, GwState), + on_gateway_load(NewGateway, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update stomp instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwType, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> +on_gateway_unload(_Insta = #{ type := GwType, + rawconf := RawConf + }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwType, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start mqttsn ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Start ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start mqttsn ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwType, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_sn_frame, @@ -134,8 +127,8 @@ start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). +name(GwType, Type) -> + list_to_atom(lists:concat([GwType, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -147,18 +140,18 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop mqttsn ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop mqttsn ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwType, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 95183ad5e..84b10e97a 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -19,14 +19,13 @@ -behavior(emqx_gateway_impl). %% APIs --export([ load/0 - , unload/0 +-export([ reg/0 + , unreg/0 ]). --export([ init/1 - , on_insta_create/3 - , on_insta_update/4 - , on_insta_destroy/3 +-export([ on_gateway_load/2 + , on_gateway_update/3 + , on_gateway_unload/2 ]). -include_lib("emqx_gateway/include/emqx_gateway.hrl"). @@ -36,79 +35,75 @@ %% APIs %%-------------------------------------------------------------------- --spec load() -> ok | {error, any()}. -load() -> +-spec reg() -> ok | {error, any()}. +reg() -> RegistryOptions = [ {cbkmod, ?MODULE} ], - emqx_gateway_registry:load(stomp, RegistryOptions, []). + emqx_gateway_registry:reg(stomp, RegistryOptions). --spec unload() -> ok | {error, any()}. -unload() -> - emqx_gateway_registry:unload(stomp). - -init(_) -> - GwState = #{}, - {ok, GwState}. +-spec unreg() -> ok | {error, any()}. +unreg() -> + emqx_gateway_registry:unreg(stomp). %%-------------------------------------------------------------------- %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_insta_create(_Insta = #{ id := InstaId, - rawconf := RawConf - }, Ctx, _GwState) -> +on_gateway_load(_Gateway = #{ type := GwType, + rawconf := RawConf + }, Ctx) -> %% 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) + start_listener(GwType, 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}}. + %% FIXME: Assign ctx to GwState + {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_insta_update(NewInsta, OldInsta, GwInstaState = #{ctx := Ctx}, GwState) -> - InstaId = maps:get(id, NewInsta), +on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> + GwType = maps:get(type, NewGateway), 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) + %% XXX: 2. Check the New confs first before destroy old state??? + on_gateway_unload(OldGateway, GwState), + on_gateway_load(NewGateway, Ctx) catch Class : Reason : Stk -> - logger:error("Failed to update stomp instance ~s; " + logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [InstaId, Class, Reason, Stk]), + [GwType, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_insta_destroy(_Insta = #{ id := InstaId, - rawconf := RawConf - }, _GwInstaState, _GwState) -> +on_gateway_unload(_Gateway = #{ type := GwType, + rawconf := RawConf + }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(InstaId, Lis) + stop_listener(GwType, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(InstaId, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start stomp ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]), + ?ULOG("Start ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start stomp ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(InstaId, Type), +start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwType, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_stomp_frame, @@ -117,8 +112,8 @@ start_listener(InstaId, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(InstaId, Type) -> - list_to_atom(lists:concat([InstaId, ":", Type])). +name(GwType, Type) -> + list_to_atom(lists:concat([GwType, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), @@ -130,18 +125,18 @@ merge_default(Options) -> [{tcp_options, Default} | Options] end. -stop_listener(InstaId, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(InstaId, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop stomp ~s:~s listener on ~s successfully.~n", - [InstaId, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", + [GwType, Type, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop stomp ~s:~s listener on ~s: ~0p~n", - [InstaId, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", + [GwType, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(InstaId, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(InstaId, Type), +stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwType, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 6e4faa40a..43c1d9a86 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -67,18 +67,17 @@ set_special_cfg(emqx_gateway) -> LisType = get(grpname), emqx_config:put( [gateway, exproto], - #{'1' => - #{authenticator => allow_anonymous, - server => #{bind => 9100}, - handler => #{address => "http://127.0.0.1:9001"}, - listener => listener_confs(LisType) - }}); + #{authenticator => allow_anonymous, + server => #{bind => 9100}, + handler => #{address => "http://127.0.0.1:9001"}, + listener => listener_confs(LisType) + }); set_special_cfg(_App) -> ok. listener_confs(Type) -> Default = #{bind => 7993, acceptors => 8}, - #{Type => #{'1' => maps:merge(Default, maps:from_list(socketopts(Type)))}}. + #{Type => maps:merge(Default, maps:from_list(socketopts(Type)))}. %%-------------------------------------------------------------------- %% Tests cases diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl index 33d577a46..7887ece0a 100644 --- a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -23,7 +23,7 @@ -define(CONF_DEFAULT, <<""" gateway: { - stomp.1 {} + stomp {} } """>>). diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index 0e19d9b4f..ff6e416e4 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -30,8 +30,8 @@ -define(CONF_DEFAULT, <<" gateway: { - lwm2m_xml_dir: \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" - lwm2m.1: { + lwm2m: { + xml_dir: \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" lifetime_min: 1s lifetime_max: 86400s qmode_time_windonw: 22 diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 0c60d964f..2b8475cf7 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -51,9 +51,9 @@ -define(CLIENTID, iolist_to_binary([atom_to_list(?FUNCTION_NAME), "-", integer_to_list(erlang:system_time())])). --define(CONF_DEFAULT, <<""" +-define(CONF_DEFAULT, <<" gateway: { - mqttsn.1: { + mqttsn: { gateway_id: 1 broadcast: true enable_stats: true @@ -73,7 +73,7 @@ gateway: { } } } -""">>). +">>). %%-------------------------------------------------------------------- %% Setups @@ -90,35 +90,6 @@ init_per_suite(Config) -> end_per_suite(_) -> emqx_ct_helpers:stop_apps([emqx_gateway]). -set_special_confs(emqx_gateway) -> - emqx_config:put( - [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. - %%-------------------------------------------------------------------- %% Test cases %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl index 6161687f2..9aebfe791 100644 --- a/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_registry_SUITE.erl @@ -26,8 +26,6 @@ -define(PREDEF_TOPICS, [#{id => 1, topic => <<"/predefined/topic/name/hello">>}, #{id => 2, topic => <<"/predefined/topic/name/nice">>}]). --define(INSTA_ID, 'mqttsn#1'). - %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -45,7 +43,7 @@ end_per_suite(_Config) -> ok. init_per_testcase(_TestCase, Config) -> - {ok, Pid} = ?REGISTRY:start_link(?INSTA_ID, ?PREDEF_TOPICS), + {ok, Pid} = ?REGISTRY:start_link('mqttsn', ?PREDEF_TOPICS), {Tab, Pid} = ?REGISTRY:lookup_name(Pid), [{reg, {Tab, Pid}} | Config]. diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index b2e9bd84e..3d05b73c3 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -23,9 +23,9 @@ -define(HEARTBEAT, <<$\n>>). --define(CONF_DEFAULT, <<""" +-define(CONF_DEFAULT, <<" gateway: { - stomp.1: { + stomp: { clientinfo_override: { username: \"${Packet.headers.login}\" password: \"${Packet.headers.passcode}\" @@ -35,7 +35,7 @@ gateway: { } } } -""">>). +">>). all() -> emqx_ct:all(?MODULE). From cac0ea4b1977e053c30e71484f430b25d26f7b64 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 16 Aug 2021 17:40:04 +0800 Subject: [PATCH 040/306] chore(gw): fix dialyzer warnigns --- apps/emqx_gateway/src/emqx_gateway_registry.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_sup.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl index cc46a0173..6eeb958ed 100644 --- a/apps/emqx_gateway/src/emqx_gateway_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -118,7 +118,7 @@ handle_call({unreg, Type}, _From, State = #state{reged = Gateways}) -> undefined -> {reply, ok, State}; _ -> - emqx_gateway_sup:unload_gateway(Type), + _ = emqx_gateway_sup:unload_gateway(Type), {reply, ok, State#state{reged = maps:remove(Type, Gateways)}} end; diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index 4f340913e..b925d420e 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -71,7 +71,7 @@ lookup_gateway(GwType) -> undefined end. --spec update_gateway(gateway_type()) +-spec update_gateway(gateway()) -> ok | {error, any()}. update_gateway(NewGateway = #{type := GwType}) -> From 630e0fb8cb578c03a5d4fa6e44b7f78e971f26ad Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 16 Aug 2021 18:25:34 +0800 Subject: [PATCH 041/306] test(gw): fix bad test cases --- .../src/emqx_gateway_insta_sup.erl | 3 ++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 4 +++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 3 ++- .../src/exproto/emqx_exproto_impl.erl | 8 ++++++-- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 6 +++--- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 8 ++++---- .../test/emqx_gateway_registry_SUITE.erl | 18 ++++++------------ apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 4 ++-- .../test/emqx_sn_protocol_SUITE.erl | 6 +++--- apps/emqx_gateway/test/emqx_stomp_SUITE.erl | 4 ++-- 10 files changed, 33 insertions(+), 31 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 4fb909483..0cdf15f9b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -254,7 +254,7 @@ cb_gateway_unload(State = #state{gw = Gateway = #{type := GwType}, gw_state = GwState}) -> try #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), - CbMod:on_gateway_unload(Gateway, GwState, GwState), + CbMod:on_gateway_unload(Gateway, GwState), {ok, State#state{child_pids = [], gw_state = undefined, status = stopped}} @@ -316,6 +316,7 @@ cb_gateway_update(NewGateway, {error, {Class, Reason1, Stk}} end. +start_child_process([]) -> []; start_child_process([Indictor|_] = ChildPidOrSpecs) -> case erlang:is_pid(Indictor) of true -> diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 85d57a51b..938da15ba 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -60,6 +60,7 @@ fields(mqttsn_structs) -> , {idle_timeout, t(duration())} , {predefined, hoconsc:array(ref(mqttsn_predefined))} , {clientinfo_override, t(ref(clientinfo_override))} + , {authentication, t(ref(authentication))} , {listener, t(ref(udp_listener_group))} ]; @@ -78,6 +79,7 @@ fields(lwm2m_structs) -> , {mountpoint, t(string())} , {update_msg_publish_condition, t(union([always, contains_object_list]))} , {translators, t(ref(translators))} + , {authentication, t(ref(authentication))} , {listener, t(ref(udp_listener_group))} ]; @@ -201,11 +203,11 @@ fields(coap) -> fields(coap_structs) -> [ {enable_stats, t(boolean(), undefined, true)} - , {authentication, t(ref(authentication))} , {heartbeat, t(duration(), undefined, "30s")} , {notify_type, t(union([non, con, qos]), undefined, qos)} , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {authentication, t(ref(authentication))} , {listener, t(ref(udp_listener_group))} ]; diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 97d62da52..8a4d24691 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -120,7 +120,8 @@ format_listenon({Addr, Port}) when is_tuple(Addr) -> , SocketOpts :: esockd:option() , Cfg :: map() }). -normalize_rawconf(RawConf = #{listener := LisMap}) -> +normalize_rawconf(RawConf) -> + LisMap = maps:get(listener, RawConf, #{}), Cfg0 = maps:without([listener], RawConf), lists:append(maps:fold(fun(Type, Liss, AccIn1) -> Listeners = diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 363af9f16..4fb5cd05a 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -47,6 +47,8 @@ unreg() -> %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- +start_grpc_server(_GwType, undefined) -> + undefined; start_grpc_server(GwType, Options = #{bind := ListenOn}) -> Services = #{protos => [emqx_exproto_pb], services => #{ @@ -60,6 +62,8 @@ start_grpc_server(GwType, Options = #{bind := ListenOn}) -> _ = grpc:start_server(GwType, ListenOn, Services, SvrOptions), ?ULOG("Start ~s gRPC server on ~p successfully.~n", [GwType, ListenOn]). +start_grpc_client_channel(_GwType, undefined) -> + undefined; start_grpc_client_channel(GwType, Options = #{address := UriStr}) -> UriMap = uri_string:parse(UriStr), Scheme = maps:get(scheme, UriMap), @@ -88,10 +92,10 @@ on_gateway_load(_Gateway = #{ type := GwType, PoolSize = emqx_vm:schedulers() * 2, {ok, _} = emqx_pool_sup:start_link(PoolName, hash, PoolSize, {emqx_exproto_gcli, start_link, []}), - _ = start_grpc_client_channel(GwType, maps:get(handler, RawConf)), + _ = start_grpc_client_channel(GwType, maps:get(handler, RawConf, undefined)), %% XXX: How to monitor it ? - _ = start_grpc_server(GwType, maps:get(server, RawConf)), + _ = start_grpc_server(GwType, maps:get(server, RawConf, undefined)), NRawConf = maps:without( [server, handler], diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 03eebfd22..3dfb8546d 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -53,17 +53,17 @@ on_gateway_load(_Gateway = #{ type := GwType, %% We Also need to start `emqx_sn_broadcast` & %% `emqx_sn_registry` process - SnGwId = maps:get(gateway_id, RawConf), - case maps:get(broadcast, RawConf) of + case maps:get(broadcast, RawConf, false) of false -> ok; true -> %% FIXME: Port = 1884, + SnGwId = maps:get(gateway_id, RawConf, undefined), _ = emqx_sn_broadcast:start_link(SnGwId, Port), ok end, - PredefTopics = maps:get(predefined, RawConf), + PredefTopics = maps:get(predefined, RawConf, []), {ok, RegistrySvr} = emqx_sn_registry:start_link(GwType, PredefTopics), NRawConf = maps:without( diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 43c1d9a86..0b241a5c8 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -55,19 +55,19 @@ metrics() -> init_per_group(GrpName, Cfg) -> put(grpname, GrpName), Svrs = emqx_exproto_echo_svr:start(), - emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_cfg/1), + emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway], fun set_special_cfg/1), emqx_logger:set_log_level(debug), [{servers, Svrs}, {listener_type, GrpName} | Cfg]. end_per_group(_, Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway]), + emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]), emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)). set_special_cfg(emqx_gateway) -> LisType = get(grpname), emqx_config:put( [gateway, exproto], - #{authenticator => allow_anonymous, + #{authentication => #{enable => false}, server => #{bind => 9100}, handler => #{address => "http://127.0.0.1:9001"}, listener => listener_confs(LisType) @@ -77,7 +77,7 @@ set_special_cfg(_App) -> listener_confs(Type) -> Default = #{bind => 7993, acceptors => 8}, - #{Type => maps:merge(Default, maps:from_list(socketopts(Type)))}. + #{Type => #{'1' => maps:merge(Default, maps:from_list(socketopts(Type)))}}. %%-------------------------------------------------------------------- %% Tests cases diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl index 7887ece0a..da03b17c5 100644 --- a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -35,11 +35,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Cfg) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), Cfg. end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway]), + emqx_ct_helpers:stop_apps([emqx_authn, emqx_gateway]), ok. %%-------------------------------------------------------------------- @@ -49,21 +49,15 @@ end_per_suite(_Cfg) -> t_load_unload(_) -> OldCnt = length(emqx_gateway_registry:list()), RgOpts = [{cbkmod, ?MODULE}], - GwOpts = [paramsin], - ok = emqx_gateway_registry:load(test, RgOpts, GwOpts), + ok = emqx_gateway_registry:reg(test, RgOpts), ?assertEqual(OldCnt+1, length(emqx_gateway_registry:list())), #{cbkmod := ?MODULE, - rgopts := RgOpts, - gwopts := GwOpts, - state := #{gwstate := 1}} = emqx_gateway_registry:lookup(test), + rgopts := RgOpts} = emqx_gateway_registry:lookup(test), - {error, already_existed} = emqx_gateway_registry:load(test, [{cbkmod, ?MODULE}], GwOpts), + {error, already_existed} = emqx_gateway_registry:reg(test, [{cbkmod, ?MODULE}]), - ok = emqx_gateway_registry:unload(test), + ok = emqx_gateway_registry:unreg(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_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index ff6e416e4..2e3e5a040 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -134,12 +134,12 @@ groups() -> ]. init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx]), + emqx_ct_helpers:start_apps([emqx_authn]), Config. end_per_suite(Config) -> timer:sleep(300), - emqx_ct_helpers:stop_apps([emqx]), + emqx_ct_helpers:stop_apps([emqx_authn]), Config. init_per_testcase(_AllTestCase, Config) -> diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 2b8475cf7..2b8f62f58 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -84,11 +84,11 @@ all() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), Config. end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_gateway]). + emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]). %%-------------------------------------------------------------------- %% Test cases @@ -98,7 +98,7 @@ end_per_suite(_) -> %% Connect t_connect(_) -> - SockName = {'mqttsn#1:udp', 1884}, + SockName = {'mqttsn:udp', 1884}, ?assertEqual(true, lists:keymember(SockName, 1, esockd:listeners())), {ok, Socket} = gen_udp:open(0, [binary]), diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index 3d05b73c3..328e9fa79 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -45,11 +45,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Cfg) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), Cfg. end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway]), + emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]), ok. %%-------------------------------------------------------------------- From 9072a60652161a8e0ca73015991ff65e35fce7f8 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 16 Aug 2021 23:16:36 +0800 Subject: [PATCH 042/306] chore(gw): refine the cli command --- .../src/bhvrs/emqx_gateway_channel.erl | 4 +++ .../src/bhvrs/emqx_gateway_conn.erl | 13 +++++--- apps/emqx_gateway/src/emqx_gateway_cli.erl | 32 +++++++++---------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl index abd7391bd..06efe4fd0 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl @@ -73,7 +73,11 @@ %% @doc Handle the custom gen_server:call/2 for its connection process -callback handle_call(Req :: any(), channel()) -> {reply, Reply :: any(), channel()} + %% Reply to caller and trigger an event(s) + | {reply, Reply :: any(), + EventOrEvents:: tuple() | list(tuple()), channel()} | {shutdown, Reason :: any(), Reply :: any(), channel()} + %% Shutdown the process, reply to caller and write a packet to client | {shutdown, Reason :: any(), Reply :: any(), emqx_gateway_frame:frame(), channel()}. diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index 1f5cff043..fa0a830e5 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -550,10 +550,16 @@ handle_call(_From, Req, State = #state{ case ChannMod:handle_call(Req, Channel) of {reply, Reply, NChannel} -> {reply, Reply, State#state{channel = NChannel}}; - {reply, Reply, Replies, NChannel} -> - {reply, Reply, Replies, State#state{channel = NChannel}}; + {reply, Reply, Msgs, NChannel} -> + {reply, Reply, Msgs, State#state{channel = NChannel}}; {shutdown, Reason, Reply, NChannel} -> - shutdown(Reason, Reply, State#state{channel = NChannel}) + shutdown(Reason, Reply, State#state{channel = NChannel}); + {shutdown, Reason, Reply, Packet, NChannel} -> + NState = State#state{channel = NChannel}, + ok = handle_outgoing(Packet, NState), + shutdown(Reason, Reply, NState) + + end. %%-------------------------------------------------------------------- @@ -829,7 +835,6 @@ inc_outgoing_stats(Ctx, FrameMod, Packet) -> %%-------------------------------------------------------------------- %% Helper functions --compile({inline, [next_msgs/1]}). next_msgs(Event) when is_tuple(Event) -> Event; next_msgs(More) when is_list(More) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index fa2363370..fbd559424 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -50,30 +50,30 @@ is_cmd(Fun) -> %% Cmds gateway(["list"]) -> - lists:foreach(fun(#{id := InstaId, name := Name, type := Type}) -> + lists:foreach(fun(#{type := Type, status := Status}) -> %% FIXME: Get the real running status - emqx_ctl:print("Gateway(~s, name=~s, type=~s, status=running~n", - [InstaId, Name, Type]) + emqx_ctl:print("Gateway(type=~s, status=~s~n", + [Type, Status]) end, emqx_gateway:list()); -gateway(["lookup", GatewayInstaId]) -> - case emqx_gateway:lookup(atom(GatewayInstaId)) of +gateway(["lookup", GwType]) -> + case emqx_gateway:lookup(atom(GwType)) of undefined -> emqx_ctl:print("undefined~n"); Info -> emqx_ctl:print("~p~n", [Info]) end; -gateway(["stop", GatewayInstaId]) -> - case emqx_gateway:stop(atom(GatewayInstaId)) of +gateway(["stop", GwType]) -> + case emqx_gateway:stop(atom(GwType)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -gateway(["start", GatewayInstaId]) -> - case emqx_gateway:start(atom(GatewayInstaId)) of +gateway(["start", GwType]) -> + case emqx_gateway:start(atom(GwType)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> @@ -83,12 +83,12 @@ gateway(["start", GatewayInstaId]) -> gateway(_) -> %% TODO: create/remove APIs emqx_ctl:usage([ {"gateway list", - "List all created gateway instances"} - , {"gateway lookup ", + "List all gateway"} + , {"gateway lookup ", "Looup a gateway detailed informations"} - , {"gateway stop ", - "Stop a gateway instance and release all resources"} - , {"gateway start ", + , {"gateway stop ", + "Stop a gateway instance"} + , {"gateway start ", "Start a gateway instance"} ]). @@ -146,8 +146,8 @@ gateway(_) -> end; 'gateway-metrics'(_) -> - emqx_ctl:usage([ {"gateway-metrics ", - "List all metrics for a type of gateway"} + emqx_ctl:usage([ {"gateway-metrics ", + "List all metrics for a gateway"} ]). atom(Id) -> From c5a4e05418024d86b63a887afadf54d1a37fb158 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 17 Aug 2021 11:51:52 +0800 Subject: [PATCH 043/306] refactor(gw): rename gateway type to name --- apps/emqx_gateway/include/emqx_gateway.hrl | 4 +- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 40 +-- apps/emqx_gateway/src/emqx_gateway.erl | 32 +-- apps/emqx_gateway/src/emqx_gateway_cli.erl | 65 ++--- apps/emqx_gateway/src/emqx_gateway_cm.erl | 255 +++++++++--------- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 40 +-- apps/emqx_gateway/src/emqx_gateway_gw_sup.erl | 54 ++-- .../src/emqx_gateway_insta_sup.erl | 47 ++-- .../emqx_gateway/src/emqx_gateway_metrics.erl | 36 +-- .../src/emqx_gateway_registry.erl | 48 ++-- apps/emqx_gateway/src/emqx_gateway_sup.erl | 56 ++-- .../src/exproto/emqx_exproto_impl.erl | 64 ++--- .../src/lwm2m/emqx_lwm2m_impl.erl | 40 +-- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 42 +-- .../src/stomp/emqx_stomp_impl.erl | 40 +-- 15 files changed, 434 insertions(+), 429 deletions(-) diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl index 997d6bc72..9099ecd4d 100644 --- a/apps/emqx_gateway/include/emqx_gateway.hrl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -18,11 +18,11 @@ -define(EMQX_GATEWAY_HRL, 1). -type instance_id() :: atom(). --type gateway_type() :: atom(). +-type gateway_name() :: atom(). %% @doc The Gateway defination -type gateway() :: - #{ type := gateway_type() + #{ name := gateway_name() , descr => binary() | undefined %% Appears only in creating or detailed info , rawconf => map() diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index a27db934f..b3f7b640a 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -48,18 +48,18 @@ unreg() -> %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_gateway_load(_Gateway = #{type := GwType, +on_gateway_load(_Gateway = #{name := GwName, rawconf := RawConf }, Ctx) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(GwType, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), {ok, ListenerPids, #{ctx => Ctx}}. on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwType = maps:get(type, NewGateway), + GwName = maps:get(name, NewGateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? @@ -69,37 +69,37 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> Class : Reason : Stk -> logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [GwType, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_gateway_unload(_Gateway = #{ type := GwType, +on_gateway_unload(_Gateway = #{ name := GwName, rawconf := RawConf }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(GwType, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]), + [GwName, Type, ListenOnStr]), Pid; {error, Reason} -> ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]), + [GwName, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwType, Type), +start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_coap_frame, @@ -114,21 +114,21 @@ do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). -name(GwType, Type) -> - list_to_atom(lists:concat([GwType, ":", Type])). +name(GwName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type])). -stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]); + [GwName, Type, ListenOnStr]); {error, Reason} -> ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]) + [GwName, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwType, Type), +stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 2835b91ff..01d5897ff 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -30,7 +30,7 @@ ]). -spec registered_gateway() -> - [{gateway_type(), emqx_gateway_registry:descriptor()}]. + [{gateway_name(), emqx_gateway_registry:descriptor()}]. registered_gateway() -> emqx_gateway_registry:list(). @@ -41,35 +41,35 @@ registered_gateway() -> list() -> emqx_gateway_sup:list_gateway_insta(). --spec load(gateway_type(), map()) +-spec load(gateway_name(), map()) -> {ok, pid()} | {error, any()}. -load(GwType, RawConf) -> - Gateway = #{ type => GwType +load(Name, RawConf) -> + Gateway = #{ name => Name , descr => undefined , rawconf => RawConf }, emqx_gateway_sup:load_gateway(Gateway). --spec unload(gateway_type()) -> ok | {error, any()}. -unload(GwType) -> - emqx_gateway_sup:unload_gateway(GwType). +-spec unload(gateway_name()) -> ok | {error, any()}. +unload(Name) -> + emqx_gateway_sup:unload_gateway(Name). --spec lookup(gateway_type()) -> gateway() | undefined. -lookup(GwType) -> - emqx_gateway_sup:lookup_gateway(GwType). +-spec lookup(gateway_name()) -> gateway() | undefined. +lookup(Name) -> + emqx_gateway_sup:lookup_gateway(Name). -spec update(gateway()) -> ok | {error, any()}. update(NewGateway) -> emqx_gateway_sup:update_gateway(NewGateway). --spec start(gateway_type()) -> ok | {error, any()}. -start(GwType) -> - emqx_gateway_sup:start_gateway_insta(GwType). +-spec start(gateway_name()) -> ok | {error, any()}. +start(Name) -> + emqx_gateway_sup:start_gateway_insta(Name). --spec stop(gateway_type()) -> ok | {error, any()}. -stop(GwType) -> - emqx_gateway_sup:stop_gateway_insta(GwType). +-spec stop(gateway_name()) -> ok | {error, any()}. +stop(Name) -> + emqx_gateway_sup:stop_gateway_insta(Name). %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index fbd559424..2430b38e6 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -50,30 +50,31 @@ is_cmd(Fun) -> %% Cmds gateway(["list"]) -> - lists:foreach(fun(#{type := Type, status := Status}) -> - %% FIXME: Get the real running status - emqx_ctl:print("Gateway(type=~s, status=~s~n", - [Type, Status]) + lists:foreach(fun(#{name := Name} = Gateway) -> + %% XXX: More infos: listeners?, connected? + Status = maps:get(status, Gateway, stopped), + emqx_ctl:print("Gateway(name=~s, status=~s)~n", + [Name, Status]) end, emqx_gateway:list()); -gateway(["lookup", GwType]) -> - case emqx_gateway:lookup(atom(GwType)) of +gateway(["lookup", Name]) -> + case emqx_gateway:lookup(atom(Name)) of undefined -> emqx_ctl:print("undefined~n"); Info -> emqx_ctl:print("~p~n", [Info]) end; -gateway(["stop", GwType]) -> - case emqx_gateway:stop(atom(GwType)) of +gateway(["stop", Name]) -> + case emqx_gateway:stop(atom(Name)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -gateway(["start", GwType]) -> - case emqx_gateway:start(atom(GwType)) of +gateway(["start", Name]) -> + case emqx_gateway:start(atom(Name)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> @@ -84,60 +85,60 @@ gateway(_) -> %% TODO: create/remove APIs emqx_ctl:usage([ {"gateway list", "List all gateway"} - , {"gateway lookup ", - "Looup a gateway detailed informations"} - , {"gateway stop ", + , {"gateway lookup ", + "Lookup a gateway detailed informations"} + , {"gateway stop ", "Stop a gateway instance"} - , {"gateway start ", + , {"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]) + fun({Name, #{cbkmod := CbMod}}) -> + emqx_ctl:print("Registered Name: ~s, Callback Module: ~s~n", [Name, CbMod]) end, emqx_gateway_registry:list()); 'gateway-registry'(_) -> emqx_ctl:usage([ {"gateway-registry list", - "List all registered gateway types"} + "List all registered gateways"} ]). -'gateway-clients'(["list", Type]) -> - InfoTab = emqx_gateway_cm:tabname(info, Type), +'gateway-clients'(["list", Name]) -> + InfoTab = emqx_gateway_cm:tabname(info, Name), dump(InfoTab, client); -'gateway-clients'(["lookup", Type, ClientId]) -> - ChanTab = emqx_gateway_cm:tabname(chan, Type), +'gateway-clients'(["lookup", Name, ClientId]) -> + ChanTab = emqx_gateway_cm:tabname(chan, Name), case ets:lookup(ChanTab, bin(ClientId)) of [] -> emqx_ctl:print("Not Found.~n"); [Chann] -> - InfoTab = emqx_gateway_cm:tabname(info, Type), + InfoTab = emqx_gateway_cm:tabname(info, Name), [ChannInfo] = ets:lookup(InfoTab, Chann), print({client, ChannInfo}) end; -'gateway-clients'(["kick", Type, ClientId]) -> - case emqx_gateway_cm:kick_session(Type, bin(ClientId)) of +'gateway-clients'(["kick", Name, ClientId]) -> + case emqx_gateway_cm:kick_session(Name, 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 ", + emqx_ctl:usage([ {"gateway-clients list ", + "List all clients for a gateway"} + , {"gateway-clients lookup ", "Lookup the Client Info for specified client"} - , {"gateway-clients kick ", + , {"gateway-clients kick ", "Kick out a client"} ]). -'gateway-metrics'([GatewayType]) -> - Tab = emqx_gateway_metrics:tabname(GatewayType), +'gateway-metrics'([Name]) -> + Tab = emqx_gateway_metrics:tabname(Name), case ets:info(Tab) of undefined -> - emqx_ctl:print("Bad Gateway Type.~n"); + emqx_ctl:print("Bad Gateway Name.~n"); _ -> lists:foreach( fun({K, V}) -> @@ -146,7 +147,7 @@ gateway(_) -> end; 'gateway-metrics'(_) -> - emqx_ctl:usage([ {"gateway-metrics ", + emqx_ctl:usage([ {"gateway-metrics ", "List all metrics for a gateway"} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 83142abb1..7a7ad055d 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -61,13 +61,13 @@ ]). -record(state, { - type :: atom(), %% Gateway Type - locker :: pid(), %% ClientId Locker for CM - registry :: pid(), %% ClientId Registry server + gwname :: gateway_name(), %% Gateway Name + locker :: pid(), %% ClientId Locker for CM + registry :: pid(), %% ClientId Registry server chan_pmon :: emqx_pmon:pmon() }). --type option() :: {type, gateway_type()}. +-type option() :: {gwname, gateway_name()}. -type options() :: list(option()). -define(T_TAKEOVER, 15000). @@ -79,142 +79,147 @@ -spec start_link(options()) -> {ok, pid()} | ignore | {error, any()}. start_link(Options) -> - Type = proplists:get_value(type, Options), - gen_server:start_link({local, procname(Type)}, ?MODULE, Options, []). + GwName = proplists:get_value(gwname, Options), + gen_server:start_link({local, procname(GwName)}, ?MODULE, Options, []). -procname(Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_cm'])). +procname(GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_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} +-spec cmtabs(GwName :: gateway_name()) + -> {ChanTab :: atom(), + ConnTab :: atom(), + ChannInfoTab :: atom()}. +cmtabs(GwName) -> + { tabname(chan, GwName) %% Client Tabname; Record: {ClientId, Pid} + , tabname(conn, GwName) %% Client ConnMod; Recrod: {{ClientId, Pid}, ConnMod} + , tabname(info, GwName) %% 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'])). +tabname(chan, GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_channel'])); +tabname(conn, GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_channel_conn'])); +tabname(info, GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_channel_info'])). -lockername(Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_locker'])). +lockername(GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_locker'])). --spec register_channel(atom(), binary(), pid(), emqx_types:conninfo()) -> ok. -register_channel(Type, ClientId, ChanPid, #{conn_mod := ConnMod}) when is_pid(ChanPid) -> +-spec register_channel(gateway_name(), + emqx_types:clientid(), + pid(), + emqx_types:conninfo()) -> ok. +register_channel(GwName, 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}). + true = ets:insert(tabname(chan, GwName), Chan), + true = ets:insert(tabname(conn, GwName), {Chan, ConnMod}), + ok = emqx_gateway_cm_registry:register_channel(GwName, Chan), + cast(procname(GwName), {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)), +-spec unregister_channel(gateway_name(), emqx_types:clientid()) -> ok. +unregister_channel(GwName, ClientId) when is_binary(ClientId) -> + true = do_unregister_channel(GwName, {ClientId, self()}, cmtabs(GwName)), ok. %% @doc Insert/Update the channel info and stats --spec insert_channel_info(atom(), +-spec insert_channel_info(gateway_name(), emqx_types:clientid(), emqx_types:infos(), emqx_types:stats()) -> ok. -insert_channel_info(Type, ClientId, Info, Stats) -> +insert_channel_info(GwName, ClientId, Info, Stats) -> Chan = {ClientId, self()}, - true = ets:insert(tabname(info, Type), {Chan, Info, Stats}), + true = ets:insert(tabname(info, GwName), {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()) +-spec get_chan_info(gateway_name(), emqx_types:clientid()) -> emqx_types:infos() | undefined. -get_chan_info(Type, ClientId) -> - with_channel(Type, ClientId, +get_chan_info(GwName, ClientId) -> + with_channel(GwName, ClientId, fun(ChanPid) -> - get_chan_info(Type, ClientId, ChanPid) + get_chan_info(GwName, ClientId, ChanPid) end). --spec get_chan_info(gateway_type(), emqx_types:clientid(), pid()) +-spec get_chan_info(gateway_name(), emqx_types:clientid(), pid()) -> emqx_types:infos() | undefined. -get_chan_info(Type, ClientId, ChanPid) when node(ChanPid) == node() -> +get_chan_info(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:lookup_element(tabname(info, Type), Chan, 2) + try ets:lookup_element(tabname(info, GwName), Chan, 2) catch error:badarg -> undefined end; -get_chan_info(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_chan_info, [Type, ClientId, ChanPid]). +get_chan_info(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_info, [GwName, ClientId, ChanPid]). %% @doc Update infos of the channel. --spec set_chan_info(gateway_type(), +-spec set_chan_info(gateway_name(), emqx_types:clientid(), emqx_types:infos()) -> boolean(). -set_chan_info(Type, ClientId, Infos) -> - set_chan_info(Type, ClientId, self(), Infos). +set_chan_info(GwName, ClientId, Infos) -> + set_chan_info(GwName, ClientId, self(), Infos). --spec set_chan_info(gateway_type(), +-spec set_chan_info(gateway_name(), emqx_types:clientid(), pid(), emqx_types:infos()) -> boolean(). -set_chan_info(Type, ClientId, ChanPid, Infos) when node(ChanPid) == node() -> +set_chan_info(GwName, ClientId, ChanPid, Infos) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:update_element(tabname(info, Type), Chan, {2, Infos}) + try ets:update_element(tabname(info, GwName), 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]). +set_chan_info(GwName, ClientId, ChanPid, Infos) -> + rpc_call(node(ChanPid), set_chan_info, [GwName, ClientId, ChanPid, Infos]). %% @doc Get channel's stats. --spec get_chan_stats(gateway_type(), emqx_types:clientid()) +-spec get_chan_stats(gateway_name(), emqx_types:clientid()) -> emqx_types:stats() | undefined. -get_chan_stats(Type, ClientId) -> - with_channel(Type, ClientId, +get_chan_stats(GwName, ClientId) -> + with_channel(GwName, ClientId, fun(ChanPid) -> - get_chan_stats(Type, ClientId, ChanPid) + get_chan_stats(GwName, ClientId, ChanPid) end). --spec get_chan_stats(gateway_type(), emqx_types:clientid(), pid()) +-spec get_chan_stats(gateway_name(), emqx_types:clientid(), pid()) -> emqx_types:stats() | undefined. -get_chan_stats(Type, ClientId, ChanPid) when node(ChanPid) == node() -> +get_chan_stats(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:lookup_element(tabname(info, Type), Chan, 3) + try ets:lookup_element(tabname(info, GwName), Chan, 3) catch error:badarg -> undefined end; -get_chan_stats(Type, ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_chan_stats, [Type, ClientId, ChanPid]). +get_chan_stats(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_stats, [GwName, ClientId, ChanPid]). --spec set_chan_stats(gateway_type(), +-spec set_chan_stats(gateway_name(), emqx_types:clientid(), emqx_types:stats()) -> boolean(). -set_chan_stats(Type, ClientId, Stats) -> - set_chan_stats(Type, ClientId, self(), Stats). +set_chan_stats(GwName, ClientId, Stats) -> + set_chan_stats(GwName, ClientId, self(), Stats). --spec set_chan_stats(gateway_type(), +-spec set_chan_stats(gateway_name(), emqx_types:clientid(), pid(), emqx_types:stats()) -> boolean(). -set_chan_stats(Type, ClientId, ChanPid, Stats) when node(ChanPid) == node() -> +set_chan_stats(GwName, ClientId, ChanPid, Stats) when node(ChanPid) == node() -> Chan = {ClientId, self()}, - try ets:update_element(tabname(info, Type), Chan, {3, Stats}) + try ets:update_element(tabname(info, GwName), 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]). +set_chan_stats(GwName, ClientId, ChanPid, Stats) -> + rpc_call(node(ChanPid), set_chan_stats, [GwName, ClientId, ChanPid, Stats]). --spec connection_closed(gateway_type(), emqx_types:clientid()) -> true. -connection_closed(Type, ClientId) -> +-spec connection_closed(gateway_name(), emqx_types:clientid()) -> true. +connection_closed(GwName, ClientId) -> %% XXX: Why we need to delete conn_mod tab ??? Chan = {ClientId, self()}, - ets:delete_object(tabname(conn, Type), Chan). + ets:delete_object(tabname(conn, GwName), Chan). --spec open_session(Type :: atom(), CleanStart :: boolean(), +-spec open_session(GwName :: gateway_name(), + CleanStart :: boolean(), ClientInfo :: emqx_types:clientinfo(), ConnInfo :: emqx_types:conninfo(), CreateSessionFun :: fun((emqx_types:clientinfo(), @@ -226,24 +231,24 @@ connection_closed(Type, ClientId) -> }} | {error, any()}. -open_session(Type, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> - open_session(Type, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, emqx_session). +open_session(GwName, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> + open_session(GwName, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, emqx_session). -open_session(Type, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> +open_session(GwName, true = _CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> Self = self(), ClientId = maps:get(clientid, ClientInfo), Fun = fun(_) -> - ok = discard_session(Type, ClientId), - Session = create_session(Type, + ok = discard_session(GwName, ClientId), + Session = create_session(GwName, ClientInfo, ConnInfo, CreateSessionFun, SessionMod ), - register_channel(Type, ClientId, Self, ConnInfo), + register_channel(GwName, ClientId, Self, ConnInfo), {ok, #{session => Session, present => false}} end, - locker_trans(Type, ClientId, Fun); + locker_trans(GwName, ClientId, Fun); open_session(_Type, false = _CleanStart, _ClientInfo, _ConnInfo, _CreateSessionFun, _SessionMod) -> @@ -251,13 +256,13 @@ open_session(_Type, false = _CleanStart, {error, not_supported_now}. %% @private -create_session(Type, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> +create_session(GwName, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> try Session = emqx_gateway_utils:apply( CreateSessionFun, [ClientInfo, ConnInfo] ), - ok = emqx_gateway_metrics:inc(Type, 'session.created'), + ok = emqx_gateway_metrics:inc(GwName, 'session.created'), SessionInfo = case is_tuple(Session) andalso element(1, Session) == session of true -> SessionMod:info(Session); @@ -279,17 +284,17 @@ create_session(Type, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> 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 +-spec discard_session(GwName :: gateway_name(), binary()) -> ok. +discard_session(GwName, ClientId) when is_binary(ClientId) -> + case lookup_channels(GwName, ClientId) of [] -> ok; - ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(Type, ClientId, Pid) end, ChanPids) + ChanPids -> lists:foreach(fun(Pid) -> do_discard_session(GwName, ClientId, Pid) end, ChanPids) end. %% @private -do_discard_session(Type, ClientId, Pid) -> +do_discard_session(GwName, ClientId, Pid) -> try - discard_session(Type, ClientId, Pid) + discard_session(GwName, ClientId, Pid) catch _ : noproc -> % emqx_ws_connection: call %?tp(debug, "session_already_gone", #{pid => Pid}), @@ -307,72 +312,72 @@ do_discard_session(Type, ClientId, Pid) -> end. %% @private -discard_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> - case get_chann_conn_mod(Type, ClientId, ChanPid) of +discard_session(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chann_conn_mod(GwName, 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]). +discard_session(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), discard_session, [GwName, ClientId, ChanPid]). --spec kick_session(gateway_type(), emqx_types:clientid()) +-spec kick_session(gateway_name(), emqx_types:clientid()) -> {error, any()} | ok. -kick_session(Type, ClientId) -> - case lookup_channels(Type, ClientId) of +kick_session(GwName, ClientId) -> + case lookup_channels(GwName, ClientId) of [] -> {error, not_found}; [ChanPid] -> - kick_session(Type, ClientId, ChanPid); + kick_session(GwName, 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) + catch discard_session(GwName, ClientId, StalePid) end, StalePids), - kick_session(Type, ClientId, ChanPid) + kick_session(GwName, ClientId, ChanPid) end. -kick_session(Type, ClientId, ChanPid) when node(ChanPid) == node() -> - case get_chan_info(Type, ClientId, ChanPid) of +kick_session(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chan_info(GwName, 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]). +kick_session(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), kick_session, [GwName, ClientId, ChanPid]). -with_channel(Type, ClientId, Fun) -> - case lookup_channels(Type, ClientId) of +with_channel(GwName, ClientId, Fun) -> + case lookup_channels(GwName, 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). +-spec(lookup_channels(gateway_name(), emqx_types:clientid()) -> list(pid())). +lookup_channels(GwName, ClientId) -> + emqx_gateway_cm_registry:lookup_channels(GwName, ClientId). -get_chann_conn_mod(Type, ClientId, ChanPid) when node(ChanPid) == node() -> +get_chann_conn_mod(GwName, ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try [ConnMod] = ets:lookup_element(tabname(conn, Type), Chan, 2), ConnMod + try [ConnMod] = ets:lookup_element(tabname(conn, GwName), 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]). +get_chann_conn_mod(GwName, ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chann_conn_mod, [GwName, ClientId, ChanPid]). %% Locker locker_trans(_Type, undefined, Fun) -> Fun([]); -locker_trans(Type, ClientId, Fun) -> - Locker = lockername(Type), +locker_trans(GwName, ClientId, Fun) -> + Locker = lockername(GwName), case locker_lock(Locker, ClientId) of {true, Nodes} -> try Fun(Nodes) after locker_unlock(Locker, ClientId) end; @@ -401,27 +406,27 @@ cast(Name, Msg) -> %%-------------------------------------------------------------------- init(Options) -> - Type = proplists:get_value(type, Options), + GwName = proplists:get_value(gwname, Options), TabOpts = [public, {write_concurrency, true}], - {ChanTab, ConnTab, InfoTab} = cmtabs(Type), + {ChanTab, ConnTab, InfoTab} = cmtabs(GwName), 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), + {ok, Registry} = emqx_gateway_cm_registry:start_link(GwName), %% Start locker process - {ok, Locker} = ekka_locker:start_link(lockername(Type)), + {ok, Locker} = ekka_locker:start_link(lockername(GwName)), %% Interval update stats %% TODO: v0.2 %ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), - {ok, #state{type = Type, + {ok, #state{gwname = GwName, locker = Locker, registry = Registry, chan_pmon = emqx_pmon:new()}}. @@ -438,12 +443,12 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({'DOWN', _MRef, process, Pid, _Reason}, - State = #state{type = Type, chan_pmon = PMon}) -> + State = #state{gwname = GwName, chan_pmon = PMon}) -> ChanPids = [Pid | emqx_misc:drain_down(?DEFAULT_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]), + CmTabs = cmtabs(GwName), + ok = emqx_pool:async_submit(fun do_unregister_channel_task/3, [Items, GwName, CmTabs]), {noreply, State#state{chan_pmon = PMon1}}; handle_info(_Info, State) -> @@ -455,18 +460,18 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -do_unregister_channel_task(Items, Type, CmTabs) -> +do_unregister_channel_task(Items, GwName, CmTabs) -> lists:foreach( fun({ChanPid, ClientId}) -> - do_unregister_channel(Type, {ClientId, ChanPid}, CmTabs) + do_unregister_channel(GwName, {ClientId, ChanPid}, CmTabs) end, Items). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -do_unregister_channel(Type, Chan, {ChanTab, ConnTab, InfoTab}) -> - ok = emqx_gateway_cm_registry:unregister_channel(Type, Chan), +do_unregister_channel(GwName, Chan, {ChanTab, ConnTab, InfoTab}) -> + ok = emqx_gateway_cm_registry:unregister_channel(GwName, 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_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index d9517b53f..22391d0b6 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -27,10 +27,8 @@ %% configuration, register devices and other common operations. %% -type context() :: - #{ %% Gateway Instance ID - instid := instance_id() - %% Gateway ID - , type := gateway_type() + #{ %% Gateway Name + gwname := gateway_name() %% Autenticator , auth := emqx_authn:chain_id() | undefined %% The ConnectionManager PID @@ -98,41 +96,43 @@ authenticate(_Ctx, ClientInfo) -> }} | {error, any()}. open_session(Ctx, CleanStart, ClientInfo, ConnInfo, CreateSessionFun) -> - open_session(Ctx, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, emqx_session). + open_session(Ctx, CleanStart, ClientInfo, ConnInfo, + CreateSessionFun, emqx_session). open_session(Ctx, false, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> logger:warning("clean_start=false is not supported now, " "fallback to clean_start mode"), open_session(Ctx, true, ClientInfo, ConnInfo, CreateSessionFun, SessionMod); -open_session(_Ctx = #{type := Type}, +open_session(_Ctx = #{gwname := GwName}, CleanStart, ClientInfo, ConnInfo, CreateSessionFun, SessionMod) -> - emqx_gateway_cm:open_session(Type, CleanStart, - ClientInfo, ConnInfo, CreateSessionFun, SessionMod). + emqx_gateway_cm:open_session(GwName, CleanStart, + ClientInfo, ConnInfo, + CreateSessionFun, SessionMod). -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). +insert_channel_info(_Ctx = #{gwname := GwName}, ClientId, Infos, Stats) -> + emqx_gateway_cm:insert_channel_info(GwName, 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). +set_chan_info(_Ctx = #{gwname := GwName}, ClientId, Infos) -> + emqx_gateway_cm:set_chan_info(GwName, 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). +set_chan_stats(_Ctx = #{gwname := GwName}, ClientId, Stats) -> + emqx_gateway_cm:set_chan_stats(GwName, ClientId, Stats). -spec connection_closed(context(), emqx_types:clientid()) -> boolean(). -connection_closed(_Ctx = #{type := Type}, ClientId) -> - emqx_gateway_cm:connection_closed(Type, ClientId). +connection_closed(_Ctx = #{gwname := GwName}, ClientId) -> + emqx_gateway_cm:connection_closed(GwName, ClientId). -spec authorize(context(), emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) @@ -140,11 +140,11 @@ connection_closed(_Ctx = #{type := Type}, ClientId) -> 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 = #{gwname := GwName}, Name) -> + emqx_gateway_metrics:inc(GwName, Name). -metrics_inc(_Ctx = #{type := Type}, Name, Oct) -> - emqx_gateway_metrics:inc(Type, Name, Oct). +metrics_inc(_Ctx = #{gwname := GwName}, Name, Oct) -> + emqx_gateway_metrics:inc(GwName, Name, Oct). %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl index 37bfd10a3..bfde2b562 100644 --- a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -42,18 +42,18 @@ %% APIs %%-------------------------------------------------------------------- -start_link(Type) -> - supervisor:start_link({local, Type}, ?MODULE, [Type]). +start_link(GwName) -> + supervisor:start_link({local, GwName}, ?MODULE, [GwName]). -spec create_insta(pid(), gateway(), map()) -> {ok, GwInstaPid :: pid()} | {error, any()}. -create_insta(Sup, Gateway = #{type := GwType}, GwDscrptr) -> - case emqx_gateway_utils:find_sup_child(Sup, GwType) of +create_insta(Sup, Gateway = #{name := Name}, GwDscrptr) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of {ok, _GwInstaPid} -> {error, alredy_existed}; false -> - Ctx = ctx(Sup, GwType), + Ctx = ctx(Sup, Name), %% ChildSpec = emqx_gateway_utils:childspec( - GwType, + Name, worker, emqx_gateway_insta_sup, [Gateway, Ctx, GwDscrptr] @@ -63,34 +63,34 @@ create_insta(Sup, Gateway = #{type := GwType}, GwDscrptr) -> ) end. --spec remove_insta(pid(), GwType :: gateway_type()) -> ok | {error, any()}. -remove_insta(Sup, GwType) -> - case emqx_gateway_utils:find_sup_child(Sup, GwType) of +-spec remove_insta(pid(), Name :: gateway_name()) -> ok | {error, any()}. +remove_insta(Sup, Name) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> ok; {ok, _GwInstaPid} -> - ok = supervisor:terminate_child(Sup, GwType), - ok = supervisor:delete_child(Sup, GwType) + ok = supervisor:terminate_child(Sup, Name), + ok = supervisor:delete_child(Sup, Name) end. -spec update_insta(pid(), NewGateway :: gateway()) -> ok | {error, any()}. -update_insta(Sup, NewGateway = #{type := GwType}) -> - case emqx_gateway_utils:find_sup_child(Sup, GwType) of +update_insta(Sup, NewGateway = #{name := Name}) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> emqx_gateway_insta_sup:update(GwInstaPid, NewGateway) end. --spec start_insta(pid(), gateway_type()) -> ok | {error, any()}. -start_insta(Sup, GwType) -> - case emqx_gateway_utils:find_sup_child(Sup, GwType) of +-spec start_insta(pid(), gateway_name()) -> ok | {error, any()}. +start_insta(Sup, Name) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> emqx_gateway_insta_sup:enable(GwInstaPid) end. --spec stop_insta(pid(), gateway_type()) -> ok | {error, any()}. -stop_insta(Sup, GwType) -> - case emqx_gateway_utils:find_sup_child(Sup, GwType) of +-spec stop_insta(pid(), gateway_name()) -> ok | {error, any()}. +stop_insta(Sup, Name) -> + case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> emqx_gateway_insta_sup:disable(GwInstaPid) @@ -99,33 +99,31 @@ stop_insta(Sup, GwType) -> -spec list_insta(pid()) -> [gateway()]. list_insta(Sup) -> lists:filtermap( - fun({GwType, GwInstaPid, _Type, _Mods}) -> - is_gateway_insta_id(GwType) + fun({Name, GwInstaPid, _Type, _Mods}) -> + is_gateway_insta_id(Name) andalso {true, emqx_gateway_insta_sup:info(GwInstaPid)} end, supervisor:which_children(Sup)). %% Supervisor callback %% @doc Initialize Top Supervisor for a Protocol -init([Type]) -> +init([GwName]) -> SupFlags = #{ strategy => one_for_one , intensity => 10 , period => 60 }, - CmOpts = [{type, Type}], + CmOpts = [{gwname, GwName}], CM = emqx_gateway_utils:childspec(worker, emqx_gateway_cm, [CmOpts]), - Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [Type]), + Metrics = emqx_gateway_utils:childspec(worker, emqx_gateway_metrics, [GwName]), {ok, {SupFlags, [CM, Metrics]}}. %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -ctx(Sup, GwType) -> - {_, Type} = erlang:process_info(Sup, registered_name), +ctx(Sup, Name) -> {ok, CM} = emqx_gateway_utils:find_sup_child(Sup, emqx_gateway_cm), - #{ instid => GwType - , type => Type + #{ gwname => Name , cm => CM }. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 0cdf15f9b..404766719 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -86,8 +86,8 @@ call(Pid, Req) -> init([Gateway, Ctx0, _GwDscrptr]) -> process_flag(trap_exit, true), - #{type := GwType, rawconf := RawConf} = Gateway, - Ctx = do_init_context(GwType, RawConf, Ctx0), + #{name := GwName, rawconf := RawConf} = Gateway, + Ctx = do_init_context(GwName, RawConf, Ctx0), State = #state{ gw = Gateway, ctx = Ctx, @@ -102,11 +102,11 @@ init([Gateway, Ctx0, _GwDscrptr]) -> {ok, NState} end. -do_init_context(GwType, RawConf, Ctx) -> +do_init_context(GwName, RawConf, Ctx) -> Auth = case maps:get(authentication, RawConf, #{enable => false}) of #{enable := true, authenticators := AuthCfgs} when is_list(AuthCfgs) -> - create_authenticators_for_gateway_insta(GwType, AuthCfgs); + create_authenticators_for_gateway_insta(GwName, AuthCfgs); _ -> undefined end, @@ -116,8 +116,8 @@ do_deinit_context(Ctx) -> cleanup_authenticators_for_gateway_insta(maps:get(auth, Ctx)), ok. -handle_call(info, _From, State = #state{gw = Gateway}) -> - {reply, Gateway, State}; +handle_call(info, _From, State = #state{gw = Gateway, status = Status}) -> + {reply, Gateway#{status => Status}, State}; handle_call(disable, _From, State = #state{status = Status}) -> case Status of @@ -146,21 +146,22 @@ handle_call(enable, _From, State = #state{status = Status}) -> end; %% Stopped -> update -handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, - status = stopped}) -> - case maps:get(type, NewGateway, undefined) - == maps:get(type, Gateway, undefined) of +handle_call({update, NewGateway}, _From, State = #state{ + gw = Gateway, + status = stopped}) -> + case maps:get(name, NewGateway, undefined) + == maps:get(name, Gateway, undefined) of true -> {reply, ok, State#state{gw = NewGateway}}; false -> - {reply, {error, gateway_type_not_match}, State} + {reply, {error, gateway_name_not_match}, State} end; %% Running -> update handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, status = running}) -> - case maps:get(type, NewGateway, undefined) - == maps:get(type, Gateway, undefined) of + case maps:get(name, NewGateway, undefined) + == maps:get(name, Gateway, undefined) of true -> case cb_gateway_update(NewGateway, State) of {ok, NState} -> @@ -169,7 +170,7 @@ handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, {reply, {error, Reason}, State} end; false -> - {reply, {error, gateway_type_not_match}, State} + {reply, {error, gateway_name_not_match}, State} end; handle_call(_Request, _From, State) -> @@ -215,8 +216,8 @@ code_change(_OldVsn, State, _Extra) -> %% @doc AuthCfgs is a array of authenticatior configurations, %% see: emqx_authn_schema:authenticators/1 -create_authenticators_for_gateway_insta(GwType, AuthCfgs) -> - ChainId = atom_to_binary(GwType, utf8), +create_authenticators_for_gateway_insta(GwName, AuthCfgs) -> + ChainId = atom_to_binary(GwName, utf8), case emqx_authn:create_chain(#{id => ChainId}) of {ok, _ChainInfo} -> Results = lists:map(fun(AuthCfg = #{name := Name}) -> @@ -250,10 +251,10 @@ cleanup_authenticators_for_gateway_insta(ChainId) -> "reason: ~p", [ChainId, Reason]) end. -cb_gateway_unload(State = #state{gw = Gateway = #{type := GwType}, +cb_gateway_unload(State = #state{gw = Gateway = #{name := GwName}, gw_state = GwState}) -> try - #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), CbMod:on_gateway_unload(Gateway, GwState), {ok, State#state{child_pids = [], gw_state = undefined, @@ -267,10 +268,10 @@ cb_gateway_unload(State = #state{gw = Gateway = #{type := GwType}, {error, {Class, Reason, Stk}} end. -cb_gateway_load(State = #state{gw = Gateway = #{type := GwType}, +cb_gateway_load(State = #state{gw = Gateway = #{name := GwName}, ctx = Ctx}) -> try - #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), case CbMod:on_gateway_load(Gateway, Ctx) of {error, Reason} -> throw({callback_return_error, Reason}); {ok, ChildPidOrSpecs, GwState} -> @@ -285,17 +286,17 @@ cb_gateway_load(State = #state{gw = Gateway = #{type := GwType}, Class : Reason1 : Stk -> logger:error("Failed to load ~s gateway (~0p, ~0p) crashed: " "{~p, ~p}, stacktrace: ~0p", - [GwType, Gateway, Ctx, + [GwName, Gateway, Ctx, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. cb_gateway_update(NewGateway, - State = #state{gw = Gateway = #{type := GwType}, + State = #state{gw = Gateway = #{name := GwName}, ctx = Ctx, gw_state = GwState}) -> try - #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwType), + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), case CbMod:on_gateway_update(NewGateway, Gateway, GwState) of {error, Reason} -> throw({callback_return_error, Reason}); {ok, ChildPidOrSpecs, NGwState} -> diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl index d4e39443c..458017118 100644 --- a/apps/emqx_gateway/src/emqx_gateway_metrics.erl +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -47,36 +47,36 @@ %% APIs %%-------------------------------------------------------------------- -start_link(Type) -> - gen_server:start_link(?MODULE, [Type], []). +start_link(GwName) -> + gen_server:start_link(?MODULE, [GwName], []). --spec inc(gateway_type(), atom()) -> ok. -inc(Type, Name) -> - inc(Type, Name, 1). +-spec inc(gateway_name(), atom()) -> ok. +inc(GwName, Name) -> + inc(GwName, Name, 1). --spec inc(gateway_type(), atom(), integer()) -> ok. -inc(Type, Name, Oct) -> - ets:update_counter(tabname(Type), Name, {2, Oct}, {Name, 0}), +-spec inc(gateway_name(), atom(), integer()) -> ok. +inc(GwName, Name, Oct) -> + ets:update_counter(tabname(GwName), Name, {2, Oct}, {Name, 0}), ok. --spec dec(gateway_type(), atom()) -> ok. -dec(Type, Name) -> - inc(Type, Name, -1). +-spec dec(gateway_name(), atom()) -> ok. +dec(GwName, Name) -> + inc(GwName, Name, -1). --spec dec(gateway_type(), atom(), non_neg_integer()) -> ok. -dec(Type, Name, Oct) -> - inc(Type, Name, -Oct). +-spec dec(gateway_name(), atom(), non_neg_integer()) -> ok. +dec(GwName, Name, Oct) -> + inc(GwName, Name, -Oct). -tabname(Type) -> - list_to_atom(lists:concat([emqx_gateway_, Type, '_metrics'])). +tabname(GwName) -> + list_to_atom(lists:concat([emqx_gateway_, GwName, '_metrics'])). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([Type]) -> +init([GwName]) -> TabOpts = [public, {write_concurrency, true}], - ok = emqx_tables:new(tabname(Type), [set|TabOpts]), + ok = emqx_tables:new(tabname(GwName), [set|TabOpts]), {ok, #state{}}. handle_call(_Request, _From, State) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl index 6eeb958ed..b311073a9 100644 --- a/apps/emqx_gateway/src/emqx_gateway_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc The Registry Centre of Gateway Type +%% @doc The Registry Centre of Gateway -module(emqx_gateway_registry). -include("include/emqx_gateway.hrl"). @@ -42,7 +42,7 @@ ]). -record(state, { - reged = #{} :: #{ gateway_type() => descriptor() } + reged = #{} :: #{ gateway_name() => descriptor() } }). -type registry_options() :: [registry_option()]. @@ -64,33 +64,33 @@ start_link() -> %% Mgmt %%-------------------------------------------------------------------- --spec reg(gateway_type(), registry_options()) +-spec reg(gateway_name(), registry_options()) -> ok | {error, any()}. -reg(Type, RgOpts) -> - CbMod = proplists:get_value(cbkmod, RgOpts, Type), +reg(Name, RgOpts) -> + CbMod = proplists:get_value(cbkmod, RgOpts, Name), Dscrptr = #{ cbkmod => CbMod , rgopts => RgOpts }, - call({reg, Type, Dscrptr}). + call({reg, Name, Dscrptr}). --spec unreg(gateway_type()) -> ok | {error, any()}. -unreg(Type) -> +-spec unreg(gateway_name()) -> ok | {error, any()}. +unreg(Name) -> %% TODO: Checking ALL INSTACE HAS STOPPED - call({unreg, Type}). + call({unreg, Name}). %% TODO: -%unreg(Type, Force) -> -% call({unreg, Type, Froce}). +%unreg(Name, Force) -> +% call({unreg, Name, Froce}). %% @doc Return all registered protocol gateway implementation --spec list() -> [{gateway_type(), descriptor()}]. +-spec list() -> [{gateway_name(), descriptor()}]. list() -> call(all). --spec lookup(gateway_type()) -> undefined | descriptor(). -lookup(Type) -> - call({lookup, Type}). +-spec lookup(gateway_name()) -> undefined | descriptor(). +lookup(Name) -> + call({lookup, Name}). call(Req) -> gen_server:call(?MODULE, Req, 5000). @@ -104,29 +104,29 @@ init([]) -> process_flag(trap_exit, true), {ok, #state{reged = #{}}}. -handle_call({reg, Type, Dscrptr}, _From, State = #state{reged = Gateways}) -> - case maps:get(Type, Gateways, notfound) of +handle_call({reg, Name, Dscrptr}, _From, State = #state{reged = Gateways}) -> + case maps:get(Name, Gateways, notfound) of notfound -> - NGateways = maps:put(Type, Dscrptr, Gateways), + NGateways = maps:put(Name, Dscrptr, Gateways), {reply, ok, State#state{reged = NGateways}}; _ -> {reply, {error, already_existed}, State} end; -handle_call({unreg, Type}, _From, State = #state{reged = Gateways}) -> - case maps:get(Type, Gateways, undefined) of +handle_call({unreg, Name}, _From, State = #state{reged = Gateways}) -> + case maps:get(Name, Gateways, undefined) of undefined -> {reply, ok, State}; _ -> - _ = emqx_gateway_sup:unload_gateway(Type), - {reply, ok, State#state{reged = maps:remove(Type, Gateways)}} + _ = emqx_gateway_sup:unload_gateway(Name), + {reply, ok, State#state{reged = maps:remove(Name, Gateways)}} end; handle_call(all, _From, State = #state{reged = Gateways}) -> {reply, maps:to_list(Gateways), State}; -handle_call({lookup, Type}, _From, State = #state{reged = Gateways}) -> - Reply = maps:get(Type, Gateways, undefined), +handle_call({lookup, Name}, _From, State = #state{reged = Gateways}) -> + Reply = maps:get(Name, Gateways, undefined), {reply, Reply, State}; handle_call(Req, _From, State) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index b925d420e..87e41d93b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -44,27 +44,27 @@ start_link() -> -spec load_gateway(gateway()) -> {ok, pid()} | {error, any()}. -load_gateway(Gateway = #{type := GwType}) -> - case emqx_gateway_registry:lookup(GwType) of - undefined -> {error, {unknown_gateway_type, GwType}}; +load_gateway(Gateway = #{name := GwName}) -> + case emqx_gateway_registry:lookup(GwName) of + undefined -> {error, {unknown_gateway_name, GwName}}; GwDscrptr -> - {ok, GwSup} = ensure_gateway_suptree_ready(GwType), + {ok, GwSup} = ensure_gateway_suptree_ready(GwName), emqx_gateway_gw_sup:create_insta(GwSup, Gateway, GwDscrptr) end. --spec unload_gateway(gateway_type()) -> ok | {error, not_found}. -unload_gateway(GwType) -> - case lists:keyfind(GwType, 1, supervisor:which_children(?MODULE)) of +-spec unload_gateway(gateway_name()) -> ok | {error, not_found}. +unload_gateway(GwName) -> + case lists:keyfind(GwName, 1, supervisor:which_children(?MODULE)) of false -> {error, not_found}; _ -> - _ = supervisor:terminate_child(?MODULE, GwType), - _ = supervisor:delete_child(?MODULE, GwType), + _ = supervisor:terminate_child(?MODULE, GwName), + _ = supervisor:delete_child(?MODULE, GwName), ok end. --spec lookup_gateway(gateway_type()) -> gateway() | undefined. -lookup_gateway(GwType) -> - case search_gateway_insta_proc(GwType) of +-spec lookup_gateway(gateway_name()) -> gateway() | undefined. +lookup_gateway(GwName) -> + case search_gateway_insta_proc(GwName) of {ok, {_, GwInstaPid}} -> emqx_gateway_insta_sup:info(GwInstaPid); _ -> @@ -74,25 +74,25 @@ lookup_gateway(GwType) -> -spec update_gateway(gateway()) -> ok | {error, any()}. -update_gateway(NewGateway = #{type := GwType}) -> - case emqx_gateway_utils:find_sup_child(?MODULE, GwType) of +update_gateway(NewGateway = #{name := GwName}) -> + case emqx_gateway_utils:find_sup_child(?MODULE, GwName) of {ok, GwSup} -> emqx_gateway_gw_sup:update_insta(GwSup, NewGateway); _ -> {error, not_found} end. -start_gateway_insta(GwType) -> - case search_gateway_insta_proc(GwType) of +start_gateway_insta(GwName) -> + case search_gateway_insta_proc(GwName) of {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:start_insta(GwSup, GwType); + emqx_gateway_gw_sup:start_insta(GwSup, GwName); _ -> {error, not_found} end. --spec stop_gateway_insta(gateway_type()) -> ok | {error, any()}. -stop_gateway_insta(GwType) -> - case search_gateway_insta_proc(GwType) of +-spec stop_gateway_insta(gateway_name()) -> ok | {error, any()}. +stop_gateway_insta(GwName) -> + case search_gateway_insta_proc(GwName) of {ok, {GwSup, _}} -> - emqx_gateway_gw_sup:stop_insta(GwSup, GwType); + emqx_gateway_gw_sup:stop_insta(GwSup, GwName); _ -> {error, not_found} end. @@ -103,9 +103,9 @@ list_gateway_insta() -> emqx_gateway_gw_sup:list_insta(SupId) end, list_started_gateway())). --spec list_started_gateway() -> [gateway_type()]. +-spec list_started_gateway() -> [gateway_name()]. list_started_gateway() -> - started_gateway_type(). + started_gateway(). %% Supervisor callback @@ -122,14 +122,14 @@ init([]) -> %% Internal funcs %%-------------------------------------------------------------------- -ensure_gateway_suptree_ready(GwType) -> - case lists:keyfind(GwType, 1, supervisor:which_children(?MODULE)) of +ensure_gateway_suptree_ready(GwName) -> + case lists:keyfind(GwName, 1, supervisor:which_children(?MODULE)) of false -> ChildSpec = emqx_gateway_utils:childspec( - GwType, + GwName, supervisor, emqx_gateway_gw_sup, - [GwType] + [GwName] ), emqx_gateway_utils:supervisor_ret( supervisor:start_child(?MODULE, ChildSpec) @@ -150,7 +150,7 @@ search_gateway_insta_proc(InstaId, [SupPid|More]) -> search_gateway_insta_proc(InstaId, More) end. -started_gateway_type() -> +started_gateway() -> lists:filtermap( fun({Id, _, _, _}) -> is_a_gateway_id(Id) andalso {true, Id} diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 4fb5cd05a..8c500be82 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -47,9 +47,9 @@ unreg() -> %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -start_grpc_server(_GwType, undefined) -> +start_grpc_server(_GwName, undefined) -> undefined; -start_grpc_server(GwType, Options = #{bind := ListenOn}) -> +start_grpc_server(GwName, Options = #{bind := ListenOn}) -> Services = #{protos => [emqx_exproto_pb], services => #{ 'emqx.exproto.v1.ConnectionAdapter' => emqx_exproto_gsvr} @@ -59,12 +59,12 @@ start_grpc_server(GwType, Options = #{bind := ListenOn}) -> SslOpts -> [{ssl_options, SslOpts}] end, - _ = grpc:start_server(GwType, ListenOn, Services, SvrOptions), - ?ULOG("Start ~s gRPC server on ~p successfully.~n", [GwType, ListenOn]). + _ = grpc:start_server(GwName, ListenOn, Services, SvrOptions), + ?ULOG("Start ~s gRPC server on ~p successfully.~n", [GwName, ListenOn]). start_grpc_client_channel(_GwType, undefined) -> undefined; -start_grpc_client_channel(GwType, Options = #{address := UriStr}) -> +start_grpc_client_channel(GwName, Options = #{address := UriStr}) -> UriMap = uri_string:parse(UriStr), Scheme = maps:get(scheme, UriMap), Host = maps:get(host, UriMap), @@ -81,36 +81,36 @@ start_grpc_client_channel(GwType, Options = #{address := UriStr}) -> transport_opts => SslOpts}}; _ -> #{} end, - grpc_client_sup:create_channel_pool(GwType, SvrAddr, ClientOpts). + grpc_client_sup:create_channel_pool(GwName, SvrAddr, ClientOpts). -on_gateway_load(_Gateway = #{ type := GwType, +on_gateway_load(_Gateway = #{ name := GwName, rawconf := RawConf }, Ctx) -> %% XXX: How to monitor it ? %% Start grpc client pool & client channel - PoolName = pool_name(GwType), + PoolName = pool_name(GwName), PoolSize = emqx_vm:schedulers() * 2, {ok, _} = emqx_pool_sup:start_link(PoolName, hash, PoolSize, {emqx_exproto_gcli, start_link, []}), - _ = start_grpc_client_channel(GwType, maps:get(handler, RawConf, undefined)), + _ = start_grpc_client_channel(GwName, maps:get(handler, RawConf, undefined)), %% XXX: How to monitor it ? - _ = start_grpc_server(GwType, maps:get(server, RawConf, undefined)), + _ = start_grpc_server(GwName, maps:get(server, RawConf, undefined)), NRawConf = maps:without( [server, handler], RawConf#{pool_name => PoolName} ), Listeners = emqx_gateway_utils:normalize_rawconf( - NRawConf#{handler => GwType} + NRawConf#{handler => GwName} ), ListenerPids = lists:map(fun(Lis) -> - start_listener(GwType, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), {ok, ListenerPids, _GwState = #{ctx => Ctx}}. on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwType = maps:get(type, NewGateway), + GwName = maps:get(name, NewGateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? @@ -120,40 +120,40 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> Class : Reason : Stk -> logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [GwType, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_gateway_unload(_Gateway = #{ type := GwType, +on_gateway_unload(_Gateway = #{ name := GwName, rawconf := RawConf }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(GwType, Lis) + stop_listener(GwName, Lis) end, Listeners). -pool_name(GwType) -> - list_to_atom(lists:concat([GwType, "_gcli_pool"])). +pool_name(GwName) -> + list_to_atom(lists:concat([GwName, "_gcli_pool"])). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]), + [GwName, Type, ListenOnStr]), Pid; {error, Reason} -> ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]), + [GwName, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwType, Type), +start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_exproto_frame, @@ -172,8 +172,8 @@ do_start_listener(udp, Name, ListenOn, Opts, MFA) -> do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). -name(GwType, Type) -> - list_to_atom(lists:concat([GwType, ":", Type])). +name(GwName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type])). merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> @@ -196,18 +196,18 @@ merge_default_by_type(Type, Options) when Type =:= udp; [{udp_options, Default} | Options] end. -stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]); + [GwName, Type, ListenOnStr]); {error, Reason} -> ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]) + [GwName, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwType, Type), +stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index cf1a1a017..d5fb0d06e 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -47,7 +47,7 @@ unreg() -> %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_gateway_load(_Gateway = #{ type := GwType, +on_gateway_load(_Gateway = #{ name := GwName, rawconf := RawConf }, Ctx) -> @@ -66,12 +66,12 @@ on_gateway_load(_Gateway = #{ type := GwType, Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(GwType, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), {ok, ListenerPids, _GwState = #{ctx => Ctx}}. on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwType = maps:get(type, NewGateway), + GwName = maps:get(name, NewGateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? @@ -81,11 +81,11 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> Class : Reason : Stk -> logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [GwType, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_gateway_unload(_Gateway = #{ type := GwType, +on_gateway_unload(_Gateway = #{ name := GwName, rawconf := RawConf }, _GwState) -> %% XXX: @@ -96,28 +96,28 @@ on_gateway_unload(_Gateway = #{ type := GwType, Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(GwType, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]), + [GwName, Type, ListenOnStr]), Pid; {error, Reason} -> ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]), + [GwName, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwType, udp), +start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, udp), NCfg = Cfg#{ctx => Ctx}, NSocketOpts = merge_default(SocketOpts), Options = [{config, NCfg}|NSocketOpts], @@ -128,8 +128,8 @@ start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> lwm2m_coap_server:start_dtls(Name, ListenOn, Options) end. -name(GwType, Type) -> - list_to_atom(lists:concat([GwType, ":", Type])). +name(GwName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -141,20 +141,20 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]); + [GwName, Type, ListenOnStr]); {error, Reason} -> ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]) + [GwName, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwType, Type), +stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, Type), case Type of udp -> lwm2m_coap_server:stop_udp(Name, ListenOn); diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 3dfb8546d..d35228e1f 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -47,7 +47,7 @@ unreg() -> %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_gateway_load(_Gateway = #{ type := GwType, +on_gateway_load(_Gateway = #{ name := GwName, rawconf := RawConf }, Ctx) -> @@ -64,7 +64,7 @@ on_gateway_load(_Gateway = #{ type := GwType, end, PredefTopics = maps:get(predefined, RawConf, []), - {ok, RegistrySvr} = emqx_sn_registry:start_link(GwType, PredefTopics), + {ok, RegistrySvr} = emqx_sn_registry:start_link(GwName, PredefTopics), NRawConf = maps:without( [broadcast, predefined], @@ -73,11 +73,11 @@ on_gateway_load(_Gateway = #{ type := GwType, Listeners = emqx_gateway_utils:normalize_rawconf(NRawConf), ListenerPids = lists:map(fun(Lis) -> - start_listener(GwType, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. -on_gateway_update(NewGateway = #{type := GwType}, OldGateway, +on_gateway_update(NewGateway = #{name := GwName}, OldGateway, GwState = #{ctx := Ctx}) -> try %% XXX: 1. How hot-upgrade the changes ??? @@ -88,37 +88,37 @@ on_gateway_update(NewGateway = #{type := GwType}, OldGateway, Class : Reason : Stk -> logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [GwType, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_gateway_unload(_Insta = #{ type := GwType, +on_gateway_unload(_Insta = #{ name := GwName, rawconf := RawConf }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(GwType, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]), + [GwName, Type, ListenOnStr]), Pid; {error, Reason} -> ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]), + [GwName, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwType, Type), +start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_sn_frame, @@ -127,8 +127,8 @@ start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwType, Type) -> - list_to_atom(lists:concat([GwType, ":", Type])). +name(GwName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -140,18 +140,18 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]); + [GwName, Type, ListenOnStr]); {error, Reason} -> ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]) + [GwName, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwType, Type), +stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 84b10e97a..19bfc16ab 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -49,21 +49,21 @@ unreg() -> %% emqx_gateway_registry callbacks %%-------------------------------------------------------------------- -on_gateway_load(_Gateway = #{ type := GwType, +on_gateway_load(_Gateway = #{ name := GwName, rawconf := RawConf }, Ctx) -> %% 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(GwType, Ctx, Lis) + start_listener(GwName, Ctx, Lis) end, Listeners), %% FIXME: How to throw an exception to interrupt the restart logic ? %% FIXME: Assign ctx to GwState {ok, ListenerPids, _GwState = #{ctx => Ctx}}. on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwType = maps:get(type, NewGateway), + GwName = maps:get(name, NewGateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old state??? @@ -73,37 +73,37 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> Class : Reason : Stk -> logger:error("Failed to update ~s; " "reason: {~0p, ~0p} stacktrace: ~0p", - [GwType, Class, Reason, Stk]), + [GwName, Class, Reason, Stk]), {error, {Class, Reason}} end. -on_gateway_unload(_Gateway = #{ type := GwType, +on_gateway_unload(_Gateway = #{ name := GwName, rawconf := RawConf }, _GwState) -> Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), lists:foreach(fun(Lis) -> - stop_listener(GwType, Lis) + stop_listener(GwName, Lis) end, Listeners). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwType, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]), + [GwName, Type, ListenOnStr]), Pid; {error, Reason} -> ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]), + [GwName, Type, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwType, Type), +start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_stomp_frame, @@ -112,8 +112,8 @@ start_listener(GwType, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwType, Type) -> - list_to_atom(lists:concat([GwType, ":", Type])). +name(GwName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), @@ -125,18 +125,18 @@ merge_default(Options) -> [{tcp_options, Default} | Options] end. -stop_listener(GwType, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwType, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwType, Type, ListenOnStr]); + [GwName, Type, ListenOnStr]); {error, Reason} -> ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwType, Type, ListenOnStr, Reason]) + [GwName, Type, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwType, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwType, Type), +stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, Type), esockd:close(Name, ListenOn). From 430f20adc116763a23844e2786e55d85f3c339b4 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 17 Aug 2021 14:17:56 +0800 Subject: [PATCH 044/306] fix(gw): fix conn_state --- apps/emqx_gateway/src/emqx_gateway_cli.erl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index 2430b38e6..b446cda92 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -107,7 +107,12 @@ gateway(_) -> 'gateway-clients'(["list", Name]) -> InfoTab = emqx_gateway_cm:tabname(info, Name), - dump(InfoTab, client); + case ets:info(InfoTab) of + undefined -> + emqx_ctl:print("Bad Gateway Name.~n"); + _ -> + dump(InfoTab, client) + end; 'gateway-clients'(["lookup", Name, ClientId]) -> ChanTab = emqx_gateway_cm:tabname(chan, Name), @@ -191,7 +196,7 @@ print({client, {_, Infos, Stats}}) -> keepalive => SafeGet(keepalive, ConnInfo), subscriptions_cnt => StatsGet(subscriptions_cnt), send_msg => StatsGet(send_msg), - connected => SafeGet(conn_state, ClientInfo) == connected, + connected => SafeGet(conn_state, Infos) == connected, created_at => ConnectedAt, connected_at => ConnectedAt }, From b8cb31c0ff9a4a80fe5a4a4e60332745bc225c77 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 17 Aug 2021 14:41:21 +0800 Subject: [PATCH 045/306] fix(gw): fix the bad return value for initiating chain --- apps/emqx_gateway/src/emqx_gateway_insta_sup.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 404766719..2edccd033 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -232,9 +232,9 @@ create_authenticators_for_gateway_insta(GwName, AuthCfgs) -> NResults /= [] andalso begin logger:error("Failed to create authenticators: ~p", [NResults]), throw({bad_autheticators, NResults}) - end, ok; + end, ChainId; {error, Reason} -> - logger:error("Failed to create authenticator chain: ~p", [Reason]), + logger:error("Failed to create authentication chain: ~p", [Reason]), throw({bad_chain, {ChainId, Reason}}) end. @@ -244,10 +244,10 @@ cleanup_authenticators_for_gateway_insta(ChainId) -> case emqx_authn:delete_chain(ChainId) of ok -> ok; {error, {not_found, _}} -> - logger:warning("Failed to clean authenticator chain: ~s, " + logger:warning("Failed to clean authentication chain: ~s, " "reason: not_found", [ChainId]); {error, Reason} -> - logger:error("Failed to clean authenticator chain: ~s, " + logger:error("Failed to clean authentication chain: ~s, " "reason: ~p", [ChainId, Reason]) end. From 9190f1f6f9c2b73e4bfd949bead7e62c8ed2e02c Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 17 Aug 2021 18:14:08 +0800 Subject: [PATCH 046/306] chore(gw): reformat config options --- apps/emqx_gateway/etc/emqx_gateway.conf | 311 ++++++++++++------------ 1 file changed, 152 insertions(+), 159 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 0a5b6065e..795066e79 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -2,171 +2,164 @@ ## EMQ X Gateway configurations ##-------------------------------------------------------------------- -gateway: { +## TODO: These configuration options are temporary example here. +## In the final version, it will be commented out. - stomp: { - - frame: { - max_headers: 10 - max_headers_length: 1024 - max_body_length: 8192 - } - - clientinfo_override: { - username: "${Packet.headers.login}" - password: "${Packet.headers.passcode}" - } - - authentication: { - enable: true - authenticators: [ - { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid - } - ] - } - - listener.tcp.1: { - bind: 61613 - acceptors: 16 - max_connections: 1024000 - max_conn_rate: 1000 - active_n: 100 - } +gateway.stomp { + frame: { + max_headers: 10 + max_headers_length: 1024 + max_body_length: 8192 } - coap: { - - enable_stats: false - - authentication: { - enable: true - authenticators: [ - { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid - } - ] - } - - #authentication.enable: false - - heartbeat: 30s - notify_type: qos - subscribe_qos: qos0 - publish_qos: qos1 - listener.udp.1: { - bind: 5683 - } - } - - mqttsn: { - ## 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 - } + clientinfo_override: { + username: "${Packet.headers.login}" + password: "${Packet.headers.passcode}" } - ## Extension Protocol Gateway - exproto: { - ## The gRPC server to accept requests - server: { - bind: 9100 - #ssl.keyfile: - #ssl.certfile: - #ssl.cacertfile: - } - - handler: { - address: "http://127.0.0.1:9001" - #ssl.keyfile: - #ssl.certfile: - #ssl.cacertfile: - } - - authentication.enable: false - - listener.tcp.1: { - bind: 7993 - acceptors: 8 - max_connections: 10240 - max_conn_rate: 1000 - } - - #listener.ssl.1: {} - #listener.udp.1: {} - #listener.dtls.1: {} + authentication: { + enable: true + authenticators: [ + { + name: "authenticator1" + mechanism: password-based + server_type: built-in-database + user_id_type: clientid + } + ] } - - lwm2m: { - - xml_dir: "{{ platform_etc_dir }}/lwm2m_xml" - - lifetime_min: 1s - - lifetime_max: 86400s - - qmode_time_windonw: 22 - - auto_observe: false - - mountpoint: "lwm2m/%e/" - - ## always | contains_object_list - update_msg_publish_condition: contains_object_list - - translators: { - command: "dn/#" - response: "up/resp" - notify: "up/notify" - register: "up/resp" - update: "up/resp" - } - - listener.udp.1 { - bind: 5783 - } + listener.tcp.1: { + bind: 61613 + acceptors: 16 + max_connections: 1024000 + max_conn_rate: 1000 + active_n: 100 + } +} + +gateway.coap: { + + enable_stats: false + + #authentication.enable: false + authentication: { + enable: true + authenticators: [ + { + name: "authenticator1" + mechanism: password-based + server_type: built-in-database + user_id_type: clientid + } + ] + } + + heartbeat: 30s + notify_type: qos + subscribe_qos: qos0 + publish_qos: qos1 + listener.udp.1: { + bind: 5683 + } +} + +gateway.mqttsn: { + ## 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 + } +} + +gateway.exproto: { + ## The gRPC server to accept requests + server: { + bind: 9100 + #ssl.keyfile: + #ssl.certfile: + #ssl.cacertfile: + } + + handler: { + address: "http://127.0.0.1:9001" + #ssl.keyfile: + #ssl.certfile: + #ssl.cacertfile: + } + + authentication.enable: false + + listener.tcp.1: { + bind: 7993 + acceptors: 8 + max_connections: 10240 + max_conn_rate: 1000 + } + + #listener.ssl.1: {} + #listener.udp.1: {} + #listener.dtls.1: {} +} + +gateway.lwm2m: { + + xml_dir: "{{ platform_etc_dir }}/lwm2m_xml" + + lifetime_min: 1s + lifetime_max: 86400s + qmode_time_windonw: 22 + auto_observe: false + + mountpoint: "lwm2m/%e/" + + ## always | contains_object_list + update_msg_publish_condition: contains_object_list + + translators: { + command: "dn/#" + response: "up/resp" + notify: "up/notify" + register: "up/resp" + update: "up/resp" + } + + listener.udp.1 { + bind: 5783 } } From d3722ea1aa8c97ab112438b6e377e6e14ae61f24 Mon Sep 17 00:00:00 2001 From: Parham Alvani Date: Fri, 13 Aug 2021 18:12:11 +0430 Subject: [PATCH 047/306] feat: Add Chart Icon --- deploy/charts/emqx/Chart.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/charts/emqx/Chart.yaml b/deploy/charts/emqx/Chart.yaml index 0bafd6780..e9b8ae3e9 100644 --- a/deploy/charts/emqx/Chart.yaml +++ b/deploy/charts/emqx/Chart.yaml @@ -1,5 +1,6 @@ apiVersion: v2 name: emqx +icon: https://github.com/emqx.png description: A Helm chart for EMQ X # A chart can be either an 'application' or a 'library' chart. # From a2067c3bf39ada686ff4bda9fb3e351cba14790c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 17 Aug 2021 10:48:29 +0800 Subject: [PATCH 048/306] refactor(spec): move some type specs from emqx_config_handler to emqx_config --- apps/emqx/src/emqx_config.erl | 17 +++++++++++++---- apps/emqx/src/emqx_config_handler.erl | 9 +-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index cff511352..a86d4b309 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -88,8 +88,17 @@ error:badarg -> EXP_ON_FAIL end). --export_type([update_request/0, raw_config/0, config/0]). +-export_type([update_request/0, raw_config/0, config/0, + update_opts/0, update_cmd/0, update_args/0]). + -type update_request() :: term(). +-type update_cmd() :: {update, update_request()} | remove. +-type update_opts() :: #{ + %% fill the default values into the rawconf map + rawconf_with_defaults => boolean() + }. +-type update_args() :: {update_cmd(), Opts :: update_opts()}. + %% raw_config() is the config that is NOT parsed and tranlated by hocon schema -type raw_config() :: #{binary() => term()} | undefined. %% config() is the config that is parsed and tranlated by hocon schema @@ -188,7 +197,7 @@ update(KeyPath, UpdateReq) -> update(KeyPath, UpdateReq, #{}). -spec update(emqx_map_lib:config_key_path(), update_request(), - emqx_config_handler:update_opts()) -> + update_opts()) -> {ok, config(), raw_config()} | {error, term()}. update([RootName | _] = KeyPath, UpdateReq, Opts) -> emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, @@ -198,12 +207,12 @@ update([RootName | _] = KeyPath, UpdateReq, Opts) -> remove(KeyPath) -> remove(KeyPath, #{}). --spec remove(emqx_map_lib:config_key_path(), emqx_config_handler:update_opts()) -> +-spec remove(emqx_map_lib:config_key_path(), update_opts()) -> ok | {error, term()}. remove([RootName | _] = KeyPath, Opts) -> emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, {remove, Opts}). --spec reset(emqx_map_lib:config_key_path(), emqx_config_handler:update_opts()) -> +-spec reset(emqx_map_lib:config_key_path(), update_opts()) -> {ok, config(), raw_config()} | {error, term()}. reset([RootName | _] = KeyPath, Opts) -> case get_default_value(KeyPath) of diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index cae104945..ee03ff4ef 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -38,15 +38,8 @@ -define(MOD, {mod}). --export_type([update_opts/0, update_cmd/0, update_args/0]). -type handler_name() :: module(). -type handlers() :: #{emqx_config:config_key() => handlers(), ?MOD => handler_name()}. --type update_cmd() :: {update, emqx_config:update_request()} | remove. --type update_opts() :: #{ - %% fill the default values into the rawconf map - rawconf_with_defaults => boolean() - }. --type update_args() :: {update_cmd(), Opts :: update_opts()}. -optional_callbacks([ pre_config_update/2 , post_config_update/3 @@ -66,7 +59,7 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []). --spec update_config(module(), emqx_config:config_key_path(), update_args()) -> +-spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) -> {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, UpdateArgs}). From bd8263e3243f172f317cba4d93c541e7d828992f Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 17 Aug 2021 11:01:20 +0800 Subject: [PATCH 049/306] refactor(config): move APIs for config update,remove,reset to emqx Move the emqx_config:update,remove,reset APIs to emqx, to remove the circular dependency between the modules emqx_config and emqx_config_handler. After this change the dependency among these modules will be: ``` emqx ---> emqx_config | ^ | | + ---> emqx_conifg_handler ``` --- apps/emqx/src/emqx.erl | 41 +++++++++++++++++++ apps/emqx/src/emqx_config.erl | 39 ------------------ apps/emqx/test/emqx_alarm_SUITE.erl | 4 +- apps/emqx_authz/src/emqx_authz.erl | 4 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 4 +- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 4 +- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 4 +- .../test/emqx_authz_mongo_SUITE.erl | 4 +- .../test/emqx_authz_mysql_SUITE.erl | 4 +- .../test/emqx_authz_pgsql_SUITE.erl | 4 +- .../test/emqx_authz_redis_SUITE.erl | 4 +- .../emqx_data_bridge/src/emqx_data_bridge.erl | 2 +- .../src/emqx_mgmt_api_configs.erl | 4 +- .../src/emqx_prometheus_api.erl | 2 +- apps/emqx_statsd/src/emqx_statsd_api.erl | 2 +- 15 files changed, 64 insertions(+), 62 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 3a5935a8b..ce9036389 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -55,6 +55,13 @@ -export([ set_debug_secret/1 ]). +-export([ update_config/2 + , update_config/3 + , remove_config/1 + , remove_config/2 + , reset_config/2 + ]). + -define(APP, ?MODULE). %% @hidden Path to the file which has debug_info encryption secret in it. @@ -184,3 +191,37 @@ run_hook(HookPoint, Args) -> -spec(run_fold_hook(emqx_hooks:hookpoint(), list(any()), any()) -> any()). run_fold_hook(HookPoint, Args, Acc) -> emqx_hooks:run_fold(HookPoint, Args, Acc). + +-spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request()) -> + {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. +update_config(KeyPath, UpdateReq) -> + update_config(KeyPath, UpdateReq, #{}). + +-spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request(), + emqx_config:update_opts()) -> + {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. +update_config([RootName | _] = KeyPath, UpdateReq, Opts) -> + emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), KeyPath, + {{update, UpdateReq}, Opts}). + +-spec remove_config(emqx_map_lib:config_key_path()) -> + {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. +remove_config(KeyPath) -> + remove_config(KeyPath, #{}). + +-spec remove_config(emqx_map_lib:config_key_path(), emqx_config:update_opts()) -> + ok | {error, term()}. +remove_config([RootName | _] = KeyPath, Opts) -> + emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), + KeyPath, {remove, Opts}). + +-spec reset_config(emqx_map_lib:config_key_path(), emqx_config:update_opts()) -> + {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. +reset_config([RootName | _] = KeyPath, Opts) -> + case emqx_config:get_default_value(KeyPath) of + {ok, Default} -> + emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), KeyPath, + {{update, Default}, Opts}); + {error, _} = Error -> + Error + end. diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index a86d4b309..1815ad66d 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -61,13 +61,6 @@ , find_listener_conf/3 ]). --export([ update/2 - , update/3 - , remove/1 - , remove/2 - , reset/2 - ]). - -export([ get_raw/1 , get_raw/2 , put_raw/1 @@ -191,38 +184,6 @@ put(Config) -> -spec put(emqx_map_lib:config_key_path(), term()) -> ok. put(KeyPath, Config) -> do_put(?CONF, KeyPath, Config). --spec update(emqx_map_lib:config_key_path(), update_request()) -> - {ok, config(), raw_config()} | {error, term()}. -update(KeyPath, UpdateReq) -> - update(KeyPath, UpdateReq, #{}). - --spec update(emqx_map_lib:config_key_path(), update_request(), - update_opts()) -> - {ok, config(), raw_config()} | {error, term()}. -update([RootName | _] = KeyPath, UpdateReq, Opts) -> - emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, - {{update, UpdateReq}, Opts}). - --spec remove(emqx_map_lib:config_key_path()) -> {ok, config(), raw_config()} | {error, term()}. -remove(KeyPath) -> - remove(KeyPath, #{}). - --spec remove(emqx_map_lib:config_key_path(), update_opts()) -> - ok | {error, term()}. -remove([RootName | _] = KeyPath, Opts) -> - emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, {remove, Opts}). - --spec reset(emqx_map_lib:config_key_path(), update_opts()) -> - {ok, config(), raw_config()} | {error, term()}. -reset([RootName | _] = KeyPath, Opts) -> - case get_default_value(KeyPath) of - {ok, Default} -> - emqx_config_handler:update_config(get_schema_mod(RootName), KeyPath, - {{update, Default}, Opts}); - {error, _} = Error -> - Error - end. - -spec get_default_value(emqx_map_lib:config_key_path()) -> {ok, term()} | {error, term()}. get_default_value([RootName | _] = KeyPath) -> BinKeyPath = [bin(Key) || Key <- KeyPath], diff --git a/apps/emqx/test/emqx_alarm_SUITE.erl b/apps/emqx/test/emqx_alarm_SUITE.erl index e797a91d2..b8af41232 100644 --- a/apps/emqx/test/emqx_alarm_SUITE.erl +++ b/apps/emqx/test/emqx_alarm_SUITE.erl @@ -28,14 +28,14 @@ all() -> emqx_ct:all(?MODULE). init_per_testcase(t_size_limit, Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - {ok, _, _} = emqx_config:update([alarm], #{ + {ok, _, _} = emqx: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([]), - {ok, _, _} = emqx_config:update([alarm], #{ + {ok, _, _} = emqx:update_config([alarm], #{ <<"validity_period">> => <<"1s">> }), Config; diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index d4399b82d..3f8177c51 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -61,10 +61,10 @@ lookup(Id) -> end. move(Id, Position) -> - emqx_config:update(?CONF_KEY_PATH, {move, Id, Position}). + emqx:update_config(?CONF_KEY_PATH, {move, Id, Position}). update(Cmd, Rules) -> - emqx_config:update(?CONF_KEY_PATH, {Cmd, Rules}). + emqx:update_config(?CONF_KEY_PATH, {Cmd, Rules}). pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_rule_by_id(Id), diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index e509542e6..85ef400f7 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -33,8 +33,8 @@ groups() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Config. end_per_suite(_Config) -> diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index b5d5902d7..23bcac31c 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -76,8 +76,8 @@ init_per_suite(Config) -> ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Config. diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index 59fdb1eb2..ff04148d9 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, <<"headers">> => #{}, diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index 8b74d5ec0..5f179c0b0 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -34,8 +34,8 @@ init_per_suite(Config) -> meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"mongo_type">> => <<"single">>, <<"server">> => <<"127.0.0.1:27017">>, diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index cfe64e2fa..61e738041 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index a6f62322c..85e8a360a 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 17947e7d9..cf0694449 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx_config:update([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx_config:update([zones, default, authorization, enable], true), + {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.erl b/apps/emqx_data_bridge/src/emqx_data_bridge.erl index 9c27ff8d5..17527ca3a 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge.erl @@ -60,4 +60,4 @@ config_key_path() -> [emqx_data_bridge, bridges]. update_config(ConfigReq) -> - emqx_config:update(config_key_path(), ConfigReq). + emqx:update_config(config_key_path(), ConfigReq). diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index f8aab5ddd..9a647e174 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -113,14 +113,14 @@ config(get, Req) -> config(put, Req) -> Path = conf_path(Req), - {ok, _, RawConf} = emqx_config:update(Path, http_body(Req), + {ok, _, RawConf} = emqx:update_config(Path, http_body(Req), #{rawconf_with_defaults => true}), {200, emqx_map_lib:deep_get(Path, emqx_map_lib:jsonable_map(RawConf))}. config_reset(post, Req) -> %% reset the config specified by the query string param 'conf_path' Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req), - case emqx_config:reset(Path, #{}) of + case emqx:reset_config(Path, #{}) of {ok, _, _} -> {200}; {error, Reason} -> {400, ?ERR_MSG(Reason)} diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 555311cdc..15762f5de 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -113,7 +113,7 @@ prometheus(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - {ok, _, _} = emqx_config:update([prometheus], Params), + {ok, _, _} = emqx:update_config([prometheus], Params), enable_prometheus(Enable). % stats(_Bindings, Params) -> diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index d749e26e4..71e72d924 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -91,7 +91,7 @@ statsd(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - {ok, _, _} = emqx_config:update([statsd], Params), + {ok, _, _} = emqx:update_config([statsd], Params), enable_statsd(Enable). enable_statsd(true) -> From 24207b80cb59ac2b7659f63b9a2eb410cc39d953 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 17 Aug 2021 17:25:19 +0800 Subject: [PATCH 050/306] refactor(config): improve the return value of emqx:update_config/2,3 --- apps/emqx/src/emqx_config.erl | 10 ++- apps/emqx/src/emqx_config_handler.erl | 102 ++++++++++++++++++-------- 2 files changed, 79 insertions(+), 33 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 1815ad66d..d5bf1a61e 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -82,7 +82,8 @@ end). -export_type([update_request/0, raw_config/0, config/0, - update_opts/0, update_cmd/0, update_args/0]). + update_opts/0, update_cmd/0, update_args/0, + update_error/0, update_result/0]). -type update_request() :: term(). -type update_cmd() :: {update, update_request()} | remove. @@ -91,6 +92,13 @@ rawconf_with_defaults => boolean() }. -type update_args() :: {update_cmd(), Opts :: update_opts()}. +-type update_stage() :: pre_config_update | post_config_update. +-type update_error() :: {update_stage(), module(), term()} | {save_configs, term()} | term(). +-type update_result() :: #{ + config := emqx_config:config(), + raw_config := emqx_config:raw_config(), + post_config_update => #{module() => any()} +}. %% raw_config() is the config that is NOT parsed and tranlated by hocon schema -type raw_config() :: #{binary() => term()} | undefined. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index ee03ff4ef..ac21afaa1 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -46,10 +46,10 @@ ]). -callback pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> - emqx_config:update_request(). + {ok, emqx_config:update_request()} | {error, term()}. -callback post_config_update(emqx_config:update_request(), emqx_config:config(), - emqx_config:config()) -> any(). + emqx_config:config()) -> ok | {ok, Result::any()} | {error, Reason::term()}. -type state() :: #{ handlers := handlers(), @@ -60,7 +60,7 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []). -spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) -> - {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, UpdateArgs}). @@ -79,24 +79,23 @@ handle_call({add_child, ConfKeyPath, HandlerName}, _From, {reply, ok, State#{handlers => emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; -handle_call({change_config, SchemaModule, ConfKeyPath, {_Cmd, Opts} = UpdateArgs}, _From, +handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> OldConf = emqx_config:get([]), OldRawConf = emqx_config:get_raw([]), - Result = try - {NewRawConf, OverrideConf} = process_upadate_request(ConfKeyPath, OldRawConf, - Handlers, UpdateArgs), - {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf), - _ = do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateArgs), - case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of - ok -> {ok, emqx_config:get([]), return_rawconf(Opts)}; - Err -> Err + Reply = try + case process_update_request(ConfKeyPath, OldRawConf, Handlers, UpdateArgs) of + {ok, NewRawConf, OverrideConf} -> + check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OldConf, + OverrideConf, UpdateArgs); + {error, Result} -> + {error, Result} end catch Error:Reason:ST -> ?LOG(error, "change_config failed: ~p", [{Error, Reason, ST}]), {error, Reason} end, - {reply, Result, State}; + {reply, Reply, State}; handle_call(_Request, _From, State) -> Reply = ok, @@ -114,32 +113,56 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -process_upadate_request(ConfKeyPath, OldRawConf, _Handlers, {remove, _Opts}) -> +process_update_request(ConfKeyPath, OldRawConf, _Handlers, {remove, _Opts}) -> BinKeyPath = bin_path(ConfKeyPath), NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf), OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()), - {NewRawConf, OverrideConf}; -process_upadate_request(ConfKeyPath, OldRawConf, Handlers, {{update, UpdateReq}, _Opts}) -> - NewRawConf = do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq), - OverrideConf = update_override_config(NewRawConf), - {NewRawConf, OverrideConf}. + {ok, NewRawConf, OverrideConf}; +process_update_request(ConfKeyPath, OldRawConf, Handlers, {{update, UpdateReq}, _Opts}) -> + case do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) of + {ok, NewRawConf} -> + OverrideConf = update_override_config(NewRawConf), + {ok, NewRawConf, OverrideConf}; + Error -> Error + end. do_update_config([], Handlers, OldRawConf, UpdateReq) -> call_pre_config_update(Handlers, OldRawConf, UpdateReq); do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) -> SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf), SubHandlers = maps:get(ConfKey, Handlers, #{}), - NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq), - call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}). + case do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq) of + {ok, NewUpdateReq} -> + call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}); + Error -> + Error + end. -do_post_config_update([], Handlers, OldConf, NewConf, UpdateArgs) -> - call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs)); -do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, UpdateArgs) -> +check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OldConf, OverrideConf, + UpdateArgs) -> + {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf), + case do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateArgs, #{}) of + {ok, Result0} -> + case save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs) of + {ok, Result1} -> + {ok, Result1#{post_config_update => Result0}}; + Error -> Error + end; + Error -> Error + end. + +do_post_config_update([], Handlers, OldConf, NewConf, UpdateArgs, Result) -> + call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs), Result); +do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, UpdateArgs, Result) -> SubOldConf = get_sub_config(ConfKey, OldConf), SubNewConf = get_sub_config(ConfKey, NewConf), SubHandlers = maps:get(ConfKey, Handlers, #{}), - _ = do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, UpdateArgs), - call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs)). + case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, UpdateArgs, + Result) of + {ok, Result1} -> + call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs), Result1); + Error -> Error + end. get_sub_config(ConfKey, Conf) when is_map(Conf) -> maps:get(ConfKey, Conf, undefined); @@ -149,15 +172,30 @@ get_sub_config(_, _Conf) -> %% the Conf is a primitive call_pre_config_update(Handlers, OldRawConf, UpdateReq) -> HandlerName = maps:get(?MOD, Handlers, undefined), case erlang:function_exported(HandlerName, pre_config_update, 2) of - true -> HandlerName:pre_config_update(UpdateReq, OldRawConf); + true -> + case HandlerName:pre_config_update(UpdateReq, OldRawConf) of + {ok, NewUpdateReq} -> {ok, NewUpdateReq}; + {error, Reason} -> {error, {pre_config_update, HandlerName, Reason}} + end; false -> merge_to_old_config(UpdateReq, OldRawConf) end. -call_post_config_update(Handlers, OldConf, NewConf, UpdateReq) -> +call_post_config_update(Handlers, OldConf, NewConf, UpdateReq, Result) -> HandlerName = maps:get(?MOD, Handlers, undefined), case erlang:function_exported(HandlerName, post_config_update, 3) of - true -> HandlerName:post_config_update(UpdateReq, NewConf, OldConf); - false -> ok + true -> + case HandlerName:post_config_update(UpdateReq, NewConf, OldConf) of + ok -> {ok, Result}; + {ok, Result1} -> {ok, Result#{HandlerName => Result1}}; + {error, Reason} -> {error, {post_config_update, HandlerName, Reason}} + end; + false -> {ok, Result} + end. + +save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf, {_Cmd, Opts}) -> + case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of + ok -> {ok, #{config => emqx_config:get([]), raw_config => return_rawconf(Opts)}}; + {error, Reason} -> {error, {save_configs, Reason}} end. %% The default callback of config handlers @@ -166,9 +204,9 @@ call_post_config_update(Handlers, OldConf, NewConf, UpdateReq) -> %% 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); + {ok, maps:merge(RawConf, UpdateReq)}; merge_to_old_config(UpdateReq, _RawConf) -> - UpdateReq. + {ok, UpdateReq}. update_override_config(RawConf) -> OldConf = emqx_config:read_override_conf(), From e5c3199d6eb7ea6fe65f705cf7d189fbdc81a901 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 17 Aug 2021 19:30:43 +0800 Subject: [PATCH 051/306] fix(config): emqx:update_config/2,3 doesn't work on binary conf paths --- apps/emqx/src/emqx.erl | 10 +++++----- apps/emqx/src/emqx_config_handler.erl | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index ce9036389..5df45bd15 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -193,30 +193,30 @@ run_fold_hook(HookPoint, Args, Acc) -> emqx_hooks:run_fold(HookPoint, Args, Acc). -spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request()) -> - {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(KeyPath, UpdateReq) -> update_config(KeyPath, UpdateReq, #{}). -spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request(), emqx_config:update_opts()) -> - {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config([RootName | _] = KeyPath, UpdateReq, Opts) -> emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), KeyPath, {{update, UpdateReq}, Opts}). -spec remove_config(emqx_map_lib:config_key_path()) -> - {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. remove_config(KeyPath) -> remove_config(KeyPath, #{}). -spec remove_config(emqx_map_lib:config_key_path(), emqx_config:update_opts()) -> - ok | {error, term()}. + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. remove_config([RootName | _] = KeyPath, Opts) -> emqx_config_handler:update_config(emqx_config:get_schema_mod(RootName), KeyPath, {remove, Opts}). -spec reset_config(emqx_map_lib:config_key_path(), emqx_config:update_opts()) -> - {ok, emqx_config:config(), emqx_config:raw_config()} | {error, term()}. + {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. reset_config([RootName | _] = KeyPath, Opts) -> case emqx_config:get_default_value(KeyPath) of {ok, Default} -> diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index ac21afaa1..2ee90cb04 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -38,6 +38,13 @@ -define(MOD, {mod}). +-define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL), + try [safe_atom(Key) || Key <- PATH] of + AtomKeyPath -> EXP + catch + error:badarg -> EXP_ON_FAIL + end). + -type handler_name() :: module(). -type handlers() :: #{emqx_config:config_key() => handlers(), ?MOD => handler_name()}. @@ -62,7 +69,8 @@ start_link() -> -spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) -> {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> - gen_server:call(?MODULE, {change_config, SchemaModule, ConfKeyPath, UpdateArgs}). + ?ATOM_CONF_PATH(ConfKeyPath, gen_server:call(?MODULE, {change_config, SchemaModule, + AtomKeyPath, UpdateArgs}), {error, ConfKeyPath}). -spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok. add_handler(ConfKeyPath, HandlerName) -> @@ -224,3 +232,10 @@ bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath]. bin(A) when is_atom(A) -> atom_to_binary(A, utf8); bin(B) when is_binary(B) -> B. + +safe_atom(Bin) when is_binary(Bin) -> + binary_to_existing_atom(Bin, latin1); +safe_atom(Str) when is_list(Str) -> + list_to_existing_atom(Str); +safe_atom(Atom) when is_atom(Atom) -> + Atom. \ No newline at end of file From bf6251e20fa4cd6b5ef2a7972c218a1bd8511406 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 17 Aug 2021 19:49:13 +0800 Subject: [PATCH 052/306] refactor(config): update the return values of config handlers --- apps/emqx/src/emqx_alarm.erl | 13 +++------ apps/emqx/test/emqx_alarm_SUITE.erl | 4 +-- apps/emqx_authz/src/emqx_authz.erl | 24 ++++++++-------- apps/emqx_authz/src/emqx_authz_api.erl | 10 +++---- apps/emqx_authz/test/emqx_authz_SUITE.erl | 28 +++++++++---------- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 8 +++--- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 8 +++--- .../test/emqx_authz_mongo_SUITE.erl | 8 +++--- .../test/emqx_authz_mysql_SUITE.erl | 8 +++--- .../test/emqx_authz_pgsql_SUITE.erl | 8 +++--- .../test/emqx_authz_redis_SUITE.erl | 8 +++--- .../src/emqx_data_bridge_api.erl | 4 +-- .../src/emqx_data_bridge_app.erl | 6 ++-- .../src/emqx_mgmt_api_configs.erl | 4 +-- .../src/emqx_prometheus_api.erl | 2 +- apps/emqx_statsd/src/emqx_statsd_api.erl | 2 +- 16 files changed, 70 insertions(+), 75 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index ab3e2d702..44b005faa 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -28,7 +28,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --export([pre_config_update/2]). +-export([post_config_update/3]). -export([ start_link/0 , stop/0 @@ -151,14 +151,9 @@ get_alarms(activated) -> get_alarms(deactivated) -> gen_server:call(?MODULE, {get_alarms, deactivated}). -pre_config_update(#{<<"validity_period">> := Period0} = NewConf, OldConf) -> - ?MODULE ! {update_timer, hocon_postprocess:duration(Period0)}, - merge(OldConf, NewConf); -pre_config_update(NewConf, OldConf) -> - merge(OldConf, NewConf). - -merge(undefined, New) -> New; -merge(Old, New) -> maps:merge(Old, New). +post_config_update(_, #{validity_period := Period0}, _OldConf) -> + ?MODULE ! {update_timer, Period0}, + ok. format(#activated_alarm{name = Name, message = Message, activate_at = At, details = Details}) -> Now = erlang:system_time(microsecond), diff --git a/apps/emqx/test/emqx_alarm_SUITE.erl b/apps/emqx/test/emqx_alarm_SUITE.erl index b8af41232..605300d2f 100644 --- a/apps/emqx/test/emqx_alarm_SUITE.erl +++ b/apps/emqx/test/emqx_alarm_SUITE.erl @@ -28,14 +28,14 @@ all() -> emqx_ct:all(?MODULE). init_per_testcase(t_size_limit, Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - {ok, _, _} = emqx:update_config([alarm], #{ + {ok, _} = emqx: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([]), - {ok, _, _} = emqx:update_config([alarm], #{ + {ok, _} = emqx:update_config([alarm], #{ <<"validity_period">> => <<"1s">> }), Config; diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 3f8177c51..6197cd685 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -69,12 +69,12 @@ update(Cmd, Rules) -> pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_rule_by_id(Id), {List1, List2} = lists:split(Index, Conf), - [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2; + {ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2}; pre_config_update({move, Id, <<"bottom">>}, Conf) when is_list(Conf) -> {Index, _} = find_rule_by_id(Id), {List1, List2} = lists:split(Index, Conf), - lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]; + {ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]}; pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Conf) -> {Index1, _} = find_rule_by_id(Id), @@ -83,9 +83,9 @@ pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Co Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), - lists:delete(Conf1, lists:droplast(List1)) - ++ [Conf1] ++ [Conf2] - ++ lists:delete(Conf1, List2); + {ok, lists:delete(Conf1, lists:droplast(List1)) + ++ [Conf1] ++ [Conf2] + ++ lists:delete(Conf1, List2)}; pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf) -> {Index1, _} = find_rule_by_id(Id), @@ -93,21 +93,21 @@ pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf {Index2, _} = find_rule_by_id(AfterId), {List1, List2} = lists:split(Index2, Conf), - lists:delete(Conf1, List1) - ++ [Conf1] - ++ lists:delete(Conf1, List2); + {ok, lists:delete(Conf1, List1) + ++ [Conf1] + ++ lists:delete(Conf1, List2)}; pre_config_update({head, Rules}, Conf) when is_list(Rules), is_list(Conf) -> - Rules ++ Conf; + {ok, Rules ++ Conf}; pre_config_update({tail, Rules}, Conf) when is_list(Rules), is_list(Conf) -> - Conf ++ Rules; + {ok, Conf ++ Rules}; pre_config_update({{replace_once, Id}, Rule}, Conf) when is_map(Rule), is_list(Conf) -> {Index, _} = find_rule_by_id(Id), {List1, List2} = lists:split(Index, Conf), - lists:droplast(List1) ++ [Rule] ++ List2; + {ok, lists:droplast(List1) ++ [Rule] ++ List2}; pre_config_update({_, Rules}, _Conf) when is_list(Rules)-> %% overwrite the entire config! - Rules. + {ok, Rules}. post_config_update(_, undefined, _Conf) -> ok; diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 21519153a..a445c7016 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -449,7 +449,7 @@ rules(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update(head, [RawConfig]) of - {ok, _, _} -> {204}; + {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} @@ -458,7 +458,7 @@ rules(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update(replace, RawConfig) of - {ok, _, _} -> {204}; + {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} @@ -486,7 +486,7 @@ rule(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update({replace_once, RuleId}, RawConfig) of - {ok, _, _} -> {204}; + {ok, _} -> {204}; {error, not_found_rule} -> {404, #{code => <<"NOT_FOUND">>, messgae => <<"rule ", RuleId/binary, " not found">>}}; @@ -497,7 +497,7 @@ rule(put, Request) -> rule(delete, Request) -> RuleId = cowboy_req:binding(id, Request), case emqx_authz:update({replace_once, RuleId}, #{}) of - {ok, _, _} -> {204}; + {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} @@ -507,7 +507,7 @@ move_rule(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), #{<<"position">> := Position} = jsx:decode(Body, [return_maps]), case emqx_authz:move(RuleId, Position) of - {ok, _, _} -> {204}; + {ok, _} -> {204}; {error, not_found_rule} -> {404, #{code => <<"NOT_FOUND">>, messgae => <<"rule ", RuleId/binary, " not found">>}}; diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 85ef400f7..514b0d48b 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -33,17 +33,17 @@ groups() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz]), ok. init_per_testcase(_, Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), Config. -define(RULE1, #{<<"principal">> => <<"all">>, @@ -82,9 +82,9 @@ init_per_testcase(_, Config) -> %%------------------------------------------------------------------------------ t_update_rule(_) -> - {ok, _, _} = emqx_authz:update(replace, [?RULE2]), - {ok, _, _} = emqx_authz:update(head, [?RULE1]), - {ok, _, _} = emqx_authz:update(tail, [?RULE3]), + {ok, _} = emqx_authz:update(replace, [?RULE2]), + {ok, _} = emqx_authz:update(head, [?RULE1]), + {ok, _} = emqx_authz:update(tail, [?RULE3]), Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), ?assertMatch(Lists1, emqx_config:get([authorization, rules], [])), @@ -107,7 +107,7 @@ t_update_rule(_) -> } ] = emqx_authz:lookup(), - {ok, _, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), + {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), ?assertMatch(Lists2, emqx_config:get([authorization, rules], [])), @@ -132,38 +132,38 @@ t_update_rule(_) -> } ] = emqx_authz:lookup(), - {ok, _, _} = emqx_authz:update(replace, []). + {ok, _} = emqx_authz:update(replace, []). t_move_rule(_) -> - {ok, _, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, #{annotations := #{id := Id4}} ] = emqx_authz:lookup(), - {ok, _, _} = emqx_authz:move(Id4, <<"top">>), + {ok, _} = emqx_authz:move(Id4, <<"top">>), ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}} ], emqx_authz:lookup()), - {ok, _, _} = emqx_authz:move(Id1, <<"bottom">>), + {ok, _} = emqx_authz:move(Id1, <<"bottom">>), ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), - {ok, _, _} = emqx_authz:move(Id3, #{<<"before">> => Id4}), + {ok, _} = emqx_authz:move(Id3, #{<<"before">> => Id4}), ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), - {ok, _, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}), + {ok, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}), ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, #{annotations := #{id := Id1}}, diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 23bcac31c..c4930ace6 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -76,13 +76,13 @@ init_per_suite(Config) -> ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_management]), ok. @@ -155,7 +155,7 @@ t_api(_) -> ok. t_move_rule(_) -> - {ok, _, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index ff04148d9..d50c7a43d 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, <<"headers">> => #{}, @@ -46,11 +46,11 @@ init_per_suite(Config) -> <<"principal">> => <<"all">>, <<"type">> => <<"http">>} ], - {ok, _, _} = emqx_authz:update(replace, Rules), + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index 5f179c0b0..ba18cb1cb 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -34,8 +34,8 @@ init_per_suite(Config) -> meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"mongo_type">> => <<"single">>, <<"server">> => <<"127.0.0.1:27017">>, @@ -47,11 +47,11 @@ init_per_suite(Config) -> <<"find">> => #{<<"a">> => <<"b">>}, <<"type">> => <<"mongo">>} ], - {ok, _, _} = emqx_authz:update(replace, Rules), + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), ok. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 61e738041..81021f6e8 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -49,11 +49,11 @@ init_per_suite(Config) -> <<"principal">> => <<"all">>, <<"sql">> => <<"abcb">>, <<"type">> => <<"mysql">> }], - {ok, _, _} = emqx_authz:update(replace, Rules), + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 85e8a360a..7f7c236d0 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -48,11 +48,11 @@ init_per_suite(Config) -> }, <<"sql">> => <<"abcb">>, <<"type">> => <<"pgsql">> }], - {ok, _, _} = emqx_authz:update(replace, Rules), + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index cf0694449..f0a571dd8 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -35,8 +35,8 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), + {ok, _} = emqx:update_config([zones, default, authorization, enable], true), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -47,11 +47,11 @@ init_per_suite(Config) -> }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">>, <<"type">> => <<"redis">> }], - {ok, _, _} = emqx_authz:update(replace, Rules), + {ok, _} = emqx_authz:update(replace, Rules), Config. end_per_suite(_Config) -> - {ok, _, _} = emqx_authz:update(replace, []), + {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl index 2039ab61f..dea3dcae8 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl @@ -124,7 +124,7 @@ format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := St update_config_and_reply(Name, BridgeType, Config, Data) -> case emqx_data_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of - {ok, _, _} -> + {ok, _} -> {200, #{code => 0, data => format_api_reply( emqx_resource_api:format_data(Data))}}; {error, Reason} -> @@ -133,7 +133,7 @@ update_config_and_reply(Name, BridgeType, Config, Data) -> delete_config_and_reply(Name) -> case emqx_data_bridge:update_config({delete, Name}) of - {ok, _, _} -> {200, #{code => 0, data => #{}}}; + {ok, _} -> {200, #{code => 0, data => #{}}}; {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} end. 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 26128841b..859952480 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl @@ -32,12 +32,12 @@ stop(_State) -> %% internal functions pre_config_update({update, Bridge = #{<<"name">> := Name}}, OldConf) -> - [Bridge | remove_bridge(Name, OldConf)]; + {ok, [Bridge | remove_bridge(Name, OldConf)]}; pre_config_update({delete, Name}, OldConf) -> - remove_bridge(Name, OldConf); + {ok, remove_bridge(Name, OldConf)}; pre_config_update(NewConf, _OldConf) when is_list(NewConf) -> %% overwrite the entire config! - NewConf. + {ok, NewConf}. remove_bridge(_Name, undefined) -> []; diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 9a647e174..2c01276ce 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -113,7 +113,7 @@ config(get, Req) -> config(put, Req) -> Path = conf_path(Req), - {ok, _, RawConf} = emqx:update_config(Path, http_body(Req), + {ok, #{raw_config := RawConf}} = emqx:update_config(Path, http_body(Req), #{rawconf_with_defaults => true}), {200, emqx_map_lib:deep_get(Path, emqx_map_lib:jsonable_map(RawConf))}. @@ -121,7 +121,7 @@ config_reset(post, Req) -> %% reset the config specified by the query string param 'conf_path' Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req), case emqx:reset_config(Path, #{}) of - {ok, _, _} -> {200}; + {ok, _} -> {200}; {error, Reason} -> {400, ?ERR_MSG(Reason)} end. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 15762f5de..63b8b6a10 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -113,7 +113,7 @@ prometheus(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - {ok, _, _} = emqx:update_config([prometheus], Params), + {ok, _} = emqx:update_config([prometheus], Params), enable_prometheus(Enable). % stats(_Bindings, Params) -> diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index 71e72d924..25f0d1879 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -91,7 +91,7 @@ statsd(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - {ok, _, _} = emqx:update_config([statsd], Params), + {ok, _} = emqx:update_config([statsd], Params), enable_statsd(Enable). enable_statsd(true) -> From a1da519d5547f55a68c6aa02b57d33aafde49f45 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 18 Aug 2021 09:19:13 +0800 Subject: [PATCH 053/306] fix(rewrite): use emqx:update_config/2 instead of emqx_config:update/2 --- apps/emqx_modules/src/emqx_rewrite.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_modules/src/emqx_rewrite.erl b/apps/emqx_modules/src/emqx_rewrite.erl index a7e98bbea..ec5a7d5ba 100644 --- a/apps/emqx_modules/src/emqx_rewrite.erl +++ b/apps/emqx_modules/src/emqx_rewrite.erl @@ -56,7 +56,8 @@ list() -> update(Rules0) -> Rewrite = emqx_config:get_raw([<<"rewrite">>], #{}), - {ok, Config, _} = emqx_config:update([rewrite], maps:put(<<"rules">>, Rules0, Rewrite)), + {ok, #{config := Config}} = emqx:update_config([rewrite], maps:put(<<"rules">>, + Rules0, Rewrite)), Rules = maps:get(rules, maps:get(rewrite, Config, #{}), []), case Rules of [] -> From 5f6bcd1ebbeff197ca923435c113df3442581c39 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 18 Aug 2021 10:40:58 +0800 Subject: [PATCH 054/306] fix(config_api): remove config APIs that already been provided by apps --- apps/emqx_management/src/emqx_mgmt_api_configs.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 2c01276ce..15849c294 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -48,12 +48,15 @@ -define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))). +-define(CORE_CONFS, [node, log, alarm, zones, cluster, rpc, broker, sysmon, + emqx_dashboard, emqx_management]). + api_spec() -> {config_apis() ++ [config_reset_api()], []}. config_apis() -> [config_api(ConfPath, Schema) || {ConfPath, Schema} <- - get_conf_schema(emqx_config:get([]), ?MAX_DEPTH)]. + get_conf_schema(emqx_config:get([]), ?MAX_DEPTH), is_core_conf(ConfPath)]. config_api(ConfPath, Schema) -> Path = path_join(ConfPath), @@ -193,6 +196,9 @@ path_join([P], _Sp) -> str(P); path_join([P | Path], Sp) -> str(P) ++ Sp ++ path_join(Path, Sp). +is_core_conf(Path) -> + lists:member(hd(Path), ?CORE_CONFS). + str(S) when is_list(S) -> S; str(S) when is_binary(S) -> binary_to_list(S); str(S) when is_atom(S) -> atom_to_list(S). From e8e95d39ef1615058a05f00168c7f71f96fb054e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 18 Aug 2021 14:52:57 +0800 Subject: [PATCH 055/306] refactor(config): move emqx_config:get/get_raw to emqx:get_config/get_raw_config (#5517) --- apps/emqx/src/emqx.erl | 23 ++++++++++++++++++- apps/emqx/src/emqx_alarm.erl | 8 +++---- apps/emqx/src/emqx_broker.erl | 2 +- apps/emqx/src/emqx_cm_locker.erl | 2 +- apps/emqx/src/emqx_cm_registry.erl | 2 +- apps/emqx/src/emqx_config.erl | 12 +++++----- apps/emqx/src/emqx_connection.erl | 2 +- apps/emqx/src/emqx_flapping.erl | 2 +- apps/emqx/src/emqx_global_gc.erl | 2 +- apps/emqx/src/emqx_listeners.erl | 2 +- apps/emqx/src/emqx_os_mon.erl | 8 +++---- apps/emqx/src/emqx_plugins.erl | 2 +- apps/emqx/src/emqx_router.erl | 2 +- apps/emqx/src/emqx_rpc.erl | 2 +- apps/emqx/src/emqx_shared_sub.erl | 4 ++-- apps/emqx/src/emqx_sys.erl | 4 ++-- apps/emqx/src/emqx_sys_mon.erl | 2 +- apps/emqx/src/emqx_trie.erl | 2 +- apps/emqx/src/emqx_vm_mon.erl | 6 ++--- apps/emqx/test/emqx_mqtt_caps_SUITE.erl | 4 ++-- apps/emqx_authn/src/emqx_authn_app.erl | 2 +- apps/emqx_authz/src/emqx_authz.erl | 2 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 4 ++-- .../src/emqx_bridge_mqtt_sup.erl | 2 +- apps/emqx_dashboard/src/emqx_dashboard.erl | 2 +- .../src/emqx_dashboard_admin.erl | 2 +- .../src/emqx_dashboard_collection.erl | 4 ++-- .../src/emqx_dashboard_token.erl | 2 +- .../emqx_data_bridge/src/emqx_data_bridge.erl | 2 +- apps/emqx_exhook/src/emqx_exhook_sup.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_app.erl | 2 +- apps/emqx_management/src/emqx_mgmt.erl | 2 +- .../src/emqx_mgmt_api_configs.erl | 4 ++-- apps/emqx_management/src/emqx_mgmt_auth.erl | 2 +- apps/emqx_management/src/emqx_mgmt_http.erl | 2 +- apps/emqx_modules/src/emqx_delayed.erl | 2 +- apps/emqx_modules/src/emqx_delayed_api.erl | 2 +- apps/emqx_modules/src/emqx_event_message.erl | 4 ++-- apps/emqx_modules/src/emqx_modules_app.erl | 12 +++++----- apps/emqx_modules/src/emqx_rewrite.erl | 6 ++--- apps/emqx_modules/src/emqx_telemetry.erl | 4 ++-- apps/emqx_modules/src/emqx_topic_metrics.erl | 2 +- .../src/emqx_plugin_libs_ssl.erl | 4 ++-- .../src/emqx_prometheus_api.erl | 6 ++--- .../src/emqx_prometheus_app.erl | 4 ++-- apps/emqx_retainer/src/emqx_retainer.erl | 18 +++++++-------- apps/emqx_retainer/src/emqx_retainer_api.erl | 2 +- .../src/emqx_retainer_mnesia.erl | 4 ++-- .../src/emqx_bridge_mqtt_actions.erl | 2 +- .../emqx_rule_engine/src/emqx_rule_events.erl | 2 +- apps/emqx_statsd/src/emqx_statsd_api.erl | 6 ++--- apps/emqx_statsd/src/emqx_statsd_app.erl | 4 ++-- 52 files changed, 117 insertions(+), 96 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 5df45bd15..1d4686561 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -55,7 +55,12 @@ -export([ set_debug_secret/1 ]). --export([ update_config/2 +%% Configs APIs +-export([ get_config/1 + , get_config/2 + , get_raw_config/1 + , get_raw_config/2 + , update_config/2 , update_config/3 , remove_config/1 , remove_config/2 @@ -192,6 +197,22 @@ run_hook(HookPoint, Args) -> run_fold_hook(HookPoint, Args, Acc) -> emqx_hooks:run_fold(HookPoint, Args, Acc). +-spec get_config(emqx_map_lib:config_key_path()) -> term(). +get_config(KeyPath) -> + emqx_config:get(KeyPath). + +-spec get_config(emqx_map_lib:config_key_path(), term()) -> term(). +get_config(KeyPath, Default) -> + emqx_config:get(KeyPath, Default). + +-spec get_raw_config(emqx_map_lib:config_key_path()) -> term(). +get_raw_config(KeyPath) -> + emqx_config:get_raw(KeyPath). + +-spec get_raw_config(emqx_map_lib:config_key_path(), term()) -> term(). +get_raw_config(KeyPath, Default) -> + emqx_config:get_raw(KeyPath, Default). + -spec update_config(emqx_map_lib:config_key_path(), emqx_config:update_request()) -> {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(KeyPath, UpdateReq) -> diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 44b005faa..e52328d66 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -199,7 +199,7 @@ handle_call({activate_alarm, Name, Details}, _From, State) -> message = normalize_message(Name, Details), activate_at = erlang:system_time(microsecond)}, ekka_mnesia:dirty_write(?ACTIVATED_ALARM, Alarm), - do_actions(activate, Alarm, emqx_config:get([alarm, actions])), + do_actions(activate, Alarm, emqx:get_config([alarm, actions])), {reply, ok, State} end; @@ -268,11 +268,11 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ get_validity_period() -> - emqx_config:get([alarm, validity_period]). + emqx:get_config([alarm, validity_period]). deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name, details = Details0, message = Msg0}) -> - SizeLimit = emqx_config:get([alarm, size_limit]), + SizeLimit = emqx:get_config([alarm, size_limit]), case SizeLimit > 0 andalso (mnesia:table_info(?DEACTIVATED_ALARM, size) >= SizeLimit) of true -> case mnesia:dirty_first(?DEACTIVATED_ALARM) of @@ -289,7 +289,7 @@ deactivate_alarm(Details, #activated_alarm{activate_at = ActivateAt, name = Name erlang:system_time(microsecond)), ekka_mnesia:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), ekka_mnesia:dirty_delete(?ACTIVATED_ALARM, Name), - do_actions(deactivate, DeActAlarm, emqx_config:get([alarm, actions])). + do_actions(deactivate, DeActAlarm, emqx:get_config([alarm, actions])). make_deactivated_alarm(ActivateAt, Name, Details, Message, DeActivateAt) -> #deactivated_alarm{ diff --git a/apps/emqx/src/emqx_broker.erl b/apps/emqx/src/emqx_broker.erl index 1248f9980..46accb9fe 100644 --- a/apps/emqx/src/emqx_broker.erl +++ b/apps/emqx/src/emqx_broker.erl @@ -242,7 +242,7 @@ route(Routes, Delivery) -> do_route({To, Node}, Delivery) when Node =:= node() -> {Node, To, dispatch(To, Delivery)}; do_route({To, Node}, Delivery) when is_atom(Node) -> - {Node, To, forward(Node, To, Delivery, emqx_config:get([rpc, mode]))}; + {Node, To, forward(Node, To, Delivery, emqx:get_config([rpc, mode]))}; do_route({To, Group}, Delivery) when is_tuple(Group); is_binary(Group) -> {share, To, emqx_shared_sub:dispatch(Group, To, Delivery)}. diff --git a/apps/emqx/src/emqx_cm_locker.erl b/apps/emqx/src/emqx_cm_locker.erl index c1a85d6c9..5a336d61c 100644 --- a/apps/emqx/src/emqx_cm_locker.erl +++ b/apps/emqx/src/emqx_cm_locker.erl @@ -62,5 +62,5 @@ unlock(ClientId) -> -spec(strategy() -> local | leader | quorum | all). strategy() -> - emqx_config:get([broker, session_locking_strategy]). + emqx:get_config([broker, session_locking_strategy]). diff --git a/apps/emqx/src/emqx_cm_registry.erl b/apps/emqx/src/emqx_cm_registry.erl index c04f6ccaf..9326d2b8e 100644 --- a/apps/emqx/src/emqx_cm_registry.erl +++ b/apps/emqx/src/emqx_cm_registry.erl @@ -65,7 +65,7 @@ start_link() -> %% @doc Is the global registry enabled? -spec(is_enabled() -> boolean()). is_enabled() -> - emqx_config:get([broker, enable_session_registry]). + emqx:get_config([broker, enable_session_registry]). %% @doc Register a global channel. -spec(register_channel(emqx_types:clientid() diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index d5bf1a61e..056929123 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -43,6 +43,12 @@ , put/2 ]). +-export([ get_raw/1 + , get_raw/2 + , put_raw/1 + , put_raw/2 + ]). + -export([ save_schema_mod_and_names/1 , get_schema_mod/0 , get_schema_mod/1 @@ -61,12 +67,6 @@ , find_listener_conf/3 ]). --export([ get_raw/1 - , get_raw/2 - , put_raw/1 - , put_raw/2 - ]). - -define(CONF, conf). -define(RAW_CONF, raw_conf). -define(PERSIS_SCHEMA_MODS, {?MODULE, schema_mods}). diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index ac66c4daf..dcb50fa4e 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -905,7 +905,7 @@ get_state(Pid) -> tl(tuple_to_list(State)))). get_active_n(Zone, Listener) -> - case emqx_config:get([zones, Zone, listeners, Listener, type]) of + case emqx:get_config([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_flapping.erl b/apps/emqx/src/emqx_flapping.erl index c4a523669..1908430be 100644 --- a/apps/emqx/src/emqx_flapping.erl +++ b/apps/emqx/src/emqx_flapping.erl @@ -160,4 +160,4 @@ start_timer(Zone) -> start_timers() -> lists:foreach(fun({Zone, _ZoneConf}) -> start_timer(Zone) - end, maps:to_list(emqx_config:get([zones], #{}))). + end, maps:to_list(emqx:get_config([zones], #{}))). diff --git a/apps/emqx/src/emqx_global_gc.erl b/apps/emqx/src/emqx_global_gc.erl index 9449efe9a..5192508e5 100644 --- a/apps/emqx/src/emqx_global_gc.erl +++ b/apps/emqx/src/emqx_global_gc.erl @@ -85,7 +85,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- ensure_timer(State) -> - case emqx_config:get([node, global_gc_interval]) of + case emqx:get_config([node, global_gc_interval]) of undefined -> State; Interval -> TRef = emqx_misc:start_timer(Interval, run), State#{timer := TRef} diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index f80800768..f39c11305 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -43,7 +43,7 @@ list() -> [{listener_id(ZoneName, LName), LConf} || {ZoneName, LName, LConf} <- do_list()]. do_list() -> - Zones = maps:to_list(emqx_config:get([zones], #{})), + Zones = maps:to_list(emqx:get_config([zones], #{})), lists:append([list(ZoneName, ZoneConf) || {ZoneName, ZoneConf} <- Zones]). list(ZoneName, ZoneConf) -> diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index 9fd52c21b..85e448f41 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -76,7 +76,7 @@ set_procmem_high_watermark(Float) -> %%-------------------------------------------------------------------- init([]) -> - Opts = emqx_config:get([sysmon, os]), + Opts = emqx:get_config([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)), @@ -91,8 +91,8 @@ handle_cast(Msg, State) -> {noreply, 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, + CPUHighWatermark = emqx:get_config([sysmon, os, cpu_high_watermark]) * 100, + CPULowWatermark = emqx:get_config([sysmon, os, cpu_low_watermark]) * 100, _ = case emqx_vm:cpu_util() of %% TODO: should be improved? 0 -> ok; Busy when Busy >= CPUHighWatermark -> @@ -123,7 +123,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- start_check_timer() -> - Interval = emqx_config:get([sysmon, os, cpu_check_interval]), + Interval = emqx:get_config([sysmon, os, cpu_check_interval]), case erlang:system_info(system_architecture) of "x86_64-pc-linux-musl" -> ok; _ -> emqx_misc:start_timer(Interval, check) diff --git a/apps/emqx/src/emqx_plugins.erl b/apps/emqx/src/emqx_plugins.erl index 6c99305d4..7bb9c084b 100644 --- a/apps/emqx/src/emqx_plugins.erl +++ b/apps/emqx/src/emqx_plugins.erl @@ -43,7 +43,7 @@ %% @doc Load all plugins when the broker started. -spec(load() -> ok | ignore | {error, term()}). load() -> - ok = load_ext_plugins(emqx_config:get([plugins, expand_plugins_dir], undefined)). + ok = load_ext_plugins(emqx:get_config([plugins, expand_plugins_dir], undefined)). %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, term()}). diff --git a/apps/emqx/src/emqx_router.erl b/apps/emqx/src/emqx_router.erl index 1a5e344f2..8989c3b10 100644 --- a/apps/emqx/src/emqx_router.erl +++ b/apps/emqx/src/emqx_router.erl @@ -250,7 +250,7 @@ delete_trie_route(Route = #route{topic = Topic}) -> %% @private -spec(maybe_trans(function(), list(any())) -> ok | {error, term()}). maybe_trans(Fun, Args) -> - case emqx_config:get([broker, perf, route_lock_type]) of + case emqx:get_config([broker, perf, route_lock_type]) of key -> trans(Fun, Args); global -> diff --git a/apps/emqx/src/emqx_rpc.erl b/apps/emqx/src/emqx_rpc.erl index e950e9e3d..527123745 100644 --- a/apps/emqx/src/emqx_rpc.erl +++ b/apps/emqx/src/emqx_rpc.erl @@ -72,4 +72,4 @@ filter_result(Delivery) -> Delivery. max_client_num() -> - emqx_config:get([rpc, tcp_client_num], ?DefaultClientNum). + emqx:get_config([rpc, tcp_client_num], ?DefaultClientNum). diff --git a/apps/emqx/src/emqx_shared_sub.erl b/apps/emqx/src/emqx_shared_sub.erl index 1968c47d8..ccc050165 100644 --- a/apps/emqx/src/emqx_shared_sub.erl +++ b/apps/emqx/src/emqx_shared_sub.erl @@ -136,11 +136,11 @@ dispatch(Group, Topic, Delivery = #delivery{message = Msg}, FailedSubs) -> -spec(strategy() -> strategy()). strategy() -> - emqx_config:get([broker, shared_subscription_strategy]). + emqx:get_config([broker, shared_subscription_strategy]). -spec(ack_enabled() -> boolean()). ack_enabled() -> - emqx_config:get([broker, shared_dispatch_ack_enabled]). + emqx:get_config([broker, shared_dispatch_ack_enabled]). do_dispatch(SubPid, Topic, Msg, _Type) when SubPid =:= self() -> %% Deadlock otherwise diff --git a/apps/emqx/src/emqx_sys.erl b/apps/emqx/src/emqx_sys.erl index 6baae8c1e..70043e2bb 100644 --- a/apps/emqx/src/emqx_sys.erl +++ b/apps/emqx/src/emqx_sys.erl @@ -102,10 +102,10 @@ datetime() -> "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). sys_interval() -> - emqx_config:get([broker, sys_msg_interval]). + emqx:get_config([broker, sys_msg_interval]). sys_heatbeat_interval() -> - emqx_config:get([broker, sys_heartbeat_interval]). + emqx:get_config([broker, sys_heartbeat_interval]). %% @doc Get sys info -spec(info() -> list(tuple())). diff --git a/apps/emqx/src/emqx_sys_mon.erl b/apps/emqx/src/emqx_sys_mon.erl index 0b981ffec..80f5e49ec 100644 --- a/apps/emqx/src/emqx_sys_mon.erl +++ b/apps/emqx/src/emqx_sys_mon.erl @@ -60,7 +60,7 @@ start_timer(State) -> State#{timer := emqx_misc:start_timer(timer:seconds(2), reset)}. sysm_opts() -> - sysm_opts(maps:to_list(emqx_config:get([sysmon, vm])), []). + sysm_opts(maps:to_list(emqx:get_config([sysmon, vm])), []). sysm_opts([], Acc) -> Acc; sysm_opts([{_, disabled}|Opts], Acc) -> diff --git a/apps/emqx/src/emqx_trie.erl b/apps/emqx/src/emqx_trie.erl index 32c176b65..ebfcfcbe3 100644 --- a/apps/emqx/src/emqx_trie.erl +++ b/apps/emqx/src/emqx_trie.erl @@ -270,7 +270,7 @@ match_compact([Word | Words], Prefix, IsWildcard, Acc0) -> lookup_topic(MlTopic). is_compact() -> - emqx_config:get([broker, perf, trie_compaction], true). + emqx:get_config([broker, perf, trie_compaction], true). set_compact(Bool) -> emqx_config:put([broker, perf, trie_compaction], Bool). diff --git a/apps/emqx/src/emqx_vm_mon.erl b/apps/emqx/src/emqx_vm_mon.erl index 13a470959..51710b5b5 100644 --- a/apps/emqx/src/emqx_vm_mon.erl +++ b/apps/emqx/src/emqx_vm_mon.erl @@ -57,8 +57,8 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, _Timer, check}, State) -> - ProcHighWatermark = emqx_config:get([sysmon, vm, process_high_watermark]), - ProcLowWatermark = emqx_config:get([sysmon, vm, process_low_watermark]), + ProcHighWatermark = emqx:get_config([sysmon, vm, process_high_watermark]), + ProcLowWatermark = emqx:get_config([sysmon, vm, process_low_watermark]), ProcessCount = erlang:system_info(process_count), case ProcessCount / erlang:system_info(process_limit) of Percent when Percent >= ProcHighWatermark -> @@ -89,5 +89,5 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- start_check_timer() -> - Interval = emqx_config:get([sysmon, vm, process_check_interval]), + Interval = emqx:get_config([sysmon, vm, process_check_interval]), emqx_misc:start_timer(Interval, check). diff --git a/apps/emqx/test/emqx_mqtt_caps_SUITE.erl b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl index c01420f49..f8b5a7ab6 100644 --- a/apps/emqx/test/emqx_mqtt_caps_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_caps_SUITE.erl @@ -25,7 +25,7 @@ all() -> emqx_ct:all(?MODULE). t_check_pub(_) -> - OldConf = emqx_config:get([zones]), + OldConf = emqx:get_config([zones]), emqx_config:put_zone_conf(default, [mqtt, max_qos_allowed], ?QOS_1), emqx_config:put_zone_conf(default, [mqtt, retain_available], false), timer:sleep(50), @@ -39,7 +39,7 @@ t_check_pub(_) -> emqx_config:put([zones], OldConf). t_check_sub(_) -> - OldConf = emqx_config:get([zones]), + OldConf = emqx:get_config([zones]), SubOpts = #{rh => 0, rap => 0, nl => 0, diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index bd9ec9cfe..7518e5a01 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -36,7 +36,7 @@ stop(_State) -> ok. initialize() -> - AuthNConfig = emqx_config:get([authentication], #{enable => false, + AuthNConfig = emqx:get_config([authentication], #{enable => false, authenticators => []}), initialize(AuthNConfig). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 6197cd685..e3e540de0 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -47,7 +47,7 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NRules = [init_rule(Rule) || Rule <- emqx_config:get(?CONF_KEY_PATH, [])], + NRules = [init_rule(Rule) || Rule <- emqx:get_config(?CONF_KEY_PATH, [])], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 514b0d48b..0452ff96c 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -87,7 +87,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update(tail, [?RULE3]), Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), - ?assertMatch(Lists1, emqx_config:get([authorization, rules], [])), + ?assertMatch(Lists1, emqx:get_config([authorization, rules], [])), [#{annotations := #{id := Id1, principal := all, @@ -109,7 +109,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), - ?assertMatch(Lists2, emqx_config:get([authorization, rules], [])), + ?assertMatch(Lists2, emqx:get_config([authorization, rules], [])), [#{annotations := #{id := Id1, principal := all, 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 ef4d076a4..4207067fe 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl @@ -39,7 +39,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - BridgesConf = emqx_config:get([?APP, bridges], []), + BridgesConf = emqx:get_config([?APP, bridges], []), BridgeSpec = lists:map(fun bridge_spec/1, BridgesConf), SupFlag = #{strategy => one_for_one, intensity => 100, diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index adbdbc8e7..fb0e25564 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -98,7 +98,7 @@ stop_listener({Proto, Port, _}) -> listeners() -> [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} || Map = #{protocol := Protocol,port := Port} - <- emqx_config:get([emqx_dashboard, listeners], [])]. + <- emqx:get_config([emqx_dashboard, listeners], [])]. listener_name(Proto) -> list_to_atom(atom_to_list(Proto) ++ ":dashboard"). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index b32d3d346..982756805 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -201,7 +201,7 @@ add_default_user() -> add_default_user(binenv(default_username), binenv(default_password)). binenv(Key) -> - iolist_to_binary(emqx_config:get([emqx_dashboard, Key], "")). + iolist_to_binary(emqx:get_config([emqx_dashboard, Key], "")). add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY(Password) -> igonre; diff --git a/apps/emqx_dashboard/src/emqx_dashboard_collection.erl b/apps/emqx_dashboard/src/emqx_dashboard_collection.erl index 91d60e1ab..8b0576342 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_collection.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_collection.erl @@ -58,7 +58,7 @@ get_collect() -> gen_server:call(whereis(?MODULE), get_collect). init([]) -> timer(next_interval(), collect), timer(get_today_remaining_seconds(), clear_expire_data), - ExpireInterval = emqx_config:get([emqx_dashboard, monitor, interval], ?EXPIRE_INTERVAL), + ExpireInterval = emqx:get_config([emqx_dashboard, monitor, interval], ?EXPIRE_INTERVAL), State = #{ count => count(), expire_interval => ExpireInterval, @@ -78,7 +78,7 @@ next_interval() -> (1000 * interval()) - (erlang:system_time(millisecond) rem (1000 * interval())) - 1. interval() -> - emqx_config:get([?APP, sample_interval], ?DEFAULT_INTERVAL). + emqx:get_config([?APP, sample_interval], ?DEFAULT_INTERVAL). count() -> 60 div interval(). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index fdba7fb7e..432a64621 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -148,7 +148,7 @@ jwk(Username, Password, Salt) -> }. jwt_expiration_time() -> - ExpTime = emqx_config:get([emqx_dashboard, token_expired_time], ?EXPTIME), + ExpTime = emqx:get_config([emqx_dashboard, token_expired_time], ?EXPTIME), erlang:system_time(millisecond) + ExpTime. salt() -> diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.erl b/apps/emqx_data_bridge/src/emqx_data_bridge.erl index 17527ca3a..52cea80fb 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge.erl @@ -27,7 +27,7 @@ ]). load_bridges() -> - Bridges = emqx_config:get([emqx_data_bridge, bridges], []), + Bridges = emqx:get_config([emqx_data_bridge, bridges], []), emqx_data_bridge_monitor:ensure_all_started(Bridges). resource_type(mysql) -> emqx_connector_mysql; diff --git a/apps/emqx_exhook/src/emqx_exhook_sup.erl b/apps/emqx_exhook/src/emqx_exhook_sup.erl index 32f8fa472..60a6a2915 100644 --- a/apps/emqx_exhook/src/emqx_exhook_sup.erl +++ b/apps/emqx_exhook/src/emqx_exhook_sup.erl @@ -58,7 +58,7 @@ request_options() -> }. env(Key, Def) -> - emqx_config:get([exhook, Key], Def). + emqx:get_config([exhook, Key], Def). %%-------------------------------------------------------------------- %% APIs diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index b27319eac..adc546767 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -79,4 +79,4 @@ load_gateway_by_default([{Type, Confs}|More]) -> load_gateway_by_default(More). confs() -> - maps:to_list(emqx_config:get([gateway], [])). + maps:to_list(emqx:get_config([gateway], [])). diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 7fb700c55..4a7fefb2d 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -590,7 +590,7 @@ check_row_limit([Tab|Tables], Limit) -> end. max_row_limit() -> - emqx_config:get([?APP, max_row_limit], ?MAX_ROW_LIMIT). + emqx:get_config([?APP, max_row_limit], ?MAX_ROW_LIMIT). table_size(Tab) -> ets:info(Tab, size). diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 15849c294..b54e357d6 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -56,7 +56,7 @@ api_spec() -> config_apis() -> [config_api(ConfPath, Schema) || {ConfPath, Schema} <- - get_conf_schema(emqx_config:get([]), ?MAX_DEPTH), is_core_conf(ConfPath)]. + get_conf_schema(emqx:get_config([]), ?MAX_DEPTH), is_core_conf(ConfPath)]. config_api(ConfPath, Schema) -> Path = path_join(ConfPath), @@ -131,7 +131,7 @@ config_reset(post, Req) -> get_full_config() -> emqx_map_lib:jsonable_map( - emqx_config:fill_defaults(emqx_config:get_raw([]))). + emqx_config:fill_defaults(emqx:get_raw_config([]))). conf_path_from_querystr(Req) -> case proplists:get_value(<<"conf_path">>, cowboy_req:parse_qs(Req)) of diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 7fb120017..73ec37fc2 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -68,7 +68,7 @@ mnesia(copy) -> %%-------------------------------------------------------------------- -spec(add_default_app() -> list()). add_default_app() -> - Apps = emqx_config:get([?APP, applications], []), + Apps = emqx:get_config([?APP, applications], []), [ begin case {AppId, AppSecret} of {undefined, _} -> ok; diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index c795e1de7..7bd393904 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -94,7 +94,7 @@ stop_listener({Proto, Port, _}) -> listeners() -> [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} || Map = #{protocol := Protocol,port := Port} - <- emqx_config:get([emqx_management, listeners], [])]. + <- emqx:get_config([emqx_management, listeners], [])]. listener_name(Proto) -> list_to_atom(atom_to_list(Proto) ++ ":management"). diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index 5e1754f4b..00532e25e 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -104,7 +104,7 @@ on_message_publish(Msg) -> -spec(start_link() -> emqx_types:startlink_ret()). start_link() -> - Opts = emqx_config:get([delayed], #{}), + Opts = emqx:get_config([delayed], #{}), gen_server:start_link({local, ?SERVER}, ?MODULE, [Opts], []). -spec(store(#delayed_message{}) -> ok | {error, atom()}). diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 06de3ab13..9d4edd940 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -179,4 +179,4 @@ rpc_call(Node, Module, Fun, Args) -> end. get_status() -> - emqx_config:get([delayed, enable], true). + emqx:get_config([delayed, enable], true). diff --git a/apps/emqx_modules/src/emqx_event_message.erl b/apps/emqx_modules/src/emqx_event_message.erl index 3bdc54c2b..5017ed3e4 100644 --- a/apps/emqx_modules/src/emqx_event_message.erl +++ b/apps/emqx_modules/src/emqx_event_message.erl @@ -38,7 +38,7 @@ -endif. enable() -> - Topics = emqx_config:get([event_message, topics], []), + Topics = emqx:get_config([event_message, topics], []), lists:foreach(fun(Topic) -> case Topic of <<"$event/client_connected">> -> @@ -61,7 +61,7 @@ enable() -> end, Topics). disable() -> - Topics = emqx_config:get([event_message, topics], []), + Topics = emqx:get_config([event_message, topics], []), lists:foreach(fun(Topic) -> case Topic of <<"$event/client_connected">> -> diff --git a/apps/emqx_modules/src/emqx_modules_app.erl b/apps/emqx_modules/src/emqx_modules_app.erl index e969fc6dd..889c566b1 100644 --- a/apps/emqx_modules/src/emqx_modules_app.erl +++ b/apps/emqx_modules/src/emqx_modules_app.erl @@ -32,17 +32,17 @@ stop(_State) -> ok. maybe_enable_modules() -> - emqx_config:get([delayed, enable], true) andalso emqx_delayed:enable(), - emqx_config:get([telemetry, enable], true) andalso emqx_telemetry:enable(), - emqx_config:get([recon, enable], true) andalso emqx_recon:enable(), + emqx:get_config([delayed, enable], true) andalso emqx_delayed:enable(), + emqx:get_config([telemetry, enable], true) andalso emqx_telemetry:enable(), + emqx:get_config([recon, enable], true) andalso emqx_recon:enable(), emqx_event_message:enable(), emqx_rewrite:enable(), emqx_topic_metrics:enable(). maybe_disable_modules() -> - emqx_config:get([delayed, enable], true) andalso emqx_delayed:disable(), - emqx_config:get([telemetry, enable], true) andalso emqx_telemetry:disable(), - emqx_config:get([recon, enable], true) andalso emqx_recon:disable(), + emqx:get_config([delayed, enable], true) andalso emqx_delayed:disable(), + emqx:get_config([telemetry, enable], true) andalso emqx_telemetry:disable(), + emqx:get_config([recon, enable], true) andalso emqx_recon:disable(), emqx_event_message:disable(), emqx_rewrite:disable(), emqx_topic_metrics:disable(). diff --git a/apps/emqx_modules/src/emqx_rewrite.erl b/apps/emqx_modules/src/emqx_rewrite.erl index ec5a7d5ba..9a6c0574b 100644 --- a/apps/emqx_modules/src/emqx_rewrite.erl +++ b/apps/emqx_modules/src/emqx_rewrite.erl @@ -43,7 +43,7 @@ %%-------------------------------------------------------------------- enable() -> - Rules = emqx_config:get([rewrite, rules], []), + Rules = emqx:get_config([rewrite, rules], []), register_hook(Rules). disable() -> @@ -52,10 +52,10 @@ disable() -> emqx_hooks:del('message.publish', {?MODULE, rewrite_publish}). list() -> - maps:get(<<"rules">>, emqx_config:get_raw([<<"rewrite">>], #{}), []). + maps:get(<<"rules">>, emqx:get_raw_config([<<"rewrite">>], #{}), []). update(Rules0) -> - Rewrite = emqx_config:get_raw([<<"rewrite">>], #{}), + Rewrite = emqx:get_raw_config([<<"rewrite">>], #{}), {ok, #{config := Config}} = emqx:update_config([rewrite], maps:put(<<"rules">>, Rules0, Rewrite)), Rules = maps:get(rules, maps:get(rewrite, Config, #{}), []), diff --git a/apps/emqx_modules/src/emqx_telemetry.erl b/apps/emqx_modules/src/emqx_telemetry.erl index c9a78e736..aa207ac95 100644 --- a/apps/emqx_modules/src/emqx_telemetry.erl +++ b/apps/emqx_modules/src/emqx_telemetry.erl @@ -107,7 +107,7 @@ mnesia(copy) -> %%-------------------------------------------------------------------- start_link() -> - Opts = emqx_config:get([telemetry], #{}), + Opts = emqx:get_config([telemetry], #{}), gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). stop() -> @@ -120,7 +120,7 @@ disable() -> gen_server:call(?MODULE, disable). get_status() -> - emqx_config:get([telemetry, enable], true). + emqx:get_config([telemetry, enable], true). get_uuid() -> gen_server:call(?MODULE, get_uuid). diff --git a/apps/emqx_modules/src/emqx_topic_metrics.erl b/apps/emqx_modules/src/emqx_topic_metrics.erl index 5e6a3bc98..fa226c9a0 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics.erl @@ -137,7 +137,7 @@ on_message_dropped(#message{topic = Topic}, _, _) -> end. start_link() -> - Opts = emqx_config:get([topic_metrics], #{}), + Opts = emqx:get_config([topic_metrics], #{}), gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). stop() -> 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 3fac97e86..e51a4b6a6 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl @@ -45,7 +45,7 @@ -spec save_files_return_opts(opts_input(), atom() | string() | binary(), string() | binary()) -> opts(). save_files_return_opts(Options, SubDir, ResId) -> - Dir = filename:join([emqx_config:get([node, data_dir]), SubDir, ResId]), + Dir = filename:join([emqx:get_config([node, data_dir]), SubDir, ResId]), save_files_return_opts(Options, Dir). %% @doc Parse ssl options input. @@ -76,7 +76,7 @@ save_files_return_opts(Options, Dir) -> %% empty string is returned if the input is empty. -spec save_file(file_input(), atom() | string() | binary()) -> string(). save_file(Param, SubDir) -> - Dir = filename:join([emqx_config:get([node, data_dir]), SubDir]), + Dir = filename:join([emqx:get_config([node, data_dir]), SubDir]), do_save_file(Param, Dir). filter([]) -> []; diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 63b8b6a10..a2ac3b708 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -106,7 +106,7 @@ prometheus_api() -> % {"/prometheus/stats", Metadata, stats}. prometheus(get, _Request) -> - Response = emqx_config:get_raw([<<"prometheus">>], #{}), + Response = emqx:get_raw_config([<<"prometheus">>], #{}), {200, Response}; prometheus(put, Request) -> @@ -128,11 +128,11 @@ prometheus(put, Request) -> enable_prometheus(true) -> ok = emqx_prometheus_sup:stop_child(?APP), - emqx_prometheus_sup:start_child(?APP, emqx_config:get([prometheus], #{})), + emqx_prometheus_sup:start_child(?APP, emqx:get_config([prometheus], #{})), {200}; enable_prometheus(false) -> _ = emqx_prometheus_sup:stop_child(?APP), {200}. get_raw(Key, Def) -> - emqx_config:get_raw([<<"prometheus">>] ++ [Key], Def). + emqx:get_raw_config([<<"prometheus">>] ++ [Key], Def). diff --git a/apps/emqx_prometheus/src/emqx_prometheus_app.erl b/apps/emqx_prometheus/src/emqx_prometheus_app.erl index f4d4fd164..4f954a792 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_app.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_app.erl @@ -34,9 +34,9 @@ stop(_State) -> ok. maybe_enable_prometheus() -> - case emqx_config:get([prometheus, enable], false) of + case emqx:get_config([prometheus, enable], false) of true -> - emqx_prometheus_sup:start_child(?APP, emqx_config:get([prometheus], #{})); + emqx_prometheus_sup:start_child(?APP, emqx:get_config([prometheus], #{})); false -> ok end. diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index acd119dee..dfbe5cc69 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -129,7 +129,7 @@ deliver(Result, #{context_id := Id} = Context, Pid, Topic, Cursor) -> false -> ok; _ -> - #{msg_deliver_quota := MaxDeliverNum} = emqx_config:get([?APP, flow_control]), + #{msg_deliver_quota := MaxDeliverNum} = emqx:get_config([?APP, flow_control]), case MaxDeliverNum of 0 -> _ = [Pid ! {deliver, Topic, Msg} || Msg <- Result], @@ -150,7 +150,7 @@ get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' : timestamp = Ts}) -> Ts + Interval * 1000; get_expiry_time(#message{timestamp = Ts}) -> - Interval = emqx_config:get([?APP, msg_expiry_interval], ?DEF_EXPIRY_INTERVAL), + Interval = emqx:get_config([?APP, msg_expiry_interval], ?DEF_EXPIRY_INTERVAL), case Interval of 0 -> 0; _ -> Ts + Interval @@ -173,7 +173,7 @@ delete(Topic) -> init([]) -> init_shared_context(), State = new_state(), - #{enable := Enable} = Cfg = emqx_config:get([?APP]), + #{enable := Enable} = Cfg = emqx:get_config([?APP]), {ok, case Enable of true -> @@ -209,7 +209,7 @@ handle_cast(Msg, State) -> handle_info(clear_expired, #{context := Context} = State) -> Mod = get_backend_module(), Mod:clear_expired(Context), - Interval = emqx_config:get([?APP, msg_clear_interval], ?DEF_EXPIRY_INTERVAL), + Interval = emqx:get_config([?APP, msg_clear_interval], ?DEF_EXPIRY_INTERVAL), {noreply, State#{clear_timer := add_timer(Interval, clear_expired)}, hibernate}; handle_info(release_deliver_quota, #{context := Context, wait_quotas := Waits} = State) -> @@ -225,7 +225,7 @@ handle_info(release_deliver_quota, #{context := Context, wait_quotas := Waits} = end, Waits2) end, - Interval = emqx_config:get([?APP, flow_control, quota_release_interval]), + Interval = emqx:get_config([?APP, flow_control, quota_release_interval]), {noreply, State#{release_quota_timer := add_timer(Interval, release_deliver_quota), wait_quotas := []}}; @@ -258,7 +258,7 @@ new_context(Id) -> #{context_id => Id}. is_too_big(Size) -> - Limit = emqx_config:get([?APP, max_payload_size], ?DEF_MAX_PAYLOAD_SIZE), + Limit = emqx:get_config([?APP, max_payload_size], ?DEF_MAX_PAYLOAD_SIZE), Limit > 0 andalso (Size > Limit). %% @private @@ -332,7 +332,7 @@ insert_shared_context(Key, Term) -> -spec get_msg_deliver_quota() -> non_neg_integer(). get_msg_deliver_quota() -> - emqx_config:get([?APP, flow_control, msg_deliver_quota]). + emqx:get_config([?APP, flow_control, msg_deliver_quota]). -spec update_config(state(), hocons:config()) -> state(). update_config(#{clear_timer := ClearTimer, @@ -342,7 +342,7 @@ update_config(#{clear_timer := ClearTimer, flow_control := #{quota_release_interval := QuotaInterval}, msg_clear_interval := ClearInterval} = Conf, - #{config := OldConfig} = emqx_config:get([?APP]), + #{config := OldConfig} = emqx:get_config([?APP]), case Enable of true -> @@ -416,7 +416,7 @@ check_timer(Timer, _, _) -> -spec get_backend_module() -> backend(). get_backend_module() -> - #{type := Backend} = emqx_config:get([?APP, config]), + #{type := Backend} = emqx:get_config([?APP, config]), ModName = if Backend =:= built_in_database -> mnesia; true -> diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 1b5b8adcc..a0b72c858 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -35,7 +35,7 @@ ]). lookup_config(_Bindings, _Params) -> - Config = emqx_config:get([emqx_retainer]), + Config = emqx:get_config([emqx_retainer]), return({ok, Config}). update_config(_Bindings, Params) -> diff --git a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl index 34e3e49db..1c9956050 100644 --- a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl +++ b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl @@ -130,7 +130,7 @@ read_message(_, Topic) -> {ok, read_messages(Topic)}. match_messages(_, Topic, Cursor) -> - MaxReadNum = emqx_config:get([?APP, flow_control, max_read_number]), + MaxReadNum = emqx:get_config([?APP, flow_control, max_read_number]), case Cursor of undefined -> case MaxReadNum of @@ -227,7 +227,7 @@ make_match_spec(Filter) -> -spec is_table_full() -> boolean(). is_table_full() -> - #{max_retained_messages := Limit} = emqx_config:get([?APP, config]), + #{max_retained_messages := Limit} = emqx:get_config([?APP, config]), Limit > 0 andalso (table_size() >= Limit). -spec table_size() -> non_neg_integer(). 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 8d17ee6f5..194fc4096 100644 --- a/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl +++ b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl @@ -506,7 +506,7 @@ connect(Options) when is_list(Options) -> connect(Options = #{disk_cache := DiskCache, ecpool_worker_id := Id, pool_name := Pool}) -> Options0 = case DiskCache of true -> - DataDir = filename:join([emqx_config:get([node, data_dir]), replayq, Pool, integer_to_list(Id)]), + DataDir = filename:join([emqx:get_config([node, data_dir]), replayq, Pool, integer_to_list(Id)]), QueueOption = #{replayq_dir => DataDir}, Options#{queue => QueueOption}; false -> diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 6eacced42..8154170b0 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -595,4 +595,4 @@ printable_maps(Headers) -> ignore_sys_message(#message{flags = Flags}) -> maps:get(sys, Flags, false) andalso - emqx_config:get([emqx_rule_engine, ignore_sys_message]). + emqx:get_config([emqx_rule_engine, ignore_sys_message]). diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index 25f0d1879..3325d8c71 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -84,7 +84,7 @@ statsd_api() -> [{"/statsd", Metadata, statsd}]. statsd(get, _Request) -> - Response = emqx_config:get_raw([<<"statsd">>], #{}), + Response = emqx:get_raw_config([<<"statsd">>], #{}), {200, Response}; statsd(put, Request) -> @@ -96,11 +96,11 @@ statsd(put, Request) -> enable_statsd(true) -> ok = emqx_statsd_sup:stop_child(?APP), - emqx_statsd_sup:start_child(?APP, emqx_config:get([statsd], #{})), + emqx_statsd_sup:start_child(?APP, emqx:get_config([statsd], #{})), {200}; enable_statsd(false) -> _ = emqx_statsd_sup:stop_child(?APP), {200}. get_raw(Key, Def) -> - emqx_config:get_raw([<<"statsd">>]++ [Key], Def). + emqx:get_raw_config([<<"statsd">>]++ [Key], Def). diff --git a/apps/emqx_statsd/src/emqx_statsd_app.erl b/apps/emqx_statsd/src/emqx_statsd_app.erl index 6dd9dc5c7..4a5ff0496 100644 --- a/apps/emqx_statsd/src/emqx_statsd_app.erl +++ b/apps/emqx_statsd/src/emqx_statsd_app.erl @@ -32,9 +32,9 @@ stop(_) -> ok. maybe_enable_statsd() -> - case emqx_config:get([statsd, enable], false) of + case emqx:get_config([statsd, enable], false) of true -> - emqx_statsd_sup:start_child(?APP, emqx_config:get([statsd], #{})); + emqx_statsd_sup:start_child(?APP, emqx:get_config([statsd], #{})); false -> ok end. From f01b77e4fe050d0e72a576674a6372d0567d0aab Mon Sep 17 00:00:00 2001 From: turtleDeng Date: Wed, 18 Aug 2021 16:26:15 +0800 Subject: [PATCH 056/306] refactor(event-message): refactor event_message * refactor(event-message): refactor event_message configuration * feat(event-message): add event_message REST API --- apps/emqx_modules/etc/emqx_modules.conf | 19 ++-- apps/emqx_modules/include/emqx_modules.hrl | 8 -- apps/emqx_modules/src/emqx_event_message.erl | 80 ++++++++------- .../src/emqx_event_message_api.erl | 99 +++++++++++++++++++ apps/emqx_modules/src/emqx_modules_schema.erl | 25 ++--- .../test/emqx_event_message_SUITE.erl | 34 +++---- 6 files changed, 176 insertions(+), 89 deletions(-) create mode 100644 apps/emqx_modules/src/emqx_event_message_api.erl diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index 92f563342..bae8fd3ed 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -11,17 +11,14 @@ telemetry: { enable: true } - -event_message: { - topics: [ - "$event/client_connected", - "$event/client_disconnected", - "$event/session_subscribed", - "$event/session_unsubscribed", - "$event/message_delivered", - "$event/message_acked", - "$event/message_dropped" - ] +event_message { + "$event/client_connected": true + "$event/client_disconnected": true + # "$event/client_subscribed": false + # "$event/client_unsubscribed": false + # "$event/message_delivered": false + # "$event/message_acked": false + # "$event/message_dropped": false } topic_metrics:{ diff --git a/apps/emqx_modules/include/emqx_modules.hrl b/apps/emqx_modules/include/emqx_modules.hrl index b7cdb154e..334173015 100644 --- a/apps/emqx_modules/include/emqx_modules.hrl +++ b/apps/emqx_modules/include/emqx_modules.hrl @@ -3,11 +3,3 @@ %% Interval for reporting telemetry data, Default: 7d -define(REPORT_INTERVAR, 604800). - --define(BASE_TOPICS, [<<"$event/client_connected">>, - <<"$event/client_disconnected">>, - <<"$event/session_subscribed">>, - <<"$event/session_unsubscribed">>, - <<"$event/message_delivered">>, - <<"$event/message_acked">>, - <<"$event/message_dropped">>]). diff --git a/apps/emqx_modules/src/emqx_event_message.erl b/apps/emqx_modules/src/emqx_event_message.erl index 5017ed3e4..842ddae04 100644 --- a/apps/emqx_modules/src/emqx_event_message.erl +++ b/apps/emqx_modules/src/emqx_event_message.erl @@ -20,14 +20,16 @@ -include_lib("emqx/include/logger.hrl"). -include("emqx_modules.hrl"). --export([ enable/0 +-export([ list/0 + , update/1 + , enable/0 , disable/0 ]). -export([ on_client_connected/2 , on_client_disconnected/3 - , on_session_subscribed/3 - , on_session_unsubscribed/3 + , on_client_subscribed/3 + , on_client_unsubscribed/3 , on_message_dropped/3 , on_message_delivered/2 , on_message_acked/2 @@ -37,51 +39,59 @@ -export([reason/1]). -endif. +list() -> + emqx:get_config([event_message], #{}). + +update(Params) -> + disable(), + {ok, _} = emqx:update_config([event_message], Params), + enable(). + enable() -> - Topics = emqx:get_config([event_message, topics], []), - lists:foreach(fun(Topic) -> + lists:foreach(fun({_Topic, false}) -> ok; + ({Topic, true}) -> case Topic of - <<"$event/client_connected">> -> + '$event/client_connected' -> emqx_hooks:put('client.connected', {?MODULE, on_client_connected, []}); - <<"$event/client_disconnected">> -> + '$event/client_disconnected' -> emqx_hooks:put('client.disconnected', {?MODULE, on_client_disconnected, []}); - <<"$event/session_subscribed">> -> - emqx_hooks:put('session.subscribed', {?MODULE, on_session_subscribed, []}); - <<"$event/session_unsubscribed">> -> - emqx_hooks:put('session.unsubscribed', {?MODULE, on_session_unsubscribed, []}); - <<"$event/message_delivered">> -> + '$event/client_subscribed' -> + emqx_hooks:put('session.subscribed', {?MODULE, on_client_subscribed, []}); + '$event/client_unsubscribed' -> + emqx_hooks:put('session.unsubscribed', {?MODULE, on_client_unsubscribed, []}); + '$event/message_delivered' -> emqx_hooks:put('message.delivered', {?MODULE, on_message_delivered, []}); - <<"$event/message_acked">> -> + '$event/message_acked' -> emqx_hooks:put('message.acked', {?MODULE, on_message_acked, []}); - <<"$event/message_dropped">> -> + '$event/message_dropped' -> emqx_hooks:put('message.dropped', {?MODULE, on_message_dropped, []}); _ -> ok end - end, Topics). + end, maps:to_list(list())). disable() -> - Topics = emqx:get_config([event_message, topics], []), - lists:foreach(fun(Topic) -> + lists:foreach(fun({_Topic, false}) -> ok; + ({Topic, true}) -> case Topic of - <<"$event/client_connected">> -> + '$event/client_connected' -> emqx_hooks:del('client.connected', {?MODULE, on_client_connected}); - <<"$event/client_disconnected">> -> + '$event/client_disconnected' -> emqx_hooks:del('client.disconnected', {?MODULE, on_client_disconnected}); - <<"$event/session_subscribed">> -> - emqx_hooks:del('session.subscribed', {?MODULE, on_session_subscribed}); - <<"$event/session_unsubscribed">> -> - emqx_hooks:del('session.unsubscribed', {?MODULE, on_session_unsubscribed}); - <<"$event/message_delivered">> -> + '$event/client_subscribed' -> + emqx_hooks:del('session.subscribed', {?MODULE, on_client_subscribed}); + '$event/client_unsubscribed' -> + emqx_hooks:del('session.unsubscribed', {?MODULE, on_client_unsubscribed}); + '$event/message_delivered' -> emqx_hooks:del('message.delivered', {?MODULE, on_message_delivered}); - <<"$event/message_acked">> -> + '$event/message_acked' -> emqx_hooks:del('message.acked', {?MODULE, on_message_acked}); - <<"$event/message_dropped">> -> + '$event/message_dropped' -> emqx_hooks:del('message.dropped', {?MODULE, on_message_dropped}); _ -> ok end - end, ?BASE_TOPICS -- Topics). + end, maps:to_list(list())). %%-------------------------------------------------------------------- %% Callbacks @@ -90,7 +100,6 @@ disable() -> on_client_connected(ClientInfo, ConnInfo) -> Payload0 = common_infos(ClientInfo, ConnInfo), Payload = Payload0#{ - connack => 0, %% XXX: connack will be removed in 5.0 keepalive => maps:get(keepalive, ConnInfo, 0), clean_start => maps:get(clean_start, ConnInfo, true), expiry_interval => maps:get(expiry_interval, ConnInfo, 0), @@ -108,8 +117,8 @@ on_client_disconnected(ClientInfo, }, publish_event_msg(<<"$event/client_disconnected">>, Payload). -on_session_subscribed(_ClientInfo = #{clientid := ClientId, - username := Username}, +on_client_subscribed(_ClientInfo = #{clientid := ClientId, + username := Username}, Topic, SubOpts) -> Payload = #{clientid => ClientId, username => Username, @@ -117,17 +126,17 @@ on_session_subscribed(_ClientInfo = #{clientid := ClientId, subopts => SubOpts, ts => erlang:system_time(millisecond) }, - publish_event_msg(<<"$event/session_subscribed">>, Payload). + publish_event_msg(<<"$event/client_subscribed">>, Payload). -on_session_unsubscribed(_ClientInfo = #{clientid := ClientId, - username := Username}, +on_client_unsubscribed(_ClientInfo = #{clientid := ClientId, + username := Username}, Topic, _SubOpts) -> Payload = #{clientid => ClientId, username => Username, topic => Topic, ts => erlang:system_time(millisecond) }, - publish_event_msg(<<"$event/session_unsubscribed">>, Payload). + publish_event_msg(<<"$event/client_unsubscribed">>, Payload). on_message_dropped(Message = #message{from = ClientId}, _, Reason) -> case ignore_sys_message(Message) of @@ -201,8 +210,7 @@ common_infos( ipaddress => ntoa(PeerHost), sockport => SockPort, proto_name => ProtoName, - proto_ver => ProtoVer, - ts => erlang:system_time(millisecond) + proto_ver => ProtoVer }. make_msg(Topic, Payload) -> diff --git a/apps/emqx_modules/src/emqx_event_message_api.erl b/apps/emqx_modules/src/emqx_event_message_api.erl new file mode 100644 index 000000000..86c3255e1 --- /dev/null +++ b/apps/emqx_modules/src/emqx_event_message_api.erl @@ -0,0 +1,99 @@ +%%-------------------------------------------------------------------- +%% 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_event_message_api). + +-behaviour(minirest_api). + +-export([api_spec/0]). + +-export([event_message/2]). + + +api_spec() -> + {[event_message_api()], [event_message_schema()]}. + +event_message_schema() -> + #{ + type => object, + properties => #{ + '$event/client_connected' => #{ + type => boolean, + description => <<"Client connected event">>, + example => get_raw(<<"$event/client_connected">>) + }, + '$event/client_disconnected' => #{ + type => boolean, + description => <<"client_disconnected">>, + example => get_raw(<<"Client disconnected event">>) + }, + '$event/client_subscribed' => #{ + type => boolean, + description => <<"client_subscribed">>, + example => get_raw(<<"Client subscribed event">>) + }, + '$event/client_unsubscribed' => #{ + type => boolean, + description => <<"client_unsubscribed">>, + example => get_raw(<<"Client unsubscribed event">>) + }, + '$event/message_delivered' => #{ + type => boolean, + description => <<"message_delivered">>, + example => get_raw(<<"Message delivered event">>) + }, + '$event/message_acked' => #{ + type => boolean, + description => <<"message_acked">>, + example => get_raw(<<"Message acked event">>) + }, + '$event/message_dropped' => #{ + type => boolean, + description => <<"message_dropped">>, + example => get_raw(<<"Message dropped event">>) + } + } + }. + +event_message_api() -> + Path = "/mqtt/event_message", + Metadata = #{ + get => #{ + description => <<"Event Message">>, + responses => #{ + <<"200">> => + emqx_mgmt_util:response_schema(<<>>, event_message_schema())}}, + post => #{ + description => <<"">>, + 'requestBody' => emqx_mgmt_util:request_body_schema(event_message_schema()), + responses => #{ + <<"200">> => + emqx_mgmt_util:response_schema(<<>>, event_message_schema()) + } + } + }, + {Path, Metadata, event_message}. + +event_message(get, _Request) -> + {200, emqx_event_message:list()}; + +event_message(post, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + Params = emqx_json:decode(Body, [return_maps]), + _ = emqx_event_message:update(Params), + {200, emqx_event_message:list()}. + +get_raw(Key) -> + emqx_config:get_raw([<<"event_message">>] ++ [Key], false). diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl index 0097fdbbe..0c5e716bf 100644 --- a/apps/emqx_modules/src/emqx_modules_schema.erl +++ b/apps/emqx_modules/src/emqx_modules_schema.erl @@ -45,8 +45,15 @@ fields("rewrite") -> [ {rules, hoconsc:array(hoconsc:ref(?MODULE, "rules"))} ]; + fields("event_message") -> - [ {topics, fun topics/1} + [ {"$event/client_connected", emqx_schema:t(boolean(), undefined, false)} + , {"$event/client_disconnected", emqx_schema:t(boolean(), undefined, false)} + , {"$event/client_subscribed", emqx_schema:t(boolean(), undefined, false)} + , {"$event/client_unsubscribed", emqx_schema:t(boolean(), undefined, false)} + , {"$event/message_delivered", emqx_schema:t(boolean(), undefined, false)} + , {"$event/message_acked", emqx_schema:t(boolean(), undefined, false)} + , {"$event/message_dropped", emqx_schema:t(boolean(), undefined, false)} ]; fields("topic_metrics") -> @@ -60,19 +67,3 @@ fields("rules") -> , {dest_topic, emqx_schema:t(binary())} ]. -topics(type) -> hoconsc:array(binary()); -topics(default) -> []; -% topics(validator) -> [ -% fun(Conf) -> -% case lists:member(Conf, ["$event/client_connected", -% "$event/client_disconnected", -% "$event/session_subscribed", -% "$event/session_unsubscribed", -% "$event/message_delivered", -% "$event/message_acked", -% "$event/message_dropped"]) of -% true -> ok; -% false -> {error, "Bad event topic"} -% end -% end]; -topics(_) -> undefined. diff --git a/apps/emqx_modules/test/emqx_event_message_SUITE.erl b/apps/emqx_modules/test/emqx_event_message_SUITE.erl index 97235ac1f..526113bcc 100644 --- a/apps/emqx_modules/test/emqx_event_message_SUITE.erl +++ b/apps/emqx_modules/test/emqx_event_message_SUITE.erl @@ -24,16 +24,14 @@ -define(EVENT_MESSAGE, <<""" event_message: { - topics : [ - \"$event/client_connected\", - \"$event/client_disconnected\", - \"$event/session_subscribed\", - \"$event/session_unsubscribed\", - \"$event/message_delivered\", - \"$event/message_acked\", - \"$event/message_dropped\" - ]}""">>). - + \"$event/client_connected\": true + \"$event/client_disconnected\": true + \"$event/client_subscribed\": true + \"$event/client_unsubscribed\": true + \"$event/message_delivered\": true + \"$event/message_acked\": true + \"$event/message_dropped\": true + }""">>). all() -> emqx_ct:all(?MODULE). @@ -56,12 +54,12 @@ t_event_topic(_) -> {ok, _} = emqtt:connect(C2), ok = recv_connected(<<"clientid">>), - {ok, _, [?QOS_1]} = emqtt:subscribe(C1, <<"$event/session_subscribed">>, qos1), + {ok, _, [?QOS_1]} = emqtt:subscribe(C1, <<"$event/client_subscribed">>, qos1), _ = receive_publish(100), timer:sleep(50), {ok, _, [?QOS_1]} = emqtt:subscribe(C2, <<"test_sub">>, qos1), ok = recv_subscribed(<<"clientid">>), - emqtt:unsubscribe(C1, <<"$event/session_subscribed">>), + emqtt:unsubscribe(C1, <<"$event/client_subscribed">>), timer:sleep(50), {ok, _, [?QOS_1]} = emqtt:subscribe(C1, <<"$event/message_delivered">>, qos1), @@ -77,7 +75,7 @@ t_event_topic(_) -> ok= emqtt:publish(C2, <<"test_sub1">>, <<"test">>), recv_message_dropped(<<"clientid">>), - {ok, _, [?QOS_1]} = emqtt:subscribe(C1, <<"$event/session_unsubscribed">>, qos1), + {ok, _, [?QOS_1]} = emqtt:subscribe(C1, <<"$event/client_unsubscribed">>, qos1), _ = emqtt:unsubscribe(C2, <<"test_sub">>), ok = recv_unsubscribed(<<"clientid">>), @@ -101,12 +99,14 @@ recv_connected(ClientId) -> <<"ipaddress">> := <<"127.0.0.1">>, <<"proto_name">> := <<"MQTT">>, <<"proto_ver">> := ?MQTT_PROTO_V4, - <<"connack">> := ?RC_SUCCESS, - <<"clean_start">> := true}, emqx_json:decode(Payload, [return_maps])). + <<"clean_start">> := true, + <<"expiry_interval">> := 0, + <<"keepalive">> := 60 + }, emqx_json:decode(Payload, [return_maps])). recv_subscribed(_ClientId) -> {ok, #{qos := ?QOS_0, topic := Topic}} = receive_publish(100), - ?assertMatch(<<"$event/session_subscribed">>, Topic). + ?assertMatch(<<"$event/client_subscribed">>, Topic). recv_message_dropped(_ClientId) -> {ok, #{qos := ?QOS_0, topic := Topic}} = receive_publish(100), @@ -123,7 +123,7 @@ recv_message_acked(_ClientId) -> recv_unsubscribed(_ClientId) -> {ok, #{qos := ?QOS_0, topic := Topic}} = receive_publish(100), - ?assertMatch(<<"$event/session_unsubscribed">>, Topic). + ?assertMatch(<<"$event/client_unsubscribed">>, Topic). recv_disconnected(ClientId) -> {ok, #{qos := ?QOS_0, topic := Topic, payload := Payload}} = receive_publish(100), From 213b7c2501409d066b6e13133b648143af904e3c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 18 Aug 2021 18:53:53 +0800 Subject: [PATCH 057/306] fix(config_api): improve the schema for config update APIs --- apps/emqx/src/emqx_map_lib.erl | 1 + .../src/emqx_mgmt_api_configs.erl | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index cda4f3a85..e5baeb850 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -24,6 +24,7 @@ , safe_atom_key_map/1 , unsafe_atom_key_map/1 , jsonable_map/1 + , jsonable_value/1 , deep_convert/2 ]). diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index b54e357d6..6ff401048 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -24,6 +24,8 @@ , config_reset/2 ]). +-export([get_conf_schema/2]). + -define(PARAM_CONF_PATH, [#{ name => conf_path, in => query, @@ -171,15 +173,19 @@ get_conf_schema(BasePath, [{Key, Conf} | Confs], Result, MaxDepth) -> %% TODO: generate from hocon schema gen_schema(Conf) when is_boolean(Conf) -> - #{type => boolean}; + with_default_value(#{type => boolean}, Conf); gen_schema(Conf) when is_binary(Conf); is_atom(Conf) -> - #{type => string}; + with_default_value(#{type => string}, Conf); gen_schema(Conf) when is_number(Conf) -> - #{type => number}; + with_default_value(#{type => number}, Conf); gen_schema(Conf) when is_list(Conf) -> #{type => array, items => case Conf of [] -> #{}; %% don't know the type - _ -> gen_schema(hd(Conf)) + _ -> + case io_lib:printable_unicode_list(Conf) of + true -> gen_schema(unicode:characters_to_binary(Conf)); + false -> gen_schema(hd(Conf)) + end end}; gen_schema(Conf) when is_map(Conf) -> #{type => object, properties => @@ -189,6 +195,9 @@ gen_schema(_Conf) -> %% by the hocon schema #{type => string}. +with_default_value(Type, Value) -> + Type#{example => emqx_map_lib:jsonable_value(Value)}. + path_join(Path) -> path_join(Path, "/"). From 9c76247cdee45b7e1f55f06199eaac51c5f6333c Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 19 Aug 2021 09:56:32 +0800 Subject: [PATCH 058/306] fix: alarms api timestamp millisecond --- apps/emqx/src/emqx_alarm.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index e52328d66..7599f9569 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -161,7 +161,7 @@ format(#activated_alarm{name = Name, message = Message, activate_at = At, detail node => node(), name => Name, message => Message, - duration => Now - At, + duration => (Now - At) div 1000, %% to millisecond details => Details }; format(#deactivated_alarm{name = Name, message = Message, activate_at = At, details = Details, From adc6226eae9a3bb62d9e062db7d4bd07ff261b32 Mon Sep 17 00:00:00 2001 From: lafirest Date: Wed, 18 Aug 2021 12:05:19 +0800 Subject: [PATCH 059/306] refactor(emqx_retainer): emqx_retainer_api use openapi model --- .../src/emqx_mgmt_api_configs.erl | 2 +- apps/emqx_retainer/src/emqx_retainer.erl | 13 +- apps/emqx_retainer/src/emqx_retainer_api.erl | 231 +++++++++++++++--- .../src/emqx_retainer_mnesia.erl | 58 +++-- .../test/emqx_retainer_api_SUITE.erl | 17 +- 5 files changed, 256 insertions(+), 65 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 6ff401048..1a89835ff 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -24,7 +24,7 @@ , config_reset/2 ]). --export([get_conf_schema/2]). +-export([get_conf_schema/2, gen_schema/1]). -define(PARAM_CONF_PATH, [#{ name => conf_path, diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index dfbe5cc69..4c36cb541 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -37,7 +37,8 @@ -export([ get_expiry_time/1 , update_config/1 , clean/0 - , delete/1]). + , delete/1 + , page_read/3]). %% gen_server callbacks -export([ init/1 @@ -66,6 +67,8 @@ -callback delete_message(context(), topic()) -> ok. -callback store_retained(context(), message()) -> ok. -callback read_message(context(), topic()) -> {ok, list()}. +-callback page_read(context(), topic(), non_neg_integer(), non_neg_integer()) -> + {ok, list()}. -callback match_messages(context(), topic(), cursor()) -> {ok, list(), cursor()}. -callback clear_expired(context()) -> ok. -callback clean(context()) -> ok. @@ -166,6 +169,9 @@ clean() -> delete(Topic) -> gen_server:call(?MODULE, {?FUNCTION_NAME, Topic}). +page_read(Topic, Page, Limit) -> + gen_server:call(?MODULE, {?FUNCTION_NAME, Topic, Page, Limit}). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- @@ -198,6 +204,11 @@ handle_call({delete, Topic}, _, #{context := Context} = State) -> delete_message(Context, Topic), {reply, ok, State}; +handle_call({page_read, Topic, Page, Limit}, _, #{context := Context} = State) -> + Mod = get_backend_module(), + Result = Mod:page_read(Context, Topic, Page, Limit), + {reply, Result, State}; + handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, State}. diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index a0b72c858..d766eab06 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -16,52 +16,213 @@ -module(emqx_retainer_api). --rest_api(#{name => lookup_config, - method => 'GET', - path => "/retainer", - func => lookup_config, - descr => "lookup retainer config" - }). +-behaviour(minirest_api). --rest_api(#{name => update_config, - method => 'PUT', - path => "/retainer", - func => update_config, - descr => "update retainer config" - }). +-include_lib("emqx/include/emqx.hrl"). --export([ lookup_config/2 - , update_config/2 - ]). +-export([api_spec/0]). -lookup_config(_Bindings, _Params) -> - Config = emqx:get_config([emqx_retainer]), - return({ok, Config}). +-export([ lookup_retained_warp/2 + , with_topic_warp/2 + , config/2]). -update_config(_Bindings, Params) -> +-import(emqx_mgmt_api_configs, [gen_schema/1]). +-import(emqx_mgmt_util, [ response_array_schema/2 + , response_schema/1 + , response_error_schema/2]). + +-define(CFG_BODY(DESCR), + #{description => list_to_binary(DESCR), + content => #{<<"application/json">> => + #{schema => gen_schema(emqx_config:get([emqx_retainer]))}}}). + +api_spec() -> + { + [ lookup_retained_api() + , with_topic_api() + , config_api() + ], + [ message_schema(message, fun message_properties/0) + , message_schema(detail_message, fun detail_message_properties/0) + ] + }. + +lookup_retained_api() -> + Metadata = + #{get => #{description => <<"lookup matching messages">>, + parameters => [ #{name => page, + in => query, + description => <<"Page">>, + schema => #{type => integer, default => 1}} + , #{name => limit, + in => query, + description => <<"Page size">>, + schema => #{type => integer, + default => emqx_mgmt:max_row_limit()}} + ], + responses => #{ <<"200">> => + response_array_schema("List retained messages", message) + , <<"405">> => response_schema(<<"NotAllowed">>) + }}}, + {"/mqtt/retainer/messages", Metadata, lookup_retained_warp}. + +with_topic_api() -> + MetaData = #{get => #{description => <<"lookup matching messages">>, + parameters => [ #{name => topic, + in => path, + required => true, + schema => #{type => "string"}} + , #{name => page, + in => query, + description => <<"Page">>, + schema => #{type => integer, default => 1}} + , #{name => limit, + in => query, + description => <<"Page size">>, + schema => #{type => integer, + default => emqx_mgmt:max_row_limit()}} + ], + responses => #{ <<"200">> => + response_array_schema("List retained messages", detail_message) + , <<"405">> => response_schema(<<"NotAllowed">>)}}, + delete => #{description => <<"delete matching messages">>, + parameters => [#{name => topic, + in => path, + required => true, + schema => #{type => "string"}}], + responses => #{ <<"200">> => response_schema(<<"Successed">>) + , <<"405">> => response_schema(<<"NotAllowed">>)}} + }, + {"/mqtt/retainer/message/:topic", MetaData, with_topic_warp}. + +config_api() -> + MetaData = #{ + get => #{ + description => <<"get retainer config">>, + responses => #{<<"200">> => ?CFG_BODY("Get configs successfully"), + <<"404">> => response_error_schema( + <<"Config not found">>, ['NOT_FOUND'])} + }, + put => #{ + description => <<"Update retainer config">>, + 'requestBody' => + ?CFG_BODY("The format of the request body is depend on the 'conf_path' parameter in the query string"), + responses => #{<<"200">> => response_schema("Update configs successfully"), + <<"400">> => response_error_schema( + <<"Update configs failed">>, ['UPDATE_FAILED'])} + } + }, + {"/mqtt/retainer", MetaData, config}. + +lookup_retained_warp(Type, Req) -> + check_backend(Type, Req, fun lookup_retained/2). + +with_topic_warp(Type, Req) -> + check_backend(Type, Req, fun with_topic/2). + +config(get, _) -> + Config = emqx_config:get([emqx_retainer]), + Body = emqx_json:encode(Config), + {200, Body}; + +config(put, Req) -> try - ConfigList = proplists:get_value(<<"emqx_retainer">>, Params), - {ok, RawConf} = hocon:binary(jsx:encode(#{<<"emqx_retainer">> => ConfigList}), + {ok, Body, _} = cowboy_req:read_body(Req), + Cfg = emqx_json:decode(Body), + {ok, RawConf} = hocon:binary(jsx:encode(#{<<"emqx_retainer">> => Cfg}), #{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), - return() - catch _:_:Reason -> - return({error, Reason}) + emqx_retainer:update_config(Conf), + {200, #{<<"content-type">> => <<"text/plain">>}, <<"Update configs successfully">>} + catch _:Reason:_ -> + {400, + #{code => 'UPDATE_FAILED', + message => erlang:list_to_binary(io_lib:format("~p~n", [Reason]))}} end. %%------------------------------------------------------------------------------ %% Interval Funcs %%------------------------------------------------------------------------------ -do_update_config(undefined, Config) -> - emqx_retainer:update_config(Config); -do_update_config(<<"test">>, _) -> - ok. +lookup_retained(get, Req) -> + lookup(undefined, Req, fun format_message/1). -%% TODO: V5 API -return() -> - ok. -return(_) -> - ok. +with_topic(get, Req) -> + Topic = cowboy_req:binding(topic, Req), + lookup(Topic, Req, fun format_detail_message/1); + +with_topic(delete, Req) -> + Topic = cowboy_req:binding(topic, Req), + emqx_retainer_mnesia:delete_message(undefined, Topic), + {200}. + +-spec lookup(undefined | binary(), + cowboy_req:req(), + fun((#message{}) -> map())) -> + {200, map()}. +lookup(Topic, Req, Formatter) -> + #{page := Page, + limit := Limit} = cowboy_req:match_qs([{page, int, 1}, + {limit, int, emqx_mgmt:max_row_limit()}], + Req), + {ok, Msgs} = emqx_retainer_mnesia:page_read(undefined, Topic, Page, Limit), + {200, format_message(Msgs, Formatter)}. + + +message_schema(Type, Properties) -> + #{Type => #{type => object, + properties => Properties()}}. + +message_properties() -> + #{msgid => #{type => string, + description => <<"Message ID">>}, + topic => #{type => string, + description => <<"Topic">>}, + qos => #{type => integer, + enum => [0, 1, 2], + description => <<"Qos">>}, + publish_at => #{type => string, + description => <<"publish datetime">>}, + from_clientid => #{type => string, + description => <<"Message from">>}, + from_username => #{type => string, + description => <<"publish username">>}}. + +detail_message_properties() -> + Base = message_properties(), + Base#{payload => #{type => string, + description => <<"Topic">>}}. + +format_message(Messages, Formatter) when is_list(Messages)-> + [Formatter(Message) || Message <- Messages]; + +format_message(Message, Formatter) -> + Formatter(Message). + +format_message(#message{id = ID, qos = Qos, topic = Topic, from = From, timestamp = Timestamp, headers = Headers}) -> + #{msgid => emqx_guid:to_hexstr(ID), + qos => Qos, + topic => Topic, + publish_at => erlang:list_to_binary(emqx_mgmt_util:strftime(Timestamp div 1000)), + from_clientid => to_bin_string(From), + from_username => maps:get(username, Headers, <<>>) + }. + +format_detail_message(#message{payload = Payload} = Msg) -> + Base = format_message(Msg), + Base#{payload => Payload}. + +to_bin_string(Data) when is_binary(Data) -> + Data; +to_bin_string(Data) -> + list_to_binary(io_lib:format("~p", [Data])). + +check_backend(Type, Req, Cont) -> + case emqx:get_config([emqx_retainer, config, type]) of + built_in_database -> + Cont(Type, Req); + _ -> + {405, + #{<<"content-type">> => <<"text/plain">>}, + <<"This API only for built in database">>} + end. diff --git a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl index 1c9956050..5f91a40c9 100644 --- a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl +++ b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl @@ -24,9 +24,10 @@ -include_lib("stdlib/include/qlc.hrl"). --export([delete_message/2 +-export([ delete_message/2 , store_retained/2 , read_message/2 + , page_read/4 , match_messages/3 , clear_expired/1 , clean/1]). @@ -129,6 +130,19 @@ delete_message(_, Topic) -> read_message(_, Topic) -> {ok, read_messages(Topic)}. +page_read(_, Topic, Page, Limit) -> + Cursor = make_cursor(Topic), + case Page > 1 of + true -> + _ = qlc:next_answers(Cursor, (Page - 1) * Limit), + ok; + _ -> + ok + end, + Rows = qlc:next_answers(Cursor, Limit), + qlc:delete_cursor(Cursor), + {ok, Rows}. + match_messages(_, Topic, Cursor) -> MaxReadNum = emqx:get_config([?APP, flow_control, max_read_number]), case Cursor of @@ -152,34 +166,28 @@ clean(_) -> sort_retained([]) -> []; sort_retained([Msg]) -> [Msg]; sort_retained(Msgs) -> - lists:sort(fun(#message{timestamp = Ts1}, #message{timestamp = Ts2}) -> - Ts1 =< Ts2 end, - Msgs). + lists:sort(fun compare_message/2, Msgs). + +compare_message(M1, M2) -> + M1#message.timestamp =< M2#message.timestamp. -%%-------------------------------------------------------------------- -%% Internal funcs -%%-------------------------------------------------------------------- topic2tokens(Topic) -> emqx_topic:words(Topic). -spec start_batch_read(topic(), pos_integer()) -> batch_read_result(). start_batch_read(Topic, MaxReadNum) -> - Ms = make_match_spec(Topic), - TabQH = ets:table(?TAB, [{traverse, {select, Ms}}]), - QH = qlc:q([E || E <- TabQH]), - Cursor = qlc:cursor(QH), + Cursor = make_cursor(Topic), batch_read_messages(Cursor, MaxReadNum). -spec batch_read_messages(emqx_retainer_storage:cursor(), pos_integer()) -> batch_read_result(). batch_read_messages(Cursor, MaxReadNum) -> Answers = qlc:next_answers(Cursor, MaxReadNum), - Orders = sort_retained(Answers), - case erlang:length(Orders) < MaxReadNum of + case erlang:length(Answers) < MaxReadNum of true -> qlc:delete_cursor(Cursor), - {ok, Orders, undefined}; + {ok, Answers, undefined}; _ -> - {ok, Orders, Cursor} + {ok, Answers, Cursor} end. -spec(read_messages(emqx_types:topic()) @@ -217,14 +225,28 @@ condition(Ws) -> _ -> (Ws1 -- ['#']) ++ '_' end. --spec make_match_spec(topic()) -> ets:match_spec(). -make_match_spec(Filter) -> +-spec make_match_spec(undefined | topic()) -> ets:match_spec(). +make_match_spec(Topic) -> NowMs = erlang:system_time(millisecond), - Cond = condition(emqx_topic:words(Filter)), + Cond = + case Topic of + undefined -> + '_'; + _ -> + condition(emqx_topic:words(Topic)) + end, MsHd = #retained{topic = Cond, msg = '$2', expiry_time = '$3'}, [{MsHd, [{'=:=', '$3', 0}], ['$2']}, {MsHd, [{'>', '$3', NowMs}], ['$2']}]. +-spec make_cursor(undefined | topic()) -> qlc:query_cursor(). +make_cursor(Topic) -> + Ms = make_match_spec(Topic), + TabQH = ets:table(?TAB, [{traverse, {select, Ms}}]), + QH = qlc:q([E || E <- TabQH]), + QH2 = qlc:sort(QH, {order, fun compare_message/2}), + qlc:cursor(QH2). + -spec is_table_full() -> boolean(). is_table_full() -> #{max_retained_messages := Limit} = emqx:get_config([?APP, config]), diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl index 6ce64ae2e..5d379e8cd 100644 --- a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl @@ -33,10 +33,11 @@ -define(HOST, "http://127.0.0.1:8081/"). -define(API_VERSION, "v4"). -define(BASE_PATH, "api"). +-define(CFG_URI, "/configs/retainer"). all() -> -%% TODO: V5 API -%% emqx_ct:all(?MODULE). + %% TODO: V5 API + %% emqx_ct:all(?MODULE). []. groups() -> @@ -69,16 +70,12 @@ set_special_configs(_) -> %%------------------------------------------------------------------------------ t_config(_Config) -> - {ok, Return} = request_http_rest_lookup(["retainer"]), + {ok, Return} = request_http_rest_lookup([?CFG_URI]), NowCfg = get_http_data(Return), NewCfg = NowCfg#{<<"msg_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, _} = request_http_rest_update([?CFG_URI], RetainerConf), {ok, UpdateReturn} = request_http_rest_lookup(["retainer"]), ?assertEqual(NewCfg, get_http_data(UpdateReturn)), ok. @@ -141,12 +138,12 @@ receive_messages(Count, Msgs) -> end. switch_emqx_retainer(undefined, IsEnable) -> - {ok, Return} = request_http_rest_lookup(["retainer"]), + {ok, Return} = request_http_rest_lookup([?COMMON_SHARD]), 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), + {ok, _} = request_http_rest_update([?CFG_URI], RetainerConf), NewCfg. From 5520326ce3435fa8b48a99ae1ce213e8f7168df9 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Tue, 17 Aug 2021 13:46:03 +0800 Subject: [PATCH 060/306] feat: add delayed api --- apps/emqx_management/src/emqx_mgmt_util.erl | 4 +- apps/emqx_modules/etc/emqx_modules.conf | 1 + apps/emqx_modules/src/emqx_delayed.erl | 67 +++++- apps/emqx_modules/src/emqx_delayed_api.erl | 229 ++++++++++++-------- apps/emqx_modules/src/emqx_rewrite_api.erl | 17 +- 5 files changed, 224 insertions(+), 94 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 7d5f85cf2..d764afb07 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -140,6 +140,8 @@ response_error_schema(Description, Enum) -> response_page_schema(Def) when is_atom(Def) -> response_page_schema(atom_to_binary(Def, utf8)); response_page_schema(Def) when is_binary(Def) -> + response_page_schema(minirest:ref(Def)); +response_page_schema(ItemSchema) when is_map(ItemSchema) -> Schema = #{ type => object, properties => #{ @@ -154,7 +156,7 @@ response_page_schema(Def) when is_binary(Def) -> type => integer}}}, data => #{ type => array, - items => minirest:ref(Def)}}}, + items => ItemSchema}}}, json_content_schema("", Schema). response_batch_schema(DefName) when is_atom(DefName) -> diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index 92f563342..0200d16c5 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -1,5 +1,6 @@ delayed: { enable: true + ## 0 is no limit max_delayed_messages: 0 } diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index 5e1754f4b..da888e547 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -21,7 +21,6 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). - %% Mnesia bootstrap -export([mnesia/1]). @@ -44,6 +43,11 @@ %% gen_server callbacks -export([ enable/0 , disable/0 + , set_max_delayed_messages/1 + , update_config/2 + , list/1 + , get_delayed_message/1 + , delete_delayed_message/1 ]). -record(delayed_message, {key, msg}). @@ -117,6 +121,64 @@ enable() -> disable() -> gen_server:call(?SERVER, disable). +set_max_delayed_messages(Max) -> + gen_server:call(?SERVER, {set_max_delayed_messages, Max}). + +list(Params) -> + emqx_mgmt_api:paginate(?TAB, Params, fun format_delayed/1). + +format_delayed(Delayed) -> + format_delayed(Delayed, false). + +format_delayed(#delayed_message{key = {TimeStamp, Id}, + msg = #message{topic = Topic, + from = From, + headers = #{username := Username}, + qos = Qos, + payload = Payload}}, WithPayload) -> + Result = #{ + id => emqx_guid:to_hexstr(Id), + publish_time => list_to_binary(calendar:system_time_to_rfc3339(TimeStamp, [{unit, second}])), + topic => Topic, + qos => Qos, + from_clientid => From, + from_username => Username + }, + case WithPayload of + true -> + Result#{payload => Payload}; + _ -> + Result + end. + +get_delayed_message(Id0) -> + Id = emqx_guid:from_hexstr(Id0), + Ms = [{{delayed_message,{'_',Id},'_'},[],['$_']}], + case ets:select(?TAB, Ms) of + [] -> + {error, not_found}; + Rows -> + Message = hd(Rows), + {ok, format_delayed(Message, true)} + end. + +delete_delayed_message(Id0) -> + Id = emqx_guid:from_hexstr(Id0), + Ms = [{{delayed_message, {'$1', Id}, '_'}, [], ['$1']}], + case ets:select(?TAB, Ms) of + [] -> + {error, not_found}; + Rows -> + Timestamp = hd(Rows), + ekka_mnesia:dirty_delete(?TAB, {Timestamp, Id}) + end. + +update_config(Enable, MaxDelayedMessages) -> + Opts0 = emqx_config:get_raw([<<"delayed">>], #{}), + Opts1 = maps:put(<<"enable">>, Enable, Opts0), + Opts = maps:put(<<"max_delayed_messages">>, MaxDelayedMessages, Opts1), + {ok, _} = emqx:update_config([delayed], Opts). + %%-------------------------------------------------------------------- %% gen_server callback %%-------------------------------------------------------------------- @@ -128,6 +190,9 @@ init([Opts]) -> publish_at => 0, max_delayed_messages => MaxDelayedMessages}))}. +handle_call({set_max_delayed_messages, Max}, _From, State) -> + {reply, ok, State#{max_delayed_messages => Max}}; + handle_call({store, DelayedMsg = #delayed_message{key = Key}}, _From, State = #{max_delayed_messages := 0}) -> ok = ekka_mnesia:dirty_write(?TAB, DelayedMsg), diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 06de3ab13..85226701a 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -20,110 +20,133 @@ -import(emqx_mgmt_util, [ response_schema/1 , response_schema/2 + , response_error_schema/2 + , response_page_schema/1 , request_body_schema/1 ]). -% -export([cli/1]). - -export([ status/2 , delayed_messages/2 - , delete_delayed_message/2 + , delayed_message/2 ]). --export([enable_delayed/2]). +%% for rpc +-export([update_config_/2]). -export([api_spec/0]). +-define(ALREADY_ENABLED, 'ALREADY_ENABLED'). +-define(ALREADY_DISABLED, 'ALREADY_DISABLED'). + +-define(BAD_REQUEST, 'BAD_REQUEST'). + +-define(MESSAGE_ID_NOT_FOUND, 'ALREADY_DISABLED'). + api_spec() -> - {[status(), delayed_messages(), delete_delayed_message()], - [delayed_message_schema()]}. + { + [status_api(), delayed_messages_api(), delayed_message_api()], + [] + }. +delayed_schema() -> + delayed_schema(false). -delayed_message_schema() -> - #{broker_info => #{ +delayed_schema(WithPayload) -> + case WithPayload of + true -> + #{ + type => object, + properties => delayed_message_properties() + }; + _ -> + #{ + type => object, + properties => maps:without([payload], delayed_message_properties()) + } + end. + +delayed_message_properties() -> + #{ + id => #{ + type => integer, + description => <<"Message Id (MQTT message id hash)">>}, + publish_time => #{ + type => string, + description => <<"publish time, rfc 3339">>}, + topic => #{ + type => string, + description => <<"Topic">>}, + qos => #{ + type => integer, + enum => [0, 1, 2], + description => <<"Qos">>}, + payload => #{ + type => string, + description => <<"Payload">>}, + form_clientid => #{ + type => string, + description => <<"Client ID">>}, + form_username => #{ + type => string, + description => <<"Username">>} + }. + +status_api() -> + Schema = #{ type => object, properties => #{ - msgid => #{ - type => string, - description => <<"Message Id">> - } - } - }}. - -status() -> + enable => #{ + type => boolean}, + max_delayed_messages => #{ + type => integer, + description => <<"Max limit, 0 is no limit">>}}}, Metadata = #{ get => #{ description => "Get delayed status", responses => #{ - <<"200">> => response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{enable => #{type => boolean}} - } - ) - } - }, + <<"200">> => response_schema(<<"Bad Request">>, Schema)}}, put => #{ - description => "Enable or disbale delayed", - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - enable => #{ - type => boolean - } - } - }), + description => "Enable or disable delayed, set max delayed messages", + 'requestBody' => request_body_schema(Schema), responses => #{ <<"200">> => - response_schema(<<"Enable or disbale delayed successfully">>), + response_schema(<<"Enable or disable delayed successfully">>), <<"400">> => - response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - } - ) - } - } - }, - {"/delayed/status", Metadata, status}. + response_error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED])}}}, + {"/mqtt/delayed_messages/status", Metadata, status}. -delayed_messages() -> +delayed_messages_api() -> Metadata = #{ get => #{ - description => "Get delayed message list", + description => "List delayed messages", responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<>>, delayed_message) - } - } - }, - {"/delayed/messages", Metadata, delayed_messages}. + <<"200">> => response_page_schema(delayed_schema())}}}, + {"/mqtt/delayed_messages", Metadata, delayed_messages}. -delete_delayed_message() -> +delayed_message_api() -> Metadata = #{ - delete => #{ - description => "Delete delayed message", + get => #{ + description => "Get delayed message", parameters => [#{ - name => msgid, + name => id, in => path, schema => #{type => string}, required => true }], responses => #{ - <<"200">> => response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{enable => #{type => boolean}} - } - ) - } - } - }, - {"/delayed/messages/:msgid", Metadata, delete_delayed_message}. - + <<"200">> => response_schema(<<"Get delayed message success">>, delayed_schema(true)), + <<"404">> => response_error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND])}}, + delete => #{ + description => "Delete delayed message", + parameters => [#{ + name => id, + in => path, + schema => #{type => string}, + required => true + }], + responses => #{ + <<"200">> => response_schema(<<"Delete delayed message success">>)}}}, + {"/mqtt/delayed_messages/:id", Metadata, delayed_message}. %%-------------------------------------------------------------------- %% HTTP API @@ -135,33 +158,60 @@ status(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), Enable = maps:get(<<"enable">>, Params), - case Enable =:= get_status() of - true -> - Reason = case Enable of - true -> <<"Telemetry status is already enabled">>; - false -> <<"Telemetry status is already disable">> - end, - {400, #{code => "BAD_REQUEST", message => Reason}}; - false -> - enable_delayed(Enable), - {200} - end. + MaxDelayedMessages = maps:get(<<"max_delayed_messages">>, Params), + update_config(Enable, MaxDelayedMessages). -delayed_messages(get, _Request) -> - {200, []}. +delayed_messages(get, Request) -> + Qs = cowboy_req:parse_qs(Request), + {200, emqx_delayed:list(Qs)}. -delete_delayed_message(delete, _Request) -> +delayed_message(get, Request) -> + Id = cowboy_req:binding(id, Request), + case emqx_delayed:get_delayed_message(Id) of + {ok, Message} -> + {200, Message}; + {error, not_found} -> + Message = list_to_binary(io_lib:format("Message ID ~p not found", [Id])), + {404, #{code => ?MESSAGE_ID_NOT_FOUND, message => Message}} + end; +delayed_message(delete, Request) -> + Id = cowboy_req:binding(id, Request), + _ = emqx_delayed:delete_delayed_message(Id), {200}. %%-------------------------------------------------------------------- %% internal function %%-------------------------------------------------------------------- -enable_delayed(Enable) -> +get_status() -> + #{ + enable => emqx:get_config([delayed, enable], true), + max_delayed_messages => emqx:get_config([delayed, max_delayed_messages], 0) + }. + +update_config(Enable, MaxDelayedMessages) when MaxDelayedMessages >= 0 -> + case Enable =:= maps:get(enable, get_status()) of + true -> + update_config_error_response(Enable); + _ -> + update_config_(Enable, MaxDelayedMessages), + {200} + end; +update_config(_Enable, _MaxDelayedMessages) -> + {400, #{code => ?BAD_REQUEST, message => <<"Max delayed must be equal or greater than 0">>}}. + +update_config_error_response(true) -> + {400, #{code => ?ALREADY_ENABLED, message => <<"Delayed message status is already enabled">>}}; +update_config_error_response(false) -> + {400, #{code => ?ALREADY_DISABLED, message => <<"Delayed message status is already disable">>}}. + +update_config_(Enable, MaxDelayedMessages) -> lists:foreach(fun(Node) -> - enable_delayed(Node, Enable) + update_config_(Node, Enable, MaxDelayedMessages) end, ekka_mnesia:running_nodes()). -enable_delayed(Node, Enable) when Node =:= node() -> +update_config_(Node, Enable, MaxDelayedMessages) when Node =:= node() -> + _ = emqx_delayed:update_config(Enable, MaxDelayedMessages), + ok = emqx_delayed:set_max_delayed_messages(MaxDelayedMessages), case Enable of true -> emqx_delayed:enable(); @@ -169,14 +219,11 @@ enable_delayed(Node, Enable) when Node =:= node() -> emqx_delayed:disable() end; -enable_delayed(Node, Enable) -> - rpc_call(Node, ?MODULE, enable_delayed, [Node, Enable]). +update_config_(Node, Enable, MaxDelayedMessages) -> + rpc_call(Node, ?MODULE, update_config_, [Node, Enable, MaxDelayedMessages]). rpc_call(Node, Module, Fun, Args) -> case rpc:call(Node, Module, Fun, Args) of {badrpc, Reason} -> {error, Reason}; Result -> Result end. - -get_status() -> - emqx_config:get([delayed, enable], true). diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl index 2be28a081..9b07b0a93 100644 --- a/apps/emqx_modules/src/emqx_rewrite_api.erl +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -1,3 +1,18 @@ +%%-------------------------------------------------------------------- +%% 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_rewrite_api). -behaviour(minirest_api). @@ -44,7 +59,7 @@ rewrite_api() -> post => #{ description => <<"Update topic rewrite">>, 'requestBody' => emqx_mgmt_util:request_body_array_schema(topic_rewrite_schema()), - response => #{ + responses => #{ <<"200">> => emqx_mgmt_util:response_schema(<<"Update topic rewrite success">>, topic_rewrite_schema()), <<"413">> => emqx_mgmt_util:response_error_schema(<<"Rules count exceed max limit">>, [?EXCEED_LIMIT])}}}, From e17612b2377b25808494b0da982550390ad4a387 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 19 Aug 2021 09:45:14 +0800 Subject: [PATCH 061/306] fix(config): return only updated confs for emqx:update_config/2,3 --- apps/emqx/src/emqx_config_handler.erl | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index 2ee90cb04..a7c28fff0 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -89,8 +89,8 @@ handle_call({add_child, ConfKeyPath, HandlerName}, _From, handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> - OldConf = emqx_config:get([]), - OldRawConf = emqx_config:get_raw([]), + OldConf = emqx_config:get_root(ConfKeyPath), + OldRawConf = emqx_config:get_root_raw(ConfKeyPath), Reply = try case process_update_request(ConfKeyPath, OldRawConf, Handlers, UpdateArgs) of {ok, NewRawConf, OverrideConf} -> @@ -151,7 +151,8 @@ check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OldConf, {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf), case do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateArgs, #{}) of {ok, Result0} -> - case save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs) of + case save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, + UpdateArgs) of {ok, Result1} -> {ok, Result1#{post_config_update => Result0}}; Error -> Error @@ -200,9 +201,10 @@ call_post_config_update(Handlers, OldConf, NewConf, UpdateReq, Result) -> false -> {ok, Result} end. -save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf, {_Cmd, Opts}) -> +save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, {_Cmd, Opts}) -> case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of - ok -> {ok, #{config => emqx_config:get([]), raw_config => return_rawconf(Opts)}}; + ok -> {ok, #{config => emqx_config:get(ConfKeyPath), + raw_config => return_rawconf(ConfKeyPath, Opts)}}; {error, Reason} -> {error, {save_configs, Reason}} end. @@ -223,10 +225,11 @@ update_override_config(RawConf) -> up_req({remove, _Opts}) -> '$remove'; up_req({{update, Req}, _Opts}) -> Req. -return_rawconf(#{rawconf_with_defaults := true}) -> - emqx_config:fill_defaults(emqx_config:get_raw([])); -return_rawconf(_) -> - emqx_config:get_raw([]). +return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) -> + FullRawConf = emqx_config:fill_defaults(emqx_config:get_raw([])), + emqx_map_lib:deep_get(bin_path(ConfKeyPath), FullRawConf); +return_rawconf(ConfKeyPath, _) -> + emqx_config:get_raw(ConfKeyPath). bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath]. From ef59309ed0b7a8d183b521f28ce4dfc2ac85a943 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 19 Aug 2021 15:29:34 +0800 Subject: [PATCH 062/306] fix(config): check config failed when updating --- apps/emqx/src/emqx_config_handler.erl | 26 ++++++++++++------- apps/emqx/src/emqx_map_lib.erl | 2 +- .../src/emqx_mgmt_api_configs.erl | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index a7c28fff0..7c66656ce 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -89,12 +89,10 @@ handle_call({add_child, ConfKeyPath, HandlerName}, _From, handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> - OldConf = emqx_config:get_root(ConfKeyPath), - OldRawConf = emqx_config:get_root_raw(ConfKeyPath), Reply = try - case process_update_request(ConfKeyPath, OldRawConf, Handlers, UpdateArgs) of + case process_update_request(ConfKeyPath, Handlers, UpdateArgs) of {ok, NewRawConf, OverrideConf} -> - check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OldConf, + check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OverrideConf, UpdateArgs); {error, Result} -> {error, Result} @@ -121,12 +119,14 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -process_update_request(ConfKeyPath, OldRawConf, _Handlers, {remove, _Opts}) -> +process_update_request(ConfKeyPath, _Handlers, {remove, _Opts}) -> + OldRawConf = emqx_config:get_root_raw(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath), NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf), OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()), {ok, NewRawConf, OverrideConf}; -process_update_request(ConfKeyPath, OldRawConf, Handlers, {{update, UpdateReq}, _Opts}) -> +process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, _Opts}) -> + OldRawConf = emqx_config:get_root_raw(ConfKeyPath), case do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) of {ok, NewRawConf} -> OverrideConf = update_override_config(NewRawConf), @@ -146,12 +146,15 @@ do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) -> Error end. -check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OldConf, OverrideConf, +check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, OverrideConf, UpdateArgs) -> - {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, NewRawConf), - case do_post_config_update(ConfKeyPath, Handlers, OldConf, CheckedConf, UpdateArgs, #{}) of + OldConf = emqx_config:get_root(ConfKeyPath), + FullRawConf = with_full_raw_confs(NewRawConf), + {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, FullRawConf), + NewConf = maps:with(maps:keys(OldConf), CheckedConf), + case do_post_config_update(ConfKeyPath, Handlers, OldConf, NewConf, UpdateArgs, #{}) of {ok, Result0} -> - case save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, + case save_configs(ConfKeyPath, AppEnvs, NewConf, NewRawConf, OverrideConf, UpdateArgs) of {ok, Result1} -> {ok, Result1#{post_config_update => Result0}}; @@ -231,6 +234,9 @@ return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) -> return_rawconf(ConfKeyPath, _) -> emqx_config:get_raw(ConfKeyPath). +with_full_raw_confs(PartialConf) -> + maps:merge(emqx_config:get_raw([]), PartialConf). + bin_path(ConfKeyPath) -> [bin(Key) || Key <- ConfKeyPath]. bin(A) when is_atom(A) -> atom_to_binary(A, utf8); diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index e5baeb850..468553193 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -118,7 +118,7 @@ unsafe_atom_key_map(Map) -> safe_atom_key_map(Map) -> covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). --spec jsonable_map(map()) -> map(). +-spec jsonable_map(map() | list()) -> map() | list(). jsonable_map(Map) -> deep_convert(Map, fun(K, V) -> {jsonable_value(K), jsonable_value(V)} diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 1a89835ff..46b679c3d 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -120,7 +120,7 @@ config(put, Req) -> Path = conf_path(Req), {ok, #{raw_config := RawConf}} = emqx:update_config(Path, http_body(Req), #{rawconf_with_defaults => true}), - {200, emqx_map_lib:deep_get(Path, emqx_map_lib:jsonable_map(RawConf))}. + {200, emqx_map_lib:jsonable_map(RawConf)}. config_reset(post, Req) -> %% reset the config specified by the query string param 'conf_path' From 61da3a4fd70e7716dc7022347eae9d13306db69c Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 10 Aug 2021 14:47:22 +0800 Subject: [PATCH 063/306] feat(authn hot config): initial support for hot config --- apps/emqx_authn/src/emqx_authn.erl | 102 +++++++++++++++++- apps/emqx_authn/src/emqx_authn_api.erl | 48 +++------ apps/emqx_authn/src/emqx_authn_app.erl | 1 + ...hema.erl => emqx_authn_implied_schema.erl} | 2 +- 4 files changed, 118 insertions(+), 35 deletions(-) rename apps/emqx_authn/src/simple_authn/{emqx_authn_other_schema.erl => emqx_authn_implied_schema.erl} (97%) diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 034e06b89..7ead53638 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -16,8 +16,17 @@ -module(emqx_authn). +-behaviour(emqx_config_handler). + -include("emqx_authn.hrl"). +-export([mnesia/1]). + +-export([ pre_config_update/2 + , post_config_update/3 + , update_config/2 + ]). + -export([ enable/0 , disable/0 , is_enabled/0 @@ -46,8 +55,6 @@ , list_users/2 ]). --export([mnesia/1]). - -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). @@ -75,6 +82,97 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies). +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +pre_config_update({enable, Enable}, _OldConfig) -> + Enable; +pre_config_update({create_authenticator, Config}, OldConfig) -> + OldConfig ++ [Config]; +pre_config_update({delete_authenticator, ID}, OldConfig) -> + case lookup_authenticator(?CHAIN, ID) of + {error, Reason} -> error(Reason); + {ok, #{name := Name}} -> + lists:filter(fun(#{<<"name">> := N}) -> + N =/= Name + end, OldConfig) + end; +pre_config_update({update_authenticator, ID, Config}, OldConfig) -> + case lookup_authenticator(?CHAIN, ID) of + {error, Reason} -> error(Reason); + {ok, #{name := Name}} -> + lists:map(fun(#{<<"name">> := N} = C) -> + case N =:= Name of + true -> Config; + false -> C + end + end, OldConfig) + end; +pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) -> + case lookup_authenticator(?CHAIN, ID) of + {error, _Reason} -> OldConfig ++ [Config]; + {ok, #{name := Name}} -> + lists:map(fun(#{<<"name">> := N} = C) -> + case N =:= Name of + true -> Config; + false -> C + end + end, OldConfig) + end. + +post_config_update({enable, true}, _NewConfig, _OldConfig) -> + emqx_authn:enable(); +post_config_update({enable, false}, _NewConfig, _OldConfig) -> + emqx_authn:disable(); +post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> + case lists:filter( + fun(#{name := N}) -> + N =:= Name + end, NewConfig) of + [Config] -> + case create_authenticator(?CHAIN, Config) of + {ok, _} -> ok; + {error, Reason} -> throw(Reason) + end; + [_Config | _] -> + error(name_has_be_used) + end; +post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig) -> + case delete_authenticator(?CHAIN, ID) of + ok -> ok; + {error, Reason} -> throw(Reason) + end; +post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> + case lists:filter( + fun(#{name := N}) -> + N =:= Name + end, NewConfig) of + [Config] -> + case update_authenticator(?CHAIN, ID, Config) of + {ok, _} -> ok; + {error, Reason} -> throw(Reason) + end; + [_Config | _] -> + error(name_has_be_used) + end; +post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> + case lists:filter( + fun(#{name := N}) -> + N =:= Name + end, NewConfig) of + [Config] -> + case update_or_create_authenticator(?CHAIN, ID, Config) of + {ok, _} -> ok; + {error, Reason} -> throw(Reason) + end; + [_Config | _] -> + error(name_has_be_used) + end. + +update_config(Path, ConfigRequest) -> + emqx_config:update(emqx_authn_schema, Path, ConfigRequest). + enable() -> case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of ok -> ok; diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 78ef5fd35..1e232d553 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1253,14 +1253,9 @@ definitions() -> authentication(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of - #{<<"enable">> := true} -> - ok = emqx_authn:enable(), + #{<<"enable">> := Enable} -> + emqx_authn:update_config([authentication, enable], {enable, Enable}), {204}; - #{<<"enable">> := false} -> - ok = emqx_authn:disable(), - {204}; - #{<<"enable">> := _} -> - serialize_error({invalid_parameter, enable}); _ -> serialize_error({missing_parameter, enable}) end; @@ -1270,16 +1265,10 @@ authentication(get, _Request) -> authenticators(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), - AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), - Config = #{<<"authentication">> => #{ - <<"authenticators">> => [AuthenticatorConfig] - }}, - NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, - #{nullable => true}), - #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), - case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of - {ok, Authenticator2} -> - {201, Authenticator2}; + Config = emqx_json:decode(Body, [return_maps]), + case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of + ok -> + {204}; {error, Reason} -> serialize_error(Reason) end; @@ -1298,22 +1287,17 @@ authenticators2(get, Request) -> authenticators2(put, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), - Config = #{<<"authentication">> => #{ - <<"authenticators">> => [AuthenticatorConfig] - }}, - NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, - #{nullable => true}), - #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), - case emqx_authn:update_or_create_authenticator(?CHAIN, AuthenticatorID, NAuthenticatorConfig) of - {ok, Authenticator} -> - {200, Authenticator}; + Config = emqx_json:decode(Body, [return_maps]), + case emqx_authn:update_config([authentication, authenticators], + {update_or_create_authenticator, AuthenticatorID, Config}) of + ok -> + {204}; {error, Reason} -> serialize_error(Reason) end; authenticators2(delete, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), - case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of + case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of ok -> {204}; {error, Reason} -> @@ -1324,7 +1308,7 @@ position(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody}, + Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"position">> => NBody}, #{nullable => true}, ["position"]), #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config), case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of @@ -1338,7 +1322,7 @@ import_users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody}, + Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"filename">> => NBody}, #{nullable => true}, ["filename"]), #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config), case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of @@ -1352,7 +1336,7 @@ users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody}, + Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"user_info">> => NBody}, #{nullable => true}, ["user_info"]), #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of @@ -1375,7 +1359,7 @@ users2(patch, Request) -> UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody}, + Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"new_user_info">> => NBody}, #{nullable => true}, ["new_user_info"]), #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 7518e5a01..b7f409bc9 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -29,6 +29,7 @@ start(_StartType, _StartArgs) -> ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), {ok, Sup} = emqx_authn_sup:start_link(), + emqx_config_handler:add_handler([authentication, authenticators], emqx_authn), initialize(), {ok, Sup}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_implied_schema.erl similarity index 97% rename from apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_implied_schema.erl index 0f5c8abb8..1a0731e92 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_implied_schema.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authn_other_schema). +-module(emqx_authn_implied_schema). -include("emqx_authn.hrl"). -include_lib("typerefl/include/types.hrl"). From 60f0e8e5a5bb305588482888c529f76f243c066e Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 10 Aug 2021 17:04:20 +0800 Subject: [PATCH 064/306] refactor(authn): replace mnesia with ets table --- apps/emqx_authn/src/emqx_authn.erl | 465 ++++++++++++++----------- apps/emqx_authn/src/emqx_authn_sup.erl | 9 +- 2 files changed, 273 insertions(+), 201 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 7ead53638..703b0efcf 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -16,11 +16,12 @@ -module(emqx_authn). +-behaviour(gen_server). + -behaviour(emqx_config_handler). -include("emqx_authn.hrl"). - --export([mnesia/1]). +-include_lib("emqx/include/logger.hrl"). -export([ pre_config_update/2 , post_config_update/3 @@ -34,6 +35,10 @@ -export([authenticate/2]). +-export([ start_link/0 + , stop/0 + ]). + -export([ create_chain/1 , delete_chain/1 , lookup_chain/1 @@ -55,33 +60,17 @@ , list_users/2 ]). --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). -define(CHAIN_TAB, emqx_authn_chain). --rlog_shard({?AUTH_SHARD, ?CHAIN_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}]); - -mnesia(copy) -> - ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies). - %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ @@ -192,7 +181,7 @@ is_enabled() -> end, Callbacks). authenticate(Credential, _AuthResult) -> - case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of + case ets:lookup(?CHAIN_TAB, ?CHAIN) of [#chain{authenticators = Authenticators}] -> do_authenticate(Authenticators, Credential); [] -> @@ -214,154 +203,39 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo {stop, Result} end. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:stop(?MODULE). + create_chain(#{id := ID}) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - Chain = #chain{id = ID, - authenticators = [], - created_at = erlang:system_time(millisecond)}, - mnesia:write(?CHAIN_TAB, Chain, write), - {ok, serialize_chain(Chain)}; - [_ | _] -> - {error, {already_exists, {chain, ID}}} - end - end). + gen_server:call(?MODULE, {create_chain, ID}). 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). + gen_server:call(?MODULE, {delete_chain, ID}). lookup_chain(ID) -> - case mnesia:dirty_read(?CHAIN_TAB, ID) of - [] -> - {error, {not_found, {chain, ID}}}; - [Chain] -> - {ok, serialize_chain(Chain)} - end. + gen_server:call(?MODULE, {lookup_chain, ID}). list_chains() -> Chains = ets:tab2list(?CHAIN_TAB), {ok, [serialize_chain(Chain) || Chain <- Chains]}. -create_authenticator(ChainID, #{name := Name} = Config) -> - UpdateFun = - fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keymember(Name, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - AlreadyExist = fun(ID) -> - lists:keymember(ID, 1, Authenticators) - end, - AuthenticatorID = gen_id(AlreadyExist), - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}], - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end, - update_chain(ChainID, UpdateFun). +create_authenticator(ChainID, Config) -> + gen_server:call(?MODULE, {create_authenticator, ChainID, Config}). delete_authenticator(ChainID, AuthenticatorID) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {value, {_, _, Authenticator}, NAuthenticators} -> - _ = do_delete_authenticator(Authenticator), - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write) - end - end, - update_chain(ChainID, UpdateFun). + gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}). update_authenticator(ChainID, AuthenticatorID, Config) -> - do_update_authenticator(ChainID, AuthenticatorID, Config, false). + gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}). update_or_create_authenticator(ChainID, AuthenticatorID, Config) -> - do_update_authenticator(ChainID, AuthenticatorID, Config, true). - -do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - case CreateWhenNotFound of - true -> - case lists:keymember(NewName, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end; - false -> - {error, {not_found, {authenticator, AuthenticatorID}}} - end; - {value, - {_, _, #authenticator{provider = Provider, - state = #{version := Version} = State} = Authenticator}, - Others} -> - case lists:keymember(NewName, 2, Others) of - true -> - {error, name_has_be_used}; - false -> - case (NewProvider = authenticator_provider(Config)) =:= Provider of - true -> - Unique = <>, - case Provider:update(Config#{'_unique' => Unique}, State) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - config = Config, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end; - false -> - Unique = <>, - case NewProvider:create(Config#{'_unique' => Unique}) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - provider = NewProvider, - config = Config, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), - _ = Provider:destroy(State), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end - end - end, - update_chain(ChainID, UpdateFun). + gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}). lookup_authenticator(ChainID, AuthenticatorID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + case ets:lookup(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; [#chain{authenticators = Authenticators}] -> @@ -374,7 +248,7 @@ lookup_authenticator(ChainID, AuthenticatorID) -> end. list_authenticators(ChainID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + case ets:lookup(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; [#chain{authenticators = Authenticators}] -> @@ -382,34 +256,172 @@ list_authenticators(ChainID) -> end. move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of - {ok, NAuthenticators} -> - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write); + gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, N}). + +import_users(ChainID, AuthenticatorID, Filename) -> + gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}). + +add_user(ChainID, AuthenticatorID, UserInfo) -> + gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}). + +delete_user(ChainID, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}). + +update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> + gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}). + +lookup_user(ChainID, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}). + +%% TODO: Support pagination +list_users(ChainID, AuthenticatorID) -> + gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init(_Opts) -> + ets:new(?CHAIN_TAB, [ named_table, set, public + , {keypos, #chain.id} + , {read_concurrency, true}]), + {ok, #{}}. + +handle_call({create_chain, ID}, _From, State) -> + case ets:member(?CHAIN_TAB, ID) of + true -> + reply({error, {already_exists, {chain, ID}}}, State); + false -> + Chain = #chain{id = ID, + authenticators = [], + created_at = erlang:system_time(millisecond)}, + true = ets:insert(?CHAIN_TAB, Chain), + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({delete_chain, ID}, _From, State) -> + case ets:lookup(?CHAIN_TAB, ID) of + [] -> + reply({error, {not_found, {chain, ID}}}, State); + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators], + true = ets:delete(?CHAIN_TAB, ID), + reply(ok, State) + end; + +handle_call({lookup_chain, ID}, _From, State) -> + case ets:lookup(?CHAIN_TAB, ID) of + [] -> + reply({error, {not_found, {chain, ID}}}, State); + [Chain] -> + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keymember(Name, 2, Authenticators) of + true -> + {error, name_has_be_used}; + false -> + AlreadyExist = fun(ID) -> + lists:keymember(ID, 1, Authenticators) + end, + AuthenticatorID = gen_id(AlreadyExist), + case do_create_authenticator(ChainID, AuthenticatorID, Config) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}], + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), + {ok, serialize_authenticator(Authenticator)}; {error, Reason} -> {error, Reason} end - end, - update_chain(ChainID, UpdateFun). + end + end, + Reply = update_chain(ChainID, UpdateFun), + reply(Reply, State); -import_users(ChainID, AuthenticatorID, Filename) -> - call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]). +handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keytake(AuthenticatorID, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, {_, _, Authenticator}, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), + ok + end + end, + Reply = update_chain(ChainID, UpdateFun), + reply(Reply, State); -add_user(ChainID, AuthenticatorID, UserInfo) -> - call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]). +handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> + Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false), + reply(Reply, State); -delete_user(ChainID, AuthenticatorID, UserID) -> - call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]). +handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> + Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true), + reply(Reply, State); -update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> - call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]). +handle_call({move_authenticator, ChainID, AuthenticatorID, N}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of + {ok, NAuthenticators} -> + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), + ok; + {error, Reason} -> + {error, Reason} + end + end, + Reply = update_chain(ChainID, UpdateFun), + reply(Reply, State); -lookup_user(ChainID, AuthenticatorID, UserID) -> - call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]). +handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]), + reply(Reply, State); -list_users(ChainID, AuthenticatorID) -> - call_authenticator(ChainID, AuthenticatorID, list_users, []). +handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]), + reply(Reply, State); + +handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]), + reply(Reply, State); + +handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]), + reply(Reply, State); + +handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]), + reply(Reply, State); + +handle_call({list_users, ChainID, AuthenticatorID}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []), + reply(Reply, State); + +handle_call(Req, _From, State) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Req, State) -> + ?LOG(error, "Unexpected case: ~p", [Req]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +reply(Reply, State) -> + {reply, Reply, State}. %%------------------------------------------------------------------------------ %% Internal functions @@ -464,6 +476,72 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> _ = Provider:destroy(State), ok. + +update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keytake(AuthenticatorID, 1, Authenticators) of + false -> + case CreateWhenNotFound of + true -> + case lists:keymember(NewName, 2, Authenticators) of + true -> + {error, name_has_be_used}; + false -> + case do_create_authenticator(ChainID, AuthenticatorID, Config) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end; + false -> + {error, {not_found, {authenticator, AuthenticatorID}}} + end; + {value, + {_, _, #authenticator{provider = Provider, + state = #{version := Version} = State} = Authenticator}, + Others} -> + case lists:keymember(NewName, 2, Others) of + true -> + {error, name_has_be_used}; + false -> + case (NewProvider = authenticator_provider(Config)) =:= Provider of + true -> + Unique = <>, + case Provider:update(Config#{'_unique' => Unique}, State) of + {ok, NewState} -> + NewAuthenticator = Authenticator#authenticator{name = NewName, + config = Config, + state = switch_version(NewState)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end; + false -> + Unique = <>, + case NewProvider:create(Config#{'_unique' => Unique}) of + {ok, NewState} -> + NewAuthenticator = Authenticator#authenticator{name = NewName, + provider = NewProvider, + config = Config, + state = switch_version(NewState)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), + _ = Provider:destroy(State), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end + end + end, + update_chain(ChainID, UpdateFun). replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). @@ -487,21 +565,16 @@ move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passe move_authenticator_to_the_nth_(AuthenticatorID, 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). - -call_authenticator(ChainID, AuthenticatorID, Func, Args) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + case ets:lookup(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> + [Chain] -> + UpdateFun(Chain) + end. + +call_authenticator(ChainID, AuthenticatorID, Func, Args) -> + UpdateFun = + fun(#chain{authenticators = Authenticators}) -> case lists:keyfind(AuthenticatorID, 1, Authenticators) of false -> {error, {not_found, {authenticator, AuthenticatorID}}}; @@ -513,7 +586,8 @@ call_authenticator(ChainID, AuthenticatorID, Func, Args) -> {error, unsupported_feature} end end - end. + end, + update_chain(ChainID, UpdateFun). serialize_chain(#chain{id = ID, authenticators = Authenticators, @@ -528,12 +602,3 @@ serialize_authenticators(Authenticators) -> serialize_authenticator(#authenticator{id = ID, config = Config}) -> Config#{id => ID}. - -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_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl index bb26af0ad..56fcf299a 100644 --- a/apps/emqx_authn/src/emqx_authn_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -26,4 +26,11 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 10, 10}, []}}. + ChildSpecs = [ + #{id => emqx_authn, + start => {emqx_authn, start_link, []}, + restart => permanent, + type => worker, + modules => [emqx_authn]} + ], + {ok, {{one_for_one, 10, 10}, ChildSpecs}}. From b7bc8b8cacf1cbced204af014cc72eb3ca4ba642 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 12 Aug 2021 13:34:45 +0800 Subject: [PATCH 065/306] feat(authn): improve apis of moving authenticators --- apps/emqx_authn/src/emqx_authn.erl | 113 +++++++++++++----- apps/emqx_authn/src/emqx_authn_api.erl | 62 ++++++---- .../emqx_authn_implied_schema.erl | 9 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 14 ++- .../test/emqx_authn_mnesia_SUITE.erl | 2 +- 5 files changed, 136 insertions(+), 64 deletions(-) rename apps/emqx_authn/src/{simple_authn => }/emqx_authn_implied_schema.erl (83%) diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 703b0efcf..90114c269 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -49,7 +49,7 @@ , update_or_create_authenticator/3 , lookup_authenticator/2 , list_authenticators/1 - , move_authenticator_to_the_nth/3 + , move_authenticator/3 ]). -export([ import_users/3 @@ -108,6 +108,30 @@ pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) -> false -> C end end, OldConfig) + end; +pre_config_update({move, ID, Position}, OldConfig) -> + case lookup_authenticator(?CHAIN, ID) of + {error, Reason} -> error(Reason); + {ok, #{name := Name}} -> + {ok, Found, Part1, Part2} = split_by_name(Name, OldConfig), + case Position of + <<"top">> -> + [Found | Part1] ++ Part2; + <<"bottom">> -> + Part1 ++ Part2 ++ [Found]; + Before -> + case binary:split(Before, <<":">>, [global]) of + [<<"before">>, ID0] -> + case lookup_authenticator(?CHAIN, ID0) of + {error, Reason} -> error(Reason); + {ok, #{name := Name1}} -> + {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 + Part2), + NPart1 ++ [Found, NFound | NPart2] + end; + _ -> + error({invalid_parameter, position}) + end + end end. post_config_update({enable, true}, _NewConfig, _OldConfig) -> @@ -157,6 +181,22 @@ post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, end; [_Config | _] -> error(name_has_be_used) + end; +post_config_update({move, ID, Position}, _NewConfig, _OldConfig) -> + NPosition = case Position of + <<"top">> -> top; + <<"bottom">> -> bottom; + Before -> + case binary:split(Before, <<":">>, [global]) of + [<<"before">>, ID0] -> + {before, ID0}; + _ -> + error({invalid_parameter, position}) + end + end, + case move_authenticator(?CHAIN, ID, NPosition) of + ok -> ok; + {error, Reason} -> throw(Reason) end. update_config(Path, ConfigRequest) -> @@ -255,8 +295,8 @@ list_authenticators(ChainID) -> {ok, serialize_authenticators(Authenticators)} end. -move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) -> - gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, N}). +move_authenticator(ChainID, AuthenticatorID, Position) -> + gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}). import_users(ChainID, AuthenticatorID, Filename) -> gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}). @@ -364,16 +404,16 @@ handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true), reply(Reply, State); -handle_call({move_authenticator, ChainID, AuthenticatorID, N}, _From, State) -> +handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) -> UpdateFun = fun(#chain{authenticators = Authenticators} = Chain) -> - case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of - {ok, NAuthenticators} -> - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - ok; - {error, Reason} -> - {error, Reason} - end + case do_move_authenticator(AuthenticatorID, Authenticators, Position) of + {ok, NAuthenticators} -> + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), + ok; + {error, Reason} -> + {error, Reason} + end end, Reply = update_chain(ChainID, UpdateFun), reply(Reply, State); @@ -458,6 +498,21 @@ switch_version(State = #{version := ?VER_2}) -> switch_version(State) -> State#{version => ?VER_1}. +split_by_name(Name, Config) -> + {Part1, Part2, true} = lists:foldl( + fun(#{<<"name">> := N} = C, {P1, P2, F0}) -> + F = case N =:= Name of + true -> true; + false -> F0 + end, + case F of + false -> {[C | P1], P2, F}; + true -> {P1, [C | P2], F} + end + end, {[], [], false}, Config), + [Found | NPart2] = lists:reverse(Part2), + {ok, Found, lists:reverse(Part1), NPart2}. + do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> Provider = authenticator_provider(Config), Unique = <>, @@ -546,23 +601,27 @@ update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Co replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). -move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) - when N =< length(Authenticators) andalso N > 0 -> - move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N, []); -move_authenticator_to_the_nth_(_, _, _) -> - {error, out_of_range}. +do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) -> + case lists:keytake(AuthenticatorID, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, Authenticator, NAuthenticators} -> + do_move_authenticator(Authenticator, NAuthenticators, Position) + end; -move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) -> - {error, {not_found, {authenticator, AuthenticatorID}}}; -move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = 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_(AuthenticatorID, [{AuthenticatorID, _, _} = 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_(AuthenticatorID, [Authenticator | More], N, Passed) -> - move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]). +do_move_authenticator(Authenticator, Authenticators, top) -> + {ok, [Authenticator | Authenticators]}; +do_move_authenticator(Authenticator, Authenticators, bottom) -> + {ok, Authenticators ++ [Authenticator]}; +do_move_authenticator(Authenticator, Authenticators, {before, ID}) -> + insert(Authenticator, Authenticators, ID, []). + +insert(_, [], ID, _) -> + {error, {not_found, {authenticator, ID}}}; +insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) -> + {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]}; +insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) -> + insert(Authenticator, More, ID, [Authenticator0 | Acc]). update_chain(ChainID, UpdateFun) -> case ets:lookup(?CHAIN_TAB, ChainID) of diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 1e232d553..2503adf10 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -24,7 +24,7 @@ , authentication/2 , authenticators/2 , authenticators2/2 - , position/2 + , move/2 , import_users/2 , users/2 , users2/2 @@ -109,7 +109,7 @@ api_spec() -> {[ authentication_api() , authenticators_api() , authenticators_api2() - , position_api() + , move_api() , import_users_api() , users_api() , users2_api() @@ -405,10 +405,10 @@ authenticators_api2() -> }, {"/authentication/authenticators/:id", Metadata, authenticators2}. -position_api() -> +move_api() -> Metadata = #{ post => #{ - description => "Change the order of authenticators", + description => "Move authenticator", parameters => [ #{ name => id, @@ -423,14 +423,30 @@ position_api() -> content => #{ 'application/json' => #{ schema => #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => integer, - example => 1 + oneOf => [ + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + enum => [<<"top">>, <<"bottom">>], + example => <<"top">> + } + } + }, + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + description => <<"before:">>, + example => <<"before:67e4c9d3">> + } + } } - } + ] } } } @@ -444,7 +460,7 @@ position_api() -> } } }, - {"/authentication/authenticators/:id/position", Metadata, position}. + {"/authentication/authenticators/:id/move", Metadata, move}. import_users_api() -> Metadata = #{ @@ -1304,18 +1320,17 @@ authenticators2(delete, Request) -> serialize_error(Reason) end. -position(post, Request) -> +move(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"position">> => NBody}, - #{nullable => true}, ["position"]), - #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{<<"position">> := Position} -> + case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, position}) end. import_users(post, Request) -> @@ -1393,9 +1408,6 @@ serialize_error({not_found, {authenticator, ID}}) -> serialize_error(name_has_be_used) -> {409, #{code => <<"ALREADY_EXISTS">>, message => <<"Name has be used">>}}; -serialize_error(out_of_range) -> - {400, #{code => <<"OUT_OF_RANGE">>, - message => <<"Out of range">>}}; serialize_error({missing_parameter, Name}) -> {400, #{code => <<"MISSING_PARAMETER">>, message => list_to_binary( diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_implied_schema.erl b/apps/emqx_authn/src/emqx_authn_implied_schema.erl similarity index 83% rename from apps/emqx_authn/src/simple_authn/emqx_authn_implied_schema.erl rename to apps/emqx_authn/src/emqx_authn_implied_schema.erl index 1a0731e92..65b4bf356 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_implied_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_implied_schema.erl @@ -25,12 +25,10 @@ , fields/1 ]). -structs() -> [ "filename", "position", "user_info", "new_user_info"]. +structs() -> [ "filename", "user_info", "new_user_info"]. fields("filename") -> [ {filename, fun filename/1} ]; -fields("position") -> - [ {position, fun position/1} ]; fields("user_info") -> [ {user_id, fun user_id/1} , {password, fun password/1} @@ -43,11 +41,6 @@ filename(type) -> string(); filename(nullable) -> false; filename(_) -> undefined. -position(type) -> integer(); -position(validate) -> [fun (Position) -> Position > 0 end]; -position(nullable) -> false; -position(_) -> undefined. - user_id(type) -> binary(); user_id(nullable) -> false; user_id(_) -> undefined. diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 92e506d51..9c4371838 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -86,10 +86,18 @@ t_authenticator(_) -> ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), + ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 0)), + + ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, bottom)), + ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), + + ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})), + + ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), + + ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 4a5a24844..fdcaf519d 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -144,7 +144,7 @@ t_multi_mnesia_authenticator(_) -> clientid => <<"myclient">>, password => <<"mypass1">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), + ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, From 3f2ca5282c3db97357b3bd1abe9b20397bad22bb Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 12 Aug 2021 13:48:25 +0800 Subject: [PATCH 066/306] chore(authn): remove implied schema --- apps/emqx_authn/src/emqx_authn_api.erl | 61 +++++++++++-------- .../src/emqx_authn_implied_schema.erl | 51 ---------------- 2 files changed, 34 insertions(+), 78 deletions(-) delete mode 100644 apps/emqx_authn/src/emqx_authn_implied_schema.erl diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 2503adf10..c4a12309e 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1336,29 +1336,35 @@ move(post, Request) -> import_users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"filename">> => NBody}, - #{nullable => true}, ["filename"]), - #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{<<"filename">> := Filename} -> + case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, filename}) end. users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"user_info">> => NBody}, - #{nullable => true}, ["user_info"]), - #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of - {ok, User} -> - {201, User}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{ <<"user_id">> := UserID + , <<"password">> := Password} -> + case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID + , password => Password}) of + {ok, User} -> + {201, User}; + {error, Reason} -> + serialize_error(Reason) + end; + #{<<"user_id">> := _} -> + serialize_error({missing_parameter, password}); + _ -> + serialize_error({missing_parameter, user_id}) end; users(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), @@ -1373,15 +1379,16 @@ users2(patch, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_implied_schema, #{<<"new_user_info">> => NBody}, - #{nullable => true}, ["new_user_info"]), - #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{<<"password">> := Password} -> + case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, #{password => Password}) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, password}) end; users2(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), diff --git a/apps/emqx_authn/src/emqx_authn_implied_schema.erl b/apps/emqx_authn/src/emqx_authn_implied_schema.erl deleted file mode 100644 index 65b4bf356..000000000 --- a/apps/emqx_authn/src/emqx_authn_implied_schema.erl +++ /dev/null @@ -1,51 +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_authn_implied_schema). - --include("emqx_authn.hrl"). --include_lib("typerefl/include/types.hrl"). - --behaviour(hocon_schema). - --export([ structs/0 - , fields/1 - ]). - -structs() -> [ "filename", "user_info", "new_user_info"]. - -fields("filename") -> - [ {filename, fun filename/1} ]; -fields("user_info") -> - [ {user_id, fun user_id/1} - , {password, fun password/1} - ]; -fields("new_user_info") -> - [ {password, fun password/1} - ]. - -filename(type) -> string(); -filename(nullable) -> false; -filename(_) -> undefined. - -user_id(type) -> binary(); -user_id(nullable) -> false; -user_id(_) -> undefined. - -password(type) -> binary(); -password(nullable) -> false; -password(_) -> undefined. - From 429def6b95c877afec4685036a8216592b6514b4 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 12 Aug 2021 13:55:21 +0800 Subject: [PATCH 067/306] fix(authn): fix http api spec --- apps/emqx_authn/src/emqx_authn_api.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c4a12309e..bcaaebe93 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1070,7 +1070,13 @@ definitions() -> PasswordBasedRedisDef = #{ type => object, - required => [], + required => [ server_type + , server + , servers + , password + , database + , query + ], properties => #{ server_type => #{ type => string, @@ -1099,7 +1105,7 @@ definitions() -> }, database => #{ type => integer, - exmaple => 0 + example => 0 }, query => #{ type => string, From e6f9767066ebc651ea3de40543fd943b541971a3 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 13 Aug 2021 10:32:31 +0800 Subject: [PATCH 068/306] feat(authn): support superuser --- apps/emqx/src/emqx_access_control.erl | 9 +- apps/emqx/src/emqx_channel.erl | 19 ++-- apps/emqx_authn/data/user-credentials.csv | 6 +- apps/emqx_authn/data/user-credentials.json | 6 +- apps/emqx_authn/src/emqx_authn.erl | 5 +- apps/emqx_authn/src/emqx_authn_api.erl | 42 ++++++--- .../emqx_enhanced_authn_scram_mnesia.erl | 88 +++++++++++++------ .../src/simple_authn/emqx_authn_http.erl | 13 +-- .../src/simple_authn/emqx_authn_jwt.erl | 9 +- .../src/simple_authn/emqx_authn_mnesia.erl | 75 +++++++++------- .../src/simple_authn/emqx_authn_mongodb.erl | 8 +- .../src/simple_authn/emqx_authn_mysql.erl | 18 ++-- .../src/simple_authn/emqx_authn_pgsql.erl | 22 +++-- .../src/simple_authn/emqx_authn_redis.erl | 12 ++- .../emqx_authn/test/data/user-credentials.csv | 6 +- .../test/data/user-credentials.json | 6 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 4 +- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 30 ++++--- .../test/emqx_authn_mnesia_SUITE.erl | 43 +++++---- 19 files changed, 270 insertions(+), 151 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 65991d222..111a86112 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -27,9 +27,14 @@ %%-------------------------------------------------------------------- -spec(authenticate(emqx_types:clientinfo()) -> - ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). + {ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). authenticate(Credential) -> - run_hooks('client.authenticate', [Credential], ok). + case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of + ok -> + {ok, #{superuser => false}}; + Other -> + Other + end. %% @doc Check Authorization -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5e4d11953..93d5e2c37 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1299,14 +1299,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti {error, ?RC_BAD_AUTHENTICATION_METHOD} end. -do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> +do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) -> Properties = #{'Authentication-Method' => AuthMethod}, case emqx_access_control:authenticate(Credential) of - ok -> - {ok, Properties, Channel#channel{auth_cache = #{}}}; - {ok, AuthData} -> + {ok, #{superuser := Superuser}} -> + {ok, Properties, + Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}, + auth_cache = #{}}}; + {ok, #{superuser := Superuser}, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, - Channel#channel{auth_cache = #{}}}; + Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}, + auth_cache = #{}}}; {continue, AuthCache} -> {continue, Properties, Channel#channel{auth_cache = AuthCache}}; {continue, AuthData, AuthCache} -> @@ -1316,10 +1319,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> {error, emqx_reason_codes:connack_error(Reason)} end; -do_authenticate(Credential, Channel) -> +do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - ok -> - {ok, #{}, Channel}; + {ok, #{superuser := Superuser}} -> + {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. diff --git a/apps/emqx_authn/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv index 2543d39ca..0548308b7 100644 --- a/apps/emqx_authn/data/user-credentials.csv +++ b/apps/emqx_authn/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json index 169122bd2..e54501233 100644 --- a/apps/emqx_authn/data/user-credentials.json +++ b/apps/emqx_authn/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "superuser": false } ] diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 90114c269..384830750 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -235,8 +235,9 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo ignore -> do_authenticate(More, Credential); Result -> - %% ok - %% {ok, AuthData} + %% {ok, Extra} + %% {ok, Extra, AuthData} + %% {ok, MetaData} %% {continue, AuthCache} %% {continue, AuthData, AuthCache} %% {error, Reason} diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index bcaaebe93..97ed94a8b 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -528,6 +528,10 @@ users_api() -> }, password => #{ type => string + }, + superuser => #{ + type => boolean, + default => false } } } @@ -541,10 +545,12 @@ users_api() -> 'application/json' => #{ schema => #{ type => object, - required => [user_id], properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -576,10 +582,12 @@ users_api() -> type => array, items => #{ type => object, - required => [user_id], properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -620,10 +628,12 @@ users2_api() -> 'application/json' => #{ schema => #{ type => object, - required => [password], properties => #{ password => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -642,6 +652,9 @@ users2_api() -> properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -685,6 +698,9 @@ users2_api() -> properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -1359,9 +1375,11 @@ users(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of #{ <<"user_id">> := UserID - , <<"password">> := Password} -> + , <<"password">> := Password} = UserInfo -> + Superuser = maps:get(<<"superuser">>, UserInfo, false), case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID - , password => Password}) of + , password => Password + , superuser => Superuser}) of {ok, User} -> {201, User}; {error, Reason} -> @@ -1385,16 +1403,18 @@ users2(patch, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"password">> := Password} -> - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, #{password => Password}) of + UserInfo = emqx_json:decode(Body, [return_maps]), + NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo), + case NUserInfo =:= #{} of + true -> + serialize_error({missing_parameter, password}); + false -> + case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of {ok, User} -> {200, User}; {error, Reason} -> serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, password}) + end end; users2(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), 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 index 56629c568..98cdb8c26 100644 --- 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 @@ -17,7 +17,6 @@ -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). @@ -48,6 +47,14 @@ -rlog_shard({?AUTH_SHARD, ?TAB}). +-record(user_info, + { user_id + , stored_key + , server_key + , salt + , superuser + }). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -57,8 +64,8 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {disc_copies, [node()]}, - {record_name, scram_user_credentail}, - {attributes, record_info(fields, scram_user_credentail)}, + {record_name, user_info}, + {attributes, record_info(fields, user_info)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> @@ -126,20 +133,21 @@ authenticate(_Credential, _State) -> destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], - ok = lists:foreach(fun(UserCredential) -> - mnesia:delete_object(?TAB, UserCredential, write) + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun(UserInfo) -> + mnesia:delete_object(?TAB, UserInfo, write) end, mnesia:select(?TAB, MatchSpec, write)) end). add_user(#{user_id := UserID, - password := Password}, #{user_group := UserGroup} = State) -> + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add_user(UserID, Password, State), - {ok, #{user_id => UserID}}; + Superuser = maps:get(superuser, UserInfo, false), + add_user(UserID, Password, Superuser, State), + {ok, #{user_id => UserID, superuser => Superuser}}; [_] -> {error, already_exist} end @@ -156,31 +164,41 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, User, #{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}} + [#user_info{superuser = Superuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)}, + UserInfo2 = case maps:get(password, User, undefined) of + undefined -> + UserInfo1; + Password -> + {StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State), + UserInfo1#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt} + end, + mnesia:write(?TAB, UserInfo2, write), + {ok, serialize_user_info(UserInfo2)} 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}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {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], + Users = [serialize_user_info(UserInfo) || + #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], {ok, Users}. %%------------------------------------------------------------------------------ @@ -195,13 +213,13 @@ ensure_auth_method(_, _) -> false. check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> - LookupFun = fun(Username) -> - lookup_user2(Username, State) + RetrieveFun = fun(Username) -> + retrieve(Username, State) end, case esasl_scram:check_client_first_message( Bin, #{iteration_count => IterationCount, - lookup => LookupFun} + retrieve => RetrieveFun} ) of {cotinue, ServerFirstMessage, Cache} -> {cotinue, ServerFirstMessage, Cache}; @@ -209,25 +227,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} end. -check_client_final_message(Bin, Cache, #{algorithm := Alg}) -> +check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) -> case esasl_scram:check_client_final_message( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, ServerFinalMessage}; + {ok, #{superuser => Superuser}, 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). +add_user(UserID, Password, Superuser, State) -> + {StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State), + UserInfo = #user_info{user_id = UserID, + stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + superuser = Superuser}, + mnesia:write(?TAB, UserInfo, write). -lookup_user2(UserID, #{user_group := UserGroup}) -> +retrieve(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#scram_user_credentail{} = UserCredential] -> - {ok, UserCredential}; + [#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + superuser = Superuser}] -> + {ok, #{stored_key => StoredKey, + server_key => ServerKey, + salt => Salt, + superuser => Superuser}}; [] -> {error, not_found} end. @@ -241,3 +270,6 @@ trans(Fun, Args) -> {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. + +serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> + #{user_id => UserID, superuser => Superuser}. \ No newline at end of file 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 aa10a3b98..026df2415 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -154,15 +154,16 @@ authenticate(Credential, #{'_unique' := Unique, try Request = generate_request(Credential, State), case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of - {ok, 204, _Headers} -> ok; + {ok, 204, _Headers} -> {ok, #{superuser => false}}; {ok, 200, Headers, Body} -> ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), case safely_parse_body(ContentType, Body) of - {ok, _NBody} -> + {ok, NBody} -> %% TODO: Return by user property - ok; + {ok, #{superuser => maps:get(<<"superuser">>, NBody, false), + user_property => NBody}}; {error, _Reason} -> - ok + {ok, #{superuser => false}} end; {error, _Reason} -> ignore @@ -291,8 +292,8 @@ safely_parse_body(ContentType, Body) -> end. parse_body(<<"application/json">>, Body) -> - {ok, emqx_json:decode(Body)}; + {ok, emqx_json:decode(Body, [return_maps])}; parse_body(<<"application/x-www-form-urlencoded">>, Body) -> - {ok, cow_qs:parse_qs(Body)}; + {ok, maps:from_list(cow_qs:parse_qs(Body))}; parse_body(ContentType, _) -> {error, {unsupported_content_type, ContentType}}. \ 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 fe034994e..74aa9e8f6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -169,7 +169,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK, end, VerifyClaims = replace_placeholder(VerifyClaims0, Credential), case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; + {ok, Extra} -> {ok, Extra}; {error, invalid_signature} -> ignore; {error, {claims, _}} -> {error, bad_username_or_password} end. @@ -239,7 +239,12 @@ 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); + case verify_claims(Claims, VerifyClaims) of + ok -> + {ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}}; + {error, Reason} -> + {error, Reason} + end; {false, _, _} -> verify(JWS, More, VerifyClaims) catch 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 ce845d4e3..08c0ffad1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -46,6 +46,7 @@ { user_id :: {user_group(), user_id()} , password_hash :: binary() , salt :: binary() + , superuser :: boolean() }). -reflect_type([ user_id_type/0 ]). @@ -147,13 +148,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> ok; + true -> {ok, #{superuser => Superuser}}; false -> {error, bad_username_or_password} end end. @@ -161,7 +162,7 @@ authenticate(#{password := Password} = Credential, destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}], + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) end). @@ -179,14 +180,16 @@ import_users(Filename0, State) -> end. add_user(#{user_id := UserID, - password := Password}, + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}}; + {PasswordHash, Salt} = hash(Password, State), + Superuser = maps:get(superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + {ok, #{user_id => UserID, superuser => Superuser}}; [_] -> {error, already_exist} end @@ -203,29 +206,38 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [_] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}} + [#user_info{ password_hash = PasswordHash + , salt = Salt + , superuser = Superuser}] -> + NSuperuser = maps:get(superuser, UserInfo, Superuser), + {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of + undefined -> + {PasswordHash, Salt}; + Password -> + hash(Password, State) + end, + insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser), + {ok, #{user_id => UserID, superuser => NSuperuser}} end end). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#user_info{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {error, not_found} end. list_users(#{user_group := UserGroup}) -> - Users = [#{user_id => UserID} || #user_info{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], + Users = [serialize_user_info(UserInfo) || #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], {ok, Users}. %%------------------------------------------------------------------------------ @@ -268,7 +280,8 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> Salt = maps:get(<<"salt">>, UserInfo, <<>>), - insert_user(UserGroup, UserID, PasswordHash, Salt), + Superuser = maps:get(<<"superuser">>, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> {error, bad_format}. @@ -282,7 +295,8 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = UserInfo} -> Salt = maps:get(salt, UserInfo, <<>>), - insert_user(UserGroup, UserID, PasswordHash, Salt), + Superuser = maps:get(superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), import(UserGroup, File, Seq); {error, Reason} -> {error, Reason} @@ -307,8 +321,6 @@ get_csv_header(File) -> get_user_info_by_seq(Fields, Seq) -> get_user_info_by_seq(Fields, Seq, #{}). -get_user_info_by_seq([], [], #{user_id := _, password_hash := _, salt := _} = Acc) -> - {ok, Acc}; get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) -> {ok, Acc}; get_user_info_by_seq(_, [], _) -> @@ -319,19 +331,13 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc) get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash}); get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) -> get_user_info_by_seq(More1, More2, Acc#{salt => Salt}); +get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{superuser => true}); +get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{superuser => false}); get_user_info_by_seq(_, _, _) -> {error, bad_format}. --compile({inline, [add/3]}). -add(UserID, Password, #{user_group := UserGroup, - password_hash_algorithm := Algorithm} = State) -> - Salt = gen_salt(State), - PasswordHash = hash(Algorithm, Password, Salt), - case Algorithm of - bcrypt -> insert_user(UserGroup, UserID, PasswordHash); - _ -> insert_user(UserGroup, UserID, PasswordHash, Salt) - end. - gen_salt(#{password_hash_algorithm := plain}) -> <<>>; gen_salt(#{password_hash_algorithm := bcrypt, @@ -347,13 +353,16 @@ hash(bcrypt, Password, Salt) -> hash(Algorithm, Password, Salt) -> emqx_passwd:hash(Algorithm, <>). -insert_user(UserGroup, UserID, PasswordHash) -> - insert_user(UserGroup, UserID, PasswordHash, <<>>). +hash(Password, #{password_hash_algorithm := Algorithm} = State) -> + Salt = gen_salt(State), + PasswordHash = hash(Algorithm, Password, Salt), + {PasswordHash, Salt}. -insert_user(UserGroup, UserID, PasswordHash, Salt) -> +insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) -> UserInfo = #user_info{user_id = {UserGroup, UserID}, password_hash = PasswordHash, - salt = Salt}, + salt = Salt, + superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -376,8 +385,10 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. - to_binary(B) when is_binary(B) -> B; to_binary(L) when is_list(L) -> iolist_to_binary(L). + +serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> + #{user_id => UserID, superuser => Superuser}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index ff1b2161a..56ced0104 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -140,7 +140,8 @@ authenticate(#{password := Password} = Credential, ignore; Doc -> case check_password(Password, Doc, State) of - ok -> ok; + ok -> + {ok, #{superuser => superuser(Doc, State)}}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), {error, bad_username_or_password}; @@ -221,6 +222,11 @@ check_password(Password, end end. +superuser(Doc, #{superuser_field := SuperuserField}) -> + maps:get(SuperuserField, Doc, false); +superuser(_, _) -> + false. + hash(Algorithm, Password, Salt, prefix) -> emqx_passwd:hash(Algorithm, <>); hash(Algorithm, Password, Salt, suffix) -> 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 f2a01e7e1..75a3392ec 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -112,15 +112,19 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {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); + case check_password(Password, Selected, State) of + ok -> + {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, _Reason} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. @@ -135,17 +139,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} 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 b83e111c3..44c7f7185 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -18,6 +18,7 @@ -include("emqx_authn.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). @@ -98,15 +99,20 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {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); + NColumns = [Name || #column{name = Name} <- Columns], + Selected = maps:from_list(lists:zip(NColumns, Rows)), + case check_password(Password, Selected, State) of + ok -> + {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, _Reason} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. @@ -121,17 +127,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 5d6e579ac..0c2696c0e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -124,7 +124,13 @@ authenticate(#{password := Password} = Credential, NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))), case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of {ok, Values} -> - check_password(Password, merge(Fields, Values), State); + Selected = merge(Fields, Values), + case check_password(Password, Selected, State) of + ok -> + {ok, #{superuser => maps:get("superuser", Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, Reason} -> ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]), ignore @@ -166,8 +172,8 @@ check_fields(["password_hash" | More], false) -> check_fields(More, true); check_fields(["salt" | More], HasPassHash) -> check_fields(More, HasPassHash); -% check_fields(["is_superuser" | More], HasPassHash) -> -% check_fields(More, HasPassHash); +check_fields(["superuser" | More], HasPassHash) -> + check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). diff --git a/apps/emqx_authn/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv index 2543d39ca..0548308b7 100644 --- a/apps/emqx_authn/test/data/user-credentials.csv +++ b/apps/emqx_authn/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json index 169122bd2..e54501233 100644 --- a/apps/emqx_authn/test/data/user-credentials.json +++ b/apps/emqx_authn/test/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "superuser": false } ] diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 9c4371838..bf6447aba 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -93,7 +93,7 @@ t_authenticator(_) -> ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})), - + ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})), @@ -108,7 +108,7 @@ t_authenticate(_) -> listener => mqtt_tcp, username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), ?assertEqual(false, emqx_authn:is_enabled()), emqx_authn:enable(), ?assertEqual(true, emqx_authn:is_enabled()), diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 7435deaa0..ddb2bb209 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -52,21 +52,27 @@ t_jwt_authenticator(_) -> JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), + + Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, + JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), + ClientInfo1 = #{username => <<"myuser">>, + password => JWS1}, + ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), ClientInfo2 = ClientInfo#{password => BadJWS}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), %% secret_base64_encoded Config2 = Config#{secret => base64:encode(<<"abcdef">>), secret_base64_encoded => true}, ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), %% Expiration @@ -74,39 +80,39 @@ t_jwt_authenticator(_) -> , <<"exp">> => erlang:system_time(second) - 60}, JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), ClientInfo3 = ClientInfo#{password => JWS3}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), Payload4 = #{ <<"username">> => <<"myuser">> , <<"exp">> => erlang:system_time(second) + 60}, JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), %% Issued At Payload5 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) - 60}, JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), Payload6 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) + 60}, JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), ClientInfo6 = ClientInfo#{password => JWS6}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)), %% Not Before Payload7 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) - 60}, JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), Payload8 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) + 60}, JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), ClientInfo8 = ClientInfo#{password => JWS8}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. @@ -128,8 +134,8 @@ t_jwt_authenticator2(_) -> JWS = generate_jws('public-key', Payload, PrivateKey), ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index fdcaf519d..d6425a89c 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -50,33 +50,36 @@ t_mnesia_authenticator(_) -> UserInfo = #{user_id => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ClientInfo = #{zone => external, username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), ?AUTH:enable(), - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), ?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({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), UserInfo2 = UserInfo#{password => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), + + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), + ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), @@ -104,10 +107,16 @@ t_import(_) -> ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), + + ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, + password => <<"mypassword2">>}, + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), + + ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, password => <<"mypassword3">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. @@ -131,11 +140,11 @@ t_multi_mnesia_authenticator(_) -> {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID1, #{user_id => <<"myuser">>, password => <<"mypass1">>})), - ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?assertMatch({ok, #{user_id := <<"myclient">>}}, ?AUTH:add_user(?CHAIN, ID2, #{user_id => <<"myclient">>, password => <<"mypass2">>})), @@ -143,12 +152,12 @@ t_multi_mnesia_authenticator(_) -> ClientInfo1 = #{username => <<"myuser">>, clientid => <<"myclient">>, password => <<"mypass1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), + ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), From e5892d16e5d0db62954e42756d4bc75203ee4de8 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 18 Aug 2021 18:24:52 +0800 Subject: [PATCH 069/306] feat(auth): support hot config --- apps/emqx_authn/include/emqx_authn.hrl | 1 - apps/emqx_authn/src/emqx_authn.erl | 104 ++++++++++------------ apps/emqx_authn/src/emqx_authn_api.erl | 50 +++++++---- apps/emqx_authn/test/emqx_authn_SUITE.erl | 6 +- 4 files changed, 82 insertions(+), 79 deletions(-) diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index f9ba7c3b5..c5a392fd0 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -26,7 +26,6 @@ { id :: binary() , name :: binary() , provider :: module() - , config :: map() , state :: map() }). diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 384830750..d6644cad5 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -76,60 +76,63 @@ %%------------------------------------------------------------------------------ pre_config_update({enable, Enable}, _OldConfig) -> - Enable; + {ok, Enable}; pre_config_update({create_authenticator, Config}, OldConfig) -> - OldConfig ++ [Config]; + {ok, OldConfig ++ [Config]}; pre_config_update({delete_authenticator, ID}, OldConfig) -> case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> error(Reason); + {error, Reason} -> {error, Reason}; {ok, #{name := Name}} -> - lists:filter(fun(#{<<"name">> := N}) -> - N =/= Name - end, OldConfig) + NewConfig = lists:filter(fun(#{<<"name">> := N}) -> + N =/= Name + end, OldConfig), + {ok, NewConfig} end; pre_config_update({update_authenticator, ID, Config}, OldConfig) -> case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> error(Reason); + {error, Reason} -> {error, Reason}; {ok, #{name := Name}} -> - lists:map(fun(#{<<"name">> := N} = C) -> - case N =:= Name of - true -> Config; - false -> C - end - end, OldConfig) + NewConfig = lists:map(fun(#{<<"name">> := N} = C) -> + case N =:= Name of + true -> Config; + false -> C + end + end, OldConfig), + {ok, NewConfig} end; pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) -> case lookup_authenticator(?CHAIN, ID) of {error, _Reason} -> OldConfig ++ [Config]; {ok, #{name := Name}} -> - lists:map(fun(#{<<"name">> := N} = C) -> - case N =:= Name of - true -> Config; - false -> C - end - end, OldConfig) + NewConfig = lists:map(fun(#{<<"name">> := N} = C) -> + case N =:= Name of + true -> Config; + false -> C + end + end, OldConfig), + {ok, NewConfig} end; -pre_config_update({move, ID, Position}, OldConfig) -> +pre_config_update({move_authenticator, ID, Position}, OldConfig) -> case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> error(Reason); + {error, Reason} -> {error, Reason}; {ok, #{name := Name}} -> {ok, Found, Part1, Part2} = split_by_name(Name, OldConfig), case Position of <<"top">> -> - [Found | Part1] ++ Part2; + {ok, [Found | Part1] ++ Part2}; <<"bottom">> -> - Part1 ++ Part2 ++ [Found]; + {ok, Part1 ++ Part2 ++ [Found]}; Before -> case binary:split(Before, <<":">>, [global]) of [<<"before">>, ID0] -> case lookup_authenticator(?CHAIN, ID0) of - {error, Reason} -> error(Reason); + {error, Reason} -> {error, Reason}; {ok, #{name := Name1}} -> - {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 + Part2), - NPart1 ++ [Found, NFound | NPart2] + {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2), + {ok, NPart1 ++ [Found, NFound | NPart2]} end; _ -> - error({invalid_parameter, position}) + {error, {invalid_parameter, position}} end end end. @@ -144,12 +147,9 @@ post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _Ol N =:= Name end, NewConfig) of [Config] -> - case create_authenticator(?CHAIN, Config) of - {ok, _} -> ok; - {error, Reason} -> throw(Reason) - end; + create_authenticator(?CHAIN, Config); [_Config | _] -> - error(name_has_be_used) + {error, name_has_be_used} end; post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig) -> case delete_authenticator(?CHAIN, ID) of @@ -162,12 +162,9 @@ post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, N =:= Name end, NewConfig) of [Config] -> - case update_authenticator(?CHAIN, ID, Config) of - {ok, _} -> ok; - {error, Reason} -> throw(Reason) - end; + update_authenticator(?CHAIN, ID, Config); [_Config | _] -> - error(name_has_be_used) + {error, name_has_be_used} end; post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> case lists:filter( @@ -175,14 +172,11 @@ post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, N =:= Name end, NewConfig) of [Config] -> - case update_or_create_authenticator(?CHAIN, ID, Config) of - {ok, _} -> ok; - {error, Reason} -> throw(Reason) - end; + update_or_create_authenticator(?CHAIN, ID, Config); [_Config | _] -> - error(name_has_be_used) + {error, name_has_be_used} end; -post_config_update({move, ID, Position}, _NewConfig, _OldConfig) -> +post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig) -> NPosition = case Position of <<"top">> -> top; <<"bottom">> -> bottom; @@ -191,16 +185,13 @@ post_config_update({move, ID, Position}, _NewConfig, _OldConfig) -> [<<"before">>, ID0] -> {before, ID0}; _ -> - error({invalid_parameter, position}) + {error, {invalid_parameter, position}} end end, - case move_authenticator(?CHAIN, ID, NPosition) of - ok -> ok; - {error, Reason} -> throw(Reason) - end. + move_authenticator(?CHAIN, ID, NPosition). update_config(Path, ConfigRequest) -> - emqx_config:update(emqx_authn_schema, Path, ConfigRequest). + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). enable() -> case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of @@ -522,7 +513,6 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> Authenticator = #authenticator{id = AuthenticatorID, name = Name, provider = Provider, - config = Config, state = switch_version(State)}, {ok, Authenticator}; {error, Reason} -> @@ -570,8 +560,7 @@ update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Co case Provider:update(Config#{'_unique' => Unique}, State) of {ok, NewState} -> NewAuthenticator = Authenticator#authenticator{name = NewName, - config = Config, - state = switch_version(NewState)}, + state = switch_version(NewState)}, NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), {ok, serialize_authenticator(NewAuthenticator)}; @@ -583,9 +572,8 @@ update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Co case NewProvider:create(Config#{'_unique' => Unique}) of {ok, NewState} -> NewAuthenticator = Authenticator#authenticator{name = NewName, - provider = NewProvider, - config = Config, - state = switch_version(NewState)}, + provider = NewProvider, + state = switch_version(NewState)}, NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), _ = Provider:destroy(State), @@ -660,5 +648,7 @@ serialize_authenticators(Authenticators) -> [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators]. serialize_authenticator(#authenticator{id = ID, - config = Config}) -> - Config#{id => ID}. + name = Name, + provider = Provider, + state = State}) -> + #{id => ID, name => Name, provider => Provider, state => State}. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 97ed94a8b..20a8e2f7d 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -790,6 +790,7 @@ definitions() -> , minirest:ref(<<"password_based_mysql">>) , minirest:ref(<<"password_based_pgsql">>) , minirest:ref(<<"password_based_mongodb">>) + , minirest:ref(<<"password_based_redis">>) , minirest:ref(<<"password_based_http_server">>) ] } @@ -1292,7 +1293,7 @@ authentication(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of #{<<"enable">> := Enable} -> - emqx_authn:update_config([authentication, enable], {enable, Enable}), + {ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}), {204}; _ -> serialize_error({missing_parameter, enable}) @@ -1305,20 +1306,28 @@ authenticators(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Config = emqx_json:decode(Body, [return_maps]), case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of - ok -> - {204}; - {error, Reason} -> + {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, + raw_config := RawConfig}} -> + [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], + {200, RawConfig1#{id => ID}}; + {error, {_, _, Reason}} -> serialize_error(Reason) end; authenticators(get, _Request) -> + RawConfig = get_raw_config([authentication, authenticators]), {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), - {200, Authenticators}. + NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) -> + Config#{id => ID} + end, RawConfig, Authenticators), + {200, NAuthenticators}. authenticators2(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of - {ok, Authenticator} -> - {200, Authenticator}; + {ok, #{id := ID, name := Name}} -> + RawConfig = get_raw_config([authentication, authenticators]), + [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], + {200, RawConfig1#{id => ID}}; {error, Reason} -> serialize_error(Reason) end; @@ -1328,17 +1337,19 @@ authenticators2(put, Request) -> Config = emqx_json:decode(Body, [return_maps]), case emqx_authn:update_config([authentication, authenticators], {update_or_create_authenticator, AuthenticatorID, Config}) of - ok -> - {204}; - {error, Reason} -> + {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, + raw_config := RawConfig}} -> + [RawConfig0] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], + {200, RawConfig0#{id => ID}}; + {error, {_, _, Reason}} -> serialize_error(Reason) end; authenticators2(delete, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of - ok -> + {ok, _} -> {204}; - {error, Reason} -> + {error, {_, _, Reason}} -> serialize_error(Reason) end. @@ -1348,8 +1359,8 @@ move(post, Request) -> case emqx_json:decode(Body, [return_maps]) of #{<<"position">> := Position} -> case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of - ok -> {204}; - {error, Reason} -> serialize_error(Reason) + {ok, _} -> {204}; + {error, {_, _, Reason}} -> serialize_error(Reason) end; _ -> serialize_error({missing_parameter, position}) @@ -1361,10 +1372,8 @@ import_users(post, Request) -> case emqx_json:decode(Body, [return_maps]) of #{<<"filename">> := Filename} -> case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) + ok -> {204}; + {error, Reason} -> serialize_error(Reason) end; _ -> serialize_error({missing_parameter, filename}) @@ -1435,6 +1444,11 @@ users2(delete, Request) -> serialize_error(Reason) end. +get_raw_config(ConfKeyPath) -> + %% TODO: call emqx_config:get_raw(ConfKeyPath) directly + NConfKeyPath = [atom_to_binary(Key, utf8) || Key <- ConfKeyPath], + emqx_map_lib:deep_get(NConfKeyPath, emqx_config:fill_defaults(emqx_config:get_raw([]))). + serialize_error({not_found, {authenticator, ID}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}}; diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index bf6447aba..0be04d6cf 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -71,7 +71,7 @@ t_authenticator(_) -> secret => <<"abcdef">>, secret_base64_encoded => false, verify_claims => []}, - {ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), + {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), ID2 = <<"random">>, ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), @@ -79,9 +79,9 @@ t_authenticator(_) -> AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2}, - {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), + {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)), - {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"fedcba">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), + {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), From 178d1006e1788ad8c04932d3b9e0e8b44d3f331a Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 19 Aug 2021 09:23:38 +0800 Subject: [PATCH 070/306] chore(match): reduce the risk of crash --- apps/emqx/src/emqx_channel.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 93d5e2c37..5988e03e5 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1302,13 +1302,13 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) -> Properties = #{'Authentication-Method' => AuthMethod}, case emqx_access_control:authenticate(Credential) of - {ok, #{superuser := Superuser}} -> + {ok, Result} -> {ok, Properties, - Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, auth_cache = #{}}}; - {ok, #{superuser := Superuser}, AuthData} -> + {ok, Result, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, - Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, auth_cache = #{}}}; {continue, AuthCache} -> {continue, Properties, Channel#channel{auth_cache = AuthCache}}; From d409cb83ff9e79e6cfc126523896da00d355aa60 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 19 Aug 2021 10:26:50 +0800 Subject: [PATCH 071/306] chore(authn): update tag of esasl --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 1abaef868..8cfc3cf7c 100644 --- a/rebar.config +++ b/rebar.config @@ -63,7 +63,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} - , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}} + , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. {xref_ignores, From 87051c0e533f7da5125e83080c2bd892fc7d0088 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 19 Aug 2021 14:40:36 +0800 Subject: [PATCH 072/306] test(authn): fix test case --- apps/emqx/test/emqx_access_control_SUITE.erl | 2 +- apps/emqx/test/emqx_channel_SUITE.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 2 +- apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index d459d28b2..8cfa17523 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -33,7 +33,7 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_authenticate(_) -> - ?assertMatch(ok, emqx_access_control:authenticate(clientinfo())). + ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index be7c94ede..dfbe56916 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -181,7 +181,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 end), + fun(_) -> {ok, #{superuser => false}} end), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 22391d0b6..b5de6cb9a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -73,7 +73,7 @@ authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> chain_id => ChainId }, case emqx_access_control:authenticate(ClientInfo) of - ok -> + {ok, _} -> {ok, mountpoint(ClientInfo)}; {error, Reason} -> {error, Reason} diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl index f14d6cb73..f96fe714c 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl @@ -87,7 +87,7 @@ 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 -> + {ok, _} -> _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined), %% FIXME: From e6c01cb6e69f9704fb3760045c15222a2ecf07ef Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 19 Aug 2021 15:01:44 +0800 Subject: [PATCH 073/306] fix(undefined function): fix undefined function --- .../src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 98cdb8c26..2d433d408 100644 --- 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 @@ -177,7 +177,7 @@ update_user(UserID, User, undefined -> UserInfo1; Password -> - {StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State), + {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), UserInfo1#user_info{stored_key = StoredKey, server_key = ServerKey, salt = Salt} @@ -239,7 +239,7 @@ check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm : end. add_user(UserID, Password, Superuser, State) -> - {StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State), + {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), UserInfo = #user_info{user_id = UserID, stored_key = StoredKey, server_key = ServerKey, From f0ba6af660e6683a5ba2e779b3fc4e866935807e Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 19 Aug 2021 15:58:09 +0800 Subject: [PATCH 074/306] chore(authn): fix dialyzer --- apps/emqx/src/emqx_config.erl | 4 ++-- apps/emqx_authn/src/emqx_authn.erl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 056929123..6441afe69 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -101,9 +101,9 @@ }. %% raw_config() is the config that is NOT parsed and tranlated by hocon schema --type raw_config() :: #{binary() => term()} | undefined. +-type raw_config() :: #{binary() => term()} | list() | undefined. %% config() is the config that is parsed and tranlated by hocon schema --type config() :: #{atom() => term()} | undefined. +-type config() :: #{atom() => term()} | list() | undefined. -type app_envs() :: [proplists:property()]. %% @doc For the given path, get root value enclosed in a single-key map. diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index d6644cad5..571d76cc7 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -314,9 +314,9 @@ list_users(ChainID, AuthenticatorID) -> %%-------------------------------------------------------------------- init(_Opts) -> - ets:new(?CHAIN_TAB, [ named_table, set, public - , {keypos, #chain.id} - , {read_concurrency, true}]), + _ = ets:new(?CHAIN_TAB, [ named_table, set, public + , {keypos, #chain.id} + , {read_concurrency, true}]), {ok, #{}}. handle_call({create_chain, ID}, _From, State) -> From 2088fe17cf90e35bcd81857698de428d8fdd21d1 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 19 Aug 2021 16:45:38 +0800 Subject: [PATCH 075/306] fix: node stats api --- apps/emqx_management/src/emqx_mgmt_api_nodes.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index b498bd060..59b427261 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -153,8 +153,8 @@ node_stats_api() -> example => node()}], responses => #{ <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Node Stats">>, stats)}}}, - {"/nodes/:node_name/stats", Metadata, node_metrics}. + <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Node Stats">>, stat)}}}, + {"/nodes/:node_name/stats", Metadata, node_stats}. %%%============================================================================================== %% parameters trans From cc1b8eca6c7069301eeb2e9323c6537db6a07a3f Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 19 Aug 2021 12:12:14 +0800 Subject: [PATCH 076/306] fix: params & config generate; api base64 encode payload --- apps/emqx_modules/src/emqx_delayed.erl | 12 +-- apps/emqx_modules/src/emqx_delayed_api.erl | 104 ++++++++++++++------- 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index da888e547..c982198bb 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -44,7 +44,7 @@ -export([ enable/0 , disable/0 , set_max_delayed_messages/1 - , update_config/2 + , update_config/1 , list/1 , get_delayed_message/1 , delete_delayed_message/1 @@ -146,7 +146,7 @@ format_delayed(#delayed_message{key = {TimeStamp, Id}, }, case WithPayload of true -> - Result#{payload => Payload}; + Result#{payload => base64:encode(Payload)}; _ -> Result end. @@ -172,12 +172,8 @@ delete_delayed_message(Id0) -> Timestamp = hd(Rows), ekka_mnesia:dirty_delete(?TAB, {Timestamp, Id}) end. - -update_config(Enable, MaxDelayedMessages) -> - Opts0 = emqx_config:get_raw([<<"delayed">>], #{}), - Opts1 = maps:put(<<"enable">>, Enable, Opts0), - Opts = maps:put(<<"max_delayed_messages">>, MaxDelayedMessages, Opts1), - {ok, _} = emqx:update_config([delayed], Opts). +update_config(Config) -> + {ok, _} = emqx:update_config([delayed], Config). %%-------------------------------------------------------------------- %% gen_server callback diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 85226701a..2d6c3ddf0 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -25,13 +25,16 @@ , request_body_schema/1 ]). +-define(MAX_PAYLOAD_LENGTH, 2048). +-define(PAYLOAD_TOO_LARGE, 'PAYLOAD_TOO_LARGE'). + -export([ status/2 , delayed_messages/2 , delayed_message/2 ]). %% for rpc --export([update_config_/2]). +-export([update_config_/1]). -export([api_spec/0]). @@ -40,7 +43,7 @@ -define(BAD_REQUEST, 'BAD_REQUEST'). --define(MESSAGE_ID_NOT_FOUND, 'ALREADY_DISABLED'). +-define(MESSAGE_ID_NOT_FOUND, 'MESSAGE_ID_NOT_FOUND'). api_spec() -> { @@ -66,6 +69,9 @@ delayed_schema(WithPayload) -> end. delayed_message_properties() -> + PayloadDesc = list_to_binary( + io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p", + [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH])), #{ id => #{ type => integer, @@ -82,7 +88,7 @@ delayed_message_properties() -> description => <<"Qos">>}, payload => #{ type => string, - description => <<"Payload">>}, + description => PayloadDesc}, form_clientid => #{ type => string, description => <<"Client ID">>}, @@ -110,7 +116,7 @@ status_api() -> 'requestBody' => request_body_schema(Schema), responses => #{ <<"200">> => - response_schema(<<"Enable or disable delayed successfully">>), + response_schema(<<"Enable or disable delayed successfully">>, Schema), <<"400">> => response_error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED])}}}, {"/mqtt/delayed_messages/status", Metadata, status}. @@ -156,10 +162,8 @@ status(get, _Request) -> status(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), - Enable = maps:get(<<"enable">>, Params), - MaxDelayedMessages = maps:get(<<"max_delayed_messages">>, Params), - update_config(Enable, MaxDelayedMessages). + Config = emqx_json:decode(Body, [return_maps]), + update_config(Config). delayed_messages(get, Request) -> Qs = cowboy_req:parse_qs(Request), @@ -169,7 +173,13 @@ delayed_message(get, Request) -> Id = cowboy_req:binding(id, Request), case emqx_delayed:get_delayed_message(Id) of {ok, Message} -> - {200, Message}; + Payload = maps:get(payload, Message), + case size(Payload) > ?MAX_PAYLOAD_LENGTH of + true -> + {200, Message#{payload => ?PAYLOAD_TOO_LARGE}}; + _ -> + {200, Message#{payload => base64:encode(Payload)}} + end; {error, not_found} -> Message = list_to_binary(io_lib:format("Message ID ~p not found", [Id])), {404, #{code => ?MESSAGE_ID_NOT_FOUND, message => Message}} @@ -183,44 +193,72 @@ delayed_message(delete, Request) -> %% internal function %%-------------------------------------------------------------------- get_status() -> - #{ - enable => emqx:get_config([delayed, enable], true), - max_delayed_messages => emqx:get_config([delayed, max_delayed_messages], 0) - }. + emqx:get_config([delayed], #{}). -update_config(Enable, MaxDelayedMessages) when MaxDelayedMessages >= 0 -> - case Enable =:= maps:get(enable, get_status()) of - true -> - update_config_error_response(Enable); +update_config(Config) -> + case generate_config(Config) of + {ok, Config} -> + update_config_(Config), + {200, get_status()}; + {error, {Code, Message}} -> + {400, #{code => Code, message => Message}} + end. +generate_config(Config) -> + generate_config(Config, [fun generate_enable/1, fun generate_max_delayed_messages/1]). + +generate_config(Config, []) -> + {ok, Config}; +generate_config(Config, [Fun | Tail]) -> + case Fun(Config) of + {ok, Config} -> + generate_config(Config, Tail); + {error, CodeMessage} -> + {error, CodeMessage} + end. + +generate_enable(Config = #{<<"enable">> := Enable}) -> + case {Enable =:= maps:get(enable, get_status()), Enable} of + {true, true} -> + {error, {?ALREADY_ENABLED, <<"Delayed message status is already enabled">>}}; + {true, false} -> + {error, {?ALREADY_DISABLED, <<"Delayed message status is already disable">>}}; _ -> - update_config_(Enable, MaxDelayedMessages), - {200} + {ok, Config} end; -update_config(_Enable, _MaxDelayedMessages) -> - {400, #{code => ?BAD_REQUEST, message => <<"Max delayed must be equal or greater than 0">>}}. +generate_enable(Config) -> + {ok, Config}. -update_config_error_response(true) -> - {400, #{code => ?ALREADY_ENABLED, message => <<"Delayed message status is already enabled">>}}; -update_config_error_response(false) -> - {400, #{code => ?ALREADY_DISABLED, message => <<"Delayed message status is already disable">>}}. +generate_max_delayed_messages(Config = #{<<"max_delayed_messages">> := Max}) when Max >= 0 -> + {ok, Config}; +generate_max_delayed_messages(#{<<"max_delayed_messages">> := Max}) when Max < 0 -> + {error, {?BAD_REQUEST, <<"Max delayed must be equal or greater than 0">>}}; +generate_max_delayed_messages(Config) -> + {ok, Config}. -update_config_(Enable, MaxDelayedMessages) -> +update_config_(Config) -> lists:foreach(fun(Node) -> - update_config_(Node, Enable, MaxDelayedMessages) + update_config_(Node, Config) end, ekka_mnesia:running_nodes()). -update_config_(Node, Enable, MaxDelayedMessages) when Node =:= node() -> - _ = emqx_delayed:update_config(Enable, MaxDelayedMessages), - ok = emqx_delayed:set_max_delayed_messages(MaxDelayedMessages), - case Enable of +update_config_(Node, Config) when Node =:= node() -> + _ = emqx_delayed:update_config(Config), + case maps:get(<<"enable">>, Config, undefined) of + undefined -> + ignore; true -> emqx_delayed:enable(); false -> emqx_delayed:disable() + end, + case maps:get(<<"max_delayed_messages">>, Config, undefined) of + undefined -> + ignore; + Max -> + ok = emqx_delayed:set_max_delayed_messages(Max) end; -update_config_(Node, Enable, MaxDelayedMessages) -> - rpc_call(Node, ?MODULE, update_config_, [Node, Enable, MaxDelayedMessages]). +update_config_(Node, Config) -> + rpc_call(Node, ?MODULE, ?FUNCTION_NAME, [Node, Config]). rpc_call(Node, Module, Fun, Args) -> case rpc:call(Node, Module, Fun, Args) of From f3efc8919237e485ce2bbab3062d01180df8f7e0 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 19 Aug 2021 19:57:42 +0800 Subject: [PATCH 077/306] refactor(config): replace all ':' with '=' in the *.conf (#5531) --- apps/emqx/etc/emqx.conf | 406 +++++++++--------- .../etc/emqx_hocon_plugin.conf | 4 +- apps/emqx_authn/etc/emqx_authn.conf | 6 +- apps/emqx_authz/etc/emqx_authz.conf | 10 +- .../etc/emqx_bridge_mqtt.conf | 10 +- apps/emqx_dashboard/etc/emqx_dashboard.conf | 30 +- .../etc/emqx_data_bridge.conf | 2 +- apps/emqx_exhook/etc/emqx_exhook.conf | 10 +- apps/emqx_gateway/etc/emqx_gateway.conf | 158 +++---- apps/emqx_machine/etc/emqx_machine.conf | 154 +++---- apps/emqx_management/etc/emqx_management.conf | 30 +- apps/emqx_modules/etc/emqx_modules.conf | 35 +- apps/emqx_prometheus/etc/emqx_prometheus.conf | 8 +- apps/emqx_retainer/etc/emqx_retainer.conf | 26 +- .../etc/emqx_rule_engine.conf | 4 +- apps/emqx_statsd/etc/emqx_statsd.conf | 10 +- 16 files changed, 452 insertions(+), 451 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 5200f5239..74a29b31d 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -7,7 +7,7 @@ broker { ## @doc broker.sys_msg_interval ## ValueType: Duration | disabled ## Default: 1m - sys_msg_interval: 1m + sys_msg_interval = 1m ## System heartbeat interval of publishing following heart beat message: ## - "$SYS/brokers//uptime" @@ -16,7 +16,7 @@ broker { ## @doc broker.sys_heartbeat_interval ## ValueType: Duration ## Default: 30s | disabled - sys_heartbeat_interval: 30s + sys_heartbeat_interval = 30s ## Session locking strategy in a cluster. ## @@ -27,7 +27,7 @@ broker { ## - 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 = quorum ## Dispatch strategy for shared subscription ## @@ -39,7 +39,7 @@ broker { ## until the susbcriber disconnected. ## - hash: select the subscribers by the hash of clientIds ## Default: round_robin - shared_subscription_strategy: round_robin + shared_subscription_strategy = round_robin ## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages ## This should allow messages to be dispatched to a different subscriber in @@ -48,14 +48,14 @@ broker { ## @doc broker.shared_dispatch_ack_enabled ## ValueType: Boolean ## Default: false - shared_dispatch_ack_enabled: false + shared_dispatch_ack_enabled = false ## Enable batch clean for deleted routes. ## ## @doc broker.route_batch_clean ## ValueType: Boolean ## Default: true - route_batch_clean: true + route_batch_clean = true ## Performance toggle for subscribe/unsubscribe wildcard topic. ## Change this toggle only when there are many wildcard topics. @@ -69,7 +69,7 @@ broker { ## - 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 + perf.route_lock_type = key ## Enable trie path compaction. ## Enabling it significantly improves wildcard topic subscribe @@ -85,7 +85,7 @@ broker { ## @doc broker.perf.trie_compaction ## ValueType: Boolean ## Default: true - perf.trie_compaction: true + perf.trie_compaction = true } ##================================================================== @@ -126,14 +126,14 @@ zones.default { ## @doc zones..auth.enable ## ValueType: Boolean ## Default: false - auth.enable: false + auth.enable = false ## Enable per connection statistics. ## ## @doc zones..stats.enable ## ValueType: Boolean ## Default: true - stats.enable: true + stats.enable = true ## Maximum number of concurrent connections in this zone. ## @@ -143,7 +143,7 @@ zones.default { ## @doc zones..overall_max_connections ## ValueType: Number | infinity ## Default: infinity - overall_max_connections: infinity + overall_max_connections = infinity mqtt { ## When publishing or subscribing, prefix all topics with a mountpoint string. @@ -167,7 +167,7 @@ zones.default { ## @doc zones..listeners..mountpoint ## ValueType: String ## Default: "" - mountpoint: "" + mountpoint = "" ## How long time the MQTT connection will be disconnected if the ## TCP connection is established but MQTT CONNECT has not been @@ -176,14 +176,14 @@ zones.default { ## @doc zones..mqtt.idle_timeout ## ValueType: Duration ## Default: 15s - idle_timeout: 15s + idle_timeout = 15s ## Maximum MQTT packet size allowed. ## ## @doc zones..mqtt.max_packet_size ## ValueType: Bytes ## Default: 1MB - max_packet_size: 1MB + max_packet_size = 1MB ## Maximum length of MQTT clientId allowed. ## @@ -191,7 +191,7 @@ zones.default { ## ValueType: Integer ## Range: [23, 65535] ## Default: 65535 - max_clientid_len: 65535 + max_clientid_len = 65535 ## Maximum topic levels allowed. ## @@ -199,14 +199,14 @@ zones.default { ## ValueType: Integer ## Range: [1, 65535] ## Default: 65535 - max_topic_levels: 65535 + max_topic_levels = 65535 ## Maximum QoS allowed. ## ## @doc zones..mqtt.max_qos_allowed ## ValueType: 0 | 1 | 2 ## Default: 2 - max_qos_allowed: 2 + max_qos_allowed = 2 ## Maximum Topic Alias, 0 means no topic alias supported. ## @@ -214,42 +214,42 @@ zones.default { ## ValueType: Integer ## Range: [0, 65535] ## Default: 65535 - max_topic_alias: 65535 + max_topic_alias = 65535 ## Whether the Server supports MQTT retained messages. ## ## @doc zones..mqtt.retain_available ## ValueType: Boolean ## Default: true - retain_available: true + retain_available = true ## Whether the Server supports MQTT Wildcard Subscriptions ## ## @doc zones..mqtt.wildcard_subscription ## ValueType: Boolean ## Default: true - wildcard_subscription: true + wildcard_subscription = true ## Whether the Server supports MQTT Shared Subscriptions. ## ## @doc zones..mqtt.shared_subscription ## ValueType: Boolean ## Default: true - shared_subscription: true + shared_subscription = true ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) ## ## @doc zones..mqtt.ignore_loop_deliver ## ValueType: Boolean ## Default: false - ignore_loop_deliver: false + ignore_loop_deliver = false ## Whether to parse the MQTT frame in strict mode ## ## @doc zones..mqtt.strict_mode ## ValueType: Boolean ## Default: false - strict_mode: false + strict_mode = false ## Specify the response information returned to the client ## @@ -258,14 +258,14 @@ zones.default { ## @doc zones..mqtt.response_information ## ValueType: String ## Default: "" - response_information: "" + response_information = "" ## Server Keep Alive of MQTT 5.0 ## ## @doc zones..mqtt.server_keepalive ## ValueType: Number | disabled ## Default: disabled - server_keepalive: disabled + server_keepalive = disabled ## The backoff for MQTT keepalive timeout. The broker will kick a connection out ## until 'Keepalive * backoff * 2' timeout. @@ -274,7 +274,7 @@ zones.default { ## ValueType: Float ## Range: (0.5, 1] ## Default: 0.75 - keepalive_backoff: 0.75 + keepalive_backoff = 0.75 ## Maximum number of subscriptions allowed. ## @@ -282,14 +282,14 @@ zones.default { ## ValueType: Integer | infinity ## Range: [1, infinity) ## Default: infinity - max_subscriptions: infinity + max_subscriptions = infinity ## Force to upgrade QoS according to subscription. ## ## @doc zones..mqtt.upgrade_qos ## ValueType: Boolean ## Default: false - upgrade_qos: false + upgrade_qos = false ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. ## @@ -297,14 +297,14 @@ zones.default { ## ValueType: Integer ## Range: [1, 65535] ## Default: 32 - max_inflight: 32 + max_inflight = 32 ## Retry interval for QoS1/2 message delivering. ## ## @doc zones..mqtt.retry_interval ## ValueType: Duration ## Default: 30s - retry_interval: 30s + retry_interval = 30s ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. ## @@ -312,21 +312,21 @@ zones.default { ## ValueType: Integer | infinity ## Range: [1, infinity) ## Default: 100 - max_awaiting_rel: 100 + max_awaiting_rel = 100 ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. ## ## @doc zones..mqtt.await_rel_timeout ## ValueType: Duration ## Default: 300s - await_rel_timeout: 300s + await_rel_timeout = 300s ## Default session expiry interval for MQTT V3.1.1 connections. ## ## @doc zones..mqtt.session_expiry_interval ## ValueType: Duration ## Default: 2h - session_expiry_interval: 2h + session_expiry_interval = 2h ## Maximum queue length. Enqueued messages when persistent client disconnected, ## or inflight window is full. @@ -335,7 +335,7 @@ zones.default { ## ValueType: Integer | infinity ## Range: [0, infinity) ## Default: 1000 - max_mqueue_len: 1000 + max_mqueue_len = 1000 ## Topic priorities. ## @@ -355,28 +355,28 @@ zones.default { ## To configure "topic/1" > "topic/2": ## mqueue_priorities: {"topic/1": 10, "topic/2": 8} ## Default: disabled - mqueue_priorities: disabled + mqueue_priorities = disabled ## Default to highest priority for topics not matching priority table ## ## @doc zones..mqtt.mqueue_default_priority ## ValueType: highest | lowest ## Default: lowest - mqueue_default_priority: lowest + mqueue_default_priority = lowest ## Whether to enqueue QoS0 messages. ## ## @doc zones..mqtt.mqueue_store_qos0 ## ValueType: Boolean ## Default: true - mqueue_store_qos0: true + mqueue_store_qos0 = true ## Whether use username replace client id ## ## @doc zones..mqtt.use_username_as_clientid ## ValueType: Boolean ## Default: false - use_username_as_clientid: 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. @@ -384,7 +384,7 @@ zones.default { ## @doc zones..mqtt.peer_cert_as_username ## ValueType: cn | dn | crt | disabled ## Default: disabled - peer_cert_as_username: 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. @@ -392,7 +392,7 @@ zones.default { ## @doc zones..mqtt.peer_cert_as_clientid ## ValueType: cn | dn | crt | disabled ## Default: disabled - peer_cert_as_clientid: disabled + peer_cert_as_clientid = disabled } @@ -403,14 +403,14 @@ zones.default { ## @doc zones..authorization.enable ## ValueType: Boolean ## Default: true - enable: true + enable = true ## The action when authorization check reject current operation ## ## @doc zones..authorization.deny_action ## ValueType: ignore | disconnect ## Default: ignore - deny_action: ignore + deny_action = ignore ## Whether to enable Authorization cache. ## @@ -419,7 +419,7 @@ zones.default { ## @doc zones..authorization.cache.enable ## ValueType: Boolean ## Default: true - cache.enable: true + cache.enable = true ## The maximum count of Authorization entries can be cached for a client. ## @@ -427,14 +427,14 @@ zones.default { ## ValueType: Integer ## Range: [0, 1048576] ## Default: 32 - cache.max_size: 32 + cache.max_size = 32 ## The time after which an Authorization cache entry will be deleted ## ## @doc zones..authorization.cache.ttl ## ValueType: Duration ## Default: 1m - cache.ttl: 1m + cache.ttl = 1m } flapping_detect { @@ -448,79 +448,79 @@ zones.default { ## @doc zones..flapping_detect.enable ## ValueType: Boolean ## Default: true - enable: false + enable = false ## The max disconnect allowed of a MQTT Client in `window_time` ## ## @doc zones..flapping_detect.max_count ## ValueType: Integer ## Default: 15 - max_count: 15 + max_count = 15 ## The time window for flapping detect ## ## @doc zones..flapping_detect.window_time ## ValueType: Duration ## Default: 1m - window_time: 1m + window_time = 1m ## How long the clientid will be banned ## ## @doc zones..flapping_detect.ban_time ## ValueType: Duration ## Default: 5m - ban_time: 5m + ban_time = 5m } - force_shutdown: { + force_shutdown { ## Enable force_shutdown ## ## @doc zones..force_shutdown.enable ## ValueType: Boolean ## Default: true - enable: true + enable = true ## Max message queue length ## @doc zones..force_shutdown.max_message_queue_len ## ValueType: Integer ## Range: (0, infinity) ## Default: 1000 - max_message_queue_len: 1000 + max_message_queue_len = 1000 ## Total heap size ## ## @doc zones..force_shutdown.max_heap_size ## ValueType: Size ## Default: 32MB - max_heap_size: 32MB + max_heap_size = 32MB } - force_gc: { + force_gc { ## Force the MQTT connection process GC after this number of ## messages or bytes passed through. ## ## @doc zones..force_gc.enable ## ValueType: Boolean ## Default: true - enable: true + enable = true ## GC the process after how many messages received ## @doc zones..force_gc.max_message_queue_len ## ValueType: Integer ## Range: (0, infinity) ## Default: 16000 - count: 16000 + count = 16000 ## GC the process after how much bytes passed through ## ## @doc zones..force_gc.bytes ## ValueType: Size ## Default: 16MB - bytes: 16MB + bytes = 16MB } - conn_congestion: { + conn_congestion { ## Whether to alarm the congested connections. ## ## Sometimes the mqtt connection (usually an MQTT subscriber) may @@ -541,7 +541,7 @@ zones.default { ## @doc zones..conn_congestion.enable_alarm ## ValueType: Boolean ## Default: true - enable_alarm: 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 @@ -553,10 +553,10 @@ zones.default { ## @doc zones..conn_congestion.min_alarm_sustain_duration ## ValueType: Duration ## Default: 1m - min_alarm_sustain_duration: 1m + min_alarm_sustain_duration = 1m } - listeners.mqtt_tcp: + listeners.mqtt_tcp #${example_common_tcp_options} # common options can be written in a separate config entry and reference it from here. { @@ -568,7 +568,7 @@ zones.default { ## - ws: MQTT over Websocket ## - quic: MQTT over QUIC ## Required: true - type: tcp + type = tcp ## The IP address and port that the listener will bind. ## @@ -576,21 +576,21 @@ zones.default { ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 1883, 127.0.0.1:1883, ::1:1883 - bind: "0.0.0.0:1883" + bind = "0.0.0.0:1883" ## The size of the acceptor pool for this listener. ## ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 - acceptors: 16 + acceptors = 16 ## Maximum number of concurrent connections. ## ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity - max_connections: 1024000 + max_connections = 1024000 ## The access control rules for this listener. ## @@ -604,7 +604,7 @@ zones.default { ## "deny 192.168.0.0/24", ## "all all" ## ] - access_rules: [ + access_rules = [ "allow all" ] @@ -616,7 +616,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: false - proxy_protocol: 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. @@ -624,7 +624,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s - proxy_protocol_timeout: 3s + proxy_protocol_timeout = 3s rate_limit { ## Maximum connections per second. @@ -634,7 +634,7 @@ zones.default { ## Default: 1000 ## Examples: ## max_conn_rate: 1000 - max_conn_rate: 1000 + max_conn_rate = 1000 ## Message limit for the a external MQTT connection. ## @@ -643,7 +643,7 @@ zones.default { ## Default: infinity ## Examples: 100 messages per 10 seconds. ## conn_messages_in: "100,10s" - 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. @@ -657,7 +657,7 @@ zones.default { ## Examples: 100KB incoming per 10 seconds. ## conn_bytes_in: "100KB,10s" ## - 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. @@ -667,7 +667,7 @@ zones.default { ## Default: infinity ## Examples: 100 messaegs per 1s: ## quota.conn_messages_routing: "100,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. @@ -678,17 +678,17 @@ zones.default { ## Examples: 200000 messages per 1s: ## quota.overall_messages_routing: "200000,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 - tcp.buffer: 4KB + tcp.backlog = 1024 + tcp.buffer = 4KB } ## MQTT/SSL - SSL Listener for MQTT Protocol - listeners.mqtt_ssl: + 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. { @@ -700,7 +700,7 @@ zones.default { ## - ws: MQTT over Websocket ## - quic: MQTT over QUIC ## Required: true - type: tcp + type = tcp ## The IP address and port that the listener will bind. ## @@ -708,21 +708,21 @@ zones.default { ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 8883, 127.0.0.1:8883, ::1:8883 - bind: "0.0.0.0:8883" + bind = "0.0.0.0:8883" ## The size of the acceptor pool for this listener. ## ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 - acceptors: 16 + acceptors = 16 ## Maximum number of concurrent connections. ## ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity - max_connections: 512000 + max_connections = 512000 ## The access control rules for this listener. ## @@ -736,7 +736,7 @@ zones.default { ## "deny 192.168.0.0/24", ## "all all" ## ] - access_rules: [ + access_rules = [ "allow all" ] @@ -748,7 +748,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: true - proxy_protocol: 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. @@ -756,7 +756,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s - proxy_protocol_timeout: 3s + proxy_protocol_timeout = 3s rate_limit { ## Maximum connections per second. @@ -766,7 +766,7 @@ zones.default { ## Default: 1000 ## Examples: ## max_conn_rate: 1000 - max_conn_rate: 1000 + max_conn_rate = 1000 ## Message limit for the a external MQTT connection. ## @@ -775,7 +775,7 @@ zones.default { ## Default: infinity ## Examples: 100 messages per 10 seconds. ## conn_messages_in: "100,10s" - 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. @@ -789,7 +789,7 @@ zones.default { ## Examples: 100KB incoming per 10 seconds. ## conn_bytes_in: "100KB,10s" ## - 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. @@ -799,7 +799,7 @@ zones.default { ## Default: infinity ## Examples: 100 messaegs per 1s: ## quota.conn_messages_routing: "100,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. @@ -810,24 +810,24 @@ zones.default { ## Examples: 200000 messages per 1s: ## quota.overall_messages_routing: "200000,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.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" + 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" ## TCP options ## See ${example_common_tcp_options} for more information - tcp.backlog: 1024 - tcp.buffer: 4KB + tcp.backlog = 1024 + tcp.buffer = 4KB } - listeners.mqtt_quic: + listeners.mqtt_quic { ## The type of the listener. ## @@ -837,7 +837,7 @@ zones.default { ## - ws: MQTT over Websocket ## - quic: MQTT over QUIC ## Required: true - type: quic + type = quic ## The IP address and port that the listener will bind. ## @@ -845,38 +845,38 @@ zones.default { ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 14567, 127.0.0.1:14567, ::1:14567 - bind: "0.0.0.0:14567" + bind = "0.0.0.0:14567" ## The size of the acceptor pool for this listener. ## ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 - acceptors: 16 + acceptors = 16 ## Maximum number of concurrent connections. ## ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity - max_connections: 1024000 + max_connections = 1024000 ## 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" + 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" + certfile = "{{ platform_etc_dir }}/certs/cert.pem" } - listeners.mqtt_ws: + 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. { @@ -888,7 +888,7 @@ zones.default { ## - ws: MQTT over Websocket ## - quic: MQTT over QUIC ## Required: true - type: ws + type = ws ## The IP address and port that the listener will bind. ## @@ -896,21 +896,21 @@ zones.default { ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 8083, 127.0.0.1:8083, ::1:8083 - bind: "0.0.0.0:8083" + bind = "0.0.0.0:8083" ## The size of the acceptor pool for this listener. ## ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 - acceptors: 16 + acceptors = 16 ## Maximum number of concurrent connections. ## ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity - max_connections: 1024000 + max_connections = 1024000 ## The access control rules for this listener. ## @@ -924,7 +924,7 @@ zones.default { ## "deny 192.168.0.0/24", ## "all all" ## ] - access_rules: [ + access_rules = [ "allow all" ] @@ -936,7 +936,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: true - proxy_protocol: 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. @@ -944,7 +944,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s - proxy_protocol_timeout: 3s + proxy_protocol_timeout = 3s rate_limit { ## Maximum connections per second. @@ -954,7 +954,7 @@ zones.default { ## Default: 1000 ## Examples: ## max_conn_rate: 1000 - max_conn_rate: 1000 + max_conn_rate = 1000 ## Message limit for the a external MQTT connection. ## @@ -963,7 +963,7 @@ zones.default { ## Default: infinity ## Examples: 100 messages per 10 seconds. ## conn_messages_in: "100,10s" - 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. @@ -977,7 +977,7 @@ zones.default { ## Examples: 100KB incoming per 10 seconds. ## conn_bytes_in: "100KB,10s" ## - 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. @@ -987,7 +987,7 @@ zones.default { ## Default: infinity ## Examples: 100 messaegs per 1s: ## quota.conn_messages_routing: "100,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. @@ -998,20 +998,20 @@ zones.default { ## Examples: 200000 messages per 1s: ## quota.overall_messages_routing: "200000,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 - tcp.buffer: 4KB + tcp.backlog = 1024 + tcp.buffer = 4KB ## Websocket options ## See ${example_common_websocket_options} for more information - websocket.idle_timeout: 86400s + websocket.idle_timeout = 86400s } - listeners.mqtt_wss: + 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. { @@ -1023,7 +1023,7 @@ zones.default { ## - ws: MQTT over Websocket ## - quic: MQTT over QUIC ## Required: true - type: ws + type = ws ## The IP address and port that the listener will bind. ## @@ -1031,21 +1031,21 @@ zones.default { ## ValueType: IPAddress | Port | IPAddrPort ## Required: true ## Examples: 8084, 127.0.0.1:8084, ::1:8084 - bind: "0.0.0.0:8084" + bind = "0.0.0.0:8084" ## The size of the acceptor pool for this listener. ## ## @doc zones..listeners..acceptors ## ValueType: Number ## Default: 16 - acceptors: 16 + acceptors = 16 ## Maximum number of concurrent connections. ## ## @doc zones..listeners..max_connections ## ValueType: Number | infinity ## Default: infinity - max_connections: 512000 + max_connections = 512000 ## The access control rules for this listener. ## @@ -1059,7 +1059,7 @@ zones.default { ## "deny 192.168.0.0/24", ## "all all" ## ] - access_rules: [ + access_rules = [ "allow all" ] @@ -1071,7 +1071,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol ## ValueType: Boolean ## Default: true - proxy_protocol: 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. @@ -1079,7 +1079,7 @@ zones.default { ## @doc zones..listeners..proxy_protocol_timeout ## ValueType: Duration ## Default: 3s - proxy_protocol_timeout: 3s + proxy_protocol_timeout = 3s rate_limit { ## Maximum connections per second. @@ -1089,7 +1089,7 @@ zones.default { ## Default: 1000 ## Examples: ## max_conn_rate: 1000 - max_conn_rate: 1000 + max_conn_rate = 1000 ## Message limit for the a external MQTT connection. ## @@ -1098,7 +1098,7 @@ zones.default { ## Default: infinity ## Examples: 100 messages per 10 seconds. ## conn_messages_in: "100,10s" - 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. @@ -1112,7 +1112,7 @@ zones.default { ## Examples: 100KB incoming per 10 seconds. ## conn_bytes_in: "100KB,10s" ## - 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. @@ -1122,7 +1122,7 @@ zones.default { ## Default: infinity ## Examples: 100 messaegs per 1s: ## quota.conn_messages_routing: "100,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. @@ -1133,24 +1133,24 @@ zones.default { ## Examples: 200000 messages per 1s: ## quota.overall_messages_routing: "200000,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" + 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" ## TCP options ## See ${example_common_tcp_options} for more information - tcp.backlog: 1024 - tcp.buffer: 4KB + tcp.backlog = 1024 + tcp.buffer = 4KB ## Websocket options ## See ${example_common_websocket_options} for more information - websocket.idle_timeout: 86400s + websocket.idle_timeout = 86400s } } @@ -1158,15 +1158,15 @@ zones.default { #This is an example zone which has less "strict" settings. #It's useful to clients connecting the broker from trusted networks. zones.internal { - authorization.enable: true - 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 + authorization.enable = true + 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 } } @@ -1179,21 +1179,21 @@ sysmon { ## @doc sysmon.vm.process_check_interval ## ValueType: Duration ## Default: 30s - vm.process_check_interval: 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% + 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% + vm.process_low_watermark = 60% ## Enable Long GC monitoring. ## Notice: don't enable the monitor in production for: @@ -1202,7 +1202,7 @@ sysmon { ## @doc sysmon.vm.long_gc ## ValueType: Duration | disabled ## Default: disabled - vm.long_gc: disabled + vm.long_gc = disabled ## Enable Long Schedule(ms) monitoring. ## @@ -1211,7 +1211,7 @@ sysmon { ## @doc sysmon.vm.long_schedule ## ValueType: Duration | disabled ## Default: disabled - vm.long_schedule: 240ms + vm.long_schedule = 240ms ## Enable Large Heap monitoring. ## @@ -1220,7 +1220,7 @@ sysmon { ## @doc sysmon.vm.large_heap ## ValueType: Size | disabled ## Default: 32MB - vm.large_heap: 32MB + vm.large_heap = 32MB ## Enable Busy Port monitoring. ## @@ -1229,7 +1229,7 @@ sysmon { ## @doc sysmon.vm.busy_port ## ValueType: Boolean ## Default: true - vm.busy_port: true + vm.busy_port = true ## Enable Busy Dist Port monitoring. ## @@ -1238,49 +1238,49 @@ sysmon { ## @doc sysmon.vm.busy_dist_port ## ValueType: Boolean ## Default: true - vm.busy_dist_port: 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 + 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% + 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% + 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 + 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% + 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% + os.procmem_high_watermark = 5% } ##================================================================== @@ -1292,21 +1292,21 @@ alarm { ## @doc alarm.actions ## ValueType: Array ## Default: [log, publish] - actions: [log, publish] + actions = [log, publish] ## The maximum number of deactivated alarms ## ## @doc alarm.size_limit ## ValueType: Integer ## Default: 1000 - size_limit: 1000 + size_limit = 1000 ## Validity Period of deactivated alarms ## ## @doc alarm.validity_period ## ValueType: Duration ## Default: 24h - validity_period: 24h + validity_period = 24h } ## Config references for listeners @@ -1321,7 +1321,7 @@ example_common_tcp_options { ## @doc listeners..tcp.active_n ## ValueType: Number ## Default: 100 - tcp.active_n: 100 + tcp.active_n = 100 ## TCP backlog defines the maximum length that the queue of ## pending connections can grow to. @@ -1330,21 +1330,21 @@ example_common_tcp_options { ## ValueType: Number ## Range: [0, 1048576] ## Default: 1024 - tcp.backlog: 1024 + tcp.backlog = 1024 ## The TCP send timeout for the connections. ## ## @doc listeners..tcp.send_timeout ## ValueType: Duration ## Default: 15s - tcp.send_timeout: 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 + tcp.send_timeout_close = true ## The TCP receive buffer(os kernel) for the connections. ## @@ -1373,21 +1373,21 @@ example_common_tcp_options { ## @doc listeners..tcp.high_watermark ## ValueType: Size ## Default: 1MB - tcp.high_watermark: 1MB + tcp.high_watermark = 1MB ## The TCP_NODELAY flag for the connections. ## ## @doc listeners..tcp.nodelay ## ValueType: Boolean ## Default: false - tcp.nodelay: false + tcp.nodelay = false ## The SO_REUSEADDR flag for the connections. ## ## @doc listeners..tcp.reuseaddr ## ValueType: Boolean ## Default: true - tcp.reuseaddr: true + tcp.reuseaddr = true } ## Socket options for SSL connections @@ -1401,7 +1401,7 @@ example_common_ssl_options { ## @doc listeners..ssl.reuse_sessions ## ValueType: Boolean ## Default: true - ssl.reuse_sessions: 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. @@ -1411,7 +1411,7 @@ example_common_ssl_options { ## @doc listeners..ssl.secure_renegotiate ## ValueType: Boolean ## Default: true - ssl.secure_renegotiate: 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, @@ -1421,21 +1421,21 @@ example_common_ssl_options { ## @doc listeners..ssl.honor_cipher_order ## ValueType: Boolean ## Default: true - ssl.honor_cipher_order: true + ssl.honor_cipher_order = true ## TLS versions only to protect from POODLE attack. ## ## @doc listeners..ssl.versions ## ValueType: Array ## Default: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] - ssl.versions: ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] ## TLS Handshake timeout. ## ## @doc listeners..ssl.handshake_timeout ## ValueType: Duration ## Default: 15s - ssl.handshake_timeout: 15s + ssl.handshake_timeout = 15s ## Maximum number of non-self-issued intermediate certificates that ## can follow the peer certificate in a valid certification path. @@ -1443,21 +1443,21 @@ example_common_ssl_options { ## @doc listeners..ssl.depth ## ValueType: Integer ## Default: 10 - ssl.depth: 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" + 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" + 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. @@ -1465,7 +1465,7 @@ example_common_ssl_options { ## @doc listeners..ssl.cacertfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/cacert.pem" - ssl.cacertfile: "{{ 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. @@ -1473,7 +1473,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. @@ -1513,7 +1513,7 @@ example_common_ssl_options { ## @doc listeners..ssl.verify ## ValueType: verify_peer | verify_none ## Default: verify_none - ssl.verify: 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, @@ -1522,7 +1522,7 @@ example_common_ssl_options { ## @doc listeners..ssl.fail_if_no_peer_cert ## ValueType: Boolean ## Default: true - ssl.fail_if_no_peer_cert: false + 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 @@ -1543,7 +1543,7 @@ 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,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] + 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] } @@ -1554,14 +1554,14 @@ example_common_websocket_options { ## @doc listeners..websocket.mqtt_path ## ValueType: Path ## Default: "/mqtt" - websocket.mqtt_path: "/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 + websocket.mqtt_piggyback = multiple ## The compress flag for external WebSocket connections. ## @@ -1570,21 +1570,21 @@ example_common_websocket_options { ## @doc listeners..websocket.compress ## ValueType: Boolean ## Default: false - websocket.compress: false + websocket.compress = false ## The idle timeout for external WebSocket connections. ## ## @doc listeners..websocket.idle_timeout ## ValueType: Duration | infinity ## Default: infinity - websocket.idle_timeout: 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 + websocket.max_frame_size = infinity ## If set to true, the server fails if the client does not ## have a Sec-WebSocket-Protocol to send. @@ -1593,21 +1593,21 @@ 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 ## 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" + 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 ## 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 @@ -1615,7 +1615,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 ## @@ -1625,7 +1625,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. @@ -1633,7 +1633,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. @@ -1641,7 +1641,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. @@ -1649,7 +1649,7 @@ example_common_websocket_options { ## @doc listeners..websocket.deflate_opts.level ## ValueType: none | default | best_compression | best_speed ## Default: default - level: default + level = default ## The mem_level of deflate options for external WebSocket connections. ## @@ -1657,28 +1657,28 @@ example_common_websocket_options { ## ValueType: Integer ## Range: [1,9] ## Default: 8 - mem_level: 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 + 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 + 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 + client_context_takeover = takeover ## The deflate options for external WebSocket connections. ## @@ -1687,7 +1687,7 @@ example_common_websocket_options { ## ValueType: Integer ## Range: [8,15] ## Default: 15 - server_max_window_bits: 15 + server_max_window_bits = 15 ## The deflate options for external WebSocket connections. ## @@ -1695,6 +1695,6 @@ example_common_websocket_options { ## ValueType: Integer ## Range: [8,15] ## Default: 15 - client_max_window_bits: 15 + client_max_window_bits = 15 } } diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf index e1f7bc5b9..991afdfdd 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/etc/emqx_hocon_plugin.conf @@ -1,3 +1,3 @@ -emqx_hocon_plugin: { - name: test +emqx_hocon_plugin { + name = test } diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index bc2036fea..59f4aa9ee 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,6 +1,6 @@ -authentication: { - enable: false - authenticators: [ +authentication { + enable = false + authenticators = [ # { # name: "authenticator1" # mechanism: password-based diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 8826a94f7..57ca290d5 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,5 +1,5 @@ -authorization:{ - rules: [ +authorization { + rules = [ # { # type: http # config: { @@ -66,9 +66,9 @@ authorization:{ # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } # }, { - permission: allow - action: all - topics: ["#"] + permission = allow + action = all + topics = ["#"] } ] } diff --git a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf index c34567ee4..0e825c8c7 100644 --- a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf +++ b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf @@ -2,7 +2,7 @@ ## Configuration for EMQ X MQTT Broker Bridge ##==================================================================== -emqx_bridge_mqtt:{ +emqx_bridge_mqtt { bridges:[ # { # name: "mqtt1" @@ -11,13 +11,13 @@ emqx_bridge_mqtt:{ # forward_mountpoint: "" # reconnect_interval: "30s" # batch_size: 100 - # queue:{ + # queue { # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" # replayq_seg_bytes: "100MB" # replayq_offload_mode: false # replayq_max_total_bytes: "1GB" # }, - # config:{ + # config { # conn_type: mqtt # address: "127.0.0.1:1883" # proto_ver: v4 @@ -43,13 +43,13 @@ emqx_bridge_mqtt:{ # forward_mountpoint: "" # reconnect_interval: "30s" # batch_size: 100 - # queue:{ + # queue { # replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/" # replayq_seg_bytes: "100MB" # replayq_offload_mode: false # replayq_max_total_bytes: "1GB" # }, - # config:{ + # config { # conn_type: rpc # node: "emqx@127.0.0.1" # } diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf index 629facf7e..31c95a9ee 100644 --- a/apps/emqx_dashboard/etc/emqx_dashboard.conf +++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf @@ -2,24 +2,24 @@ ## EMQ X Dashboard ##-------------------------------------------------------------------- -emqx_dashboard:{ - default_username: "admin" - default_password: "public" +emqx_dashboard { + default_username = "admin" + default_password = "public" ## notice: sample_interval should be divisible by 60. - sample_interval: 10s + sample_interval = 10s ## api jwt timeout. default is 30 minute - token_expired_time: 60m - listeners: [ + token_expired_time = 60m + listeners = [ { - num_acceptors: 4 - max_connections: 512 - protocol: http - port: 18083 - backlog: 512 - send_timeout: 15s - send_timeout_close: true - inet6: false - ipv6_v6only: false + num_acceptors = 4 + max_connections = 512 + protocol = http + port = 18083 + backlog = 512 + send_timeout = 15s + send_timeout_close = true + inet6 = false + ipv6_v6only = false } ## , ## { diff --git a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf index c299b97a1..99a49dba3 100644 --- a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf +++ b/apps/emqx_data_bridge/etc/emqx_data_bridge.conf @@ -2,7 +2,7 @@ ## EMQ X Bridge Plugin ##-------------------------------------------------------------------- -emqx_data_bridge:{ +emqx_data_bridge { bridges:[ # {name: "mysql_bridge_1" # type: mysql diff --git a/apps/emqx_exhook/etc/emqx_exhook.conf b/apps/emqx_exhook/etc/emqx_exhook.conf index 4df798fed..8f3e25686 100644 --- a/apps/emqx_exhook/etc/emqx_exhook.conf +++ b/apps/emqx_exhook/etc/emqx_exhook.conf @@ -2,19 +2,19 @@ ## EMQ X Hooks ##==================================================================== -exhook: { +exhook { ## The default value or action will be returned, while the request to ## the gRPC server failed or no available grpc server running. ## ## Default: deny ## Value: ignore | deny - request_failed_action: deny + request_failed_action = deny ## The timeout to request grpc server ## ## Default: 5s ## Value: Duration - request_timeout: 5s + request_timeout = 5s ## Whether to automatically reconnect (initialize) the gRPC server ## @@ -23,9 +23,9 @@ exhook: { ## ## Default: false ## Value: false | Duration - auto_reconnect: 60s + auto_reconnect = 60s - servers: [ + servers = [ # { name: "default" # url: "http://127.0.0.1:9000" # #ssl: { diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 795066e79..c4b732c39 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -6,130 +6,130 @@ ## In the final version, it will be commented out. gateway.stomp { - frame: { - max_headers: 10 - max_headers_length: 1024 - max_body_length: 8192 + frame { + max_headers = 10 + max_headers_length = 1024 + max_body_length = 8192 } - clientinfo_override: { - username: "${Packet.headers.login}" - password: "${Packet.headers.passcode}" + clientinfo_override { + username = "${Packet.headers.login}" + password = "${Packet.headers.passcode}" } - authentication: { - enable: true - authenticators: [ + authentication { + enable = true + authenticators = [ { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid + name = "authenticator1" + mechanism = password-based + server_type = built-in-database + user_id_type = clientid } ] } - listener.tcp.1: { - bind: 61613 - acceptors: 16 - max_connections: 1024000 - max_conn_rate: 1000 - active_n: 100 + listener.tcp.1 { + bind = 61613 + acceptors = 16 + max_connections = 1024000 + max_conn_rate = 1000 + active_n = 100 } } -gateway.coap: { +gateway.coap { - enable_stats: false + enable_stats = false #authentication.enable: false - authentication: { - enable: true - authenticators: [ + authentication { + enable = true + authenticators = [ { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid + name = "authenticator1" + mechanism = password-based + server_type = built-in-database + user_id_type = clientid } ] } - heartbeat: 30s - notify_type: qos - subscribe_qos: qos0 - publish_qos: qos1 - listener.udp.1: { - bind: 5683 + heartbeat = 30s + notify_type = qos + subscribe_qos = qos0 + publish_qos = qos1 + listener.udp.1 { + bind = 5683 } } -gateway.mqttsn: { +gateway.mqttsn { ## The MQTT-SN Gateway ID in ADVERTISE message. - gateway_id: 1 + gateway_id = 1 ## Enable broadcast this gateway to WLAN - broadcast: true + broadcast = true ## To control whether write statistics data into ETS table ## for dashbord to read. - enable_stats: true + enable_stats = true ## To control whether accept and process the received ## publish message with qos=-1. - enable_qos3: true + enable_qos3 = true ## Idle timeout for a MQTT-SN channel - idle_timeout: 30s + 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" + predefined = [ + { id = 1 + topic = "/predefined/topic/name/hello" }, - { id: 2 - topic: "/predefined/topic/name/nice" + { id = 2 + topic = "/predefined/topic/name/nice" } ] ### ClientInfo override - clientinfo_override: { - username: "mqtt_sn_user" - password: "abc" + clientinfo_override { + username = "mqtt_sn_user" + password = "abc" } - listener.udp.1: { - bind: 1884 - max_connections: 10240000 - max_conn_rate: 1000 + listener.udp.1 { + bind = 1884 + max_connections = 10240000 + max_conn_rate = 1000 } } -gateway.exproto: { +gateway.exproto { ## The gRPC server to accept requests - server: { - bind: 9100 + server { + bind = 9100 #ssl.keyfile: #ssl.certfile: #ssl.cacertfile: } - handler: { - address: "http://127.0.0.1:9001" + handler { + address = "http://127.0.0.1:9001" #ssl.keyfile: #ssl.certfile: #ssl.cacertfile: } - authentication.enable: false + authentication.enable = false - listener.tcp.1: { - bind: 7993 - acceptors: 8 - max_connections: 10240 - max_conn_rate: 1000 + listener.tcp.1 { + bind = 7993 + acceptors = 8 + max_connections = 10240 + max_conn_rate = 1000 } #listener.ssl.1: {} @@ -137,29 +137,29 @@ gateway.exproto: { #listener.dtls.1: {} } -gateway.lwm2m: { +gateway.lwm2m { - xml_dir: "{{ platform_etc_dir }}/lwm2m_xml" + xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" - lifetime_min: 1s - lifetime_max: 86400s - qmode_time_windonw: 22 - auto_observe: false + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false - mountpoint: "lwm2m/%e/" + mountpoint = "lwm2m/%e/" ## always | contains_object_list - update_msg_publish_condition: contains_object_list + update_msg_publish_condition = contains_object_list - translators: { - command: "dn/#" - response: "up/resp" - notify: "up/notify" - register: "up/resp" - update: "up/resp" + translators { + command = "dn/#" + response = "up/resp" + notify = "up/notify" + register = "up/resp" + update = "up/resp" } listener.udp.1 { - bind: 5783 + bind = 5783 } } diff --git a/apps/emqx_machine/etc/emqx_machine.conf b/apps/emqx_machine/etc/emqx_machine.conf index 989665f97..d80ac6ddb 100644 --- a/apps/emqx_machine/etc/emqx_machine.conf +++ b/apps/emqx_machine/etc/emqx_machine.conf @@ -11,35 +11,35 @@ node { ## @doc node.name ## ValueType: NodeName ## Default: emqx@127.0.0.1 - name: "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 + cookie = emqxsecretcookie ## Data dir for the node ## ## @doc node.data_dir ## ValueType: Folder ## Default: "{{ platform_data_dir }}/" - data_dir: "{{ platform_data_dir }}/" + data_dir = "{{ platform_data_dir }}/" ## Dir of crash dump file. ## ## @doc node.crash_dump_dir ## ValueType: Folder ## Default: "{{ platform_log_dir }}/" - crash_dump_dir: "{{ 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 + global_gc_interval = 15m ## Sets the net_kernel tick time in seconds. ## Notice that all communicating nodes are to have the same @@ -50,7 +50,7 @@ node { ## @doc node.dist_net_ticktime ## ValueType: Number ## Default: 2m - dist_net_ticktime: 2m + dist_net_ticktime = 2m ## Sets the port range for the listener socket of a distributed ## Erlang node. @@ -63,7 +63,7 @@ node { ## ValueType: Integer ## Range: [1024,65535] ## Default: 6369 - dist_listen_min: 6369 + dist_listen_min = 6369 ## Sets the port range for the listener socket of a distributed ## Erlang node. @@ -76,7 +76,7 @@ node { ## ValueType: Integer ## Range: [1024,65535] ## Default: 6369 - dist_listen_max: 6369 + dist_listen_max = 6369 ## Sets the maximum depth of call stack back-traces in the exit ## reason element of 'EXIT' tuples. @@ -87,7 +87,7 @@ node { ## ValueType: Integer ## Range: [0,1024] ## Default: 23 - backtrace_depth: 23 + backtrace_depth = 23 } @@ -100,14 +100,14 @@ cluster { ## @doc cluster.name ## ValueType: String ## Default: emqxcl - name: emqxcl + name = emqxcl ## Enable cluster autoheal from network partition. ## ## @doc cluster.autoheal ## ValueType: Boolean ## Default: true - autoheal: true + autoheal = true ## Autoclean down node. A down node will be removed from the cluster ## if this value > 0. @@ -115,7 +115,7 @@ cluster { ## @doc cluster.autoclean ## ValueType: Duration ## Default: 5m - autoclean: 5m + autoclean = 5m ## Node discovery strategy to join the cluster. ## @@ -129,7 +129,7 @@ cluster { ## - k8s: Kubernetes ## ## Default: manual - discovery_strategy: manual + discovery_strategy = manual ##---------------------------------------------------------------- ## Cluster using static node list @@ -140,7 +140,7 @@ cluster { ## @doc cluster.static.seeds ## ValueType: Array ## Default: [] - seeds: ["emqx1@127.0.0.1", "emqx2@127.0.0.1"] + seeds = ["emqx1@127.0.0.1", "emqx2@127.0.0.1"] } ##---------------------------------------------------------------- @@ -152,21 +152,21 @@ cluster { ## @doc cluster.mcast.addr ## ValueType: IPAddress ## Default: "239.192.0.1" - addr: "239.192.0.1" + addr = "239.192.0.1" ## Multicast Ports. ## ## @doc cluster.mcast.ports ## ValueType: Array ## Default: [4369, 4370] - ports: [4369, 4370] + ports = [4369, 4370] ## Multicast Iface. ## ## @doc cluster.mcast.iface ## ValueType: IPAddress ## Default: "0.0.0.0" - iface: "0.0.0.0" + iface = "0.0.0.0" ## Multicast Ttl. ## @@ -174,14 +174,14 @@ cluster { ## ValueType: Integer ## Range: [0,255] ## Default: 255 - ttl: 255 + ttl = 255 ## Multicast loop. ## ## @doc cluster.mcast.loop ## ValueType: Boolean ## Default: true - loop: true + loop = true } ##---------------------------------------------------------------- @@ -193,14 +193,14 @@ cluster { ## @doc cluster.dns.name ## ValueType: String ## Default: localhost - name: 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 + app = emqx } ##---------------------------------------------------------------- @@ -212,7 +212,7 @@ cluster { ## @doc cluster.etcd.server ## ValueType: URL ## Required: true - server: "http://127.0.0.1:2379" + 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/// @@ -220,28 +220,28 @@ cluster { ## @doc cluster.etcd.prefix ## ValueType: String ## Default: emqxcl - prefix: emqxcl + prefix = emqxcl ## The TTL for node's path in etcd. ## ## @doc cluster.etcd.node_ttl ## ValueType: Duration ## Default: 1m - node_ttl: 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" + 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" + 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. @@ -249,7 +249,7 @@ cluster { ## @doc cluster.etcd.ssl.cacertfile ## ValueType: File ## Default: "{{ platform_etc_dir }}/certs/cacert.pem" - ssl.cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" } ##---------------------------------------------------------------- @@ -261,47 +261,47 @@ cluster { ## @doc cluster.k8s.apiserver ## ValueType: URL ## Required: true - apiserver: "http://10.110.111.204:8080" + 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 + 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 + address_type = ip ## The app name helps build 'node.name'. ## ## @doc cluster.k8s.app_name ## ValueType: String ## Default: emqx - app_name: 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" + suffix = "pod.local" ## Kubernetes Namespace ## ## @doc cluster.k8s.namespace ## ValueType: String ## Default: default - namespace: default + namespace = default } - db_backend: mnesia + db_backend = mnesia - rlog: { + rlog { # role: core # core_nodes: [] } @@ -326,7 +326,7 @@ log { ## @doc log.primary_level ## ValueType: debug | info | notice | warning | error | critical | alert | emergency ## Default: warning - primary_level: warning + primary_level = warning ##---------------------------------------------------------------- ## The console log handler send log messages to emqx console @@ -335,7 +335,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 @@ -344,13 +344,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_log: { + file_handlers.emqx_log { ## The log level filter of this handler ## All the log messages with levels lower than this level will ## be dropped. @@ -358,7 +358,7 @@ log { ## @doc log.file_handlers..level ## ValueType: debug | info | notice | warning | error | critical | alert | emergency ## Default: warning - level: warning + level = warning ## The log file for specified level. ## @@ -373,7 +373,7 @@ log { ## @doc log.file_handlers..file ## ValueType: File ## Required: true - file: "{{ platform_log_dir }}/emqx.log" + file = "{{ platform_log_dir }}/emqx.log" ## Enables the log rotation. ## With this enabled, new log files will be created when the current @@ -382,7 +382,7 @@ log { ## @doc log.file_handlers..rotation.enable ## ValueType: Boolean ## Default: true - rotation.enable: true + rotation.enable = true ## Maximum rotation count of log files. ## @@ -390,7 +390,7 @@ log { ## ValueType: Integer ## Range: [1, 2048] ## Default: 10 - rotation.count: 10 + rotation.count = 10 ## Maximum size of each log file. ## @@ -401,16 +401,16 @@ log { ## @doc log.file_handlers..max_size ## ValueType: Size | infinity ## Default: 10MB - max_size: 10MB + max_size = 10MB } ## file_handlers. ## ## You could also create multiple file handlers for different ## log level for example: - file_handlers.emqx_error_log: { - level: error - file: "{{ platform_log_dir }}/error.log" + file_handlers.emqx_error_log { + level = error + file = "{{ platform_log_dir }}/error.log" } ## Timezone offset to display in logs @@ -421,7 +421,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. ## @@ -429,7 +429,7 @@ log { ## ValueType: unlimited | Integer ## Range: [0, +Inf) ## Default: unlimited - chars_limit: unlimited + chars_limit = unlimited ## Maximum depth for Erlang term log formatting ## and Erlang process message queue inspection. @@ -437,19 +437,19 @@ log { ## @doc log.max_depth ## ValueType: unlimited | Integer ## Default: 80 - max_depth: 80 + max_depth = 80 ## Log formatter ## @doc log.formatter ## ValueType: text | json ## Default: text - formatter: text + formatter = text ## Log to single line ## @doc log.single_line ## ValueType: Boolean ## Default: true - single_line: true + single_line = true ## The max allowed queue length before switching to sync mode. ## @@ -460,7 +460,7 @@ log { ## ValueType: Integer ## Range: [0, ${log.drop_mode_qlen}] ## Default: 100 - sync_mode_qlen: 100 + sync_mode_qlen = 100 ## The max allowed queue length before switching to drop mode. ## @@ -472,7 +472,7 @@ log { ## ValueType: Integer ## Range: [${log.sync_mode_qlen}, ${log.flush_qlen}] ## Default: 3000 - drop_mode_qlen: 3000 + drop_mode_qlen = 3000 ## The max allowed queue length before switching to flush mode. ## @@ -485,7 +485,7 @@ log { ## ValueType: Integer ## Range: [${log.drop_mode_qlen}, infinity) ## Default: 8000 - flush_qlen: 8000 + flush_qlen = 8000 ## Kill the log handler when it gets overloaded. ## @@ -498,7 +498,7 @@ log { ## @doc log.overload_kill.enable ## ValueType: Boolean ## Default: true - overload_kill.enable: true + overload_kill.enable = true ## The max allowed queue length before killing the log hanlder. ## @@ -510,7 +510,7 @@ log { ## 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. ## @@ -521,7 +521,7 @@ log { ## @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. ## @@ -531,7 +531,7 @@ log { ## @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. ## @@ -547,7 +547,7 @@ log { ## @doc log.burst_limit.enable ## ValueType: Boolean ## Default: false - burst_limit.enable: 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 @@ -556,14 +556,14 @@ log { ## @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 ## ValueType: duration ## Default: 1s - burst_limit.window_time: 1s + burst_limit.window_time = 1s } ##================================================================== @@ -575,7 +575,7 @@ rpc { ## @doc rpc.mode ## ValueType: sync | async ## Default: async - mode: async + mode = async ## Max batch size of async RPC requests. ## @@ -586,7 +586,7 @@ rpc { ## ValueType: Integer ## Range: [0, 1048576] ## Default: 0 - async_batch_size: 256 + async_batch_size = 256 ## RPC port discovery ## @@ -601,7 +601,7 @@ rpc { ## an integer, then the listening port will be `5370 + ` ## ## Default: `stateless`. - port_discovery: stateless + port_discovery = stateless ## TCP server port for RPC. ## @@ -611,7 +611,7 @@ rpc { ## ValueType: Integer ## Range: [1024-65535] ## Defaults: 5369 - tcp_server_port: 5369 + tcp_server_port = 5369 ## Number of outgoing RPC connections. ## @@ -622,75 +622,75 @@ rpc { ## ValueType: Integer ## Range: [1, 256] ## Defaults: 1 - tcp_client_num: 1 + tcp_client_num = 1 ## RCP Client connect timeout. ## ## @doc rpc.connect_timeout ## ValueType: Duration ## Default: 5s - connect_timeout: 5s + connect_timeout = 5s ## TCP send timeout of RPC client and server. ## ## @doc rpc.send_timeout ## ValueType: Duration ## Default: 5s - send_timeout: 5s + send_timeout = 5s ## Authentication timeout ## ## @doc rpc.authentication_timeout ## ValueType: Duration ## Default: 5s - authentication_timeout: 5s + authentication_timeout = 5s ## Default receive timeout for call() functions ## ## @doc rpc.call_receive_timeout ## ValueType: Duration ## Default: 15s - call_receive_timeout: 15s + call_receive_timeout = 15s ## Socket idle keepalive. ## ## @doc rpc.socket_keepalive_idle ## ValueType: Duration ## Default: 900s - socket_keepalive_idle: 900s + socket_keepalive_idle = 900s ## TCP Keepalive probes interval. ## ## @doc rpc.socket_keepalive_interval ## ValueType: Duration ## Default: 75s - socket_keepalive_interval: 75s + socket_keepalive_interval = 75s ## Probes lost to close the connection ## ## @doc rpc.socket_keepalive_count ## ValueType: Integer ## Default: 9 - socket_keepalive_count: 9 + socket_keepalive_count = 9 ## Size of TCP send buffer. ## ## @doc rpc.socket_sndbuf ## ValueType: Size ## Default: 1MB - socket_sndbuf: 1MB + socket_sndbuf = 1MB ## Size of TCP receive buffer. ## ## @doc rpc.socket_recbuf ## ValueType: Size ## Default: 1MB - socket_recbuf: 1MB + socket_recbuf = 1MB ## Size of user-level software socket buffer. ## ## @doc rpc.socket_buffer ## ValueType: Size ## Default: 1MB - socket_buffer: 1MB + socket_buffer = 1MB } diff --git a/apps/emqx_management/etc/emqx_management.conf b/apps/emqx_management/etc/emqx_management.conf index 127a21e3b..8517aec7f 100644 --- a/apps/emqx_management/etc/emqx_management.conf +++ b/apps/emqx_management/etc/emqx_management.conf @@ -1,22 +1,22 @@ -emqx_management:{ - applications: [ +emqx_management { + applications = [ { - id: "admin", - secret: "public" + id = "admin", + secret = "public" } ] - max_row_limit: 10000 - listeners: [ + max_row_limit = 10000 + listeners = [ { - num_acceptors: 4 - max_connections: 512 - protocol: http - port: 8081 - backlog: 512 - send_timeout: 15s - send_timeout_close: true - inet6: false - ipv6_v6only: false + num_acceptors = 4 + max_connections = 512 + protocol = http + port = 8081 + backlog = 512 + send_timeout = 15s + send_timeout_close = true + inet6 = false + ipv6_v6only = false } ## , ## { diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index d939f90f4..7b6f60cf1 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -1,20 +1,21 @@ -delayed: { - enable: true + +delayed { + enable = true ## 0 is no limit - max_delayed_messages: 0 + max_delayed_messages = 0 } -recon: { - enable: true +recon { + enable = true } -telemetry: { - enable: true +telemetry { + enable = true } event_message { - "$event/client_connected": true - "$event/client_disconnected": true + "$event/client_connected" = true + "$event/client_disconnected" = true # "$event/client_subscribed": false # "$event/client_unsubscribed": false # "$event/message_delivered": false @@ -22,17 +23,17 @@ event_message { # "$event/message_dropped": false } -topic_metrics:{ - topics: ["topic/#"] +topic_metrics { + topics = ["topic/#"] } -rewrite:{ - rules: [ +rewrite { + rules = [ { - action: publish - source_topic: "x/#" - re: "^x/y/(.+)$" - dest_topic: "z/y/$1" + action = publish + source_topic = "x/#" + re = "^x/y/(.+)$" + dest_topic = "z/y/$1" } ] } diff --git a/apps/emqx_prometheus/etc/emqx_prometheus.conf b/apps/emqx_prometheus/etc/emqx_prometheus.conf index 38ce5e501..19b88dd84 100644 --- a/apps/emqx_prometheus/etc/emqx_prometheus.conf +++ b/apps/emqx_prometheus/etc/emqx_prometheus.conf @@ -1,8 +1,8 @@ ##-------------------------------------------------------------------- ## emqx_prometheus for EMQ X ##-------------------------------------------------------------------- -prometheus: { - push_gateway_server: "http://127.0.0.1:9091" - interval: "15s" - enable: true +prometheus { + push_gateway_server = "http://127.0.0.1:9091" + interval = "15s" + enable = true } diff --git a/apps/emqx_retainer/etc/emqx_retainer.conf b/apps/emqx_retainer/etc/emqx_retainer.conf index 4a4308b2e..ba6bdfa6c 100644 --- a/apps/emqx_retainer/etc/emqx_retainer.conf +++ b/apps/emqx_retainer/etc/emqx_retainer.conf @@ -5,9 +5,9 @@ ## Where to store the retained messages. ## ## Notice that all nodes in the same cluster have to be configured to -emqx_retainer: { +emqx_retainer { ## enable/disable emqx_retainer - enable: true + enable = true ## Periodic interval for cleaning up expired messages. Never clear if the value is 0. ## @@ -22,12 +22,12 @@ emqx_retainer: { ## - 20s: 20 seconds ## ## Default: 0s - msg_clear_interval: 0s + msg_clear_interval = 0s ## Message retention time. 0 means message will never be expired. ## ## Default: 0s - msg_expiry_interval: 0s + msg_expiry_interval = 0s ## The message read and deliver flow rate control ## When a client subscribe to a wildcard topic, may many retained messages will be loaded. @@ -37,42 +37,42 @@ emqx_retainer: { ## deliver -> ## repeat this, until all retianed messages are delivered ## - flow_control: { + flow_control { ## The max messages number per read from storage. 0 means no limit ## ## Default: 0 - max_read_number: 0 + max_read_number = 0 ## The max number of retained message can be delivered in emqx per quota_release_interval.0 means no limit ## ## Default: 0 - msg_deliver_quota: 0 + msg_deliver_quota = 0 ## deliver quota reset interval ## ## Default: 0s - quota_release_interval: 0s + quota_release_interval = 0s } ## Maximum retained message size. ## ## Value: Bytes - max_payload_size: 1MB + max_payload_size = 1MB ## Storage connect parameters ## ## Value: built_in_database ## - config: { + config { - type: built_in_database + type = built_in_database ## storage_type: ram | disc | disc_only - storage_type: ram + storage_type = ram ## Maximum number of retained messages. 0 means no limit. ## ## Value: Number >= 0 - max_retained_messages: 0 + max_retained_messages = 0 } } diff --git a/apps/emqx_rule_engine/etc/emqx_rule_engine.conf b/apps/emqx_rule_engine/etc/emqx_rule_engine.conf index c1637d66d..22543a977 100644 --- a/apps/emqx_rule_engine/etc/emqx_rule_engine.conf +++ b/apps/emqx_rule_engine/etc/emqx_rule_engine.conf @@ -1,6 +1,6 @@ ##==================================================================== ## Rule Engine for EMQ X R5.0 ##==================================================================== -emqx_rule_engine:{ - ignore_sys_message: true +emqx_rule_engine { + ignore_sys_message = true } diff --git a/apps/emqx_statsd/etc/emqx_statsd.conf b/apps/emqx_statsd/etc/emqx_statsd.conf index 2bb6014a4..e3ecfcc2b 100644 --- a/apps/emqx_statsd/etc/emqx_statsd.conf +++ b/apps/emqx_statsd/etc/emqx_statsd.conf @@ -2,9 +2,9 @@ ## Statsd for EMQ X ##-------------------------------------------------------------------- -statsd:{ - enable: true - server: "127.0.0.1:8125" - sample_time_interval: "10s" - flush_time_interval: "10s" +statsd { + enable = true + server = "127.0.0.1:8125" + sample_time_interval = "10s" + flush_time_interval = "10s" } From c0447d58b4a4ba346e5fb4a496969d6b94570cd6 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 20 Aug 2021 19:04:52 +0800 Subject: [PATCH 078/306] fix(config): configs in emqx_machine.conf was merged twice into emqx.conf --- scripts/merge-config.escript | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/merge-config.escript b/scripts/merge-config.escript index b324b7e5c..372ff3900 100755 --- a/scripts/merge-config.escript +++ b/scripts/merge-config.escript @@ -11,9 +11,8 @@ -mode(compile). main(_) -> - BaseConf = "apps/emqx_machine/etc/emqx_machine.conf", - {ok, Bin} = file:read_file(BaseConf), - Apps = filelib:wildcard("*", "apps/"), + {ok, BaseConf} = file:read_file("apps/emqx_machine/etc/emqx_machine.conf"), + Apps = filelib:wildcard("*", "apps/") -- ["emqx_machine"], Conf = lists:foldl(fun(App, Acc) -> Filename = filename:join([apps, App, "etc", App]) ++ ".conf", case filelib:is_regular(Filename) of @@ -22,5 +21,5 @@ main(_) -> [Acc, io_lib:nl(), Bin1]; false -> Acc end - end, Bin, Apps), + end, BaseConf, Apps), ok = file:write_file("apps/emqx_machine/etc/emqx.conf.all", Conf). From 91f787533dbba30ab3dfb9e2ebab5a59e326f986 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sat, 21 Aug 2021 11:36:35 +0800 Subject: [PATCH 079/306] fix(log): text formatter crash ``` (emqx@127.0.0.1)1> logger:error("abc"). ok 2021-08-20T18:10:01.180622+08:00 error: FORMATTER CRASH: {string,"abc"} ``` --- apps/emqx/src/emqx_logger_textfmt.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl index cf419f381..94af0ca2a 100644 --- a/apps/emqx/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -23,17 +23,17 @@ check_config(X) -> logger_formatter:check_config(X). format(#{msg := {report, Report}, meta := Meta} = Event, Config) when is_map(Report) -> logger_formatter:format(Event#{msg := {report, enrich(Report, Meta)}}, Config); -format(#{msg := {Fmt, Args}, meta := Meta} = Event, Config) when is_list(Fmt) -> - {NewFmt, NewArgs} = enrich_fmt(Fmt, Args, Meta), - logger_formatter:format(Event#{msg := {NewFmt, NewArgs}}, Config). +format(#{msg := Msg, meta := Meta} = Event, Config) -> + NewMsg = enrich_fmt(Msg, Meta), + logger_formatter:format(Event#{msg := NewMsg}, Config). enrich(Report, #{mfa := Mfa, line := Line}) -> Report#{mfa => mfa(Mfa), line => Line}; enrich(Report, _) -> Report. -enrich_fmt(Fmt, Args, #{mfa := Mfa, line := Line}) -> +enrich_fmt({Fmt, Args}, #{mfa := Mfa, line := Line}) when is_list(Fmt) -> {Fmt ++ " mfa: ~s line: ~w", Args ++ [mfa(Mfa), Line]}; -enrich_fmt(Fmt, Args, _) -> - {Fmt, Args}. +enrich_fmt(Msg, _) -> + Msg. mfa({M, F, A}) -> atom_to_list(M) ++ ":" ++ atom_to_list(F) ++ "/" ++ integer_to_list(A). From af902df040d6c43b3430ddb0d5501311f41f6c51 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 16 Aug 2021 15:41:21 +0200 Subject: [PATCH 080/306] fix(emqx_machine): Fix application startup order Ekka application should be started first --- apps/emqx_machine/src/emqx_machine.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/emqx_machine/src/emqx_machine.erl b/apps/emqx_machine/src/emqx_machine.erl index bcfb1f501..3e5772b4a 100644 --- a/apps/emqx_machine/src/emqx_machine.erl +++ b/apps/emqx_machine/src/emqx_machine.erl @@ -120,7 +120,7 @@ start_one_app(App) -> ?SLOG(debug, #{msg => "started_apps", apps => Apps}); {error, Reason} -> ?SLOG(critical, #{msg => "failed_to_start_app", app => App, reason => Reason}), - error({faile_to_start_app, App, Reason}) + error({failed_to_start_app, App, Reason}) end. %% list of app names which should be rebooted when: @@ -131,7 +131,6 @@ reboot_apps() -> , esockd , ranch , cowboy - , ekka , emqx , emqx_prometheus , emqx_modules From ca0fb214a7095b3ed0b2613fe162d5a175d23e54 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Mon, 16 Aug 2021 15:42:20 +0200 Subject: [PATCH 081/306] feat(emqx-sn): Add tables to the SN shard --- apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl index 1249831cc..c29f290b8 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl @@ -149,9 +149,11 @@ init([InstaId, PredefTopics]) -> {ram_copies, [node()]}, {record_name, emqx_sn_registry}, {attributes, record_info(fields, emqx_sn_registry)}, - {storage_properties, [{ets, [{read_concurrency, true}]}]} + {storage_properties, [{ets, [{read_concurrency, true}]}]}, + {rlog_shard, ?SN_SHARD} ]), ok = ekka_mnesia:copy_table(Tab, ram_copies), + ok = ekka_rlog:wait_for_shards([?SN_SHARD], infinity), % FIXME: %ok = ekka_rlog:wait_for_shards([?CM_SHARD], infinity), MaxPredefId = lists:foldl( From 53a0c8b8a8f37d66b035d3959431f7d31b82151a Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Wed, 18 Aug 2021 17:17:31 +0200 Subject: [PATCH 082/306] chore(ekka): Bump version to 0.10.7 --- 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 f34173fae..34b12e233 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.2"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.4"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.7"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} diff --git a/rebar.config b/rebar.config index 8cfc3cf7c..13efa22a7 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,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.2"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.4"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.7"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.1.7"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} From 9c74fa42a59dfa30e084db2dbe9c0f3aee57b7b6 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:41:25 +0200 Subject: [PATCH 083/306] chore(rlog): Use the new method of creating shards --- apps/emqx/include/emqx.hrl | 1 + apps/emqx/src/emqx_alarm.erl | 3 --- apps/emqx/src/emqx_banned.erl | 3 +-- apps/emqx/src/emqx_cm_registry.erl | 5 +---- apps/emqx/src/emqx_router.erl | 2 +- apps/emqx/src/emqx_router_helper.erl | 3 +-- apps/emqx/src/emqx_shared_sub.erl | 3 +-- apps/emqx/src/emqx_trie.erl | 3 +-- .../enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl | 7 +++---- apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl | 2 +- apps/emqx_dashboard/src/emqx_dashboard_admin.erl | 3 +-- apps/emqx_dashboard/src/emqx_dashboard_token.erl | 3 +-- apps/emqx_gateway/src/emqx_gateway_cm_registry.erl | 3 +++ apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl | 2 -- apps/emqx_management/src/emqx_mgmt_auth.erl | 3 +-- apps/emqx_modules/src/emqx_delayed.erl | 2 -- apps/emqx_modules/src/emqx_telemetry.erl | 2 -- apps/emqx_retainer/src/emqx_retainer.erl | 2 -- apps/emqx_retainer/src/emqx_retainer_mnesia.erl | 3 +-- apps/emqx_rule_engine/src/emqx_rule_registry.erl | 9 ++++----- 20 files changed, 22 insertions(+), 42 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 60dccd9a3..58fdc5f98 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -26,6 +26,7 @@ -define(COMMON_SHARD, emqx_common_shard). -define(SHARED_SUB_SHARD, emqx_shared_sub_shard). -define(MOD_DELAYED_SHARD, emqx_delayed_shard). +-define(CM_SHARD, emqx_cm_shard). %%-------------------------------------------------------------------- %% Banner diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 7599f9569..8f3e1c568 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -85,9 +85,6 @@ -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). diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index f0991d967..c143a20a6 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -50,8 +50,6 @@ -define(BANNED_TAB, ?MODULE). --rlog_shard({?COMMON_SHARD, ?BANNED_TAB}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -59,6 +57,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?BANNED_TAB, [ {type, set}, + {rlog_shard, ?COMMON_SHARD}, {disc_copies, [node()]}, {record_name, banned}, {attributes, record_info(fields, banned)}, diff --git a/apps/emqx/src/emqx_cm_registry.erl b/apps/emqx/src/emqx_cm_registry.erl index 9326d2b8e..6fc34dee8 100644 --- a/apps/emqx/src/emqx_cm_registry.erl +++ b/apps/emqx/src/emqx_cm_registry.erl @@ -47,10 +47,6 @@ -define(TAB, emqx_channel_registry). -define(LOCK, {?MODULE, cleanup_down}). --define(CM_SHARD, emqx_cm_shard). - --rlog_shard({?CM_SHARD, ?TAB}). - -record(channel, {chid, pid}). %% @doc Start the global channel registry. @@ -106,6 +102,7 @@ record(ClientId, ChanPid) -> init([]) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, + {rlog_shard, ?CM_SHARD}, {ram_copies, [node()]}, {record_name, channel}, {attributes, record_info(fields, channel)}, diff --git a/apps/emqx/src/emqx_router.erl b/apps/emqx/src/emqx_router.erl index 8989c3b10..c39571d9f 100644 --- a/apps/emqx/src/emqx_router.erl +++ b/apps/emqx/src/emqx_router.erl @@ -68,7 +68,6 @@ -type(dest() :: node() | {group(), node()}). -define(ROUTE_TAB, emqx_route). --rlog_shard({?ROUTE_SHARD, ?ROUTE_TAB}). %%-------------------------------------------------------------------- %% Mnesia bootstrap @@ -77,6 +76,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTE_TAB, [ {type, bag}, + {rlog_shard, ?ROUTE_SHARD}, {ram_copies, [node()]}, {record_name, route}, {attributes, record_info(fields, route)}, diff --git a/apps/emqx/src/emqx_router_helper.erl b/apps/emqx/src/emqx_router_helper.erl index 5866e86b3..78d763cac 100644 --- a/apps/emqx/src/emqx_router_helper.erl +++ b/apps/emqx/src/emqx_router_helper.erl @@ -52,8 +52,6 @@ -define(ROUTING_NODE, emqx_routing_node). -define(LOCK, {?MODULE, cleanup_routes}). --rlog_shard({?ROUTE_SHARD, ?ROUTING_NODE}). - -dialyzer({nowarn_function, [cleanup_routes/1]}). %%-------------------------------------------------------------------- @@ -63,6 +61,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTING_NODE, [ {type, set}, + {rlog_shard, ?ROUTE_SHARD}, {ram_copies, [node()]}, {record_name, routing_node}, {attributes, record_info(fields, routing_node)}, diff --git a/apps/emqx/src/emqx_shared_sub.erl b/apps/emqx/src/emqx_shared_sub.erl index ccc050165..9e5dd726f 100644 --- a/apps/emqx/src/emqx_shared_sub.erl +++ b/apps/emqx/src/emqx_shared_sub.erl @@ -76,8 +76,6 @@ -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}). @@ -89,6 +87,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, + {rlog_shard, ?SHARED_SUB_SHARD}, {ram_copies, [node()]}, {record_name, emqx_shared_subscription}, {attributes, record_info(fields, emqx_shared_subscription)}]); diff --git a/apps/emqx/src/emqx_trie.erl b/apps/emqx/src/emqx_trie.erl index ebfcfcbe3..ea70ff7f3 100644 --- a/apps/emqx/src/emqx_trie.erl +++ b/apps/emqx/src/emqx_trie.erl @@ -50,8 +50,6 @@ , count = 0 :: non_neg_integer() }). --rlog_shard({?ROUTE_SHARD, ?TRIE}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- @@ -64,6 +62,7 @@ mnesia(boot) -> {write_concurrency, true} ]}], ok = ekka_mnesia:create_table(?TRIE, [ + {rlog_shard, ?ROUTE_SHARD}, {ram_copies, [node()]}, {record_name, ?TRIE}, {attributes, record_info(fields, ?TRIE)}, 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 index 2d433d408..f4fede9f5 100644 --- 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 @@ -45,8 +45,6 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --rlog_shard({?AUTH_SHARD, ?TAB}). - -record(user_info, { user_id , stored_key @@ -63,6 +61,7 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ + {rlog_shard, ?AUTH_SHARD}, {disc_copies, [node()]}, {record_name, user_info}, {attributes, record_info(fields, user_info)}, @@ -112,7 +111,7 @@ create(#{ algorithm := Algorithm update(Config, #{user_group := Unique}) -> create(Config#{'_unique' => Unique}). - + authenticate(#{auth_method := AuthMethod, auth_data := AuthData, auth_cache := AuthCache}, State) -> @@ -272,4 +271,4 @@ trans(Fun, Args) -> end. serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> - #{user_id => UserID, superuser => Superuser}. \ No newline at end of file + #{user_id => UserID, superuser => Superuser}. 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 08c0ffad1..9bbf3239c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -58,7 +58,6 @@ -define(TAB, ?MODULE). --rlog_shard({?AUTH_SHARD, ?TAB}). %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -67,6 +66,7 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ + {rlog_shard, ?AUTH_SHARD}, {disc_copies, [node()]}, {record_name, user_info}, {attributes, record_info(fields, user_info)}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 982756805..8a1306e94 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -20,8 +20,6 @@ -include("emqx_dashboard.hrl"). --rlog_shard({?DASHBOARD_SHARD, mqtt_admin}). - -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). @@ -54,6 +52,7 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(mqtt_admin, [ {type, set}, + {rlog_shard, ?DASHBOARD_SHARD}, {disc_copies, [node()]}, {record_name, mqtt_admin}, {attributes, record_info(fields, mqtt_admin)}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index 432a64621..9086b4c2e 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -26,8 +26,6 @@ , destroy_by_username/1 ]). --rlog_shard({?DASHBOARD_SHARD, mqtt_admin_jwt}). - -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). @@ -80,6 +78,7 @@ destroy_by_username(Username) -> mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {type, set}, + {rlog_shard, ?DASHBOARD_SHARD}, {disc_copies, [node()]}, {record_name, mqtt_admin_jwt}, {attributes, record_info(fields, mqtt_admin_jwt)}, diff --git a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl index 2c449828e..1d9daa637 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl @@ -40,6 +40,8 @@ , code_change/3 ]). +-include_lib("emqx/include/emqx.hrl"). + -define(LOCK, {?MODULE, cleanup_down}). -record(channel, {chid, pid}). @@ -89,6 +91,7 @@ init([Type]) -> Tab = tabname(Type), ok = ekka_mnesia:create_table(Tab, [ {type, bag}, + {rlog_shard, ?CM_SHARD}, {ram_copies, [node()]}, {record_name, channel}, {attributes, record_info(fields, channel)}, diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl index c29f290b8..2534eee26 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_registry.erl @@ -60,8 +60,6 @@ %-boot_mnesia({mnesia, [boot]}). %-copy_mnesia({mnesia, [copy]}). -%-rlog_shard({?SN_SHARD, ?TAB}). - %%% @doc Create or replicate tables. %-spec(mnesia(boot | copy) -> ok). %mnesia(boot) -> diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 73ec37fc2..7c0eb8e82 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -48,14 +48,13 @@ -include("emqx_mgmt.hrl"). --rlog_shard({?MANAGEMENT_SHARD, mqtt_app}). - %%-------------------------------------------------------------------- %% Mnesia Bootstrap %%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(mqtt_app, [ + {rlog_shard, ?MANAGEMENT_SHARD}, {disc_copies, [node()]}, {record_name, mqtt_app}, {attributes, record_info(fields, mqtt_app)}]); diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index df8387aaf..f7de5d69d 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -56,8 +56,6 @@ -define(SERVER, ?MODULE). -define(MAX_INTERVAL, 4294967). --rlog_shard({?MOD_DELAYED_SHARD, ?TAB}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- diff --git a/apps/emqx_modules/src/emqx_telemetry.erl b/apps/emqx_modules/src/emqx_telemetry.erl index aa207ac95..65852df93 100644 --- a/apps/emqx_modules/src/emqx_telemetry.erl +++ b/apps/emqx_modules/src/emqx_telemetry.erl @@ -86,8 +86,6 @@ -define(TELEMETRY, emqx_telemetry). --rlog_shard({?COMMON_SHARD, ?TELEMETRY}). - %%-------------------------------------------------------------------- %% Mnesia bootstrap %%-------------------------------------------------------------------- diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 4c36cb541..cb4262451 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -57,8 +57,6 @@ , wait_quotas := list() }. --rlog_shard({?RETAINER_SHARD, ?TAB}). - -define(DEF_MAX_PAYLOAD_SIZE, (1024 * 1024)). -define(DEF_EXPIRY_INTERVAL, 0). diff --git a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl index 5f91a40c9..54d0f2c3a 100644 --- a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl +++ b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl @@ -34,8 +34,6 @@ -export([create_resource/1]). --rlog_shard({?RETAINER_SHARD, ?TAB}). - -record(retained, {topic, msg, expiry_time}). -type batch_read_result() :: @@ -56,6 +54,7 @@ create_resource(#{storage_type := StorageType}) -> {dets, [{auto_save, 1000}]}], ok = ekka_mnesia:create_table(?TAB, [ {type, set}, + {rlog_shard, ?RETAINER_SHARD}, {Copies, [node()]}, {record_name, retained}, {attributes, record_info(fields, retained)}, diff --git a/apps/emqx_rule_engine/src/emqx_rule_registry.erl b/apps/emqx_rule_engine/src/emqx_rule_registry.erl index d335a601b..c0bd5de7b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_registry.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_registry.erl @@ -96,11 +96,6 @@ -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 %%------------------------------------------------------------------------------ @@ -112,6 +107,7 @@ mnesia(boot) -> StoreProps = [{ets, [{read_concurrency, true}]}], %% Rule table ok = ekka_mnesia:create_table(?RULE_TAB, [ + {rlog_shard, ?RULE_ENGINE_SHARD}, {disc_copies, [node()]}, {record_name, rule}, {index, [#rule.for]}, @@ -119,6 +115,7 @@ mnesia(boot) -> {storage_properties, StoreProps}]), %% Rule action table ok = ekka_mnesia:create_table(?ACTION_TAB, [ + {rlog_shard, ?RULE_ENGINE_SHARD}, {ram_copies, [node()]}, {record_name, action}, {index, [#action.for, #action.app]}, @@ -126,6 +123,7 @@ mnesia(boot) -> {storage_properties, StoreProps}]), %% Resource table ok = ekka_mnesia:create_table(?RES_TAB, [ + {rlog_shard, ?RULE_ENGINE_SHARD}, {disc_copies, [node()]}, {record_name, resource}, {index, [#resource.type]}, @@ -133,6 +131,7 @@ mnesia(boot) -> {storage_properties, StoreProps}]), %% Resource type table ok = ekka_mnesia:create_table(?RES_TYPE_TAB, [ + {rlog_shard, ?RULE_ENGINE_SHARD}, {ram_copies, [node()]}, {record_name, resource_type}, {index, [#resource_type.provider]}, From ecd7964a5de3fad6cd4a1053707227674a29c071 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Sat, 21 Aug 2021 17:42:03 +0200 Subject: [PATCH 084/306] fix(authn): Use local content shard for the chain table --- apps/emqx_authn/src/emqx_authn.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 571d76cc7..84629be78 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -350,7 +350,7 @@ handle_call({lookup_chain, ID}, _From, State) -> end; handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) -> - UpdateFun = + UpdateFun = fun(#chain{authenticators = Authenticators} = Chain) -> case lists:keymember(Name, 2, Authenticators) of true -> @@ -374,7 +374,7 @@ handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, St reply(Reply, State); handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) -> - UpdateFun = + UpdateFun = fun(#chain{authenticators = Authenticators} = Chain) -> case lists:keytake(AuthenticatorID, 1, Authenticators) of false -> @@ -397,7 +397,7 @@ handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, reply(Reply, State); handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) -> - UpdateFun = + UpdateFun = fun(#chain{authenticators = Authenticators} = Chain) -> case do_move_authenticator(AuthenticatorID, Authenticators, Position) of {ok, NAuthenticators} -> @@ -524,7 +524,7 @@ do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> ok. update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> - UpdateFun = + UpdateFun = fun(#chain{authenticators = Authenticators} = Chain) -> case lists:keytake(AuthenticatorID, 1, Authenticators) of false -> @@ -586,7 +586,7 @@ update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Co end end, update_chain(ChainID, UpdateFun). - + replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). From d4d4ba9ea416aabb5ff29a154f4334065dfa4568 Mon Sep 17 00:00:00 2001 From: Turtle Date: Sat, 21 Aug 2021 14:15:30 +0800 Subject: [PATCH 085/306] refactor(prometheus): refactor prometheus swagger schema --- .../src/emqx_mgmt_api_configs.erl | 14 +++-- .../src/emqx_prometheus_api.erl | 53 ++++++------------- 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 46b679c3d..a8a54a9a9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -179,14 +179,12 @@ gen_schema(Conf) when is_binary(Conf); is_atom(Conf) -> gen_schema(Conf) when is_number(Conf) -> with_default_value(#{type => number}, Conf); gen_schema(Conf) when is_list(Conf) -> - #{type => array, items => case Conf of - [] -> #{}; %% don't know the type - _ -> - case io_lib:printable_unicode_list(Conf) of - true -> gen_schema(unicode:characters_to_binary(Conf)); - false -> gen_schema(hd(Conf)) - end - end}; + case io_lib:printable_unicode_list(Conf) of + true -> + gen_schema(unicode:characters_to_binary(Conf)); + false -> + #{type => array, items => gen_schema(hd(Conf))} + end; gen_schema(Conf) when is_map(Conf) -> #{type => object, properties => maps:map(fun(_K, V) -> gen_schema(V) end, Conf)}; diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index a2ac3b708..8a94a0ffa 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -20,8 +20,7 @@ -include("emqx_prometheus.hrl"). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 +-import(emqx_mgmt_util, [ response_schema/2 , request_body_schema/1 ]). @@ -35,38 +34,21 @@ api_spec() -> {[prometheus_api()], schemas()}. schemas() -> - [#{prometheus => #{ - type => object, - properties => #{ - push_gateway_server => #{ - type => string, - description => <<"prometheus PushGateway Server">>, - example => get_raw(<<"push_gateway_server">>, <<"http://127.0.0.1:9091">>)}, - interval => #{ - type => string, - description => <<"Interval">>, - example => get_raw(<<"interval">>, <<"15s">>)}, - enable => #{ - type => boolean, - description => <<"Prometheus status">>, - example => get_raw(<<"enable">>, false)} - } - }}]. + [#{prometheus => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([prometheus]))}]. prometheus_api() -> Metadata = #{ get => #{ description => <<"Get Prometheus info">>, responses => #{ - <<"200">> => response_schema(prometheus) + <<"200">> => response_schema(<<>>, prometheus) } }, put => #{ description => <<"Update Prometheus">>, 'requestBody' => request_body_schema(prometheus), responses => #{ - <<"200">> => - response_schema(<<"Update Prometheus successfully">>), + <<"200">> =>response_schema(<<>>, prometheus), <<"400">> => response_schema(<<"Bad Request">>, #{ type => object, @@ -106,15 +88,21 @@ prometheus_api() -> % {"/prometheus/stats", Metadata, stats}. prometheus(get, _Request) -> - Response = emqx:get_raw_config([<<"prometheus">>], #{}), - {200, Response}; + {200, emqx:get_raw_config([<<"prometheus">>], #{})}; prometheus(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), - Enable = maps:get(<<"enable">>, Params), - {ok, _} = emqx:update_config([prometheus], Params), - enable_prometheus(Enable). + {ok, Config} = emqx:update_config([prometheus], Params), + case maps:get(<<"enable">>, Params) of + true -> + _ = emqx_prometheus_sup:stop_child(?APP), + emqx_prometheus_sup:start_child(?APP, maps:get(config, Config)); + false -> + _ = emqx_prometheus_sup:stop_child(?APP), + ok + end, + {200, emqx:get_raw_config([<<"prometheus">>], #{})}. % stats(_Bindings, Params) -> % Type = proplists:get_value(<<"format_type">>, Params, <<"json">>), @@ -125,14 +113,3 @@ prometheus(put, Request) -> % <<"prometheus">> -> % {ok, #{<<"content-type">> => <<"text/plain">>}, Data} % end. - -enable_prometheus(true) -> - ok = emqx_prometheus_sup:stop_child(?APP), - emqx_prometheus_sup:start_child(?APP, emqx:get_config([prometheus], #{})), - {200}; -enable_prometheus(false) -> - _ = emqx_prometheus_sup:stop_child(?APP), - {200}. - -get_raw(Key, Def) -> - emqx:get_raw_config([<<"prometheus">>] ++ [Key], Def). From 31f588671d750fd8c315201ae5f3015110511601 Mon Sep 17 00:00:00 2001 From: Turtle Date: Sat, 21 Aug 2021 14:12:26 +0800 Subject: [PATCH 086/306] refactor(statsd): refactor statsd swagger schema --- apps/emqx_statsd/src/emqx_statsd_api.erl | 60 ++++++--------------- apps/emqx_statsd/src/emqx_statsd_schema.erl | 2 +- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index 3325d8c71..0efd2d479 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -20,10 +20,7 @@ -include("emqx_statsd.hrl"). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , request_body_schema/1 - ]). +-import(emqx_mgmt_util, [response_schema/2, request_body_schema/1]). -export([api_spec/0]). @@ -34,42 +31,22 @@ api_spec() -> {statsd_api(), schemas()}. schemas() -> - [#{statsd => #{ - type => object, - properties => #{ - server => #{ - type => string, - description => <<"Statsd Server">>, - example => get_raw(<<"server">>, <<"127.0.0.1:8125">>)}, - enable => #{ - type => boolean, - description => <<"Statsd status">>, - example => get_raw(<<"enable">>, false)}, - sample_time_interval => #{ - type => string, - description => <<"Sample Time Interval">>, - example => get_raw(<<"sample_time_interval">>, <<"10s">>)}, - flush_time_interval => #{ - type => string, - description => <<"Flush Time Interval">>, - example => get_raw(<<"flush_time_interval">>, <<"10s">>)} - } - }}]. + [#{statsd => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([statsd]))}]. statsd_api() -> Metadata = #{ get => #{ description => <<"Get statsd info">>, responses => #{ - <<"200">> => response_schema(<<"statsd">>) + <<"200">> => response_schema(<<>>, statsd) } }, put => #{ description => <<"Update Statsd">>, - 'requestBody' => request_body_schema(<<"statsd">>), + 'requestBody' => request_body_schema(statsd), responses => #{ <<"200">> => - response_schema(<<"Update Statsd successfully">>), + response_schema(<<>>, statsd), <<"400">> => response_schema(<<"Bad Request">>, #{ type => object, @@ -84,23 +61,18 @@ statsd_api() -> [{"/statsd", Metadata, statsd}]. statsd(get, _Request) -> - Response = emqx:get_raw_config([<<"statsd">>], #{}), - {200, Response}; + {200, emqx:get_raw_config([<<"statsd">>], #{})}; statsd(put, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), - Enable = maps:get(<<"enable">>, Params), - {ok, _} = emqx:update_config([statsd], Params), - enable_statsd(Enable). - -enable_statsd(true) -> - ok = emqx_statsd_sup:stop_child(?APP), - emqx_statsd_sup:start_child(?APP, emqx:get_config([statsd], #{})), - {200}; -enable_statsd(false) -> - _ = emqx_statsd_sup:stop_child(?APP), - {200}. - -get_raw(Key, Def) -> - emqx:get_raw_config([<<"statsd">>]++ [Key], Def). + {ok, Config} = emqx:update_config([statsd], Params), + case maps:get(<<"enable">>, Params) of + true -> + _ = emqx_statsd_sup:stop_child(?APP), + emqx_statsd_sup:start_child(?APP, maps:get(config, Config)); + false -> + _ = emqx_statsd_sup:stop_child(?APP), + ok + end, + {200, emqx:get_raw_config([<<"statsd">>], #{})}. diff --git a/apps/emqx_statsd/src/emqx_statsd_schema.erl b/apps/emqx_statsd/src/emqx_statsd_schema.erl index 3af8a112c..0e96c37d0 100644 --- a/apps/emqx_statsd/src/emqx_statsd_schema.erl +++ b/apps/emqx_statsd/src/emqx_statsd_schema.erl @@ -21,8 +21,8 @@ fields("statsd") -> ]. server(type) -> emqx_schema:ip_port(); -server(default) -> "127.0.0.1:8125"; server(nullable) -> false; +server(default) -> "127.0.0.1:8125"; server(_) -> undefined. duration_ms(type) -> emqx_schema:duration_ms(); From 25bae9e3971888915df16e4e4b295dfdfd2d34e6 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 23 Aug 2021 10:31:24 +0800 Subject: [PATCH 087/306] fix: clients subscribe api --- apps/emqx_management/src/emqx_mgmt_api_clients.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 1bc01e6d6..4c7479eea 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -642,7 +642,6 @@ format_authz_cache({{PubSub, Topic}, {AuthzResult, 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}; From 985dce786c112335e79bf3c4c5bfd808663be75b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 20 Aug 2021 18:46:47 +0800 Subject: [PATCH 088/306] refactor(config): update the conf struct for logger --- apps/emqx_machine/etc/emqx_machine.conf | 511 +++++++++++------- apps/emqx_machine/src/emqx_machine_schema.erl | 161 +++--- 2 files changed, 414 insertions(+), 258 deletions(-) diff --git a/apps/emqx_machine/etc/emqx_machine.conf b/apps/emqx_machine/etc/emqx_machine.conf index d80ac6ddb..d21a19bc2 100644 --- a/apps/emqx_machine/etc/emqx_machine.conf +++ b/apps/emqx_machine/etc/emqx_machine.conf @@ -312,45 +312,183 @@ cluster { ## 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.primary_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 + console_handler { + ## Log to single line + ## @doc log.console_handler..enable + ## ValueType: Boolean + ## Default: false + 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: warning - console_handler.level = warning + ## 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: warning + level = warning + + ## Timezone offset to display in logs + ## + ## @doc log.console_handler..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.console_handler..chars_limit + ## ValueType: unlimited | Integer + ## Range: [0, +Inf) + ## Default: unlimited + chars_limit = unlimited + + ## Maximum depth for Erlang term log formatting + ## and Erlang process message queue inspection. + ## + ## @doc log.console_handler..max_depth + ## ValueType: unlimited | Integer + ## Default: 100 + max_depth = 100 + + ## Log formatter + ## @doc log.console_handler..formatter + ## ValueType: text | json + ## Default: text + formatter = text + + ## Log to single line + ## @doc log.console_handler..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.console_handler..sync_mode_qlen + ## ValueType: Integer + ## Range: [0, ${log.console_handler..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.console_handler..drop_mode_qlen + ## ValueType: Integer + ## Range: [${log.console_handler..sync_mode_qlen}, ${log.console_handler..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.console_handler..flush_qlen + ## ValueType: Integer + ## Range: [${log.console_handler..drop_mode_qlen}, infinity) + ## 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.console_handler..overload_kill.enable + ## ValueType: Boolean + ## Default: true + overload_kill.enable = 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.console_handler..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.console_handler..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.console_handler..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.console_handler..burst_limit.enable + ## ValueType: Boolean + ## Default: 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.console_handler..burst_limit.max_count + ## ValueType: Integer + ## Default: 10000 + burst_limit.max_count = 10000 + + ## See the previous description of burst_limit_max_count. + ## + ## @doc log.console_handler..burst_limit.window_time + ## ValueType: duration + ## Default: 1s + burst_limit.window_time = 1s + } ##---------------------------------------------------------------- ## The file log handlers send log messages to files ##---------------------------------------------------------------- ## file_handlers. - file_handlers.emqx_log { + file_handlers.default { ## The log level filter of this handler ## All the log messages with levels lower than this level will ## be dropped. @@ -402,168 +540,159 @@ log { ## ValueType: Size | infinity ## Default: 10MB max_size = 10MB + + ## Timezone offset to display in logs + ## + ## @doc log.file_handlers..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.file_handlers..chars_limit + ## ValueType: unlimited | Integer + ## Range: [0, +Inf) + ## Default: unlimited + chars_limit = unlimited + + ## Maximum depth for Erlang term log formatting + ## and Erlang process message queue inspection. + ## + ## @doc log.file_handlers..max_depth + ## ValueType: unlimited | Integer + ## Default: 100 + max_depth = 100 + + ## Log formatter + ## @doc log.file_handlers..formatter + ## ValueType: text | json + ## Default: text + formatter = text + + ## Log to single line + ## @doc log.file_handlers..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.file_handlers..sync_mode_qlen + ## ValueType: Integer + ## Range: [0, ${log.file_handlers..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.file_handlers..drop_mode_qlen + ## ValueType: Integer + ## Range: [${log.file_handlers..sync_mode_qlen}, ${log.file_handlers..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.file_handlers..flush_qlen + ## ValueType: Integer + ## Range: [${log.file_handlers..drop_mode_qlen}, infinity) + ## 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.file_handlers..overload_kill.enable + ## ValueType: Boolean + ## Default: true + overload_kill.enable = 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.file_handlers..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.file_handlers..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.file_handlers..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.file_handlers..burst_limit.enable + ## ValueType: Boolean + ## Default: 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.file_handlers..burst_limit.max_count + ## ValueType: Integer + ## Default: 10000 + burst_limit.max_count = 10000 + + ## See the previous description of burst_limit_max_count. + ## + ## @doc log.file_handlers..burst_limit.window_time + ## ValueType: duration + ## Default: 1s + burst_limit.window_time = 1s } - - ## file_handlers. - ## - ## You could also create multiple file handlers for different - ## log level for example: - file_handlers.emqx_error_log { - level = error - 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 - ## ValueType: unlimited | Integer - ## Range: [0, +Inf) - ## Default: unlimited - chars_limit = unlimited - - ## Maximum depth for Erlang term log formatting - ## and Erlang process message queue inspection. - ## - ## @doc log.max_depth - ## ValueType: unlimited | Integer - ## Default: 80 - max_depth = 80 - - ## 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}, infinity) - ## 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.enable - ## ValueType: Boolean - ## Default: true - overload_kill.enable = 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.enable - ## ValueType: Boolean - ## Default: 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 - ## 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 } ##================================================================== diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index cef5e525f..23b43d5aa 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -159,42 +159,25 @@ fields("rpc") -> ]; fields("log") -> - [ {"primary_level", t(log_level(), undefined, warning)} - , {"console_handler", ref("console_handler")} + [ {"console_handler", ref("console_handler")} , {"file_handlers", ref("file_handlers")} - , {"time_offset", t(string(), undefined, "system")} - , {"chars_limit", #{type => hoconsc:union([unlimited, range(1, inf)]), - default => unlimited - }} - , {"supervisor_reports", t(union([error, progress]), undefined, error)} - , {"max_depth", t(union([unlimited, integer()]), - "kernel.error_logger_format_depth", 80)} - , {"formatter", t(union([text, json]), undefined, text)} - , {"single_line", t(boolean(), undefined, true)} - , {"sync_mode_qlen", t(integer(), undefined, 100)} - , {"drop_mode_qlen", t(integer(), undefined, 3000)} - , {"flush_qlen", t(integer(), undefined, 8000)} - , {"overload_kill", ref("log_overload_kill")} - , {"burst_limit", ref("log_burst_limit")} , {"error_logger", t(atom(), "kernel.error_logger", silent)} ]; fields("console_handler") -> [ {"enable", t(boolean(), undefined, false)} - , {"level", t(log_level(), undefined, warning)} - ]; + ] ++ log_handler_common_confs(); fields("file_handlers") -> [ {"$name", ref("log_file_handler")} ]; fields("log_file_handler") -> - [ {"level", t(log_level(), undefined, warning)} - , {"file", t(file(), undefined, undefined)} + [ {"file", t(file(), undefined, undefined)} , {"rotation", ref("log_rotation")} , {"max_size", #{type => union([infinity, emqx_schema:bytesize()]), default => "10MB"}} - ]; + ] ++ log_handler_common_confs(); fields("log_rotation") -> [ {"enable", t(boolean(), undefined, true)} @@ -213,6 +196,7 @@ fields("log_burst_limit") -> , {"max_count", t(integer(), undefined, 10000)} , {"window_time", t(emqx_schema:duration(), undefined, "1s")} ]; + fields(Name) -> find_field(Name, ?MERGED_CONFIGS). @@ -259,49 +243,31 @@ 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_level(Conf) -> + %% TODO: use the lowest level of all the handlers + io:format(standard_error, "primary level", []), + conf_get("log.console_handler.level", Conf). tr_logger(Conf) -> - CharsLimit = case conf_get("log.chars_limit", Conf) of - unlimited -> unlimited; - V when V > 0 -> V - end, - SingleLine = conf_get("log.single_line", Conf), - FmtName = conf_get("log.formatter", Conf), - Formatter = formatter(FmtName, CharsLimit, SingleLine), - 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 ConsoleHandler = case conf_get("log.console_handler.enable", Conf) of true -> + ConsoleConf = conf_get("log.console_handler", Conf), [{handler, console, logger_std_h, #{ level => conf_get("log.console_handler.level", Conf), - config => BasicConf#{type => standard_io}, - formatter => Formatter, - filters => Filters + config => (log_handler_conf(ConsoleConf)) #{type => standard_io}, + formatter => log_formatter(ConsoleConf), + filters => log_filter(ConsoleConf) }}]; false -> [] end, %% For the file logger FileHandlers = - [{handler, binary_to_atom(HandlerName, latin1), logger_disk_log_h, #{ + [begin + {handler, binary_to_atom(HandlerName, latin1), logger_disk_log_h, #{ level => conf_get("level", SubConf), - config => BasicConf#{ + config => (log_handler_conf(SubConf)) #{ type => case conf_get("rotation.enable", SubConf) of true -> wrap; _ -> halt @@ -310,36 +276,97 @@ tr_logger(Conf) -> max_no_files => conf_get("rotation.count", SubConf), max_no_bytes => conf_get("max_size", SubConf) }, - formatter => Formatter, - filters => Filters, + formatter => log_formatter(SubConf), + filters => log_filter(SubConf), filesync_repeat_interval => no_repeat }} - || {HandlerName, SubConf} <- maps:to_list(conf_get("log.file_handlers", Conf, #{}))], + end || {HandlerName, SubConf} <- maps:to_list(conf_get("log.file_handlers", Conf, #{}))], [{handler, default, undefined}] ++ ConsoleHandler ++ FileHandlers. +log_handler_common_confs() -> + [ {"level", t(log_level(), undefined, warning)} + , {"time_offset", t(string(), undefined, "system")} + , {"chars_limit", #{type => hoconsc:union([unlimited, range(1, inf)]), + default => unlimited + }} + , {"formatter", t(union([text, json]), undefined, text)} + , {"single_line", t(boolean(), undefined, true)} + , {"sync_mode_qlen", t(integer(), undefined, 100)} + , {"drop_mode_qlen", t(integer(), undefined, 3000)} + , {"flush_qlen", t(integer(), undefined, 8000)} + , {"overload_kill", ref("log_overload_kill")} + , {"burst_limit", ref("log_burst_limit")} + , {"supervisor_reports", t(union([error, progress]), undefined, error)} + , {"max_depth", t(union([unlimited, integer()]), undefined, 100)} + ]. + +log_handler_conf(Conf) -> + SycModeQlen = conf_get("sync_mode_qlen", Conf), + DropModeQlen = conf_get("drop_mode_qlen", Conf), + FlushQlen = conf_get("flush_qlen", Conf), + Overkill = conf_get("overload_kill", Conf), + BurstLimit = conf_get("burst_limit", Conf), + #{ + sync_mode_qlen => SycModeQlen, + drop_mode_qlen => DropModeQlen, + flush_qlen => FlushQlen, + overload_kill_enable => conf_get("enable", Overkill), + overload_kill_qlen => conf_get("qlen", Overkill), + overload_kill_mem_size => conf_get("mem_size", Overkill), + overload_kill_restart_after => conf_get("restart_after", Overkill), + burst_limit_enable => conf_get("enable", BurstLimit), + burst_limit_max_count => conf_get("max_count", BurstLimit), + burst_limit_window_time => conf_get("window_time", BurstLimit) + }. + +log_formatter(Conf) -> + io:format(standard_error, "log_formatter: ~p~n", [Conf]), + CharsLimit = case conf_get("chars_limit", Conf) of + unlimited -> unlimited; + V when V > 0 -> V + end, + TimeOffSet = case conf_get("time_offset", Conf) of + "system" -> ""; + "utc" -> 0; + OffSetStr -> OffSetStr + end, + SingleLine = conf_get("single_line", Conf), + Depth = conf_get("max_depth", Conf), + do_formatter(conf_get("formatter", Conf), CharsLimit, SingleLine, TimeOffSet, Depth). + %% helpers -formatter(json, CharsLimit, SingleLine) -> +do_formatter(json, CharsLimit, SingleLine, TimeOffSet, Depth) -> {emqx_logger_jsonfmt, #{chars_limit => CharsLimit, - single_line => SingleLine + single_line => SingleLine, + time_offset => TimeOffSet, + depth => Depth }}; -formatter(text, CharsLimit, SingleLine) -> +do_formatter(text, CharsLimit, SingleLine, TimeOffSet, Depth) -> {emqx_logger_textfmt, #{template => - [time," [",level,"] ", - {clientid, - [{peername, - [clientid,"@",peername," "], - [clientid, " "]}], - [{peername, - [peername," "], - []}]}, - msg,"\n"], - chars_limit => CharsLimit, - single_line => SingleLine + [time," [",level,"] ", + {clientid, + [{peername, + [clientid,"@",peername," "], + [clientid, " "]}], + [{peername, + [peername," "], + []}]}, + msg,"\n"], + chars_limit => CharsLimit, + single_line => SingleLine, + time_offset => TimeOffSet, + depth => Depth }}. +log_filter(Conf) -> + case conf_get("supervisor_reports", Conf) of + error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; + progress -> [] + end. + %% utils -spec(conf_get(string() | [string()], hocon:config()) -> term()). conf_get(Key, Conf) -> From 8ac8b9785b6410be8921914513f4e5495814837a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 20 Aug 2021 18:46:47 +0800 Subject: [PATCH 089/306] refactor(config): update the conf struct for logger --- apps/emqx_machine/src/emqx_machine_schema.erl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 23b43d5aa..91e1a58eb 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -244,8 +244,7 @@ tr_cluster__discovery(Conf) -> {Strategy, filter(options(Strategy, Conf))}. tr_logger_level(Conf) -> - %% TODO: use the lowest level of all the handlers - io:format(standard_error, "primary level", []), + io:format(standard_error, "TODO: use the lowest level of all the handlers as primary level~n", []), conf_get("log.console_handler.level", Conf). tr_logger(Conf) -> @@ -321,7 +320,6 @@ log_handler_conf(Conf) -> }. log_formatter(Conf) -> - io:format(standard_error, "log_formatter: ~p~n", [Conf]), CharsLimit = case conf_get("chars_limit", Conf) of unlimited -> unlimited; V when V > 0 -> V From de92cd411b3e5e8c9298edc6ebc98bc5b76abdab Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sat, 21 Aug 2021 12:16:45 +0800 Subject: [PATCH 090/306] feat(log): get the least severe level of handlers as the primary level --- apps/emqx_machine/src/emqx_machine_schema.erl | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 91e1a58eb..0d1e9b176 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -244,8 +244,14 @@ tr_cluster__discovery(Conf) -> {Strategy, filter(options(Strategy, Conf))}. tr_logger_level(Conf) -> - io:format(standard_error, "TODO: use the lowest level of all the handlers as primary level~n", []), - conf_get("log.console_handler.level", Conf). + ConsoleLevel = conf_get("log.console_handler.level", Conf, undefined), + FileLevels = [conf_get("level", SubConf) || {_, SubConf} + <- maps:to_list(conf_get("log.file_handlers", Conf, #{}))], + case FileLevels ++ [ConsoleLevel || ConsoleLevel =/= undefined] of + [] -> warning; %% warning is the default level we should use + Levels -> + least_severe_log_level(Levels) + end. tr_logger(Conf) -> %% For the default logger that outputs to console @@ -365,6 +371,17 @@ log_filter(Conf) -> progress -> [] end. +least_severe_log_level(Levels) -> + hd(sort_log_levels(Levels)). + +sort_log_levels(Levels) -> + lists:sort(fun(A, B) -> + case logger:compare_levels(A, B) of + R when R == lt; R == eq -> true; + gt -> false + end + end, Levels). + %% utils -spec(conf_get(string() | [string()], hocon:config()) -> term()). conf_get(Key, Conf) -> From 5177ba02d6cf9fb0af38a1db981279005c94e5af Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sat, 21 Aug 2021 22:51:18 +0800 Subject: [PATCH 091/306] fix(config): update ENVs for logger --- .ci/build_packages/tests.sh | 3 +-- .ci/docker-compose-file/conf.cluster.env | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index b01d6ca27..c01d07d0a 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -122,8 +122,7 @@ run_test(){ export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 export EMQX_MQTT__MAX_TOPIC_ALIAS=10 export EMQX_LOG__CONSOLE_HANDLER__LEVEL=debug -export EMQX_LOG__FILE_HANDLERS__EMQX_LOG__LEVEL=debug -export EMQX_LOG__PRIMARY_LEVEL=debug +export EMQX_LOG__FILE_HANDLERS__DEFAULT__LEVEL=debug EOF ## for ARM, due to CI env issue, skip start of quic listener for the moment [[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" < Date: Mon, 23 Aug 2021 14:10:26 +0200 Subject: [PATCH 092/306] chore(ekka): Bump version to 0.10.8 --- 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 34b12e233..19c91d6ac 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.2"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.7"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} diff --git a/rebar.config b/rebar.config index 13efa22a7..65384c9ab 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,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.2"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.7"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.1.7"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} From 5c93c355591b8d5489b96bc1a05087cbc0156c64 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 00:32:51 +0800 Subject: [PATCH 093/306] refactor(schema-utils): refactor mgmt swagger schema utils --- .../emqx_dashboard/src/emqx_dashboard_api.erl | 167 +++++--------- .../src/emqx_dashboard_monitor_api.erl | 12 +- .../src/emqx_mgmt_api_alarms.erl | 44 ++-- .../src/emqx_mgmt_api_apps.erl | 85 +++---- .../src/emqx_mgmt_api_clients.erl | 32 +-- .../src/emqx_mgmt_api_configs.erl | 31 +-- .../src/emqx_mgmt_api_listeners.erl | 127 +++++------ .../src/emqx_mgmt_api_metrics.erl | 9 +- .../src/emqx_mgmt_api_nodes.erl | 143 +++++------- .../src/emqx_mgmt_api_publish.erl | 99 ++------- .../src/emqx_mgmt_api_routes.erl | 52 ++--- .../src/emqx_mgmt_api_status.erl | 5 +- .../src/emqx_mgmt_api_subscriptions.erl | 128 +++++------ apps/emqx_management/src/emqx_mgmt_http.erl | 1 + apps/emqx_management/src/emqx_mgmt_util.erl | 208 +++++++++++------- apps/emqx_modules/src/emqx_delayed_api.erl | 138 +++++------- .../src/emqx_event_message_api.erl | 59 +---- apps/emqx_modules/src/emqx_rewrite_api.erl | 47 ++-- apps/emqx_modules/src/emqx_telemetry_api.erl | 112 +++------- .../src/emqx_prometheus_api.erl | 22 +- apps/emqx_retainer/src/emqx_retainer.erl | 2 +- apps/emqx_retainer/src/emqx_retainer_api.erl | 176 +++++++-------- apps/emqx_statsd/src/emqx_statsd_api.erl | 21 +- 23 files changed, 689 insertions(+), 1031 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 422d246f4..a7d0adade 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -30,10 +30,12 @@ -include("emqx_dashboard.hrl"). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , request_body_schema/1 - , response_array_schema/2 +-import(emqx_mgmt_util, [ schema/1 + , object_schema/1 + , object_schema/2 + , object_array_schema/1 + , bad_request/0 + , properties/1 ]). -export([api_spec/0]). @@ -58,95 +60,59 @@ api_spec() -> []}. login_api() -> - AuthSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - password => #{ - type => string, - description => <<"Password">>}}}, - TokenSchema = #{ - type => object, - properties => #{ - token => #{ - type => string, - description => <<"JWT Token">>}, - license => #{ - type => object, - properties => #{ - edition => #{ - type => string, - enum => [community, enterprise]}}}, - version => #{ - type => string}}}, + AuthProps = properties([{username, string, <<"Username">>}, + {password, string, <<"Password">>}]), + TokenProps = properties([{token, string, <<"JWT Token">>}, + {license, object, [{edition, string, <<"License">>, [community, enterprise]}]}, + {version, string}]), Metadata = #{ post => #{ + tags => [dashboard], description => <<"Dashboard Auth">>, - 'requestBody' => request_body_schema(AuthSchema), + 'requestBody' => object_schema(AuthProps), responses => #{ <<"200">> => - response_schema(<<"Dashboard Auth successfully">>, TokenSchema), + object_schema(TokenProps, <<"Dashboard Auth successfully">>), <<"401">> => unauthorized_request() }, security => [] } }, {"/login", Metadata, login}. + logout_api() -> - AuthSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}}}, + LogoutProps = properties([{username, string, <<"Username">>}]), Metadata = #{ post => #{ + tags => [dashboard], description => <<"Dashboard Auth">>, - 'requestBody' => request_body_schema(AuthSchema), + 'requestBody' => object_schema(LogoutProps), responses => #{ - <<"200">> => - response_schema(<<"Dashboard Auth successfully">>)} + <<"200">> => schema(<<"Dashboard Auth successfully">>) + } } }, {"/logout", Metadata, logout}. users_api() -> - ShowSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - tag => #{ - type => string, - description => <<"Tag">>}}}, - CreateSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - password => #{ - type => string, - description => <<"Password">>}, - tag => #{ - type => string, - description => <<"Tag">>}}}, + BaseProps = properties([{username, string, <<"Username">>}, + {password, string, <<"Password">>}, + {tag, string, <<"Tag">>}]), Metadata = #{ get => #{ + tags => [dashboard], description => <<"Get dashboard users">>, responses => #{ - <<"200">> => response_array_schema(<<"">>, ShowSchema) + <<"200">> => object_array_schema(maps:without([password], BaseProps)) } }, post => #{ + tags => [dashboard], description => <<"Create dashboard users">>, - 'requestBody' => request_body_schema(CreateSchema), + 'requestBody' => object_schema(BaseProps), responses => #{ - <<"200">> => response_schema(<<"Create Users successfully">>), + <<"200">> => schema(<<"Create Users successfully">>), <<"400">> => bad_request() } } @@ -156,26 +122,21 @@ users_api() -> user_api() -> Metadata = #{ delete => #{ + tags => [dashboard], description => <<"Delete dashboard users">>, - parameters => [path_param_username()], + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Delete User successfully">>), + <<"200">> => schema(<<"Delete User successfully">>), <<"400">> => bad_request() } }, put => #{ + tags => [dashboard], description => <<"Update dashboard users">>, - parameters => [path_param_username()], - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - tag => #{ - type => string - } - } - }), + parameters => parameters(), + 'requestBody' => object_schema(properties([{tag, string, <<"Tag">>}])), responses => #{ - <<"200">> => response_schema(<<"Update Users successfully">>), + <<"200">> => schema(<<"Update Users successfully">>), <<"400">> => bad_request() } } @@ -185,36 +146,18 @@ user_api() -> change_pwd_api() -> Metadata = #{ put => #{ + tags => [dashboard], description => <<"Update dashboard users password">>, - parameters => [path_param_username()], - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - old_pwd => #{ - type => string - }, - new_pwd => #{ - type => string - } - } - }), + parameters => parameters(), + 'requestBody' => object_schema(properties([old_pwd, new_pwd])), responses => #{ - <<"200">> => response_schema(<<"Update Users password successfully">>), + <<"200">> => schema(<<"Update Users password successfully">>), <<"400">> => bad_request() } } }, {"/users/:username/change_pwd", Metadata, change_pwd}. -path_param_username() -> - #{ - name => username, - in => path, - required => true, - schema => #{type => string}, - example => <<"admin">> - }. - login(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), @@ -292,21 +235,19 @@ change_pwd(put, Request) -> row(#mqtt_admin{username = Username, tags = Tag}) -> #{username => Username, tag => Tag}. -bad_request() -> - response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }). +parameters() -> + [#{ + name => username, + in => path, + required => true, + schema => #{type => string}, + example => <<"admin">> + }]. + unauthorized_request() -> - response_schema(<<"Unauthorized">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string, enum => ['PASSWORD_ERROR', 'USERNAME_ERROR']} - } - }). + object_schema( + properties([{message, string}, + {code, string, <<"Resp Code">>, ['PASSWORD_ERROR','USERNAME_ERROR']} + ]), + <<"Unauthorized">> + ). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 58b95f093..1193dfad1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -8,6 +8,7 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [schema/2]). -export([api_spec/0]). -export([ monitor/2 @@ -47,7 +48,7 @@ monitor_api() -> } ], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data">>, counters_schema())}}}, + <<"200">> => schema(counters_schema(), <<"Monitor count data">>)}}}, {"/monitor", Metadata, monitor}. monitor_nodes_api() -> @@ -56,7 +57,7 @@ monitor_nodes_api() -> description => <<"List monitor data">>, parameters => [path_param_node()], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data in node">>, counters_schema())}}}, + <<"200">> => schema(counters_schema(), <<"Monitor count data in node">>)}}}, {"/monitor/nodes/:node", Metadata, monitor_nodes}. monitor_nodes_counters_api() -> @@ -68,7 +69,7 @@ monitor_nodes_counters_api() -> path_param_counter() ], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor single count data in node">>, counter_schema())}}}, + <<"200">> => schema(counter_schema(), <<"Monitor single count data in node">>)}}}, {"/monitor/nodes/:node/counters/:counter", Metadata, monitor_nodes_counters}. monitor_counters_api() -> @@ -80,15 +81,14 @@ monitor_counters_api() -> ], responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Monitor single count data">>, counter_schema())}}}, + schema(counter_schema(), <<"Monitor single count data">>)}}}, {"/monitor/counters/:counter", Metadata, counters}. monitor_current_api() -> Metadata = #{ get => #{ description => <<"Current monitor data">>, responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Current monitor data">>, - current_counters_schema())}}}, + <<"200">> => schema(current_counters_schema(), <<"Current monitor data">>)}}}, {"/monitor/current", Metadata, current_counters}. path_param_node() -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 36a0f3a5b..8f26f9075 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -28,32 +28,22 @@ -define(ACTIVATED_ALARM, emqx_activated_alarm). -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). -api_spec() -> - {[alarms_api()], [alarm_schema()]}. +-import(emqx_mgmt_util, [ object_array_schema/2 + , schema/1 + , properties/1 + ]). -alarm_schema() -> - #{ - alarm => #{ - type => object, - properties => #{ - node => #{ - type => string, - description => <<"Alarm in node">>}, - name => #{ - type => string, - description => <<"Alarm name">>}, - message => #{ - type => string, - description => <<"Alarm readable information">>}, - details => #{ - type => object, - description => <<"Alarm detail">>}, - duration => #{ - type => integer, - description => <<"Alarms duration time; UNIX time stamp">>} - } - } - }. +api_spec() -> + {[alarms_api()], []}. + +properties() -> + properties([ + {node, string, <<"Alarm in node">>}, + {name, string, <<"Alarm name">>}, + {message, string, <<"Alarm readable information">>}, + {details, object}, + {duration, integer, <<"Alarms duration time; UNIX time stamp">>} + ]). alarms_api() -> Metadata = #{ @@ -68,12 +58,12 @@ alarms_api() -> }], responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List all alarms">>, alarm)}}, + object_array_schema(properties(), <<"List all alarms">>)}}, delete => #{ description => <<"Remove all deactivated alarms">>, responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Remove all deactivated alarms ok">>)}}}, + schema(<<"Remove all deactivated alarms ok">>)}}}, {"/alarms", Metadata, alarms}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_apps.erl b/apps/emqx_management/src/emqx_mgmt_api_apps.erl index 2a5f330c4..71dabf4c6 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_apps.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_apps.erl @@ -18,8 +18,17 @@ -behaviour(minirest_api). --export([api_spec/0]). +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , object_array_schema/2 + , error_schema/1 + , error_schema/2 + , properties/1 + ]). +-export([api_spec/0]). -export([ apps/2 , app/2]). @@ -30,48 +39,22 @@ api_spec() -> { [apps_api(), app_api()], - [app_schema(), app_secret_schema()] + [] }. -app_schema() -> - #{app => #{ - type => object, - properties => app_properties()}}. - -app_properties() -> - #{ - app_id => #{ - type => string, - description => <<"App ID">>}, - secret => #{ - type => string, - description => <<"App Secret">>}, - name => #{ - type => string, - description => <<"Dsiplay name">>}, - desc => #{ - type => string, - description => <<"App description">>}, - status => #{ - type => boolean, - description => <<"Enable or disable">>}, - expired => #{ - type => integer, - description => <<"Expired time">>} - }. - -app_secret_schema() -> - #{app_secret => #{ - type => object, - properties => #{ - secret => #{type => string}}}}. +properties() -> + properties([ + {app_id, string, <<"App ID">>}, + {secret, string, <<"App Secret">>}, + {name, string, <<"Dsiplay name">>}, + {desc, string, <<"App description">>}, + {status, boolean, <<"Enable or disable">>}, + {expired, integer, <<"Expired time">>} + ]). %% not export schema app_without_secret_schema() -> - #{ - type => object, - properties => maps:without([secret], app_properties()) - }. + maps:without([secret], properties()). apps_api() -> Metadata = #{ @@ -79,16 +62,20 @@ apps_api() -> description => <<"List EMQ X apps">>, responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"All apps">>, - app_without_secret_schema())}}, + object_array_schema(app_without_secret_schema(), <<"All apps">>) + } + }, post => #{ description => <<"EMQ X create apps">>, - 'requestBody' => emqx_mgmt_util:request_body_schema(<<"app">>), + 'requestBody' => schema(app), responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Create apps">>, app_secret), + schema(app_secret, <<"Create apps">>), <<"400">> => - emqx_mgmt_util:response_error_schema(<<"App ID already exist">>, [?BAD_APP_ID])}}}, + error_schema(<<"App ID already exist">>, [?BAD_APP_ID]) + } + } + }, {"/apps", Metadata, apps}. app_api() -> @@ -102,9 +89,9 @@ app_api() -> schema => #{type => string}}], responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"App id not found">>), + error_schema(<<"App id not found">>), <<"200">> => - emqx_mgmt_util:response_schema(<<"Get App">>, app_without_secret_schema())}}, + object_schema(app_without_secret_schema(), <<"Get App">>)}}, delete => #{ description => <<"EMQ X apps">>, parameters => [#{ @@ -114,7 +101,7 @@ app_api() -> schema => #{type => string} }], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Remove app ok">>)}}, + <<"200">> => schema(<<"Remove app ok">>)}}, put => #{ description => <<"EMQ X update apps">>, parameters => [#{ @@ -123,12 +110,12 @@ app_api() -> required => true, schema => #{type => string} }], - 'requestBody' => emqx_mgmt_util:request_body_schema(app_without_secret_schema()), + 'requestBody' => object_schema(app_without_secret_schema()), responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"App id not found">>, [?BAD_APP_ID]), + error_schema(<<"App id not found">>, [?BAD_APP_ID]), <<"200">> => - emqx_mgmt_util:response_schema(<<"Update ok">>, app_without_secret_schema())}}}, + object_schema(app_without_secret_schema(), <<"Update ok">>)}}}, {"/apps/:app_id", Metadata, app}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 1bc01e6d6..1e8444b91 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -333,7 +333,7 @@ clients_api() -> } ], responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<"List clients 200 OK">>, client)}}}, + <<"200">> => emqx_mgmt_util:array_schema(client, <<"List clients 200 OK">>)}}}, {"/clients", Metadata, clients}. client_api() -> @@ -347,8 +347,8 @@ client_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"List clients 200 OK">>, client)}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(client, <<"List clients 200 OK">>)}}, delete => #{ description => <<"Kick out client by client ID">>, parameters => [#{ @@ -358,8 +358,8 @@ client_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"List clients 200 OK">>, client)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(client, <<"List clients 200 OK">>)}}}, {"/clients/:clientid", Metadata, client}. clients_authz_cache_api() -> @@ -373,8 +373,8 @@ clients_authz_cache_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get client authz cache">>, <<"authz_cache">>)}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(authz_cache, <<"Get client authz cache">>)}}, delete => #{ description => <<"Clean client authz cache">>, parameters => [#{ @@ -384,8 +384,8 @@ clients_authz_cache_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Delete clients 200 OK">>)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(<<"Delete clients 200 OK">>)}}}, {"/clients/:clientid/authz_cache", Metadata, authz_cache}. clients_subscriptions_api() -> @@ -400,7 +400,7 @@ clients_subscriptions_api() -> }], responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"Get client subscriptions">>, subscription)}} + emqx_mgmt_util:array_schema(subscription, <<"Get client subscriptions">>)}} }, {"/clients/:clientid/subscriptions", Metadata, subscriptions}. @@ -416,15 +416,15 @@ unsubscribe_api() -> required => true } ], - 'requestBody' => emqx_mgmt_util:request_body_schema(#{ + 'requestBody' => emqx_mgmt_util:schema(#{ type => object, properties => #{ topic => #{ type => string, description => <<"Topic">>}}}), responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Unsubscribe ok">>)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(<<"Unsubscribe ok">>)}}}, {"/clients/:clientid/unsubscribe", Metadata, unsubscribe}. subscribe_api() -> Metadata = #{ @@ -436,7 +436,7 @@ subscribe_api() -> schema => #{type => string}, required => true }], - 'requestBody' => emqx_mgmt_util:request_body_schema(#{ + 'requestBody' => emqx_mgmt_util:schema(#{ type => object, properties => #{ topic => #{ @@ -448,8 +448,8 @@ subscribe_api() -> example => 0, description => <<"QoS">>}}}), responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Subscribe ok">>)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(<<"Subscribe ok">>)}}}, {"/clients/:clientid/subscribe", Metadata, subscribe}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index a8a54a9a9..f6425a4d6 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -18,6 +18,11 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , error_schema/2 + ]). + -export([api_spec/0]). -export([ config/2 @@ -34,15 +39,6 @@ schema => #{type => string, default => <<".">>} }]). --define(TEXT_BODY(DESCR, SCHEMA), #{ - description => list_to_binary(DESCR), - content => #{ - <<"text/plain">> => #{ - schema => SCHEMA - } - } -}). - -define(PREFIX, "/configs"). -define(PREFIX_RESET, "/configs_reset"). @@ -69,18 +65,16 @@ config_api(ConfPath, Schema) -> get => #{ description => Descr("Get configs for"), responses => #{ - <<"200">> => ?TEXT_BODY("Get configs successfully", Schema), - <<"404">> => emqx_mgmt_util:response_error_schema( - <<"Config not found">>, ['NOT_FOUND']) + <<"200">> => schema(Schema, <<"Get configs successfully">>), + <<"404">> => emqx_mgmt_util:error_schema(<<"Config not found">>, ['NOT_FOUND']) } }, put => #{ description => Descr("Update configs for"), - 'requestBody' => ?TEXT_BODY("The format of the request body is depend on the 'conf_path' parameter in the query string", Schema), + 'requestBody' => schema(Schema), responses => #{ - <<"200">> => ?TEXT_BODY("Update configs successfully", Schema), - <<"400">> => emqx_mgmt_util:response_error_schema( - <<"Update configs failed">>, ['UPDATE_FAILED']) + <<"200">> => schema(Schema, <<"Update configs successfully">>), + <<"400">> => error_schema(<<"Update configs failed">>, ['UPDATE_FAILED']) } } }, @@ -97,9 +91,8 @@ config_reset_api() -> %% We only return "200" rather than the new configs that has been changed, as %% the schema of the changed configs is depends on the request parameter %% `conf_path`, it cannot be defined here. - <<"200">> => emqx_mgmt_util:response_schema(<<"Reset configs successfully">>), - <<"400">> => emqx_mgmt_util:response_error_schema( - <<"It's not able to reset the config">>, ['INVALID_OPERATION']) + <<"200">> => schema(<<"Reset configs successfully">>), + <<"400">> => error_schema(<<"It's not able to reset the config">>, ['INVALID_OPERATION']) } } }, diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 78bdba615..5d0eaaf74 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -27,6 +27,15 @@ , manage_listeners/2 , manage_nodes_listeners/2]). +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/2 + , object_array_schema/2 + , error_schema/1 + , error_schema/2 + , properties/1 + ]). + -export([format/1]). -include_lib("emqx/include/emqx.hrl"). @@ -41,39 +50,20 @@ api_spec() -> manage_listeners_api(), manage_nodes_listeners_api() ], - [listener_schema()] + [] }. -listener_schema() -> - #{ - listener => #{ - type => object, - properties => #{ - node => #{ - type => string, - description => <<"Node">>, - example => node()}, - id => #{ - type => string, - description => <<"Identifier">>}, - acceptors => #{ - type => integer, - description => <<"Number of Acceptor process">>}, - max_conn => #{ - type => integer, - description => <<"Maximum number of allowed connection">>}, - type => #{ - type => string, - description => <<"Listener type">>}, - listen_on => #{ - type => string, - description => <<"Listening port">>}, - running => #{ - type => boolean, - description => <<"Open or close">>}, - auth => #{ - type => boolean, - description => <<"Has auth">>}}}}. +properties() -> + properties([ + {node, string, <<"Node">>}, + {id, string, <<"Identifier">>}, + {acceptors, integer, <<"Number of Acceptor process">>}, + {max_conn, integer, <<"Maximum number of allowed connection">>}, + {type, string, <<"Listener type">>}, + {listen_on, string, <<"Listener port">>}, + {running, boolean, <<"Open or close">>}, + {auth, boolean, <<"Has auth">>} + ]). listeners_api() -> Metadata = #{ @@ -81,7 +71,7 @@ listeners_api() -> description => <<"List listeners in cluster">>, responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List all listeners">>, listener)}}}, + object_array_schema(properties(), <<"List all listeners">>)}}}, {"/listeners", Metadata, listeners}. listener_api() -> @@ -91,9 +81,9 @@ listener_api() -> parameters => [param_path_id()], responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), + error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List listener info ok">>, listener)}}}, + object_array_schema(properties(), <<"List listener info ok">>)}}}, {"/listeners/:id", Metadata, listener}. manage_listeners_api() -> @@ -105,15 +95,12 @@ manage_listeners_api() -> param_path_operation()], responses => #{ <<"500">> => - emqx_mgmt_util:response_error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, - ['BAD_LISTENER_ID']), + error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), <<"400">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, - ['BAD_REQUEST']), - <<"200">> => - emqx_mgmt_util:response_schema(<<"Operation success">>)}}}, + error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), + <<"200">> => schema(<<"Operation success">>)}}}, {"/listeners/:id/:operation", Metadata, manage_listeners}. manage_nodes_listeners_api() -> @@ -126,15 +113,14 @@ manage_nodes_listeners_api() -> param_path_operation()], responses => #{ <<"500">> => - emqx_mgmt_util:response_error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Bad node or Listener id not found">>, + error_schema(<<"Bad node or Listener id not found">>, ['BAD_NODE_NAME','BAD_LISTENER_ID']), <<"400">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, - ['BAD_REQUEST']), + error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), <<"200">> => - emqx_mgmt_util:response_schema(<<"Operation success">>)}}}, + schema(<<"Operation success">>)}}}, {"/node/:node/listeners/:id/:operation", Metadata, manage_nodes_listeners}. nodes_listeners_api() -> @@ -144,10 +130,10 @@ nodes_listeners_api() -> parameters => [param_path_node(), param_path_id()], responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Node name or listener id not found">>, + error_schema(<<"Node name or listener id not found">>, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:response_schema(<<"Get listener info ok">>, listener)}}}, + schema(properties(), <<"Get listener info ok">>)}}}, {"/nodes/:node/listeners/:id", Metadata, node_listener}. nodes_listener_api() -> @@ -156,10 +142,8 @@ nodes_listener_api() -> description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>), - <<"200">> => - emqx_mgmt_util:response_schema(<<"Get listener info ok">>, listener)}}}, + <<"404">> => error_schema(<<"Listener id not found">>), + <<"200">> => object_schema(properties(), <<"Get listener info ok">>)}}}, {"/nodes/:node/listeners", Metadata, node_listeners}. %%%============================================================================================== %% parameters @@ -199,27 +183,27 @@ listeners(get, _Request) -> list(). listener(get, Request) -> - ID = binary_to_atom(cowboy_req:binding(id, Request)), + ID = b2a(cowboy_req:binding(id, Request)), get_listeners(#{id => ID}). node_listeners(get, Request) -> - Node = binary_to_atom(cowboy_req:binding(node, Request)), + Node = b2a(cowboy_req:binding(node, Request)), get_listeners(#{node => Node}). node_listener(get, Request) -> - Node = binary_to_atom(cowboy_req:binding(node, Request)), - ID = binary_to_atom(cowboy_req:binding(id, Request)), + Node = b2a(cowboy_req:binding(node, Request)), + ID = b2a(cowboy_req:binding(id, Request)), get_listeners(#{node => Node, id => ID}). manage_listeners(_, Request) -> - ID = binary_to_atom(cowboy_req:binding(id, Request)), - Operation = binary_to_atom(cowboy_req:binding(operation, Request)), + ID = b2a(cowboy_req:binding(id, Request)), + Operation = b2a(cowboy_req:binding(operation, Request)), manage(Operation, #{id => ID}). manage_nodes_listeners(_, Request) -> - Node = binary_to_atom(cowboy_req:binding(node, Request)), - ID = binary_to_atom(cowboy_req:binding(id, Request)), - Operation = binary_to_atom(cowboy_req:binding(operation, Request)), + Node = b2a(cowboy_req:binding(node, Request)), + ID = b2a(cowboy_req:binding(id, Request)), + Operation = b2a(cowboy_req:binding(operation, Request)), manage(Operation, #{id => ID, node => Node}). %%%============================================================================================== @@ -232,16 +216,16 @@ get_listeners(Param) -> case list_listener(Param) of {error, not_found} -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; {error, nodedown} -> Node = maps:get(node, Param), - Reason = list_to_binary(io_lib:format("Node ~p rpc failed", [Node])), + Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), Response = #{code => 'BAD_NODE_NAME', message => Reason}, {404, Response}; [] -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; Data -> {200, Data} @@ -253,16 +237,16 @@ manage(Operation0, Param) -> case list_listener(Param) of {error, not_found} -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; {error, nodedown} -> Node = maps:get(node, Param), - Reason = list_to_binary(io_lib:format("Node ~p rpc failed", [Node])), + Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), Response = #{code => 'BAD_NODE_NAME', message => Reason}, {404, Response}; [] -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'RESOURCE_NOT_FOUND', message => Reason}}; ListenersOrSingleListener -> manage_(Operation, ListenersOrSingleListener) @@ -279,16 +263,16 @@ manage_(Operation, Listeners) when is_list(Listeners) -> case lists:filter(fun({error, {already_started, _}}) -> false; (_) -> true end, Results) of [] -> ID = maps:get(id, hd(Listeners)), - Message = list_to_binary(io_lib:format("Already Started: ~s", [ID])), + Message = iolist_to_binary(io_lib:format("Already Started: ~s", [ID])), {400, #{code => 'BAD_REQUEST', message => Message}}; _ -> case lists:filter(fun({error,not_found}) -> false; (_) -> true end, Results) of [] -> ID = maps:get(id, hd(Listeners)), - Message = list_to_binary(io_lib:format("Already Stopped: ~s", [ID])), + Message = iolist_to_binary(io_lib:format("Already Stopped: ~s", [ID])), {400, #{code => 'BAD_REQUEST', message => Message}}; _ -> - Reason = list_to_binary(io_lib:format("~p", [Errors])), + Reason = iolist_to_binary(io_lib:format("~p", [Errors])), {500, #{code => 'UNKNOW_ERROR', message => Reason}} end end @@ -332,3 +316,6 @@ trans_running(Conf) -> Running -> Running end. + + +b2a(B) when is_binary(B) -> binary_to_atom(B, utf8). diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index 6f7d7c5f0..49035417a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -304,14 +304,7 @@ metrics_api() -> schema => #{type => boolean} }], responses => #{ - <<"200">> => #{ - description => <<"List all metrics">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"metrics_info">>) - } - } - } + <<"200">> => emqx_mgmt_util:schema(metrics_info, <<"List all metrics">>) } } }, diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 59b427261..31d17f432 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -17,6 +17,13 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [ schema/2 + , object_schema/2 + , object_array_schema/2 + , error_schema/2 + , properties/1 + ]). + -export([api_spec/0]). -export([ nodes/2 @@ -27,7 +34,7 @@ -include_lib("emqx/include/emqx.hrl"). api_spec() -> - {apis(), schemas()}. + {apis(), []}. apis() -> [ nodes_api() @@ -35,125 +42,75 @@ apis() -> , 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() -> - #{ - node => #{ - type => object, - properties => #{ - 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 => integer, - description => <<"EMQ X Broker runtime, millisecond">>}, - 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">>} - } - } - }. +properties() -> + properties([ + {node, string, <<"Node name">>}, + {connections, integer, <<"Number of clients currently connected to this node">>}, + {load1, string, <<"CPU average load in 1 minute">>}, + {load5, string, <<"CPU average load in 5 minute">>}, + {load15, string, <<"CPU average load in 15 minute">>}, + {max_fds, integer, <<"Maximum file descriptor limit for the operating system">>}, + {memory_total, string, <<"VM allocated system memory">>}, + {memory_used, string, <<"VM occupied system memory">>}, + {node_status, string, <<"Node status">>}, + {otp_release, string, <<"Erlang/OTP version used by EMQ X Broker">>}, + {process_available, integer, <<"Number of available processes">>}, + {process_used, integer, <<"Number of used processes">>}, + {uptime, integer, <<"EMQ X Broker runtime, millisecond">>}, + {version, string, <<"EMQ X Broker version">>}, + {sys_path, string, <<"EMQ X system file location">>}, + {log_path, string, <<"EMQ X log file location">>}, + {config_path, string, <<"EMQ X config file location">>} + ]). +parameters() -> + [#{ + name => node_name, + in => path, + description => <<"node name">>, + schema => #{type => string}, + required => true, + example => node() + }]. nodes_api() -> Metadata = #{ get => #{ description => <<"List EMQ X nodes">>, responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<"List EMQ X Nodes">>, node)}}}, + <<"200">> => object_array_schema(properties(), <<"List EMQ X Nodes">>) + } + } + }, {"/nodes", Metadata, nodes}. node_api() -> Metadata = #{ get => #{ description => <<"Get node info">>, - parameters => [#{ - name => node_name, - in => path, - description => "node name", - schema => #{type => string}, - required => true, - example => node()}], + parameters => parameters(), responses => #{ - <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Nodes info by name">>, node)}}}, + <<"400">> => error_schema(<<"Node error">>, ['SOURCE_ERROR']), + <<"200">> => object_schema(properties(), <<"Get EMQ X Nodes info by name">>)}}}, {"/nodes/:node_name", Metadata, node}. node_metrics_api() -> Metadata = #{ get => #{ description => <<"Get node metrics">>, - parameters => [#{ - name => node_name, - in => path, - description => "node name", - schema => #{type => string}, - required => true, - example => node()}], + parameters => parameters(), responses => #{ - <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Node Metrics">>, metrics)}}}, + <<"400">> => error_schema(<<"Node error">>, ['SOURCE_ERROR']), + <<"200">> => schema(metrics, <<"Get EMQ X Node 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", - schema => #{type => string}, - required => true, - example => node()}], + parameters => parameters(), responses => #{ - <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Node Stats">>, stat)}}}, + <<"400">> => error_schema(<<"Node error">>, ['SOURCE_ERROR']), + <<"200">> => schema(stat, <<"Get EMQ X Node Stats">>)}}}, {"/nodes/:node_name/stats", Metadata, node_stats}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl index 1e4555160..058e18160 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -19,88 +19,48 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [ object_schema/1 + , object_schema/2 + , object_array_schema/1 + , object_array_schema/2 + , properties/1 + ]). + -export([api_spec/0]). -export([ publish/2 , publish_batch/2]). api_spec() -> - { - [publish_api(), publish_bulk_api()], - [message_schema()] - }. + {[publish_api(), publish_bulk_api()], []}. publish_api() -> - Schema = #{ - type => object, - properties => maps:without([id], message_properties()) - }, MeteData = #{ post => #{ description => <<"Publish">>, - 'requestBody' => emqx_mgmt_util:request_body_schema(Schema), + 'requestBody' => object_schema(maps:without([id], properties())), responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"publish ok">>, message)}}}, + <<"200">> => object_schema(properties(), <<"publish ok">>)}}}, {"/publish", MeteData, publish}. publish_bulk_api() -> - Schema = #{ - type => object, - properties => maps:without([id], message_properties()) - }, MeteData = #{ post => #{ description => <<"publish">>, - 'requestBody' => emqx_mgmt_util:request_body_array_schema(Schema), + 'requestBody' => object_array_schema(maps:without([id], properties())), responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<"publish ok">>, message)}}}, + <<"200">> => object_array_schema(properties(), <<"publish ok">>)}}}, {"/publish/bulk", MeteData, publish_batch}. -message_schema() -> - #{ - message => #{ - type => object, - properties => message_properties() - } - }. - -message_properties() -> - #{ - 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">>} - } - } - }. +properties() -> + properties([ + {id, string, <<"Message Id">>}, + {topic, string, <<"Topic">>}, + {qos, integer, <<"QoS">>, [0, 1, 2]}, + {payload, string, <<"Topic">>}, + {from, string, <<"Message from">>}, + {retain, boolean, <<"Retain message flag, nullable, default false">>} + ]). publish(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), @@ -119,19 +79,8 @@ message(Map) -> 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 - }. + Retain = maps:get(<<"retain">>, Map, false), + emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}). messages(List) -> [message(MessageMap) || MessageMap <- List]. @@ -144,7 +93,7 @@ format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload qos => Qos, topic => Topic, payload => Payload, - flag => Flags, + retain => maps:get(retain, Flags, false), from => to_binary(From) }. diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 258680546..b193bac34 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -28,43 +28,35 @@ -define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND'). +-import(emqx_mgmt_util, [ object_schema/2 + , object_array_schema/2 + , error_schema/2 + , properties/1 + , page_params/0 + ]). + api_spec() -> { [routes_api(), route_api()], - [route_schema()] + [] }. -route_schema() -> - #{ - route => #{ - type => object, - properties => #{ - topic => #{ - type => string}, - node => #{ - type => string, - example => node()}}}}. +properties() -> + properties([ + {topic, string}, + {node, string} + ]). routes_api() -> Metadata = #{ get => #{ description => <<"EMQ X routes">>, - parameters => [ - #{ - name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer, default => 1} - }, - #{ - name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer, default => emqx_mgmt:max_row_limit()} - }], + parameters => page_params(), responses => #{ - <<"200">> => - emqx_mgmt_util:response_array_schema("List route info", route)}}}, + <<"200">> => object_array_schema(properties(), <<"List route info">>) + } + } + }, {"/routes", Metadata, routes}. route_api() -> @@ -80,10 +72,12 @@ route_api() -> }], responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Route info">>, route), + object_schema(properties(), <<"Route info">>), <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Topic not found">>, [?TOPIC_NOT_FOUND]) - }}}, + error_schema(<<"Topic not found">>, [?TOPIC_NOT_FOUND]) + } + } + }, {"/routes/:topic", Metadata, route}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_status.erl b/apps/emqx_management/src/emqx_mgmt_api_status.erl index fa46b1d25..2fa47d1d9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_status.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_status.erl @@ -30,7 +30,10 @@ status_api() -> get => #{ security => [], responses => #{ - <<"200">> => #{description => <<"running">>}}}}, + <<"200">> => #{description => <<"running">>} + } + } + }, {Path, Metadata, running_status}. running_status(get, _Request) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 27e8c898a..f7a37b861 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -20,6 +20,11 @@ -include_lib("emqx/include/emqx.hrl"). +-import(emqx_mgmt_util, [ page_schema/1 + , properties/1 + , page_params/0 + ]). + -export([api_spec/0]). -export([subscriptions/2]). @@ -39,84 +44,67 @@ -define(format_fun, {?MODULE, format}). api_spec() -> - { - [subscriptions_api()], - [subscription_schema()] - }. + {subscriptions_api(), subscription_schema()}. subscriptions_api() -> MetaData = #{ get => #{ description => <<"List subscriptions">>, - parameters => [ - #{ - name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer} - }, - #{ - name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer} - }, - #{ - name => clientid, - in => query, - description => <<"Client ID">>, - schema => #{type => string} - }, - #{ - name => node, - in => query, - description => <<"Node name">>, - schema => #{type => string} - }, - #{ - name => qos, - in => query, - description => <<"QoS">>, - schema => #{type => integer, enum => [0, 1, 2]} - }, - #{ - name => share, - in => query, - description => <<"Shared subscription">>, - schema => #{type => boolean} - }, - #{ - name => topic, - in => query, - description => <<"Topic">>, - schema => #{type => string} - } - #{ - name => match_topic, - in => query, - description => <<"Match topic string">>, - schema => #{type => string} - } - ], + parameters => parameters(), responses => #{ - <<"200">> => emqx_mgmt_util:response_page_schema(subscription)}}}, - {"/subscriptions", MetaData, subscriptions}. + <<"200">> => page_schema(subscription) + } + } + }, + [{"/subscriptions", MetaData, subscriptions}]. subscription_schema() -> - #{ - subscription => #{ - type => object, - properties => #{ - node => #{ - type => string}, - topic => #{ - type => string}, - clientid => #{ - type => string}, - qos => #{ - type => integer, - enum => [0,1,2]}}} - }. + Props = properties([ + {node, string}, + {topic, string}, + {clientid, string}, + {qos, integer, <<>>, [0,1,2]}]), + [#{subscription => #{type => object, properties => Props}}]. + +parameters() -> + [ + #{ + name => clientid, + in => query, + description => <<"Client ID">>, + schema => #{type => string} + }, + #{ + name => node, + in => query, + description => <<"Node name">>, + schema => #{type => string} + }, + #{ + name => qos, + in => query, + description => <<"QoS">>, + schema => #{type => integer, enum => [0, 1, 2]} + }, + #{ + name => share, + in => query, + description => <<"Shared subscription">>, + schema => #{type => boolean} + }, + #{ + name => topic, + in => query, + description => <<"Topic">>, + schema => #{type => string} + } + #{ + name => match_topic, + in => query, + description => <<"Match topic string">>, + schema => #{type => string} + } | page_params() + ]. subscriptions(get, Request) -> Params = cowboy_req:parse_qs(Request), diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index 7bd393904..da509a36b 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -133,3 +133,4 @@ api_modules() -> api_modules() -> minirest_api:find_api_modules(apps()) -- [emqx_mgmt_api_apps]. -endif. + diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index d764afb07..5a95238e3 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -24,15 +24,26 @@ , batch_operation/3 ]). --export([ request_body_schema/1 - , request_body_array_schema/1 - , response_schema/1 - , response_schema/2 - , response_array_schema/2 - , response_error_schema/1 - , response_error_schema/2 - , response_page_schema/1 - , response_batch_schema/1]). +-export([ bad_request/0 + , bad_request/1 + , properties/1 + , page_params/0 + , schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , array_schema/1 + , array_schema/2 + , object_array_schema/1 + , object_array_schema/2 + , page_schema/1 + , page_object_schema/1 + , error_schema/1 + , error_schema/2 + , batch_schema/1 + ]). + + -export([urldecode/1]). @@ -90,78 +101,69 @@ urldecode(S) -> %%%============================================================================================== %% schema util +schema(Ref) when is_atom(Ref) -> + json_content_schema(minirest:ref(atom_to_binary(Ref, utf8))); +schema(SchemaOrDesc) -> + json_content_schema(SchemaOrDesc). +schema(Ref, Desc) when is_atom(Ref) -> + json_content_schema(minirest:ref(atom_to_binary(Ref, utf8)), Desc); +schema(Schema, Desc) -> + json_content_schema(Schema, Desc). -request_body_array_schema(Schema) when is_map(Schema) -> - json_content_schema("", #{type => array, items => Schema}); -request_body_array_schema(Ref) when is_atom(Ref) -> - request_body_array_schema(atom_to_binary(Ref, utf8)); -request_body_array_schema(Ref) when is_binary(Ref) -> - json_content_schema("", #{type => array, items => minirest:ref(Ref)}). +object_schema(Properties) when is_map(Properties) -> + json_content_schema(#{type => object, properties => Properties}). +object_schema(Properties, Desc) when is_map(Properties) -> + json_content_schema(#{type => object, properties => Properties}, Desc). -request_body_schema(Schema) when is_map(Schema) -> - json_content_schema("", Schema); -request_body_schema(Ref) when is_atom(Ref) -> - request_body_schema(atom_to_binary(Ref)); -request_body_schema(Ref) when is_binary(Ref) -> - json_content_schema("", minirest:ref(Ref)). +array_schema(Ref) when is_atom(Ref) -> + json_content_schema(#{type => array, items => minirest:ref(atom_to_binary(Ref, utf8))}). +array_schema(Ref, Desc) when is_atom(Ref) -> + json_content_schema(#{type => array, items => minirest:ref(atom_to_binary(Ref, utf8))}, Desc); +array_schema(Schema, Desc) -> + json_content_schema(#{type => array, items => Schema}, Desc). -response_array_schema(Description, Schema) when is_map(Schema) -> - json_content_schema(Description, #{type => array, items => Schema}); -response_array_schema(Description, Ref) when is_atom(Ref) -> - response_array_schema(Description, atom_to_binary(Ref, utf8)); -response_array_schema(Description, Ref) when is_binary(Ref) -> - json_content_schema(Description, #{type => array, items => minirest:ref(Ref)}). +object_array_schema(Properties) when is_map(Properties) -> + json_content_schema(#{type => array, items => #{type => object, properties => Properties}}). +object_array_schema(Properties, Desc) -> + json_content_schema(#{type => array, items => #{type => object, properties => Properties}}, Desc). -response_schema(Description) -> - json_content_schema(Description). - -response_schema(Description, Schema) when is_map(Schema) -> - json_content_schema(Description, Schema); -response_schema(Description, Ref) when is_atom(Ref) -> - response_schema(Description, atom_to_binary(Ref, utf8)); -response_schema(Description, Ref) when is_binary(Ref) -> - json_content_schema(Description, minirest:ref(Ref)). - -%% @doc default code is RESOURCE_NOT_FOUND -response_error_schema(Description) -> - response_error_schema(Description, ['RESOURCE_NOT_FOUND']). - -response_error_schema(Description, Enum) -> - Schema = #{ - type => object, - properties => #{ - code => #{ - type => string, - enum => Enum}, - message => #{ - type => string}}}, - json_content_schema(Description, Schema). - -response_page_schema(Def) when is_atom(Def) -> - response_page_schema(atom_to_binary(Def, utf8)); -response_page_schema(Def) when is_binary(Def) -> - response_page_schema(minirest:ref(Def)); -response_page_schema(ItemSchema) when is_map(ItemSchema) -> - Schema = #{ +page_schema(Ref) when is_atom(Ref) -> + page_schema(minirest:ref(atom_to_binary(Ref, utf8))); +page_schema(Schema) -> + Schema1 = #{ type => object, properties => #{ meta => #{ type => object, - properties => #{ - page => #{ - type => integer}, - limit => #{ - type => integer}, - count => #{ - type => integer}}}, + properties => properties([{page, integer}, + {limit, integer}, + {count, integer}]) + }, data => #{ type => array, - items => ItemSchema}}}, - json_content_schema("", Schema). + items => Schema + } + } + }, + json_content_schema(Schema1). -response_batch_schema(DefName) when is_atom(DefName) -> - response_batch_schema(atom_to_binary(DefName, utf8)); -response_batch_schema(DefName) when is_binary(DefName) -> +page_object_schema(Properties) when is_map(Properties) -> + page_schema(#{type => object, properties => Properties}). + +error_schema(Description) -> + error_schema(Description, ['RESOURCE_NOT_FOUND']). + +error_schema(Description, Enum) -> + Schema = #{ + type => object, + properties => properties([{code, string, <<>>, Enum}, + {message, string}]) + }, + json_content_schema(Schema, Description). + +batch_schema(DefName) when is_atom(DefName) -> + batch_schema(atom_to_binary(DefName, utf8)); +batch_schema(DefName) when is_binary(DefName) -> Schema = #{ type => object, properties => #{ @@ -181,22 +183,17 @@ response_batch_schema(DefName) when is_binary(DefName) -> data => minirest:ref(DefName), reason => #{ type => <<"string">>}}}}}}, - json_content_schema("", Schema). + json_content_schema(Schema). -json_content_schema(Description, Schema) -> - Content = - #{content => #{ - 'application/json' => #{ - schema => Schema}}}, - case Description of - "" -> - Content; - _ -> - maps:merge(#{description => Description}, Content) - end. - -json_content_schema(Description) -> - #{description => Description}. +json_content_schema(Schema) when is_map(Schema) -> + #{content => #{'application/json' => #{schema => Schema}}}; +json_content_schema(Desc) when is_binary(Desc) -> + #{description => Desc}. +json_content_schema(Schema, Desc) -> + #{ + content => #{'application/json' => #{schema => Schema}}, + description => Desc + }. %%%============================================================================================== batch_operation(Module, Function, ArgsList) -> @@ -215,3 +212,44 @@ batch_operation(Module, Function, [Args | ArgsList], Failed) -> {error ,Reason} -> batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed]) end. + +properties(Props) -> + properties(Props, #{}). +properties([], Acc) -> + Acc; +properties([Key| Props], Acc) when is_atom(Key) -> + properties(Props, maps:put(Key, #{type => string}, Acc)); +properties([{Key, Type} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => Type}, Acc)); +properties([{Key, object, Props1} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => object, + properties => properties(Props1)}, Acc)); +properties([{Key, {array, Type}, Desc} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => array, + items => #{type => Type}, + description => Desc}, Acc)); +properties([{Key, Type, Desc} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc)); +properties([{Key, Type, Desc, Enum} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => Type, + description => Desc, + emum => Enum}, Acc)). +page_params() -> + [#{ + name => page, + in => query, + description => <<"Page">>, + schema => #{type => integer, default => 1} + }, + #{ + name => limit, + in => query, + description => <<"Page size">>, + schema => #{type => integer, default => emqx_mgmt:max_row_limit()} + }]. + +bad_request() -> + bad_request(<<"Bad Request">>). +bad_request(Desc) -> + object_schema(properties([{message, string}, {code, string}]), Desc). + diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 2d6c3ddf0..06a50fa37 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -18,11 +18,12 @@ -behavior(minirest_api). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , response_error_schema/2 - , response_page_schema/1 - , request_body_schema/1 +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/2 + , error_schema/2 + , page_object_schema/1 + , properties/1 ]). -define(MAX_PAYLOAD_LENGTH, 2048). @@ -48,77 +49,50 @@ api_spec() -> { [status_api(), delayed_messages_api(), delayed_message_api()], - [] + schemas() }. -delayed_schema() -> - delayed_schema(false). +schemas() -> + [#{delayed => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([delayed]))}]. +properties() -> + PayloadDesc = io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p", + [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH]), + properties([ + {id, integer, <<"Message Id (MQTT message id hash)">>}, + {publish_time, string, <<"publish time, rfc 3339">>}, + {topic, string, <<"Topic">>}, + {qos, string, <<"QoS">>}, + {payload, string, iolist_to_binary(PayloadDesc)}, + {form_clientid, string, <<"Form ClientId">>}, + {form_username, string, <<"Form Username">>} + ]). -delayed_schema(WithPayload) -> - case WithPayload of - true -> - #{ - type => object, - properties => delayed_message_properties() - }; - _ -> - #{ - type => object, - properties => maps:without([payload], delayed_message_properties()) - } - end. - -delayed_message_properties() -> - PayloadDesc = list_to_binary( - io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p", - [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH])), - #{ - id => #{ - type => integer, - description => <<"Message Id (MQTT message id hash)">>}, - publish_time => #{ - type => string, - description => <<"publish time, rfc 3339">>}, - topic => #{ - type => string, - description => <<"Topic">>}, - qos => #{ - type => integer, - enum => [0, 1, 2], - description => <<"Qos">>}, - payload => #{ - type => string, - description => PayloadDesc}, - form_clientid => #{ - type => string, - description => <<"Client ID">>}, - form_username => #{ - type => string, - description => <<"Username">>} - }. +parameters() -> + [#{ + name => id, + in => path, + schema => #{type => string}, + required => true + }]. status_api() -> - Schema = #{ - type => object, - properties => #{ - enable => #{ - type => boolean}, - max_delayed_messages => #{ - type => integer, - description => <<"Max limit, 0 is no limit">>}}}, Metadata = #{ get => #{ - description => "Get delayed status", + description => <<"Get delayed status">>, responses => #{ - <<"200">> => response_schema(<<"Bad Request">>, Schema)}}, + <<"200">> => schema(delayed)} + }, put => #{ - description => "Enable or disable delayed, set max delayed messages", - 'requestBody' => request_body_schema(Schema), + description => <<"Enable or disable delayed, set max delayed messages">>, + 'requestBody' => schema(delayed), responses => #{ <<"200">> => - response_schema(<<"Enable or disable delayed successfully">>, Schema), + schema(delayed, <<"Enable or disable delayed successfully">>), <<"400">> => - response_error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED])}}}, + error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED]) + } + } + }, {"/mqtt/delayed_messages/status", Metadata, status}. delayed_messages_api() -> @@ -126,32 +100,30 @@ delayed_messages_api() -> get => #{ description => "List delayed messages", responses => #{ - <<"200">> => response_page_schema(delayed_schema())}}}, + <<"200">> => page_object_schema(properties()) + } + } + }, {"/mqtt/delayed_messages", Metadata, delayed_messages}. delayed_message_api() -> Metadata = #{ get => #{ - description => "Get delayed message", - parameters => [#{ - name => id, - in => path, - schema => #{type => string}, - required => true - }], + description => <<"Get delayed message">>, + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Get delayed message success">>, delayed_schema(true)), - <<"404">> => response_error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND])}}, + <<"200">> => object_schema(maps:without([payload], properties()), <<"Get delayed message success">>), + <<"404">> => error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND]) + } + }, delete => #{ - description => "Delete delayed message", - parameters => [#{ - name => id, - in => path, - schema => #{type => string}, - required => true - }], + description => <<"Delete delayed message">>, + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Delete delayed message success">>)}}}, + <<"200">> => schema(<<"Delete delayed message success">>) + } + } + }, {"/mqtt/delayed_messages/:id", Metadata, delayed_message}. %%-------------------------------------------------------------------- @@ -181,7 +153,7 @@ delayed_message(get, Request) -> {200, Message#{payload => base64:encode(Payload)}} end; {error, not_found} -> - Message = list_to_binary(io_lib:format("Message ID ~p not found", [Id])), + Message = iolist_to_binary(io_lib:format("Message ID ~p not found", [Id])), {404, #{code => ?MESSAGE_ID_NOT_FOUND, message => Message}} end; delayed_message(delete, Request) -> diff --git a/apps/emqx_modules/src/emqx_event_message_api.erl b/apps/emqx_modules/src/emqx_event_message_api.erl index 86c3255e1..43216ef63 100644 --- a/apps/emqx_modules/src/emqx_event_message_api.erl +++ b/apps/emqx_modules/src/emqx_event_message_api.erl @@ -21,51 +21,15 @@ -export([event_message/2]). +-import(emqx_mgmt_util, [ schema/1 + ]). api_spec() -> {[event_message_api()], [event_message_schema()]}. event_message_schema() -> - #{ - type => object, - properties => #{ - '$event/client_connected' => #{ - type => boolean, - description => <<"Client connected event">>, - example => get_raw(<<"$event/client_connected">>) - }, - '$event/client_disconnected' => #{ - type => boolean, - description => <<"client_disconnected">>, - example => get_raw(<<"Client disconnected event">>) - }, - '$event/client_subscribed' => #{ - type => boolean, - description => <<"client_subscribed">>, - example => get_raw(<<"Client subscribed event">>) - }, - '$event/client_unsubscribed' => #{ - type => boolean, - description => <<"client_unsubscribed">>, - example => get_raw(<<"Client unsubscribed event">>) - }, - '$event/message_delivered' => #{ - type => boolean, - description => <<"message_delivered">>, - example => get_raw(<<"Message delivered event">>) - }, - '$event/message_acked' => #{ - type => boolean, - description => <<"message_acked">>, - example => get_raw(<<"Message acked event">>) - }, - '$event/message_dropped' => #{ - type => boolean, - description => <<"message_dropped">>, - example => get_raw(<<"Message dropped event">>) - } - } - }. + Conf = emqx:get_raw_config([event_message]), + #{event_message => emqx_mgmt_api_configs:gen_schema(Conf)}. event_message_api() -> Path = "/mqtt/event_message", @@ -73,14 +37,14 @@ event_message_api() -> get => #{ description => <<"Event Message">>, responses => #{ - <<"200">> => - emqx_mgmt_util:response_schema(<<>>, event_message_schema())}}, + <<"200">> => schema(event_message) + } + }, post => #{ - description => <<"">>, - 'requestBody' => emqx_mgmt_util:request_body_schema(event_message_schema()), + description => <<"Update Event Message">>, + 'requestBody' => schema(event_message), responses => #{ - <<"200">> => - emqx_mgmt_util:response_schema(<<>>, event_message_schema()) + <<"200">> => schema(event_message) } } }, @@ -94,6 +58,3 @@ event_message(post, Request) -> Params = emqx_json:decode(Body, [return_maps]), _ = emqx_event_message:update(Params), {200, emqx_event_message:list()}. - -get_raw(Key) -> - emqx_config:get_raw([<<"event_message">>] ++ [Key], false). diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl index 9b07b0a93..8a5a5dc6b 100644 --- a/apps/emqx_modules/src/emqx_rewrite_api.erl +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -25,28 +25,20 @@ -define(EXCEED_LIMIT, 'EXCEED_LIMIT'). +-import(emqx_mgmt_util, [ object_array_schema/1 + , object_array_schema/2 + , error_schema/2 + , properties/1 + ]). + api_spec() -> {[rewrite_api()], []}. -topic_rewrite_schema() -> - #{ - type => object, - properties => #{ - action => #{ - type => string, - description => <<"Node">>, - enum => [subscribe, publish]}, - source_topic => #{ - type => string, - description => <<"Topic">>}, - re => #{ - type => string, - description => <<"Regular expressions">>}, - dest_topic => #{ - type => string, - description => <<"Destination topic">>} - } - }. +properties() -> + properties([{action, string, <<"Node">>, [subscribe, publish]}, + {source_topic, string, <<"Topic">>}, + {re, string, <<"Regular expressions">>}, + {dest_topic, string, <<"Destination topic">>}]). rewrite_api() -> Path = "/mqtt/topic_rewrite", @@ -54,15 +46,18 @@ rewrite_api() -> get => #{ description => <<"List topic rewrite">>, responses => #{ - <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List all rewrite rules">>, topic_rewrite_schema())}}, + <<"200">> => object_array_schema(properties(), <<"List all rewrite rules">>) + } + }, post => #{ description => <<"Update topic rewrite">>, - 'requestBody' => emqx_mgmt_util:request_body_array_schema(topic_rewrite_schema()), + 'requestBody' => object_array_schema(properties()), responses => #{ - <<"200">> => - emqx_mgmt_util:response_schema(<<"Update topic rewrite success">>, topic_rewrite_schema()), - <<"413">> => emqx_mgmt_util:response_error_schema(<<"Rules count exceed max limit">>, [?EXCEED_LIMIT])}}}, + <<"200">> =>object_array_schema(properties(), <<"Update topic rewrite success">>), + <<"413">> => error_schema(<<"Rules count exceed max limit">>, [?EXCEED_LIMIT]) + } + } + }, {Path, Metadata, topic_rewrite}. topic_rewrite(get, _Request) -> @@ -76,6 +71,6 @@ topic_rewrite(post, Request) -> ok = emqx_rewrite:update(Params), {200, emqx_rewrite:list()}; _ -> - Message = list_to_binary(io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT])), + Message = iolist_to_binary(io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT])), {413, #{code => ?EXCEED_LIMIT, message => Message}} end. diff --git a/apps/emqx_modules/src/emqx_telemetry_api.erl b/apps/emqx_modules/src/emqx_telemetry_api.erl index af5f40b02..e1d297afc 100644 --- a/apps/emqx_modules/src/emqx_telemetry_api.erl +++ b/apps/emqx_modules/src/emqx_telemetry_api.erl @@ -18,9 +18,11 @@ -behavior(minirest_api). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , request_body_schema/1 +-import(emqx_mgmt_util, [ schema/1 + , object_schema/1 + , object_schema/2 + , properties/1 + , bad_request/0 ]). % -export([cli/1]). @@ -34,96 +36,38 @@ -export([api_spec/0]). api_spec() -> - {[status_api(), data_api()], schemas()}. + {[status_api(), data_api()], []}. -schemas() -> - [#{broker_info => #{ - type => object, - properties => #{ - emqx_version => #{ - type => string, - description => <<"EMQ X Version">>}, - license => #{ - type => object, - properties => #{ - edition => #{type => string} - }, - description => <<"EMQ X License">>}, - os_name => #{ - type => string, - description => <<"OS Name">>}, - os_version => #{ - type => string, - description => <<"OS Version">>}, - otp_version => #{ - type => string, - description => <<"Erlang/OTP Version">>}, - up_time => #{ - type => integer, - description => <<"EMQ X Runtime">>}, - uuid => #{ - type => string, - description => <<"EMQ X UUID">>}, - nodes_uuid => #{ - type => array, - items => #{type => string}, - description => <<"EMQ X Cluster Nodes UUID">>}, - active_plugins => #{ - type => array, - items => #{type => string}, - description => <<"EMQ X Active Plugins">>}, - active_modules => #{ - type => array, - items => #{type => string}, - description => <<"EMQ X Active Modules">>}, - num_clients => #{ - type => integer, - description => <<"EMQ X Current Connections">>}, - messages_received => #{ - type => integer, - description => <<"EMQ X Current Received Message">>}, - messages_sent => #{ - type => integer, - description => <<"EMQ X Current Sent Message">>} - } - }}]. +properties() -> + properties([ + {emqx_version, string, <<"EMQ X Version">>}, + {license, object, [{edition, string, <<"EMQ X License">>}]}, + {os_name, string, <<"OS Name">>}, + {os_version, string, <<"OS Version">>}, + {otp_version, string, <<"Erlang/OTP Version">>}, + {up_time, string, <<"EMQ X Runtime">>}, + {uuid, string, <<"EMQ X UUID">>}, + {nodes_uuid, string, <<"EMQ X Cluster Nodes UUID">>}, + {active_plugins, {array, string}, <<"EMQ X Active Plugins">>}, + {active_modules, {array, string}, <<"EMQ X Active Modules">>}, + {num_clients, integer, <<"EMQ X Current Connections">>}, + {messages_received, integer, <<"EMQ X Current Received Message">>}, + {messages_sent, integer, <<"EMQ X Current Sent Message">>} + ]). status_api() -> + Props = properties([{enable, boolean}]), Metadata = #{ get => #{ description => "Get telemetry status", - responses => #{ - <<"200">> => response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{enable => #{type => boolean}} - } - ) - } + responses => #{<<"200">> => object_schema(Props)} }, put => #{ description => "Enable or disbale telemetry", - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - enable => #{ - type => boolean - } - } - }), + 'requestBody' => object_schema(Props), responses => #{ - <<"200">> => - response_schema(<<"Enable or disbale telemetry successfully">>), - <<"400">> => - response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - } - ) + <<"200">> => schema(<<"Enable or disbale telemetry successfully">>), + <<"400">> => bad_request() } } }, @@ -133,7 +77,7 @@ data_api() -> Metadata = #{ get => #{ responses => #{ - <<"200">> => response_schema(<<"Get telemetry data">>, <<"broker_info">>) + <<"200">> => object_schema(properties(), <<"Get telemetry data">>) } } }, diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 8a94a0ffa..4c974146c 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -20,9 +20,8 @@ -include("emqx_prometheus.hrl"). --import(emqx_mgmt_util, [ response_schema/2 - , request_body_schema/1 - ]). +-import(emqx_mgmt_util, [ schema/1 + , bad_request/0]). -export([api_spec/0]). @@ -40,23 +39,14 @@ prometheus_api() -> Metadata = #{ get => #{ description => <<"Get Prometheus info">>, - responses => #{ - <<"200">> => response_schema(<<>>, prometheus) - } + responses => #{<<"200">> => schema(prometheus)} }, put => #{ description => <<"Update Prometheus">>, - 'requestBody' => request_body_schema(prometheus), + 'requestBody' => schema(prometheus), responses => #{ - <<"200">> =>response_schema(<<>>, prometheus), - <<"400">> => - response_schema(<<"Bad Request">>, #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }) + <<"200">> => schema(prometheus), + <<"400">> => bad_request() } } }, diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 4c36cb541..981e6a879 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -190,7 +190,7 @@ init([]) -> handle_call({update_config, Conf}, _, State) -> State2 = update_config(State, Conf), - emqx_config:put([?APP], Conf), + _ = emqx:update_config([?APP], Conf), {reply, ok, State2}; handle_call({wait_semaphore, Id}, From, #{wait_quotas := Waits} = State) -> diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index d766eab06..34e75e567 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -27,14 +27,12 @@ , config/2]). -import(emqx_mgmt_api_configs, [gen_schema/1]). --import(emqx_mgmt_util, [ response_array_schema/2 - , response_schema/1 - , response_error_schema/2]). - --define(CFG_BODY(DESCR), - #{description => list_to_binary(DESCR), - content => #{<<"application/json">> => - #{schema => gen_schema(emqx_config:get([emqx_retainer]))}}}). +-import(emqx_mgmt_util, [ object_array_schema/2 + , schema/1 + , schema/2 + , error_schema/2 + , page_params/0 + , properties/1]). api_spec() -> { @@ -42,76 +40,86 @@ api_spec() -> , with_topic_api() , config_api() ], - [ message_schema(message, fun message_properties/0) - , message_schema(detail_message, fun detail_message_properties/0) - ] + schemas() }. +schemas() -> + MqttRetainer = gen_schema(emqx:get_raw_config([emqx_retainer])), + [#{emqx_retainer => MqttRetainer}]. + +message_props() -> + properties([ + {id, string, <<"Message ID">>}, + {topic, string, <<"MQTT Topic">>}, + {qos, string, <<"MQTT QoS">>}, + {payload, string, <<"MQTT Payload">>}, + {publish_at, string, <<"publish datetime">>}, + {from_clientid, string, <<"publisher ClientId">>}, + {from_username, string, <<"publisher Username">>} + ]). + +parameters() -> + [#{ + name => topic, + in => path, + required => true, + schema => #{type => "string"} + }]. + lookup_retained_api() -> - Metadata = - #{get => #{description => <<"lookup matching messages">>, - parameters => [ #{name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer, default => 1}} - , #{name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer, - default => emqx_mgmt:max_row_limit()}} - ], - responses => #{ <<"200">> => - response_array_schema("List retained messages", message) - , <<"405">> => response_schema(<<"NotAllowed">>) - }}}, + Metadata = #{ + get => #{ + description => <<"lookup matching messages">>, + parameters => page_params(), + responses => #{ + <<"200">> => object_array_schema( + maps:without([payload], message_props()), + <<"List retained messages">>), + <<"405">> => schema(<<"NotAllowed">>) + } + } + }, {"/mqtt/retainer/messages", Metadata, lookup_retained_warp}. with_topic_api() -> - MetaData = #{get => #{description => <<"lookup matching messages">>, - parameters => [ #{name => topic, - in => path, - required => true, - schema => #{type => "string"}} - , #{name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer, default => 1}} - , #{name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer, - default => emqx_mgmt:max_row_limit()}} - ], - responses => #{ <<"200">> => - response_array_schema("List retained messages", detail_message) - , <<"405">> => response_schema(<<"NotAllowed">>)}}, - delete => #{description => <<"delete matching messages">>, - parameters => [#{name => topic, - in => path, - required => true, - schema => #{type => "string"}}], - responses => #{ <<"200">> => response_schema(<<"Successed">>) - , <<"405">> => response_schema(<<"NotAllowed">>)}} - }, + MetaData = #{ + get => #{ + description => <<"lookup matching messages">>, + parameters => parameters() ++ page_params(), + responses => #{ + <<"200">> => object_array_schema(message_props(), <<"List retained messages">>), + <<"405">> => schema(<<"NotAllowed">>) + } + }, + delete => #{ + description => <<"delete matching messages">>, + parameters => parameters(), + responses => #{ + <<"200">> => schema(<<"Successed">>), + <<"405">> => schema(<<"NotAllowed">>) + } + } + }, {"/mqtt/retainer/message/:topic", MetaData, with_topic_warp}. config_api() -> MetaData = #{ - get => #{ - description => <<"get retainer config">>, - responses => #{<<"200">> => ?CFG_BODY("Get configs successfully"), - <<"404">> => response_error_schema( - <<"Config not found">>, ['NOT_FOUND'])} - }, - put => #{ - description => <<"Update retainer config">>, - 'requestBody' => - ?CFG_BODY("The format of the request body is depend on the 'conf_path' parameter in the query string"), - responses => #{<<"200">> => response_schema("Update configs successfully"), - <<"400">> => response_error_schema( - <<"Update configs failed">>, ['UPDATE_FAILED'])} - } - }, + get => #{ + description => <<"get retainer config">>, + responses => #{ + <<"200">> => schema(mqtt_retainer, <<"Get configs successfully">>), + <<"404">> => error_schema(<<"Config not found">>, ['NOT_FOUND']) + } + }, + put => #{ + description => <<"Update retainer config">>, + 'requestBody' => schema(mqtt_retainer), + responses => #{ + <<"200">> => schema(mqtt_retainer, <<"Update configs successfully">>), + <<"400">> => error_schema(<<"Update configs failed">>, ['UPDATE_FAILED']) + } + } + }, {"/mqtt/retainer", MetaData, config}. lookup_retained_warp(Type, Req) -> @@ -121,7 +129,7 @@ with_topic_warp(Type, Req) -> check_backend(Type, Req, fun with_topic/2). config(get, _) -> - Config = emqx_config:get([emqx_retainer]), + Config = emqx:get_config([mqtt_retainer]), Body = emqx_json:encode(Config), {200, Body}; @@ -129,16 +137,16 @@ config(put, Req) -> try {ok, Body, _} = cowboy_req:read_body(Req), Cfg = emqx_json:decode(Body), - {ok, RawConf} = hocon:binary(jsx:encode(#{<<"emqx_retainer">> => Cfg}), + {ok, RawConf} = hocon:binary(jsx:encode(#{<<"mqtt_retainer">> => Cfg}), #{format => richmap}), RichConf = hocon_schema:check(emqx_retainer_schema, RawConf, #{atom_key => true}), - #{emqx_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), + #{mqtt_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), emqx_retainer:update_config(Conf), {200, #{<<"content-type">> => <<"text/plain">>}, <<"Update configs successfully">>} catch _:Reason:_ -> {400, #{code => 'UPDATE_FAILED', - message => erlang:list_to_binary(io_lib:format("~p~n", [Reason]))}} + message => iolist_to_binary(io_lib:format("~p~n", [Reason]))}} end. %%------------------------------------------------------------------------------ @@ -169,30 +177,6 @@ lookup(Topic, Req, Formatter) -> {200, format_message(Msgs, Formatter)}. -message_schema(Type, Properties) -> - #{Type => #{type => object, - properties => Properties()}}. - -message_properties() -> - #{msgid => #{type => string, - description => <<"Message ID">>}, - topic => #{type => string, - description => <<"Topic">>}, - qos => #{type => integer, - enum => [0, 1, 2], - description => <<"Qos">>}, - publish_at => #{type => string, - description => <<"publish datetime">>}, - from_clientid => #{type => string, - description => <<"Message from">>}, - from_username => #{type => string, - description => <<"publish username">>}}. - -detail_message_properties() -> - Base = message_properties(), - Base#{payload => #{type => string, - description => <<"Topic">>}}. - format_message(Messages, Formatter) when is_list(Messages)-> [Formatter(Message) || Message <- Messages]; diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index 0efd2d479..a859d4d66 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -20,7 +20,8 @@ -include("emqx_statsd.hrl"). --import(emqx_mgmt_util, [response_schema/2, request_body_schema/1]). +-import(emqx_mgmt_util, [ schema/1 + , bad_request/0]). -export([api_spec/0]). @@ -37,24 +38,14 @@ statsd_api() -> Metadata = #{ get => #{ description => <<"Get statsd info">>, - responses => #{ - <<"200">> => response_schema(<<>>, statsd) - } + responses => #{<<"200">> => schema(statsd)} }, put => #{ description => <<"Update Statsd">>, - 'requestBody' => request_body_schema(statsd), + 'requestBody' => schema(statsd), responses => #{ - <<"200">> => - response_schema(<<>>, statsd), - <<"400">> => - response_schema(<<"Bad Request">>, #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }) + <<"200">> => schema(statsd), + <<"400">> => bad_request() } } }, From 55c3ea6064952197a0401d10a7368d7e9aa64dda Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 10:47:42 +0800 Subject: [PATCH 094/306] refactor(retainer): refactor emqx_retainer test case --- apps/emqx_retainer/src/emqx_retainer.erl | 4 +- apps/emqx_retainer/src/emqx_retainer_api.erl | 6 +- .../test/emqx_retainer_SUITE.erl | 67 +++++++------------ .../test/mqtt_protocol_v5_SUITE.erl | 29 +++++--- 4 files changed, 48 insertions(+), 58 deletions(-) diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 981e6a879..cb5410cac 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -189,8 +189,8 @@ init([]) -> end}. handle_call({update_config, Conf}, _, State) -> - State2 = update_config(State, Conf), - _ = emqx:update_config([?APP], Conf), + {ok, Config} = emqx:update_config([?APP], Conf), + State2 = update_config(State, maps:get(config, Config)), {reply, ok, State2}; handle_call({wait_semaphore, Id}, From, #{wait_quotas := Waits} = State) -> diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 34e75e567..704d12deb 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -137,11 +137,7 @@ config(put, Req) -> try {ok, Body, _} = cowboy_req:read_body(Req), Cfg = emqx_json:decode(Body), - {ok, RawConf} = hocon:binary(jsx:encode(#{<<"mqtt_retainer">> => Cfg}), - #{format => richmap}), - RichConf = hocon_schema:check(emqx_retainer_schema, RawConf, #{atom_key => true}), - #{mqtt_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), - emqx_retainer:update_config(Conf), + emqx_retainer:update_config(Cfg), {200, #{<<"content-type">> => <<"text/plain">>}, <<"Update configs successfully">>} catch _:Reason:_ -> {400, diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index a2efd0357..0348e065a 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl @@ -26,59 +26,38 @@ all() -> emqx_ct:all(?MODULE). +-define(BASE_CONF, <<""" +emqx_retainer { + enable = true + msg_clear_interval = 0s + msg_expiry_interval = 0s + max_payload_size = 1MB + flow_control { + max_read_number = 0 + msg_deliver_quota = 0 + quota_release_interval = 0s + } + config { + type = built_in_database + storage_type = ram + max_retained_messages = 0 + } + }""">>). + %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- init_per_suite(Config) -> - application:stop(emqx_retainer), - emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), + ok = emqx_config:init_load(emqx_retainer_schema, ?BASE_CONF), + emqx_ct_helpers:start_apps([emqx_retainer]), Config. end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([emqx_retainer]). - -init_per_testcase(TestCase, Config) -> - emqx_retainer:clean(), - DefaultCfg = new_emqx_retainer_conf(), - NewCfg = case TestCase of - t_message_expiry_2 -> - DefaultCfg#{msg_expiry_interval := 2000}; - t_flow_control -> - DefaultCfg#{flow_control := #{max_read_number => 1, - msg_deliver_quota => 1, - quota_release_interval => timer:seconds(1)}}; - _ -> - DefaultCfg - end, - emqx_retainer:update_config(NewCfg), - application:ensure_all_started(emqx_retainer), - Config. - -set_special_configs(emqx_retainer) -> - init_emqx_retainer_conf(); -set_special_configs(_) -> - ok. - -init_emqx_retainer_conf() -> - emqx_config:put([?APP], new_emqx_retainer_conf()). - -new_emqx_retainer_conf() -> - #{enable => true, - msg_expiry_interval => 0, - msg_clear_interval => 0, - config => #{type => built_in_database, - max_retained_messages => 0, - storage_type => ram}, - flow_control => #{max_read_number => 0, - msg_deliver_quota => 0, - quota_release_interval => 0}, - max_payload_size => 1024 * 1024}. - %%-------------------------------------------------------------------- %% Test Cases %%-------------------------------------------------------------------- - t_store_and_clean(_) -> {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), {ok, _} = emqtt:connect(C1), @@ -184,13 +163,14 @@ t_message_expiry(_) -> ok = emqtt:disconnect(C1). t_message_expiry_2(_) -> + emqx_retainer:update_config(#{<<"msg_expiry_interval">> => <<"2s">>}), {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), {ok, _} = emqtt:connect(C1), emqtt:publish(C1, <<"retained">>, <<"expire">>, [{qos, 0}, {retain, true}]), {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]), ?assertEqual(1, length(receive_messages(1))), - timer:sleep(3000), + timer:sleep(4000), {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]), ?assertEqual(0, length(receive_messages(1))), {ok, #{}, [0]} = emqtt:unsubscribe(C1, <<"retained">>), @@ -216,6 +196,9 @@ t_clean(_) -> ok = emqtt:disconnect(C1). t_flow_control(_) -> + emqx_retainer:update_config(#{<<"flow_control">> => #{<<"max_read_number">> => 1, + <<"msg_deliver_quota">> => 1, + <<"quota_release_interval">> => <<"1s">>}}), {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), {ok, _} = emqtt:connect(C1), emqtt:publish(C1, <<"retained/0">>, <<"this is a retained message 0">>, [{qos, 0}, {retain, true}]), diff --git a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl b/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl index 70c8a0554..cba40de69 100644 --- a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl @@ -21,27 +21,38 @@ -include_lib("eunit/include/eunit.hrl"). +-define(BASE_CONF, <<""" +emqx_retainer { + enable = true + msg_clear_interval = 0s + msg_expiry_interval = 0s + max_payload_size = 1MB + flow_control { + max_read_number = 0 + msg_deliver_quota = 0 + quota_release_interval = 0s + } + config { + type = built_in_database + storage_type = ram + max_retained_messages = 0 + } + }""">>). + all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_retainer_schema, ?BASE_CONF), %% Meck emqtt ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), %% Start Apps - emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), + emqx_ct_helpers:start_apps([emqx_retainer]), Config. end_per_suite(_Config) -> ok = meck:unload(emqtt), emqx_ct_helpers:stop_apps([emqx_retainer]). -%%-------------------------------------------------------------------- -%% Helpers -%%-------------------------------------------------------------------- -set_special_configs(emqx_retainer) -> - emqx_retainer_SUITE:init_emqx_retainer_conf(); -set_special_configs(_) -> - ok. - client_info(Key, Client) -> maps:get(Key, maps:from_list(emqtt:info(Client)), undefined). From 32a84b3aba3d5f31fbd4b0fde005419e227e848c Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 23 Aug 2021 16:39:14 +0800 Subject: [PATCH 095/306] fix: clients api node params --- apps/emqx_management/src/emqx_mgmt_api_clients.erl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 4c7479eea..fe83719d2 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -514,8 +514,15 @@ subscriptions(get, Request) -> %% api apply list(Params) -> - Response = emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun), - {200, Response}. + case proplists:get_value(<<"node">>, Params, undefined) of + undefined -> + Response = emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun), + {200, Response}; + Node1 -> + Node = binary_to_atom(Node1, utf8), + Response = emqx_mgmt_api:node_query(Node, proplists:delete(<<"node">>, Params), ?CLIENT_QS_SCHEMA, ?query_fun), + {200, Response} + end. lookup(#{clientid := ClientID}) -> case emqx_mgmt:lookup_client({clientid, ClientID}, ?format_fun) of From 8125ec7d0849dc8f1533476518bcdceeaa275439 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 24 Aug 2021 10:52:18 +0800 Subject: [PATCH 096/306] feat: topic metrics api (#5520) --- apps/emqx_modules/etc/emqx_modules.conf | 2 +- apps/emqx_modules/src/emqx_topic_metrics.erl | 188 +++++++-- .../src/emqx_topic_metrics_api.erl | 378 ++++++++++-------- .../test/emqx_topic_metrics_SUITE.erl | 12 +- apps/emqx_rule_engine/src/emqx_rule_funcs.erl | 5 + 5 files changed, 375 insertions(+), 210 deletions(-) diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index 7b6f60cf1..3bbf8b52c 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -24,7 +24,7 @@ event_message { } topic_metrics { - topics = ["topic/#"] + topics = [] } rewrite { diff --git a/apps/emqx_modules/src/emqx_topic_metrics.erl b/apps/emqx_modules/src/emqx_topic_metrics.erl index fa226c9a0..3a7e5e3c0 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics.erl @@ -37,12 +37,17 @@ , disable/0 ]). --export([ metrics/1 +-export([ max_limit/0]). + +-export([ metrics/0 + , metrics/1 , register/1 - , unregister/1 - , unregister_all/0 + , deregister/1 + , deregister_all/0 , is_registered/1 , all_registered_topics/0 + , reset/0 + , reset/1 ]). %% gen_server callbacks @@ -92,6 +97,9 @@ %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ +max_limit() -> + ?MAX_TOPICS. + enable() -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}), emqx_hooks:put('message.dropped', {?MODULE, on_message_dropped, []}), @@ -100,7 +108,8 @@ enable() -> disable() -> emqx_hooks:del('message.publish', {?MODULE, on_message_publish}), emqx_hooks:del('message.dropped', {?MODULE, on_message_dropped}), - emqx_hooks:del('message.delivered', {?MODULE, on_message_delivered}). + emqx_hooks:del('message.delivered', {?MODULE, on_message_delivered}), + deregister_all(). on_message_publish(#message{topic = Topic, qos = QoS}) -> case is_registered(Topic) of @@ -143,77 +152,101 @@ start_link() -> stop() -> gen_server:stop(?MODULE). +metrics() -> + [format(TopicMetrics) || TopicMetrics <- ets:tab2list(?TAB)]. + metrics(Topic) -> case ets:lookup(?TAB, Topic) of [] -> {error, topic_not_found}; - [{Topic, CRef}] -> - lists:foldl(fun(Metric, Acc) -> - [{to_count(Metric), counters:get(CRef, metric_idx(Metric))}, - {to_rate(Metric), rate(Topic, Metric)} | Acc] - end, [], ?TOPIC_METRICS) + [TopicMetrics] -> + format(TopicMetrics) end. register(Topic) when is_binary(Topic) -> gen_server:call(?MODULE, {register, Topic}). -unregister(Topic) when is_binary(Topic) -> - gen_server:call(?MODULE, {unregister, Topic}). +deregister(Topic) when is_binary(Topic) -> + gen_server:call(?MODULE, {deregister, Topic}). -unregister_all() -> - gen_server:call(?MODULE, {unregister, all}). +deregister_all() -> + gen_server:call(?MODULE, {deregister, all}). is_registered(Topic) -> ets:member(?TAB, Topic). all_registered_topics() -> - [Topic || {Topic, _CRef} <- ets:tab2list(?TAB)]. + [Topic || {Topic, _} <- ets:tab2list(?TAB)]. + +reset(Topic) -> + case is_registered(Topic) of + true -> + gen_server:call(?MODULE, {reset, Topic}); + false -> + {error, topic_not_found} + end. + +reset() -> + gen_server:call(?MODULE, {reset, all}). %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- -init([_Opts]) -> +init([Opts]) -> erlang:process_flag(trap_exit, true), ok = emqx_tables:new(?TAB, [{read_concurrency, true}]), erlang:send_after(timer:seconds(?TICKING_INTERVAL), self(), ticking), - {ok, #state{speeds = #{}}, hibernate}. + Fun = + fun(Topic, CurrentSpeeds) -> + case do_register(Topic, CurrentSpeeds) of + {ok, NSpeeds} -> + NSpeeds; + {error, already_existed} -> + CurrentSpeeds; + {error, quota_exceeded} -> + error("max topic metrics quota exceeded") + end + end, + {ok, #state{speeds = lists:foldl(Fun, #{}, maps:get(topics, Opts, []))}, hibernate}. handle_call({register, Topic}, _From, State = #state{speeds = Speeds}) -> - case is_registered(Topic) of - true -> - {reply, {error, already_existed}, State}; - false -> - case number_of_registered_topics() < ?MAX_TOPICS of - true -> - CRef = counters:new(counters_size(), [write_concurrency]), - true = ets:insert(?TAB, {Topic, CRef}), - [counters:put(CRef, Idx, 0) || Idx <- lists:seq(1, counters_size())], - NSpeeds = lists:foldl(fun(Metric, Acc) -> - maps:put({Topic, Metric}, #speed{}, Acc) - end, Speeds, ?TOPIC_METRICS), - {reply, ok, State#state{speeds = NSpeeds}}; - false -> - {reply, {error, quota_exceeded}, State} - end + case do_register(Topic, Speeds) of + {ok, NSpeeds} -> + {reply, ok, State#state{speeds = NSpeeds}}; + Error -> + {reply, Error, State} end; -handle_call({unregister, all}, _From, State) -> - [delete_counters(Topic) || {Topic, _CRef} <- ets:tab2list(?TAB)], +handle_call({deregister, all}, _From, State) -> + true = ets:delete_all_objects(?TAB), + update_config([]), {reply, ok, State#state{speeds = #{}}}; -handle_call({unregister, Topic}, _From, State = #state{speeds = Speeds}) -> +handle_call({deregister, Topic}, _From, State = #state{speeds = Speeds}) -> case is_registered(Topic) of false -> {reply, ok, State}; true -> - ok = delete_counters(Topic), + true = ets:delete(?TAB, Topic), NSpeeds = lists:foldl(fun(Metric, Acc) -> maps:remove({Topic, Metric}, Acc) end, Speeds, ?TOPIC_METRICS), + remove_topic_config(Topic), {reply, ok, State#state{speeds = NSpeeds}} end; +handle_call({reset, all}, _From, State = #state{speeds = Speeds}) -> + Fun = + fun(T, NSpeeds) -> + reset_topic(T, NSpeeds) + end, + {reply, ok, State#state{speeds = lists:foldl(Fun, Speeds, ets:tab2list(?TAB))}}; + +handle_call({reset, Topic}, _From, State = #state{speeds = Speeds}) -> + NSpeeds = reset_topic(Topic, Speeds), + {reply, ok, State#state{speeds = NSpeeds}}; + handle_call({get_rates, Topic, Metric}, _From, State = #state{speeds = Speeds}) -> case is_registered(Topic) of false -> @@ -249,9 +282,83 @@ handle_info(Info, State) -> terminate(_Reason, _State) -> ok. +reset_topic({Topic, Data}, Speeds) -> + CRef = maps:get(counter_ref, Data), + ok = reset_counter(CRef), + ResetTime = emqx_rule_funcs:now_rfc3339(), + true = ets:insert(?TAB, {Topic, Data#{reset_time => ResetTime}}), + Fun = + fun(Metric, CurrentSpeeds) -> + maps:put({Topic, Metric}, #speed{}, CurrentSpeeds) + end, + lists:foldl(Fun, Speeds, ?TOPIC_METRICS); +reset_topic(Topic, Speeds) -> + T = hd(ets:lookup(?TAB, Topic)), + reset_topic(T, Speeds). + %%------------------------------------------------------------------------------ %% Internal Functions %%------------------------------------------------------------------------------ +do_register(Topic, Speeds) -> + case is_registered(Topic) of + true -> + {error, already_existed}; + false -> + case number_of_registered_topics() < ?MAX_TOPICS of + true -> + CreateTime = emqx_rule_funcs:now_rfc3339(), + CRef = counters:new(counters_size(), [write_concurrency]), + ok = reset_counter(CRef), + Data = #{ + counter_ref => CRef, + create_time => CreateTime}, + true = ets:insert(?TAB, {Topic, Data}), + NSpeeds = lists:foldl(fun(Metric, Acc) -> + maps:put({Topic, Metric}, #speed{}, Acc) + end, Speeds, ?TOPIC_METRICS), + add_topic_config(Topic), + {ok, NSpeeds}; + false -> + {error, quota_exceeded} + end + end. + +format({Topic, Data}) -> + CRef = maps:get(counter_ref, Data), + Fun = + fun(Key, Metrics) -> + CounterKey = to_count(Key), + Counter = counters:get(CRef, metric_idx(Key)), + RateKey = to_rate(Key), + Rate = emqx_rule_funcs:float(rate(Topic, Key), 4), + maps:put(RateKey, Rate, maps:put(CounterKey, Counter, Metrics)) + end, + Metrics = lists:foldl(Fun, #{}, ?TOPIC_METRICS), + CreateTime = maps:get(create_time, Data), + TopicMetrics = #{ + topic => Topic, + metrics => Metrics, + create_time => CreateTime + }, + case maps:get(reset_time, Data, undefined) of + undefined -> + TopicMetrics; + ResetTime -> + TopicMetrics#{reset_time => ResetTime} + end. + +remove_topic_config(Topic) when is_binary(Topic) -> + Topics = emqx_config:get_raw([<<"topic_metrics">>, <<"topics">>], []) -- [Topic], + update_config(Topics). + +add_topic_config(Topic) when is_binary(Topic) -> + Topics = emqx_config:get_raw([<<"topic_metrics">>, <<"topics">>], []) ++ [Topic], + update_config(Topics). + +update_config(Topics) when is_list(Topics) -> + Opts = emqx_config:get_raw([<<"topic_metrics">>], #{}), + {ok, _} = emqx:update_config([topic_metrics], maps:put(<<"topics">>, Topics, Opts)), + ok. try_inc(Topic, Metric) -> _ = inc(Topic, Metric), @@ -277,7 +384,8 @@ val(Topic, Metric) -> case ets:lookup(?TAB, Topic) of [] -> {error, topic_not_found}; - [{Topic, CRef}] -> + [{Topic, Data}] -> + CRef = maps:get(counter_ref, Data), case metric_idx(Metric) of {error, invalid_metric} -> {error, invalid_metric}; @@ -344,14 +452,14 @@ to_rate('messages.qos2.out') -> to_rate('messages.dropped') -> 'messages.dropped.rate'. -delete_counters(Topic) -> - true = ets:delete(?TAB, Topic), +reset_counter(CRef) -> + [counters:put(CRef, Idx, 0) || Idx <- lists:seq(1, counters_size())], ok. get_counters(Topic) -> case ets:lookup(?TAB, Topic) of [] -> {error, topic_not_found}; - [{Topic, CRef}] -> CRef + [{Topic, Data}] -> maps:get(counter_ref, Data) end. counters_size() -> diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index 1a2365703..1f16b3759 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -13,195 +13,241 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- - +%% TODO: refactor uri path -module(emqx_topic_metrics_api). -% -rest_api(#{name => list_all_topic_metrics, -% method => 'GET', -% path => "/topic-metrics", -% func => list, -% descr => "A list of all topic metrics of all nodes in the cluster"}). +-behavior(minirest_api). -% -rest_api(#{name => list_topic_metrics, -% method => 'GET', -% path => "/topic-metrics/:bin:topic", -% func => list, -% descr => "A list of specfied topic metrics of all nodes in the cluster"}). +-import(emqx_mgmt_util, [ request_body_schema/1 + , response_schema/1 + , response_schema/2 + , response_array_schema/2 + , response_error_schema/2 + ]). -% -rest_api(#{name => register_topic_metrics, -% method => 'POST', -% path => "/topic-metrics", -% func => register, -% descr => "Register topic metrics"}). +-export([api_spec/0]). -% -rest_api(#{name => unregister_all_topic_metrics, -% method => 'DELETE', -% path => "/topic-metrics", -% func => unregister, -% descr => "Unregister all topic metrics"}). +-export([ list_topic/2 + , list_topic_metrics/2 + , operate_topic_metrics/2 + , reset_all_topic_metrics/2 + , reset_topic_metrics/2 + ]). -% -rest_api(#{name => unregister_topic_metrics, -% method => 'DELETE', -% path => "/topic-metrics/:bin:topic", -% func => unregister, -% descr => "Unregister topic metrics"}). +-define(ERROR_TOPIC, 'ERROR_TOPIC'). -% -export([ list/2 -% , register/2 -% , unregister/2 -% ]). +-define(EXCEED_LIMIT, 'EXCEED_LIMIT'). -% -export([ get_topic_metrics/2 -% , register_topic_metrics/2 -% , unregister_topic_metrics/2 -% , unregister_all_topic_metrics/1 -% ]). +-define(BAD_REQUEST, 'BAD_REQUEST'). -% list(#{topic := Topic0}, _Params) -> -% execute_when_enabled(fun() -> -% Topic = emqx_mgmt_util:urldecode(Topic0), -% case safe_validate(Topic) of -% true -> -% case get_topic_metrics(Topic) of -% {error, Reason} -> return({error, Reason}); -% Metrics -> return({ok, maps:from_list(Metrics)}) -% end; -% false -> -% return({error, invalid_topic_name}) -% end -% end); +api_spec() -> + { + [ + list_topic_api(), + list_topic_metrics_api(), + get_topic_metrics_api(), + reset_all_topic_metrics_api(), + reset_topic_metrics_api() + ], + [ + topic_metrics_schema() + ] + }. -% list(_Bindings, _Params) -> -% execute_when_enabled(fun() -> -% case get_all_topic_metrics() of -% {error, Reason} -> return({error, Reason}); -% Metrics -> return({ok, Metrics}) -% end -% end). +topic_metrics_schema() -> + #{ + topic_metrics => #{ + type => object, + properties => #{ + topic => #{type => string}, + create_time => #{ + type => string, + description => <<"Date time, rfc3339">> + }, + reset_time => #{ + type => string, + description => <<"Nullable. Date time, rfc3339.">> + }, + metrics => #{ + type => object, + properties => #{ + 'messages.dropped.count' => #{type => integer}, + 'messages.dropped.rate' => #{type => number}, + 'messages.in.count' => #{type => integer}, + 'messages.in.rate' => #{type => number}, + 'messages.out.count' => #{type => integer}, + 'messages.out.rate' => #{type => number}, + 'messages.qos0.in.count' => #{type => integer}, + 'messages.qos0.in.rate' => #{type => number}, + 'messages.qos0.out.count' => #{type => integer}, + 'messages.qos0.out.rate' => #{type => number}, + 'messages.qos1.in.count' => #{type => integer}, + 'messages.qos1.in.rate' => #{type => number}, + 'messages.qos1.out.count' => #{type => integer}, + 'messages.qos1.out.rate' => #{type => number}, + 'messages.qos2.in.count' => #{type => integer}, + 'messages.qos2.in.rate' => #{type => number}, + 'messages.qos2.out.count' => #{type => integer}, + 'messages.qos2.out.rate' => #{type => number} + } + } + } + } + }. -% register(_Bindings, Params) -> -% execute_when_enabled(fun() -> -% case proplists:get_value(<<"topic">>, Params) of -% undefined -> -% return({error, missing_required_params}); -% Topic -> -% case safe_validate(Topic) of -% true -> -% register_topic_metrics(Topic), -% return(ok); -% false -> -% return({error, invalid_topic_name}) -% end -% end -% end). +list_topic_api() -> + Path = "/mqtt/topic_metrics", + TopicSchema = #{ + type => object, + properties => #{ + topic => #{ + type => string}}}, + MetaData = #{ + get => #{ + description => <<"List topic">>, + responses => #{ + <<"200">> => + response_array_schema(<<"List topic">>, TopicSchema)}}}, + {Path, MetaData, list_topic}. -% unregister(Bindings, _Params) when map_size(Bindings) =:= 0 -> -% execute_when_enabled(fun() -> -% unregister_all_topic_metrics(), -% return(ok) -% end); +list_topic_metrics_api() -> + Path = "/mqtt/topic_metrics/metrics", + MetaData = #{ + get => #{ + description => <<"List topic metrics">>, + responses => #{ + <<"200">> => + response_array_schema(<<"List topic metrics">>, topic_metrics)}}}, + {Path, MetaData, list_topic_metrics}. -% unregister(#{topic := Topic0}, _Params) -> -% execute_when_enabled(fun() -> -% Topic = emqx_mgmt_util:urldecode(Topic0), -% case safe_validate(Topic) of -% true -> -% unregister_topic_metrics(Topic), -% return(ok); -% false -> -% return({error, invalid_topic_name}) -% end -% end). +get_topic_metrics_api() -> + Path = "/mqtt/topic_metrics/metrics/:topic", + MetaData = #{ + get => #{ + description => <<"List topic metrics">>, + parameters => [topic_param()], + responses => #{ + <<"200">> => + response_schema(<<"List topic metrics">>, topic_metrics)}}, + put => #{ + description => <<"Register topic metrics">>, + parameters => [topic_param()], + responses => #{ + <<"200">> => + response_schema(<<"Register topic metrics">>), + <<"409">> => + response_error_schema(<<"Topic metrics max limit">>, [?EXCEED_LIMIT]), + <<"400">> => + response_error_schema(<<"Topic metrics already exist">>, [?BAD_REQUEST])}}, + delete => #{ + description => <<"Deregister topic metrics">>, + parameters => [topic_param()], + responses => #{ + <<"200">> => + response_schema(<<"Deregister topic metrics">>)}}}, + {Path, MetaData, operate_topic_metrics}. -% execute_when_enabled(Fun) -> -% case emqx_modules:find_module(topic_metrics) of -% true -> -% Fun(); -% false -> -% return({error, module_not_loaded}) -% end. +reset_all_topic_metrics_api() -> + Path = "/mqtt/topic_metrics/reset", + MetaData = #{ + put => #{ + description => <<"Reset all topic metrics">>, + responses => #{ + <<"200">> => + response_schema(<<"Reset all topic metrics">>)}}}, + {Path, MetaData, reset_all_topic_metrics}. -% safe_validate(Topic) -> -% try emqx_topic:validate(name, Topic) of -% true -> true -% catch -% error:_Error -> -% false -% end. +reset_topic_metrics_api() -> + Path = "/mqtt/topic_metrics/reset/:topic", + MetaData = #{ + put => #{ + description => <<"Reset topic metrics">>, + parameters => [topic_param()], + responses => #{ + <<"200">> => + response_schema(<<"Reset topic metrics">>)}}}, + {Path, MetaData, reset_topic_metrics}. -% get_all_topic_metrics() -> -% lists:foldl(fun(Topic, Acc) -> -% case get_topic_metrics(Topic) of -% {error, _Reason} -> -% Acc; -% Metrics -> -% [#{topic => Topic, metrics => Metrics} | Acc] -% end -% end, [], emqx_mod_topic_metrics:all_registered_topics()). +topic_param() -> + #{ + name => topic, + in => path, + required => true, + schema => #{type => string} + }. -% get_topic_metrics(Topic) -> -% lists:foldl(fun(Node, Acc) -> -% case get_topic_metrics(Node, Topic) of -% {error, _Reason} -> -% Acc; -% Metrics -> -% case Acc of -% [] -> Metrics; -% _ -> -% lists:foldl(fun({K, V}, Acc0) -> -% [{K, V + proplists:get_value(K, Metrics, 0)} | Acc0] -% end, [], Acc) -% end -% end -% end, [], ekka_mnesia:running_nodes()). +topic_param(Request) -> + cowboy_req:binding(topic, Request). -% get_topic_metrics(Node, Topic) when Node =:= node() -> -% emqx_mod_topic_metrics:metrics(Topic); -% get_topic_metrics(Node, Topic) -> -% rpc_call(Node, get_topic_metrics, [Node, Topic]). +%%-------------------------------------------------------------------- +%% api callback +list_topic(get, _) -> + list_topics(). -% register_topic_metrics(Topic) -> -% Results = [register_topic_metrics(Node, Topic) || Node <- ekka_mnesia:running_nodes()], -% case lists:any(fun(Item) -> Item =:= ok end, Results) of -% true -> ok; -% false -> lists:last(Results) -% end. +list_topic_metrics(get, _) -> + list_metrics(). -% register_topic_metrics(Node, Topic) when Node =:= node() -> -% emqx_mod_topic_metrics:register(Topic); -% register_topic_metrics(Node, Topic) -> -% rpc_call(Node, register_topic_metrics, [Node, Topic]). +operate_topic_metrics(Method, Request) -> + Topic = topic_param(Request), + case Method of + get -> + get_metrics(Topic); + put -> + register(Topic); + delete -> + deregister(Topic) + end. -% unregister_topic_metrics(Topic) -> -% Results = [unregister_topic_metrics(Node, Topic) || Node <- ekka_mnesia:running_nodes()], -% case lists:any(fun(Item) -> Item =:= ok end, Results) of -% true -> ok; -% false -> lists:last(Results) -% end. +reset_all_topic_metrics(put, _) -> + reset(). -% unregister_topic_metrics(Node, Topic) when Node =:= node() -> -% emqx_mod_topic_metrics:unregister(Topic); -% unregister_topic_metrics(Node, Topic) -> -% rpc_call(Node, unregister_topic_metrics, [Node, Topic]). +reset_topic_metrics(put, Request) -> + Topic = topic_param(Request), + reset(Topic). -% unregister_all_topic_metrics() -> -% Results = [unregister_all_topic_metrics(Node) || Node <- ekka_mnesia:running_nodes()], -% case lists:any(fun(Item) -> Item =:= ok end, Results) of -% true -> ok; -% false -> lists:last(Results) -% end. +%%-------------------------------------------------------------------- +%% api apply +list_topics() -> + {200, emqx_topic_metrics:all_registered_topics()}. -% unregister_all_topic_metrics(Node) when Node =:= node() -> -% emqx_mod_topic_metrics:unregister_all(); -% unregister_all_topic_metrics(Node) -> -% rpc_call(Node, unregister_topic_metrics, [Node]). +list_metrics() -> + {200, emqx_topic_metrics:metrics()}. -% rpc_call(Node, Fun, Args) -> -% case rpc:call(Node, ?MODULE, Fun, Args) of -% {badrpc, Reason} -> {error, Reason}; -% Res -> Res -% end. +register(Topic) -> + case emqx_topic_metrics:register(Topic) of + {error, quota_exceeded} -> + Message = list_to_binary(io_lib:format("Max topic metrics count is ~p", + [emqx_topic_metrics:max_limit()])), + {409, #{code => ?EXCEED_LIMIT, message => Message}}; + {error, already_existed} -> + Message = list_to_binary(io_lib:format("Topic ~p already registered", [Topic])), + {400, #{code => ?BAD_REQUEST, message => Message}}; + ok -> + {200} + end. -% return(_) -> -% %% TODO: V5 API -% ok. +deregister(Topic) -> + _ = emqx_topic_metrics:deregister(Topic), + {200}. + +get_metrics(Topic) -> + case emqx_topic_metrics:metrics(Topic) of + {error, topic_not_found} -> + Message = list_to_binary(io_lib:format("Topic ~p not found", [Topic])), + {404, #{code => ?ERROR_TOPIC, message => Message}}; + Metrics -> + {200, Metrics} + end. + +reset() -> + ok = emqx_topic_metrics:reset(), + {200}. + +reset(Topic) -> + case emqx_topic_metrics:reset(Topic) of + {error, topic_not_found} -> + Message = list_to_binary(io_lib:format("Topic ~p not found", [Topic])), + {404, #{code => ?ERROR_TOPIC, message => Message}}; + ok -> + {200} + end. diff --git a/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl b/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl index 5d1c5f84a..958131716 100644 --- a/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl +++ b/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl @@ -19,6 +19,11 @@ -compile(export_all). -compile(nowarn_export_all). + +-define(TOPIC, <<""" +topic_metrics: { + topics : []}""">>). + -include_lib("eunit/include/eunit.hrl"). all() -> emqx_ct:all(?MODULE). @@ -26,6 +31,7 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([emqx_modules]), + ok = emqx_config:init_load(emqx_modules_schema, ?TOPIC), Config. end_per_suite(_Config) -> @@ -43,7 +49,7 @@ t_nonexistent_topic_metrics(_) -> ?assertEqual({error, invalid_metric}, emqx_topic_metrics:inc(<<"a/b/c">>, 'invalid.metrics')), ?assertEqual({error, invalid_metric}, emqx_topic_metrics:rate(<<"a/b/c">>, 'invalid.metrics')), % ?assertEqual({error, invalid_metric}, emqx_topic_metrics:rates(<<"a/b/c">>, 'invalid.metrics')), - emqx_topic_metrics:unregister(<<"a/b/c">>), + emqx_topic_metrics:deregister(<<"a/b/c">>), emqx_topic_metrics:disable(). t_topic_metrics(_) -> @@ -60,7 +66,7 @@ t_topic_metrics(_) -> ?assertEqual(1, emqx_topic_metrics:val(<<"a/b/c">>, 'messages.in')), ?assert(emqx_topic_metrics:rate(<<"a/b/c">>, 'messages.in') =:= 0), % ?assert(emqx_topic_metrics:rates(<<"a/b/c">>, 'messages.in') =:= #{long => 0,medium => 0,short => 0}), - emqx_topic_metrics:unregister(<<"a/b/c">>), + emqx_topic_metrics:deregister(<<"a/b/c">>), emqx_topic_metrics:disable(). t_hook(_) -> @@ -91,5 +97,5 @@ t_hook(_) -> ?assertEqual(1, emqx_topic_metrics:val(<<"a/b/c">>, 'messages.out')), ?assertEqual(1, emqx_topic_metrics:val(<<"a/b/c">>, 'messages.qos0.out')), ?assertEqual(1, emqx_topic_metrics:val(<<"a/b/c">>, 'messages.dropped')), - emqx_topic_metrics:unregister(<<"a/b/c">>), + emqx_topic_metrics:deregister(<<"a/b/c">>), emqx_topic_metrics:disable(). diff --git a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl index a96ee7a62..89fdde579 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl @@ -93,6 +93,7 @@ , bool/1 , int/1 , float/1 + , float/2 , map/1 , bin2hexstr/1 , hexstr2bin/1 @@ -516,6 +517,10 @@ int(Data) -> float(Data) -> emqx_rule_utils:float(Data). +float(Data, Decimals) when Decimals > 0 -> + Data1 = ?MODULE:float(Data), + list_to_float(float_to_list(Data1, [{decimals, Decimals}])). + map(Data) -> emqx_rule_utils:map(Data). From 47378e0e96e7b983046127cfc364a10ec59fb780 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 00:32:51 +0800 Subject: [PATCH 097/306] refactor(schema-utils): refactor mgmt swagger schema utils --- .../emqx_dashboard/src/emqx_dashboard_api.erl | 167 +++++--------- .../src/emqx_dashboard_monitor_api.erl | 12 +- .../src/emqx_mgmt_api_alarms.erl | 44 ++-- .../src/emqx_mgmt_api_apps.erl | 85 +++---- .../src/emqx_mgmt_api_clients.erl | 32 +-- .../src/emqx_mgmt_api_configs.erl | 31 +-- .../src/emqx_mgmt_api_listeners.erl | 127 +++++------ .../src/emqx_mgmt_api_metrics.erl | 9 +- .../src/emqx_mgmt_api_nodes.erl | 143 +++++------- .../src/emqx_mgmt_api_publish.erl | 99 ++------- .../src/emqx_mgmt_api_routes.erl | 52 ++--- .../src/emqx_mgmt_api_status.erl | 5 +- .../src/emqx_mgmt_api_subscriptions.erl | 128 +++++------ apps/emqx_management/src/emqx_mgmt_http.erl | 1 + apps/emqx_management/src/emqx_mgmt_util.erl | 208 +++++++++++------- apps/emqx_modules/src/emqx_delayed_api.erl | 138 +++++------- .../src/emqx_event_message_api.erl | 59 +---- apps/emqx_modules/src/emqx_rewrite_api.erl | 47 ++-- apps/emqx_modules/src/emqx_telemetry_api.erl | 112 +++------- .../src/emqx_prometheus_api.erl | 22 +- apps/emqx_retainer/src/emqx_retainer.erl | 2 +- apps/emqx_retainer/src/emqx_retainer_api.erl | 176 +++++++-------- apps/emqx_statsd/src/emqx_statsd_api.erl | 21 +- 23 files changed, 689 insertions(+), 1031 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 422d246f4..a7d0adade 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -30,10 +30,12 @@ -include("emqx_dashboard.hrl"). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , request_body_schema/1 - , response_array_schema/2 +-import(emqx_mgmt_util, [ schema/1 + , object_schema/1 + , object_schema/2 + , object_array_schema/1 + , bad_request/0 + , properties/1 ]). -export([api_spec/0]). @@ -58,95 +60,59 @@ api_spec() -> []}. login_api() -> - AuthSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - password => #{ - type => string, - description => <<"Password">>}}}, - TokenSchema = #{ - type => object, - properties => #{ - token => #{ - type => string, - description => <<"JWT Token">>}, - license => #{ - type => object, - properties => #{ - edition => #{ - type => string, - enum => [community, enterprise]}}}, - version => #{ - type => string}}}, + AuthProps = properties([{username, string, <<"Username">>}, + {password, string, <<"Password">>}]), + TokenProps = properties([{token, string, <<"JWT Token">>}, + {license, object, [{edition, string, <<"License">>, [community, enterprise]}]}, + {version, string}]), Metadata = #{ post => #{ + tags => [dashboard], description => <<"Dashboard Auth">>, - 'requestBody' => request_body_schema(AuthSchema), + 'requestBody' => object_schema(AuthProps), responses => #{ <<"200">> => - response_schema(<<"Dashboard Auth successfully">>, TokenSchema), + object_schema(TokenProps, <<"Dashboard Auth successfully">>), <<"401">> => unauthorized_request() }, security => [] } }, {"/login", Metadata, login}. + logout_api() -> - AuthSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}}}, + LogoutProps = properties([{username, string, <<"Username">>}]), Metadata = #{ post => #{ + tags => [dashboard], description => <<"Dashboard Auth">>, - 'requestBody' => request_body_schema(AuthSchema), + 'requestBody' => object_schema(LogoutProps), responses => #{ - <<"200">> => - response_schema(<<"Dashboard Auth successfully">>)} + <<"200">> => schema(<<"Dashboard Auth successfully">>) + } } }, {"/logout", Metadata, logout}. users_api() -> - ShowSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - tag => #{ - type => string, - description => <<"Tag">>}}}, - CreateSchema = #{ - type => object, - properties => #{ - username => #{ - type => string, - description => <<"Username">>}, - password => #{ - type => string, - description => <<"Password">>}, - tag => #{ - type => string, - description => <<"Tag">>}}}, + BaseProps = properties([{username, string, <<"Username">>}, + {password, string, <<"Password">>}, + {tag, string, <<"Tag">>}]), Metadata = #{ get => #{ + tags => [dashboard], description => <<"Get dashboard users">>, responses => #{ - <<"200">> => response_array_schema(<<"">>, ShowSchema) + <<"200">> => object_array_schema(maps:without([password], BaseProps)) } }, post => #{ + tags => [dashboard], description => <<"Create dashboard users">>, - 'requestBody' => request_body_schema(CreateSchema), + 'requestBody' => object_schema(BaseProps), responses => #{ - <<"200">> => response_schema(<<"Create Users successfully">>), + <<"200">> => schema(<<"Create Users successfully">>), <<"400">> => bad_request() } } @@ -156,26 +122,21 @@ users_api() -> user_api() -> Metadata = #{ delete => #{ + tags => [dashboard], description => <<"Delete dashboard users">>, - parameters => [path_param_username()], + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Delete User successfully">>), + <<"200">> => schema(<<"Delete User successfully">>), <<"400">> => bad_request() } }, put => #{ + tags => [dashboard], description => <<"Update dashboard users">>, - parameters => [path_param_username()], - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - tag => #{ - type => string - } - } - }), + parameters => parameters(), + 'requestBody' => object_schema(properties([{tag, string, <<"Tag">>}])), responses => #{ - <<"200">> => response_schema(<<"Update Users successfully">>), + <<"200">> => schema(<<"Update Users successfully">>), <<"400">> => bad_request() } } @@ -185,36 +146,18 @@ user_api() -> change_pwd_api() -> Metadata = #{ put => #{ + tags => [dashboard], description => <<"Update dashboard users password">>, - parameters => [path_param_username()], - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - old_pwd => #{ - type => string - }, - new_pwd => #{ - type => string - } - } - }), + parameters => parameters(), + 'requestBody' => object_schema(properties([old_pwd, new_pwd])), responses => #{ - <<"200">> => response_schema(<<"Update Users password successfully">>), + <<"200">> => schema(<<"Update Users password successfully">>), <<"400">> => bad_request() } } }, {"/users/:username/change_pwd", Metadata, change_pwd}. -path_param_username() -> - #{ - name => username, - in => path, - required => true, - schema => #{type => string}, - example => <<"admin">> - }. - login(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Params = emqx_json:decode(Body, [return_maps]), @@ -292,21 +235,19 @@ change_pwd(put, Request) -> row(#mqtt_admin{username = Username, tags = Tag}) -> #{username => Username, tag => Tag}. -bad_request() -> - response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }). +parameters() -> + [#{ + name => username, + in => path, + required => true, + schema => #{type => string}, + example => <<"admin">> + }]. + unauthorized_request() -> - response_schema(<<"Unauthorized">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string, enum => ['PASSWORD_ERROR', 'USERNAME_ERROR']} - } - }). + object_schema( + properties([{message, string}, + {code, string, <<"Resp Code">>, ['PASSWORD_ERROR','USERNAME_ERROR']} + ]), + <<"Unauthorized">> + ). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 58b95f093..1193dfad1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -8,6 +8,7 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [schema/2]). -export([api_spec/0]). -export([ monitor/2 @@ -47,7 +48,7 @@ monitor_api() -> } ], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data">>, counters_schema())}}}, + <<"200">> => schema(counters_schema(), <<"Monitor count data">>)}}}, {"/monitor", Metadata, monitor}. monitor_nodes_api() -> @@ -56,7 +57,7 @@ monitor_nodes_api() -> description => <<"List monitor data">>, parameters => [path_param_node()], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor count data in node">>, counters_schema())}}}, + <<"200">> => schema(counters_schema(), <<"Monitor count data in node">>)}}}, {"/monitor/nodes/:node", Metadata, monitor_nodes}. monitor_nodes_counters_api() -> @@ -68,7 +69,7 @@ monitor_nodes_counters_api() -> path_param_counter() ], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Monitor single count data in node">>, counter_schema())}}}, + <<"200">> => schema(counter_schema(), <<"Monitor single count data in node">>)}}}, {"/monitor/nodes/:node/counters/:counter", Metadata, monitor_nodes_counters}. monitor_counters_api() -> @@ -80,15 +81,14 @@ monitor_counters_api() -> ], responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Monitor single count data">>, counter_schema())}}}, + schema(counter_schema(), <<"Monitor single count data">>)}}}, {"/monitor/counters/:counter", Metadata, counters}. monitor_current_api() -> Metadata = #{ get => #{ description => <<"Current monitor data">>, responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Current monitor data">>, - current_counters_schema())}}}, + <<"200">> => schema(current_counters_schema(), <<"Current monitor data">>)}}}, {"/monitor/current", Metadata, current_counters}. path_param_node() -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 36a0f3a5b..8f26f9075 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -28,32 +28,22 @@ -define(ACTIVATED_ALARM, emqx_activated_alarm). -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). -api_spec() -> - {[alarms_api()], [alarm_schema()]}. +-import(emqx_mgmt_util, [ object_array_schema/2 + , schema/1 + , properties/1 + ]). -alarm_schema() -> - #{ - alarm => #{ - type => object, - properties => #{ - node => #{ - type => string, - description => <<"Alarm in node">>}, - name => #{ - type => string, - description => <<"Alarm name">>}, - message => #{ - type => string, - description => <<"Alarm readable information">>}, - details => #{ - type => object, - description => <<"Alarm detail">>}, - duration => #{ - type => integer, - description => <<"Alarms duration time; UNIX time stamp">>} - } - } - }. +api_spec() -> + {[alarms_api()], []}. + +properties() -> + properties([ + {node, string, <<"Alarm in node">>}, + {name, string, <<"Alarm name">>}, + {message, string, <<"Alarm readable information">>}, + {details, object}, + {duration, integer, <<"Alarms duration time; UNIX time stamp">>} + ]). alarms_api() -> Metadata = #{ @@ -68,12 +58,12 @@ alarms_api() -> }], responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List all alarms">>, alarm)}}, + object_array_schema(properties(), <<"List all alarms">>)}}, delete => #{ description => <<"Remove all deactivated alarms">>, responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Remove all deactivated alarms ok">>)}}}, + schema(<<"Remove all deactivated alarms ok">>)}}}, {"/alarms", Metadata, alarms}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_apps.erl b/apps/emqx_management/src/emqx_mgmt_api_apps.erl index 2a5f330c4..71dabf4c6 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_apps.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_apps.erl @@ -18,8 +18,17 @@ -behaviour(minirest_api). --export([api_spec/0]). +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , object_array_schema/2 + , error_schema/1 + , error_schema/2 + , properties/1 + ]). +-export([api_spec/0]). -export([ apps/2 , app/2]). @@ -30,48 +39,22 @@ api_spec() -> { [apps_api(), app_api()], - [app_schema(), app_secret_schema()] + [] }. -app_schema() -> - #{app => #{ - type => object, - properties => app_properties()}}. - -app_properties() -> - #{ - app_id => #{ - type => string, - description => <<"App ID">>}, - secret => #{ - type => string, - description => <<"App Secret">>}, - name => #{ - type => string, - description => <<"Dsiplay name">>}, - desc => #{ - type => string, - description => <<"App description">>}, - status => #{ - type => boolean, - description => <<"Enable or disable">>}, - expired => #{ - type => integer, - description => <<"Expired time">>} - }. - -app_secret_schema() -> - #{app_secret => #{ - type => object, - properties => #{ - secret => #{type => string}}}}. +properties() -> + properties([ + {app_id, string, <<"App ID">>}, + {secret, string, <<"App Secret">>}, + {name, string, <<"Dsiplay name">>}, + {desc, string, <<"App description">>}, + {status, boolean, <<"Enable or disable">>}, + {expired, integer, <<"Expired time">>} + ]). %% not export schema app_without_secret_schema() -> - #{ - type => object, - properties => maps:without([secret], app_properties()) - }. + maps:without([secret], properties()). apps_api() -> Metadata = #{ @@ -79,16 +62,20 @@ apps_api() -> description => <<"List EMQ X apps">>, responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"All apps">>, - app_without_secret_schema())}}, + object_array_schema(app_without_secret_schema(), <<"All apps">>) + } + }, post => #{ description => <<"EMQ X create apps">>, - 'requestBody' => emqx_mgmt_util:request_body_schema(<<"app">>), + 'requestBody' => schema(app), responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Create apps">>, app_secret), + schema(app_secret, <<"Create apps">>), <<"400">> => - emqx_mgmt_util:response_error_schema(<<"App ID already exist">>, [?BAD_APP_ID])}}}, + error_schema(<<"App ID already exist">>, [?BAD_APP_ID]) + } + } + }, {"/apps", Metadata, apps}. app_api() -> @@ -102,9 +89,9 @@ app_api() -> schema => #{type => string}}], responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"App id not found">>), + error_schema(<<"App id not found">>), <<"200">> => - emqx_mgmt_util:response_schema(<<"Get App">>, app_without_secret_schema())}}, + object_schema(app_without_secret_schema(), <<"Get App">>)}}, delete => #{ description => <<"EMQ X apps">>, parameters => [#{ @@ -114,7 +101,7 @@ app_api() -> schema => #{type => string} }], responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"Remove app ok">>)}}, + <<"200">> => schema(<<"Remove app ok">>)}}, put => #{ description => <<"EMQ X update apps">>, parameters => [#{ @@ -123,12 +110,12 @@ app_api() -> required => true, schema => #{type => string} }], - 'requestBody' => emqx_mgmt_util:request_body_schema(app_without_secret_schema()), + 'requestBody' => object_schema(app_without_secret_schema()), responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"App id not found">>, [?BAD_APP_ID]), + error_schema(<<"App id not found">>, [?BAD_APP_ID]), <<"200">> => - emqx_mgmt_util:response_schema(<<"Update ok">>, app_without_secret_schema())}}}, + object_schema(app_without_secret_schema(), <<"Update ok">>)}}}, {"/apps/:app_id", Metadata, app}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index fe83719d2..0f7cf487d 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -333,7 +333,7 @@ clients_api() -> } ], responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<"List clients 200 OK">>, client)}}}, + <<"200">> => emqx_mgmt_util:array_schema(client, <<"List clients 200 OK">>)}}}, {"/clients", Metadata, clients}. client_api() -> @@ -347,8 +347,8 @@ client_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"List clients 200 OK">>, client)}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(client, <<"List clients 200 OK">>)}}, delete => #{ description => <<"Kick out client by client ID">>, parameters => [#{ @@ -358,8 +358,8 @@ client_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"List clients 200 OK">>, client)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(client, <<"List clients 200 OK">>)}}}, {"/clients/:clientid", Metadata, client}. clients_authz_cache_api() -> @@ -373,8 +373,8 @@ clients_authz_cache_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get client authz cache">>, <<"authz_cache">>)}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(authz_cache, <<"Get client authz cache">>)}}, delete => #{ description => <<"Clean client authz cache">>, parameters => [#{ @@ -384,8 +384,8 @@ clients_authz_cache_api() -> required => true }], responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Delete clients 200 OK">>)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(<<"Delete clients 200 OK">>)}}}, {"/clients/:clientid/authz_cache", Metadata, authz_cache}. clients_subscriptions_api() -> @@ -400,7 +400,7 @@ clients_subscriptions_api() -> }], responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"Get client subscriptions">>, subscription)}} + emqx_mgmt_util:array_schema(subscription, <<"Get client subscriptions">>)}} }, {"/clients/:clientid/subscriptions", Metadata, subscriptions}. @@ -416,15 +416,15 @@ unsubscribe_api() -> required => true } ], - 'requestBody' => emqx_mgmt_util:request_body_schema(#{ + 'requestBody' => emqx_mgmt_util:schema(#{ type => object, properties => #{ topic => #{ type => string, description => <<"Topic">>}}}), responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Unsubscribe ok">>)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(<<"Unsubscribe ok">>)}}}, {"/clients/:clientid/unsubscribe", Metadata, unsubscribe}. subscribe_api() -> Metadata = #{ @@ -436,7 +436,7 @@ subscribe_api() -> schema => #{type => string}, required => true }], - 'requestBody' => emqx_mgmt_util:request_body_schema(#{ + 'requestBody' => emqx_mgmt_util:schema(#{ type => object, properties => #{ topic => #{ @@ -448,8 +448,8 @@ subscribe_api() -> example => 0, description => <<"QoS">>}}}), responses => #{ - <<"404">> => emqx_mgmt_util:response_error_schema(<<"Client id not found">>), - <<"200">> => emqx_mgmt_util:response_schema(<<"Subscribe ok">>)}}}, + <<"404">> => emqx_mgmt_util:error_schema(<<"Client id not found">>), + <<"200">> => emqx_mgmt_util:schema(<<"Subscribe ok">>)}}}, {"/clients/:clientid/subscribe", Metadata, subscribe}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index a8a54a9a9..f6425a4d6 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -18,6 +18,11 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , error_schema/2 + ]). + -export([api_spec/0]). -export([ config/2 @@ -34,15 +39,6 @@ schema => #{type => string, default => <<".">>} }]). --define(TEXT_BODY(DESCR, SCHEMA), #{ - description => list_to_binary(DESCR), - content => #{ - <<"text/plain">> => #{ - schema => SCHEMA - } - } -}). - -define(PREFIX, "/configs"). -define(PREFIX_RESET, "/configs_reset"). @@ -69,18 +65,16 @@ config_api(ConfPath, Schema) -> get => #{ description => Descr("Get configs for"), responses => #{ - <<"200">> => ?TEXT_BODY("Get configs successfully", Schema), - <<"404">> => emqx_mgmt_util:response_error_schema( - <<"Config not found">>, ['NOT_FOUND']) + <<"200">> => schema(Schema, <<"Get configs successfully">>), + <<"404">> => emqx_mgmt_util:error_schema(<<"Config not found">>, ['NOT_FOUND']) } }, put => #{ description => Descr("Update configs for"), - 'requestBody' => ?TEXT_BODY("The format of the request body is depend on the 'conf_path' parameter in the query string", Schema), + 'requestBody' => schema(Schema), responses => #{ - <<"200">> => ?TEXT_BODY("Update configs successfully", Schema), - <<"400">> => emqx_mgmt_util:response_error_schema( - <<"Update configs failed">>, ['UPDATE_FAILED']) + <<"200">> => schema(Schema, <<"Update configs successfully">>), + <<"400">> => error_schema(<<"Update configs failed">>, ['UPDATE_FAILED']) } } }, @@ -97,9 +91,8 @@ config_reset_api() -> %% We only return "200" rather than the new configs that has been changed, as %% the schema of the changed configs is depends on the request parameter %% `conf_path`, it cannot be defined here. - <<"200">> => emqx_mgmt_util:response_schema(<<"Reset configs successfully">>), - <<"400">> => emqx_mgmt_util:response_error_schema( - <<"It's not able to reset the config">>, ['INVALID_OPERATION']) + <<"200">> => schema(<<"Reset configs successfully">>), + <<"400">> => error_schema(<<"It's not able to reset the config">>, ['INVALID_OPERATION']) } } }, diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 78bdba615..5d0eaaf74 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -27,6 +27,15 @@ , manage_listeners/2 , manage_nodes_listeners/2]). +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/2 + , object_array_schema/2 + , error_schema/1 + , error_schema/2 + , properties/1 + ]). + -export([format/1]). -include_lib("emqx/include/emqx.hrl"). @@ -41,39 +50,20 @@ api_spec() -> manage_listeners_api(), manage_nodes_listeners_api() ], - [listener_schema()] + [] }. -listener_schema() -> - #{ - listener => #{ - type => object, - properties => #{ - node => #{ - type => string, - description => <<"Node">>, - example => node()}, - id => #{ - type => string, - description => <<"Identifier">>}, - acceptors => #{ - type => integer, - description => <<"Number of Acceptor process">>}, - max_conn => #{ - type => integer, - description => <<"Maximum number of allowed connection">>}, - type => #{ - type => string, - description => <<"Listener type">>}, - listen_on => #{ - type => string, - description => <<"Listening port">>}, - running => #{ - type => boolean, - description => <<"Open or close">>}, - auth => #{ - type => boolean, - description => <<"Has auth">>}}}}. +properties() -> + properties([ + {node, string, <<"Node">>}, + {id, string, <<"Identifier">>}, + {acceptors, integer, <<"Number of Acceptor process">>}, + {max_conn, integer, <<"Maximum number of allowed connection">>}, + {type, string, <<"Listener type">>}, + {listen_on, string, <<"Listener port">>}, + {running, boolean, <<"Open or close">>}, + {auth, boolean, <<"Has auth">>} + ]). listeners_api() -> Metadata = #{ @@ -81,7 +71,7 @@ listeners_api() -> description => <<"List listeners in cluster">>, responses => #{ <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List all listeners">>, listener)}}}, + object_array_schema(properties(), <<"List all listeners">>)}}}, {"/listeners", Metadata, listeners}. listener_api() -> @@ -91,9 +81,9 @@ listener_api() -> parameters => [param_path_id()], responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), + error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List listener info ok">>, listener)}}}, + object_array_schema(properties(), <<"List listener info ok">>)}}}, {"/listeners/:id", Metadata, listener}. manage_listeners_api() -> @@ -105,15 +95,12 @@ manage_listeners_api() -> param_path_operation()], responses => #{ <<"500">> => - emqx_mgmt_util:response_error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, - ['BAD_LISTENER_ID']), + error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), <<"400">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, - ['BAD_REQUEST']), - <<"200">> => - emqx_mgmt_util:response_schema(<<"Operation success">>)}}}, + error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), + <<"200">> => schema(<<"Operation success">>)}}}, {"/listeners/:id/:operation", Metadata, manage_listeners}. manage_nodes_listeners_api() -> @@ -126,15 +113,14 @@ manage_nodes_listeners_api() -> param_path_operation()], responses => #{ <<"500">> => - emqx_mgmt_util:response_error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Bad node or Listener id not found">>, + error_schema(<<"Bad node or Listener id not found">>, ['BAD_NODE_NAME','BAD_LISTENER_ID']), <<"400">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>, - ['BAD_REQUEST']), + error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), <<"200">> => - emqx_mgmt_util:response_schema(<<"Operation success">>)}}}, + schema(<<"Operation success">>)}}}, {"/node/:node/listeners/:id/:operation", Metadata, manage_nodes_listeners}. nodes_listeners_api() -> @@ -144,10 +130,10 @@ nodes_listeners_api() -> parameters => [param_path_node(), param_path_id()], responses => #{ <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Node name or listener id not found">>, + error_schema(<<"Node name or listener id not found">>, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:response_schema(<<"Get listener info ok">>, listener)}}}, + schema(properties(), <<"Get listener info ok">>)}}}, {"/nodes/:node/listeners/:id", Metadata, node_listener}. nodes_listener_api() -> @@ -156,10 +142,8 @@ nodes_listener_api() -> description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Listener id not found">>), - <<"200">> => - emqx_mgmt_util:response_schema(<<"Get listener info ok">>, listener)}}}, + <<"404">> => error_schema(<<"Listener id not found">>), + <<"200">> => object_schema(properties(), <<"Get listener info ok">>)}}}, {"/nodes/:node/listeners", Metadata, node_listeners}. %%%============================================================================================== %% parameters @@ -199,27 +183,27 @@ listeners(get, _Request) -> list(). listener(get, Request) -> - ID = binary_to_atom(cowboy_req:binding(id, Request)), + ID = b2a(cowboy_req:binding(id, Request)), get_listeners(#{id => ID}). node_listeners(get, Request) -> - Node = binary_to_atom(cowboy_req:binding(node, Request)), + Node = b2a(cowboy_req:binding(node, Request)), get_listeners(#{node => Node}). node_listener(get, Request) -> - Node = binary_to_atom(cowboy_req:binding(node, Request)), - ID = binary_to_atom(cowboy_req:binding(id, Request)), + Node = b2a(cowboy_req:binding(node, Request)), + ID = b2a(cowboy_req:binding(id, Request)), get_listeners(#{node => Node, id => ID}). manage_listeners(_, Request) -> - ID = binary_to_atom(cowboy_req:binding(id, Request)), - Operation = binary_to_atom(cowboy_req:binding(operation, Request)), + ID = b2a(cowboy_req:binding(id, Request)), + Operation = b2a(cowboy_req:binding(operation, Request)), manage(Operation, #{id => ID}). manage_nodes_listeners(_, Request) -> - Node = binary_to_atom(cowboy_req:binding(node, Request)), - ID = binary_to_atom(cowboy_req:binding(id, Request)), - Operation = binary_to_atom(cowboy_req:binding(operation, Request)), + Node = b2a(cowboy_req:binding(node, Request)), + ID = b2a(cowboy_req:binding(id, Request)), + Operation = b2a(cowboy_req:binding(operation, Request)), manage(Operation, #{id => ID, node => Node}). %%%============================================================================================== @@ -232,16 +216,16 @@ get_listeners(Param) -> case list_listener(Param) of {error, not_found} -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; {error, nodedown} -> Node = maps:get(node, Param), - Reason = list_to_binary(io_lib:format("Node ~p rpc failed", [Node])), + Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), Response = #{code => 'BAD_NODE_NAME', message => Reason}, {404, Response}; [] -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; Data -> {200, Data} @@ -253,16 +237,16 @@ manage(Operation0, Param) -> case list_listener(Param) of {error, not_found} -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; {error, nodedown} -> Node = maps:get(node, Param), - Reason = list_to_binary(io_lib:format("Node ~p rpc failed", [Node])), + Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), Response = #{code => 'BAD_NODE_NAME', message => Reason}, {404, Response}; [] -> ID = maps:get(id, Param), - Reason = list_to_binary(io_lib:format("Error listener id ~p", [ID])), + Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'RESOURCE_NOT_FOUND', message => Reason}}; ListenersOrSingleListener -> manage_(Operation, ListenersOrSingleListener) @@ -279,16 +263,16 @@ manage_(Operation, Listeners) when is_list(Listeners) -> case lists:filter(fun({error, {already_started, _}}) -> false; (_) -> true end, Results) of [] -> ID = maps:get(id, hd(Listeners)), - Message = list_to_binary(io_lib:format("Already Started: ~s", [ID])), + Message = iolist_to_binary(io_lib:format("Already Started: ~s", [ID])), {400, #{code => 'BAD_REQUEST', message => Message}}; _ -> case lists:filter(fun({error,not_found}) -> false; (_) -> true end, Results) of [] -> ID = maps:get(id, hd(Listeners)), - Message = list_to_binary(io_lib:format("Already Stopped: ~s", [ID])), + Message = iolist_to_binary(io_lib:format("Already Stopped: ~s", [ID])), {400, #{code => 'BAD_REQUEST', message => Message}}; _ -> - Reason = list_to_binary(io_lib:format("~p", [Errors])), + Reason = iolist_to_binary(io_lib:format("~p", [Errors])), {500, #{code => 'UNKNOW_ERROR', message => Reason}} end end @@ -332,3 +316,6 @@ trans_running(Conf) -> Running -> Running end. + + +b2a(B) when is_binary(B) -> binary_to_atom(B, utf8). diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index 6f7d7c5f0..49035417a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -304,14 +304,7 @@ metrics_api() -> schema => #{type => boolean} }], responses => #{ - <<"200">> => #{ - description => <<"List all metrics">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"metrics_info">>) - } - } - } + <<"200">> => emqx_mgmt_util:schema(metrics_info, <<"List all metrics">>) } } }, diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 59b427261..31d17f432 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -17,6 +17,13 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [ schema/2 + , object_schema/2 + , object_array_schema/2 + , error_schema/2 + , properties/1 + ]). + -export([api_spec/0]). -export([ nodes/2 @@ -27,7 +34,7 @@ -include_lib("emqx/include/emqx.hrl"). api_spec() -> - {apis(), schemas()}. + {apis(), []}. apis() -> [ nodes_api() @@ -35,125 +42,75 @@ apis() -> , 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() -> - #{ - node => #{ - type => object, - properties => #{ - 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 => integer, - description => <<"EMQ X Broker runtime, millisecond">>}, - 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">>} - } - } - }. +properties() -> + properties([ + {node, string, <<"Node name">>}, + {connections, integer, <<"Number of clients currently connected to this node">>}, + {load1, string, <<"CPU average load in 1 minute">>}, + {load5, string, <<"CPU average load in 5 minute">>}, + {load15, string, <<"CPU average load in 15 minute">>}, + {max_fds, integer, <<"Maximum file descriptor limit for the operating system">>}, + {memory_total, string, <<"VM allocated system memory">>}, + {memory_used, string, <<"VM occupied system memory">>}, + {node_status, string, <<"Node status">>}, + {otp_release, string, <<"Erlang/OTP version used by EMQ X Broker">>}, + {process_available, integer, <<"Number of available processes">>}, + {process_used, integer, <<"Number of used processes">>}, + {uptime, integer, <<"EMQ X Broker runtime, millisecond">>}, + {version, string, <<"EMQ X Broker version">>}, + {sys_path, string, <<"EMQ X system file location">>}, + {log_path, string, <<"EMQ X log file location">>}, + {config_path, string, <<"EMQ X config file location">>} + ]). +parameters() -> + [#{ + name => node_name, + in => path, + description => <<"node name">>, + schema => #{type => string}, + required => true, + example => node() + }]. nodes_api() -> Metadata = #{ get => #{ description => <<"List EMQ X nodes">>, responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<"List EMQ X Nodes">>, node)}}}, + <<"200">> => object_array_schema(properties(), <<"List EMQ X Nodes">>) + } + } + }, {"/nodes", Metadata, nodes}. node_api() -> Metadata = #{ get => #{ description => <<"Get node info">>, - parameters => [#{ - name => node_name, - in => path, - description => "node name", - schema => #{type => string}, - required => true, - example => node()}], + parameters => parameters(), responses => #{ - <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Nodes info by name">>, node)}}}, + <<"400">> => error_schema(<<"Node error">>, ['SOURCE_ERROR']), + <<"200">> => object_schema(properties(), <<"Get EMQ X Nodes info by name">>)}}}, {"/nodes/:node_name", Metadata, node}. node_metrics_api() -> Metadata = #{ get => #{ description => <<"Get node metrics">>, - parameters => [#{ - name => node_name, - in => path, - description => "node name", - schema => #{type => string}, - required => true, - example => node()}], + parameters => parameters(), responses => #{ - <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Node Metrics">>, metrics)}}}, + <<"400">> => error_schema(<<"Node error">>, ['SOURCE_ERROR']), + <<"200">> => schema(metrics, <<"Get EMQ X Node 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", - schema => #{type => string}, - required => true, - example => node()}], + parameters => parameters(), responses => #{ - <<"400">> => emqx_mgmt_util:response_error_schema(<<"Node error">>, ['SOURCE_ERROR']), - <<"200">> => emqx_mgmt_util:response_schema(<<"Get EMQ X Node Stats">>, stat)}}}, + <<"400">> => error_schema(<<"Node error">>, ['SOURCE_ERROR']), + <<"200">> => schema(stat, <<"Get EMQ X Node Stats">>)}}}, {"/nodes/:node_name/stats", Metadata, node_stats}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl index 1e4555160..058e18160 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -19,88 +19,48 @@ -behaviour(minirest_api). +-import(emqx_mgmt_util, [ object_schema/1 + , object_schema/2 + , object_array_schema/1 + , object_array_schema/2 + , properties/1 + ]). + -export([api_spec/0]). -export([ publish/2 , publish_batch/2]). api_spec() -> - { - [publish_api(), publish_bulk_api()], - [message_schema()] - }. + {[publish_api(), publish_bulk_api()], []}. publish_api() -> - Schema = #{ - type => object, - properties => maps:without([id], message_properties()) - }, MeteData = #{ post => #{ description => <<"Publish">>, - 'requestBody' => emqx_mgmt_util:request_body_schema(Schema), + 'requestBody' => object_schema(maps:without([id], properties())), responses => #{ - <<"200">> => emqx_mgmt_util:response_schema(<<"publish ok">>, message)}}}, + <<"200">> => object_schema(properties(), <<"publish ok">>)}}}, {"/publish", MeteData, publish}. publish_bulk_api() -> - Schema = #{ - type => object, - properties => maps:without([id], message_properties()) - }, MeteData = #{ post => #{ description => <<"publish">>, - 'requestBody' => emqx_mgmt_util:request_body_array_schema(Schema), + 'requestBody' => object_array_schema(maps:without([id], properties())), responses => #{ - <<"200">> => emqx_mgmt_util:response_array_schema(<<"publish ok">>, message)}}}, + <<"200">> => object_array_schema(properties(), <<"publish ok">>)}}}, {"/publish/bulk", MeteData, publish_batch}. -message_schema() -> - #{ - message => #{ - type => object, - properties => message_properties() - } - }. - -message_properties() -> - #{ - 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">>} - } - } - }. +properties() -> + properties([ + {id, string, <<"Message Id">>}, + {topic, string, <<"Topic">>}, + {qos, integer, <<"QoS">>, [0, 1, 2]}, + {payload, string, <<"Topic">>}, + {from, string, <<"Message from">>}, + {retain, boolean, <<"Retain message flag, nullable, default false">>} + ]). publish(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), @@ -119,19 +79,8 @@ message(Map) -> 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 - }. + Retain = maps:get(<<"retain">>, Map, false), + emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}). messages(List) -> [message(MessageMap) || MessageMap <- List]. @@ -144,7 +93,7 @@ format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload qos => Qos, topic => Topic, payload => Payload, - flag => Flags, + retain => maps:get(retain, Flags, false), from => to_binary(From) }. diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 258680546..b193bac34 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -28,43 +28,35 @@ -define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND'). +-import(emqx_mgmt_util, [ object_schema/2 + , object_array_schema/2 + , error_schema/2 + , properties/1 + , page_params/0 + ]). + api_spec() -> { [routes_api(), route_api()], - [route_schema()] + [] }. -route_schema() -> - #{ - route => #{ - type => object, - properties => #{ - topic => #{ - type => string}, - node => #{ - type => string, - example => node()}}}}. +properties() -> + properties([ + {topic, string}, + {node, string} + ]). routes_api() -> Metadata = #{ get => #{ description => <<"EMQ X routes">>, - parameters => [ - #{ - name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer, default => 1} - }, - #{ - name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer, default => emqx_mgmt:max_row_limit()} - }], + parameters => page_params(), responses => #{ - <<"200">> => - emqx_mgmt_util:response_array_schema("List route info", route)}}}, + <<"200">> => object_array_schema(properties(), <<"List route info">>) + } + } + }, {"/routes", Metadata, routes}. route_api() -> @@ -80,10 +72,12 @@ route_api() -> }], responses => #{ <<"200">> => - emqx_mgmt_util:response_schema(<<"Route info">>, route), + object_schema(properties(), <<"Route info">>), <<"404">> => - emqx_mgmt_util:response_error_schema(<<"Topic not found">>, [?TOPIC_NOT_FOUND]) - }}}, + error_schema(<<"Topic not found">>, [?TOPIC_NOT_FOUND]) + } + } + }, {"/routes/:topic", Metadata, route}. %%%============================================================================================== diff --git a/apps/emqx_management/src/emqx_mgmt_api_status.erl b/apps/emqx_management/src/emqx_mgmt_api_status.erl index fa46b1d25..2fa47d1d9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_status.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_status.erl @@ -30,7 +30,10 @@ status_api() -> get => #{ security => [], responses => #{ - <<"200">> => #{description => <<"running">>}}}}, + <<"200">> => #{description => <<"running">>} + } + } + }, {Path, Metadata, running_status}. running_status(get, _Request) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 27e8c898a..f7a37b861 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -20,6 +20,11 @@ -include_lib("emqx/include/emqx.hrl"). +-import(emqx_mgmt_util, [ page_schema/1 + , properties/1 + , page_params/0 + ]). + -export([api_spec/0]). -export([subscriptions/2]). @@ -39,84 +44,67 @@ -define(format_fun, {?MODULE, format}). api_spec() -> - { - [subscriptions_api()], - [subscription_schema()] - }. + {subscriptions_api(), subscription_schema()}. subscriptions_api() -> MetaData = #{ get => #{ description => <<"List subscriptions">>, - parameters => [ - #{ - name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer} - }, - #{ - name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer} - }, - #{ - name => clientid, - in => query, - description => <<"Client ID">>, - schema => #{type => string} - }, - #{ - name => node, - in => query, - description => <<"Node name">>, - schema => #{type => string} - }, - #{ - name => qos, - in => query, - description => <<"QoS">>, - schema => #{type => integer, enum => [0, 1, 2]} - }, - #{ - name => share, - in => query, - description => <<"Shared subscription">>, - schema => #{type => boolean} - }, - #{ - name => topic, - in => query, - description => <<"Topic">>, - schema => #{type => string} - } - #{ - name => match_topic, - in => query, - description => <<"Match topic string">>, - schema => #{type => string} - } - ], + parameters => parameters(), responses => #{ - <<"200">> => emqx_mgmt_util:response_page_schema(subscription)}}}, - {"/subscriptions", MetaData, subscriptions}. + <<"200">> => page_schema(subscription) + } + } + }, + [{"/subscriptions", MetaData, subscriptions}]. subscription_schema() -> - #{ - subscription => #{ - type => object, - properties => #{ - node => #{ - type => string}, - topic => #{ - type => string}, - clientid => #{ - type => string}, - qos => #{ - type => integer, - enum => [0,1,2]}}} - }. + Props = properties([ + {node, string}, + {topic, string}, + {clientid, string}, + {qos, integer, <<>>, [0,1,2]}]), + [#{subscription => #{type => object, properties => Props}}]. + +parameters() -> + [ + #{ + name => clientid, + in => query, + description => <<"Client ID">>, + schema => #{type => string} + }, + #{ + name => node, + in => query, + description => <<"Node name">>, + schema => #{type => string} + }, + #{ + name => qos, + in => query, + description => <<"QoS">>, + schema => #{type => integer, enum => [0, 1, 2]} + }, + #{ + name => share, + in => query, + description => <<"Shared subscription">>, + schema => #{type => boolean} + }, + #{ + name => topic, + in => query, + description => <<"Topic">>, + schema => #{type => string} + } + #{ + name => match_topic, + in => query, + description => <<"Match topic string">>, + schema => #{type => string} + } | page_params() + ]. subscriptions(get, Request) -> Params = cowboy_req:parse_qs(Request), diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index 7bd393904..da509a36b 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -133,3 +133,4 @@ api_modules() -> api_modules() -> minirest_api:find_api_modules(apps()) -- [emqx_mgmt_api_apps]. -endif. + diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index d764afb07..5a95238e3 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -24,15 +24,26 @@ , batch_operation/3 ]). --export([ request_body_schema/1 - , request_body_array_schema/1 - , response_schema/1 - , response_schema/2 - , response_array_schema/2 - , response_error_schema/1 - , response_error_schema/2 - , response_page_schema/1 - , response_batch_schema/1]). +-export([ bad_request/0 + , bad_request/1 + , properties/1 + , page_params/0 + , schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , array_schema/1 + , array_schema/2 + , object_array_schema/1 + , object_array_schema/2 + , page_schema/1 + , page_object_schema/1 + , error_schema/1 + , error_schema/2 + , batch_schema/1 + ]). + + -export([urldecode/1]). @@ -90,78 +101,69 @@ urldecode(S) -> %%%============================================================================================== %% schema util +schema(Ref) when is_atom(Ref) -> + json_content_schema(minirest:ref(atom_to_binary(Ref, utf8))); +schema(SchemaOrDesc) -> + json_content_schema(SchemaOrDesc). +schema(Ref, Desc) when is_atom(Ref) -> + json_content_schema(minirest:ref(atom_to_binary(Ref, utf8)), Desc); +schema(Schema, Desc) -> + json_content_schema(Schema, Desc). -request_body_array_schema(Schema) when is_map(Schema) -> - json_content_schema("", #{type => array, items => Schema}); -request_body_array_schema(Ref) when is_atom(Ref) -> - request_body_array_schema(atom_to_binary(Ref, utf8)); -request_body_array_schema(Ref) when is_binary(Ref) -> - json_content_schema("", #{type => array, items => minirest:ref(Ref)}). +object_schema(Properties) when is_map(Properties) -> + json_content_schema(#{type => object, properties => Properties}). +object_schema(Properties, Desc) when is_map(Properties) -> + json_content_schema(#{type => object, properties => Properties}, Desc). -request_body_schema(Schema) when is_map(Schema) -> - json_content_schema("", Schema); -request_body_schema(Ref) when is_atom(Ref) -> - request_body_schema(atom_to_binary(Ref)); -request_body_schema(Ref) when is_binary(Ref) -> - json_content_schema("", minirest:ref(Ref)). +array_schema(Ref) when is_atom(Ref) -> + json_content_schema(#{type => array, items => minirest:ref(atom_to_binary(Ref, utf8))}). +array_schema(Ref, Desc) when is_atom(Ref) -> + json_content_schema(#{type => array, items => minirest:ref(atom_to_binary(Ref, utf8))}, Desc); +array_schema(Schema, Desc) -> + json_content_schema(#{type => array, items => Schema}, Desc). -response_array_schema(Description, Schema) when is_map(Schema) -> - json_content_schema(Description, #{type => array, items => Schema}); -response_array_schema(Description, Ref) when is_atom(Ref) -> - response_array_schema(Description, atom_to_binary(Ref, utf8)); -response_array_schema(Description, Ref) when is_binary(Ref) -> - json_content_schema(Description, #{type => array, items => minirest:ref(Ref)}). +object_array_schema(Properties) when is_map(Properties) -> + json_content_schema(#{type => array, items => #{type => object, properties => Properties}}). +object_array_schema(Properties, Desc) -> + json_content_schema(#{type => array, items => #{type => object, properties => Properties}}, Desc). -response_schema(Description) -> - json_content_schema(Description). - -response_schema(Description, Schema) when is_map(Schema) -> - json_content_schema(Description, Schema); -response_schema(Description, Ref) when is_atom(Ref) -> - response_schema(Description, atom_to_binary(Ref, utf8)); -response_schema(Description, Ref) when is_binary(Ref) -> - json_content_schema(Description, minirest:ref(Ref)). - -%% @doc default code is RESOURCE_NOT_FOUND -response_error_schema(Description) -> - response_error_schema(Description, ['RESOURCE_NOT_FOUND']). - -response_error_schema(Description, Enum) -> - Schema = #{ - type => object, - properties => #{ - code => #{ - type => string, - enum => Enum}, - message => #{ - type => string}}}, - json_content_schema(Description, Schema). - -response_page_schema(Def) when is_atom(Def) -> - response_page_schema(atom_to_binary(Def, utf8)); -response_page_schema(Def) when is_binary(Def) -> - response_page_schema(minirest:ref(Def)); -response_page_schema(ItemSchema) when is_map(ItemSchema) -> - Schema = #{ +page_schema(Ref) when is_atom(Ref) -> + page_schema(minirest:ref(atom_to_binary(Ref, utf8))); +page_schema(Schema) -> + Schema1 = #{ type => object, properties => #{ meta => #{ type => object, - properties => #{ - page => #{ - type => integer}, - limit => #{ - type => integer}, - count => #{ - type => integer}}}, + properties => properties([{page, integer}, + {limit, integer}, + {count, integer}]) + }, data => #{ type => array, - items => ItemSchema}}}, - json_content_schema("", Schema). + items => Schema + } + } + }, + json_content_schema(Schema1). -response_batch_schema(DefName) when is_atom(DefName) -> - response_batch_schema(atom_to_binary(DefName, utf8)); -response_batch_schema(DefName) when is_binary(DefName) -> +page_object_schema(Properties) when is_map(Properties) -> + page_schema(#{type => object, properties => Properties}). + +error_schema(Description) -> + error_schema(Description, ['RESOURCE_NOT_FOUND']). + +error_schema(Description, Enum) -> + Schema = #{ + type => object, + properties => properties([{code, string, <<>>, Enum}, + {message, string}]) + }, + json_content_schema(Schema, Description). + +batch_schema(DefName) when is_atom(DefName) -> + batch_schema(atom_to_binary(DefName, utf8)); +batch_schema(DefName) when is_binary(DefName) -> Schema = #{ type => object, properties => #{ @@ -181,22 +183,17 @@ response_batch_schema(DefName) when is_binary(DefName) -> data => minirest:ref(DefName), reason => #{ type => <<"string">>}}}}}}, - json_content_schema("", Schema). + json_content_schema(Schema). -json_content_schema(Description, Schema) -> - Content = - #{content => #{ - 'application/json' => #{ - schema => Schema}}}, - case Description of - "" -> - Content; - _ -> - maps:merge(#{description => Description}, Content) - end. - -json_content_schema(Description) -> - #{description => Description}. +json_content_schema(Schema) when is_map(Schema) -> + #{content => #{'application/json' => #{schema => Schema}}}; +json_content_schema(Desc) when is_binary(Desc) -> + #{description => Desc}. +json_content_schema(Schema, Desc) -> + #{ + content => #{'application/json' => #{schema => Schema}}, + description => Desc + }. %%%============================================================================================== batch_operation(Module, Function, ArgsList) -> @@ -215,3 +212,44 @@ batch_operation(Module, Function, [Args | ArgsList], Failed) -> {error ,Reason} -> batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed]) end. + +properties(Props) -> + properties(Props, #{}). +properties([], Acc) -> + Acc; +properties([Key| Props], Acc) when is_atom(Key) -> + properties(Props, maps:put(Key, #{type => string}, Acc)); +properties([{Key, Type} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => Type}, Acc)); +properties([{Key, object, Props1} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => object, + properties => properties(Props1)}, Acc)); +properties([{Key, {array, Type}, Desc} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => array, + items => #{type => Type}, + description => Desc}, Acc)); +properties([{Key, Type, Desc} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc)); +properties([{Key, Type, Desc, Enum} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => Type, + description => Desc, + emum => Enum}, Acc)). +page_params() -> + [#{ + name => page, + in => query, + description => <<"Page">>, + schema => #{type => integer, default => 1} + }, + #{ + name => limit, + in => query, + description => <<"Page size">>, + schema => #{type => integer, default => emqx_mgmt:max_row_limit()} + }]. + +bad_request() -> + bad_request(<<"Bad Request">>). +bad_request(Desc) -> + object_schema(properties([{message, string}, {code, string}]), Desc). + diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 2d6c3ddf0..06a50fa37 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -18,11 +18,12 @@ -behavior(minirest_api). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , response_error_schema/2 - , response_page_schema/1 - , request_body_schema/1 +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/2 + , error_schema/2 + , page_object_schema/1 + , properties/1 ]). -define(MAX_PAYLOAD_LENGTH, 2048). @@ -48,77 +49,50 @@ api_spec() -> { [status_api(), delayed_messages_api(), delayed_message_api()], - [] + schemas() }. -delayed_schema() -> - delayed_schema(false). +schemas() -> + [#{delayed => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([delayed]))}]. +properties() -> + PayloadDesc = io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p", + [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH]), + properties([ + {id, integer, <<"Message Id (MQTT message id hash)">>}, + {publish_time, string, <<"publish time, rfc 3339">>}, + {topic, string, <<"Topic">>}, + {qos, string, <<"QoS">>}, + {payload, string, iolist_to_binary(PayloadDesc)}, + {form_clientid, string, <<"Form ClientId">>}, + {form_username, string, <<"Form Username">>} + ]). -delayed_schema(WithPayload) -> - case WithPayload of - true -> - #{ - type => object, - properties => delayed_message_properties() - }; - _ -> - #{ - type => object, - properties => maps:without([payload], delayed_message_properties()) - } - end. - -delayed_message_properties() -> - PayloadDesc = list_to_binary( - io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p", - [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH])), - #{ - id => #{ - type => integer, - description => <<"Message Id (MQTT message id hash)">>}, - publish_time => #{ - type => string, - description => <<"publish time, rfc 3339">>}, - topic => #{ - type => string, - description => <<"Topic">>}, - qos => #{ - type => integer, - enum => [0, 1, 2], - description => <<"Qos">>}, - payload => #{ - type => string, - description => PayloadDesc}, - form_clientid => #{ - type => string, - description => <<"Client ID">>}, - form_username => #{ - type => string, - description => <<"Username">>} - }. +parameters() -> + [#{ + name => id, + in => path, + schema => #{type => string}, + required => true + }]. status_api() -> - Schema = #{ - type => object, - properties => #{ - enable => #{ - type => boolean}, - max_delayed_messages => #{ - type => integer, - description => <<"Max limit, 0 is no limit">>}}}, Metadata = #{ get => #{ - description => "Get delayed status", + description => <<"Get delayed status">>, responses => #{ - <<"200">> => response_schema(<<"Bad Request">>, Schema)}}, + <<"200">> => schema(delayed)} + }, put => #{ - description => "Enable or disable delayed, set max delayed messages", - 'requestBody' => request_body_schema(Schema), + description => <<"Enable or disable delayed, set max delayed messages">>, + 'requestBody' => schema(delayed), responses => #{ <<"200">> => - response_schema(<<"Enable or disable delayed successfully">>, Schema), + schema(delayed, <<"Enable or disable delayed successfully">>), <<"400">> => - response_error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED])}}}, + error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED]) + } + } + }, {"/mqtt/delayed_messages/status", Metadata, status}. delayed_messages_api() -> @@ -126,32 +100,30 @@ delayed_messages_api() -> get => #{ description => "List delayed messages", responses => #{ - <<"200">> => response_page_schema(delayed_schema())}}}, + <<"200">> => page_object_schema(properties()) + } + } + }, {"/mqtt/delayed_messages", Metadata, delayed_messages}. delayed_message_api() -> Metadata = #{ get => #{ - description => "Get delayed message", - parameters => [#{ - name => id, - in => path, - schema => #{type => string}, - required => true - }], + description => <<"Get delayed message">>, + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Get delayed message success">>, delayed_schema(true)), - <<"404">> => response_error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND])}}, + <<"200">> => object_schema(maps:without([payload], properties()), <<"Get delayed message success">>), + <<"404">> => error_schema(<<"Message ID not found">>, [?MESSAGE_ID_NOT_FOUND]) + } + }, delete => #{ - description => "Delete delayed message", - parameters => [#{ - name => id, - in => path, - schema => #{type => string}, - required => true - }], + description => <<"Delete delayed message">>, + parameters => parameters(), responses => #{ - <<"200">> => response_schema(<<"Delete delayed message success">>)}}}, + <<"200">> => schema(<<"Delete delayed message success">>) + } + } + }, {"/mqtt/delayed_messages/:id", Metadata, delayed_message}. %%-------------------------------------------------------------------- @@ -181,7 +153,7 @@ delayed_message(get, Request) -> {200, Message#{payload => base64:encode(Payload)}} end; {error, not_found} -> - Message = list_to_binary(io_lib:format("Message ID ~p not found", [Id])), + Message = iolist_to_binary(io_lib:format("Message ID ~p not found", [Id])), {404, #{code => ?MESSAGE_ID_NOT_FOUND, message => Message}} end; delayed_message(delete, Request) -> diff --git a/apps/emqx_modules/src/emqx_event_message_api.erl b/apps/emqx_modules/src/emqx_event_message_api.erl index 86c3255e1..43216ef63 100644 --- a/apps/emqx_modules/src/emqx_event_message_api.erl +++ b/apps/emqx_modules/src/emqx_event_message_api.erl @@ -21,51 +21,15 @@ -export([event_message/2]). +-import(emqx_mgmt_util, [ schema/1 + ]). api_spec() -> {[event_message_api()], [event_message_schema()]}. event_message_schema() -> - #{ - type => object, - properties => #{ - '$event/client_connected' => #{ - type => boolean, - description => <<"Client connected event">>, - example => get_raw(<<"$event/client_connected">>) - }, - '$event/client_disconnected' => #{ - type => boolean, - description => <<"client_disconnected">>, - example => get_raw(<<"Client disconnected event">>) - }, - '$event/client_subscribed' => #{ - type => boolean, - description => <<"client_subscribed">>, - example => get_raw(<<"Client subscribed event">>) - }, - '$event/client_unsubscribed' => #{ - type => boolean, - description => <<"client_unsubscribed">>, - example => get_raw(<<"Client unsubscribed event">>) - }, - '$event/message_delivered' => #{ - type => boolean, - description => <<"message_delivered">>, - example => get_raw(<<"Message delivered event">>) - }, - '$event/message_acked' => #{ - type => boolean, - description => <<"message_acked">>, - example => get_raw(<<"Message acked event">>) - }, - '$event/message_dropped' => #{ - type => boolean, - description => <<"message_dropped">>, - example => get_raw(<<"Message dropped event">>) - } - } - }. + Conf = emqx:get_raw_config([event_message]), + #{event_message => emqx_mgmt_api_configs:gen_schema(Conf)}. event_message_api() -> Path = "/mqtt/event_message", @@ -73,14 +37,14 @@ event_message_api() -> get => #{ description => <<"Event Message">>, responses => #{ - <<"200">> => - emqx_mgmt_util:response_schema(<<>>, event_message_schema())}}, + <<"200">> => schema(event_message) + } + }, post => #{ - description => <<"">>, - 'requestBody' => emqx_mgmt_util:request_body_schema(event_message_schema()), + description => <<"Update Event Message">>, + 'requestBody' => schema(event_message), responses => #{ - <<"200">> => - emqx_mgmt_util:response_schema(<<>>, event_message_schema()) + <<"200">> => schema(event_message) } } }, @@ -94,6 +58,3 @@ event_message(post, Request) -> Params = emqx_json:decode(Body, [return_maps]), _ = emqx_event_message:update(Params), {200, emqx_event_message:list()}. - -get_raw(Key) -> - emqx_config:get_raw([<<"event_message">>] ++ [Key], false). diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl index 9b07b0a93..8a5a5dc6b 100644 --- a/apps/emqx_modules/src/emqx_rewrite_api.erl +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -25,28 +25,20 @@ -define(EXCEED_LIMIT, 'EXCEED_LIMIT'). +-import(emqx_mgmt_util, [ object_array_schema/1 + , object_array_schema/2 + , error_schema/2 + , properties/1 + ]). + api_spec() -> {[rewrite_api()], []}. -topic_rewrite_schema() -> - #{ - type => object, - properties => #{ - action => #{ - type => string, - description => <<"Node">>, - enum => [subscribe, publish]}, - source_topic => #{ - type => string, - description => <<"Topic">>}, - re => #{ - type => string, - description => <<"Regular expressions">>}, - dest_topic => #{ - type => string, - description => <<"Destination topic">>} - } - }. +properties() -> + properties([{action, string, <<"Node">>, [subscribe, publish]}, + {source_topic, string, <<"Topic">>}, + {re, string, <<"Regular expressions">>}, + {dest_topic, string, <<"Destination topic">>}]). rewrite_api() -> Path = "/mqtt/topic_rewrite", @@ -54,15 +46,18 @@ rewrite_api() -> get => #{ description => <<"List topic rewrite">>, responses => #{ - <<"200">> => - emqx_mgmt_util:response_array_schema(<<"List all rewrite rules">>, topic_rewrite_schema())}}, + <<"200">> => object_array_schema(properties(), <<"List all rewrite rules">>) + } + }, post => #{ description => <<"Update topic rewrite">>, - 'requestBody' => emqx_mgmt_util:request_body_array_schema(topic_rewrite_schema()), + 'requestBody' => object_array_schema(properties()), responses => #{ - <<"200">> => - emqx_mgmt_util:response_schema(<<"Update topic rewrite success">>, topic_rewrite_schema()), - <<"413">> => emqx_mgmt_util:response_error_schema(<<"Rules count exceed max limit">>, [?EXCEED_LIMIT])}}}, + <<"200">> =>object_array_schema(properties(), <<"Update topic rewrite success">>), + <<"413">> => error_schema(<<"Rules count exceed max limit">>, [?EXCEED_LIMIT]) + } + } + }, {Path, Metadata, topic_rewrite}. topic_rewrite(get, _Request) -> @@ -76,6 +71,6 @@ topic_rewrite(post, Request) -> ok = emqx_rewrite:update(Params), {200, emqx_rewrite:list()}; _ -> - Message = list_to_binary(io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT])), + Message = iolist_to_binary(io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT])), {413, #{code => ?EXCEED_LIMIT, message => Message}} end. diff --git a/apps/emqx_modules/src/emqx_telemetry_api.erl b/apps/emqx_modules/src/emqx_telemetry_api.erl index af5f40b02..e1d297afc 100644 --- a/apps/emqx_modules/src/emqx_telemetry_api.erl +++ b/apps/emqx_modules/src/emqx_telemetry_api.erl @@ -18,9 +18,11 @@ -behavior(minirest_api). --import(emqx_mgmt_util, [ response_schema/1 - , response_schema/2 - , request_body_schema/1 +-import(emqx_mgmt_util, [ schema/1 + , object_schema/1 + , object_schema/2 + , properties/1 + , bad_request/0 ]). % -export([cli/1]). @@ -34,96 +36,38 @@ -export([api_spec/0]). api_spec() -> - {[status_api(), data_api()], schemas()}. + {[status_api(), data_api()], []}. -schemas() -> - [#{broker_info => #{ - type => object, - properties => #{ - emqx_version => #{ - type => string, - description => <<"EMQ X Version">>}, - license => #{ - type => object, - properties => #{ - edition => #{type => string} - }, - description => <<"EMQ X License">>}, - os_name => #{ - type => string, - description => <<"OS Name">>}, - os_version => #{ - type => string, - description => <<"OS Version">>}, - otp_version => #{ - type => string, - description => <<"Erlang/OTP Version">>}, - up_time => #{ - type => integer, - description => <<"EMQ X Runtime">>}, - uuid => #{ - type => string, - description => <<"EMQ X UUID">>}, - nodes_uuid => #{ - type => array, - items => #{type => string}, - description => <<"EMQ X Cluster Nodes UUID">>}, - active_plugins => #{ - type => array, - items => #{type => string}, - description => <<"EMQ X Active Plugins">>}, - active_modules => #{ - type => array, - items => #{type => string}, - description => <<"EMQ X Active Modules">>}, - num_clients => #{ - type => integer, - description => <<"EMQ X Current Connections">>}, - messages_received => #{ - type => integer, - description => <<"EMQ X Current Received Message">>}, - messages_sent => #{ - type => integer, - description => <<"EMQ X Current Sent Message">>} - } - }}]. +properties() -> + properties([ + {emqx_version, string, <<"EMQ X Version">>}, + {license, object, [{edition, string, <<"EMQ X License">>}]}, + {os_name, string, <<"OS Name">>}, + {os_version, string, <<"OS Version">>}, + {otp_version, string, <<"Erlang/OTP Version">>}, + {up_time, string, <<"EMQ X Runtime">>}, + {uuid, string, <<"EMQ X UUID">>}, + {nodes_uuid, string, <<"EMQ X Cluster Nodes UUID">>}, + {active_plugins, {array, string}, <<"EMQ X Active Plugins">>}, + {active_modules, {array, string}, <<"EMQ X Active Modules">>}, + {num_clients, integer, <<"EMQ X Current Connections">>}, + {messages_received, integer, <<"EMQ X Current Received Message">>}, + {messages_sent, integer, <<"EMQ X Current Sent Message">>} + ]). status_api() -> + Props = properties([{enable, boolean}]), Metadata = #{ get => #{ description => "Get telemetry status", - responses => #{ - <<"200">> => response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{enable => #{type => boolean}} - } - ) - } + responses => #{<<"200">> => object_schema(Props)} }, put => #{ description => "Enable or disbale telemetry", - 'requestBody' => request_body_schema(#{ - type => object, - properties => #{ - enable => #{ - type => boolean - } - } - }), + 'requestBody' => object_schema(Props), responses => #{ - <<"200">> => - response_schema(<<"Enable or disbale telemetry successfully">>), - <<"400">> => - response_schema(<<"Bad Request">>, - #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - } - ) + <<"200">> => schema(<<"Enable or disbale telemetry successfully">>), + <<"400">> => bad_request() } } }, @@ -133,7 +77,7 @@ data_api() -> Metadata = #{ get => #{ responses => #{ - <<"200">> => response_schema(<<"Get telemetry data">>, <<"broker_info">>) + <<"200">> => object_schema(properties(), <<"Get telemetry data">>) } } }, diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 8a94a0ffa..4c974146c 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -20,9 +20,8 @@ -include("emqx_prometheus.hrl"). --import(emqx_mgmt_util, [ response_schema/2 - , request_body_schema/1 - ]). +-import(emqx_mgmt_util, [ schema/1 + , bad_request/0]). -export([api_spec/0]). @@ -40,23 +39,14 @@ prometheus_api() -> Metadata = #{ get => #{ description => <<"Get Prometheus info">>, - responses => #{ - <<"200">> => response_schema(<<>>, prometheus) - } + responses => #{<<"200">> => schema(prometheus)} }, put => #{ description => <<"Update Prometheus">>, - 'requestBody' => request_body_schema(prometheus), + 'requestBody' => schema(prometheus), responses => #{ - <<"200">> =>response_schema(<<>>, prometheus), - <<"400">> => - response_schema(<<"Bad Request">>, #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }) + <<"200">> => schema(prometheus), + <<"400">> => bad_request() } } }, diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index cb4262451..65e79ec40 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -188,7 +188,7 @@ init([]) -> handle_call({update_config, Conf}, _, State) -> State2 = update_config(State, Conf), - emqx_config:put([?APP], Conf), + _ = emqx:update_config([?APP], Conf), {reply, ok, State2}; handle_call({wait_semaphore, Id}, From, #{wait_quotas := Waits} = State) -> diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index d766eab06..34e75e567 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -27,14 +27,12 @@ , config/2]). -import(emqx_mgmt_api_configs, [gen_schema/1]). --import(emqx_mgmt_util, [ response_array_schema/2 - , response_schema/1 - , response_error_schema/2]). - --define(CFG_BODY(DESCR), - #{description => list_to_binary(DESCR), - content => #{<<"application/json">> => - #{schema => gen_schema(emqx_config:get([emqx_retainer]))}}}). +-import(emqx_mgmt_util, [ object_array_schema/2 + , schema/1 + , schema/2 + , error_schema/2 + , page_params/0 + , properties/1]). api_spec() -> { @@ -42,76 +40,86 @@ api_spec() -> , with_topic_api() , config_api() ], - [ message_schema(message, fun message_properties/0) - , message_schema(detail_message, fun detail_message_properties/0) - ] + schemas() }. +schemas() -> + MqttRetainer = gen_schema(emqx:get_raw_config([emqx_retainer])), + [#{emqx_retainer => MqttRetainer}]. + +message_props() -> + properties([ + {id, string, <<"Message ID">>}, + {topic, string, <<"MQTT Topic">>}, + {qos, string, <<"MQTT QoS">>}, + {payload, string, <<"MQTT Payload">>}, + {publish_at, string, <<"publish datetime">>}, + {from_clientid, string, <<"publisher ClientId">>}, + {from_username, string, <<"publisher Username">>} + ]). + +parameters() -> + [#{ + name => topic, + in => path, + required => true, + schema => #{type => "string"} + }]. + lookup_retained_api() -> - Metadata = - #{get => #{description => <<"lookup matching messages">>, - parameters => [ #{name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer, default => 1}} - , #{name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer, - default => emqx_mgmt:max_row_limit()}} - ], - responses => #{ <<"200">> => - response_array_schema("List retained messages", message) - , <<"405">> => response_schema(<<"NotAllowed">>) - }}}, + Metadata = #{ + get => #{ + description => <<"lookup matching messages">>, + parameters => page_params(), + responses => #{ + <<"200">> => object_array_schema( + maps:without([payload], message_props()), + <<"List retained messages">>), + <<"405">> => schema(<<"NotAllowed">>) + } + } + }, {"/mqtt/retainer/messages", Metadata, lookup_retained_warp}. with_topic_api() -> - MetaData = #{get => #{description => <<"lookup matching messages">>, - parameters => [ #{name => topic, - in => path, - required => true, - schema => #{type => "string"}} - , #{name => page, - in => query, - description => <<"Page">>, - schema => #{type => integer, default => 1}} - , #{name => limit, - in => query, - description => <<"Page size">>, - schema => #{type => integer, - default => emqx_mgmt:max_row_limit()}} - ], - responses => #{ <<"200">> => - response_array_schema("List retained messages", detail_message) - , <<"405">> => response_schema(<<"NotAllowed">>)}}, - delete => #{description => <<"delete matching messages">>, - parameters => [#{name => topic, - in => path, - required => true, - schema => #{type => "string"}}], - responses => #{ <<"200">> => response_schema(<<"Successed">>) - , <<"405">> => response_schema(<<"NotAllowed">>)}} - }, + MetaData = #{ + get => #{ + description => <<"lookup matching messages">>, + parameters => parameters() ++ page_params(), + responses => #{ + <<"200">> => object_array_schema(message_props(), <<"List retained messages">>), + <<"405">> => schema(<<"NotAllowed">>) + } + }, + delete => #{ + description => <<"delete matching messages">>, + parameters => parameters(), + responses => #{ + <<"200">> => schema(<<"Successed">>), + <<"405">> => schema(<<"NotAllowed">>) + } + } + }, {"/mqtt/retainer/message/:topic", MetaData, with_topic_warp}. config_api() -> MetaData = #{ - get => #{ - description => <<"get retainer config">>, - responses => #{<<"200">> => ?CFG_BODY("Get configs successfully"), - <<"404">> => response_error_schema( - <<"Config not found">>, ['NOT_FOUND'])} - }, - put => #{ - description => <<"Update retainer config">>, - 'requestBody' => - ?CFG_BODY("The format of the request body is depend on the 'conf_path' parameter in the query string"), - responses => #{<<"200">> => response_schema("Update configs successfully"), - <<"400">> => response_error_schema( - <<"Update configs failed">>, ['UPDATE_FAILED'])} - } - }, + get => #{ + description => <<"get retainer config">>, + responses => #{ + <<"200">> => schema(mqtt_retainer, <<"Get configs successfully">>), + <<"404">> => error_schema(<<"Config not found">>, ['NOT_FOUND']) + } + }, + put => #{ + description => <<"Update retainer config">>, + 'requestBody' => schema(mqtt_retainer), + responses => #{ + <<"200">> => schema(mqtt_retainer, <<"Update configs successfully">>), + <<"400">> => error_schema(<<"Update configs failed">>, ['UPDATE_FAILED']) + } + } + }, {"/mqtt/retainer", MetaData, config}. lookup_retained_warp(Type, Req) -> @@ -121,7 +129,7 @@ with_topic_warp(Type, Req) -> check_backend(Type, Req, fun with_topic/2). config(get, _) -> - Config = emqx_config:get([emqx_retainer]), + Config = emqx:get_config([mqtt_retainer]), Body = emqx_json:encode(Config), {200, Body}; @@ -129,16 +137,16 @@ config(put, Req) -> try {ok, Body, _} = cowboy_req:read_body(Req), Cfg = emqx_json:decode(Body), - {ok, RawConf} = hocon:binary(jsx:encode(#{<<"emqx_retainer">> => Cfg}), + {ok, RawConf} = hocon:binary(jsx:encode(#{<<"mqtt_retainer">> => Cfg}), #{format => richmap}), RichConf = hocon_schema:check(emqx_retainer_schema, RawConf, #{atom_key => true}), - #{emqx_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), + #{mqtt_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), emqx_retainer:update_config(Conf), {200, #{<<"content-type">> => <<"text/plain">>}, <<"Update configs successfully">>} catch _:Reason:_ -> {400, #{code => 'UPDATE_FAILED', - message => erlang:list_to_binary(io_lib:format("~p~n", [Reason]))}} + message => iolist_to_binary(io_lib:format("~p~n", [Reason]))}} end. %%------------------------------------------------------------------------------ @@ -169,30 +177,6 @@ lookup(Topic, Req, Formatter) -> {200, format_message(Msgs, Formatter)}. -message_schema(Type, Properties) -> - #{Type => #{type => object, - properties => Properties()}}. - -message_properties() -> - #{msgid => #{type => string, - description => <<"Message ID">>}, - topic => #{type => string, - description => <<"Topic">>}, - qos => #{type => integer, - enum => [0, 1, 2], - description => <<"Qos">>}, - publish_at => #{type => string, - description => <<"publish datetime">>}, - from_clientid => #{type => string, - description => <<"Message from">>}, - from_username => #{type => string, - description => <<"publish username">>}}. - -detail_message_properties() -> - Base = message_properties(), - Base#{payload => #{type => string, - description => <<"Topic">>}}. - format_message(Messages, Formatter) when is_list(Messages)-> [Formatter(Message) || Message <- Messages]; diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index 0efd2d479..a859d4d66 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -20,7 +20,8 @@ -include("emqx_statsd.hrl"). --import(emqx_mgmt_util, [response_schema/2, request_body_schema/1]). +-import(emqx_mgmt_util, [ schema/1 + , bad_request/0]). -export([api_spec/0]). @@ -37,24 +38,14 @@ statsd_api() -> Metadata = #{ get => #{ description => <<"Get statsd info">>, - responses => #{ - <<"200">> => response_schema(<<>>, statsd) - } + responses => #{<<"200">> => schema(statsd)} }, put => #{ description => <<"Update Statsd">>, - 'requestBody' => request_body_schema(statsd), + 'requestBody' => schema(statsd), responses => #{ - <<"200">> => - response_schema(<<>>, statsd), - <<"400">> => - response_schema(<<"Bad Request">>, #{ - type => object, - properties => #{ - message => #{type => string}, - code => #{type => string} - } - }) + <<"200">> => schema(statsd), + <<"400">> => bad_request() } } }, From 5a87d941f62f90bc7ebd295d114a49770e531f1e Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 10:47:42 +0800 Subject: [PATCH 098/306] refactor(retainer): refactor emqx_retainer test case --- apps/emqx_retainer/src/emqx_retainer.erl | 4 +- apps/emqx_retainer/src/emqx_retainer_api.erl | 6 +- .../test/emqx_retainer_SUITE.erl | 67 +++++++------------ .../test/mqtt_protocol_v5_SUITE.erl | 29 +++++--- 4 files changed, 48 insertions(+), 58 deletions(-) diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 65e79ec40..7a37abbee 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -187,8 +187,8 @@ init([]) -> end}. handle_call({update_config, Conf}, _, State) -> - State2 = update_config(State, Conf), - _ = emqx:update_config([?APP], Conf), + {ok, Config} = emqx:update_config([?APP], Conf), + State2 = update_config(State, maps:get(config, Config)), {reply, ok, State2}; handle_call({wait_semaphore, Id}, From, #{wait_quotas := Waits} = State) -> diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 34e75e567..704d12deb 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -137,11 +137,7 @@ config(put, Req) -> try {ok, Body, _} = cowboy_req:read_body(Req), Cfg = emqx_json:decode(Body), - {ok, RawConf} = hocon:binary(jsx:encode(#{<<"mqtt_retainer">> => Cfg}), - #{format => richmap}), - RichConf = hocon_schema:check(emqx_retainer_schema, RawConf, #{atom_key => true}), - #{mqtt_retainer := Conf} = hocon_schema:richmap_to_map(RichConf), - emqx_retainer:update_config(Conf), + emqx_retainer:update_config(Cfg), {200, #{<<"content-type">> => <<"text/plain">>}, <<"Update configs successfully">>} catch _:Reason:_ -> {400, diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index a2efd0357..0348e065a 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl @@ -26,59 +26,38 @@ all() -> emqx_ct:all(?MODULE). +-define(BASE_CONF, <<""" +emqx_retainer { + enable = true + msg_clear_interval = 0s + msg_expiry_interval = 0s + max_payload_size = 1MB + flow_control { + max_read_number = 0 + msg_deliver_quota = 0 + quota_release_interval = 0s + } + config { + type = built_in_database + storage_type = ram + max_retained_messages = 0 + } + }""">>). + %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- init_per_suite(Config) -> - application:stop(emqx_retainer), - emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), + ok = emqx_config:init_load(emqx_retainer_schema, ?BASE_CONF), + emqx_ct_helpers:start_apps([emqx_retainer]), Config. end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([emqx_retainer]). - -init_per_testcase(TestCase, Config) -> - emqx_retainer:clean(), - DefaultCfg = new_emqx_retainer_conf(), - NewCfg = case TestCase of - t_message_expiry_2 -> - DefaultCfg#{msg_expiry_interval := 2000}; - t_flow_control -> - DefaultCfg#{flow_control := #{max_read_number => 1, - msg_deliver_quota => 1, - quota_release_interval => timer:seconds(1)}}; - _ -> - DefaultCfg - end, - emqx_retainer:update_config(NewCfg), - application:ensure_all_started(emqx_retainer), - Config. - -set_special_configs(emqx_retainer) -> - init_emqx_retainer_conf(); -set_special_configs(_) -> - ok. - -init_emqx_retainer_conf() -> - emqx_config:put([?APP], new_emqx_retainer_conf()). - -new_emqx_retainer_conf() -> - #{enable => true, - msg_expiry_interval => 0, - msg_clear_interval => 0, - config => #{type => built_in_database, - max_retained_messages => 0, - storage_type => ram}, - flow_control => #{max_read_number => 0, - msg_deliver_quota => 0, - quota_release_interval => 0}, - max_payload_size => 1024 * 1024}. - %%-------------------------------------------------------------------- %% Test Cases %%-------------------------------------------------------------------- - t_store_and_clean(_) -> {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), {ok, _} = emqtt:connect(C1), @@ -184,13 +163,14 @@ t_message_expiry(_) -> ok = emqtt:disconnect(C1). t_message_expiry_2(_) -> + emqx_retainer:update_config(#{<<"msg_expiry_interval">> => <<"2s">>}), {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), {ok, _} = emqtt:connect(C1), emqtt:publish(C1, <<"retained">>, <<"expire">>, [{qos, 0}, {retain, true}]), {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]), ?assertEqual(1, length(receive_messages(1))), - timer:sleep(3000), + timer:sleep(4000), {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]), ?assertEqual(0, length(receive_messages(1))), {ok, #{}, [0]} = emqtt:unsubscribe(C1, <<"retained">>), @@ -216,6 +196,9 @@ t_clean(_) -> ok = emqtt:disconnect(C1). t_flow_control(_) -> + emqx_retainer:update_config(#{<<"flow_control">> => #{<<"max_read_number">> => 1, + <<"msg_deliver_quota">> => 1, + <<"quota_release_interval">> => <<"1s">>}}), {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), {ok, _} = emqtt:connect(C1), emqtt:publish(C1, <<"retained/0">>, <<"this is a retained message 0">>, [{qos, 0}, {retain, true}]), diff --git a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl b/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl index 70c8a0554..cba40de69 100644 --- a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl @@ -21,27 +21,38 @@ -include_lib("eunit/include/eunit.hrl"). +-define(BASE_CONF, <<""" +emqx_retainer { + enable = true + msg_clear_interval = 0s + msg_expiry_interval = 0s + max_payload_size = 1MB + flow_control { + max_read_number = 0 + msg_deliver_quota = 0 + quota_release_interval = 0s + } + config { + type = built_in_database + storage_type = ram + max_retained_messages = 0 + } + }""">>). + all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_retainer_schema, ?BASE_CONF), %% Meck emqtt ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]), %% Start Apps - emqx_ct_helpers:start_apps([emqx_retainer], fun set_special_configs/1), + emqx_ct_helpers:start_apps([emqx_retainer]), Config. end_per_suite(_Config) -> ok = meck:unload(emqtt), emqx_ct_helpers:stop_apps([emqx_retainer]). -%%-------------------------------------------------------------------- -%% Helpers -%%-------------------------------------------------------------------- -set_special_configs(emqx_retainer) -> - emqx_retainer_SUITE:init_emqx_retainer_conf(); -set_special_configs(_) -> - ok. - client_info(Key, Client) -> maps:get(Key, maps:from_list(emqtt:info(Client)), undefined). From f1ba482ed6f0ee2e0ba4f83b92cd7582031d31c4 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Tue, 24 Aug 2021 11:04:39 +0800 Subject: [PATCH 099/306] fix: alarms api page & limit parameters --- .../src/emqx_mgmt_api_alarms.erl | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 8f26f9075..87face878 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -49,7 +49,7 @@ alarms_api() -> Metadata = #{ get => #{ description => <<"EMQ X alarms">>, - parameters => [#{ + parameters => emqx_mgmt_util:page_params() ++ [#{ name => activated, in => query, description => <<"All alarms, if not specified">>, @@ -69,26 +69,25 @@ alarms_api() -> %%%============================================================================================== %% parameters trans alarms(get, Request) -> - case proplists:get_value(<<"activated">>, cowboy_req:parse_qs(Request), undefined) of - undefined -> - list(#{activated => undefined}); - <<"true">> -> - list(#{activated => true}); - <<"false">> -> - list(#{activated => false}) - end; + Params = cowboy_req:parse_qs(Request), + list(Params); alarms(delete, _Request) -> delete(). %%%============================================================================================== %% api apply -list(#{activated := true}) -> - do_list(activated); -list(#{activated := false}) -> - do_list(deactivated); -list(#{activated := undefined}) -> - do_list(activated). +list(Params) -> + {Table, Function} = + case proplists:get_value(<<"activated">>, Params, <<"true">>) of + <<"true">> -> + {?ACTIVATED_ALARM, query_activated}; + <<"false">> -> + {?DEACTIVATED_ALARM, query_deactivated} + end, + Params1 = proplists:delete(<<"activated">>, Params), + Response = emqx_mgmt_api:cluster_query(Params1, {Table, []}, {?MODULE, Function}), + {200, Response}. delete() -> _ = emqx_mgmt:delete_all_deactivated_alarms(), @@ -96,17 +95,6 @@ delete() -> %%%============================================================================================== %% internal -do_list(Type) -> - {Table, Function} = - case Type of - activated -> - {?ACTIVATED_ALARM, query_activated}; - deactivated -> - {?DEACTIVATED_ALARM, query_deactivated} - end, - Response = emqx_mgmt_api:cluster_query([], {Table, []}, {?MODULE, Function}), - {200, Response}. - query_activated(_, Start, Limit) -> query(?ACTIVATED_ALARM, Start, Limit). From 0cb5c3e6ec3860a8e6e32125b2d70c6deec0da80 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 11:15:25 +0800 Subject: [PATCH 100/306] refactor(topic-metrics): refactor topic_metrics api --- .../src/emqx_topic_metrics_api.erl | 141 +++++++----------- 1 file changed, 58 insertions(+), 83 deletions(-) diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index 1f16b3759..323363b72 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -18,11 +18,11 @@ -behavior(minirest_api). --import(emqx_mgmt_util, [ request_body_schema/1 - , response_schema/1 - , response_schema/2 - , response_array_schema/2 - , response_error_schema/2 +-import(emqx_mgmt_util, [ properties/1 + , schema/1 + , object_schema/2 + , object_array_schema/2 + , error_schema/2 ]). -export([api_spec/0]). @@ -49,113 +49,88 @@ api_spec() -> reset_all_topic_metrics_api(), reset_topic_metrics_api() ], - [ - topic_metrics_schema() - ] + [] }. -topic_metrics_schema() -> - #{ - topic_metrics => #{ - type => object, - properties => #{ - topic => #{type => string}, - create_time => #{ - type => string, - description => <<"Date time, rfc3339">> - }, - reset_time => #{ - type => string, - description => <<"Nullable. Date time, rfc3339.">> - }, - metrics => #{ - type => object, - properties => #{ - 'messages.dropped.count' => #{type => integer}, - 'messages.dropped.rate' => #{type => number}, - 'messages.in.count' => #{type => integer}, - 'messages.in.rate' => #{type => number}, - 'messages.out.count' => #{type => integer}, - 'messages.out.rate' => #{type => number}, - 'messages.qos0.in.count' => #{type => integer}, - 'messages.qos0.in.rate' => #{type => number}, - 'messages.qos0.out.count' => #{type => integer}, - 'messages.qos0.out.rate' => #{type => number}, - 'messages.qos1.in.count' => #{type => integer}, - 'messages.qos1.in.rate' => #{type => number}, - 'messages.qos1.out.count' => #{type => integer}, - 'messages.qos1.out.rate' => #{type => number}, - 'messages.qos2.in.count' => #{type => integer}, - 'messages.qos2.in.rate' => #{type => number}, - 'messages.qos2.out.count' => #{type => integer}, - 'messages.qos2.out.rate' => #{type => number} - } - } - } - } - }. +properties() -> + properties([ + {topic, string}, + {create_time, string, <<"Date time, rfc3339">>}, + {reset_time, string, <<"Nullable. Date time, rfc3339.">>}, + {metrics, object, [{'messages.dropped.count', integer}, + {'messages.dropped.rate', number}, + {'messages.in.count', integer}, + {'messages.in.rate', number}, + {'messages.out.count', integer}, + {'messages.out.rate', number}, + {'messages.qos0.in.count', integer}, + {'messages.qos0.in.rate', number}, + {'messages.qos0.out.count', integer}, + {'messages.qos0.out.rate', number}, + {'messages.qos1.in.count', integer}, + {'messages.qos1.in.rate', number}, + {'messages.qos1.out.count', integer}, + {'messages.qos1.out.rate', number}, + {'messages.qos2.in.count', integer}, + {'messages.qos2.in.rate', number}, + {'messages.qos2.out.count', integer}, + {'messages.qos2.out.rate', number}]} + ]). + list_topic_api() -> - Path = "/mqtt/topic_metrics", - TopicSchema = #{ - type => object, - properties => #{ - topic => #{ - type => string}}}, + Props = properties([{topic, string}]), MetaData = #{ get => #{ description => <<"List topic">>, - responses => #{ - <<"200">> => - response_array_schema(<<"List topic">>, TopicSchema)}}}, - {Path, MetaData, list_topic}. + responses => #{<<"200">> => object_array_schema(Props, <<"List topic">>)} + } + }, + {"/mqtt/topic_metrics", MetaData, list_topic}. list_topic_metrics_api() -> - Path = "/mqtt/topic_metrics/metrics", MetaData = #{ get => #{ description => <<"List topic metrics">>, responses => #{ - <<"200">> => - response_array_schema(<<"List topic metrics">>, topic_metrics)}}}, - {Path, MetaData, list_topic_metrics}. + <<"200">> => object_array_schema(properties(), <<"List topic metrics">>) + } + } + }, + {"/mqtt/topic_metrics/metrics", MetaData, list_topic_metrics}. get_topic_metrics_api() -> - Path = "/mqtt/topic_metrics/metrics/:topic", MetaData = #{ get => #{ description => <<"List topic metrics">>, parameters => [topic_param()], responses => #{ - <<"200">> => - response_schema(<<"List topic metrics">>, topic_metrics)}}, + <<"200">> => object_schema(properties(), <<"List topic metrics">>)}}, put => #{ description => <<"Register topic metrics">>, parameters => [topic_param()], responses => #{ - <<"200">> => - response_schema(<<"Register topic metrics">>), - <<"409">> => - response_error_schema(<<"Topic metrics max limit">>, [?EXCEED_LIMIT]), - <<"400">> => - response_error_schema(<<"Topic metrics already exist">>, [?BAD_REQUEST])}}, + <<"200">> => schema(<<"Register topic metrics">>), + <<"409">> => error_schema(<<"Topic metrics max limit">>, [?EXCEED_LIMIT]), + <<"400">> => error_schema(<<"Topic metrics already exist">>, [?BAD_REQUEST]) + } + }, delete => #{ description => <<"Deregister topic metrics">>, parameters => [topic_param()], - responses => #{ - <<"200">> => - response_schema(<<"Deregister topic metrics">>)}}}, - {Path, MetaData, operate_topic_metrics}. + responses => #{ <<"200">> => schema(<<"Deregister topic metrics">>)} + } + }, + {"/mqtt/topic_metrics/metrics/:topic", MetaData, operate_topic_metrics}. reset_all_topic_metrics_api() -> - Path = "/mqtt/topic_metrics/reset", MetaData = #{ put => #{ description => <<"Reset all topic metrics">>, - responses => #{ - <<"200">> => - response_schema(<<"Reset all topic metrics">>)}}}, - {Path, MetaData, reset_all_topic_metrics}. + responses => #{<<"200">> => schema(<<"Reset all topic metrics">>)} + } + }, + {"/mqtt/topic_metrics/reset", MetaData, reset_all_topic_metrics}. reset_topic_metrics_api() -> Path = "/mqtt/topic_metrics/reset/:topic", @@ -163,9 +138,9 @@ reset_topic_metrics_api() -> put => #{ description => <<"Reset topic metrics">>, parameters => [topic_param()], - responses => #{ - <<"200">> => - response_schema(<<"Reset topic metrics">>)}}}, + responses => #{<<"200">> => schema(<<"Reset topic metrics">>)} + } + }, {Path, MetaData, reset_topic_metrics}. topic_param() -> From 41f2b77ec3d99075fa67500340feda51f3255cff Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 23 Aug 2021 09:41:37 +0800 Subject: [PATCH 101/306] chore(CI): update workflows Signed-off-by: zhanghongtong --- .github/workflows/run_fvt_tests.yaml | 456 ++++++++++--------------- .github/workflows/run_relup_tests.yaml | 130 +++++++ build | 2 +- 3 files changed, 302 insertions(+), 286 deletions(-) create mode 100644 .github/workflows/run_relup_tests.yaml diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 272d69ca7..a4b9df5c2 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -8,300 +8,186 @@ on: pull_request: jobs: - docker_test: - runs-on: ubuntu-20.04 + prepare: + strategy: + matrix: + container: + - "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + - "emqx/build-env:erl24.0.5-emqx-1-ubuntu20.04" - steps: - - uses: actions/checkout@v1 - - uses: gleam-lang/setup-erlang@v1.1.2 - id: install_erlang - with: - otp-version: 24.0.5 - - name: prepare - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials - git config --global credential.helper store - echo "${{ secrets.CI_GIT_TOKEN }}" >> scripts/git-token - make deps-emqx-ee - echo "PROFILE=emqx-ee" >> $GITHUB_ENV - echo "TARGET=emqx/emqx-ee" >> $GITHUB_ENV - echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV - else - echo "PROFILE=emqx" >> $GITHUB_ENV - echo "TARGET=emqx/emqx" >> $GITHUB_ENV - echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV - fi - - name: make emqx image - run: make $PROFILE-docker - - name: run emqx - timeout-minutes: 5 - run: | - set -e -u -x - echo "HOCON_ENV_OVERRIDE_PREFIX=EMQX_" >> .ci/docker-compose-file/conf.cluster.env - echo "EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s" >> .ci/docker-compose-file/conf.cluster.env - echo "EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10" >> .ci/docker-compose-file/conf.cluster.env - docker-compose \ - -f .ci/docker-compose-file/docker-compose-emqx-cluster.yaml \ - -f .ci/docker-compose-file/docker-compose-python.yaml \ - up -d - while ! docker exec -i node1.emqx.io bash -c "emqx eval \"['emqx@node1.emqx.io','emqx@node2.emqx.io'] = maps:get(running_nodes, ekka_cluster:info()).\"" > /dev/null 2>&1; do - 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: make paho tests - run: | - if ! docker exec -i python /scripts/pytest.sh; then - echo "DUMP_CONTAINER_LOGS_BGN" - docker logs haproxy - docker logs node1.emqx.io - docker logs node2.emqx.io - echo "DUMP_CONTAINER_LOGS_END" - exit 1 - fi + runs-on: ubuntu-20.04 + container: ${{ matrix.container }} - helm_test: - runs-on: ubuntu-20.04 + outputs: + profile: ${{ steps.profile.outputs.profile }} - steps: - - uses: actions/checkout@v1 - - uses: gleam-lang/setup-erlang@v1.1.2 - id: install_erlang - with: - otp-version: 24.0.5 - - name: prepare - run: | - if make emqx-ee --dry-run > /dev/null 2>&1; then - echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials - git config --global credential.helper store - echo "${{ secrets.CI_GIT_TOKEN }}" >> scripts/git-token - make deps-emqx-ee - echo "TARGET=emqx/emqx-ee" >> $GITHUB_ENV - echo "PROFILE=emqx-ee" >> $GITHUB_ENV - else - echo "TARGET=emqx/emqx" >> $GITHUB_ENV - echo "PROFILE=emqx" >> $GITHUB_ENV - fi - - name: make emqx image - run: make $PROFILE-docker - - name: install k3s - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - sudo sh -c "echo \"127.0.0.1 $(hostname)\" >> /etc/hosts" - curl -sfL https://get.k3s.io | sh - - sudo chmod 644 /etc/rancher/k3s/k3s.yaml - kubectl cluster-info - - name: install helm - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 - sudo chmod 700 get_helm.sh - sudo ./get_helm.sh - helm version - - name: run emqx on chart - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - timeout-minutes: 5 - run: | - version=$(./pkg-vsn.sh) - sudo docker save ${TARGET}:$version -o emqx.tar.gz - sudo k3s ctr image import emqx.tar.gz + steps: + - name: get otp version + id: get_otp_version + run: | + otp="$(erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().' -noshell)" + echo "::set-output name=otp::$otp" + - uses: actions/checkout@v2 + with: + path: source + fetch-depth: 0 + - name: set profile + id: profile + shell: bash + working-directory: source + run: | + vsn="$(./pkg-vsn.sh)" + if make emqx-ee --dry-run > /dev/null 2>&1; then + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + echo "::set-output name=profile::emqx-ee" + else + echo "::set-output name=profile::emqx" + fi + - name: get deps + working-directory: source + run: | + make ensure-rebar3 + ./rebar3 as default get-deps + rm -rf rebar.lock + - name: gen zip file + run: zip -ryq source-${{ steps.get_otp_version.outputs.otp }}.zip source/* source/.[^.]* + - uses: actions/upload-artifact@v2 + with: + name: source-${{ steps.get_otp_version.outputs.otp }} + path: source-${{ steps.get_otp_version.outputs.otp }}.zip - sed -i -r "s/^appVersion: .*$/appVersion: \"${version}\"/g" deploy/charts/emqx/Chart.yaml - sed -i '/emqx_telemetry/d' deploy/charts/emqx/values.yaml + docker_test: + runs-on: ubuntu-20.04 + needs: prepare - helm install emqx \ - --set image.repository=${TARGET} \ - --set image.pullPolicy=Never \ - --set emqxAclConfig="" \ - --set image.pullPolicy=Never \ - --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s \ - --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10 \ - deploy/charts/emqx \ - --debug + strategy: + fail-fast: false + matrix: + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 - while [ "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.replicas}')" \ - != "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.readyReplicas}')" ]; do - echo "=============================="; - kubectl get pods; - echo "=============================="; - echo "waiting emqx started"; - sleep 10; - done - - name: get emqx-0 pods log - if: failure() - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - 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 - ref: develop-4.0 - path: paho.mqtt.testing - - name: install pytest - run: | - pip install pytest - echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: run paho test - env: - KUBECONFIG: "/etc/rancher/k3s/k3s.yaml" - run: | - emqx_svc=$(kubectl get svc --namespace default emqx -o jsonpath="{.spec.clusterIP}") - emqx1=$(kubectl get pods emqx-1 -o jsonpath='{.status.podIP}') - emqx2=$(kubectl get pods emqx-2 -o jsonpath='{.status.podIP}') + steps: + - uses: actions/download-artifact@v2 + with: + name: source-${{ matrix.otp }} + path: . + - name: unzip source code + run: unzip -q source-${{ matrix.otp }}.zip + - name: make docker image + working-directory: source + env: + OTP: ${{ matrix.otp }} + run: | + make ${{ needs.prepare.outputs.profile }}-docker + echo "TARGET=emqx/${{ needs.prepare.outputs.profile }}" >> $GITHUB_ENV + echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV + - name: run emqx + timeout-minutes: 5 + working-directory: source + run: | + set -e -u -x + echo "HOCON_ENV_OVERRIDE_PREFIX=EMQX_" >> .ci/docker-compose-file/conf.cluster.env + echo "EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s" >> .ci/docker-compose-file/conf.cluster.env + echo "EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10" >> .ci/docker-compose-file/conf.cluster.env + docker-compose \ + -f .ci/docker-compose-file/docker-compose-emqx-cluster.yaml \ + -f .ci/docker-compose-file/docker-compose-python.yaml \ + up -d + while ! docker exec -i node1.emqx.io bash -c "emqx eval \"['emqx@node1.emqx.io','emqx@node2.emqx.io'] = maps:get(running_nodes, ekka_cluster:info()).\"" > /dev/null 2>&1; do + echo "['$(date -u +"%Y-%m-%dT%H:%M:%SZ")']:waiting emqx"; + sleep 5; + done + - name: make paho tests + run: | + if ! docker exec -i python /scripts/pytest.sh; then + echo "DUMP_CONTAINER_LOGS_BGN" + docker logs haproxy + docker logs node1.emqx.io + docker logs node2.emqx.io + echo "DUMP_CONTAINER_LOGS_END" + exit 1 + fi - pytest -v paho.mqtt.testing/interoperability/test_client/V5/test_connect.py -k test_basic --host $emqx_svc - RESULT=$? - pytest -v paho.mqtt.testing/interoperability/test_cluster --host1 $emqx1 --host2 $emqx2 - RESULT=$((RESULT + $?)) - if [ 0 -ne $RESULT ]; then - kubectl logs emqx-1 - kubectl logs emqx-2 - fi - exit $RESULT + helm_test: + runs-on: ubuntu-20.04 + needs: prepare - relup_test: - strategy: - matrix: - container: - - "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" - - "emqx/build-env:erl24.0.5-emqx-1-ubuntu20.04" + strategy: + fail-fast: false + matrix: + otp: + - 23.2.7.2-emqx-2 + - 24.0.5-emqx-1 - runs-on: ubuntu-20.04 - container: ${{ matrix.container }} + steps: + - uses: actions/download-artifact@v2 + with: + name: source-${{ matrix.otp }} + path: . + - name: unzip source code + run: unzip -q source-${{ matrix.otp }}.zip + - name: make docker image + working-directory: source + env: + OTP: ${{ matrix.otp }} + run: | + make ${{ needs.prepare.outputs.profile }}-docker + echo "TARGET=emqx/${{ needs.prepare.outputs.profile }}" >> $GITHUB_ENV + echo "EMQX_TAG=$(./pkg-vsn.sh)" >> $GITHUB_ENV + - run: minikube start + - name: run emqx on chart + timeout-minutes: 5 + working-directory: source + run: | + minikube image load $TARGET:$EMQX_TAG - defaults: - run: - shell: bash - steps: - - uses: actions/setup-python@v2 - with: - python-version: '3.8' - architecture: 'x64' - - uses: actions/checkout@v2 - with: - repository: emqx/paho.mqtt.testing - ref: develop-4.0 - path: paho.mqtt.testing - - uses: actions/checkout@v2 - with: - repository: terry-xiaoyu/one_more_emqx - ref: master - path: one_more_emqx - - uses: actions/checkout@v2 - with: - repository: emqx/emqtt-bench - ref: master - path: emqtt-bench - - uses: actions/checkout@v2 - with: - repository: hawk/lux - ref: lux-2.6 - path: lux - - uses: actions/checkout@v2 - with: - repository: ${{ github.repository }} - path: emqx - fetch-depth: 0 - - name: prepare - run: | - if make -C emqx emqx-ee --dry-run > /dev/null 2>&1; then - echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials - git config --global credential.helper store - echo "${{ secrets.CI_GIT_TOKEN }}" >> emqx/scripts/git-token - echo "PROFILE=emqx-ee" >> $GITHUB_ENV - else - echo "PROFILE=emqx" >> $GITHUB_ENV - fi - - name: get version - run: | - set -e -x -u - cd emqx - if [ $PROFILE = "emqx" ];then - broker="emqx-ce" - edition='opensource' - else - broker="emqx-ee" - edition='enterprise' - fi - echo "BROKER=$broker" >> $GITHUB_ENV + sed -i -r "s/^appVersion: .*$/appVersion: \"$EMQX_TAG\"/g" deploy/charts/emqx/Chart.yaml - vsn="$(./pkg-vsn.sh)" - echo "VSN=$vsn" >> $GITHUB_ENV + helm install emqx \ + --set image.repository=$TARGET \ + --set image.pullPolicy=Never \ + --set emqxAclConfig="" \ + --set image.pullPolicy=Never \ + --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__RETRY_INTERVAL=2s \ + --set emqxConfig.EMQX_ZONES__DEFAULT__MQTT__MAX_TOPIC_ALIAS=10 \ + deploy/charts/emqx \ + --debug - pre_vsn="$(echo $vsn | grep -oE '^[0-9]+.[0-9]')" - if [ $PROFILE = "emqx" ]; then - old_vsns="$(git tag -l "v$pre_vsn.[0-9]" | xargs echo -n | sed "s/v$vsn//")" - else - old_vsns="$(git tag -l "e$pre_vsn.[0-9]" | xargs echo -n | sed "s/e$vsn//")" - fi - echo "OLD_VSNS=$old_vsns" >> $GITHUB_ENV - - name: download emqx - run: | - set -e -x -u - mkdir -p emqx/_upgrade_base - cd emqx/_upgrade_base - old_vsns=($(echo $OLD_VSNS | tr ' ' ' ')) - for old_vsn in ${old_vsns[@]}; do - wget --no-verbose https://s3-us-west-2.amazonaws.com/packages.emqx/$BROKER/$old_vsn/$PROFILE-ubuntu20.04-${old_vsn#[e|v]}-amd64.zip - done - - name: build emqx - run: make -C emqx ${PROFILE}-zip - - name: build emqtt-bench - run: make -C emqtt-bench - - name: build lux - run: | - set -e -u -x - cd lux - autoconf - ./configure - make - make install - - name: run relup test - timeout-minutes: 20 - run: | - set -e -x -u - if [ -n "$OLD_VSNS" ]; then - mkdir -p packages - cp emqx/_packages/${PROFILE}/*.zip packages - cp emqx/_upgrade_base/*.zip packages - lux \ - --case_timeout infinity \ - --var PROFILE=$PROFILE \ - --var PACKAGE_PATH=$(pwd)/packages \ - --var BENCH_PATH=$(pwd)/emqtt-bench \ - --var ONE_MORE_EMQX_PATH=$(pwd)/one_more_emqx \ - --var VSN="$VSN" \ - --var OLD_VSNS="$OLD_VSNS" \ - emqx/.ci/fvt_tests/relup.lux - fi - - uses: actions/upload-artifact@v1 - if: failure() - with: - name: lux_logs - path: lux_logs + while [ "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.replicas}')" \ + != "$(kubectl get StatefulSet -l app.kubernetes.io/name=emqx -o jsonpath='{.items[0].status.readyReplicas}')" ]; do + echo "=============================="; + kubectl get pods; + echo "=============================="; + echo "waiting emqx started"; + sleep 10; + done + - name: get emqx-0 pods log + if: failure() + run: | + kubectl describe pods emqx-0 + kubectl logs emqx-0 + - name: get emqx-1 pods log + if: failure() + run: | + kubectl describe pods emqx-1 + kubectl logs emqx-1 + - name: get emqx-2 pods log + if: failure() + run: | + kubectl describe pods emqx-2 + kubectl logs emqx-2 + - uses: actions/checkout@v2 + with: + repository: emqx/paho.mqtt.testing + ref: develop-4.0 + path: paho.mqtt.testing + - name: install pytest + run: | + pip install pytest + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: run paho test + run: | + kubectl port-forward service/emqx 1883:1883 > /dev/null & + pytest -v paho.mqtt.testing/interoperability/test_client/V5/test_connect.py -k test_basic --host "127.0.0.1" diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml new file mode 100644 index 000000000..312ef1152 --- /dev/null +++ b/.github/workflows/run_relup_tests.yaml @@ -0,0 +1,130 @@ +name: Release Upgrade Tests + +on: + push: + tags: + - v* + - e* + pull_request: + +jobs: + relup_test: + strategy: + matrix: + container: + - "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + - "emqx/build-env:erl24.0.5-emqx-1-ubuntu20.04" + + runs-on: ubuntu-20.04 + container: ${{ matrix.container }} + + defaults: + run: + shell: bash + steps: + - uses: actions/setup-python@v2 + with: + python-version: '3.8' + architecture: 'x64' + - uses: actions/checkout@v2 + with: + repository: emqx/paho.mqtt.testing + ref: develop-4.0 + path: paho.mqtt.testing + - uses: actions/checkout@v2 + with: + repository: terry-xiaoyu/one_more_emqx + ref: master + path: one_more_emqx + - uses: actions/checkout@v2 + with: + repository: emqx/emqtt-bench + ref: master + path: emqtt-bench + - uses: actions/checkout@v2 + with: + repository: hawk/lux + ref: lux-2.6 + path: lux + - uses: actions/checkout@v2 + with: + repository: ${{ github.repository }} + path: emqx + fetch-depth: 0 + - name: prepare + run: | + if make -C emqx emqx-ee --dry-run > /dev/null 2>&1; then + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + echo "${{ secrets.CI_GIT_TOKEN }}" >> emqx/scripts/git-token + echo "PROFILE=emqx-ee" >> $GITHUB_ENV + else + echo "PROFILE=emqx" >> $GITHUB_ENV + fi + - name: get version + run: | + set -e -x -u + cd emqx + if [ $PROFILE = "emqx" ];then + broker="emqx-ce" + edition='opensource' + else + broker="emqx-ee" + edition='enterprise' + fi + echo "BROKER=$broker" >> $GITHUB_ENV + + vsn="$(./pkg-vsn.sh)" + echo "VSN=$vsn" >> $GITHUB_ENV + + pre_vsn="$(echo $vsn | grep -oE '^[0-9]+.[0-9]')" + if [ $PROFILE = "emqx" ]; then + old_vsns="$(git tag -l "v$pre_vsn.[0-9]" | xargs echo -n | sed "s/v$vsn//")" + else + old_vsns="$(git tag -l "e$pre_vsn.[0-9]" | xargs echo -n | sed "s/e$vsn//")" + fi + echo "OLD_VSNS=$old_vsns" >> $GITHUB_ENV + - name: download emqx + run: | + set -e -x -u + mkdir -p emqx/_upgrade_base + cd emqx/_upgrade_base + old_vsns=($(echo $OLD_VSNS | tr ' ' ' ')) + for old_vsn in ${old_vsns[@]}; do + wget --no-verbose https://s3-us-west-2.amazonaws.com/packages.emqx/$BROKER/$old_vsn/$PROFILE-ubuntu20.04-${old_vsn#[e|v]}-amd64.zip + done + - name: build emqx + run: make -C emqx ${PROFILE}-zip + - name: build emqtt-bench + run: make -C emqtt-bench + - name: build lux + run: | + set -e -u -x + cd lux + autoconf + ./configure + make + make install + - name: run relup test + timeout-minutes: 20 + run: | + set -e -x -u + if [ -n "$OLD_VSNS" ]; then + mkdir -p packages + cp emqx/_packages/${PROFILE}/*.zip packages + cp emqx/_upgrade_base/*.zip packages + lux \ + --case_timeout infinity \ + --var PROFILE=$PROFILE \ + --var PACKAGE_PATH=$(pwd)/packages \ + --var BENCH_PATH=$(pwd)/emqtt-bench \ + --var ONE_MORE_EMQX_PATH=$(pwd)/one_more_emqx \ + --var VSN="$VSN" \ + --var OLD_VSNS="$OLD_VSNS" \ + emqx/.ci/fvt_tests/relup.lux + fi + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: lux_logs + path: lux_logs diff --git a/build b/build index 2b030a1de..727c3a1a2 100755 --- a/build +++ b/build @@ -128,7 +128,7 @@ make_docker() { docker build --no-cache \ --build-arg PKG_VSN="$PKG_VSN" \ - --build-arg BUILD_FROM=emqx/build-env:erl23.2.7.2-emqx-2-alpine \ + --build-arg BUILD_FROM="emqx/build-env:erl${OTP:-23.2.7.2-emqx-2}-alpine" \ --build-arg EMQX_NAME="$PROFILE" \ --tag "emqx/$PROFILE:$PKG_VSN" \ -f deploy/docker/Dockerfile . From 01113062244041286cf710764189b184c0b98c00 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Tue, 24 Aug 2021 16:07:28 +0800 Subject: [PATCH 102/306] chore(CI): fix log attachment overwrite error --- .github/workflows/run_test_cases.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index c2b30ed3d..fcd5e8374 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -106,11 +106,11 @@ jobs: - uses: actions/upload-artifact@v1 if: failure() with: - name: logs + name: logs_${{ matrix.otp_release }} path: _build/test/logs - uses: actions/upload-artifact@v1 with: - name: cover + name: cover_${{ matrix.otp_release }} path: _build/test/cover finish: From e0c05242a7eb23a1fb30fe0261f98d986aba7189 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 15:25:02 +0800 Subject: [PATCH 103/306] refactor(minirest-callback): refactor minirest callback function --- apps/emqx_authn/src/emqx_authn_api.erl | 62 +++++++++---------- apps/emqx_authz/src/emqx_authz_api.erl | 20 +++--- apps/emqx_authz/src/emqx_authz_api_schema.erl | 2 - .../emqx_dashboard/src/emqx_dashboard_api.erl | 25 ++------ .../src/emqx_dashboard_monitor_api.erl | 16 ++--- apps/emqx_management/src/emqx_mgmt_api.erl | 31 ++++++---- .../src/emqx_mgmt_api_alarms.erl | 20 ++---- .../src/emqx_mgmt_api_apps.erl | 17 ++--- .../src/emqx_mgmt_api_clients.erl | 47 +++++--------- .../src/emqx_mgmt_api_configs.erl | 10 +-- .../src/emqx_mgmt_api_listeners.erl | 60 +++++++----------- .../src/emqx_mgmt_api_metrics.erl | 5 +- .../src/emqx_mgmt_api_nodes.erl | 27 +++----- .../src/emqx_mgmt_api_publish.erl | 10 ++- .../src/emqx_mgmt_api_routes.erl | 10 ++- .../src/emqx_mgmt_api_stats.erl | 5 +- .../src/emqx_mgmt_api_status.erl | 2 +- .../src/emqx_mgmt_api_subscriptions.erl | 5 +- apps/emqx_modules/src/emqx_delayed_api.erl | 17 ++--- .../src/emqx_event_message_api.erl | 8 +-- apps/emqx_modules/src/emqx_rewrite_api.erl | 10 ++- apps/emqx_modules/src/emqx_telemetry_api.erl | 8 +-- .../src/emqx_topic_metrics_api.erl | 9 +-- .../src/emqx_prometheus_api.erl | 10 ++- apps/emqx_retainer/src/emqx_retainer_api.erl | 48 +++++++------- apps/emqx_statsd/src/emqx_statsd_api.erl | 10 ++- rebar.config | 2 +- 27 files changed, 198 insertions(+), 298 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 20a8e2f7d..9512dc7f3 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -21,13 +21,13 @@ -include("emqx_authn.hrl"). -export([ api_spec/0 - , authentication/2 - , authenticators/2 - , authenticators2/2 - , move/2 - , import_users/2 - , users/2 - , users2/2 + , authentication/3 + , authenticators/3 + , authenticators2/3 + , move/3 + , import_users/3 + , users/3 + , users2/3 ]). -define(EXAMPLE_1, #{name => <<"example 1">>, @@ -35,7 +35,7 @@ server_type => <<"built-in-database">>, user_id_type => <<"username">>, password_hash_algorithm => #{ - name => <<"sha256">> + name => <<"sha256">> }}). -define(EXAMPLE_2, #{name => <<"example 2">>, @@ -332,7 +332,7 @@ authenticators_api2() -> oneOf => [ minirest:ref(<<"password_based">>) , minirest:ref(<<"jwt">>) , minirest:ref(<<"scram">>) - ] + ] }, examples => #{ example1 => #{ @@ -633,7 +633,7 @@ users2_api() -> type => string }, superuser => #{ - type => boolean + type => boolean } } } @@ -746,7 +746,7 @@ definitions() -> oneOf => [ minirest:ref(<<"password_based">>) , minirest:ref(<<"jwt">>) , minirest:ref(<<"scram">>) - ] + ] }, ReturnedAuthenticatorDef = #{ @@ -763,7 +763,7 @@ definitions() -> oneOf => [ minirest:ref(<<"password_based">>) , minirest:ref(<<"jwt">>) , minirest:ref(<<"scram">>) - ] + ] } ] }, @@ -792,7 +792,7 @@ definitions() -> , minirest:ref(<<"password_based_mongodb">>) , minirest:ref(<<"password_based_redis">>) , minirest:ref(<<"password_based_http_server">>) - ] + ] } ] }, @@ -840,7 +840,7 @@ definitions() -> ssl => minirest:ref(<<"ssl">>) } }, - + SCRAMDef = #{ type => object, required => [name, mechanism, server_type], @@ -1205,7 +1205,7 @@ definitions() -> type => boolean, default => true } - } + } }, PasswordHashAlgorithmDef = #{ @@ -1229,7 +1229,7 @@ definitions() -> properties => #{ enable => #{ type => boolean, - default => false + default => false }, certfile => #{ type => string @@ -1289,7 +1289,7 @@ definitions() -> , #{<<"error">> => ErrorDef} ]. -authentication(post, Request) -> +authentication(post, _Params, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of #{<<"enable">> := Enable} -> @@ -1298,11 +1298,11 @@ authentication(post, Request) -> _ -> serialize_error({missing_parameter, enable}) end; -authentication(get, _Request) -> +authentication(get, _Params, _Request) -> Enabled = emqx_authn:is_enabled(), {200, #{enabled => Enabled}}. -authenticators(post, Request) -> +authenticators(post, _Params, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), Config = emqx_json:decode(Body, [return_maps]), case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of @@ -1313,7 +1313,7 @@ authenticators(post, Request) -> {error, {_, _, Reason}} -> serialize_error(Reason) end; -authenticators(get, _Request) -> +authenticators(get, _Params, _Request) -> RawConfig = get_raw_config([authentication, authenticators]), {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) -> @@ -1321,7 +1321,7 @@ authenticators(get, _Request) -> end, RawConfig, Authenticators), {200, NAuthenticators}. -authenticators2(get, Request) -> +authenticators2(get, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of {ok, #{id := ID, name := Name}} -> @@ -1331,7 +1331,7 @@ authenticators2(get, Request) -> {error, Reason} -> serialize_error(Reason) end; -authenticators2(put, Request) -> +authenticators2(put, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), Config = emqx_json:decode(Body, [return_maps]), @@ -1344,7 +1344,7 @@ authenticators2(put, Request) -> {error, {_, _, Reason}} -> serialize_error(Reason) end; -authenticators2(delete, Request) -> +authenticators2(delete, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of {ok, _} -> @@ -1353,7 +1353,7 @@ authenticators2(delete, Request) -> serialize_error(Reason) end. -move(post, Request) -> +move(post, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of @@ -1366,7 +1366,7 @@ move(post, Request) -> serialize_error({missing_parameter, position}) end. -import_users(post, Request) -> +import_users(post, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of @@ -1379,7 +1379,7 @@ import_users(post, Request) -> serialize_error({missing_parameter, filename}) end. -users(post, Request) -> +users(post, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of @@ -1399,7 +1399,7 @@ users(post, Request) -> _ -> serialize_error({missing_parameter, user_id}) end; -users(get, Request) -> +users(get, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), case emqx_authn:list_users(?CHAIN, AuthenticatorID) of {ok, Users} -> @@ -1408,7 +1408,7 @@ users(get, Request) -> serialize_error(Reason) end. -users2(patch, Request) -> +users2(patch, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), @@ -1425,7 +1425,7 @@ users2(patch, Request) -> serialize_error(Reason) end end; -users2(get, Request) -> +users2(get, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of @@ -1434,7 +1434,7 @@ users2(get, Request) -> {error, Reason} -> serialize_error(Reason) end; -users2(delete, Request) -> +users2(delete, _Params, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of @@ -1467,4 +1467,4 @@ serialize_error({invalid_parameter, Name}) -> )}}; serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, - message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. \ No newline at end of file + message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index a445c7016..693870696 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -40,9 +40,9 @@ topics => [<<"#">>]}). -export([ api_spec/0 - , rules/2 - , rule/2 - , move_rule/2 + , rules/3 + , rule/3 + , move_rule/3 ]). api_spec() -> @@ -418,7 +418,7 @@ move_rule_api() -> }, {"/authorization/:id/move", Metadata, move_rule}. -rules(get, Request) -> +rules(get, _Params, Request) -> Rules = lists:foldl(fun (#{type := _Type, enable := true, annotations := #{id := Id} = Annotations} = Rule, AccIn) -> NRule = case emqx_resource:health_check(Id) of ok -> @@ -445,7 +445,7 @@ rules(get, Request) -> end; false -> {200, #{rules => Rules}} end; -rules(post, Request) -> +rules(post, _Params, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update(head, [RawConfig]) of @@ -454,7 +454,7 @@ rules(post, Request) -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rules(put, Request) -> +rules(put, _Params, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), case emqx_authz:update(replace, RawConfig) of @@ -464,7 +464,7 @@ rules(put, Request) -> messgae => atom_to_binary(Reason)}} end. -rule(get, Request) -> +rule(get, _Params, Request) -> Id = cowboy_req:binding(id, Request), case emqx_authz:lookup(Id) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; @@ -481,7 +481,7 @@ rule(get, Request) -> end end; -rule(put, Request) -> +rule(put, _Params, Request) -> RuleId = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), RawConfig = jsx:decode(Body, [return_maps]), @@ -494,7 +494,7 @@ rule(put, Request) -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rule(delete, Request) -> +rule(delete, _Params, Request) -> RuleId = cowboy_req:binding(id, Request), case emqx_authz:update({replace_once, RuleId}, #{}) of {ok, _} -> {204}; @@ -502,7 +502,7 @@ rule(delete, Request) -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. -move_rule(post, Request) -> +move_rule(post, _Params, Request) -> RuleId = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), #{<<"position">> := Position} = jsx:decode(Body, [return_maps]), diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 2dcc7c564..64ecc58eb 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -31,9 +31,7 @@ definitions() -> }, principal => minirest:ref(<<"principal">>) } - } - } } , minirest:ref(<<"rules">>) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index a7d0adade..ef3808f85 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -158,9 +158,7 @@ change_pwd_api() -> }, {"/users/:username/change_pwd", Metadata, change_pwd}. -login(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +login(post, #{body := Params}) -> Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), case emqx_dashboard_admin:sign_token(Username, Password) of @@ -171,9 +169,7 @@ login(post, Request) -> {401, #{code => Code, message => <<"Auth filed">>}} end. -logout(_, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +logout(_, #{body := Params}) -> Username = maps:get(<<"username">>, Params), emqx_dashboard_admin:destroy_token_by_username(Username), {200}. @@ -181,9 +177,7 @@ logout(_, Request) -> users(get, _Request) -> {200, [row(User) || User <- emqx_dashboard_admin:all_users()]}; -users(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +users(post, #{body := Params}) -> Tag = maps:get(<<"tag">>, Params), Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), @@ -199,10 +193,7 @@ users(post, Request) -> end end. -user(put, Request) -> - Username = cowboy_req:binding(username, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +user(put, #{bindings := #{username := Username}, body := Params}) -> Tag = maps:get(<<"tag">>, Params), case emqx_dashboard_admin:update_user(Username, Tag) of ok -> {200}; @@ -210,8 +201,7 @@ user(put, Request) -> {400, #{code => <<"UPDATE_FAIL">>, message => Reason}} end; -user(delete, Request) -> - Username = cowboy_req:binding(username, Request), +user(delete, #{bindings := #{username := Username}}) -> case Username == <<"admin">> of true -> {400, #{code => <<"CONNOT_DELETE_ADMIN">>, message => <<"Cannot delete admin">>}}; @@ -220,10 +210,7 @@ user(delete, Request) -> {200} end. -change_pwd(put, Request) -> - Username = cowboy_req:binding(username, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), +change_pwd(put, #{bindings := #{username := Username}, body := Params}) -> OldPwd = maps:get(<<"old_pwd">>, Params), NewPwd = maps:get(<<"new_pwd">>, Params), case emqx_dashboard_admin:change_password(Username, OldPwd, NewPwd) of diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index 1193dfad1..c49148534 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -145,24 +145,20 @@ counter_schema() -> type => integer}}}}. %%%============================================================================================== %% parameters trans -monitor(get, Request) -> - Aggregate = proplists:get_value(<<"aggregate">>, cowboy_req:parse_qs(Request), <<"false">>), +monitor(get, #{query_string := Qs}) -> + Aggregate = maps:get(<<"aggregate">>, Qs, <<"false">>), {200, list_collect(Aggregate)}. -monitor_nodes(get, Request) -> - Node = cowboy_req:binding(node, Request), +monitor_nodes(get, #{bindings := #{node := Node}}) -> lookup([{<<"node">>, Node}]). -monitor_nodes_counters(get, Request) -> - Node = cowboy_req:binding(node, Request), - Counter = cowboy_req:binding(counter, Request), +monitor_nodes_counters(get, #{bindings := #{node := Node, counter := Counter}}) -> lookup([{<<"node">>, Node}, {<<"counter">>, Counter}]). -counters(get, Request) -> - Counter = cowboy_req:binding(counter, Request), +counters(get, #{bindings := #{counter := Counter}}) -> lookup([{<<"counter">>, Counter}]). -current_counters(get, _) -> +current_counters(get, _Params) -> Data = [get_collect(Node) || Node <- ekka_mnesia:running_nodes()], Nodes = length(ekka_mnesia:running_nodes()), {Received, Sent, Sub, Conn} = format_current_metrics(Data), diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 61cae83cc..e9aaa2725 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -33,8 +33,8 @@ paginate(Tables, Params, RowFun) -> Qh = query_handle(Tables), Count = count(Tables), - Page = page(Params), - Limit = limit(Params), + Page = b2i(page(Params)), + Limit = b2i(limit(Params)), Cursor = qlc:cursor(Qh), case Page > 1 of true -> @@ -64,14 +64,15 @@ count(Tables) -> count(Table, Nodes) -> lists:sum([rpc_call(Node, ets, info, [Table, size], 5000) || Node <- Nodes]). +page(Params) when is_map(Params) -> + maps:get(<<"page">>, Params, 1); page(Params) -> - binary_to_integer(proplists:get_value(<<"page">>, Params, <<"1">>)). + proplists:get_value(<<"page">>, Params, <<"1">>). +limit(Params) when is_map(Params) -> + maps:get(<<"limit">>, Params, emqx_mgmt:max_row_limit()); limit(Params) -> - case proplists:get_value(<<"limit">>, Params) of - undefined -> emqx_mgmt:max_row_limit(); - Size -> binary_to_integer(Size) - end. + proplists:get_value(<<"limit">>, Params, emqx_mgmt:max_row_limit()). %%-------------------------------------------------------------------- %% Node Query @@ -79,8 +80,8 @@ limit(Params) -> node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), - Limit = limit(Params), - Page = page(Params), + Limit = b2i(limit(Params)), + Page = b2i(page(Params)), Start = if Page > 1 -> (Page-1) * Limit; true -> 0 end, @@ -111,8 +112,8 @@ rpc_call(Node, M, F, A, T) -> cluster_query(Params, {Tab, QsSchema}, QueryFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), - Limit = limit(Params), - Page = page(Params), + Limit = b2i(limit(Params)), + Page = b2i(page(Params)), Start = if Page > 1 -> (Page-1) * Limit; true -> 0 end, @@ -199,6 +200,8 @@ select_n_by_one({Rows0, Cons}, Start, Limit, Acc) -> select_n_by_one(ets:select(Cons), 0, NLimit, [Got|Acc]) end. +params2qs(Params, QsSchema) when is_map(Params) -> + params2qs(maps:to_list(Params), QsSchema); params2qs(Params, QsSchema) -> {Qs, Fuzzy} = pick_params_to_qs(Params, QsSchema, [], []), {length(Qs) + length(Fuzzy), {Qs, Fuzzy}}. @@ -342,3 +345,9 @@ params2qs_test() -> {0, {[], []}} = params2qs([{not_a_predefined_params, val}], Schema). -endif. + + +b2i(Bin) when is_binary(Bin) -> + binary_to_integer(Bin); +b2i(Any) -> + Any. diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 87face878..40956fd11 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -68,28 +68,18 @@ alarms_api() -> %%%============================================================================================== %% parameters trans -alarms(get, Request) -> - Params = cowboy_req:parse_qs(Request), - list(Params); - -alarms(delete, _Request) -> - delete(). - -%%%============================================================================================== -%% api apply -list(Params) -> +alarms(get, #{query_string := Qs}) -> {Table, Function} = - case proplists:get_value(<<"activated">>, Params, <<"true">>) of + case maps:get(<<"activated">>, Qs, <<"true">>) of <<"true">> -> {?ACTIVATED_ALARM, query_activated}; <<"false">> -> {?DEACTIVATED_ALARM, query_deactivated} end, - Params1 = proplists:delete(<<"activated">>, Params), - Response = emqx_mgmt_api:cluster_query(Params1, {Table, []}, {?MODULE, Function}), - {200, Response}. + Response = emqx_mgmt_api:cluster_query(Qs, {Table, []}, {?MODULE, Function}), + {200, Response}; -delete() -> +alarms(delete, _Params) -> _ = emqx_mgmt:delete_all_deactivated_alarms(), {200}. diff --git a/apps/emqx_management/src/emqx_mgmt_api_apps.erl b/apps/emqx_management/src/emqx_mgmt_api_apps.erl index 71dabf4c6..64a84b381 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_apps.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_apps.erl @@ -120,12 +120,10 @@ app_api() -> %%%============================================================================================== %% parameters trans -apps(get, _Request) -> +apps(get, _Params) -> list(#{}); -apps(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Data = emqx_json:decode(Body, [return_maps]), +apps(post, #{body := Data}) -> Parameters = #{ app_id => maps:get(<<"app_id">>, Data), name => maps:get(<<"name">>, Data), @@ -136,18 +134,13 @@ apps(post, Request) -> }, create(Parameters). -app(get, Request) -> - AppID = cowboy_req:binding(app_id, Request), +app(get, #{bindings := #{app_id := AppID}}) -> lookup(#{app_id => AppID}); -app(delete, Request) -> - AppID = cowboy_req:binding(app_id, Request), +app(delete, #{bindings := #{app_id := AppID}}) -> delete(#{app_id => AppID}); -app(put, Request) -> - AppID = cowboy_req:binding(app_id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - Data = emqx_json:decode(Body, [return_maps]), +app(put, #{bindings := #{app_id := AppID}, body := Data}) -> Parameters = #{ app_id => AppID, name => maps:get(<<"name">>, Data), diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 0f7cf487d..d847d2b10 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -454,46 +454,32 @@ subscribe_api() -> %%%============================================================================================== %% parameters trans -clients(get, Request) -> - Params = cowboy_req:parse_qs(Request), - list(Params). +clients(get, #{query_string := Qs}) -> + list(Qs). -client(get, Request) -> - ClientID = cowboy_req:binding(clientid, Request), - lookup(#{clientid => ClientID}); +client(get, #{bindings := Bindings}) -> + lookup(Bindings); -client(delete, Request) -> - ClientID = cowboy_req:binding(clientid, Request), - kickout(#{clientid => ClientID}). +client(delete, #{bindings := Bindings}) -> + kickout(Bindings). -authz_cache(get, Request) -> - ClientID = cowboy_req:binding(clientid, Request), - get_authz_cache(#{clientid => ClientID}); +authz_cache(get, #{bindings := Bindings}) -> + get_authz_cache(Bindings); -authz_cache(delete, Request) -> - ClientID = cowboy_req:binding(clientid, Request), - clean_authz_cache(#{clientid => ClientID}). +authz_cache(delete, #{bindings := Bindings}) -> + clean_authz_cache(Bindings). -subscribe(post, Request) -> - ClientID = cowboy_req:binding(clientid, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - TopicInfo = emqx_json:decode(Body, [return_maps]), +subscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) -> Topic = maps:get(<<"topic">>, TopicInfo), Qos = maps:get(<<"qos">>, TopicInfo, 0), subscribe(#{clientid => ClientID, topic => Topic, qos => Qos}). -unsubscribe(post, Request) -> - ClientID = cowboy_req:binding(clientid, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - TopicInfo = emqx_json:decode(Body, [return_maps]), +unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) -> Topic = maps:get(<<"topic">>, TopicInfo), unsubscribe(#{clientid => ClientID, topic => Topic}). %% 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]), +subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) -> Topics = [begin Topic = maps:get(<<"topic">>, TopicInfo), @@ -502,8 +488,7 @@ subscribe_batch(post, Request) -> end || TopicInfo <- TopicInfos], subscribe_batch(#{clientid => ClientID, topics => Topics}). -subscriptions(get, Request) -> - ClientID = cowboy_req:binding(clientid, Request), +subscriptions(get, #{bindings := #{clientid := ClientID}}) -> {Node, Subs0} = emqx_mgmt:list_client_subscriptions(ClientID), Subs = lists:map(fun({Topic, SubOpts}) -> #{node => Node, clientid => ClientID, topic => Topic, qos => maps:get(qos, SubOpts)} @@ -514,13 +499,13 @@ subscriptions(get, Request) -> %% api apply list(Params) -> - case proplists:get_value(<<"node">>, Params, undefined) of + case maps:get(<<"node">>, Params, undefined) of undefined -> Response = emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun), {200, Response}; Node1 -> Node = binary_to_atom(Node1, utf8), - Response = emqx_mgmt_api:node_query(Node, proplists:delete(<<"node">>, Params), ?CLIENT_QS_SCHEMA, ?query_fun), + Response = emqx_mgmt_api:node_query(Node, Params, ?CLIENT_QS_SCHEMA, ?query_fun), {200, Response} end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index f6425a4d6..9a5c3e361 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -25,8 +25,8 @@ -export([api_spec/0]). --export([ config/2 - , config_reset/2 +-export([ config/3 + , config_reset/3 ]). -export([get_conf_schema/2, gen_schema/1]). @@ -100,7 +100,7 @@ config_reset_api() -> %%%============================================================================================== %% parameters trans -config(get, Req) -> +config(get, _Params, Req) -> Path = conf_path(Req), case emqx_map_lib:deep_find(Path, get_full_config()) of {ok, Conf} -> @@ -109,13 +109,13 @@ config(get, Req) -> {404, #{code => 'NOT_FOUND', message => <<"Config cannot found">>}} end; -config(put, Req) -> +config(put, _Params, Req) -> Path = conf_path(Req), {ok, #{raw_config := RawConf}} = emqx:update_config(Path, http_body(Req), #{rawconf_with_defaults => true}), {200, emqx_map_lib:jsonable_map(RawConf)}. -config_reset(post, Req) -> +config_reset(post, _Params, Req) -> %% reset the config specified by the query string param 'conf_path' Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req), case emqx:reset_config(Path, #{}) of diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 5d0eaaf74..5cd70a468 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -22,10 +22,7 @@ -export([ listeners/2 , listener/2 - , node_listener/2 - , node_listeners/2 - , manage_listeners/2 - , manage_nodes_listeners/2]). + , manage_listeners/2]). -import(emqx_mgmt_util, [ schema/1 , schema/2 @@ -121,7 +118,7 @@ manage_nodes_listeners_api() -> error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), <<"200">> => schema(<<"Operation success">>)}}}, - {"/node/:node/listeners/:id/:operation", Metadata, manage_nodes_listeners}. + {"/node/:node/listeners/:id/:operation", Metadata, manage_listeners}. nodes_listeners_api() -> Metadata = #{ @@ -134,7 +131,7 @@ nodes_listeners_api() -> ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => schema(properties(), <<"Get listener info ok">>)}}}, - {"/nodes/:node/listeners/:id", Metadata, node_listener}. + {"/nodes/:node/listeners/:id", Metadata, listener}. nodes_listener_api() -> Metadata = #{ @@ -144,7 +141,7 @@ nodes_listener_api() -> responses => #{ <<"404">> => error_schema(<<"Listener id not found">>), <<"200">> => object_schema(properties(), <<"Get listener info ok">>)}}}, - {"/nodes/:node/listeners", Metadata, node_listeners}. + {"/nodes/:node/listeners", Metadata, listeners}. %%%============================================================================================== %% parameters param_path_node() -> @@ -182,29 +179,11 @@ param_path_operation()-> listeners(get, _Request) -> list(). -listener(get, Request) -> - ID = b2a(cowboy_req:binding(id, Request)), - get_listeners(#{id => ID}). +listener(get, #{bindings := Bindings}) -> + get_listeners(Bindings). -node_listeners(get, Request) -> - Node = b2a(cowboy_req:binding(node, Request)), - get_listeners(#{node => Node}). - -node_listener(get, Request) -> - Node = b2a(cowboy_req:binding(node, Request)), - ID = b2a(cowboy_req:binding(id, Request)), - get_listeners(#{node => Node, id => ID}). - -manage_listeners(_, Request) -> - ID = b2a(cowboy_req:binding(id, Request)), - Operation = b2a(cowboy_req:binding(operation, Request)), - manage(Operation, #{id => ID}). - -manage_nodes_listeners(_, Request) -> - Node = b2a(cowboy_req:binding(node, Request)), - ID = b2a(cowboy_req:binding(id, Request)), - Operation = b2a(cowboy_req:binding(operation, Request)), - manage(Operation, #{id => ID, node => Node}). +manage_listeners(_, #{bindings := Bindings}) -> + manage(Bindings). %%%============================================================================================== @@ -215,37 +194,39 @@ list() -> get_listeners(Param) -> case list_listener(Param) of {error, not_found} -> - ID = maps:get(id, Param), + ID = b2a(maps:get(id, Param)), Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; {error, nodedown} -> - Node = maps:get(node, Param), + Node = b2a(maps:get(node, Param)), Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), Response = #{code => 'BAD_NODE_NAME', message => Reason}, {404, Response}; [] -> - ID = maps:get(id, Param), + ID = b2a(maps:get(id, Param)), Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; Data -> {200, Data} end. -manage(Operation0, Param) -> - OperationMap = #{start => start_listener, stop => stop_listener, restart => restart_listener}, - Operation = maps:get(Operation0, OperationMap), +manage(Param) -> + OperationMap = #{start => start_listener, + stop => stop_listener, + restart => restart_listener}, + Operation = maps:get(b2a(maps:get(operation, Param)), OperationMap), case list_listener(Param) of {error, not_found} -> - ID = maps:get(id, Param), + ID = b2a(maps:get(id, Param)), Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; {error, nodedown} -> - Node = maps:get(node, Param), + Node = b2a(maps:get(node, Param)), Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), Response = #{code => 'BAD_NODE_NAME', message => Reason}, {404, Response}; [] -> - ID = maps:get(id, Param), + ID = b2a(maps:get(id, Param)), Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), {404, #{code => 'RESOURCE_NOT_FOUND', message => Reason}}; ListenersOrSingleListener -> @@ -318,4 +299,5 @@ trans_running(Conf) -> end. -b2a(B) when is_binary(B) -> binary_to_atom(B, utf8). +b2a(B) when is_binary(B) -> binary_to_atom(B, utf8); +b2a(Any) -> Any. diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index 49035417a..eae9cd76b 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -312,9 +312,8 @@ metrics_api() -> %%%============================================================================================== %% api apply -list(get, Request) -> - Params = cowboy_req:parse_qs(Request), - case proplists:get_value(<<"aggregate">>, Params, undefined) of +list(get, #{query_string := Qs}) -> + case maps:get(<<"aggregate">>, Qs, undefined) of <<"true">> -> {200, emqx_mgmt:get_metrics()}; _ -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 31d17f432..b1b61a2cc 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -115,20 +115,17 @@ node_stats_api() -> %%%============================================================================================== %% parameters trans -nodes(get, _Request) -> +nodes(get, _Params) -> list(#{}). -node(get, Request) -> - Params = node_name_path_parameter(Request), - get_node(Params). +node(get, #{bingings := #{node_name := NodeName}}) -> + get_node(binary_to_atom(NodeName, utf8)). -node_metrics(get, Request) -> - Params = node_name_path_parameter(Request), - get_metrics(Params). +node_metrics(get, #{bingings := #{node_name := NodeName}}) -> + get_metrics(binary_to_atom(NodeName, utf8)). -node_stats(get, Request) -> - Params = node_name_path_parameter(Request), - get_stats(Params). +node_stats(get, #{bingings := #{node_name := NodeName}}) -> + get_stats(binary_to_atom(NodeName, utf8)). %%%============================================================================================== %% api apply @@ -136,7 +133,7 @@ list(#{}) -> NodesInfo = [format(Node, NodeInfo) || {Node, NodeInfo} <- emqx_mgmt:list_nodes()], {200, NodesInfo}. -get_node(#{node := Node}) -> +get_node(Node) -> case emqx_mgmt:lookup_node(Node) of #{node_status := 'ERROR'} -> {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; @@ -144,7 +141,7 @@ get_node(#{node := Node}) -> {200, format(Node, NodeInfo)} end. -get_metrics(#{node := Node}) -> +get_metrics(Node) -> case emqx_mgmt:get_metrics(Node) of {error, _} -> {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; @@ -152,7 +149,7 @@ get_metrics(#{node := Node}) -> {200, Metrics} end. -get_stats(#{node := Node}) -> +get_stats(Node) -> case emqx_mgmt:get_stats(Node) of {error, _} -> {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; @@ -162,10 +159,6 @@ get_stats(#{node := Node}) -> %%============================================================================================================ %% 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(), diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl index 058e18160..fc218e257 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -62,15 +62,13 @@ properties() -> {retain, boolean, <<"Retain message flag, nullable, default false">>} ]). -publish(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Message = message(emqx_json:decode(Body, [return_maps])), +publish(post, #{body := Body}) -> + Message = message(Body), _ = emqx_mgmt:publish(Message), {200, format_message(Message)}. -publish_batch(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Messages = messages(emqx_json:decode(Body, [return_maps])), +publish_batch(post, #{body := Body}) -> + Messages = messages(Body), _ = [emqx_mgmt:publish(Message) || Message <- Messages], {200, format_message(Messages)}. diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index b193bac34..97e3fd0dd 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -82,13 +82,11 @@ route_api() -> %%%============================================================================================== %% parameters trans -routes(get, Request) -> - Params = cowboy_req:parse_qs(Request), - list(Params). +routes(get, #{query_string := Qs}) -> + list(Qs). -route(get, Request) -> - Topic = cowboy_req:binding(topic, Request), - lookup(#{topic => Topic}). +route(get, #{bindings := Bindings}) -> + lookup(Bindings). %%%============================================================================================== %% api apply diff --git a/apps/emqx_management/src/emqx_mgmt_api_stats.erl b/apps/emqx_management/src/emqx_mgmt_api_stats.erl index 706723f8b..d01e4f0c0 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_stats.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_stats.erl @@ -130,9 +130,8 @@ stats_api() -> %%%============================================================================================== %% api apply -list(get, Request) -> - Params = cowboy_req:parse_qs(Request), - case proplists:get_value(<<"aggregate">>, Params, undefined) of +list(get, #{query_string := Qs}) -> + case maps:get(<<"aggregate">>, Qs, undefined) of <<"true">> -> {200, emqx_mgmt:get_stats()}; _ -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_status.erl b/apps/emqx_management/src/emqx_mgmt_api_status.erl index 2fa47d1d9..fcc2a2a79 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_status.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_status.erl @@ -36,7 +36,7 @@ status_api() -> }, {Path, Metadata, running_status}. -running_status(get, _Request) -> +running_status(get, _Params) -> {InternalStatus, _ProvidedStatus} = init:get_status(), AppStatus = case lists:keysearch(emqx, 1, application:which_applications()) of diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index f7a37b861..62514e314 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -106,12 +106,11 @@ parameters() -> } | page_params() ]. -subscriptions(get, Request) -> - Params = cowboy_req:parse_qs(Request), +subscriptions(get, #{query_string := Params}) -> list(Params). list(Params) -> - case proplists:get_value(<<"node">>, Params, undefined) of + case maps:get(<<"node">>, Params, undefined) of undefined -> {200, emqx_mgmt_api:cluster_query(Params, ?SUBS_QS_SCHEMA, ?query_fun)}; Node -> diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 06a50fa37..27b8f44ea 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -129,20 +129,16 @@ delayed_message_api() -> %%-------------------------------------------------------------------- %% HTTP API %%-------------------------------------------------------------------- -status(get, _Request) -> +status(get, _Params) -> {200, get_status()}; -status(put, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Config = emqx_json:decode(Body, [return_maps]), - update_config(Config). +status(put, #{body := Body}) -> + update_config(Body). -delayed_messages(get, Request) -> - Qs = cowboy_req:parse_qs(Request), +delayed_messages(get, #{query_string := Qs}) -> {200, emqx_delayed:list(Qs)}. -delayed_message(get, Request) -> - Id = cowboy_req:binding(id, Request), +delayed_message(get, #{bindings := #{id := Id}}) -> case emqx_delayed:get_delayed_message(Id) of {ok, Message} -> Payload = maps:get(payload, Message), @@ -156,8 +152,7 @@ delayed_message(get, Request) -> Message = iolist_to_binary(io_lib:format("Message ID ~p not found", [Id])), {404, #{code => ?MESSAGE_ID_NOT_FOUND, message => Message}} end; -delayed_message(delete, Request) -> - Id = cowboy_req:binding(id, Request), +delayed_message(delete, #{bindings := #{id := Id}}) -> _ = emqx_delayed:delete_delayed_message(Id), {200}. diff --git a/apps/emqx_modules/src/emqx_event_message_api.erl b/apps/emqx_modules/src/emqx_event_message_api.erl index 43216ef63..2939ce9ea 100644 --- a/apps/emqx_modules/src/emqx_event_message_api.erl +++ b/apps/emqx_modules/src/emqx_event_message_api.erl @@ -50,11 +50,9 @@ event_message_api() -> }, {Path, Metadata, event_message}. -event_message(get, _Request) -> +event_message(get, _Params) -> {200, emqx_event_message:list()}; -event_message(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), - _ = emqx_event_message:update(Params), +event_message(post, #{body := Body}) -> + _ = emqx_event_message:update(Body), {200, emqx_event_message:list()}. diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl index 8a5a5dc6b..887e2148e 100644 --- a/apps/emqx_modules/src/emqx_rewrite_api.erl +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -60,15 +60,13 @@ rewrite_api() -> }, {Path, Metadata, topic_rewrite}. -topic_rewrite(get, _Request) -> +topic_rewrite(get, _Params) -> {200, emqx_rewrite:list()}; -topic_rewrite(post, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), - case length(Params) < ?MAX_RULES_LIMIT of +topic_rewrite(post, #{body := Body}) -> + case length(Body) < ?MAX_RULES_LIMIT of true -> - ok = emqx_rewrite:update(Params), + ok = emqx_rewrite:update(Body), {200, emqx_rewrite:list()}; _ -> Message = iolist_to_binary(io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT])), diff --git a/apps/emqx_modules/src/emqx_telemetry_api.erl b/apps/emqx_modules/src/emqx_telemetry_api.erl index e1d297afc..93f938a5c 100644 --- a/apps/emqx_modules/src/emqx_telemetry_api.erl +++ b/apps/emqx_modules/src/emqx_telemetry_api.erl @@ -86,13 +86,11 @@ data_api() -> %%-------------------------------------------------------------------- %% HTTP API %%-------------------------------------------------------------------- -status(get, _Request) -> +status(get, _Params) -> {200, get_telemetry_status()}; -status(put, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), - Enable = maps:get(<<"enable">>, Params), +status(put, #{body := Body}) -> + Enable = maps:get(<<"enable">>, Body), case Enable =:= emqx_telemetry:get_status() of true -> Reason = case Enable of diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index 323363b72..4f7885254 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -151,9 +151,6 @@ topic_param() -> schema => #{type => string} }. -topic_param(Request) -> - cowboy_req:binding(topic, Request). - %%-------------------------------------------------------------------- %% api callback list_topic(get, _) -> @@ -162,8 +159,7 @@ list_topic(get, _) -> list_topic_metrics(get, _) -> list_metrics(). -operate_topic_metrics(Method, Request) -> - Topic = topic_param(Request), +operate_topic_metrics(Method, #{bindings := #{topic := Topic}}) -> case Method of get -> get_metrics(Topic); @@ -176,8 +172,7 @@ operate_topic_metrics(Method, Request) -> reset_all_topic_metrics(put, _) -> reset(). -reset_topic_metrics(put, Request) -> - Topic = topic_param(Request), +reset_topic_metrics(put, #{bindings := #{topic := Topic}}) -> reset(Topic). %%-------------------------------------------------------------------- diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 4c974146c..1529df470 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -77,14 +77,12 @@ prometheus_api() -> % }, % {"/prometheus/stats", Metadata, stats}. -prometheus(get, _Request) -> +prometheus(get, _Params) -> {200, emqx:get_raw_config([<<"prometheus">>], #{})}; -prometheus(put, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), - {ok, Config} = emqx:update_config([prometheus], Params), - case maps:get(<<"enable">>, Params) of +prometheus(put, #{body := Body}) -> + {ok, Config} = emqx:update_config([prometheus], Body), + case maps:get(<<"enable">>, Body) of true -> _ = emqx_prometheus_sup:stop_child(?APP), emqx_prometheus_sup:start_child(?APP, maps:get(config, Config)); diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 704d12deb..2b15bd615 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -122,23 +122,19 @@ config_api() -> }, {"/mqtt/retainer", MetaData, config}. -lookup_retained_warp(Type, Req) -> - check_backend(Type, Req, fun lookup_retained/2). +lookup_retained_warp(Type, Params) -> + check_backend(Type, Params, fun lookup_retained/2). -with_topic_warp(Type, Req) -> - check_backend(Type, Req, fun with_topic/2). +with_topic_warp(Type, Params) -> + check_backend(Type, Params, fun with_topic/2). config(get, _) -> - Config = emqx:get_config([mqtt_retainer]), - Body = emqx_json:encode(Config), - {200, Body}; + {200, emqx:get_raw_config([emqx_retainer])}; -config(put, Req) -> +config(put, #{body := Body}) -> try - {ok, Body, _} = cowboy_req:read_body(Req), - Cfg = emqx_json:decode(Body), - emqx_retainer:update_config(Cfg), - {200, #{<<"content-type">> => <<"text/plain">>}, <<"Update configs successfully">>} + ok = emqx_retainer:update_config(Body), + {200, emqx:get_raw_config([emqx_retainer])} catch _:Reason:_ -> {400, #{code => 'UPDATE_FAILED', @@ -148,27 +144,25 @@ config(put, Req) -> %%------------------------------------------------------------------------------ %% Interval Funcs %%------------------------------------------------------------------------------ -lookup_retained(get, Req) -> - lookup(undefined, Req, fun format_message/1). +lookup_retained(get, Params) -> + lookup(undefined, Params, fun format_message/1). -with_topic(get, Req) -> - Topic = cowboy_req:binding(topic, Req), - lookup(Topic, Req, fun format_detail_message/1); +with_topic(get, #{bindings := Bindings} = Params) -> + Topic = maps:get(topic, Bindings), + lookup(Topic, Params, fun format_detail_message/1); -with_topic(delete, Req) -> - Topic = cowboy_req:binding(topic, Req), +with_topic(delete, #{bindings := Bindings}) -> + Topic = maps:get(topic, Bindings), emqx_retainer_mnesia:delete_message(undefined, Topic), {200}. -spec lookup(undefined | binary(), - cowboy_req:req(), + map(), fun((#message{}) -> map())) -> {200, map()}. -lookup(Topic, Req, Formatter) -> - #{page := Page, - limit := Limit} = cowboy_req:match_qs([{page, int, 1}, - {limit, int, emqx_mgmt:max_row_limit()}], - Req), +lookup(Topic, #{query_string := Qs}, Formatter) -> + Page = maps:get(page, Qs, 1), + Limit = maps:get(page, Qs, emqx_mgmt:max_row_limit()), {ok, Msgs} = emqx_retainer_mnesia:page_read(undefined, Topic, Page, Limit), {200, format_message(Msgs, Formatter)}. @@ -197,10 +191,10 @@ to_bin_string(Data) when is_binary(Data) -> to_bin_string(Data) -> list_to_binary(io_lib:format("~p", [Data])). -check_backend(Type, Req, Cont) -> +check_backend(Type, Params, Cont) -> case emqx:get_config([emqx_retainer, config, type]) of built_in_database -> - Cont(Type, Req); + Cont(Type, Params); _ -> {405, #{<<"content-type">> => <<"text/plain">>}, diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index a859d4d66..9c5945602 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -51,14 +51,12 @@ statsd_api() -> }, [{"/statsd", Metadata, statsd}]. -statsd(get, _Request) -> +statsd(get, _Params) -> {200, emqx:get_raw_config([<<"statsd">>], #{})}; -statsd(put, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Params = emqx_json:decode(Body, [return_maps]), - {ok, Config} = emqx:update_config([statsd], Params), - case maps:get(<<"enable">>, Params) of +statsd(put, #{body := Body}) -> + {ok, Config} = emqx:update_config([statsd], Body), + case maps:get(<<"enable">>, Body) of true -> _ = emqx_statsd_sup:stop_child(?APP), emqx_statsd_sup:start_child(?APP, maps:get(config, Config)); diff --git a/rebar.config b/rebar.config index 65384c9ab..83de68ecb 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.1.7"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.0"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, "0.3.3"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From bc325e55fce85c294ccacf2888d4ce87c9219e03 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 17:35:38 +0800 Subject: [PATCH 104/306] fix(authn-authz-api): fix authn/authz test cases fail --- apps/emqx_authn/src/emqx_authn_api.erl | 78 +++++++------------ apps/emqx_authz/src/emqx_authz_api.erl | 40 ++++------ apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 3 + .../emqx_authz/test/emqx_authz_http_SUITE.erl | 2 + .../test/emqx_authz_mongo_SUITE.erl | 3 + .../test/emqx_authz_mysql_SUITE.erl | 3 + .../test/emqx_authz_pgsql_SUITE.erl | 3 + .../test/emqx_authz_redis_SUITE.erl | 2 + .../src/emqx_mgmt_api_listeners.erl | 6 +- .../src/emqx_mgmt_api_nodes.erl | 8 +- 10 files changed, 65 insertions(+), 83 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 9512dc7f3..5f2b96b57 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -21,13 +21,13 @@ -include("emqx_authn.hrl"). -export([ api_spec/0 - , authentication/3 - , authenticators/3 - , authenticators2/3 - , move/3 - , import_users/3 - , users/3 - , users2/3 + , authentication/2 + , authenticators/2 + , authenticators2/2 + , move/2 + , import_users/2 + , users/2 + , users2/2 ]). -define(EXAMPLE_1, #{name => <<"example 1">>, @@ -1289,22 +1289,19 @@ definitions() -> , #{<<"error">> => ErrorDef} ]. -authentication(post, _Params, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of +authentication(post, #{body := Config}) -> + case Config of #{<<"enable">> := Enable} -> {ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}), {204}; _ -> serialize_error({missing_parameter, enable}) end; -authentication(get, _Params, _Request) -> +authentication(get, _Params) -> Enabled = emqx_authn:is_enabled(), {200, #{enabled => Enabled}}. -authenticators(post, _Params, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - Config = emqx_json:decode(Body, [return_maps]), +authenticators(post, #{body := Config}) -> case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, raw_config := RawConfig}} -> @@ -1313,7 +1310,7 @@ authenticators(post, _Params, Request) -> {error, {_, _, Reason}} -> serialize_error(Reason) end; -authenticators(get, _Params, _Request) -> +authenticators(get, _Params) -> RawConfig = get_raw_config([authentication, authenticators]), {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) -> @@ -1321,8 +1318,7 @@ authenticators(get, _Params, _Request) -> end, RawConfig, Authenticators), {200, NAuthenticators}. -authenticators2(get, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), +authenticators2(get, #{bindings := #{id := AuthenticatorID}}) -> case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of {ok, #{id := ID, name := Name}} -> RawConfig = get_raw_config([authentication, authenticators]), @@ -1331,10 +1327,7 @@ authenticators2(get, _Params, Request) -> {error, Reason} -> serialize_error(Reason) end; -authenticators2(put, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - Config = emqx_json:decode(Body, [return_maps]), +authenticators2(put, #{bindings := #{id := AuthenticatorID}, body := Config}) -> case emqx_authn:update_config([authentication, authenticators], {update_or_create_authenticator, AuthenticatorID, Config}) of {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, @@ -1344,8 +1337,7 @@ authenticators2(put, _Params, Request) -> {error, {_, _, Reason}} -> serialize_error(Reason) end; -authenticators2(delete, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), +authenticators2(delete, #{bindings := #{id := AuthenticatorID}}) -> case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of {ok, _} -> {204}; @@ -1353,10 +1345,8 @@ authenticators2(delete, _Params, Request) -> serialize_error(Reason) end. -move(post, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of +move(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> + case Body of #{<<"position">> := Position} -> case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of {ok, _} -> {204}; @@ -1366,10 +1356,8 @@ move(post, _Params, Request) -> serialize_error({missing_parameter, position}) end. -import_users(post, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of +import_users(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> + case Body of #{<<"filename">> := Filename} -> case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of ok -> {204}; @@ -1379,12 +1367,9 @@ import_users(post, _Params, Request) -> serialize_error({missing_parameter, filename}) end. -users(post, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{ <<"user_id">> := UserID - , <<"password">> := Password} = UserInfo -> +users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> + case UserInfo of + #{ <<"user_id">> := UserID, <<"password">> := Password} -> Superuser = maps:get(<<"superuser">>, UserInfo, false), case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID , password => Password @@ -1399,8 +1384,7 @@ users(post, _Params, Request) -> _ -> serialize_error({missing_parameter, user_id}) end; -users(get, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), +users(get, #{bindings := #{id := AuthenticatorID}}) -> case emqx_authn:list_users(?CHAIN, AuthenticatorID) of {ok, Users} -> {200, Users}; @@ -1408,11 +1392,9 @@ users(get, _Params, Request) -> serialize_error(Reason) end. -users2(patch, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - UserID = cowboy_req:binding(user_id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - UserInfo = emqx_json:decode(Body, [return_maps]), +users2(patch, #{bindings := #{id := AuthenticatorID, + user_id := UserID}, + body := UserInfo}) -> NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo), case NUserInfo =:= #{} of true -> @@ -1425,18 +1407,14 @@ users2(patch, _Params, Request) -> serialize_error(Reason) end end; -users2(get, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - UserID = cowboy_req:binding(user_id, Request), +users2(get, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of {ok, User} -> {200, User}; {error, Reason} -> serialize_error(Reason) end; -users2(delete, _Params, Request) -> - AuthenticatorID = cowboy_req:binding(id, Request), - UserID = cowboy_req:binding(user_id, Request), +users2(delete, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of ok -> {204}; diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 693870696..15aaed65a 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -40,9 +40,9 @@ topics => [<<"#">>]}). -export([ api_spec/0 - , rules/3 - , rule/3 - , move_rule/3 + , rules/2 + , rule/2 + , move_rule/2 ]). api_spec() -> @@ -418,7 +418,7 @@ move_rule_api() -> }, {"/authorization/:id/move", Metadata, move_rule}. -rules(get, _Params, Request) -> +rules(get, #{query_string := Query}) -> Rules = lists:foldl(fun (#{type := _Type, enable := true, annotations := #{id := Id} = Annotations} = Rule, AccIn) -> NRule = case emqx_resource:health_check(Id) of ok -> @@ -430,11 +430,10 @@ rules(get, _Params, Request) -> (Rule, AccIn) -> lists:append(AccIn, [Rule]) end, [], emqx_authz:lookup()), - Query = cowboy_req:parse_qs(Request), - case lists:keymember(<<"page">>, 1, Query) andalso lists:keymember(<<"limit">>, 1, Query) of + case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of true -> - {<<"page">>, Page} = lists:keyfind(<<"page">>, 1, Query), - {<<"limit">>, Limit} = lists:keyfind(<<"limit">>, 1, Query), + Page = maps:get(<<"page">>, Query), + Limit = maps:get(<<"limit">>, Query), Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), {_, Rules1} = lists:split(Index, Rules), case binary_to_integer(Limit) < length(Rules1) of @@ -445,18 +444,14 @@ rules(get, _Params, Request) -> end; false -> {200, #{rules => Rules}} end; -rules(post, _Params, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - RawConfig = jsx:decode(Body, [return_maps]), +rules(post, #{body := RawConfig}) -> case emqx_authz:update(head, [RawConfig]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rules(put, _Params, Request) -> - {ok, Body, _} = cowboy_req:read_body(Request), - RawConfig = jsx:decode(Body, [return_maps]), +rules(put, #{body := RawConfig}) -> case emqx_authz:update(replace, RawConfig) of {ok, _} -> {204}; {error, Reason} -> @@ -464,8 +459,7 @@ rules(put, _Params, Request) -> messgae => atom_to_binary(Reason)}} end. -rule(get, _Params, Request) -> - Id = cowboy_req:binding(id, Request), +rule(get, #{bindings := #{id := Id}}) -> case emqx_authz:lookup(Id) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; Rule -> @@ -481,10 +475,7 @@ rule(get, _Params, Request) -> end end; -rule(put, _Params, Request) -> - RuleId = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - RawConfig = jsx:decode(Body, [return_maps]), +rule(put, #{bindings := #{id := RuleId}, body := RawConfig}) -> case emqx_authz:update({replace_once, RuleId}, RawConfig) of {ok, _} -> {204}; {error, not_found_rule} -> @@ -494,18 +485,15 @@ rule(put, _Params, Request) -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rule(delete, _Params, Request) -> - RuleId = cowboy_req:binding(id, Request), +rule(delete, #{bindings := #{id := RuleId}}) -> case emqx_authz:update({replace_once, RuleId}, #{}) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. -move_rule(post, _Params, Request) -> - RuleId = cowboy_req:binding(id, Request), - {ok, Body, _} = cowboy_req:read_body(Request), - #{<<"position">> := Position} = jsx:decode(Body, [return_maps]), +move_rule(post, #{bindings := #{id := RuleId}, body := Body}) -> + #{<<"position">> := Position} = Body, case emqx_authz:move(RuleId, Position) of {ok, _} -> {204}; {error, not_found_rule} -> diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index c4930ace6..0600125f9 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -35,6 +35,8 @@ -define(API_VERSION, "v5"). -define(BASE_PATH, "api"). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). + -define(RULE1, #{<<"principal">> => <<"all">>, <<"topics">> => [<<"#">>], <<"action">> => <<"all">>, @@ -75,6 +77,7 @@ groups() -> init_per_suite(Config) -> ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), {ok, _} = emqx:update_config([zones, default, authorization, enable], true), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index d50c7a43d..b284744af 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -21,6 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -33,6 +34,7 @@ init_per_suite(Config) -> meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index ba18cb1cb..b68ee2800 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -33,6 +35,7 @@ init_per_suite(Config) -> meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), {ok, _} = emqx:update_config([zones, default, authorization, enable], true), diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 81021f6e8..e6164cacd 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -33,6 +35,7 @@ init_per_suite(Config) -> meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 7f7c236d0..c304f06a9 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -33,6 +35,7 @@ init_per_suite(Config) -> meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index f0a571dd8..a494159e3 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -21,6 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -33,6 +34,7 @@ init_per_suite(Config) -> meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 5cd70a468..bce6ef210 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -265,11 +265,11 @@ list_listener(Params) -> format(list_listener_(Params)). list_listener_(#{node := Node, id := Identifier}) -> - emqx_mgmt:get_listener(Node, Identifier); + emqx_mgmt:get_listener(b2a(Node), b2a(Identifier)); list_listener_(#{id := Identifier}) -> - emqx_mgmt:list_listeners_by_id(Identifier); + emqx_mgmt:list_listeners_by_id(b2a(Identifier)); list_listener_(#{node := Node}) -> - emqx_mgmt:list_listeners(Node); + emqx_mgmt:list_listeners(b2a(Node)); list_listener_(#{}) -> emqx_mgmt:list_listeners(). diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index b1b61a2cc..1bca74546 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -118,13 +118,13 @@ node_stats_api() -> nodes(get, _Params) -> list(#{}). -node(get, #{bingings := #{node_name := NodeName}}) -> +node(get, #{bindings := #{node_name := NodeName}}) -> get_node(binary_to_atom(NodeName, utf8)). -node_metrics(get, #{bingings := #{node_name := NodeName}}) -> +node_metrics(get, #{bindings := #{node_name := NodeName}}) -> get_metrics(binary_to_atom(NodeName, utf8)). -node_stats(get, #{bingings := #{node_name := NodeName}}) -> +node_stats(get, #{bindings := #{node_name := NodeName}}) -> get_stats(binary_to_atom(NodeName, utf8)). %%%============================================================================================== @@ -135,7 +135,7 @@ list(#{}) -> get_node(Node) -> case emqx_mgmt:lookup_node(Node) of - #{node_status := 'ERROR'} -> + {error, _} -> {400, #{code => 'SOURCE_ERROR', message => <<"rpc_failed">>}}; NodeInfo -> {200, format(Node, NodeInfo)} From a1ae4457df23967d485e5cffbb72cdab21acccbe Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 20:13:20 +0800 Subject: [PATCH 105/306] feat(prometheus): add get prometheus_format stats api --- apps/emqx_prometheus/src/emqx_prometheus.erl | 14 ++-- .../src/emqx_prometheus_api.erl | 65 +++++++------------ 2 files changed, 31 insertions(+), 48 deletions(-) diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index e369179ee..bbdbab6b2 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -125,13 +125,13 @@ collect(<<"json">>) -> Metrics = emqx_metrics:all(), Stats = emqx_stats:getstats(), VMData = emqx_vm_data(), - [{stats, [collect_stats(Name, Stats) || Name <- emqx_stats()]}, - {metrics, [collect_stats(Name, VMData) || Name <- emqx_vm()]}, - {packets, [collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]}, - {messages, [collect_stats(Name, Metrics) || Name <- emqx_metrics_messages()]}, - {delivery, [collect_stats(Name, Metrics) || Name <- emqx_metrics_delivery()]}, - {client, [collect_stats(Name, Metrics) || Name <- emqx_metrics_client()]}, - {session, [collect_stats(Name, Metrics) || Name <- emqx_metrics_session()]}]; + #{stats => maps:from_list([collect_stats(Name, Stats) || Name <- emqx_stats()]), + metrics => maps:from_list([collect_stats(Name, VMData) || Name <- emqx_vm()]), + packets => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]), + messages => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_messages()]), + delivery => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_delivery()]), + client => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_client()]), + session => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_session()])}; collect(<<"prometheus">>) -> prometheus_text_format:format(). diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 1529df470..d7e445735 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -20,17 +20,16 @@ -include("emqx_prometheus.hrl"). --import(emqx_mgmt_util, [ schema/1 - , bad_request/0]). +-import(emqx_mgmt_util, [ schema/1]). -export([api_spec/0]). -export([ prometheus/2 - % , stats/2 + , stats/2 ]). api_spec() -> - {[prometheus_api()], schemas()}. + {[prometheus_api(), prometheus_data_api()], schemas()}. schemas() -> [#{prometheus => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([prometheus]))}]. @@ -44,38 +43,24 @@ prometheus_api() -> put => #{ description => <<"Update Prometheus">>, 'requestBody' => schema(prometheus), - responses => #{ - <<"200">> => schema(prometheus), - <<"400">> => bad_request() - } + responses => #{<<"200">> => schema(prometheus)} } }, {"/prometheus", Metadata, prometheus}. -% prometheus_data_api() -> -% Metadata = #{ -% get => #{ -% description => <<"Get Prometheus Data">>, -% parameters => [#{ -% name => format_type, -% in => path, -% schema => #{type => string} -% }], -% responses => #{ -% <<"200">> => -% response_schema(<<"Update Prometheus successfully">>), -% <<"400">> => -% response_schema(<<"Bad Request">>, #{ -% type => object, -% properties => #{ -% message => #{type => string}, -% code => #{type => string} -% } -% }) -% } -% } -% }, -% {"/prometheus/stats", Metadata, stats}. +prometheus_data_api() -> + Metadata = #{ + get => #{ + description => <<"Get Prometheus Data">>, + parameters => [#{ + name => format_type, + in => path, + schema => #{type => string} + }], + responses => #{<<"200">> => schema(#{type => object})} + } + }, + {"/prometheus/stats", Metadata, stats}. prometheus(get, _Params) -> {200, emqx:get_raw_config([<<"prometheus">>], #{})}; @@ -92,12 +77,10 @@ prometheus(put, #{body := Body}) -> end, {200, emqx:get_raw_config([<<"prometheus">>], #{})}. -% stats(_Bindings, Params) -> -% Type = proplists:get_value(<<"format_type">>, Params, <<"json">>), -% Data = emqx_prometheus:collect(Type), -% case Type of -% <<"json">> -> -% {ok, Data}; -% <<"prometheus">> -> -% {ok, #{<<"content-type">> => <<"text/plain">>}, Data} -% end. +stats(get, #{query_string := Qs}) -> + Type = maps:get(<<"format_type">>, Qs, <<"json">>), + Data = emqx_prometheus:collect(Type), + case Type of + <<"json">> -> {200, Data}; + <<"prometheus">> -> {200, #{<<"content-type">> => <<"text/plain">>}, Data} + end. From bf67fa1be1b07f58ea6457c5b378a98fc6f80027 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Tue, 24 Aug 2021 20:58:21 +0800 Subject: [PATCH 106/306] fix: generate topic metrics api & delayed message api path --- apps/emqx_modules/src/emqx_delayed_api.erl | 6 +-- .../src/emqx_topic_metrics_api.erl | 37 ++++++------------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 27b8f44ea..f45519134 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -93,7 +93,7 @@ status_api() -> } } }, - {"/mqtt/delayed_messages/status", Metadata, status}. + {"/mqtt/delayed", Metadata, status}. delayed_messages_api() -> Metadata = #{ @@ -104,7 +104,7 @@ delayed_messages_api() -> } } }, - {"/mqtt/delayed_messages", Metadata, delayed_messages}. + {"/mqtt/delayed/messages", Metadata, delayed_messages}. delayed_message_api() -> Metadata = #{ @@ -124,7 +124,7 @@ delayed_message_api() -> } } }, - {"/mqtt/delayed_messages/:id", Metadata, delayed_message}. + {"/mqtt/delayed/messages/:id", Metadata, delayed_message}. %%-------------------------------------------------------------------- %% HTTP API diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index 4f7885254..e63af61f6 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -27,8 +27,7 @@ -export([api_spec/0]). --export([ list_topic/2 - , list_topic_metrics/2 +-export([ list_topic_metrics/2 , operate_topic_metrics/2 , reset_all_topic_metrics/2 , reset_topic_metrics/2 @@ -43,7 +42,6 @@ api_spec() -> { [ - list_topic_api(), list_topic_metrics_api(), get_topic_metrics_api(), reset_all_topic_metrics_api(), @@ -77,17 +75,6 @@ properties() -> {'messages.qos2.out.rate', number}]} ]). - -list_topic_api() -> - Props = properties([{topic, string}]), - MetaData = #{ - get => #{ - description => <<"List topic">>, - responses => #{<<"200">> => object_array_schema(Props, <<"List topic">>)} - } - }, - {"/mqtt/topic_metrics", MetaData, list_topic}. - list_topic_metrics_api() -> MetaData = #{ get => #{ @@ -97,7 +84,7 @@ list_topic_metrics_api() -> } } }, - {"/mqtt/topic_metrics/metrics", MetaData, list_topic_metrics}. + {"/mqtt/topic_metrics", MetaData, list_topic_metrics}. get_topic_metrics_api() -> MetaData = #{ @@ -121,7 +108,7 @@ get_topic_metrics_api() -> responses => #{ <<"200">> => schema(<<"Deregister topic metrics">>)} } }, - {"/mqtt/topic_metrics/metrics/:topic", MetaData, operate_topic_metrics}. + {"/mqtt/topic_metrics/:topic", MetaData, operate_topic_metrics}. reset_all_topic_metrics_api() -> MetaData = #{ @@ -133,7 +120,7 @@ reset_all_topic_metrics_api() -> {"/mqtt/topic_metrics/reset", MetaData, reset_all_topic_metrics}. reset_topic_metrics_api() -> - Path = "/mqtt/topic_metrics/reset/:topic", + Path = "/mqtt/topic_metrics/:topic/reset", MetaData = #{ put => #{ description => <<"Reset topic metrics">>, @@ -148,18 +135,17 @@ topic_param() -> name => topic, in => path, required => true, + description => <<"Notice: Topic string url must encode">>, schema => #{type => string} }. %%-------------------------------------------------------------------- %% api callback -list_topic(get, _) -> - list_topics(). - list_topic_metrics(get, _) -> list_metrics(). -operate_topic_metrics(Method, #{bindings := #{topic := Topic}}) -> +operate_topic_metrics(Method, #{bindings := #{topic := Topic0}}) -> + Topic = decode_topic(Topic0), case Method of get -> get_metrics(Topic); @@ -172,14 +158,15 @@ operate_topic_metrics(Method, #{bindings := #{topic := Topic}}) -> reset_all_topic_metrics(put, _) -> reset(). -reset_topic_metrics(put, #{bindings := #{topic := Topic}}) -> +reset_topic_metrics(put, #{bindings := #{topic := Topic0}}) -> + Topic = decode_topic(Topic0), reset(Topic). +decode_topic(Topic) -> + uri_string:percent_decode(Topic). + %%-------------------------------------------------------------------- %% api apply -list_topics() -> - {200, emqx_topic_metrics:all_registered_topics()}. - list_metrics() -> {200, emqx_topic_metrics:metrics()}. From 675d23111cdd0a32ad0dce0d8c593e39d056ad9c Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 24 Aug 2021 21:39:34 +0800 Subject: [PATCH 107/306] chore(swagger-schema): Delete the schema that is not reused --- .../emqx_dashboard/src/emqx_dashboard_api.erl | 15 +++++++------ .../src/emqx_dashboard_monitor_api.erl | 15 +++++++------ .../src/emqx_mgmt_api_routes.erl | 5 +---- .../src/emqx_mgmt_api_status.erl | 4 +--- apps/emqx_modules/src/emqx_delayed_api.erl | 12 +++++------ .../src/emqx_event_message_api.erl | 13 ++++++------ .../src/emqx_prometheus_api.erl | 12 +++++------ apps/emqx_retainer/src/emqx_retainer_api.erl | 21 +++++++------------ apps/emqx_statsd/src/emqx_statsd_api.erl | 12 +++++------ 9 files changed, 47 insertions(+), 62 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index ef3808f85..88ae85d9d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -50,14 +50,13 @@ -define(EMPTY(V), (V == undefined orelse V == <<>>)). api_spec() -> - { - [ login_api() - , logout_api() - , users_api() - , user_api() - , change_pwd_api() - ], - []}. + {[ login_api() + , logout_api() + , users_api() + , user_api() + , change_pwd_api() + ], + []}. login_api() -> AuthProps = properties([{username, string, <<"Username">>}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl index c49148534..c00310211 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl @@ -26,14 +26,13 @@ , dropped]). api_spec() -> - { - [ monitor_api() - , monitor_nodes_api() - , monitor_nodes_counters_api() - , monitor_counters_api() - , monitor_current_api()], - [] - }. + {[ monitor_api() + , monitor_nodes_api() + , monitor_nodes_counters_api() + , monitor_counters_api() + , monitor_current_api() + ], + []}. monitor_api() -> Metadata = #{ diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 97e3fd0dd..6c74105c0 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -36,10 +36,7 @@ ]). api_spec() -> - { - [routes_api(), route_api()], - [] - }. + {[routes_api(), route_api()], []}. properties() -> properties([ diff --git a/apps/emqx_management/src/emqx_mgmt_api_status.erl b/apps/emqx_management/src/emqx_mgmt_api_status.erl index fcc2a2a79..d13bc5394 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_status.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_status.erl @@ -29,9 +29,7 @@ status_api() -> Metadata = #{ get => #{ security => [], - responses => #{ - <<"200">> => #{description => <<"running">>} - } + responses => #{<<"200">> => #{description => <<"running">>}} } }, {Path, Metadata, running_status}. diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index f45519134..e99242206 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -49,11 +49,11 @@ api_spec() -> { [status_api(), delayed_messages_api(), delayed_message_api()], - schemas() + [] }. -schemas() -> - [#{delayed => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([delayed]))}]. +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([delayed])). properties() -> PayloadDesc = io_lib:format("Payload, base64 encode. Payload will be ~p if length large than ~p", [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH]), @@ -80,14 +80,14 @@ status_api() -> get => #{ description => <<"Get delayed status">>, responses => #{ - <<"200">> => schema(delayed)} + <<"200">> => schema(conf_schema())} }, put => #{ description => <<"Enable or disable delayed, set max delayed messages">>, - 'requestBody' => schema(delayed), + 'requestBody' => schema(conf_schema()), responses => #{ <<"200">> => - schema(delayed, <<"Enable or disable delayed successfully">>), + schema(conf_schema(), <<"Enable or disable delayed successfully">>), <<"400">> => error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED]) } diff --git a/apps/emqx_modules/src/emqx_event_message_api.erl b/apps/emqx_modules/src/emqx_event_message_api.erl index 2939ce9ea..3dc94fd9f 100644 --- a/apps/emqx_modules/src/emqx_event_message_api.erl +++ b/apps/emqx_modules/src/emqx_event_message_api.erl @@ -25,11 +25,10 @@ ]). api_spec() -> - {[event_message_api()], [event_message_schema()]}. + {[event_message_api()], []}. -event_message_schema() -> - Conf = emqx:get_raw_config([event_message]), - #{event_message => emqx_mgmt_api_configs:gen_schema(Conf)}. +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(emqx:get_config([event_message])). event_message_api() -> Path = "/mqtt/event_message", @@ -37,14 +36,14 @@ event_message_api() -> get => #{ description => <<"Event Message">>, responses => #{ - <<"200">> => schema(event_message) + <<"200">> => schema(conf_schema()) } }, post => #{ description => <<"Update Event Message">>, - 'requestBody' => schema(event_message), + 'requestBody' => schema(conf_schema()), responses => #{ - <<"200">> => schema(event_message) + <<"200">> => schema(conf_schema()) } } }, diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index d7e445735..ee2444bac 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -29,21 +29,21 @@ ]). api_spec() -> - {[prometheus_api(), prometheus_data_api()], schemas()}. + {[prometheus_api(), prometheus_data_api()], []}. -schemas() -> - [#{prometheus => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([prometheus]))}]. +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([prometheus])). prometheus_api() -> Metadata = #{ get => #{ description => <<"Get Prometheus info">>, - responses => #{<<"200">> => schema(prometheus)} + responses => #{<<"200">> => schema(conf_schema())} }, put => #{ description => <<"Update Prometheus">>, - 'requestBody' => schema(prometheus), - responses => #{<<"200">> => schema(prometheus)} + 'requestBody' => schema(conf_schema()), + responses => #{<<"200">> => schema(conf_schema())} } }, {"/prometheus", Metadata, prometheus}. diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 2b15bd615..313ae9b02 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -35,17 +35,10 @@ , properties/1]). api_spec() -> - { - [ lookup_retained_api() - , with_topic_api() - , config_api() - ], - schemas() - }. + {[lookup_retained_api(), with_topic_api(), config_api()], []}. -schemas() -> - MqttRetainer = gen_schema(emqx:get_raw_config([emqx_retainer])), - [#{emqx_retainer => MqttRetainer}]. +conf_schema() -> + gen_schema(emqx:get_raw_config([emqx_retainer])). message_props() -> properties([ @@ -56,7 +49,7 @@ message_props() -> {publish_at, string, <<"publish datetime">>}, {from_clientid, string, <<"publisher ClientId">>}, {from_username, string, <<"publisher Username">>} - ]). + ]). parameters() -> [#{ @@ -107,15 +100,15 @@ config_api() -> get => #{ description => <<"get retainer config">>, responses => #{ - <<"200">> => schema(mqtt_retainer, <<"Get configs successfully">>), + <<"200">> => schema(conf_schema(), <<"Get configs successfully">>), <<"404">> => error_schema(<<"Config not found">>, ['NOT_FOUND']) } }, put => #{ description => <<"Update retainer config">>, - 'requestBody' => schema(mqtt_retainer), + 'requestBody' => schema(conf_schema()), responses => #{ - <<"200">> => schema(mqtt_retainer, <<"Update configs successfully">>), + <<"200">> => schema(conf_schema(), <<"Update configs successfully">>), <<"400">> => error_schema(<<"Update configs failed">>, ['UPDATE_FAILED']) } } diff --git a/apps/emqx_statsd/src/emqx_statsd_api.erl b/apps/emqx_statsd/src/emqx_statsd_api.erl index 9c5945602..97e803f5b 100644 --- a/apps/emqx_statsd/src/emqx_statsd_api.erl +++ b/apps/emqx_statsd/src/emqx_statsd_api.erl @@ -29,22 +29,22 @@ ]). api_spec() -> - {statsd_api(), schemas()}. + {statsd_api(), []}. -schemas() -> - [#{statsd => emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([statsd]))}]. +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(emqx:get_raw_config([statsd])). statsd_api() -> Metadata = #{ get => #{ description => <<"Get statsd info">>, - responses => #{<<"200">> => schema(statsd)} + responses => #{<<"200">> => schema(conf_schema())} }, put => #{ description => <<"Update Statsd">>, - 'requestBody' => schema(statsd), + 'requestBody' => schema(conf_schema()), responses => #{ - <<"200">> => schema(statsd), + <<"200">> => schema(conf_schema()), <<"400">> => bad_request() } } From 87881621bbc98677f61eed04e13c920c75ea04f9 Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 25 Aug 2021 10:09:31 +0800 Subject: [PATCH 108/306] fix: fix typo --- apps/emqx_management/src/emqx_mgmt_util.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 5a95238e3..4c1009610 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -233,7 +233,7 @@ properties([{Key, Type, Desc} | Props], Acc) -> properties([{Key, Type, Desc, Enum} | Props], Acc) -> properties(Props, maps:put(Key, #{type => Type, description => Desc, - emum => Enum}, Acc)). + enum => Enum}, Acc)). page_params() -> [#{ name => page, From 5652917af6fd4cbe9016cac85c7f1159097caa1e Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 19 Aug 2021 10:27:32 +0800 Subject: [PATCH 109/306] chore(authorization): moves authorization configuration items from zone to root --- apps/emqx/etc/emqx.conf | 85 +++++++++++------------ apps/emqx/src/emqx_access_control.erl | 13 ++-- apps/emqx/src/emqx_authz_cache.erl | 64 ++++++++--------- apps/emqx/src/emqx_channel.erl | 25 +++---- apps/emqx/src/emqx_schema.erl | 9 ++- apps/emqx/test/emqx_authz_cache_SUITE.erl | 4 -- 6 files changed, 93 insertions(+), 107 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 74a29b31d..50ddbfcde 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -88,6 +88,48 @@ broker { perf.trie_compaction = true } + +authorization { + ## Behaviour after not matching a rule. + ## + ## @doc authorization.no_match + ## ValueType: allow | deny + ## Default: allow + no_match: allow + + ## The action when authorization check reject current operation + ## + ## @doc authorization.deny_action + ## ValueType: ignore | disconnect + ## Default: ignore + deny_action: ignore + + ## Whether to enable Authorization cache. + ## + ## If enabled, Authorization roles for each client will be cached in the memory + ## + ## @doc authorization.cache.enable + ## ValueType: Boolean + ## Default: true + cache.enable: true + + ## The maximum count of Authorization entries can be cached for a client. + ## + ## @doc authorization.cache.max_size + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 32 + cache.max_size: 32 + + ## The time after which an Authorization cache entry will be deleted + ## + ## @doc authorization.cache.ttl + ## ValueType: Duration + ## Default: 1m + cache.ttl: 1m +} + + ##================================================================== ## Zones and Listeners ##================================================================== @@ -114,7 +156,6 @@ broker { ## - `auth.*` ## - `stats.*` ## - `mqtt.*` -## - `authorization.*` ## - `flapping_detect.*` ## - `force_shutdown.*` ## - `conn_congestion.*` @@ -396,47 +437,6 @@ zones.default { } - authorization { - - ## Enable Authorization check. - ## - ## @doc zones..authorization.enable - ## ValueType: Boolean - ## Default: true - enable = true - - ## The action when authorization check reject current operation - ## - ## @doc zones..authorization.deny_action - ## ValueType: ignore | disconnect - ## Default: ignore - deny_action = ignore - - ## Whether to enable Authorization cache. - ## - ## If enabled, Authorization roles for each client will be cached in the memory - ## - ## @doc zones..authorization.cache.enable - ## ValueType: Boolean - ## Default: true - cache.enable = true - - ## The maximum count of Authorization entries can be cached for a client. - ## - ## @doc zones..authorization.cache.max_size - ## ValueType: Integer - ## Range: [0, 1048576] - ## Default: 32 - cache.max_size = 32 - - ## The time after which an Authorization cache entry will be deleted - ## - ## @doc zones..authorization.cache.ttl - ## ValueType: Duration - ## Default: 1m - cache.ttl = 1m - } - flapping_detect { ## Enable Flapping Detection. ## @@ -1158,7 +1158,6 @@ zones.default { #This is an example zone which has less "strict" settings. #It's useful to clients connecting the broker from trusted networks. zones.internal { - authorization.enable = true auth.enable = false listeners.mqtt_internal { type = tcp diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 111a86112..7d5b009ba 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -39,23 +39,24 @@ authenticate(Credential) -> %% @doc Check Authorization -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny. -authorize(ClientInfo = #{zone := Zone}, PubSub, Topic) -> - case emqx_authz_cache:is_enabled(Zone) of +authorize(ClientInfo, PubSub, Topic) -> + case emqx_authz_cache:is_enabled() of true -> check_authorization_cache(ClientInfo, PubSub, Topic); false -> do_authorize(ClientInfo, PubSub, Topic) end. -check_authorization_cache(ClientInfo = #{zone := Zone}, PubSub, Topic) -> - case emqx_authz_cache:get_authz_cache(Zone, PubSub, Topic) of +check_authorization_cache(ClientInfo, PubSub, Topic) -> + case emqx_authz_cache:get_authz_cache(PubSub, Topic) of not_found -> AuthzResult = do_authorize(ClientInfo, PubSub, Topic), - emqx_authz_cache:put_authz_cache(Zone, PubSub, Topic, AuthzResult), + emqx_authz_cache:put_authz_cache(PubSub, Topic, AuthzResult), AuthzResult; AuthzResult -> AuthzResult end. do_authorize(ClientInfo, PubSub, Topic) -> - case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], allow) of + NoMatch = emqx:get_config([authorization, no_match], allow), + case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], NoMatch) of allow -> allow; _Other -> deny end. diff --git a/apps/emqx/src/emqx_authz_cache.erl b/apps/emqx/src/emqx_authz_cache.erl index a13294da2..10ddbd21c 100644 --- a/apps/emqx/src/emqx_authz_cache.erl +++ b/apps/emqx/src/emqx_authz_cache.erl @@ -18,15 +18,15 @@ -include("emqx.hrl"). --export([ list_authz_cache/1 - , get_authz_cache/3 - , put_authz_cache/4 - , cleanup_authz_cache/1 +-export([ list_authz_cache/0 + , get_authz_cache/2 + , put_authz_cache/3 + , cleanup_authz_cache/0 , empty_authz_cache/0 , dump_authz_cache/0 - , get_cache_max_size/1 - , get_cache_ttl/1 - , is_enabled/1 + , get_cache_max_size/0 + , get_cache_ttl/0 + , is_enabled/0 , drain_cache/0 ]). @@ -50,45 +50,45 @@ cache_k(PubSub, Topic)-> {PubSub, Topic}. cache_v(AuthzResult)-> {AuthzResult, time_now()}. drain_k() -> {?MODULE, drain_timestamp}. --spec(is_enabled(atom()) -> boolean()). -is_enabled(Zone) -> - emqx_config:get_zone_conf(Zone, [authorization, cache, enable]). +-spec(is_enabled() -> boolean()). +is_enabled() -> + emqx:get_config([authorization, cache, enable], false). --spec(get_cache_max_size(atom()) -> integer()). -get_cache_max_size(Zone) -> - emqx_config:get_zone_conf(Zone, [authorization, cache, max_size]). +-spec(get_cache_max_size() -> integer()). +get_cache_max_size() -> + emqx:get_config([authorization, cache, max_size]). --spec(get_cache_ttl(atom()) -> integer()). -get_cache_ttl(Zone) -> - emqx_config:get_zone_conf(Zone, [authorization, cache, ttl]). +-spec(get_cache_ttl() -> integer()). +get_cache_ttl() -> + emqx:get_config([authorization, cache, ttl]). --spec(list_authz_cache(atom()) -> [authz_cache_entry()]). -list_authz_cache(Zone) -> - cleanup_authz_cache(Zone), +-spec(list_authz_cache() -> [authz_cache_entry()]). +list_authz_cache() -> + cleanup_authz_cache(), map_authz_cache(fun(Cache) -> Cache end). %% We'll cleanup the cache before replacing an expired authz. --spec get_authz_cache(atom(), emqx_types:pubsub(), emqx_topic:topic()) -> +-spec get_authz_cache(emqx_types:pubsub(), emqx_topic:topic()) -> authz_result() | not_found. -get_authz_cache(Zone, PubSub, Topic) -> +get_authz_cache(PubSub, Topic) -> case erlang:get(cache_k(PubSub, Topic)) of undefined -> not_found; {AuthzResult, CachedAt} -> - if_expired(get_cache_ttl(Zone), CachedAt, + if_expired(get_cache_ttl(), CachedAt, fun(false) -> AuthzResult; (true) -> - cleanup_authz_cache(Zone), + cleanup_authz_cache(), not_found end) end. %% If the cache get full, and also the latest one %% is expired, then delete all the cache entries --spec put_authz_cache(atom(), emqx_types:pubsub(), emqx_topic:topic(), authz_result()) +-spec put_authz_cache(emqx_types:pubsub(), emqx_topic:topic(), authz_result()) -> ok. -put_authz_cache(Zone, PubSub, Topic, AuthzResult) -> - MaxSize = get_cache_max_size(Zone), true = (MaxSize =/= 0), +put_authz_cache(PubSub, Topic, AuthzResult) -> + MaxSize = get_cache_max_size(), true = (MaxSize =/= 0), Size = get_cache_size(), case Size < MaxSize of true -> @@ -96,7 +96,7 @@ put_authz_cache(Zone, PubSub, Topic, AuthzResult) -> false -> NewestK = get_newest_key(), {_AuthzResult, CachedAt} = erlang:get(NewestK), - if_expired(get_cache_ttl(Zone), CachedAt, + if_expired(get_cache_ttl(), CachedAt, fun(true) -> % all cache expired, cleanup first empty_authz_cache(), @@ -123,10 +123,10 @@ evict_authz_cache() -> decr_cache_size(). %% cleanup all the expired cache entries --spec(cleanup_authz_cache(atom()) -> ok). -cleanup_authz_cache(Zone) -> +-spec(cleanup_authz_cache() -> ok). +cleanup_authz_cache() -> keys_queue_set( - cleanup_authz(get_cache_ttl(Zone), keys_queue_get())). + cleanup_authz(get_cache_ttl(), keys_queue_get())). get_oldest_key() -> keys_queue_pick(queue_front()). @@ -143,8 +143,8 @@ dump_authz_cache() -> map_authz_cache(fun(Cache) -> Cache end). map_authz_cache(Fun) -> - [Fun(R) || R = {{SubPub, _T}, _Authz} <- get(), SubPub =:= publish - orelse SubPub =:= subscribe]. + [Fun(R) || R = {{SubPub, _T}, _Authz} <- erlang:get(), + SubPub =:= publish orelse SubPub =:= subscribe]. foreach_authz_cache(Fun) -> _ = map_authz_cache(Fun), ok. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5988e03e5..3bfe6bbf0 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -425,7 +425,7 @@ 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}) -> case emqx_packet:check(Packet) of ok -> TopicFilters0 = parse_topic_filters(TopicFilters), @@ -434,7 +434,7 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), HasAuthzDeny = lists:any(fun({_TopicFilter, ReasonCode}) -> ReasonCode =:= ?RC_NOT_AUTHORIZED end, TupleTopicFilters0), - DenyAction = emqx_config:get_zone_conf(Zone, [authorization, deny_action]), + DenyAction = emqx:get_config([authorization, deny_action], ignore), case DenyAction =:= disconnect andalso HasAuthzDeny of true -> handle_out(disconnect, ?RC_NOT_AUTHORIZED, Channel); false -> @@ -536,8 +536,7 @@ process_connect(AckProps, Channel = #channel{conninfo = ConnInfo, %% Process Publish %%-------------------------------------------------------------------- -process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), - Channel = #channel{clientinfo = #{zone := Zone}}) -> +process_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) -> case pipeline([fun check_quota_exceeded/2, fun process_alias/2, fun check_pub_alias/2, @@ -550,7 +549,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_config:get_zone_conf(Zone, [authorization, deny_action]) of + case emqx:get_config([authorization, deny_action], ignore) of ignore -> case QoS of ?QOS_0 -> {ok, NChannel}; @@ -955,9 +954,8 @@ handle_call({takeover, 'end'}, Channel = #channel{session = Session, AllPendings = lists:append(Delivers, Pendings), disconnect_and_shutdown(takeovered, AllPendings, Channel); -handle_call(list_authz_cache, #channel{clientinfo = #{zone := Zone}} - = Channel) -> - {reply, emqx_authz_cache:list_authz_cache(Zone), Channel}; +handle_call(list_authz_cache, Channel) -> + {reply, emqx_authz_cache:list_authz_cache(), Channel}; handle_call({quota, Policy}, Channel) -> Zone = info(zone, Channel), @@ -1420,8 +1418,7 @@ check_pub_alias(_Packet, _Channel) -> ok. check_pub_authz(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #channel{clientinfo = ClientInfo}) -> - case is_authz_enabled(ClientInfo) andalso - emqx_access_control:authorize(ClientInfo, publish, Topic) of + case emqx_access_control:authorize(ClientInfo, publish, Topic) of false -> ok; allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} @@ -1454,8 +1451,7 @@ check_sub_authzs([], _Channel, Acc) -> lists:reverse(Acc). check_sub_authz(TopicFilter, #channel{clientinfo = ClientInfo}) -> - case is_authz_enabled(ClientInfo) andalso - emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of + case emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of false -> allow; Result -> Result end. @@ -1621,11 +1617,6 @@ maybe_shutdown(Reason, Channel = #channel{conninfo = ConnInfo}) -> _ -> shutdown(Reason, Channel) end. -%%-------------------------------------------------------------------- -%% Is Authorization enabled? -is_authz_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> - (not IsSuperuser) andalso emqx_config:get_zone_conf(Zone, [authorization, enable]). - %%-------------------------------------------------------------------- %% Parse Topic Filters diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 820754363..cf18f1256 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -70,7 +70,7 @@ -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([ssl/1]). -structs() -> ["zones", "listeners", "broker", "plugins", "sysmon", "alarm"]. +structs() -> ["zones", "listeners", "broker", "plugins", "sysmon", "alarm", "authorization"]. fields("stats") -> [ {"enable", t(boolean(), undefined, true)} @@ -80,10 +80,10 @@ fields("auth") -> [ {"enable", t(boolean(), undefined, false)} ]; -fields("authorization_settings") -> - [ {"enable", t(boolean(), undefined, true)} - , {"cache", ref("authorization_cache")} +fields("authorization") -> + [ {"no_match", t(union(allow, deny), undefined, allow)} , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} + , {"cache", ref("authorization_cache")} ]; fields("authorization_cache") -> @@ -129,7 +129,6 @@ fields("zones") -> fields("zone_settings") -> [ {"mqtt", ref("mqtt")} - , {"authorization", ref("authorization_settings")} , {"auth", ref("auth")} , {"stats", ref("stats")} , {"flapping_detect", ref("flapping_detect")} diff --git a/apps/emqx/test/emqx_authz_cache_SUITE.erl b/apps/emqx/test/emqx_authz_cache_SUITE.erl index 849997298..46a4d7d74 100644 --- a/apps/emqx/test/emqx_authz_cache_SUITE.erl +++ b/apps/emqx/test/emqx_authz_cache_SUITE.erl @@ -26,7 +26,6 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - toggle_authz(true), Config. end_per_suite(_Config) -> @@ -78,6 +77,3 @@ t_drain_authz_cache(_) -> {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), ?assert(length(gen_server:call(ClientPid, list_authz_cache)) > 0), emqtt:stop(Client). - -toggle_authz(Bool) when is_boolean(Bool) -> - emqx_config:put_zone_conf(default, [authorization, enable], Bool). From bfb363bc61dc3004eecdf4fb4afbaec990335428 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 19 Aug 2021 11:29:49 +0800 Subject: [PATCH 110/306] chore(emqx_authz): rename authorization to authorization_rules in emqx_authz.conf --- apps/emqx_authz/etc/emqx_authz.conf | 2 +- apps/emqx_authz/src/emqx_authz.erl | 6 +++--- apps/emqx_authz/src/emqx_authz_schema.erl | 4 ++-- apps/emqx_authz/test/emqx_authz_SUITE.erl | 10 +++++----- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 9 ++++++--- apps/emqx_authz/test/emqx_authz_http_SUITE.erl | 6 ++++-- apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl | 6 +++--- apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl | 6 +++--- apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl | 6 +++--- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 6 ++++-- 10 files changed, 34 insertions(+), 27 deletions(-) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 57ca290d5..a100c5140 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,4 +1,4 @@ -authorization { +authorization_rules { rules = [ # { # type: http diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index e3e540de0..aceb967c2 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -38,7 +38,7 @@ -export([post_config_update/3, pre_config_update/2]). --define(CONF_KEY_PATH, [authorization, rules]). +-define(CONF_KEY_PATH, [authorization_rules, rules]). -spec(register_metrics() -> ok). register_metrics() -> @@ -187,9 +187,9 @@ post_config_update(_, NewRules, _OldConf) -> %%-------------------------------------------------------------------- check_rules(RawRules) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"rules">> => RawRules}}), #{format => richmap}), + {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization_rules">> => #{<<"rules">> => RawRules}}), #{format => richmap}), CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), + #{authorization_rules := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), Rules. find_rule_by_id(Id) -> find_rule_by_id(Id, lookup()). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index cc109534f..0c36ccd90 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -17,9 +17,9 @@ , fields/1 ]). -structs() -> ["authorization"]. +structs() -> ["authorization_rules"]. -fields("authorization") -> +fields("authorization_rules") -> [ {rules, rules()} ]; fields(http) -> diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 0452ff96c..bcc855a59 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -33,8 +33,8 @@ groups() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Config. end_per_suite(_Config) -> @@ -87,7 +87,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update(tail, [?RULE3]), Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), - ?assertMatch(Lists1, emqx:get_config([authorization, rules], [])), + ?assertMatch(Lists1, emqx:get_config([authorization_rules, rules], [])), [#{annotations := #{id := Id1, principal := all, @@ -109,7 +109,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), - ?assertMatch(Lists2, emqx:get_config([authorization, rules], [])), + ?assertMatch(Lists2, emqx:get_config([authorization_rules, rules], [])), [#{annotations := #{id := Id1, principal := all, diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 0600125f9..9b6153465 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). + -import(emqx_ct_http, [ request_api/3 , request_api/5 , get_http_data/1 @@ -77,10 +79,11 @@ groups() -> init_per_suite(Config) -> ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Config. @@ -94,7 +97,7 @@ set_special_configs(emqx_management) -> applications =>[#{id => "admin", secret => "public"}]}), ok; set_special_configs(emqx_authz) -> - emqx_config:put([authorization], #{rules => []}), + emqx_config:put([authorization_rules], #{rules => []}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index b284744af..fb95c1b00 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -23,6 +23,8 @@ -include_lib("common_test/include/ct.hrl"). -define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -37,8 +39,8 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{ <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, <<"headers">> => #{}, diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index b68ee2800..cffc0ad76 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -37,8 +37,8 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{ <<"config">> => #{ <<"mongo_type">> => <<"single">>, <<"server">> => <<"127.0.0.1:27017">>, diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index e6164cacd..dcc0e47d7 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -38,8 +38,8 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index c304f06a9..b4383e21e 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -38,8 +38,8 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index a494159e3..d3eebeb2e 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -23,6 +23,8 @@ -include_lib("common_test/include/ct.hrl"). -define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). + all() -> emqx_ct:all(?MODULE). @@ -37,8 +39,8 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), - {ok, _} = emqx:update_config([zones, default, authorization, cache, enable], false), - {ok, _} = emqx:update_config([zones, default, authorization, enable], true), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{ <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, From 1886aa8bff3924ca8301221f26380d94b8ffe695 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Thu, 19 Aug 2021 11:43:00 +0800 Subject: [PATCH 111/306] chore: fix dialyzer error --- apps/emqx/src/emqx_channel.erl | 13 ++++--------- apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 3bfe6bbf0..59c6447ab 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1419,7 +1419,6 @@ check_pub_alias(_Packet, _Channel) -> ok. check_pub_authz(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #channel{clientinfo = ClientInfo}) -> case emqx_access_control:authorize(ClientInfo, publish, Topic) of - false -> ok; allow -> ok; deny -> {error, ?RC_NOT_AUTHORIZED} end. @@ -1440,8 +1439,10 @@ check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, check_sub_authzs(TopicFilters, Channel) -> check_sub_authzs(TopicFilters, Channel, []). -check_sub_authzs([ TopicFilter = {Topic, _} | More] , Channel, Acc) -> - case check_sub_authz(Topic, Channel) of +check_sub_authzs([ TopicFilter = {Topic, _} | More], + Channel = #channel{clientinfo = ClientInfo}, + Acc) -> + case emqx_access_control:authorize(ClientInfo, subscribe, Topic) of allow -> check_sub_authzs(More, Channel, [ {TopicFilter, 0} | Acc]); deny -> @@ -1450,12 +1451,6 @@ check_sub_authzs([ TopicFilter = {Topic, _} | More] , Channel, Acc) -> check_sub_authzs([], _Channel, Acc) -> lists:reverse(Acc). -check_sub_authz(TopicFilter, #channel{clientinfo = ClientInfo}) -> - case emqx_access_control:authorize(ClientInfo, subscribe, TopicFilter) of - false -> allow; - Result -> Result - end. - %%-------------------------------------------------------------------- %% Check Sub Caps diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index fa7b2a357..b1a74375d 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -649,7 +649,7 @@ handle_call(discard, Channel) -> handle_call(list_authz_cache, Channel) -> %% This won't work - {reply, emqx_authz_cache:list_authz_cache(default), Channel}; + {reply, emqx_authz_cache:list_authz_cache(), Channel}; %% XXX: No Quota Now % handle_call({quota, Policy}, Channel) -> From a7fac1a7a39b53b2babca99fd0edcb502e488cfd Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 20 Aug 2021 09:43:44 +0800 Subject: [PATCH 112/306] feat(authz): support authorization config file part 1. --- apps/emqx_authz/etc/authorization_rules.conf | 30 ++++ apps/emqx_authz/etc/emqx_authz.conf | 4 + apps/emqx_authz/include/emqx_authz.hrl | 18 ++- apps/emqx_authz/src/emqx_authz.erl | 22 +++ apps/emqx_authz/src/emqx_authz_rule.erl | 148 ++++++++++++++++++ apps/emqx_authz/src/emqx_authz_schema.erl | 14 ++ .../emqx_authz/test/emqx_authz_rule_SUITE.erl | 138 ++++++++++++++++ rebar.config.erl | 1 + 8 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 apps/emqx_authz/etc/authorization_rules.conf create mode 100644 apps/emqx_authz/src/emqx_authz_rule.erl create mode 100644 apps/emqx_authz/test/emqx_authz_rule_SUITE.erl diff --git a/apps/emqx_authz/etc/authorization_rules.conf b/apps/emqx_authz/etc/authorization_rules.conf new file mode 100644 index 000000000..79493b57a --- /dev/null +++ b/apps/emqx_authz/etc/authorization_rules.conf @@ -0,0 +1,30 @@ +%%-------------------------------------------------------------------- +%% -type(ipaddress() :: {ipaddress, string() | [string()]}) +%% +%% -type(username() :: {username, regex()}) +%% +%% -type(clientid() :: {clientid, regex()}) +%% +%% -type(who() :: ipaddress() | username() | clientid() | +%% {'and', [ipaddress() | username() | clientid()]} | +%% {'or', [ipaddress() | username() | clientid()]} | +%% all). +%% +%% -type(action() :: subscribe | publish | all). +%% +%% -type(topic_filters() :: string()). +%% +%% -type(topics() :: [topic_filters() | {eq, topic_filters()}]). +%% +%% -type(permission() :: allow | deny). +%% +%% -type(rule() :: {permission(), who(), access(), topics()}). +%%-------------------------------------------------------------------- + +{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_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index a100c5140..baabd8a37 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,5 +1,9 @@ authorization_rules { rules = [ + # { + # type: file + # path: {{ platform_etc_dir }}/authorization_rules.conf + # }, # { # type: http # config: { diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index 76aa20688..30297ac66 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -1,4 +1,20 @@ --type(rule() :: #{atom() => any()}). +-type(ipaddress() :: {ipaddr, esockd_cidr:cidr_string()} | + {ipaddrs, list(esockd_cidr:cidr_string())}). + +-type(username() :: {username, binary()}). + +-type(clientid() :: {clientid, binary()}). + +-type(who() :: ipaddress() | username() | clientid() | + {'and', [ipaddress() | username() | clientid()]} | + {'or', [ipaddress() | username() | clientid()]} | + all). + +-type(action() :: subscribe | publish | all). + +-type(permission() :: allow | deny). + +-type(rule() :: {permission(), who(), action(), list(emqx_topic:topic())}). -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 aceb967c2..2c6395199 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -253,6 +253,28 @@ init_rule(#{topics := Topics, } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> init_rule(Rule#{annotations =>#{id => gen_id(simple)}}); +init_rule(#{principal := Principal, + enable := true, + type := file, + path := Path + } = Rule) -> + Rules = case file:consult(Path) of + {ok, Terms} -> + [emqx_authz_rule:compile(Term) || Term <- Terms]; + {error, eacces} -> + ?LOG(alert, "Insufficient permissions to read the ~s file", [Path]), + error(eaccess); + {error, enoent} -> + ?LOG(alert, "The ~s file does not exist", [Path]), + error(enoent); + {error, Reason} -> + ?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]), + error(Reason) + end, + Rule#{annotations => + #{id => gen_id(file), + rules => Rules + }}; init_rule(#{principal := Principal, enable := true, type := http, diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl new file mode 100644 index 000000000..8fd7b9721 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -0,0 +1,148 @@ +%%-------------------------------------------------------------------- +%% 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_rule). + +-include("emqx_authz.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-endif. + +%% APIs +-export([ match/4 + , compile/1 + ]). + +-export_type([rule/0]). + +compile({Permission, Who, Action, TopicFilters}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) -> + {Permission, compile_who(Who), Action, [compile_topic(Topic) || Topic <- TopicFilters]}; +compile({Permission, Who, Action, Topic}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action) -> + {Permission, compile_who(Who), Action, [compile_topic(Topic)]}. + +compile_who(all) -> all; +compile_who({username, Username}) -> + {ok, MP} = re:compile(bin(Username)), + {username, MP}; +compile_who({clientid, Clientid}) -> + {ok, MP} = re:compile(bin(Clientid)), + {clientid, MP}; +compile_who({ipaddr, CIDR}) -> + {ipaddr, esockd_cidr:parse(CIDR, true)}; +compile_who({ipaddrs, CIDRs}) -> + {ipaddrs, lists:map(fun(CIDR) -> esockd_cidr:parse(CIDR, true) end, CIDRs)}; +compile_who({'and', L}) when is_list(L) -> + {'and', [compile_who(Who) || Who <- L]}; +compile_who({'or', L}) when is_list(L) -> + {'or', [compile_who(Who) || Who <- L]}. + +compile_topic({eq, Topic}) -> + {eq, emqx_topic:words(bin(Topic))}; +compile_topic(Topic) -> + Words = emqx_topic:words(bin(Topic)), + case pattern(Words) of + true -> {pattern, Words}; + false -> Words + end. + +pattern(Words) -> + lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). + +bin(L) when is_list(L) -> + list_to_binary(L); +bin(B) when is_binary(B) -> + B. + +-spec(match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) + -> {matched, allow} | {matched, deny} | nomatch). +match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) -> + case match_action(PubSub, Action) andalso + match_who(Client, Who) andalso + match_topics(Client, Topic, TopicFilters) of + true -> {matched, Permission}; + _ -> nomatch + end. + +match_action(publish, publish) -> true; +match_action(subscribe, subscribe) -> true; +match_action(_, all) -> true; +match_action(_, _) -> false. + +match_who(_, all) -> true; +match_who(#{username := undefined}, {username, _MP}) -> + false; +match_who(#{username := Username}, {username, MP}) -> + case re:run(Username, MP) of + {match, _} -> true; + _ -> false + end; +match_who(#{clientid := Clientid}, {clientid, MP}) -> + case re:run(Clientid, MP) of + {match, _} -> true; + _ -> false + end; +match_who(#{peerhost := undefined}, {ipaddr, _CIDR}) -> + false; +match_who(#{peerhost := IpAddress}, {ipaddr, CIDR}) -> + esockd_cidr:match(IpAddress, CIDR); +match_who(#{peerhost := undefined}, {ipaddrs, _CIDR}) -> + false; +match_who(#{peerhost := IpAddress}, {ipaddrs, CIDRs}) -> + lists:any(fun(CIDR) -> + esockd_cidr:match(IpAddress, CIDR) + end, CIDRs); +match_who(ClientInfo, {'and', Principals}) when is_list(Principals) -> + lists:foldl(fun(Principal, Permission) -> + match_who(ClientInfo, Principal) andalso Permission + end, true, Principals); +match_who(ClientInfo, {'or', Principals}) when is_list(Principals) -> + lists:foldl(fun(Principal, Permission) -> + match_who(ClientInfo, Principal) orelse Permission + end, false, Principals); +match_who(_, _) -> false. + +match_topics(_ClientInfo, _Topic, []) -> + false; +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); +match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> + match_topic(emqx_topic:words(Topic), TopicFilter) + orelse match_topics(ClientInfo, Topic, Filters). + +match_topic(Topic, {'eq', TopicFilter}) -> + Topic =:= TopicFilter; +match_topic(Topic, TopicFilter) -> + emqx_topic:match(Topic, TopicFilter). + +feed_var(ClientInfo, Pattern) -> + feed_var(ClientInfo, Pattern, []). +feed_var(_ClientInfo, [], Acc) -> + lists:reverse(Acc); +feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) -> + feed_var(ClientInfo, Words, [<<"%c">>|Acc]); +feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) -> + feed_var(ClientInfo, Words, [ClientId |Acc]); +feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) -> + feed_var(ClientInfo, Words, [<<"%u">>|Acc]); +feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) -> + feed_var(ClientInfo, Words, [Username|Acc]); +feed_var(ClientInfo, [W|Words], Acc) -> + feed_var(ClientInfo, Words, [W|Acc]). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 0c36ccd90..958ad9dec 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -22,6 +22,19 @@ structs() -> ["authorization_rules"]. fields("authorization_rules") -> [ {rules, rules()} ]; +fields(file) -> + [ {principal, principal()} + , {type, #{type => http}} + , {enable, #{type => boolean(), + default => true}} + , {path, #{type => string(), + validator => fun(S) -> case filelib:is_file(S) of + true -> ok; + _ -> {error, "File does not exist"} + end + end + }} + ]; fields(http) -> [ {principal, principal()} , {type, #{type => http}} @@ -148,6 +161,7 @@ union_array(Item) when is_list(Item) -> rules() -> #{type => union_array( [ hoconsc:ref(?MODULE, simple_rule) + , hoconsc:ref(?MODULE, file) , hoconsc:ref(?MODULE, http) , hoconsc:ref(?MODULE, mysql) , hoconsc:ref(?MODULE, pgsql) diff --git a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl new file mode 100644 index 000000000..e6e450a63 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl @@ -0,0 +1,138 @@ +%%-------------------------------------------------------------------- +%% 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_rule_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"). + +-define(RULE1, {deny, all, all, ["#"]}). +-define(RULE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}). +-define(RULE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}). +-define(RULE4, {allow, {'and', [{clientid, "^test?"}, {username, "^test?"}]}, publish, ["topic/test"]}). +-define(RULE5, {allow, {'or', [{username, "^test"}, {clientid, "test?"}]}, publish, ["%u", "%c"]}). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_ct_helpers:start_apps([emqx_authz]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([emqx_authz]), + ok. + +t_compile(_) -> + ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?RULE1)), + + ?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?RULE2)), + + ?assertEqual({allow, + {ipaddrs,[{{127,0,0,1},{127,0,0,1},32}, + {{192,168,1,0},{192,168,1,255},24}]}, + subscribe, + [{pattern,[<<"%c">>]}] + }, emqx_authz_rule:compile(?RULE3)), + + ?assertMatch({allow, + {'and', [{clientid, {re_pattern, _, _, _, _}}, {username, {re_pattern, _, _, _, _}}]}, + publish, + [[<<"topic">>, <<"test">>]] + }, emqx_authz_rule:compile(?RULE4)), + + ?assertMatch({allow, + {'or', [{username, {re_pattern, _, _, _, _}}, {clientid, {re_pattern, _, _, _, _}}]}, + publish, + [{pattern, [<<"%u">>]}, {pattern, [<<"%c">>]}] + }, emqx_authz_rule:compile(?RULE5)), + ok. + + +t_match(_) -> + ClientInfo1 = #{clientid => <<"test">>, + username => <<"test">>, + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp + }, + ClientInfo2 = #{clientid => <<"test">>, + username => <<"test">>, + peerhost => {192,168,1,10}, + zone => default, + listener => mqtt_tcp + }, + ClientInfo3 = #{clientid => <<"test">>, + username => <<"fake">>, + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp + }, + ClientInfo4 = #{clientid => <<"fake">>, + username => <<"test">>, + peerhost => {127,0,0,1}, + zone => default, + listener => mqtt_tcp + }, + + ?assertEqual({matched, deny}, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE1))), + ?assertEqual({matched, deny}, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"+">>, emqx_authz_rule:compile(?RULE1))), + ?assertEqual({matched, deny}, + emqx_authz_rule:match(ClientInfo3, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE1))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE2))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE2))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE2))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, subscribe, <<"test">>, emqx_authz_rule:compile(?RULE3))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"test">>, emqx_authz_rule:compile(?RULE3))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo2, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE3))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo2, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo3, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + ?assertEqual(nomatch, + emqx_authz_rule:match(ClientInfo4, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo1, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo2, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo3, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo3, publish, <<"fake">>, emqx_authz_rule:compile(?RULE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo4, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + ?assertEqual({matched, allow}, + emqx_authz_rule:match(ClientInfo4, publish, <<"fake">>, emqx_authz_rule:compile(?RULE5))), + + ok. + diff --git a/rebar.config.erl b/rebar.config.erl index ae2cbfe88..eee5d69b1 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -340,6 +340,7 @@ relx_overlay(ReleaseType) -> , {copy, "bin/emqx_ctl", "bin/emqx_ctl-{{release_version}}"} %% for relup , {copy, "bin/install_upgrade.escript", "bin/install_upgrade.escript-{{release_version}}"} %% for relup , {copy, "apps/emqx_gateway/src/lwm2m/lwm2m_xml", "etc/lwm2m_xml"} + , {copy, "apps/emqx_authz/etc/authorization_rules.conf", "etc/authorization_rules.conf"} , {template, "bin/emqx.cmd", "bin/emqx.cmd"} , {template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"} , {copy, "bin/nodetool", "bin/nodetool"} From a2bafd1a18ff333d54d1e5a1d2b61e08ec810d1f Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Tue, 24 Aug 2021 15:08:21 +0800 Subject: [PATCH 113/306] feat(authz): support authorization config file part 2 --- .github/workflows/build_packages.yaml | 4 +- apps/emqx_authz/etc/emqx_authz.conf | 7 +- apps/emqx_authz/include/emqx_authz.hrl | 9 +- apps/emqx_authz/src/emqx_authz.erl | 226 ++++-------------- apps/emqx_authz/src/emqx_authz_api.erl | 49 +++- apps/emqx_authz/src/emqx_authz_mongo.erl | 27 +-- apps/emqx_authz/src/emqx_authz_mysql.erl | 42 ++-- apps/emqx_authz/src/emqx_authz_pgsql.erl | 40 +--- apps/emqx_authz/src/emqx_authz_redis.erl | 28 +-- apps/emqx_authz/src/emqx_authz_rule.erl | 28 ++- apps/emqx_authz/src/emqx_authz_schema.erl | 34 +-- apps/emqx_authz/test/emqx_authz_SUITE.erl | 194 ++++++--------- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 164 ++++++++----- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 15 +- .../test/emqx_authz_mongo_SUITE.erl | 21 +- .../test/emqx_authz_mysql_SUITE.erl | 36 ++- .../test/emqx_authz_pgsql_SUITE.erl | 35 ++- .../test/emqx_authz_redis_SUITE.erl | 20 +- .../src/emqx_connector_mongo.erl | 12 +- .../src/emqx_connector_redis.erl | 11 +- 20 files changed, 396 insertions(+), 606 deletions(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index a96ef705e..f42835a18 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -20,8 +20,8 @@ jobs: container: ${{ matrix.container }} outputs: - profiles: ${{ steps.set_profile.outputs.profiles}} - old_vsns: ${{ steps.set_profile.outputs.old_vsns}} + profiles: ${{ steps.set_profile.outputs.profiles }} + old_vsns: ${{ steps.set_profile.outputs.old_vsns }} steps: - uses: actions/checkout@v2 diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index baabd8a37..5928b1f97 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -68,11 +68,6 @@ authorization_rules { # } # collection: mqtt_authz # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } - # }, - { - permission = allow - action = all - topics = ["#"] - } + # } ] } diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index 30297ac66..a4f10c5f9 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -19,8 +19,13 @@ -define(APP, emqx_authz). --define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). --define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= all))). +-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse + (A =:= deny) orelse (A =:= <<"deny">>) + )). +-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse + (A =:= publish) orelse (A =:= <<"publish">>) orelse + (A =:= all) orelse (A =:= <<"all">>) + )). -record(authz_metrics, { allow = 'client.authorize.allow', diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 2c6395199..3c8a56629 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -27,13 +27,11 @@ -export([ register_metrics/0 , init/0 - , init_rule/1 , lookup/0 , lookup/1 , move/2 , update/2 , authorize/5 - , match/4 ]). -export([post_config_update/3, pre_config_update/2]). @@ -47,7 +45,7 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NRules = [init_rule(Rule) || Rule <- emqx:get_config(?CONF_KEY_PATH, [])], + NRules = [init_provider(Rule) || Rule <- emqx:get_config(?CONF_KEY_PATH, [])], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> @@ -148,12 +146,12 @@ post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewRules, _OldRules) ok = emqx_authz_cache:drain_cache(); post_config_update({head, Rules}, _NewRules, _OldConf) -> - InitedRules = [init_rule(R) || R <- check_rules(Rules)], + InitedRules = [init_provider(R) || R <- check_rules(Rules)], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules ++ lookup()]}, -1), ok = emqx_authz_cache:drain_cache(); post_config_update({tail, Rules}, _NewRules, _OldConf) -> - InitedRules = [init_rule(R) || R <- check_rules(Rules)], + InitedRules = [init_provider(R) || R <- check_rules(Rules)], emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedRules]}, -1), ok = emqx_authz_cache:drain_cache(); @@ -167,14 +165,14 @@ post_config_update({{replace_once, Id}, Rule}, _NewRules, _OldConf) when is_map( ok = emqx_resource:remove(Id) end, {OldRules1, OldRules2 } = lists:split(Index, OldInitedRules), - InitedRules = [init_rule(R#{annotations => #{id => Id}}) || R <- check_rules([Rule])], + InitedRules = [init_provider(R#{annotations => #{id => Id}}) || R <- check_rules([Rule])], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldRules1) ++ InitedRules ++ OldRules2]}, -1), ok = emqx_authz_cache:drain_cache(); post_config_update(_, NewRules, _OldConf) -> %% overwrite the entire config! OldInitedRules = lookup(), - InitedRules = [init_rule(Rule) || Rule <- NewRules], + InitedRules = [init_provider(Rule) || Rule <- NewRules], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules]}, -1), lists:foreach(fun (#{type := _Type, enable := true, annotations := #{id := Id}}) -> ok = emqx_resource:remove(Id); @@ -235,29 +233,11 @@ create_resource(#{type := DB, {error, Reason} -> {error, Reason} end. --spec(init_rule(rule()) -> rule()). -init_rule(#{topics := Topics, - action := Action, - permission := Permission, - principal := Principal, - annotations := #{id := Id} - } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> - Rule#{annotations => - #{id => Id, - principal => compile_principal(Principal), - topics => [compile_topic(Topic) || Topic <- Topics]} - }; -init_rule(#{topics := Topics, - action := Action, - permission := Permission - } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> - init_rule(Rule#{annotations =>#{id => gen_id(simple)}}); - -init_rule(#{principal := Principal, - enable := true, - type := file, - path := Path - } = Rule) -> +-spec(init_provider(rule()) -> rule()). +init_provider(#{enable := true, + type := file, + path := Path + } = Rule) -> Rules = case file:consult(Path) of {ok, Terms} -> [emqx_authz_rule:compile(Term) || Term <- Terms]; @@ -275,92 +255,42 @@ init_rule(#{principal := Principal, #{id => gen_id(file), rules => Rules }}; -init_rule(#{principal := Principal, - enable := true, - type := http, - config := #{url := Url} = Config - } = Rule) -> +init_provider(#{enable := true, + type := http, + config := #{url := Url} = Config + } = Rule) -> NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), case create_resource(Rule#{config := NConfig}) of {error, Reason} -> error({load_config_error, Reason}); Id -> Rule#{annotations => - #{id => Id, - principal => compile_principal(Principal) - } + #{id => Id} } end; - -init_rule(#{principal := Principal, - enable := true, - type := DB - } = Rule) when DB =:= redis; - DB =:= mongo -> +init_provider(#{enable := true, + type := DB + } = Rule) when DB =:= redis; + DB =:= mongo -> case create_resource(Rule) of {error, Reason} -> error({load_config_error, Reason}); Id -> Rule#{annotations => - #{id => Id, - principal => compile_principal(Principal) - } + #{id => Id} } end; - -init_rule(#{principal := Principal, - enable := true, - type := DB, - sql := SQL - } = Rule) when DB =:= mysql; - DB =:= pgsql -> +init_provider(#{enable := true, + type := DB, + sql := SQL + } = Rule) when DB =:= mysql; + DB =:= pgsql -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), case create_resource(Rule) of {error, Reason} -> error({load_config_error, Reason}); Id -> Rule#{annotations => #{id => Id, - principal => compile_principal(Principal), sql => Mod:parse_query(SQL) } } end; - -init_rule(#{enable := false, - type := _DB - } = Rule) -> - Rule. - -compile_principal(all) -> all; -compile_principal(#{username := Username}) -> - {ok, MP} = re:compile(bin(Username)), - #{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]}. - -compile_topic(<<"eq ", Topic/binary>>) -> - 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}; - false -> Words - end. - -pattern(Words) -> - lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). - -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. - -b2l(B) when is_list(B) -> B; -b2l(B) when is_binary(B) -> binary_to_list(B). +init_provider(#{enable := false} = Rule) ->Rule. %%-------------------------------------------------------------------- %% AuthZ callbacks @@ -387,97 +317,21 @@ authorize(#{username := Username, end. do_authorize(Client, PubSub, Topic, - [Connector = #{type := DB, - enable := true, - annotations := #{principal := Principal} - } | Tail] ) -> - case match_principal(Client, Principal) of - true -> - Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])), - case Mod:authorize(Client, PubSub, Topic, Connector) of - nomatch -> do_authorize(Client, PubSub, Topic, Tail); - Matched -> Matched - end; - false -> do_authorize(Client, PubSub, Topic, Tail) + [#{type := file, + enable := true, + annotations := #{rule := Rules} + } | Tail] ) -> + case emqx_authz_rule:match(Client, PubSub, Topic, Rules) of + nomatch -> do_authorize(Client, PubSub, Topic, Tail); + Matched -> Matched end; do_authorize(Client, PubSub, Topic, - [#{permission := Permission} = Rule | Tail]) -> - case match(Client, PubSub, Topic, Rule) of - true -> {matched, Permission}; - false -> do_authorize(Client, PubSub, Topic, Tail) + [Connector = #{type := Type, + enable := true + } | Tail] ) -> + Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, Type])), + case Mod:authorize(Client, PubSub, Topic, Connector) of + nomatch -> do_authorize(Client, PubSub, Topic, Tail); + Matched -> Matched end; do_authorize(_Client, _PubSub, _Topic, []) -> nomatch. - -match(Client, PubSub, Topic, - #{action := Action, - annotations := #{ - principal := Principal, - topics := TopicFilters - } - }) -> - match_action(PubSub, Action) andalso - match_principal(Client, Principal) andalso - match_topics(Client, Topic, TopicFilters). - -match_action(publish, publish) -> true; -match_action(subscribe, subscribe) -> true; -match_action(_, all) -> true; -match_action(_, _) -> false. - -match_principal(_, all) -> true; -match_principal(#{username := undefined}, #{username := _MP}) -> - false; -match_principal(#{username := Username}, #{username := MP}) -> - case re:run(Username, MP) of - {match, _} -> true; - _ -> false - end; -match_principal(#{clientid := Clientid}, #{clientid := MP}) -> - case re:run(Clientid, MP) of - {match, _} -> true; - _ -> false - end; -match_principal(#{peerhost := undefined}, #{ipaddress := _CIDR}) -> - false; -match_principal(#{peerhost := IpAddress}, #{ipaddress := CIDR}) -> - esockd_cidr:match(IpAddress, CIDR); -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) -> - lists:foldl(fun(Principal, Permission) -> - match_principal(ClientInfo, Principal) orelse Permission - end, false, Principals); -match_principal(_, _) -> false. - -match_topics(_ClientInfo, _Topic, []) -> - false; -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); -match_topics(ClientInfo, Topic, [TopicFilter|Filters]) -> - match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(ClientInfo, Topic, Filters). - -match_topic(Topic, #{'eq' := TopicFilter}) -> - Topic == TopicFilter; -match_topic(Topic, TopicFilter) -> - emqx_topic:match(Topic, TopicFilter). - -feed_var(ClientInfo, Pattern) -> - feed_var(ClientInfo, Pattern, []). -feed_var(_ClientInfo, [], Acc) -> - lists:reverse(Acc); -feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%c">>|Acc]); -feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) -> - feed_var(ClientInfo, Words, [ClientId |Acc]); -feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [<<"%u">>|Acc]); -feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) -> - feed_var(ClientInfo, Words, [Username|Acc]); -feed_var(ClientInfo, [W|Words], Acc) -> - feed_var(ClientInfo, Words, [W|Acc]). - diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 15aaed65a..1646a9af2 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -419,12 +419,26 @@ move_rule_api() -> {"/authorization/:id/move", Metadata, move_rule}. rules(get, #{query_string := Query}) -> - Rules = lists:foldl(fun (#{type := _Type, enable := true, annotations := #{id := Id} = Annotations} = Rule, AccIn) -> + Rules = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Rule, AccIn) -> NRule = case emqx_resource:health_check(Id) of ok -> - Rule#{annotations => Annotations#{status => healthy}}; + Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => healthy}}; _ -> - Rule#{annotations => Annotations#{status => unhealthy}} + Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => unhealthy}} + end, + lists:append(AccIn, [NRule]); + (#{type := _Type, enable := true, annotations := #{id := Id}} = Rule, AccIn) -> + NRule = case emqx_resource:health_check(Id) of + ok -> + Rule#{annotations => #{id => Id, + status => healthy}}; + _ -> + Rule#{annotations => #{id => Id, + status => unhealthy}} end, lists:append(AccIn, [NRule]); (Rule, AccIn) -> @@ -462,17 +476,26 @@ rules(put, #{body := RawConfig}) -> rule(get, #{bindings := #{id := Id}}) -> case emqx_authz:lookup(Id) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - Rule -> - case maps:get(type, Rule, undefined) of - undefined -> {200, Rule}; + #{type := file} = Rule -> {200, Rule}; + #{config := #{server := Server} = Config} = Rule -> + case emqx_resource:health_check(Id) of + ok -> + {200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => healthy}}}; _ -> - case emqx_resource:health_check(Id) of - ok -> - {200, Rule#{annotations => #{status => healthy}}}; - _ -> - {200, Rule#{annotations => #{status => unhealthy}}} - end - + {200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => unhealthy}}} + end; + Rule -> + case emqx_resource:health_check(Id) of + ok -> + {200, Rule#{annotations => #{id => Id, + status => healthy}}}; + _ -> + {200, Rule#{annotations => #{id => Id, + status => unhealthy}}} end end; rule(put, #{bindings := #{id := RuleId}, body := RawConfig}) -> diff --git a/apps/emqx_authz/src/emqx_authz_mongo.erl b/apps/emqx_authz/src/emqx_authz_mongo.erl index c015f8208..25a787b8f 100644 --- a/apps/emqx_authz/src/emqx_authz_mongo.erl +++ b/apps/emqx_authz/src/emqx_authz_mongo.erl @@ -44,38 +44,19 @@ authorize(Client, PubSub, Topic, nomatch; [] -> nomatch; Rows -> - do_authorize(Client, PubSub, Topic, Rows) + Rules = [ emqx_authz_rule:compile({Permission, all, Action, Topics}) + || #{<<"topics">> := Topics, <<"permission">> := Permission, <<"action">> := Action} <- Rows], + do_authorize(Client, PubSub, Topic, Rules) end. do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [Rule | Tail]) -> - case match(Client, PubSub, Topic, Rule) of + case emqx_authz_rule: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 = #{<<"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:init_rule(NRule)) of - true -> {matched, NPermission}; - false -> nomatch - end. - replvar(Find, #{clientid := Clientid, username := Username, peerhost := IpAddress diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index 2ce991eba..d5550b2fb 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -62,39 +62,25 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> - case match(Client, PubSub, Topic, format_result(Columns, Row)) of + case emqx_authz_rule:match(Client, PubSub, Topic, + emqx_authz_rule:compile(format_result(Columns, Row)) + ) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. -format_result(Columns, Row) -> - L = [ begin - K = lists:nth(I, Columns), - V = lists:nth(I, Row), - {K, V} - end || I <- lists:seq(1, length(Columns)) ], - maps:from_list(L). -match(Client, PubSub, Topic, - #{<<"permission">> := Permission, - <<"action">> := Action, - <<"topic">> := TopicFilter - }) -> - Rule = #{<<"topics">> => [TopicFilter], - <<"action">> => Action, - <<"permission">> => Permission - }, - #{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:init_rule(NRule)) of - true -> {matched, NPermission}; - false -> nomatch - end. +format_result(Columns, Row) -> + Permission = lists:nth(index(<<"permission">>, Columns), Row), + Action = lists:nth(index(<<"action">>, Columns), Row), + Topic = lists:nth(index(<<"topic">>, Columns), Row), + {Permission, all, Action, [Topic]}. + +index(Elem, List) -> + index(Elem, List, 1). +index(_Elem, [], _Index) -> {error, not_found}; +index(Elem, [ Elem | _List], Index) -> Index; +index(Elem, [ _ | List], Index) -> index(Elem, List, Index + 1). 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 f3e793763..d9555b85d 100644 --- a/apps/emqx_authz/src/emqx_authz_pgsql.erl +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -66,39 +66,25 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> nomatch; do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> - case match(Client, PubSub, Topic, format_result(Columns, Row)) of + case emqx_authz_rule:match(Client, PubSub, Topic, + emqx_authz_rule:compile(format_result(Columns, Row)) + ) of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) end. format_result(Columns, Row) -> - L = [ begin - {column, K, _, _, _, _, _, _, _} = lists:nth(I, Columns), - V = lists:nth(I, tuple_to_list(Row)), - {K, V} - end || I <- lists:seq(1, length(Columns)) ], - maps:from_list(L). + Permission = lists:nth(index(<<"permission">>, 2, Columns), erlang:tuple_to_list(Row)), + Action = lists:nth(index(<<"action">>, 2, Columns), erlang:tuple_to_list(Row)), + Topic = lists:nth(index(<<"topic">>, 2, Columns), erlang:tuple_to_list(Row)), + {Permission, all, Action, [Topic]}. -match(Client, PubSub, Topic, - #{<<"permission">> := Permission, - <<"action">> := Action, - <<"topic">> := TopicFilter - }) -> - Rule = #{<<"topics">> => [TopicFilter], - <<"action">> => Action, - <<"permission">> => Permission - }, - #{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:init_rule(NRule)) of - true -> {matched, NPermission}; - false -> nomatch - end. +index(Key, N, TupleList) when is_integer(N) -> + Tuple = lists:keyfind(Key, N, TupleList), + index(Tuple, TupleList, 1); +index(_Tuple, [], _Index) -> {error, not_found}; +index(Tuple, [Tuple | _TupleList], Index) -> Index; +index(Tuple, [_ | TupleList], Index) -> index(Tuple, TupleList, Index + 1). replvar(Params, ClientInfo) -> replvar(Params, ClientInfo, []). diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 8f6731fd8..3ac7d7e3f 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -50,35 +50,13 @@ authorize(Client, PubSub, Topic, do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> - case match(Client, PubSub, Topic, - #{topics => TopicFilter, - action => Action - }) - of + case emqx_authz_rule:match(Client, PubSub, Topic, + emqx_authz_rule:compile({allow, all, Action, [TopicFilter]}) + )of {matched, Permission} -> {matched, Permission}; nomatch -> do_authorize(Client, PubSub, Topic, Tail) end. -match(Client, PubSub, Topic, - #{topics := TopicFilter, - action := Action - }) -> - Rule = #{<<"principal">> => all, - <<"topics">> => [TopicFilter], - <<"action">> => Action, - <<"permission">> => allow - }, - #{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:init_rule(NRule)) of - true -> {matched, allow}; - false -> nomatch - end. - replvar(Cmd, Client = #{cn := CN}) -> replvar(repl(Cmd, "%C", CN), maps:remove(cn, Client)); replvar(Cmd, Client = #{dn := DN}) -> diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl index 8fd7b9721..4786bf39d 100644 --- a/apps/emqx_authz/src/emqx_authz_rule.erl +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -26,15 +26,14 @@ %% APIs -export([ match/4 + , matches/4 , compile/1 ]). -export_type([rule/0]). compile({Permission, Who, Action, TopicFilters}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) -> - {Permission, compile_who(Who), Action, [compile_topic(Topic) || Topic <- TopicFilters]}; -compile({Permission, Who, Action, Topic}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action) -> - {Permission, compile_who(Who), Action, [compile_topic(Topic)]}. + {atom(Permission), compile_who(Who), atom(Action), [compile_topic(Topic) || Topic <- TopicFilters]}. compile_who(all) -> all; compile_who({username, Username}) -> @@ -52,6 +51,8 @@ compile_who({'and', L}) when is_list(L) -> compile_who({'or', L}) when is_list(L) -> {'or', [compile_who(Who) || Who <- L]}. +compile_topic(<<"eq ", Topic/binary>>) -> + {eq, emqx_topic:words(Topic)}; compile_topic({eq, Topic}) -> {eq, emqx_topic:words(bin(Topic))}; compile_topic(Topic) -> @@ -64,11 +65,32 @@ compile_topic(Topic) -> pattern(Words) -> lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words). +atom(B) when is_binary(B) -> + try binary_to_existing_atom(B, utf8) + catch + _ -> binary_to_atom(B) + end; +atom(L) when is_list(L) -> + try list_to_existing_atom(L) + catch + _ -> list_to_atom(L) + end; +atom(A) when is_atom(A) -> A. + bin(L) when is_list(L) -> list_to_binary(L); bin(B) when is_binary(B) -> B. +-spec(matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) + -> {matched, allow} | {matched, deny} | nomatch). +matches(Client, PubSub, Topic, []) -> nomatch; +matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) -> + case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of + nomatch -> matches(Client, PubSub, Topic, Tail); + Matched -> Matched + end. + -spec(match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) -> diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 958ad9dec..a09046572 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -23,8 +23,7 @@ fields("authorization_rules") -> [ {rules, rules()} ]; fields(file) -> - [ {principal, principal()} - , {type, #{type => http}} + [ {type, #{type => http}} , {enable, #{type => boolean(), default => true}} , {path, #{type => string(), @@ -36,8 +35,7 @@ fields(file) -> }} ]; fields(http) -> - [ {principal, principal()} - , {type, #{type => http}} + [ {type, #{type => http}} , {enable, #{type => boolean(), default => true}} , {config, #{type => hoconsc:union([ hoconsc:ref(?MODULE, http_get) @@ -113,16 +111,6 @@ fields(mysql) -> fields(pgsql) -> connector_fields(pgsql) ++ [ {sql, query()} ]; -fields(simple_rule) -> - [ {permission, #{type => permission()}} - , {action, #{type => action()}} - , {topics, #{type => union_array( - [ binary() - , hoconsc:ref(?MODULE, eq_topic) - ] - )}} - , {principal, principal()} - ]; fields(username) -> [{username, #{type => binary()}}]; fields(clientid) -> @@ -160,8 +148,7 @@ union_array(Item) when is_list(Item) -> rules() -> #{type => union_array( - [ hoconsc:ref(?MODULE, simple_rule) - , hoconsc:ref(?MODULE, file) + [ hoconsc:ref(?MODULE, file) , hoconsc:ref(?MODULE, http) , hoconsc:ref(?MODULE, mysql) , hoconsc:ref(?MODULE, pgsql) @@ -170,18 +157,6 @@ rules() -> ]) }. -principal() -> - #{default => all, - type => hoconsc:union( - [ all - , hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - , hoconsc:ref(?MODULE, andlist) - , hoconsc:ref(?MODULE, orlist) - ]) - }. - query() -> #{type => binary(), validator => fun(S) -> @@ -202,8 +177,7 @@ connector_fields(DB) -> Error -> erlang:error(Error) end, - [ {principal, principal()} - , {type, #{type => DB}} + [ {type, #{type => DB}} , {enable, #{type => boolean(), default => true}} ] ++ Mod:fields(""). diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index bcc855a59..30848f3d1 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -31,6 +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), + meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([authorization, cache, enable], false), @@ -39,43 +43,63 @@ init_per_suite(Config) -> end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), - emqx_ct_helpers:stop_apps([emqx_authz]), + emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), + meck:unload(emqx_resource), ok. init_per_testcase(_, Config) -> {ok, _} = emqx_authz:update(replace, []), Config. --define(RULE1, #{<<"principal">> => <<"all">>, - <<"topics">> => [<<"#">>], - <<"action">> => <<"all">>, - <<"permission">> => <<"deny">>} - ). --define(RULE2, #{<<"principal">> => - #{<<"ipaddress">> => <<"127.0.0.1">>}, - <<"topics">> => - [#{<<"eq">> => <<"#">>}, - #{<<"eq">> => <<"+">>} - ] , - <<"action">> => <<"all">>, - <<"permission">> => <<"allow">>} - ). --define(RULE3,#{<<"principal">> => - #{<<"and">> => [#{<<"username">> => <<"^test?">>}, - #{<<"clientid">> => <<"^test?">>} - ]}, - <<"topics">> => [<<"test">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"allow">>} - ). --define(RULE4,#{<<"principal">> => - #{<<"or">> => [#{<<"username">> => <<"^test">>}, - #{<<"clientid">> => <<"test?">>} - ]}, - <<"topics">> => [<<"%u">>,<<"%c">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"deny">>} - ). +-define(RULE1, #{<<"type">> => <<"http">>, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000} + }). +-define(RULE2, #{<<"type">> => <<"mongo">>, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). +-define(RULE3, #{<<"type">> => <<"mysql">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). +-define(RULE4, #{<<"type">> => <<"pgsql">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). +-define(RULE5, #{<<"type">> => <<"redis">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). %%------------------------------------------------------------------------------ %% Testcases @@ -86,73 +110,50 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update(head, [?RULE1]), {ok, _} = emqx_authz:update(tail, [?RULE3]), + dbg:tracer(),dbg:p(all,c), + dbg:tpl(hocon_schema, check, cx), Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), ?assertMatch(Lists1, emqx:get_config([authorization_rules, rules], [])), - [#{annotations := #{id := Id1, - principal := all, - topics := [['#']]} - }, - #{annotations := #{id := Id2, - principal := #{ipaddress := {{127,0,0,1},{127,0,0,1},32}}, - topics := [#{eq := ['#']}, #{eq := ['+']}]} - }, - #{annotations := #{id := Id3, - principal := - #{'and' := [#{username := {re_pattern, _, _, _, _}}, - #{clientid := {re_pattern, _, _, _, _}} - ] - }, - topics := [[<<"test">>]]} - } + [#{annotations := #{id := Id1}, type := http}, + #{annotations := #{id := Id2}, type := mongo}, + #{annotations := #{id := Id3}, type := mysql} ] = emqx_authz:lookup(), + {ok, _} = emqx_authz:update({replace_once, Id1}, ?RULE5), {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), ?assertMatch(Lists2, emqx:get_config([authorization_rules, rules], [])), - [#{annotations := #{id := Id1, - principal := all, - topics := [['#']]} - }, - #{annotations := #{id := Id2, - principal := #{ipaddress := {{127,0,0,1},{127,0,0,1},32}}, - topics := [#{eq := ['#']}, - #{eq := ['+']}]} - }, - #{annotations := #{id := Id3, - principal := - #{'or' := [#{username := {re_pattern, _, _, _, _}}, - #{clientid := {re_pattern, _, _, _, _}} - ] - }, - topics := [#{pattern := [<<"%u">>]}, - #{pattern := [<<"%c">>]} - ]} - } + [#{annotations := #{id := Id1}, type := redis}, + #{annotations := #{id := Id2}, type := mongo}, + #{annotations := #{id := Id3}, type := pgsql} ] = emqx_authz:lookup(), {ok, _} = emqx_authz:update(replace, []). t_move_rule(_) -> - {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4, ?RULE5]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}} + #{annotations := #{id := Id4}}, + #{annotations := #{id := Id5}} ] = emqx_authz:lookup(), {ok, _} = emqx_authz:move(Id4, <<"top">>), ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}} + #{annotations := #{id := Id3}}, + #{annotations := #{id := Id5}} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(Id1, <<"bottom">>), ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, + #{annotations := #{id := Id5}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), @@ -160,66 +161,15 @@ t_move_rule(_) -> ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, + #{annotations := #{id := Id5}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}), ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, + #{annotations := #{id := Id5}}, #{annotations := #{id := Id1}}, #{annotations := #{id := Id2}} ], emqx_authz:lookup()), ok. - -t_authz(_) -> - ClientInfo1 = #{clientid => <<"test">>, - username => <<"test">>, - peerhost => {127,0,0,1}, - zone => default, - listener => mqtt_tcp - }, - ClientInfo2 = #{clientid => <<"test">>, - username => <<"test">>, - peerhost => {192,168,0,10}, - zone => default, - listener => mqtt_tcp - }, - ClientInfo3 = #{clientid => <<"test">>, - username => <<"fake">>, - peerhost => {127,0,0,1}, - zone => default, - listener => mqtt_tcp - }, - ClientInfo4 = #{clientid => <<"fake">>, - username => <<"test">>, - peerhost => {127,0,0,1}, - zone => default, - listener => mqtt_tcp - }, - - Rules1 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE1, ?RULE2])], - Rules2 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE2, ?RULE1])], - Rules3 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE3, ?RULE4])], - Rules4 = [emqx_authz:init_rule(Rule) || Rule <- emqx_authz:check_rules([?RULE4, ?RULE1])], - - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)), - ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)), - ?assertEqual({stop, allow}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)), - ?assertEqual({stop, deny}, - emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)), - ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 9b6153465..77d620ccb 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -37,46 +37,100 @@ -define(API_VERSION, "v5"). -define(BASE_PATH, "api"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +% -define(RULE1, #{<<"principal">> => <<"all">>, +% <<"topics">> => [<<"#">>], +% <<"action">> => <<"all">>, +% <<"permission">> => <<"deny">>} +% ). +% -define(RULE2, #{<<"principal">> => +% #{<<"ipaddress">> => <<"127.0.0.1">>}, +% <<"topics">> => +% [#{<<"eq">> => <<"#">>}, +% #{<<"eq">> => <<"+">>} +% ] , +% <<"action">> => <<"all">>, +% <<"permission">> => <<"allow">>} +% ). +% -define(RULE3,#{<<"principal">> => +% #{<<"and">> => [#{<<"username">> => <<"^test?">>}, +% #{<<"clientid">> => <<"^test?">>} +% ]}, +% <<"topics">> => [<<"test">>], +% <<"action">> => <<"publish">>, +% <<"permission">> => <<"allow">>} +% ). +% -define(RULE4,#{<<"principal">> => +% #{<<"or">> => [#{<<"username">> => <<"^test">>}, +% #{<<"clientid">> => <<"test?">>} +% ]}, +% <<"topics">> => [<<"%u">>,<<"%c">>], +% <<"action">> => <<"publish">>, +% <<"permission">> => <<"deny">>} +% ). --define(RULE1, #{<<"principal">> => <<"all">>, - <<"topics">> => [<<"#">>], - <<"action">> => <<"all">>, - <<"permission">> => <<"deny">>} - ). --define(RULE2, #{<<"principal">> => - #{<<"ipaddress">> => <<"127.0.0.1">>}, - <<"topics">> => - [#{<<"eq">> => <<"#">>}, - #{<<"eq">> => <<"+">>} - ] , - <<"action">> => <<"all">>, - <<"permission">> => <<"allow">>} - ). --define(RULE3,#{<<"principal">> => - #{<<"and">> => [#{<<"username">> => <<"^test?">>}, - #{<<"clientid">> => <<"^test?">>} - ]}, - <<"topics">> => [<<"test">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"allow">>} - ). --define(RULE4,#{<<"principal">> => - #{<<"or">> => [#{<<"username">> => <<"^test">>}, - #{<<"clientid">> => <<"test?">>} - ]}, - <<"topics">> => [<<"%u">>,<<"%c">>], - <<"action">> => <<"publish">>, - <<"permission">> => <<"deny">>} - ). +-define(RULE1, #{<<"type">> => <<"http">>, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000} + }). +-define(RULE2, #{<<"type">> => <<"mongo">>, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). +-define(RULE3, #{<<"type">> => <<"mysql">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). +-define(RULE4, #{<<"type">> => <<"pgsql">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). +-define(RULE5, #{<<"type">> => <<"redis">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). all() -> - emqx_ct:all(?MODULE). + % emqx_ct:all(?MODULE). + []. 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), + meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, remove, fun(_) -> ok end ), + ekka_mnesia:start(), emqx_mgmt_auth:mnesia(boot), @@ -89,7 +143,8 @@ init_per_suite(Config) -> end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), - emqx_ct_helpers:stop_apps([emqx_authz, emqx_management]), + emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_management]), + meck:unload(emqx_resource), ok. set_special_configs(emqx_management) -> @@ -111,12 +166,7 @@ t_api(_) -> ?assertEqual([], get_rules(Result1)), lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization"]), - #{<<"action">> => <<"all">>, - <<"permission">> => <<"deny">>, - <<"principal">> => <<"all">>, - <<"topics">> => [<<"#">>]} - ) + {ok, 204, _} = request(post, uri(["authorization"]), ?RULE1) end, lists:seq(1, 20)), {ok, 200, Result2} = request(get, uri(["authorization"]), []), ?assertEqual(20, length(get_rules(Result2))), @@ -128,30 +178,23 @@ t_api(_) -> ?assertEqual(10, length(get_rules(Result))) end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization"]), - [ #{<<"action">> => <<"all">>, <<"permission">> => <<"allow">>, <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]} - , #{<<"action">> => <<"all">>, <<"permission">> => <<"allow">>, <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]} - , #{<<"action">> => <<"all">>, <<"permission">> => <<"allow">>, <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]} - ]), + {ok, 204, _} = request(put, uri(["authorization"]), [?RULE1, ?RULE2, ?RULE3, ?RULE4]), {ok, 200, Result3} = request(get, uri(["authorization"]), []), Rules = get_rules(Result3), - ?assertEqual(3, length(Rules)), - - lists:foreach(fun(#{<<"permission">> := Allow}) -> - ?assertEqual(<<"allow">>, Allow) - end, Rules), + ?assertEqual(4, length(Rules)), + ?assertMatch([ #{<<"type">> := <<"http">>} + , #{<<"type">> := <<"mongo">>} + , #{<<"type">> := <<"mysql">>} + , #{<<"type">> := <<"pgsql">>} + ], Rules), #{<<"annotations">> := #{<<"id">> := Id}} = lists:nth(2, Rules), - {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), - #{<<"action">> => <<"all">>, <<"permission">> => <<"deny">>, - <<"principal">> => <<"all">>, <<"topics">> => [<<"#">>]}), + {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), ?RULE5), {ok, 200, Result4} = request(get, uri(["authorization", binary_to_list(Id)]), []), - ?assertMatch(#{<<"annotations">> := #{<<"id">> := Id}, - <<"permission">> := <<"deny">> - }, jsx:decode(Result4)), + ?assertMatch(#{<<"type">> := <<"redis">>}, jsx:decode(Result4)), lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id0}}) -> {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id0)]), []) @@ -161,11 +204,12 @@ t_api(_) -> ok. t_move_rule(_) -> - {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4, ?RULE5]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}} + #{annotations := #{id := Id4}}, + #{annotations := #{id := Id5}} ] = emqx_authz:lookup(), {ok, 204, _} = request(post, uri(["authorization", Id4, "move"]), @@ -173,7 +217,8 @@ t_move_rule(_) -> ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}} + #{annotations := #{id := Id3}}, + #{annotations := #{id := Id5}} ], emqx_authz:lookup()), {ok, 204, _} = request(post, uri(["authorization", Id1, "move"]), @@ -181,6 +226,7 @@ t_move_rule(_) -> ?assertMatch([#{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, + #{annotations := #{id := Id5}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), @@ -189,6 +235,7 @@ t_move_rule(_) -> ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, #{annotations := #{id := Id2}}, + #{annotations := #{id := Id5}}, #{annotations := #{id := Id1}} ], emqx_authz:lookup()), @@ -196,6 +243,7 @@ t_move_rule(_) -> #{<<"position">> => #{<<"after">> => Id1}}), ?assertMatch([#{annotations := #{id := Id3}}, #{annotations := #{id := Id4}}, + #{annotations := #{id := Id5}}, #{annotations := #{id := Id1}}, #{annotations := #{id := Id2}} ], emqx_authz:lookup()), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index fb95c1b00..c7a3fc449 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -41,14 +41,13 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), - Rules = [#{ <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000 - }, - <<"principal">> => <<"all">>, - <<"type">> => <<"http">>} + Rules = [#{<<"type">> => <<"http">>, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 + }} ], {ok, _} = emqx_authz:update(replace, Rules), Config. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index cffc0ad76..dac106b37 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -39,17 +39,16 @@ init_per_suite(Config) -> ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), - Rules = [#{ <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, - <<"principal">> => <<"all">>, - <<"collection">> => <<"fake">>, - <<"find">> => #{<<"a">> => <<"b">>}, - <<"type">> => <<"mongo">>} - ], + Rules = [#{<<"type">> => <<"mongo">>, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }], {ok, _} = emqx_authz:update(replace, Rules), Config. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index dcc0e47d7..0fba033a6 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -40,18 +40,17 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), - Rules = [#{ <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"principal">> => <<"all">>, - <<"sql">> => <<"abcb">>, - <<"type">> => <<"mysql">> }], + Rules = [#{<<"type">> => <<"mysql">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }], {ok, _} = emqx_authz:update(replace, Rules), Config. @@ -60,17 +59,14 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource). --define(COLUMNS, [ <<"ipaddress">> - , <<"username">> - , <<"clientid">> - , <<"action">> +-define(COLUMNS, [ <<"action">> , <<"permission">> , <<"topic">> ]). --define(RULE1, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>]]). --define(RULE2, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>]]). --define(RULE3, [[<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>]]). --define(RULE4, [[<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>]]). +-define(RULE1, [[<<"all">>, <<"deny">>, <<"#">>]]). +-define(RULE2, [[<<"all">>, <<"allow">>, <<"eq #">>]]). +-define(RULE3, [[<<"subscribe">>, <<"allow">>, <<"test/%c">>]]). +-define(RULE4, [[<<"publish">>, <<"allow">>, <<"test/%u">>]]). %%------------------------------------------------------------------------------ %% Testcases diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index b4383e21e..d21caa223 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -40,17 +40,17 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), - Rules = [#{ <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"sql">> => <<"abcb">>, - <<"type">> => <<"pgsql">> }], + Rules = [#{<<"type">> => <<"pgsql">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }], {ok, _} = emqx_authz:update(replace, Rules), Config. @@ -59,17 +59,14 @@ end_per_suite(_Config) -> 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} - , {column, <<"clientid">>, meck, meck, meck, meck, meck, meck, meck} - , {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck} +-define(COLUMNS, [ {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"permission">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"topic">>, meck, meck, meck, meck, meck, meck, meck} ]). --define(RULE1, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>}]). --define(RULE2, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>}]). --define(RULE3, [{<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>}]). --define(RULE4, [{<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>}]). +-define(RULE1, [{<<"all">>, <<"deny">>, <<"#">>}]). +-define(RULE2, [{<<"all">>, <<"allow">>, <<"eq #">>}]). +-define(RULE3, [{<<"subscribe">>, <<"allow">>, <<"test/%c">>}]). +-define(RULE4, [{<<"publish">>, <<"allow">>, <<"test/%u">>}]). %%------------------------------------------------------------------------------ %% Testcases diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index d3eebeb2e..073c339ae 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -41,16 +41,16 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), - Rules = [#{ <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, - <<"cmd">> => <<"HGETALL mqtt_authz:%u">>, - <<"type">> => <<"redis">> }], + Rules = [#{<<"type">> => <<"redis">>, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }], {ok, _} = emqx_authz:update(replace, Rules), Config. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 36ea01db2..6f69fafc4 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -19,8 +19,9 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --type server() :: string(). +-type server() :: emqx_schema:ip_port(). -reflect_type([server/0]). +-typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -95,7 +96,7 @@ on_start(InstId, Config = #{server := Server, mongo_type := single}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, single}, - {hosts, [Server]} + {hosts, [emqx_connector_schema_lib:ip_port_to_string(Server)]} ], do_start(InstId, Opts, Config); @@ -104,14 +105,17 @@ on_start(InstId, Config = #{servers := Servers, replica_set_name := RsName}) -> logger:info("starting mongodb connector: ~p, config: ~p", [InstId, Config]), Opts = [{type, {rs, RsName}}, - {hosts, Servers}], + {hosts, [emqx_connector_schema_lib:ip_port_to_string(S) + || S <- Servers]} + ], do_start(InstId, Opts, 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} + {hosts, [emqx_connector_schema_lib:ip_port_to_string(S) + || S <- Servers]} ], do_start(InstId, Opts, Config). diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 1ea31ced8..60087188f 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -19,10 +19,9 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --type server() :: tuple(). +-type server() :: emqx_schema:ip_port(). -reflect_type([server/0]). --typerefl_from_string({server/0, ?MODULE, to_server}). --export([to_server/1]). +-typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}). -export([structs/0, fields/1]). @@ -170,9 +169,3 @@ redis_fields() -> default => 0}} , {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} ]. - -to_server(Server) -> - case string:tokens(Server, ":") of - [Host, Port] -> {ok, {Host, list_to_integer(Port)}}; - _ -> {error, Server} - end. From 7e8dde7e26741f4fb2b42d99c43422bb47f8c652 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Tue, 24 Aug 2021 15:56:17 +0800 Subject: [PATCH 114/306] chore: fix dialyzer error Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 5 ++- apps/emqx_authz/src/emqx_authz_rule.erl | 7 +--- apps/emqx_authz/test/emqx_authz_SUITE.erl | 9 ++--- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 35 ++----------------- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 2 -- .../test/emqx_authz_redis_SUITE.erl | 2 -- .../src/emqx_connector_mongo.erl | 4 ++- 7 files changed, 11 insertions(+), 53 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 3c8a56629..f158322e1 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -233,7 +233,6 @@ create_resource(#{type := DB, {error, Reason} -> {error, Reason} end. --spec(init_provider(rule()) -> rule()). init_provider(#{enable := true, type := file, path := Path @@ -301,7 +300,7 @@ init_provider(#{enable := false} = Rule) ->Rule. -> {stop, allow} | {ok, deny}). authorize(#{username := Username, peerhost := IpAddress - } = Client, PubSub, Topic, _DefaultResult, Rules) -> + } = 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]), @@ -313,7 +312,7 @@ authorize(#{username := Username, {stop, deny}; nomatch -> ?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]), - {stop, deny} + {stop, DefaultResult} end. do_authorize(Client, PubSub, Topic, diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl index 4786bf39d..deb8968c6 100644 --- a/apps/emqx_authz/src/emqx_authz_rule.erl +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -70,11 +70,6 @@ atom(B) when is_binary(B) -> catch _ -> binary_to_atom(B) end; -atom(L) when is_list(L) -> - try list_to_existing_atom(L) - catch - _ -> list_to_atom(L) - end; atom(A) when is_atom(A) -> A. bin(L) when is_list(L) -> @@ -84,7 +79,7 @@ bin(B) when is_binary(B) -> -spec(matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) -> {matched, allow} | {matched, deny} | nomatch). -matches(Client, PubSub, Topic, []) -> nomatch; +matches(_Client, _PubSub, _Topic, []) -> nomatch; matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) -> case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of nomatch -> matches(Client, PubSub, Topic, Tail); diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 30848f3d1..a766ecd82 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -33,6 +33,7 @@ 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), + meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), @@ -110,10 +111,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update(head, [?RULE1]), {ok, _} = emqx_authz:update(tail, [?RULE3]), - dbg:tracer(),dbg:p(all,c), - dbg:tpl(hocon_schema, check, cx), - Lists1 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE3]), - ?assertMatch(Lists1, emqx:get_config([authorization_rules, rules], [])), + ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization_rules, rules], [])), [#{annotations := #{id := Id1}, type := http}, #{annotations := #{id := Id2}, type := mongo}, @@ -122,8 +120,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update({replace_once, Id1}, ?RULE5), {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), - Lists2 = emqx_authz:check_rules([?RULE1, ?RULE2, ?RULE4]), - ?assertMatch(Lists2, emqx:get_config([authorization_rules, rules], [])), + ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization_rules, rules], [])), [#{annotations := #{id := Id1}, type := redis}, #{annotations := #{id := Id2}, type := mongo}, diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 77d620ccb..56d0170c1 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -37,37 +37,6 @@ -define(API_VERSION, "v5"). -define(BASE_PATH, "api"). -% -define(RULE1, #{<<"principal">> => <<"all">>, -% <<"topics">> => [<<"#">>], -% <<"action">> => <<"all">>, -% <<"permission">> => <<"deny">>} -% ). -% -define(RULE2, #{<<"principal">> => -% #{<<"ipaddress">> => <<"127.0.0.1">>}, -% <<"topics">> => -% [#{<<"eq">> => <<"#">>}, -% #{<<"eq">> => <<"+">>} -% ] , -% <<"action">> => <<"all">>, -% <<"permission">> => <<"allow">>} -% ). -% -define(RULE3,#{<<"principal">> => -% #{<<"and">> => [#{<<"username">> => <<"^test?">>}, -% #{<<"clientid">> => <<"^test?">>} -% ]}, -% <<"topics">> => [<<"test">>], -% <<"action">> => <<"publish">>, -% <<"permission">> => <<"allow">>} -% ). -% -define(RULE4,#{<<"principal">> => -% #{<<"or">> => [#{<<"username">> => <<"^test">>}, -% #{<<"clientid">> => <<"test?">>} -% ]}, -% <<"topics">> => [<<"%u">>,<<"%c">>], -% <<"action">> => <<"publish">>, -% <<"permission">> => <<"deny">>} -% ). - -define(RULE1, #{<<"type">> => <<"http">>, <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, @@ -119,8 +88,7 @@ }). all() -> - % emqx_ct:all(?MODULE). - []. + emqx_ct:all(?MODULE). groups() -> []. @@ -129,6 +97,7 @@ 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), meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), + meck:expect(emqx_resource, health_check, fun(_) -> ok end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), ekka_mnesia:start(), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index c7a3fc449..bbd4232dd 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -21,8 +21,6 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). - -define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). all() -> diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 073c339ae..0da931cc7 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -21,8 +21,6 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). - -define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). all() -> diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 6f69fafc4..c4953c3fb 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -84,7 +84,9 @@ mongo_fields() -> nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} , {topology, #{type => hoconsc:ref(?MODULE, topology), - nullable => true}} + default => #{}}} + %% TODO: Does the ref type support nullable=ture ? + % nullable => true}} ] ++ emqx_connector_schema_lib:ssl_fields(). From aa6f1ac88d13168518fdd8f4309dd1b1d9524495 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 23 Aug 2021 09:44:41 +0800 Subject: [PATCH 115/306] feat: auto subscribe, todo: test SUITE --- apps/emqx_auto_subscribe/.gitignore | 19 ++ apps/emqx_auto_subscribe/LICENSE | 191 ++++++++++++++++++ apps/emqx_auto_subscribe/README.md | 9 + .../etc/emqx_auto_subscribe.conf | 29 +++ .../include/emqx_auto_subscribe.hrl | 0 apps/emqx_auto_subscribe/rebar.config | 6 + .../src/emqx_auto_subscribe.app.src | 15 ++ .../src/emqx_auto_subscribe.erl | 80 ++++++++ .../src/emqx_auto_subscribe_api.erl | 64 ++++++ .../src/emqx_auto_subscribe_app.erl | 30 +++ .../src/emqx_auto_subscribe_placeholder.erl | 72 +++++++ .../src/emqx_auto_subscribe_schema.erl | 34 ++++ .../src/emqx_auto_subscribe_sup.erl | 37 ++++ .../emqx_auto_subscribe_handler.erl | 29 +++ .../emqx_auto_subscribe_internal.erl | 29 +++ apps/emqx_machine/src/emqx_machine_schema.erl | 1 + rebar.config.erl | 1 + 17 files changed, 646 insertions(+) create mode 100644 apps/emqx_auto_subscribe/.gitignore create mode 100644 apps/emqx_auto_subscribe/LICENSE create mode 100644 apps/emqx_auto_subscribe/README.md create mode 100644 apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf create mode 100644 apps/emqx_auto_subscribe/include/emqx_auto_subscribe.hrl create mode 100644 apps/emqx_auto_subscribe/rebar.config create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl create mode 100644 apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl create mode 100644 apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.erl create mode 100644 apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.erl diff --git a/apps/emqx_auto_subscribe/.gitignore b/apps/emqx_auto_subscribe/.gitignore new file mode 100644 index 000000000..f1c455451 --- /dev/null +++ b/apps/emqx_auto_subscribe/.gitignore @@ -0,0 +1,19 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +*.iml +rebar3.crashdump +*~ diff --git a/apps/emqx_auto_subscribe/LICENSE b/apps/emqx_auto_subscribe/LICENSE new file mode 100644 index 000000000..e16434416 --- /dev/null +++ b/apps/emqx_auto_subscribe/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, DDDHuang <904897578@qq.com>. + + 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_auto_subscribe/README.md b/apps/emqx_auto_subscribe/README.md new file mode 100644 index 000000000..96d368715 --- /dev/null +++ b/apps/emqx_auto_subscribe/README.md @@ -0,0 +1,9 @@ +emqx_auto_subscribe +===== + +An OTP application + +Build +----- + + $ rebar3 compile diff --git a/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf new file mode 100644 index 000000000..c91b77aa1 --- /dev/null +++ b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf @@ -0,0 +1,29 @@ + +auto_subscribe { + topics = [ + # { + # topic = "/c/${clientid}", + # qos = 0 + # }, + # { + # topic = "/u/${username}", + # qos = 1 + # }, + # { + # topic = "/h/${host}", + # qos = 2 + # }, + # { + # topic = "/p/${port}", + # qos = 0 + # }, + # { + # topic = "/topic/abc", + # qos = 0 + # }, + # { + # topic = "/client/${clientid}/username/${username}/host/${host}/port/${port}", + # qos = 0 + # } + ] +} diff --git a/apps/emqx_auto_subscribe/include/emqx_auto_subscribe.hrl b/apps/emqx_auto_subscribe/include/emqx_auto_subscribe.hrl new file mode 100644 index 000000000..e69de29bb diff --git a/apps/emqx_auto_subscribe/rebar.config b/apps/emqx_auto_subscribe/rebar.config new file mode 100644 index 000000000..88793f7ba --- /dev/null +++ b/apps/emqx_auto_subscribe/rebar.config @@ -0,0 +1,6 @@ +{erl_opts, [debug_info]}. +{deps, []}. + +{shell, [ + {apps, [emqx_auto_subscribe]} +]}. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src new file mode 100644 index 000000000..0d87af87a --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src @@ -0,0 +1,15 @@ +{application, emqx_auto_subscribe, + [{description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {emqx_auto_subscribe_app, []}}, + {applications, + [kernel, + stdlib + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl new file mode 100644 index 000000000..b13b3760a --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl @@ -0,0 +1,80 @@ +%%-------------------------------------------------------------------- +%% 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_auto_subscribe). + +-define(HOOK_POINT, 'client.connected'). + +-define(MAX_AUTO_SUBSCRIBE, 20). + +-export([load/0]). + +-export([ max_limit/0 + , list/0 + , update/1 + , test/1 + ]). + +%% hook callback +-export([on_client_connected/3]). + +load() -> + update_hook(). + +max_limit() -> + ?MAX_AUTO_SUBSCRIBE. + +list() -> + emqx:get_config([auto_subscribe, topics], []). + +update(Topics) -> + update_(Topics). + +test(_) -> +%% TODO: test rule with info map + ok. + +% test(Topic) when is_map(Topic) -> +% test([Topic]); + +% test(Topics) when is_list(Topics) -> +% PlaceHolders = emqx_auto_subscribe_placeholder:generate(Topics), +% ClientInfo = #{}, +% ConnInfo = #{}, +% emqx_auto_subscribe_placeholder:to_topic_table([PlaceHolders], ClientInfo, ConnInfo). + +%%-------------------------------------------------------------------- +%% hook + +on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) -> + TopicTables = erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]), + self() ! {subscribe, TopicTables}; +on_client_connected(_, _, _) -> + ok. + +%%-------------------------------------------------------------------- +%% internal + +update_(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE -> + {ok, _} = emqx:update_config([auto_subscribe, topics], Topics), + update_hook(); +update_(_Topics) -> + {error, quota_exceeded}. + +update_hook() -> + {TopicHandler, Options} = emqx_auto_subscribe_handler:init(), + emqx_hooks:put(?HOOK_POINT, {?MODULE, on_client_connected, [{TopicHandler, Options}]}), + ok. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl new file mode 100644 index 000000000..d55444dba --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -0,0 +1,64 @@ +%%-------------------------------------------------------------------- +%% 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_auto_subscribe_api). + +-behaviour(minirest_api). + +-export([api_spec/0]). + +-export([auto_subscribe/2]). + +-define(EXCEED_LIMIT, 'EXCEED_LIMIT'). + +api_spec() -> + {[auto_subscribe_api()], []}. + +schema() -> + emqx_mgmt_util:schema( + emqx_mgmt_api_configs:gen_schema( + emqx:get_raw_config([auto_subscribe, topics]))). + +auto_subscribe_api() -> + Metadata = #{ + get => #{ + description => <<"Auto subscribe list">>, + responses => #{ + <<"200">> => schema()}}, + put => #{ + description => <<"Update auto subscribe topic list">>, + 'requestBody' => schema(), + responses => #{ + <<"200">> => schema(), + <<"409">> => emqx_mgmt_util:error_schema( + <<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}} + }, + {"/mqtt/auto_subscribe", Metadata, auto_subscribe}. + +%%%============================================================================================== +%% api apply +auto_subscribe(get, _) -> + {200, emqx_auto_subscribe:list()}; + +auto_subscribe(put, #{body := Params}) -> + case emqx_auto_subscribe:update(Params) of + {error, quota_exceeded} -> + Message = list_to_binary(io_lib:format("Max auto subscribe topic count is ~p", + [emqx_auto_subscribe:max_limit()])), + {409, #{code => ?EXCEED_LIMIT, message => Message}}; + ok -> + {200, emqx_auto_subscribe:list()} + end. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl new file mode 100644 index 000000000..116e35b1c --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl @@ -0,0 +1,30 @@ +%%-------------------------------------------------------------------- +%% 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_auto_subscribe_app). + +-behaviour(application). + +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + ok = emqx_auto_subscribe:load(), + emqx_auto_subscribe_sup:start_link(). + +stop(_State) -> + ok. + +%% internal functions diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl new file mode 100644 index 000000000..72b2509cc --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl @@ -0,0 +1,72 @@ +%%-------------------------------------------------------------------- +%% 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_auto_subscribe_placeholder). + +-export([generate/1]). + +-export([to_topic_table/3]). + +-spec(generate(list() | map()) -> list() | map()). +generate(Topics) when is_list(Topics) -> + [generate(Topic) || Topic <- Topics]; + +generate(#{qos := Qos, topic := Topic}) when is_binary(Topic) -> + #{qos => Qos, placeholder => generate(Topic, [])}. + +-spec(to_topic_table(list(), map(), map()) -> list()). +to_topic_table(PlaceHolders, ClientInfo, ConnInfo) -> + [begin + Topic0 = to_topic(PlaceHolder, ClientInfo, ConnInfo, []), + {Topic, Opts} = emqx_topic:parse(Topic0), + {Topic, Opts#{qos => Qos}} + end || #{qos := Qos, placeholder := PlaceHolder} <- PlaceHolders]. + +%%-------------------------------------------------------------------- +%% internal + +generate(<<"">>, Result) -> + lists:reverse(Result); +generate(<<"${clientid}", Tail/binary>>, Result) -> + generate(Tail, [clientid | Result]); +generate(<<"${username}", Tail/binary>>, Result) -> + generate(Tail, [username | Result]); +generate(<<"${host}", Tail/binary>>, Result) -> + generate(Tail, [host | Result]); +generate(<<"${port}", Tail/binary>>, Result) -> + generate(Tail, [port | Result]); +generate(<>, []) -> + generate(Tail, [<>]); +generate(<>, [R | Result]) when is_binary(R) -> + generate(Tail, [<> | Result]); +generate(<>, [R | Result]) when is_atom(R) -> + generate(Tail, [<> | [R | Result]]). + +to_topic([], _, _, Res) -> + list_to_binary(lists:reverse(Res)); +to_topic([Binary | PTs], C, Co, Res) when is_binary(Binary) -> + to_topic(PTs, C, Co, [Binary | Res]); +to_topic([clientid | PTs], C = #{clientid := ClientID}, Co, Res) -> + to_topic(PTs, C, Co, [ClientID | Res]); +to_topic([username | PTs], C = #{username := undefined}, Co, Res) -> + to_topic(PTs, C, Co, [<<"undefined">> | Res]); +to_topic([username | PTs], C = #{username := Username}, Co, Res) -> + to_topic(PTs, C, Co, [Username | Res]); +to_topic([host | PTs], C, Co = #{peername := {Host, _}}, Res) -> + HostBinary = list_to_binary(inet:ntoa(Host)), + to_topic(PTs, C, Co, [HostBinary | Res]); +to_topic([port | PTs], C, Co = #{peername := {_, Port}}, Res) -> + PortBinary = integer_to_binary(Port), + to_topic(PTs, C, Co, [PortBinary | Res]). diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl new file mode 100644 index 000000000..3a68b59ab --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -0,0 +1,34 @@ +%%-------------------------------------------------------------------- +%% 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_auto_subscribe_schema). + +-behaviour(hocon_schema). + +-include_lib("typerefl/include/types.hrl"). + +-export([ structs/0 + , fields/1]). + +structs() -> + ["auto_subscribe"]. + +fields("auto_subscribe") -> + [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))}]; + +fields("topic") -> + [ {topic, emqx_schema:t(binary(), undefined, <<"">>)} + , {qos, emqx_schema:t(integer(), undefined, 0)} + ]. diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl new file mode 100644 index 000000000..9e35f825f --- /dev/null +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_sup.erl @@ -0,0 +1,37 @@ +%%-------------------------------------------------------------------- +%% 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_auto_subscribe_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +init([]) -> + SupFlags = #{strategy => one_for_all, + intensity => 0, + period => 1}, + ChildSpecs = [], + {ok, {SupFlags, ChildSpecs}}. + +%% internal functions diff --git a/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.erl b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.erl new file mode 100644 index 000000000..11865153a --- /dev/null +++ b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_handler.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_auto_subscribe_handler). + +-export([init/0]). + +-spec(init() -> {Module :: atom(), Config :: term()}). +init() -> + do_init(emqx:get_config([auto_subscribe], #{})). + +do_init(Config = #{topics := _Topics}) -> + Options = emqx_auto_subscribe_internal:init(Config), + {emqx_auto_subscribe_internal, Options}; + +do_init(_) -> + erlang:error("only support in EMQ X Enterprise"). diff --git a/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.erl b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.erl new file mode 100644 index 000000000..f3cd58980 --- /dev/null +++ b/apps/emqx_auto_subscribe/src/topics_handler/emqx_auto_subscribe_internal.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_auto_subscribe_internal). + +-export([init/1]). + +-export([handle/3]). + +-spec(init(Config :: map()) -> HandlerOptions :: term()). +init(#{topics := Topics}) -> + emqx_auto_subscribe_placeholder:generate(Topics). + +-spec(handle(ClientInfo :: map(), ConnInfo :: map(), HandlerOptions :: term()) -> + TopicTables :: list()). +handle(ClientInfo, ConnInfo, PlaceHolders) -> + emqx_auto_subscribe_placeholder:to_topic_table(PlaceHolders, ClientInfo, ConnInfo). diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 0d1e9b176..38ed98898 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -48,6 +48,7 @@ , emqx_statsd_schema , emqx_authn_schema , emqx_authz_schema + , emqx_auto_subscribe_schema , emqx_bridge_mqtt_schema , emqx_modules_schema , emqx_management_schema diff --git a/rebar.config.erl b/rebar.config.erl index eee5d69b1..3a189dba0 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -267,6 +267,7 @@ relx_apps(ReleaseType) -> , emqx_connector , emqx_authn , emqx_authz + , emqx_auto_subscribe , emqx_gateway , emqx_exhook , emqx_data_bridge From b13ae50bed22e661b8da5a137b91d85dd38cf25c Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 25 Aug 2021 11:17:02 +0800 Subject: [PATCH 116/306] fix: start sup --- apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl index 116e35b1c..0be813bcd 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_app.erl @@ -21,8 +21,9 @@ -export([start/2, stop/1]). start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_auto_subscribe_sup:start_link(), ok = emqx_auto_subscribe:load(), - emqx_auto_subscribe_sup:start_link(). + {ok, Sup}. stop(_State) -> ok. From bafe5bae1cd0c7243ad1fb59709a45b2ba01ed15 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 25 Aug 2021 11:29:13 +0800 Subject: [PATCH 117/306] fix: placeholder & topic schema no default --- .../emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl | 2 +- apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl index 72b2509cc..cbe881bde 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl @@ -61,7 +61,7 @@ to_topic([Binary | PTs], C, Co, Res) when is_binary(Binary) -> to_topic([clientid | PTs], C = #{clientid := ClientID}, Co, Res) -> to_topic(PTs, C, Co, [ClientID | Res]); to_topic([username | PTs], C = #{username := undefined}, Co, Res) -> - to_topic(PTs, C, Co, [<<"undefined">> | Res]); + to_topic(PTs, C, Co, [<<"${username}">> | Res]); to_topic([username | PTs], C = #{username := Username}, Co, Res) -> to_topic(PTs, C, Co, [Username | Res]); to_topic([host | PTs], C, Co = #{peername := {Host, _}}, Res) -> diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl index 3a68b59ab..c3621f3a4 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -29,6 +29,6 @@ fields("auto_subscribe") -> [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))}]; fields("topic") -> - [ {topic, emqx_schema:t(binary(), undefined, <<"">>)} + [ {topic, emqx_schema:t(binary())} , {qos, emqx_schema:t(integer(), undefined, 0)} ]. From f184cd3d0e47b611a20b133fbc3e100f9c59939b Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 25 Aug 2021 11:38:00 +0800 Subject: [PATCH 118/306] fix: node parameters --- apps/emqx_management/src/emqx_mgmt_api_clients.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index d847d2b10..b006a4a49 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -505,7 +505,8 @@ list(Params) -> {200, Response}; Node1 -> Node = binary_to_atom(Node1, utf8), - Response = emqx_mgmt_api:node_query(Node, Params, ?CLIENT_QS_SCHEMA, ?query_fun), + ParamsWithoutNode = maps:without([<<"node">>], Params), + Response = emqx_mgmt_api:node_query(Node, ParamsWithoutNode, ?CLIENT_QS_SCHEMA, ?query_fun), {200, Response} end. From 78cf115a906347482c3b85b82ecf466b24f48ae9 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 25 Aug 2021 11:06:34 +0800 Subject: [PATCH 119/306] fix: swagger doc --- apps/emqx_management/src/emqx_mgmt_api_listeners.erl | 3 +-- apps/emqx_prometheus/src/emqx_prometheus_api.erl | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index bce6ef210..b085db597 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -25,7 +25,6 @@ , manage_listeners/2]). -import(emqx_mgmt_util, [ schema/1 - , schema/2 , object_schema/2 , object_array_schema/2 , error_schema/1 @@ -130,7 +129,7 @@ nodes_listeners_api() -> error_schema(<<"Node name or listener id not found">>, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - schema(properties(), <<"Get listener info ok">>)}}}, + object_schema(properties(), <<"Get listener info ok">>)}}}, {"/nodes/:node/listeners/:id", Metadata, listener}. nodes_listener_api() -> diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index ee2444bac..ff8cff158 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -52,11 +52,6 @@ prometheus_data_api() -> Metadata = #{ get => #{ description => <<"Get Prometheus Data">>, - parameters => [#{ - name => format_type, - in => path, - schema => #{type => string} - }], responses => #{<<"200">> => schema(#{type => object})} } }, From e4f5e9332e31c58f4ae77733a9b7a078cb16c63c Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 25 Aug 2021 12:53:53 +0800 Subject: [PATCH 120/306] feat: support array bridge_mqtt conf --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_config.erl | 5 +- .../etc/emqx_bridge_mqtt.conf | 106 +++++++++--------- .../src/emqx_bridge_mqtt_schema.erl | 8 +- apps/emqx_machine/src/emqx_machine_schema.erl | 3 +- rebar.config | 2 +- 6 files changed, 62 insertions(+), 64 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 19c91d6ac..60f257f30 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -15,7 +15,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.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.14.1"}}} diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 6441afe69..101abbd2b 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -299,7 +299,7 @@ save_schema_mod_and_names(SchemaMod) -> RootNames = SchemaMod:structs(), OldMods = get_schema_mod(), OldNames = get_root_names(), - NewMods = maps:from_list([{bin(Name), SchemaMod} || Name <- RootNames]), + NewMods = maps:from_list([{root_bin(Name), SchemaMod} || Name <- RootNames]), persistent_term:put(?PERSIS_SCHEMA_MODS, #{ mods => maps:merge(OldMods, NewMods), names => lists:usort(OldNames ++ RootNames) @@ -440,3 +440,6 @@ conf_key(?CONF, RootName) -> atom(RootName); conf_key(?RAW_CONF, RootName) -> bin(RootName). + +root_bin({array, Bin}) -> bin(Bin); +root_bin(Bin) -> bin(Bin). diff --git a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf index 0e825c8c7..247c09d8d 100644 --- a/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf +++ b/apps/emqx_bridge_mqtt/etc/emqx_bridge_mqtt.conf @@ -2,57 +2,55 @@ ## Configuration for EMQ X MQTT Broker Bridge ##==================================================================== -emqx_bridge_mqtt { - 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" - # } - # } - ] -} +bridge_mqtt: [ + # { + # 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 8cc87ef64..02078fac0 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -23,13 +23,9 @@ -export([ structs/0 , fields/1]). -structs() -> ["emqx_bridge_mqtt"]. +structs() -> [{array, "bridge_mqtt"}]. -fields("emqx_bridge_mqtt") -> - [ {bridges, hoconsc:array(hoconsc:ref(?MODULE, "bridges"))} - ]; - -fields("bridges") -> +fields("bridge_mqtt") -> [ {name, emqx_schema:t(string(), undefined, true)} , {start_type, fun start_type/1} , {forwards, fun forwards/1} diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 38ed98898..7dd193e63 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -204,7 +204,8 @@ fields(Name) -> find_field(Name, []) -> error({unknown_config_struct_field, Name}); find_field(Name, [SchemaModule | Rest]) -> - case lists:member(Name, SchemaModule:structs()) of + case lists:member(Name, SchemaModule:structs()) orelse + lists:keymember(Name, 2, SchemaModule:structs()) of true -> SchemaModule:fields(Name); false -> find_field(Name, Rest) end. diff --git a/rebar.config b/rebar.config index 83de68ecb..e2559c885 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.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.1"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. From 4be58ae759fda148b8384ccc4d075ffad5c34523 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Wed, 25 Aug 2021 16:37:15 +0800 Subject: [PATCH 121/306] fix: event message api method --- apps/emqx_modules/src/emqx_event_message_api.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_modules/src/emqx_event_message_api.erl b/apps/emqx_modules/src/emqx_event_message_api.erl index 3dc94fd9f..80e5825d1 100644 --- a/apps/emqx_modules/src/emqx_event_message_api.erl +++ b/apps/emqx_modules/src/emqx_event_message_api.erl @@ -39,7 +39,7 @@ event_message_api() -> <<"200">> => schema(conf_schema()) } }, - post => #{ + put => #{ description => <<"Update Event Message">>, 'requestBody' => schema(conf_schema()), responses => #{ @@ -52,6 +52,6 @@ event_message_api() -> event_message(get, _Params) -> {200, emqx_event_message:list()}; -event_message(post, #{body := Body}) -> +event_message(put, #{body := Body}) -> _ = emqx_event_message:update(Body), {200, emqx_event_message:list()}. From c5f0091b5dd3315d6e23aed89b88c20ce561168d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 24 Aug 2021 18:48:25 +0800 Subject: [PATCH 122/306] refactor(config): rework - config struct for zones and listeners ``` listeners.tcp.default { bind = "0.0.0.0:1883" acceptors = 16 max_connections = 1024000 access_rules = [ "allow all" ] proxy_protocol = false proxy_protocol_timeout = 3s mountpoint = "" tcp.backlog = 1024 tcp.buffer = 4KB } listeners.ssl.default { bind = "0.0.0.0:8883" acceptors = 16 max_connections = 512000 access_rules = [ "allow all" ] proxy_protocol = false proxy_protocol_timeout = 3s mountpoint = "" ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] ssl.keyfile = "etc/certs/key.pem" ssl.certfile = "etc/certs/cert.pem" ssl.cacertfile = "etc/certs/cacert.pem" tcp.backlog = 1024 tcp.buffer = 4KB } listeners.quic.default { bind = "0.0.0.0:14567" acceptors = 16 max_connections = 1024000 keyfile = "etc/certs/key.pem" certfile = "etc/certs/cert.pem" mountpoint = "" } listeners.ws.default { bind = "0.0.0.0:8083" acceptors = 16 max_connections = 1024000 access_rules = [ "allow all" ] proxy_protocol = false proxy_protocol_timeout = 3s mountpoint = "" tcp.backlog = 1024 tcp.buffer = 4KB websocket.idle_timeout = 86400s } listeners.wss.default { bind = "0.0.0.0:8084" acceptors = 16 max_connections = 512000 access_rules = [ "allow all" ] proxy_protocol = false proxy_protocol_timeout = 3s mountpoint = "" ssl.keyfile = "etc/certs/key.pem" ssl.certfile = "etc/certs/cert.pem" ssl.cacertfile = "etc/certs/cacert.pem" tcp.backlog = 1024 tcp.buffer = 4KB websocket.idle_timeout = 86400s } ``` ``` zones.default { } ``` --- apps/emqx/etc/emqx.conf | 2069 ++++++++--------- apps/emqx/src/emqx_config.erl | 36 +- apps/emqx/src/emqx_listeners.erl | 203 +- apps/emqx/src/emqx_schema.erl | 63 +- .../src/emqx_mgmt_api_listeners.erl | 4 +- 5 files changed, 1145 insertions(+), 1230 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 50ddbfcde..251f2d0a5 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -1,3 +1,991 @@ +##================================================================== +## Listeners +##================================================================== +## MQTT/TCP - TCP Listeners for MQTT Protocol +## syntax: listeners.tcp. +## example: listeners.tcp.my_tcp_listener +listeners.tcp.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.tcp..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 1883, 127.0.0.1:1883, ::1:1883 + bind = "0.0.0.0:1883" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.tcp..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.tcp..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.tcp..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 listeners.tcp..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 listeners.tcp..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 listeners.tcp..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## 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 `listeners.tcp..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 listeners.tcp..mqtt.mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog = 1024 + tcp.buffer = 4KB +} + +## MQTT/SSL - SSL Listeners for MQTT Protocol +## syntax: listeners.ssl. +## example: listeners.ssl.my_ssl_listener +listeners.ssl.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.ssl..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8883, 127.0.0.1:8883, ::1:8883 + bind = "0.0.0.0:8883" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.ssl..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.ssl..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.ssl..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 listeners.ssl..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 listeners.ssl..proxy_protocol + ## ValueType: Boolean + ## Default: 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 listeners.ssl..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## 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 `listeners.ssl..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 listeners.ssl..mqtt.mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## SSL options + ## See ${example_common_ssl_options} for more information + 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" + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.backlog = 1024 + tcp.buffer = 4KB +} + +## MQTT/QUIC - QUIC Listeners for MQTT Protocol +## syntax: listeners.quic. +## example: listeners.quic.my_quic_listener +listeners.quic.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.quic..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 14567, 127.0.0.1:14567, ::1:14567 + bind = "0.0.0.0:14567" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.quic..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.quic..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.quic..max_connections + ## ValueType: Number | infinity + ## Default: infinity + max_connections = 1024000 + + ## Path to the file containing the user's private PEM-encoded key. + ## + ## @doc listeners.quic..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 listeners.quic..certfile + ## ValueType: String + ## Default: "{{ platform_etc_dir }}/certs/cert.pem" + certfile = "{{ platform_etc_dir }}/certs/cert.pem" + + ## 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 `listeners.quic..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 listeners.quic..mqtt.mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" +} + +## MQTT/WS - Websocket Listeners for MQTT Protocol +## syntax: listeners.ws. +## example: listeners.ws.my_ws_listener +listeners.ws.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.ws..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8083, 127.0.0.1:8083, ::1:8083 + bind = "0.0.0.0:8083" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.ws..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.ws..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.ws..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 listeners.ws..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 listeners.ws..proxy_protocol + ## ValueType: Boolean + ## Default: 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 listeners.ws..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## 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 `listeners.ws..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 listeners.ws..mqtt.mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## 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 +} + +## MQTT/WSS - WebSocket Secure Listeners for MQTT Protocol +## syntax: listeners.wss. +## example: listeners.wss.my_wss_listener +listeners.wss.default { + ## The IP address and port that the listener will bind. + ## + ## @doc listeners.wss..bind + ## ValueType: IPAddress | Port | IPAddrPort + ## Required: true + ## Examples: 8084, 127.0.0.1:8084, ::1:8084 + bind = "0.0.0.0:8084" + + ## The configuration zone this listener is using. + ## If not set, the global configs are used for this listener. + ## + ## See `zones.` for more details. + ## + ## @doc listeners.wss..zone + ## ValueType: String + ## Required: false + #zone = default + + ## The size of the acceptor pool for this listener. + ## + ## @doc listeners.wss..acceptors + ## ValueType: Number + ## Default: 16 + acceptors = 16 + + ## Maximum number of concurrent connections. + ## + ## @doc listeners.wss..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 listeners.wss..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 listeners.wss..proxy_protocol + ## ValueType: Boolean + ## Default: 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 listeners.wss..proxy_protocol_timeout + ## ValueType: Duration + ## Default: 3s + proxy_protocol_timeout = 3s + + ## 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 `listeners.wss..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 listeners.wss..mqtt.mountpoint + ## ValueType: String + ## Default: "" + mountpoint = "" + + ## 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 +} + +## Enable per connection statistics. +## +## @doc stats.enable +## ValueType: Boolean +## Default: true +stats.enable = true + +authorization { + ## Behaviour after not matching a rule. + ## + ## @doc authorization.no_match + ## ValueType: allow | deny + ## Default: allow + no_match: allow + + ## The action when authorization check reject current operation + ## + ## @doc authorization.deny_action + ## ValueType: ignore | disconnect + ## Default: ignore + deny_action: ignore + + ## Whether to enable Authorization cache. + ## + ## If enabled, Authorization roles for each client will be cached in the memory + ## + ## @doc authorization.cache.enable + ## ValueType: Boolean + ## Default: true + cache.enable: true + + ## The maximum count of Authorization entries can be cached for a client. + ## + ## @doc authorization.cache.max_size + ## ValueType: Integer + ## Range: [0, 1048576] + ## Default: 32 + cache.max_size: 32 + + ## The time after which an Authorization cache entry will be deleted + ## + ## @doc authorization.cache.ttl + ## ValueType: Duration + ## Default: 1m + cache.ttl: 1m +} + +mqtt { + ## How long time the MQTT connection will be disconnected if the + ## TCP connection is established but MQTT CONNECT has not been + ## received. + ## + ## @doc mqtt.idle_timeout + ## ValueType: Duration + ## Default: 15s + idle_timeout = 15s + + ## Maximum MQTT packet size allowed. + ## + ## @doc mqtt.max_packet_size + ## ValueType: Bytes + ## Default: 1MB + max_packet_size = 1MB + + ## Maximum length of MQTT clientId allowed. + ## + ## @doc mqtt.max_clientid_len + ## ValueType: Integer + ## Range: [23, 65535] + ## Default: 65535 + max_clientid_len = 65535 + + ## Maximum topic levels allowed. + ## + ## @doc mqtt.max_topic_levels + ## ValueType: Integer + ## Range: [1, 65535] + ## Default: 65535 + max_topic_levels = 65535 + + ## Maximum QoS allowed. + ## + ## @doc mqtt.max_qos_allowed + ## ValueType: 0 | 1 | 2 + ## Default: 2 + max_qos_allowed = 2 + + ## Maximum Topic Alias, 0 means no topic alias supported. + ## + ## @doc mqtt.max_topic_alias + ## ValueType: Integer + ## Range: [0, 65535] + ## Default: 65535 + max_topic_alias = 65535 + + ## Whether the Server supports MQTT retained messages. + ## + ## @doc mqtt.retain_available + ## ValueType: Boolean + ## Default: true + retain_available = true + + ## Whether the Server supports MQTT Wildcard Subscriptions + ## + ## @doc mqtt.wildcard_subscription + ## ValueType: Boolean + ## Default: true + wildcard_subscription = true + + ## Whether the Server supports MQTT Shared Subscriptions. + ## + ## @doc mqtt.shared_subscription + ## ValueType: Boolean + ## Default: true + shared_subscription = true + + ## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) + ## + ## @doc mqtt.ignore_loop_deliver + ## ValueType: Boolean + ## Default: false + ignore_loop_deliver = false + + ## Whether to parse the MQTT frame in strict mode + ## + ## @doc mqtt.strict_mode + ## ValueType: Boolean + ## Default: false + strict_mode = false + + ## Specify the response information returned to the client + ## + ## This feature is disabled if is set to "" + ## + ## @doc mqtt.response_information + ## ValueType: String + ## Default: "" + response_information = "" + + ## Server Keep Alive of MQTT 5.0 + ## + ## @doc mqtt.server_keepalive + ## ValueType: Number | disabled + ## Default: disabled + server_keepalive = disabled + + ## The backoff for MQTT keepalive timeout. The broker will kick a connection out + ## until 'Keepalive * backoff * 2' timeout. + ## + ## @doc mqtt.keepalive_backoff + ## ValueType: Float + ## Range: (0.5, 1] + ## Default: 0.75 + keepalive_backoff = 0.75 + + ## Maximum number of subscriptions allowed. + ## + ## @doc mqtt.max_subscriptions + ## ValueType: Integer | infinity + ## Range: [1, infinity) + ## Default: infinity + max_subscriptions = infinity + + ## Force to upgrade QoS according to subscription. + ## + ## @doc mqtt.upgrade_qos + ## ValueType: Boolean + ## Default: false + upgrade_qos = false + + ## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. + ## + ## @doc mqtt.max_inflight + ## ValueType: Integer + ## Range: [1, 65535] + ## Default: 32 + max_inflight = 32 + + ## Retry interval for QoS1/2 message delivering. + ## + ## @doc mqtt.retry_interval + ## ValueType: Duration + ## Default: 30s + retry_interval = 30s + + ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. + ## + ## @doc mqtt.max_awaiting_rel + ## ValueType: Integer | infinity + ## Range: [1, infinity) + ## Default: 100 + max_awaiting_rel = 100 + + ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. + ## + ## @doc mqtt.await_rel_timeout + ## ValueType: Duration + ## Default: 300s + await_rel_timeout = 300s + + ## Default session expiry interval for MQTT V3.1.1 connections. + ## + ## @doc 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. + ## + ## @doc mqtt.max_mqueue_len + ## ValueType: Integer | infinity + ## Range: [0, infinity) + ## Default: 1000 + max_mqueue_len = 1000 + + ## Topic priorities. + ## + ## There's no priority table by default, hence all messages + ## are treated equal. + ## + ## 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 mqtt.mqueue_priorities + ## ValueType: Map | disabled + ## Examples: + ## 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 mqtt.mqueue_default_priority + ## ValueType: highest | lowest + ## Default: lowest + mqueue_default_priority = lowest + + ## Whether to enqueue QoS0 messages. + ## + ## @doc mqtt.mqueue_store_qos0 + ## ValueType: Boolean + ## Default: true + mqueue_store_qos0 = true + + ## Whether use username replace client id + ## + ## @doc 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 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 mqtt.peer_cert_as_clientid + ## ValueType: cn | dn | crt | disabled + ## Default: disabled + peer_cert_as_clientid = disabled +} + +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 flapping_detect.enable + ## ValueType: Boolean + ## Default: true + enable = false + + ## The max disconnect allowed of a MQTT Client in `window_time` + ## + ## @doc flapping_detect.max_count + ## ValueType: Integer + ## Default: 15 + max_count = 15 + + ## The time window for flapping detect + ## + ## @doc flapping_detect.window_time + ## ValueType: Duration + ## Default: 1m + window_time = 1m + + ## How long the clientid will be banned + ## + ## @doc flapping_detect.ban_time + ## ValueType: Duration + ## Default: 5m + ban_time = 5m + +} + +force_shutdown { + ## Enable force_shutdown + ## + ## @doc force_shutdown.enable + ## ValueType: Boolean + ## Default: true + enable = true + + ## Max message queue length + ## @doc force_shutdown.max_message_queue_len + ## ValueType: Integer + ## Range: (0, infinity) + ## Default: 1000 + max_message_queue_len = 1000 + + ## Total heap size + ## + ## @doc 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 force_gc.enable + ## ValueType: Boolean + ## Default: true + enable = true + + ## GC the process after how many messages received + ## @doc force_gc.max_message_queue_len + ## ValueType: Integer + ## Range: (0, infinity) + ## Default: 16000 + count = 16000 + + ## GC the process after how much bytes passed through + ## + ## @doc 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 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 conn_congestion.min_alarm_sustain_duration + ## ValueType: Duration + ## Default: 1m + min_alarm_sustain_duration = 1m +} + +rate_limit { + ## Maximum connections per second. + ## + ## @doc zones..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 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 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" +} + +quota { + ## Messages quota for the each of external MQTT connection. + ## This value consumed by the number of recipient on a message. + ## + ## @doc quota.conn_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 100 messaegs per 1s: + ## quota.conn_messages_routing: "100,1s" + 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 quota.overall_messages_routing + ## ValueType: String | infinity + ## Default: infinity + ## Examples: 200000 messages per 1s: + ## quota.overall_messages_routing: "200000,1s" + ## + overall_messages_routing = "200000,1s" +} + +##================================================================== +## Zones +##================================================================== +## A zone contains a set of configurations for listeners. +## +## A zone can be used by a listener via `listener...zone`. +## +## The configs defined in zones will override the global configs with the same key. +## +## For example given the following config: +## +## ``` +## a { +## b: 1, c: 1 +## } +## +## zone.my_zone { +## a { +## b:2 +## } +## } +## ``` +## +## The global config "a" is overridden by the configs "a" inside the zone "my_zone". +## If there is a listener uses the zone "my_zone", the value of config "a" will be: +## `{b:2, c: 1}`. +## Note that although the default value of `a.c` is `0`, the global value is used. +## i.e. configs in the zone have no default values. To overridde `a.c` we must configure +## it explicitly in the zone. +## +## All the global configs that can be overridden in zones are: +## - `stats.*` +## - `mqtt.*` +## - `authorization.*` +## - `flapping_detect.*` +## - `force_shutdown.*` +## - `conn_congestion.*` +## +## syntax: zones. +## example: zones.my_zone +zones.default { + +} + ##================================================================== ## Broker ##================================================================== @@ -88,1087 +1076,6 @@ broker { perf.trie_compaction = true } - -authorization { - ## Behaviour after not matching a rule. - ## - ## @doc authorization.no_match - ## ValueType: allow | deny - ## Default: allow - no_match: allow - - ## The action when authorization check reject current operation - ## - ## @doc authorization.deny_action - ## ValueType: ignore | disconnect - ## Default: ignore - deny_action: ignore - - ## Whether to enable Authorization cache. - ## - ## If enabled, Authorization roles for each client will be cached in the memory - ## - ## @doc authorization.cache.enable - ## ValueType: Boolean - ## Default: true - cache.enable: true - - ## The maximum count of Authorization entries can be cached for a client. - ## - ## @doc authorization.cache.max_size - ## ValueType: Integer - ## Range: [0, 1048576] - ## Default: 32 - cache.max_size: 32 - - ## The time after which an Authorization cache entry will be deleted - ## - ## @doc authorization.cache.ttl - ## ValueType: Duration - ## Default: 1m - cache.ttl: 1m -} - - -##================================================================== -## Zones and Listeners -##================================================================== -## A zone contains a set of configurations for listeners. -## -## The configurations defined in zone can be overridden by the ones -## defined in listeners with the same key. -## -## For example given the following config: -## ``` -## -## 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.*` -## - `flapping_detect.*` -## - `force_shutdown.*` -## - `conn_congestion.*` -## -## Syntax: zones. {} -zones.default { - ## Enable authentication - ## - ## @doc zones..auth.enable - ## ValueType: Boolean - ## Default: false - auth.enable = false - - ## Enable per connection statistics. - ## - ## @doc zones..stats.enable - ## ValueType: Boolean - ## Default: true - 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. - ## - ## @doc zones..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 - ## 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 `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 - ## 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 zones..listeners..mountpoint - ## ValueType: String - ## Default: "" - mountpoint = "" - - ## How long time the MQTT connection will be disconnected if the - ## TCP connection is established but MQTT CONNECT has not been - ## received. - ## - ## @doc zones..mqtt.idle_timeout - ## ValueType: Duration - ## Default: 15s - idle_timeout = 15s - - ## Maximum MQTT packet size allowed. - ## - ## @doc zones..mqtt.max_packet_size - ## ValueType: Bytes - ## Default: 1MB - max_packet_size = 1MB - - ## Maximum length of MQTT clientId allowed. - ## - ## @doc zones..mqtt.max_clientid_len - ## ValueType: Integer - ## Range: [23, 65535] - ## Default: 65535 - max_clientid_len = 65535 - - ## Maximum topic levels allowed. - ## - ## @doc zones..mqtt.max_topic_levels - ## ValueType: Integer - ## Range: [1, 65535] - ## Default: 65535 - max_topic_levels = 65535 - - ## Maximum 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 zones..mqtt.max_topic_alias - ## ValueType: Integer - ## Range: [0, 65535] - ## Default: 65535 - max_topic_alias = 65535 - - ## Whether the Server supports MQTT retained messages. - ## - ## @doc zones..mqtt.retain_available - ## ValueType: Boolean - ## Default: true - retain_available = true - - ## Whether the Server supports MQTT Wildcard Subscriptions - ## - ## @doc zones..mqtt.wildcard_subscription - ## ValueType: Boolean - ## Default: true - wildcard_subscription = true - - ## Whether the Server supports MQTT Shared Subscriptions. - ## - ## @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 zones..mqtt.ignore_loop_deliver - ## ValueType: Boolean - ## Default: false - ignore_loop_deliver = false - - ## Whether to parse the MQTT frame in strict mode - ## - ## @doc zones..mqtt.strict_mode - ## ValueType: Boolean - ## Default: false - strict_mode = false - - ## Specify the response information returned to the client - ## - ## This feature is disabled if is set to "" - ## - ## @doc zones..mqtt.response_information - ## ValueType: String - ## Default: "" - response_information = "" - - ## Server Keep Alive of MQTT 5.0 - ## - ## @doc zones..mqtt.server_keepalive - ## ValueType: Number | disabled - ## Default: disabled - server_keepalive = disabled - - ## The backoff for MQTT keepalive timeout. The broker will kick a connection out - ## until 'Keepalive * backoff * 2' timeout. - ## - ## @doc zones..mqtt.keepalive_backoff - ## ValueType: Float - ## Range: (0.5, 1] - ## Default: 0.75 - keepalive_backoff = 0.75 - - ## Maximum number of subscriptions allowed. - ## - ## @doc zones..mqtt.max_subscriptions - ## ValueType: Integer | infinity - ## Range: [1, infinity) - ## Default: infinity - max_subscriptions = infinity - - ## Force to upgrade QoS according to subscription. - ## - ## @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 zones..mqtt.max_inflight - ## ValueType: Integer - ## Range: [1, 65535] - ## Default: 32 - max_inflight = 32 - - ## Retry interval for QoS1/2 message delivering. - ## - ## @doc zones..mqtt.retry_interval - ## ValueType: Duration - ## Default: 30s - retry_interval = 30s - - ## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL. - ## - ## @doc zones..mqtt.max_awaiting_rel - ## ValueType: Integer | infinity - ## Range: [1, infinity) - ## Default: 100 - max_awaiting_rel = 100 - - ## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL 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 zones..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. - ## - ## @doc zones..mqtt.max_mqueue_len - ## ValueType: Integer | infinity - ## Range: [0, infinity) - ## Default: 1000 - max_mqueue_len = 1000 - - ## Topic priorities. - ## - ## There's no priority table by default, hence all messages - ## are treated equal. - ## - ## 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: Map | disabled - ## Examples: - ## 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: lowest - mqueue_default_priority = lowest - - ## Whether to enqueue QoS0 messages. - ## - ## @doc zones..mqtt.mqueue_store_qos0 - ## ValueType: Boolean - ## Default: true - mqueue_store_qos0 = true - - ## Whether use username replace client id - ## - ## @doc zones..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 zones..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 zones..mqtt.peer_cert_as_clientid - ## ValueType: cn | dn | crt | disabled - ## Default: disabled - peer_cert_as_clientid = disabled - - } - - 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 zones..flapping_detect.enable - ## ValueType: Boolean - ## Default: true - enable = false - - ## The max disconnect allowed of a MQTT Client in `window_time` - ## - ## @doc zones..flapping_detect.max_count - ## ValueType: Integer - ## Default: 15 - max_count = 15 - - ## The time window for flapping detect - ## - ## @doc zones..flapping_detect.window_time - ## ValueType: Duration - ## Default: 1m - window_time = 1m - - ## How long the clientid will be banned - ## - ## @doc zones..flapping_detect.ban_time - ## ValueType: Duration - ## Default: 5m - ban_time = 5m - - } - - force_shutdown { - ## Enable force_shutdown - ## - ## @doc zones..force_shutdown.enable - ## ValueType: Boolean - ## Default: true - enable = true - - ## Max message queue length - ## @doc zones..force_shutdown.max_message_queue_len - ## ValueType: Integer - ## Range: (0, infinity) - ## Default: 1000 - max_message_queue_len = 1000 - - ## Total heap size - ## - ## @doc zones..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 zones..force_gc.enable - ## ValueType: Boolean - ## Default: true - enable = true - - ## GC the process after how many messages received - ## @doc zones..force_gc.max_message_queue_len - ## ValueType: Integer - ## Range: (0, infinity) - ## Default: 16000 - count = 16000 - - ## GC the process after how much bytes passed through - ## - ## @doc zones..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 zones..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 zones..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 zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type = tcp - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..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 zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors = 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..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 zones..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 zones..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 zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout = 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..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 zones..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 zones..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 zones..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 zones..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 - 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 zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type = tcp - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..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 zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors = 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..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 zones..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 zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: 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 zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout = 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..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 zones..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 zones..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 zones..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 zones..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.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" - - ## TCP options - ## See ${example_common_tcp_options} for more information - tcp.backlog = 1024 - tcp.buffer = 4KB - } - - listeners.mqtt_quic - { - ## The type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type = quic - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..listeners..bind - ## ValueType: IPAddress | Port | IPAddrPort - ## Required: true - ## Examples: 14567, 127.0.0.1:14567, ::1:14567 - bind = "0.0.0.0:14567" - - ## The size of the acceptor pool for this listener. - ## - ## @doc zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors = 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..listeners..max_connections - ## ValueType: Number | infinity - ## Default: infinity - max_connections = 1024000 - - ## 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 - #${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 zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type = ws - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..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 zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors = 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..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 zones..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 zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: 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 zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout = 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..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 zones..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 zones..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 zones..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 zones..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 - 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 type of the listener. - ## - ## @doc zones..listeners..type - ## ValueType: tcp | ws - ## - tcp: MQTT over TCP - ## - ws: MQTT over Websocket - ## - quic: MQTT over QUIC - ## Required: true - type = ws - - ## The IP address and port that the listener will bind. - ## - ## @doc zones..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 zones..listeners..acceptors - ## ValueType: Number - ## Default: 16 - acceptors = 16 - - ## Maximum number of concurrent connections. - ## - ## @doc zones..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 zones..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 zones..listeners..proxy_protocol - ## ValueType: Boolean - ## Default: 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 zones..listeners..proxy_protocol_timeout - ## ValueType: Duration - ## Default: 3s - proxy_protocol_timeout = 3s - - rate_limit { - ## Maximum connections per second. - ## - ## @doc zones..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 zones..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 zones..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 zones..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 zones..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" - - ## 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. -zones.internal { - 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 ##================================================================== diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 101abbd2b..516831600 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -58,7 +58,6 @@ -export([ get_zone_conf/2 , get_zone_conf/3 , put_zone_conf/3 - , find_zone_conf/2 ]). -export([ get_listener_conf/3 @@ -72,7 +71,7 @@ -define(PERSIS_SCHEMA_MODS, {?MODULE, schema_mods}). -define(PERSIS_KEY(TYPE, ROOT), {?MODULE, TYPE, ROOT}). -define(ZONE_CONF_PATH(ZONE, PATH), [zones, ZONE | PATH]). --define(LISTENER_CONF_PATH(ZONE, LISTENER, PATH), [zones, ZONE, listeners, LISTENER | PATH]). +-define(LISTENER_CONF_PATH(TYPE, LISTENER, PATH), [listeners, TYPE, LISTENER | PATH]). -define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL), try [atom(Key) || Key <- PATH] of @@ -151,37 +150,40 @@ find_raw(KeyPath) -> -spec get_zone_conf(atom(), emqx_map_lib:config_key_path()) -> term(). get_zone_conf(Zone, KeyPath) -> - ?MODULE:get(?ZONE_CONF_PATH(Zone, KeyPath)). + case find(?ZONE_CONF_PATH(Zone, KeyPath)) of + {not_found, _, _} -> %% not found in zones, try to find the global config + ?MODULE:get(KeyPath); + {ok, Value} -> Value + end. -spec get_zone_conf(atom(), emqx_map_lib:config_key_path(), term()) -> term(). get_zone_conf(Zone, KeyPath, Default) -> - ?MODULE:get(?ZONE_CONF_PATH(Zone, KeyPath), Default). + case find(?ZONE_CONF_PATH(Zone, KeyPath)) of + {not_found, _, _} -> %% not found in zones, try to find the global config + ?MODULE:get(KeyPath, Default); + {ok, Value} -> Value + end. -spec put_zone_conf(atom(), emqx_map_lib:config_key_path(), term()) -> ok. put_zone_conf(Zone, KeyPath, Conf) -> ?MODULE:put(?ZONE_CONF_PATH(Zone, KeyPath), Conf). --spec find_zone_conf(atom(), emqx_map_lib:config_key_path()) -> - {ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}. -find_zone_conf(Zone, KeyPath) -> - find(?ZONE_CONF_PATH(Zone, KeyPath)). - -spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> term(). -get_listener_conf(Zone, Listener, KeyPath) -> - ?MODULE:get(?LISTENER_CONF_PATH(Zone, Listener, KeyPath)). +get_listener_conf(Type, Listener, KeyPath) -> + ?MODULE:get(?LISTENER_CONF_PATH(Type, Listener, KeyPath)). -spec get_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> term(). -get_listener_conf(Zone, Listener, KeyPath, Default) -> - ?MODULE:get(?LISTENER_CONF_PATH(Zone, Listener, KeyPath), Default). +get_listener_conf(Type, Listener, KeyPath, Default) -> + ?MODULE:get(?LISTENER_CONF_PATH(Type, Listener, KeyPath), Default). -spec put_listener_conf(atom(), atom(), emqx_map_lib:config_key_path(), term()) -> ok. -put_listener_conf(Zone, Listener, KeyPath, Conf) -> - ?MODULE:put(?LISTENER_CONF_PATH(Zone, Listener, KeyPath), Conf). +put_listener_conf(Type, Listener, KeyPath, Conf) -> + ?MODULE:put(?LISTENER_CONF_PATH(Type, Listener, KeyPath), Conf). -spec find_listener_conf(atom(), atom(), emqx_map_lib:config_key_path()) -> {ok, term()} | {not_found, emqx_map_lib:config_key_path(), term()}. -find_listener_conf(Zone, Listener, KeyPath) -> - find(?LISTENER_CONF_PATH(Zone, Listener, KeyPath)). +find_listener_conf(Type, Listener, KeyPath) -> + find(?LISTENER_CONF_PATH(Type, Listener, KeyPath)). -spec put(map()) -> ok. put(Config) -> diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index f39c11305..375a5c990 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -43,18 +43,14 @@ list() -> [{listener_id(ZoneName, LName), LConf} || {ZoneName, LName, LConf} <- do_list()]. do_list() -> - Zones = maps:to_list(emqx:get_config([zones], #{})), - lists:append([list(ZoneName, ZoneConf) || {ZoneName, ZoneConf} <- Zones]). + Listeners = maps:to_list(emqx:get_config([listeners], #{})), + lists:append([list(Type, maps:to_list(Conf)) || {Type, Conf} <- Listeners]). -list(ZoneName, ZoneConf) -> - Listeners = maps:to_list(maps:get(listeners, ZoneConf, #{})), - [ - begin - Conf = merge_zone_and_listener_confs(ZoneConf, LConf), - Running = is_running(listener_id(ZoneName, LName), Conf), - {ZoneName , LName, maps:put(running, Running, Conf)} - end - || {LName, LConf} <- Listeners, is_map(LConf)]. +list(Type, Conf) -> + [begin + Running = is_running(Type, listener_id(Type, LName), LConf), + {Type, LName, maps:put(running, Running, LConf)} + end || {LName, LConf} <- Conf, is_map(LConf)]. -spec is_running(ListenerId :: atom()) -> boolean() | {error, no_found}. is_running(ListenerId) -> @@ -65,7 +61,7 @@ is_running(ListenerId) -> [] -> {error, not_found} end. -is_running(ListenerId, #{type := tcp, bind := ListenOn})-> +is_running(Type, ListenerId, #{bind := ListenOn}) when Type =:= tcp; Type =:= ssl -> try esockd:listener({ListenerId, ListenOn}) of Pid when is_pid(Pid)-> true @@ -73,7 +69,7 @@ is_running(ListenerId, #{type := tcp, bind := ListenOn})-> false end; -is_running(ListenerId, #{type := ws})-> +is_running(Type, ListenerId, _Conf) when Type =:= ws; Type =:= wss -> try Info = ranch:info(ListenerId), proplists:get_value(status, Info) =:= running @@ -81,8 +77,8 @@ is_running(ListenerId, #{type := ws})-> false end; -is_running(_ListenerId, #{type := quic})-> -%% TODO: quic support +is_running(quic, _ListenerId, _Conf)-> + %% TODO: quic support {error, no_found}. %% @doc Start all listeners. @@ -95,23 +91,56 @@ start_listener(ListenerId) -> apply_on_listener(ListenerId, fun start_listener/3). -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 +start_listener(Type, ListenerName, #{bind := Bind} = Conf) -> + case do_start_listener(Type, ListenerName, Conf) of {ok, {skipped, Reason}} when Reason =:= listener_disabled; Reason =:= quic_app_missing -> - console_print("- Skip - starting ~s listener ~s on ~s ~n due to ~p", - [Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]); + console_print("- Skip - starting listener ~s on ~s ~n due to ~p", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]); {ok, _} -> - console_print("Start ~s listener ~s on ~s successfully.~n", - [Type, listener_id(ZoneName, ListenerName), format(Bind)]); + console_print("Start listener ~s on ~s successfully.~n", + [listener_id(Type, ListenerName), format_addr(Bind)]); {error, {already_started, Pid}} -> {error, {already_started, Pid}}; {error, Reason} -> - ?ELOG("Failed to start ~s listener ~s on ~s: ~0p~n", - [Type, listener_id(ZoneName, ListenerName), format(Bind), Reason]), + ?ELOG("Failed to start listener ~s on ~s: ~0p~n", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]), error(Reason) end. +%% @doc Restart all listeners +-spec(restart() -> ok). +restart() -> + foreach_listeners(fun restart_listener/3). + +-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, term()}). +restart_listener(Type, ListenerName, Conf) -> + case stop_listener(Type, ListenerName, Conf) of + ok -> start_listener(Type, ListenerName, Conf); + Error -> Error + end. + +%% @doc Stop all listeners. +-spec(stop() -> ok). +stop() -> + foreach_listeners(fun stop_listener/3). + +-spec(stop_listener(atom()) -> ok | {error, term()}). +stop_listener(ListenerId) -> + apply_on_listener(ListenerId, fun stop_listener/3). + +-spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). +stop_listener(Type, ListenerName, #{type := tcp, bind := ListenOn}) -> + esockd:close(listener_id(Type, ListenerName), ListenOn); +stop_listener(Type, ListenerName, #{type := ws}) -> + cowboy:stop_listener(listener_id(Type, ListenerName)); +stop_listener(Type, ListenerName, #{type := quic}) -> + quicer:stop_listener(listener_id(Type, ListenerName)). + -ifndef(TEST). console_print(Fmt, Args) -> ?ULOG(Fmt, Args). -else. @@ -121,27 +150,28 @@ console_print(_Fmt, _Args) -> ok. %% Start MQTT/TCP listener -spec(do_start_listener(atom(), atom(), map()) -> {ok, pid() | {skipped, atom()}} | {error, term()}). -do_start_listener(_ZoneName, _ListenerName, #{enabled := false}) -> +do_start_listener(_Type, _ListenerName, #{enabled := false}) -> {ok, {skipped, listener_disabled}}; -do_start_listener(ZoneName, ListenerName, #{type := tcp, bind := ListenOn} = Opts) -> - esockd:open(listener_id(ZoneName, ListenerName), ListenOn, merge_default(esockd_opts(Opts)), +do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) + when Type == tcp; Type == ssl -> + esockd:open(listener_id(Type, ListenerName), ListenOn, merge_default(esockd_opts(Type, Opts)), {emqx_connection, start_link, - [#{zone => ZoneName, listener => ListenerName}]}); + [#{type => Type, listener => ListenerName, + zone => zone(Opts)}]}); %% Start MQTT/WS listener -do_start_listener(ZoneName, ListenerName, #{type := ws, bind := ListenOn} = Opts) -> - Id = listener_id(ZoneName, ListenerName), - RanchOpts = ranch_opts(ListenOn, Opts), - WsOpts = ws_opts(ZoneName, ListenerName, Opts), - case is_ssl(Opts) of - false -> - cowboy:start_clear(Id, RanchOpts, WsOpts); - true -> - cowboy:start_tls(Id, RanchOpts, WsOpts) +do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) + when Type == ws; Type == wss -> + Id = listener_id(Type, ListenerName), + RanchOpts = ranch_opts(Type, ListenOn, Opts), + WsOpts = ws_opts(Type, ListenerName, Opts), + case Type of + ws -> cowboy:start_clear(Id, RanchOpts, WsOpts); + wss -> cowboy:start_tls(Id, RanchOpts, WsOpts) end; %% Start MQTT/QUIC listener -do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Opts) -> +do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> case [ A || {quicer, _, _} = A<-application:which_applications() ] of [_] -> %% @fixme unsure why we need reopen lib and reopen config. @@ -152,48 +182,48 @@ do_start_listener(ZoneName, ListenerName, #{type := quic, bind := ListenOn} = Op , {key, maps:get(keyfile, Opts)} , {alpn, ["mqtt"]} , {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)} - , {idle_timeout_ms, emqx_config:get_zone_conf(ZoneName, [mqtt, idle_timeout])} + , {idle_timeout_ms, emqx_config:get_zone_conf(zone(Opts), + [mqtt, idle_timeout])} ], ConnectionOpts = #{conn_callback => emqx_quic_connection , peer_unidi_stream_count => 1 , peer_bidi_stream_count => 10 - , zone => ZoneName + , zone => zone(Opts) + , type => quic , listener => ListenerName }, StreamOpts = [], - quicer:start_listener(listener_id(ZoneName, ListenerName), + quicer:start_listener(listener_id(quic, ListenerName), port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts}); [] -> {ok, {skipped, quic_app_missing}} end. -esockd_opts(Opts0) -> +esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), Opts2 = case emqx_map_lib: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(case is_ssl(Opts0) of - false -> - Opts3#{tcp_options => tcp_opts(Opts0)}; - true -> - Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} + maps:to_list(case Type of + tcp -> Opts3#{tcp_options => tcp_opts(Opts0)}; + ssl -> Opts3#{ssl_options => ssl_opts(Opts0), tcp_options => tcp_opts(Opts0)} end). -ws_opts(ZoneName, ListenerName, Opts) -> +ws_opts(Type, ListenerName, Opts) -> WsPaths = [{maps:get(mqtt_path, Opts, "/mqtt"), emqx_ws_connection, - #{zone => ZoneName, listener => ListenerName}}], + #{zone => zone(Opts), type => Type, listener => ListenerName}}], Dispatch = cowboy_router:compile([{'_', WsPaths}]), ProxyProto = maps:get(proxy_protocol, Opts, false), #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. -ranch_opts(ListenOn, Opts) -> +ranch_opts(Type, 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) + SocketOpts = case Type of + wss -> tcp_opts(Opts) ++ proplists:delete(handshake_timeout, ssl_opts(Opts)); + ws -> tcp_opts(Opts) end, #{num_acceptors => NumAcceptors, max_connections => MaxConnections, @@ -217,39 +247,6 @@ esockd_access_rules(StrRules) -> end, [Access(R) || R <- StrRules]. -%% @doc Restart all listeners --spec(restart() -> ok). -restart() -> - foreach_listeners(fun restart_listener/3). - --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, term()}). -restart_listener(ZoneName, ListenerName, Conf) -> - case stop_listener(ZoneName, ListenerName, Conf) of - ok -> start_listener(ZoneName, ListenerName, Conf); - Error -> Error - end. - -%% @doc Stop all listeners. --spec(stop() -> ok). -stop() -> - foreach_listeners(fun stop_listener/3). - --spec(stop_listener(atom()) -> ok | {error, term()}). -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}) -> - esockd:close(listener_id(ZoneName, ListenerName), ListenOn); -stop_listener(ZoneName, ListenerName, #{type := ws}) -> - 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 {value, {tcp_options, TcpOpts}, Options1} -> @@ -258,15 +255,15 @@ merge_default(Options) -> [{tcp_options, ?MQTT_SOCKOPTS} | Options] end. -format(Port) when is_integer(Port) -> +format_addr(Port) when is_integer(Port) -> io_lib:format("0.0.0.0:~w", [Port]); -format({Addr, Port}) when is_list(Addr) -> +format_addr({Addr, Port}) when is_list(Addr) -> io_lib:format("~s:~w", [Addr, Port]); -format({Addr, Port}) when is_tuple(Addr) -> +format_addr({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). -listener_id(ZoneName, ListenerName) -> - list_to_atom(lists:append([atom_to_list(ZoneName), ":", atom_to_list(ListenerName)])). +listener_id(Type, ListenerName) -> + list_to_atom(lists:append([atom_to_list(Type), ":", atom_to_list(ListenerName)])). decode_listener_id(Id) -> try @@ -276,6 +273,9 @@ decode_listener_id(Id) -> _ : _ -> error({invalid_listener_id, Id}) end. +zone(Opts) -> + maps:get(zone, Opts, undefined). + ssl_opts(Opts) -> maps:to_list( emqx_tls_lib:drop_tls13_for_old_otp( @@ -287,9 +287,6 @@ tcp_opts(Opts) -> maps:without([active_n], maps:get(tcp, Opts, #{}))). -is_ssl(Opts) -> - emqx_map_lib:deep_get([ssl, enable], Opts, false). - foreach_listeners(Do) -> lists:foreach( fun({ZoneName, LName, LConf}) -> @@ -298,21 +295,13 @@ foreach_listeners(Do) -> has_enabled_listener_conf_by_type(Type) -> lists:any( - fun({_Zone, _LName, LConf}) when is_map(LConf) -> - Type =:= maps:get(type, LConf) andalso - maps:get(enabled, LConf, true) + fun({Type0, _LName, LConf}) when is_map(LConf) -> + Type =:= Type0 andalso maps:get(enabled, LConf, true) end, do_list()). -%% 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). - apply_on_listener(ListenerId, Do) -> - {ZoneName, ListenerName} = decode_listener_id(ListenerId), - case emqx_config:find_listener_conf(ZoneName, ListenerName, []) of - {not_found, _, _} -> error({listener_config_not_found, ZoneName, ListenerName}); - {ok, Conf} -> Do(ZoneName, ListenerName, Conf) + {Type, ListenerName} = decode_listener_id(ListenerId), + case emqx_config:find_listener_conf(Type, ListenerName, []) of + {not_found, _, _} -> error({listener_config_not_found, Type, ListenerName}); + {ok, Conf} -> Do(Type, ListenerName, Conf) end. diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index cf18f1256..866e14d48 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -70,18 +70,17 @@ -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([ssl/1]). -structs() -> ["zones", "listeners", "broker", "plugins", "sysmon", "alarm", "authorization"]. +structs() -> ["zones", "mqtt", "flapping_detect", "force_shutdown", "force_gc", + "conn_congestion", "rate_limit", "quota", "listeners", "broker", "plugins", + "sysmon", "alarm", "authorization"]. fields("stats") -> [ {"enable", t(boolean(), undefined, true)} ]; -fields("auth") -> - [ {"enable", t(boolean(), undefined, false)} - ]; - fields("authorization") -> [ {"no_match", t(union(allow, deny), undefined, allow)} + , {"enable", t(boolean(), undefined, true)} , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} , {"cache", ref("authorization_cache")} ]; @@ -93,8 +92,7 @@ fields("authorization_cache") -> ]; fields("mqtt") -> - [ {"mountpoint", t(binary(), undefined, <<>>)} - , {"idle_timeout", maybe_infinity(duration(), "15s")} + [ {"idle_timeout", maybe_infinity(duration(), "15s")} , {"max_packet_size", t(bytesize(), undefined, "1MB")} , {"max_clientid_len", t(range(23, 65535), undefined, 65535)} , {"max_topic_levels", t(range(1, 65535), undefined, 65535)} @@ -129,13 +127,11 @@ fields("zones") -> fields("zone_settings") -> [ {"mqtt", ref("mqtt")} - , {"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", maybe_infinity(integer())} , {"listeners", t("listeners")} ]; @@ -143,10 +139,10 @@ fields("rate_limit") -> [ {"max_conn_rate", maybe_infinity(integer(), 1000)} , {"conn_messages_in", maybe_infinity(comma_separated_list())} , {"conn_bytes_in", maybe_infinity(comma_separated_list())} - , {"quota", ref("rate_limit_quota")} + , {"quota", ref("quota")} ]; -fields("rate_limit_quota") -> +fields("quota") -> [ {"conn_messages_routing", maybe_infinity(comma_separated_list())} , {"overall_messages_routing", maybe_infinity(comma_separated_list())} ]; @@ -190,30 +186,51 @@ fields("force_gc") -> ]; fields("listeners") -> - [ {"$name", hoconsc:union( - [ disabled - , hoconsc:ref("mqtt_tcp_listener") - , hoconsc:ref("mqtt_ws_listener") - , hoconsc:ref("mqtt_quic_listener") - ])} + [ {"tcp", ref("t_tcp_listeners")} + , {"ssl", ref("t_ssl_listeners")} + , {"ws", ref("t_ws_listeners")} + , {"wss", ref("t_wss_listeners")} + , {"quic", ref("t_quic_listeners")} + ]; + +fields("t_tcp_listeners") -> + [ {"$name", ref("mqtt_tcp_listener")} + ]; +fields("t_ssl_listeners") -> + [ {"$name", ref("mqtt_ssl_listener")} + ]; +fields("t_ws_listeners") -> + [ {"$name", ref("mqtt_ws_listener")} + ]; +fields("t_wss_listeners") -> + [ {"$name", ref("mqtt_wss_listener")} + ]; +fields("t_quic_listeners") -> + [ {"$name", ref("mqtt_quic_listener")} ]; fields("mqtt_tcp_listener") -> - [ {"type", t(tcp)} - , {"tcp", ref("tcp_opts")} + [ {"tcp", ref("tcp_opts")} + ] ++ mqtt_listener(); + +fields("mqtt_ssl_listener") -> + [ {"tcp", ref("tcp_opts")} , {"ssl", ref("ssl_opts")} ] ++ mqtt_listener(); fields("mqtt_ws_listener") -> - [ {"type", t(ws)} - , {"tcp", ref("tcp_opts")} + [ {"tcp", ref("tcp_opts")} + , {"websocket", ref("ws_opts")} + ] ++ mqtt_listener(); + +fields("mqtt_wss_listener") -> + [ {"tcp", ref("tcp_opts")} , {"ssl", ref("ssl_opts")} , {"websocket", ref("ws_opts")} ] ++ mqtt_listener(); fields("mqtt_quic_listener") -> [ {"enabled", t(boolean(), undefined, true)} - , {"type", t(quic)} , {"certfile", t(string(), undefined, undefined)} , {"keyfile", t(string(), undefined, undefined)} , {"ciphers", t(comma_separated_list(), undefined, "TLS_AES_256_GCM_SHA384," @@ -332,6 +349,8 @@ base_listener() -> , {"acceptors", t(integer(), undefined, 16)} , {"max_connections", maybe_infinity(integer(), infinity)} , {"rate_limit", ref("rate_limit")} + , {"mountpoint", t(binary(), undefined, <<>>)} + , {"zone", t(binary(), undefined, undefined)} ]. %% utils diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index b085db597..b7a69afe1 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -153,13 +153,11 @@ param_path_node() -> }. param_path_id() -> - {Example,_} = hd(emqx_mgmt:list_listeners(node())), #{ name => id, in => path, schema => #{type => string}, - required => true, - example => Example + required => true }. param_path_operation()-> From cc56c74964e3d4ce7f69bc4f2211ee4c5ac91fff Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 Aug 2021 16:37:05 +0800 Subject: [PATCH 123/306] refactor(emqx): update the tests for the new zone,listener config --- apps/emqx/etc/emqx.conf | 20 +- apps/emqx/src/emqx_channel.erl | 7 +- apps/emqx/src/emqx_connection.erl | 26 +- apps/emqx/src/emqx_listeners.erl | 17 +- apps/emqx/src/emqx_schema.erl | 6 +- apps/emqx/src/emqx_ws_connection.erl | 63 ++--- apps/emqx/test/emqx_access_control_SUITE.erl | 2 +- apps/emqx/test/emqx_channel_SUITE.erl | 237 ++++++++---------- apps/emqx/test/emqx_client_SUITE.erl | 4 +- apps/emqx/test/emqx_cm_SUITE.erl | 4 +- apps/emqx/test/emqx_connection_SUITE.erl | 14 +- apps/emqx/test/emqx_flapping_SUITE.erl | 4 +- apps/emqx/test/emqx_session_SUITE.erl | 2 +- apps/emqx/test/emqx_ws_connection_SUITE.erl | 22 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 2 +- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 2 +- .../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 | 2 +- .../emqx_authz/test/emqx_authz_rule_SUITE.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 2 +- .../src/lwm2m/emqx_lwm2m_protocol.erl | 2 +- .../src/stomp/emqx_stomp_channel.erl | 2 +- .../test/emqx_mgmt_listeners_api_SUITE.erl | 2 +- 25 files changed, 215 insertions(+), 253 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 251f2d0a5..8dba8b0f8 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -76,7 +76,7 @@ listeners.tcp.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 `listeners.tcp..mqtt.mountpoint` + ## For example if a clientA subscribes to "t" with `listeners.tcp..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 @@ -89,7 +89,7 @@ listeners.tcp.default { ## - %c: clientid ## - %u: username ## - ## @doc listeners.tcp..mqtt.mountpoint + ## @doc listeners.tcp..mountpoint ## ValueType: String ## Default: "" mountpoint = "" @@ -175,7 +175,7 @@ listeners.ssl.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 `listeners.ssl..mqtt.mountpoint` + ## For example if a clientA subscribes to "t" with `listeners.ssl..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 @@ -188,7 +188,7 @@ listeners.ssl.default { ## - %c: clientid ## - %u: username ## - ## @doc listeners.ssl..mqtt.mountpoint + ## @doc listeners.ssl..mountpoint ## ValueType: String ## Default: "" mountpoint = "" @@ -261,7 +261,7 @@ listeners.quic.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 `listeners.quic..mqtt.mountpoint` + ## For example if a clientA subscribes to "t" with `listeners.quic..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 @@ -274,7 +274,7 @@ listeners.quic.default { ## - %c: clientid ## - %u: username ## - ## @doc listeners.quic..mqtt.mountpoint + ## @doc listeners.quic..mountpoint ## ValueType: String ## Default: "" mountpoint = "" @@ -355,7 +355,7 @@ listeners.ws.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 `listeners.ws..mqtt.mountpoint` + ## For example if a clientA subscribes to "t" with `listeners.ws..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 @@ -368,7 +368,7 @@ listeners.ws.default { ## - %c: clientid ## - %u: username ## - ## @doc listeners.ws..mqtt.mountpoint + ## @doc listeners.ws..mountpoint ## ValueType: String ## Default: "" mountpoint = "" @@ -458,7 +458,7 @@ listeners.wss.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 `listeners.wss..mqtt.mountpoint` + ## For example if a clientA subscribes to "t" with `listeners.wss..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 @@ -471,7 +471,7 @@ listeners.wss.default { ## - %c: clientid ## - %u: username ## - ## @doc listeners.wss..mqtt.mountpoint + ## @doc listeners.wss..mountpoint ## ValueType: String ## Default: "" mountpoint = "" diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 59c6447ab..553b038f6 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -202,14 +202,15 @@ caps(#channel{clientinfo = #{zone := Zone}}) -> -spec(init(emqx_types:conninfo(), opts()) -> channel()). init(ConnInfo = #{peername := {PeerHost, _Port}, - sockname := {_Host, SockPort}}, #{zone := Zone, listener := Listener}) -> + sockname := {_Host, SockPort}}, + #{zone := Zone, listener := {Type, Listener}}) -> Peercert = maps:get(peercert, ConnInfo, undefined), Protocol = maps:get(protocol, ConnInfo, mqtt), - MountPoint = case get_mqtt_conf(Zone, mountpoint) of + MountPoint = case emqx_config:get_listener_conf(Type, Listener, [mountpoint]) of <<>> -> undefined; MP -> MP end, - QuotaPolicy = emqx_config:get_listener_conf(Zone, Listener,[rate_limit, quota], []), + QuotaPolicy = emqx_config:get_zone_conf(Zone, [quota], #{}), ClientInfo = set_peercert_infos( Peercert, #{zone => Zone, diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index dcb50fa4e..7d7f215c2 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -102,8 +102,8 @@ idle_timer :: maybe(reference()), %% Zone name zone :: atom(), - %% Listener Name - listener :: atom() + %% Listener Type and Name + listener :: {Type::atom(), Name::atom()} }). -type(state() :: #state{}). @@ -463,15 +463,15 @@ handle_msg({Passive, _Sock}, State) NState1 = check_oom(run_gc(InStats, NState)), handle_info(activate_socket, NState1); -handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{zone = Zone, - listener = Listener} = State) -> - ActiveN = get_active_n(Zone, Listener), +handle_msg(Deliver = {deliver, _Topic, _Msg}, #state{ + listener = {Type, Listener}} = State) -> + ActiveN = get_active_n(Type, 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) > get_active_n(Zone, Listener) of +handle_msg({inet_reply, _Sock, ok}, State = #state{listener = {Type, Listener}}) -> + case emqx_pd:get_counter(outgoing_pubs) > get_active_n(Type, Listener) of true -> Pubs = emqx_pd:reset_counter(outgoing_pubs), Bytes = emqx_pd:reset_counter(outgoing_bytes), @@ -820,8 +820,8 @@ activate_socket(State = #state{sockstate = closed}) -> activate_socket(State = #state{sockstate = blocked}) -> {ok, State}; activate_socket(State = #state{transport = Transport, socket = Socket, - zone = Zone, listener = Listener}) -> - ActiveN = get_active_n(Zone, Listener), + listener = {Type, Listener}}) -> + ActiveN = get_active_n(Type, Listener), case Transport:setopts(Socket, [{active, ActiveN}]) of ok -> {ok, State#state{sockstate = running}}; Error -> Error @@ -904,8 +904,6 @@ get_state(Pid) -> maps:from_list(lists:zip(record_info(fields, state), tl(tuple_to_list(State)))). -get_active_n(Zone, Listener) -> - case emqx:get_config([zones, Zone, listeners, Listener, type]) of - quic -> 100; - _ -> emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) - end. +get_active_n(quic, _Listener) -> 100; +get_active_n(Type, Listener) -> + emqx_config:get_listener_conf(Type, Listener, [tcp, active_n]). diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 375a5c990..d5c6c9173 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -134,12 +134,12 @@ stop_listener(ListenerId) -> apply_on_listener(ListenerId, fun stop_listener/3). -spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). -stop_listener(Type, ListenerName, #{type := tcp, bind := ListenOn}) -> +stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl -> esockd:close(listener_id(Type, ListenerName), ListenOn); -stop_listener(Type, ListenerName, #{type := ws}) -> +stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss -> cowboy:stop_listener(listener_id(Type, ListenerName)); -stop_listener(Type, ListenerName, #{type := quic}) -> - quicer:stop_listener(listener_id(Type, ListenerName)). +stop_listener(quic, ListenerName, _Conf) -> + quicer:stop_listener(listener_id(quic, ListenerName)). -ifndef(TEST). console_print(Fmt, Args) -> ?ULOG(Fmt, Args). @@ -156,7 +156,7 @@ do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) when Type == tcp; Type == ssl -> esockd:open(listener_id(Type, ListenerName), ListenOn, merge_default(esockd_opts(Type, Opts)), {emqx_connection, start_link, - [#{type => Type, listener => ListenerName, + [#{listener => {Type, ListenerName}, zone => zone(Opts)}]}); %% Start MQTT/WS listener @@ -189,8 +189,7 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> , peer_unidi_stream_count => 1 , peer_bidi_stream_count => 10 , zone => zone(Opts) - , type => quic - , listener => ListenerName + , listener => {quic, ListenerName} }, StreamOpts = [], quicer:start_listener(listener_id(quic, ListenerName), @@ -201,7 +200,7 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), - Opts2 = case emqx_map_lib:deep_get([rate_limit, max_conn_rate], Opts0) of + Opts2 = case emqx_config:get_zone_conf(zone(Opts0), [rate_limit, max_conn_rate]) of infinity -> Opts1; Rate -> Opts1#{max_conn_rate => Rate} end, @@ -213,7 +212,7 @@ esockd_opts(Type, Opts0) -> ws_opts(Type, ListenerName, Opts) -> WsPaths = [{maps:get(mqtt_path, Opts, "/mqtt"), emqx_ws_connection, - #{zone => zone(Opts), type => Type, listener => ListenerName}}], + #{zone => zone(Opts), listener => {Type, ListenerName}}}], Dispatch = cowboy_router:compile([{'_', WsPaths}]), ProxyProto = maps:get(proxy_protocol, Opts, false), #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 866e14d48..347f9068b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -72,7 +72,7 @@ structs() -> ["zones", "mqtt", "flapping_detect", "force_shutdown", "force_gc", "conn_congestion", "rate_limit", "quota", "listeners", "broker", "plugins", - "sysmon", "alarm", "authorization"]. + "stats", "sysmon", "alarm", "authorization"]. fields("stats") -> [ {"enable", t(boolean(), undefined, true)} @@ -132,7 +132,6 @@ fields("zone_settings") -> , {"force_shutdown", ref("force_shutdown")} , {"conn_congestion", ref("conn_congestion")} , {"force_gc", ref("force_gc")} - , {"listeners", t("listeners")} ]; fields("rate_limit") -> @@ -348,9 +347,8 @@ base_listener() -> [ {"bind", t(union(ip_port(), integer()))} , {"acceptors", t(integer(), undefined, 16)} , {"max_connections", maybe_infinity(integer(), infinity)} - , {"rate_limit", ref("rate_limit")} , {"mountpoint", t(binary(), undefined, <<>>)} - , {"zone", t(binary(), undefined, undefined)} + , {"zone", t(atom(), undefined, default)} ]. %% utils diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index b76567f6c..32a81c26a 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -85,8 +85,8 @@ idle_timer :: maybe(reference()), %% Zone name zone :: atom(), - %% Listener Name - listener :: atom() + %% Listener Type and Name + listener :: {Type::atom(), Name::atom()} }). -type(state() :: #state{}). @@ -173,12 +173,12 @@ call(WsPid, Req, Timeout) when is_pid(WsPid) -> %% WebSocket callbacks %%-------------------------------------------------------------------- -init(Req, #{zone := Zone, listener := Listener} = Opts) -> +init(Req, #{listener := {Type, Listener}} = Opts) -> %% WS Transport Idle Timeout - 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) + WsOpts = #{compress => get_ws_opts(Type, Listener, compress), + deflate_opts => get_ws_opts(Type, Listener, deflate_opts), + max_frame_size => get_ws_opts(Type, Listener, max_frame_size), + idle_timeout => get_ws_opts(Type, Listener, idle_timeout) }, case check_origin_header(Req, Opts) of {error, Message} -> @@ -187,17 +187,17 @@ init(Req, #{zone := Zone, listener := Listener} = Opts) -> ok -> parse_sec_websocket_protocol(Req, Opts, WsOpts) end. -parse_sec_websocket_protocol(Req, #{zone := Zone, listener := Listener} = Opts, WsOpts) -> +parse_sec_websocket_protocol(Req, #{listener := {Type, Listener}} = Opts, WsOpts) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> - case get_ws_opts(Zone, Listener, fail_if_no_subprotocol) of + case get_ws_opts(Type, Listener, fail_if_no_subprotocol) of true -> {ok, cowboy_req:reply(400, Req), WsOpts}; false -> {cowboy_websocket, Req, [Req, Opts], WsOpts} end; Subprotocols -> - SupportedSubprotocols = get_ws_opts(Zone, Listener, supported_subprotocols), + SupportedSubprotocols = get_ws_opts(Type, Listener, supported_subprotocols), NSupportedSubprotocols = [list_to_binary(Subprotocol) || Subprotocol <- SupportedSubprotocols], case pick_subprotocol(Subprotocols, NSupportedSubprotocols) of @@ -221,29 +221,29 @@ pick_subprotocol([Subprotocol | Rest], SupportedSubprotocols) -> pick_subprotocol(Rest, SupportedSubprotocols) end. -parse_header_fun_origin(Req, #{zone := Zone, listener := Listener}) -> +parse_header_fun_origin(Req, #{listener := {Type, Listener}}) -> case cowboy_req:header(<<"origin">>, Req) of undefined -> - case get_ws_opts(Zone, Listener, allow_origin_absence) of + case get_ws_opts(Type, Listener, allow_origin_absence) of true -> ok; false -> {error, origin_header_cannot_be_absent} end; Value -> - case lists:member(Value, get_ws_opts(Zone, Listener, check_origins)) of + case lists:member(Value, get_ws_opts(Type, Listener, check_origins)) of true -> ok; false -> {origin_not_allowed, Value} end end. -check_origin_header(Req, #{zone := Zone, listener := Listener} = Opts) -> - case get_ws_opts(Zone, Listener, check_origin_enable) of +check_origin_header(Req, #{listener := {Type, Listener}} = Opts) -> + case get_ws_opts(Type, Listener, check_origin_enable) of true -> parse_header_fun_origin(Req, Opts); false -> ok end. -websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> +websocket_init([Req, #{zone := Zone, listener := {Type, Listener}} = Opts]) -> {Peername, Peercert} = - case emqx_config:get_listener_conf(Zone, Listener, [proxy_protocol]) andalso + case emqx_config:get_listener_conf(Type, Listener, [proxy_protocol]) andalso maps:get(proxy_header, Req) of #{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} -> SourceName = {SrcAddr, SrcPort}, @@ -278,7 +278,7 @@ websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> conn_mod => ?MODULE }, Limiter = emqx_limiter:init(Zone, undefined, undefined, []), - MQTTPiggyback = get_ws_opts(Zone, Listener, mqtt_piggyback), + MQTTPiggyback = get_ws_opts(Type, Listener, mqtt_piggyback), FrameOpts = #{ strict_mode => emqx_config:get_zone_conf(Zone, [mqtt, strict_mode]), max_size => emqx_config:get_zone_conf(Zone, [mqtt, max_packet_size]) @@ -317,7 +317,7 @@ websocket_init([Req, #{zone := Zone, listener := Listener} = Opts]) -> idle_timeout = IdleTimeout, idle_timer = IdleTimer, zone = Zone, - listener = Listener + listener = {Type, Listener} }, hibernate}. websocket_handle({binary, Data}, State) when is_list(Data) -> @@ -370,8 +370,8 @@ websocket_info({check_gc, Stats}, State) -> return(check_oom(run_gc(Stats, State))); websocket_info(Deliver = {deliver, _Topic, _Msg}, - State = #state{zone = Zone, listener = Listener}) -> - ActiveN = emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]), + State = #state{listener = {Type, Listener}}) -> + ActiveN = get_active_n(Type, Listener), Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)], with_channel(handle_deliver, [Delivers], State); @@ -558,12 +558,12 @@ parse_incoming(Data, State = #state{parse_state = ParseState}) -> %% Handle incoming packet %%-------------------------------------------------------------------- -handle_incoming(Packet, State = #state{zone = Zone, listener = Listener}) +handle_incoming(Packet, State = #state{listener = {Type, 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) > - emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of + get_active_n(Type, Listener) of true -> postpone({cast, rate_limit}, State); false -> State end, @@ -595,12 +595,12 @@ with_channel(Fun, Args, State = #state{channel = Channel}) -> %%-------------------------------------------------------------------- handle_outgoing(Packets, State = #state{mqtt_piggyback = MQTTPiggyback, - zone = Zone, listener = Listener}) -> + listener = {Type, 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) > - emqx_config:get_listener_conf(Zone, Listener, [tcp, active_n]) of + get_active_n(Type, Listener) of true -> Stats = #{cnt => emqx_pd:reset_counter(outgoing_pubs), oct => emqx_pd:reset_counter(outgoing_bytes) @@ -749,10 +749,10 @@ classify([Event|More], Packets, Cmds, Events) -> trigger(Event) -> erlang:send(self(), Event). -get_peer(Req, #{zone := Zone, listener := Listener}) -> +get_peer(Req, #{listener := {Type, Listener}}) -> {PeerAddr, PeerPort} = cowboy_req:peer(Req), AddrHeader = cowboy_req:header( - get_ws_opts(Zone, Listener, proxy_address_header), Req, <<>>), + get_ws_opts(Type, Listener, proxy_address_header), Req, <<>>), ClientAddr = case string:tokens(binary_to_list(AddrHeader), ", ") of [] -> undefined; @@ -766,7 +766,7 @@ get_peer(Req, #{zone := Zone, listener := Listener}) -> PeerAddr end, PortHeader = cowboy_req:header( - get_ws_opts(Zone, Listener, proxy_port_header), Req, <<>>), + get_ws_opts(Type, Listener, proxy_port_header), Req, <<>>), ClientPort = case string:tokens(binary_to_list(PortHeader), ", ") of [] -> undefined; @@ -787,5 +787,8 @@ 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]). +get_ws_opts(Type, Listener, Key) -> + emqx_config:get_listener_conf(Type, Listener, [websocket, Key]). + +get_active_n(Type, Listener) -> + emqx_config:get_listener_conf(Type, Listener, [tcp, active_n]). \ No newline at end of file diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index 8cfa17523..00a1f9fbe 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -46,7 +46,7 @@ t_authorize(_) -> clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> maps:merge(#{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index dfbe56916..031f89612 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -27,149 +27,112 @@ all() -> emqx_ct:all(?MODULE). +force_gc_conf() -> + #{bytes => 16777216,count => 16000,enable => true}. + +force_shutdown_conf() -> + #{enable => true,max_heap_size => 4194304, max_message_queue_len => 1000}. + +rate_limit_conf() -> + #{conn_bytes_in => ["100KB","10s"], + conn_messages_in => ["100","10s"], + max_conn_rate => 1000, + quota => + #{conn_messages_routing => infinity, + overall_messages_routing => infinity}}. + +rpc_conf() -> + #{async_batch_size => 256,authentication_timeout => 5000, + call_receive_timeout => 15000,connect_timeout => 5000, + mode => async,port_discovery => stateless, + send_timeout => 5000,socket_buffer => 1048576, + socket_keepalive_count => 9,socket_keepalive_idle => 900, + socket_keepalive_interval => 75,socket_recbuf => 1048576, + socket_sndbuf => 1048576,tcp_client_num => 1, + tcp_server_port => 5369}. + mqtt_conf() -> - #{await_rel_timeout => 300000, - 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 => lowest, - mqueue_priorities => #{}, - mqueue_store_qos0 => true, - peer_cert_as_clientid => disabled, - peer_cert_as_username => disabled, - response_information => [], - retain_available => true, - retry_interval => 30000, - server_keepalive => disabled, - session_expiry_interval => 7200000, - shared_subscription => true, - strict_mode => false, - upgrade_qos => false, - use_username_as_clientid => false, - wildcard_subscription => true}. + #{await_rel_timeout => 300000,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,mqueue_default_priority => lowest, + mqueue_priorities => disabled,mqueue_store_qos0 => true, + peer_cert_as_clientid => disabled, + peer_cert_as_username => disabled, + response_information => [],retain_available => true, + retry_interval => 30000,server_keepalive => disabled, + session_expiry_interval => 7200000, + 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}. + zone => default, + access_rules => ["allow all"], + bind => {{0,0,0,0},1883}, + max_connections => 1024000,mountpoint => <<>>, + proxy_protocol => false,proxy_protocol_timeout => 3000, + tcp => #{ + active_n => 100,backlog => 1024,buffer => 4096, + high_watermark => 1048576,nodelay => false, + reuseaddr => true,send_timeout => 15000, + send_timeout_close => true}}. 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"]}}. + zone => default, + access_rules => ["allow all"], + bind => {{0,0,0,0},8083}, + max_connections => 1024000,mountpoint => <<>>, + proxy_protocol => false,proxy_protocol_timeout => 3000, + tcp => + #{active_n => 100,backlog => 1024,buffer => 4096, + high_watermark => 1048576,nodelay => false, + reuseaddr => true,send_timeout => 15000, + send_timeout_close => true}, + 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 => - #{ authorization => #{ - 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 => false, - 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()} - } - } +listeners_conf() -> + #{tcp => #{default => listener_mqtt_tcp_conf()}, + ws => #{default => listener_mqtt_ws_conf()} }. -set_default_zone_conf() -> - emqx_config:put(default_zone_conf()). +stats_conf() -> + #{enable => true}. + +zone_conf() -> + #{}. + +basic_conf() -> + #{rate_limit => rate_limit_conf(), + force_gc => force_gc_conf(), + force_shutdown => force_shutdown_conf(), + mqtt => mqtt_conf(), + rpc => rpc_conf(), + stats => stats_conf(), + listeners => listeners_conf(), + zones => zone_conf() + }. + +set_test_listenser_confs() -> + emqx_config:put(basic_conf()). %%-------------------------------------------------------------------- %% CT Callbacks @@ -211,7 +174,7 @@ end_per_suite(_Config) -> ]). init_per_testcase(_TestCase, Config) -> - set_default_zone_conf(), + set_test_listenser_confs(), Config. end_per_testcase(_TestCase, Config) -> @@ -917,7 +880,7 @@ t_ws_cookie_init(_) -> conn_mod => emqx_ws_connection, ws_cookie => WsCookie }, - Channel = emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), + Channel = emqx_channel:init(ConnInfo, #{zone => default, listener => {tcp, default}}), ?assertMatch(#{ws_cookie := WsCookie}, emqx_channel:info(clientinfo, Channel)). %%-------------------------------------------------------------------- @@ -942,7 +905,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), + emqx_channel:init(ConnInfo, #{zone => default, listener => {tcp, default}}), maps:merge(#{clientinfo => clientinfo(), session => session(), conn_state => connected @@ -951,7 +914,7 @@ channel(InitFields) -> clientinfo() -> clientinfo(#{}). clientinfo(InitProps) -> maps:merge(#{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index c6a450471..117a0f5b9 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -79,8 +79,8 @@ groups() -> init_per_suite(Config) -> emqx_ct_helpers:boot_modules(all), emqx_ct_helpers:start_apps([]), - emqx_config:put_listener_conf(default, mqtt_ssl, [ssl, verify], verify_peer), - emqx_listeners:restart_listener('default:mqtt_ssl'), + emqx_config:put_listener_conf(ssl, default, [ssl, verify], verify_peer), + emqx_listeners:restart_listener('ssl:default'), Config. end_per_suite(_Config) -> diff --git a/apps/emqx/test/emqx_cm_SUITE.erl b/apps/emqx/test/emqx_cm_SUITE.erl index 75d0a899c..d492edd0e 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 => default, listener => mqtt_tcp, + ClientInfo = #{zone => default, listener => {tcp, default}, 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 => default, listener => mqtt_tcp, + ClientInfo = #{zone => default, listener => {tcp, default}, clientid => ClientId, username => <<"username">>, peerhost => {127,0,0,1}}, diff --git a/apps/emqx/test/emqx_connection_SUITE.erl b/apps/emqx/test/emqx_connection_SUITE.erl index 0d5114325..5784ad6aa 100644 --- a/apps/emqx/test/emqx_connection_SUITE.erl +++ b/apps/emqx/test/emqx_connection_SUITE.erl @@ -57,7 +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(), + emqx_channel_SUITE:set_test_listenser_confs(), Config. end_per_suite(_Config) -> @@ -219,9 +219,9 @@ t_handle_msg_deliver(_) -> t_handle_msg_inet_reply(_) -> ok = meck:expect(emqx_pd, get_counter, fun(_) -> 10 end), - emqx_config:put_listener_conf(default, mqtt_tcp, [tcp, active_n], 0), + emqx_config:put_listener_conf(tcp, default, [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), + emqx_config:put_listener_conf(tcp, default, [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())). @@ -456,7 +456,7 @@ 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, - maps:merge(Opts, #{zone => default, listener => mqtt_tcp})), + maps:merge(Opts, #{zone => default, listener => {tcp, default}})), TestFun(CPid), TrapExit orelse emqx_connection:stop(CPid), ok. @@ -479,7 +479,7 @@ st(InitFields) when is_map(InitFields) -> st(InitFields, #{}). st(InitFields, ChannelFields) when is_map(InitFields) -> St = emqx_connection:init_state(emqx_transport, sock, #{zone => default, - listener => mqtt_tcp}), + listener => {tcp, default}}), maps:fold(fun(N, V, S) -> emqx_connection:set_field(N, V, S) end, emqx_connection:set_field(channel, channel(ChannelFields), St), InitFields @@ -500,7 +500,7 @@ channel(InitFields) -> expiry_interval => 0 }, ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -513,7 +513,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_tcp}), + emqx_channel:init(ConnInfo, #{zone => default, listener => {tcp, default}}), maps:merge(#{clientinfo => ClientInfo, session => Session, conn_state => connected diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index eca276b84..5ac6b9cdf 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -40,7 +40,7 @@ end_per_suite(_Config) -> t_detect_check(_) -> ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, clientid => <<"client007">>, peerhost => {127,0,0,1} }, @@ -64,7 +64,7 @@ t_detect_check(_) -> t_expired_detecting(_) -> ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, clientid => <<"client008">>, peerhost => {127,0,0,1}}, false = emqx_flapping:detect(ClientInfo), diff --git a/apps/emqx/test/emqx_session_SUITE.erl b/apps/emqx/test/emqx_session_SUITE.erl index 1aa5b0196..f52dacc14 100644 --- a/apps/emqx/test/emqx_session_SUITE.erl +++ b/apps/emqx/test/emqx_session_SUITE.erl @@ -29,7 +29,7 @@ all() -> emqx_ct:all(?MODULE). %%-------------------------------------------------------------------- init_per_suite(Config) -> - emqx_channel_SUITE:set_default_zone_conf(), + emqx_channel_SUITE:set_test_listenser_confs(), ok = meck:new([emqx_hooks, emqx_metrics, emqx_broker], [passthrough, no_history, no_link]), ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end), diff --git a/apps/emqx/test/emqx_ws_connection_SUITE.erl b/apps/emqx/test/emqx_ws_connection_SUITE.erl index b25f051eb..767a7994e 100644 --- a/apps/emqx/test/emqx_ws_connection_SUITE.erl +++ b/apps/emqx/test/emqx_ws_connection_SUITE.erl @@ -48,7 +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(), + emqx_channel_SUITE:set_test_listenser_confs(), %% Mock cowboy_req ok = meck:new(cowboy_req, [passthrough, no_history, no_link]), ok = meck:expect(cowboy_req, header, fun(_, _, _) -> <<>> end), @@ -119,7 +119,7 @@ t_info(_) -> } = SockInfo. set_ws_opts(Key, Val) -> - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, Key], Val). + emqx_config:put_listener_conf(ws, default, [websocket, Key], Val). t_header(_) -> ok = meck:expect(cowboy_req, header, @@ -127,7 +127,7 @@ t_header(_) -> (<<"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}]), + {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => {ws, default}}]), WsPid = spawn(fun() -> receive {call, From, info} -> gen_server:reply(From, ?ws_conn:info(St)) @@ -222,8 +222,8 @@ t_ws_sub_protocols_mqtt_equivalents(_) -> start_ws_client(#{protocols => [<<"not-mqtt">>]})). t_ws_check_origin(_) -> - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origin_enable], true), - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origins], + emqx_config:put_listener_conf(ws, default, [websocket, check_origin_enable], true), + emqx_config:put_listener_conf(ws, default, [websocket, check_origins], [<<"http://localhost:18083">>]), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, @@ -234,8 +234,8 @@ t_ws_check_origin(_) -> headers => [{<<"origin">>, <<"http://localhost:18080">>}]})). t_ws_non_check_origin(_) -> - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origin_enable], false), - emqx_config:put_listener_conf(default, mqtt_ws, [websocket, check_origins], []), + emqx_config:put_listener_conf(ws, default, [websocket, check_origin_enable], false), + emqx_config:put_listener_conf(ws, default, [websocket, check_origins], []), {ok, _} = application:ensure_all_started(gun), ?assertMatch({gun_upgrade, _}, start_ws_client(#{protocols => [<<"mqtt">>], @@ -245,7 +245,7 @@ t_ws_non_check_origin(_) -> headers => [{<<"origin">>, <<"http://localhost:18080">>}]})). t_init(_) -> - Opts = #{listener => mqtt_ws, zone => default}, + Opts = #{listener => {ws, default}, zone => default}, ok = meck:expect(cowboy_req, parse_header, fun(_, req) -> undefined end), ok = meck:expect(cowboy_req, reply, fun(_, Req) -> Req end), {ok, req, _} = ?ws_conn:init(req, Opts), @@ -438,7 +438,7 @@ t_shutdown(_) -> st() -> st(#{}). st(InitFields) when is_map(InitFields) -> - {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => mqtt_ws}]), + {ok, St, _} = ?ws_conn:websocket_init([req, #{zone => default, listener => {ws, default}}]), maps:fold(fun(N, V, S) -> ?ws_conn:set_field(N, V, S) end, ?ws_conn:set_field(channel, channel(), St), InitFields @@ -459,7 +459,7 @@ channel(InitFields) -> expiry_interval => 0 }, ClientInfo = #{zone => default, - listener => mqtt_ws, + listener => {ws, default}, protocol => mqtt, peerhost => {127,0,0,1}, clientid => <<"clientid">>, @@ -472,7 +472,7 @@ channel(InitFields) -> maps:fold(fun(Field, Value, Channel) -> emqx_channel:set_field(Field, Value, Channel) end, - emqx_channel:init(ConnInfo, #{zone => default, listener => mqtt_ws}), + emqx_channel:init(ConnInfo, #{zone => default, listener => {ws, default}}), maps:merge(#{clientinfo => ClientInfo, session => Session, conn_state => connected diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 0be04d6cf..eb7f0291a 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -105,7 +105,7 @@ t_authenticator(_) -> t_authenticate(_) -> ClientInfo = #{zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, username => <<"myuser">>, password => <<"mypass">>}, ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index bbd4232dd..a455d0ab8 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -67,7 +67,7 @@ t_authz(_) -> protocol => mqtt, mountpoint => <<"fake">>, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, 204, fake_headers} end), diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index dac106b37..1ad7b4f7a 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -80,19 +80,19 @@ t_authz(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, peerhost => {192,168,0,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, 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 0fba033a6..3c0320a42 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -77,19 +77,19 @@ t_authz(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, peerhost => {192,168,0,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, 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 d21caa223..43ea271a6 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -77,19 +77,19 @@ t_authz(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test_clientid">>, username => <<"test_username">>, peerhost => {192,168,0,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test_clientid">>, username => <<"fake_username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, 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 0da931cc7..46ed9579e 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -70,7 +70,7 @@ t_authz(_) -> username => <<"username">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end), diff --git a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl index e6e450a63..ff215354a 100644 --- a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl @@ -70,25 +70,25 @@ t_match(_) -> username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo2 = #{clientid => <<"test">>, username => <<"test">>, peerhost => {192,168,1,10}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo3 = #{clientid => <<"test">>, username => <<"fake">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ClientInfo4 = #{clientid => <<"fake">>, username => <<"test">>, peerhost => {127,0,0,1}, zone => default, - listener => mqtt_tcp + listener => {tcp, default} }, ?assertEqual({matched, deny}, diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index b5de6cb9a..8022c3797 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -69,7 +69,7 @@ authenticate(_Ctx = #{auth := undefined}, ClientInfo) -> authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> ClientInfo = ClientInfo0#{ zone => default, - listener => mqtt_tcp, + listener => {tcp, default}, chain_id => ChainId }, case emqx_access_control:authenticate(ClientInfo) of diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl index f96fe714c..1c8b581a4 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl @@ -455,7 +455,7 @@ clientinfo(#lwm2m_state{peername = {PeerHost, _}, endpoint_name = EndpointName, mountpoint = Mountpoint}) -> #{zone => default, - listener => mqtt_tcp, %% FIXME: this won't work + listener => {tcp, default}, %% FIXME: this won't work protocol => lwm2m, peerhost => PeerHost, sockport => 5683, %% FIXME: diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index b1a74375d..250f43988 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -113,7 +113,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, ClientInfo = setting_peercert_infos( Peercert, #{ zone => default - , listener => mqtt_tcp + , listener => {tcp, default} , protocol => stomp , peerhost => PeerHost , sockport => SockPort diff --git a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl index 07697243e..51ac403b2 100644 --- a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl @@ -61,7 +61,7 @@ t_get_node_listeners(_) -> get_api(Path). t_manage_listener(_) -> - ID = "default:mqtt_tcp", + ID = "tcp:default", manage_listener(ID, "stop", false), manage_listener(ID, "start", true), manage_listener(ID, "restart", true). From 092f29fecdabe1cc80fb7bbad777fe1a7bdbeb48 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 Aug 2021 17:55:14 +0800 Subject: [PATCH 124/306] refactor(CI): update the ENVs for new listener,zone configs --- .ci/build_packages/tests.sh | 6 +++--- .ci/docker-compose-file/conf.cluster.env | 4 ++-- deploy/charts/emqx/templates/StatefulSet.yaml | 12 ++++++------ deploy/charts/emqx/templates/service.yaml | 4 ++-- deploy/docker/README.md | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index c01d07d0a..d3a2a6858 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -36,9 +36,9 @@ emqx_test(){ "zip") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.zip) unzip -q "${PACKAGE_PATH}/${packagename}" - export EMQX_ZONE__EXTERNAL__SERVER_KEEPALIVE=60 \ + export EMQX_ZONES__DEFAULT__MQTT__SERVER_KEEPALIVE=60 \ EMQX_MQTT__MAX_TOPIC_ALIAS=10 - [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_ZONES__DEFAULT__LISTENERS__MQTT_QUIC__ENABLED=false + [[ $(arch) == *arm* || $(arch) == aarch64 ]] && export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false # sed -i '/emqx_telemetry/d' "${PACKAGE_PATH}"/emqx/data/loaded_plugins echo "running ${packagename} start" @@ -119,7 +119,7 @@ run_test(){ if [ -f "$emqx_env_vars" ]; then tee -a "$emqx_env_vars" < zones.default.listeners.mqtt_ssl.acceptors +EMQX_LISTENERS__SSL__DEFAULT__ACCEPTORS <--> listeners.ssl.default.acceptors EMQX_ZONES__DEFAULT__MQTT__MAX_PACKET_SIZE <--> zones.default.mqtt.max_packet_size ``` @@ -87,7 +87,7 @@ If set ``EMQX_NAME`` and ``EMQX_HOST``, and unset ``EMQX_NODE_NAME``, ``EMQX_NOD For example, set mqtt tcp port to 1883 -``docker run -d --name emqx -e EMQX_ZONES__DEFAULT__LISTENERS__MQTT_TCP__BIND=1883 -p 18083:18083 -p 1883:1883 emqx/emqx:latest`` +``docker run -d --name emqx -e EMQX__LISTENERS__TCP__DEFAULT__BIND=1883 -p 18083:18083 -p 1883:1883 emqx/emqx:latest`` #### EMQ Loaded Modules Configuration @@ -169,7 +169,7 @@ Assume you are using redis auth plugin, for example: #EMQX_RETAINER.MAX_PAYLOAD_SIZE = 1MB docker run -d --name emqx -p 18083:18083 -p 1883:1883 -p 4369:4369 \ - -e EMQX_LISTENER__TCP__EXTERNAL=1883 \ + -e EMQX_LISTENERS__TCP__DEFAULT=1883 \ -e EMQX_LOADED_PLUGINS="emqx_retainer" \ -e EMQX_RETAINER__STORAGE_TYPE = "ram" \ -e EMQX_RETAINER__MAX_PAYLOAD_SIZE = 1MB \ From 4ea451e2075bbb10c71d174fec6cf3b4fd3d4217 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 Aug 2021 18:05:13 +0800 Subject: [PATCH 125/306] fix(emqx): update the type spec for listener --- apps/emqx/src/emqx_channel.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 553b038f6..e25a9c8d6 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -99,7 +99,7 @@ -type(channel() :: #channel{}). --type(opts() :: #{zone := atom(), listener := atom(), atom() => term()}). +-type(opts() :: #{zone := atom(), listener := {Type::atom(), Name::atom()}, atom() => term()}). -type(conn_state() :: idle | connecting | connected | reauthenticating | disconnected). From 7b63f7f18b9458e9a929c6ab2b11b3de67386bfb Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 Aug 2021 20:20:39 +0800 Subject: [PATCH 126/306] refactor(emqx_mangement): update emqx_mangement for new listener,zone configs --- apps/emqx/src/emqx_listeners.erl | 8 ++++++-- apps/emqx_management/src/emqx_mgmt_api_listeners.erl | 6 +++--- .../test/emqx_mgmt_listeners_api_SUITE.erl | 3 +-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index d5c6c9173..521366877 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -37,6 +37,10 @@ , has_enabled_listener_conf_by_type/1 ]). +-export([ listener_id/2 + , parse_listener_id/1 + ]). + %% @doc List configured listeners. -spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]). list() -> @@ -264,7 +268,7 @@ format_addr({Addr, Port}) when is_tuple(Addr) -> listener_id(Type, ListenerName) -> list_to_atom(lists:append([atom_to_list(Type), ":", atom_to_list(ListenerName)])). -decode_listener_id(Id) -> +parse_listener_id(Id) -> try [Zone, Listen] = string:split(atom_to_list(Id), ":", leading), {list_to_existing_atom(Zone), list_to_existing_atom(Listen)} @@ -299,7 +303,7 @@ has_enabled_listener_conf_by_type(Type) -> end, do_list()). apply_on_listener(ListenerId, Do) -> - {Type, ListenerName} = decode_listener_id(ListenerId), + {Type, ListenerName} = parse_listener_id(ListenerId), case emqx_config:find_listener_conf(Type, ListenerName, []) of {not_found, _, _} -> error({listener_config_not_found, Type, ListenerName}); {ok, Conf} -> Do(Type, ListenerName, Conf) diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index b7a69afe1..fb098634f 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -277,15 +277,15 @@ format({error, Reason}) -> {error, Reason}; format({ID, Conf}) -> + {Type, _Name} = emqx_listeners:parse_listener_id(ID), #{ id => ID, node => maps:get(node, Conf), acceptors => maps:get(acceptors, Conf), max_conn => maps:get(max_connections, Conf), - type => maps:get(type, Conf), + type => Type, listen_on => list_to_binary(esockd:to_string(maps:get(bind, Conf))), - running => trans_running(Conf), - auth => maps:get(enable, maps:get(auth, Conf)) + running => trans_running(Conf) }. trans_running(Conf) -> case maps:get(running, Conf) of diff --git a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl index 51ac403b2..93632dda4 100644 --- a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl @@ -117,8 +117,7 @@ comparison_listener(Local, Response) -> ?assertEqual(maps:get(acceptors, Local), maps:get(<<"acceptors">>, Response)), ?assertEqual(maps:get(max_conn, Local), maps:get(<<"max_conn">>, Response)), ?assertEqual(maps:get(listen_on, Local), maps:get(<<"listen_on">>, Response)), - ?assertEqual(maps:get(running, Local), maps:get(<<"running">>, Response)), - ?assertEqual(maps:get(auth, Local), maps:get(<<"auth">>, Response)). + ?assertEqual(maps:get(running, Local), maps:get(<<"running">>, Response)). listener_stats(Listener, Stats) -> From 005332d45de743e17cdd830641458ae755d47a35 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 25 Aug 2021 22:59:53 +0800 Subject: [PATCH 127/306] fix(config): do not allow default values for configs in zones --- apps/emqx/etc/emqx.conf | 3 +++ apps/emqx/src/emqx_schema.erl | 29 ++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 8dba8b0f8..c85e7aa29 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -979,6 +979,9 @@ quota { ## - `flapping_detect.*` ## - `force_shutdown.*` ## - `conn_congestion.*` +## - `rate_limit.*` +## - `quota.*` +## - `force_gc.*` ## ## syntax: zones. ## example: zones.my_zone diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 347f9068b..3bbeb1d07 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -126,19 +126,14 @@ fields("zones") -> [ {"$name", ref("zone_settings")}]; fields("zone_settings") -> - [ {"mqtt", ref("mqtt")} - , {"stats", ref("stats")} - , {"flapping_detect", ref("flapping_detect")} - , {"force_shutdown", ref("force_shutdown")} - , {"conn_congestion", ref("conn_congestion")} - , {"force_gc", ref("force_gc")} - ]; + Fields = ["mqtt", "stats", "authorization", "flapping_detect", "force_shutdown", + "conn_congestion", "rate_limit", "quota", "force_gc"], + [{F, ref("strip_default:" ++ F)} || F <- Fields]; fields("rate_limit") -> [ {"max_conn_rate", maybe_infinity(integer(), 1000)} , {"conn_messages_in", maybe_infinity(comma_separated_list())} , {"conn_bytes_in", maybe_infinity(comma_separated_list())} - , {"quota", ref("quota")} ]; fields("quota") -> @@ -334,7 +329,10 @@ fields("alarm") -> [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} , {"size_limit", t(integer(), undefined, 1000)} , {"validity_period", t(duration(), undefined, "24h")} - ]. + ]; + +fields("strip_default:" ++ Name) -> + strip_default(fields(Name)). mqtt_listener() -> base_listener() ++ @@ -547,6 +545,19 @@ to_erl_cipher_suite(Str) -> Cipher -> Cipher end. +strip_default(Fields) -> + [do_strip_default(F) || F <- Fields]. + +do_strip_default({Name, #{type := {ref, Ref}}}) -> + {Name, nullable_no_def(ref("strip_default:" ++ Ref))}; +do_strip_default({Name, #{type := {ref, _Mod, Ref}}}) -> + {Name, nullable_no_def(ref("strip_default:" ++ Ref))}; +do_strip_default({Name, Type}) -> + {Name, nullable_no_def(Type)}. + +nullable_no_def(Type) when is_map(Type) -> + Type#{default => undefined, nullable => true}. + to_atom(Atom) when is_atom(Atom) -> Atom; to_atom(Str) when is_list(Str) -> From 6d7d94c45233049d99afccef0bcf86c86dc8a650 Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 26 Aug 2021 10:08:01 +0800 Subject: [PATCH 128/306] fix: topic rewrite api method --- apps/emqx_modules/src/emqx_rewrite_api.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl index 887e2148e..9f9b588b4 100644 --- a/apps/emqx_modules/src/emqx_rewrite_api.erl +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -49,7 +49,7 @@ rewrite_api() -> <<"200">> => object_array_schema(properties(), <<"List all rewrite rules">>) } }, - post => #{ + put => #{ description => <<"Update topic rewrite">>, 'requestBody' => object_array_schema(properties()), responses => #{ @@ -63,7 +63,7 @@ rewrite_api() -> topic_rewrite(get, _Params) -> {200, emqx_rewrite:list()}; -topic_rewrite(post, #{body := Body}) -> +topic_rewrite(put, #{body := Body}) -> case length(Body) < ?MAX_RULES_LIMIT of true -> ok = emqx_rewrite:update(Body), From bcd15e93889b702aa4c2a1e3a97d409bbae4d7f3 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Thu, 26 Aug 2021 15:11:41 +0800 Subject: [PATCH 129/306] fix: delayed message api doc (#5569) * fix: delayed message api doc & add delayed message internal & remaning params --- apps/emqx_modules/src/emqx_delayed.erl | 24 +++++++++++++------ apps/emqx_modules/src/emqx_delayed_api.erl | 9 ++++--- apps/emqx_modules/test/emqx_delayed_SUITE.erl | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index f7de5d69d..b773f04ac 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -50,7 +50,7 @@ , delete_delayed_message/1 ]). --record(delayed_message, {key, msg}). +-record(delayed_message, {key, delayed, msg}). -define(TAB, ?MODULE). -define(SERVER, ?MODULE). @@ -78,19 +78,19 @@ on_message_publish(Msg = #message{ timestamp = Ts }) -> [Delay, Topic1] = binary:split(Topic, <<"/">>), - PubAt = case binary_to_integer(Delay) of + {PubAt, Delayed} = case binary_to_integer(Delay) of Interval when Interval < ?MAX_INTERVAL -> - Interval + erlang:round(Ts / 1000); + {Interval + erlang:round(Ts / 1000), Interval}; Timestamp -> %% Check malicious timestamp? case (Timestamp - erlang:round(Ts / 1000)) > ?MAX_INTERVAL of true -> error(invalid_delayed_timestamp); - false -> Timestamp + false -> {Timestamp, Timestamp - erlang:round(Ts / 1000)} end end, PubMsg = Msg#message{topic = Topic1}, Headers = PubMsg#message.headers, - case store(#delayed_message{key = {PubAt, Id}, msg = PubMsg}) of + case store(#delayed_message{key = {PubAt, Id}, delayed = Delayed, msg = PubMsg}) of ok -> ok; {error, Error} -> ?LOG(error, "Store delayed message fail: ~p", [Error]) @@ -128,15 +128,22 @@ list(Params) -> format_delayed(Delayed) -> format_delayed(Delayed, false). -format_delayed(#delayed_message{key = {TimeStamp, Id}, +format_delayed(#delayed_message{key = {ExpectTimeStamp, Id}, delayed = Delayed, msg = #message{topic = Topic, from = From, headers = #{username := Username}, qos = Qos, + timestamp = PublishTimeStamp, payload = Payload}}, WithPayload) -> + PublishTime = to_rfc3339(PublishTimeStamp div 1000), + ExpectTime = to_rfc3339(ExpectTimeStamp), + RemainingTime = ExpectTimeStamp - erlang:system_time(second), Result = #{ id => emqx_guid:to_hexstr(Id), - publish_time => list_to_binary(calendar:system_time_to_rfc3339(TimeStamp, [{unit, second}])), + publish_at => PublishTime, + delayed_interval => Delayed, + delayed_remaining => RemainingTime, + expected_at => ExpectTime, topic => Topic, qos => Qos, from_clientid => From, @@ -149,6 +156,9 @@ format_delayed(#delayed_message{key = {TimeStamp, Id}, Result end. +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])). + get_delayed_message(Id0) -> Id = emqx_guid:from_hexstr(Id0), Ms = [{{delayed_message,{'_',Id},'_'},[],['$_']}], diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index e99242206..24f4822ab 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -59,12 +59,15 @@ properties() -> [?PAYLOAD_TOO_LARGE, ?MAX_PAYLOAD_LENGTH]), properties([ {id, integer, <<"Message Id (MQTT message id hash)">>}, - {publish_time, string, <<"publish time, rfc 3339">>}, + {publish_at, string, <<"Client publish message time, rfc 3339">>}, + {delayed_interval, integer, <<"Delayed interval, second">>}, + {delayed_remaining, integer, <<"Delayed remaining, second">>}, + {expected_at, string, <<"Expect publish time, rfc 3339">>}, {topic, string, <<"Topic">>}, {qos, string, <<"QoS">>}, {payload, string, iolist_to_binary(PayloadDesc)}, - {form_clientid, string, <<"Form ClientId">>}, - {form_username, string, <<"Form Username">>} + {from_clientid, string, <<"From ClientId">>}, + {from_username, string, <<"From Username">>} ]). parameters() -> diff --git a/apps/emqx_modules/test/emqx_delayed_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_SUITE.erl index a9af83b1d..3e386ea1d 100644 --- a/apps/emqx_modules/test/emqx_delayed_SUITE.erl +++ b/apps/emqx_modules/test/emqx_delayed_SUITE.erl @@ -21,7 +21,7 @@ -compile(export_all). -compile(nowarn_export_all). --record(delayed_message, {key, msg}). +-record(delayed_message, {key, delayed, msg}). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). From 53e386ad4ece13d10690057ef963825371c55bff Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Thu, 19 Aug 2021 22:55:38 +0800 Subject: [PATCH 130/306] feat(emqx_cluster_call): ensure the consistency of resources When EMQX updates the cluster resources via HTTP API, it first updates the local node resources, and then updates all other nodes via RPC Multi Call to ensure the consistency of resources (configuration) in the cluster. --- apps/emqx/include/emqx_cluster_rpc.hrl | 35 +++ apps/emqx/src/emqx_cluster_rpc.erl | 321 +++++++++++++++++++++ apps/emqx/src/emqx_cluster_rpc_handler.erl | 94 ++++++ apps/emqx/src/emqx_kernel_sup.erl | 2 + apps/emqx/test/emqx_cluster_rpc_SUITE.erl | 247 ++++++++++++++++ 5 files changed, 699 insertions(+) create mode 100644 apps/emqx/include/emqx_cluster_rpc.hrl create mode 100644 apps/emqx/src/emqx_cluster_rpc.erl create mode 100644 apps/emqx/src/emqx_cluster_rpc_handler.erl create mode 100644 apps/emqx/test/emqx_cluster_rpc_SUITE.erl diff --git a/apps/emqx/include/emqx_cluster_rpc.hrl b/apps/emqx/include/emqx_cluster_rpc.hrl new file mode 100644 index 000000000..5c04346b7 --- /dev/null +++ b/apps/emqx/include/emqx_cluster_rpc.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(EMQ_X_CLUSTER_RPC_HRL). +-define(EMQ_X_CLUSTER_RPC_HRL, true). + +-define(CLUSTER_MFA, cluster_rpc_mfa). +-define(CLUSTER_COMMIT, cluster_rpc_commit). + +-record(cluster_rpc_mfa, { + tnx_id :: pos_integer(), + mfa :: mfa(), + created_at :: calendar:datetime(), + initiator :: node() +}). + +-record(cluster_rpc_commit, { + node :: node(), + tnx_id :: pos_integer() +}). + +-endif. diff --git a/apps/emqx/src/emqx_cluster_rpc.erl b/apps/emqx/src/emqx_cluster_rpc.erl new file mode 100644 index 000000000..301603c63 --- /dev/null +++ b/apps/emqx/src/emqx_cluster_rpc.erl @@ -0,0 +1,321 @@ +%%-------------------------------------------------------------------- +%% 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_cluster_rpc). +-behaviour(gen_statem). + +%% API +-export([start_link/0, mnesia/1]). +-export([multicall/3, multicall/4, query/1, reset/0, status/0]). + +-export([init/1, format_status/2, handle_event/4, terminate/3, + code_change/4, callback_mode/0]). + +-ifdef(TEST). +-compile(export_all). +-compile(nowarn_export_all). +-export([start_link/2]). +-endif. + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-include("emqx.hrl"). +-include("logger.hrl"). +-include("emqx_cluster_rpc.hrl"). + +-rlog_shard({?COMMON_SHARD, ?CLUSTER_MFA}). +-rlog_shard({?COMMON_SHARD, ?CLUSTER_COMMIT}). + +-define(CATCH_UP, catch_up). +-define(REALTIME, realtime). +-define(CATCH_UP_AFTER(_Sec_), {state_timeout, timer:seconds(_Sec_), catch_up_delay}). + +%%%=================================================================== +%%% API +%%%=================================================================== +mnesia(boot) -> + ok = ekka_mnesia:create_table(?CLUSTER_MFA, [ + {type, ordered_set}, + {disc_copies, [node()]}, + {local_content, true}, + {record_name, cluster_rpc_mfa}, + {attributes, record_info(fields, cluster_rpc_mfa)}]), + ok = ekka_mnesia:create_table(?CLUSTER_COMMIT, [ + {type, set}, + {disc_copies, [node()]}, + {local_content, true}, + {record_name, cluster_rpc_commit}, + {attributes, record_info(fields, cluster_rpc_commit)}]); +mnesia(copy) -> + ok = ekka_mnesia:copy_table(cluster_rpc_mfa, disc_copies), + ok = ekka_mnesia:copy_table(cluster_rpc_commit, disc_copies). + +start_link() -> + start_link(node(), ?MODULE). +start_link(Node, Name) -> + gen_statem:start_link({local, Name}, ?MODULE, [Node], []). + +multicall(M, F, A) -> + multicall(M, F, A, timer:minutes(2)). + +-spec multicall(Module, Function, Args, Timeout) -> {ok, TnxId} |{error, Reason} when + Module :: module(), + Function :: atom(), + Args :: [term()], + TnxId :: pos_integer(), + Timeout :: timeout(), + Reason :: term(). +multicall(M, F, A, Timeout) -> + MFA = {initiate, {M, F, A}}, + case ekka_rlog:role() of + core -> gen_statem:call(?MODULE, MFA, Timeout); + replicant -> + case ekka_rlog_status:upstream_node(?COMMON_SHARD) of + {ok, Node} -> gen_statem:call({?MODULE, Node}, MFA, Timeout); + disconnected -> {error, disconnected} + end + end. + +-spec query(pos_integer()) -> {'atomic', map()} | {'aborted', Reason :: term()}. +query(TnxId) -> + Fun = fun() -> + case mnesia:read(?CLUSTER_MFA, TnxId) of + [] -> mnesia:abort(not_found); + [#cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt}] -> + #{tnx_id => TnxId, mfa => MFA, initiator => InitNode, created_at => CreatedAt} + end + end, + transaction(Fun). + +-spec reset() -> reset. +reset() -> gen_statem:call(?MODULE, reset). + +-spec status() -> {'atomic', [map()]} | {'aborted', Reason :: term()}. +status() -> + Fun = fun() -> + mnesia:foldl(fun(Rec, Acc) -> + #cluster_rpc_commit{node = Node, tnx_id = TnxId} = Rec, + case mnesia:read(?CLUSTER_MFA, TnxId) of + [MFARec] -> + #cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt} = MFARec, + [#{ + node => Node, + tnx_id => TnxId, + initiator => InitNode, + mfa => MFA, + created_at => CreatedAt + } | Acc]; + [] -> Acc + end end, [], ?CLUSTER_COMMIT) + end, + transaction(Fun). + +%%%=================================================================== +%%% gen_statem callbacks +%%%=================================================================== + +%% @private +init([Node]) -> + {ok, _} = mnesia:subscribe({table, ?CLUSTER_MFA, simple}), + {ok, ?CATCH_UP, Node, ?CATCH_UP_AFTER(0)}. + +callback_mode() -> + handle_event_function. + +%% @private +format_status(Opt, [_PDict, StateName, Node]) -> + #{state => StateName, node => Node, opt => Opt}. + +%% @private +handle_event(state_timeout, catch_up_delay, _State, Node) -> + catch_up(Node); + +handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{} = MFARec, _AId}}, ?REALTIME, Node) -> + handle_mfa_write_event(MFARec, Node); +handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{}, _ActivityId}}, ?CATCH_UP, _Node) -> + {keep_state_and_data, [postpone, ?CATCH_UP_AFTER(0)]}; +%% we should catch up as soon as possible when we reset all. +handle_event(info, {mnesia_table_event, {delete,{schema, ?CLUSTER_MFA}, _Tid}}, _, _Node) -> + {keep_state_and_data, [?CATCH_UP_AFTER(0)]}; + +handle_event({call, From}, reset, _State, _Node) -> + _ = ekka_mnesia:clear_table(?CLUSTER_COMMIT), + _ = ekka_mnesia:clear_table(?CLUSTER_MFA), + {keep_state_and_data, [{reply, From, ok}, ?CATCH_UP_AFTER(0)]}; + +handle_event({call, From}, {initiate, MFA}, ?REALTIME, Node) -> + case transaction(fun() -> init_mfa(Node, MFA) end) of + {atomic, {ok, TnxId}} -> + {keep_state, Node, [{reply, From, {ok, TnxId}}]}; + {aborted, Reason} -> + {keep_state, Node, [{reply, From, {error, Reason}}]} + end; +handle_event({call, From}, {initiate, _MFA}, ?CATCH_UP, Node) -> + case catch_up(Node) of + {next_state, ?REALTIME, Node} -> + {next_state, ?REALTIME, Node, [{postpone, true}]}; + _ -> + Reason = "There are still transactions that have not been executed.", + {keep_state, Node, [{reply, From, {error, Reason}}, ?CATCH_UP_AFTER(1)]} + end; + +handle_event(_EventType, _EventContent, ?CATCH_UP, _Node) -> + {keep_state_and_data, [?CATCH_UP_AFTER(10)]}; +handle_event(_EventType, _EventContent, _StateName, _Node) -> + keep_state_and_data. + +terminate(_Reason, _StateName, _Node) -> + ok. + +code_change(_OldVsn, StateName, Node, _Extra) -> + {ok, StateName, Node}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +catch_up(Node) -> + case get_next_mfa(Node) of + {atomic, caught_up} -> {next_state, ?REALTIME, Node}; + {atomic, {still_lagging, NextId, MFA}} -> + case apply_mfa(NextId, MFA) of + ok -> + case transaction(fun() -> commit(Node, NextId) end) of + {atomic, ok} -> catch_up(Node); + _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + end; + _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + end; + {aborted, _Reason} -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + end. + +get_next_mfa(Node) -> + Fun = + fun() -> + NextId = + case mnesia:wread({?CLUSTER_COMMIT, Node}) of + [] -> + LatestId = get_latest_id(), + TnxId = max(LatestId - 1, 0), + commit(Node, TnxId), + ?LOG(notice, "New node(~p) first catch up and start commit at ~p", [Node, TnxId]), + TnxId; + [#cluster_rpc_commit{tnx_id = LastAppliedID}] -> LastAppliedID + 1 + end, + case mnesia:read(?CLUSTER_MFA, NextId) of + [] -> caught_up; + [#cluster_rpc_mfa{mfa = MFA}] -> {still_lagging, NextId, MFA} + end + end, + transaction(Fun). + +do_catch_up(ToTnxId, Node) -> + case mnesia:wread({?CLUSTER_COMMIT, Node}) of + [] -> + commit(Node, ToTnxId), + caught_up; + [#cluster_rpc_commit{tnx_id = LastAppliedId}] when ToTnxId =:= LastAppliedId -> + caught_up; + [#cluster_rpc_commit{tnx_id = LastAppliedId}] when ToTnxId > LastAppliedId -> + CurTnxId = LastAppliedId + 1, + [#cluster_rpc_mfa{mfa = MFA}] = mnesia:read(?CLUSTER_MFA, CurTnxId), + case apply_mfa(CurTnxId, MFA) of + ok -> ok = commit(Node, CurTnxId); + {error, Reason} -> mnesia:abort(Reason); + Other -> mnesia:abort(Other) + end; + [#cluster_rpc_commit{tnx_id = LastAppliedId}] -> + Reason = lists:flatten(io_lib:format("~p catch up failed by LastAppliedId(~p) > ToTnxId(~p)", + [Node, LastAppliedId, ToTnxId])), + ?LOG(error, Reason), + {error, Reason} + end. + +commit(Node, TnxId) -> + ok = mnesia:write(?CLUSTER_COMMIT, #cluster_rpc_commit{node = Node, tnx_id = TnxId}, write). + +get_latest_id() -> + case mnesia:last(?CLUSTER_MFA) of + '$end_of_table' -> 0; + Id -> Id + end. + +handle_mfa_write_event(#cluster_rpc_mfa{tnx_id = EventId, mfa = MFA}, Node) -> + {atomic, LastAppliedId} = transaction(fun() -> get_last_applied_id(Node, EventId - 1) end), + if LastAppliedId + 1 =:= EventId -> + case apply_mfa(EventId, MFA) of + ok -> + case transaction(fun() -> commit(Node, EventId) end) of + {atomic, ok} -> + {next_state, ?REALTIME, Node}; + _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + end; + _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + end; + LastAppliedId >= EventId -> %% It's means the initiator receive self event or other receive stale event. + keep_state_and_data; + true -> + ?LOG(error, "LastAppliedId+1 + case mnesia:wread({?CLUSTER_COMMIT, Node}) of + [#cluster_rpc_commit{tnx_id = TnxId}] -> TnxId; + [] -> + commit(Node, Default), + Default + end. + +init_mfa(Node, MFA) -> + mnesia:write_lock_table(?CLUSTER_MFA), + LatestId = get_latest_id(), + ok = do_catch_up_in_one_trans(LatestId, Node), + TnxId = LatestId + 1, + MFARec = #cluster_rpc_mfa{tnx_id = TnxId, mfa = MFA, initiator = Node, created_at = erlang:localtime()}, + ok = mnesia:write(?CLUSTER_MFA, MFARec, write), + ok = commit(Node, TnxId), + case apply_mfa(TnxId, MFA) of + ok -> {ok, TnxId}; + {error, Reason} -> mnesia:abort(Reason); + Other -> mnesia:abort(Other) + end. + +do_catch_up_in_one_trans(LatestId, Node) -> + case do_catch_up(LatestId, Node) of + caught_up -> ok; + ok -> do_catch_up_in_one_trans(LatestId, Node); + {error, Reason} -> mnesia:abort(Reason) + end. + +transaction(Fun) -> + ekka_mnesia:transaction(?COMMON_SHARD, Fun). + +apply_mfa(TnxId, {M, F, A} = MFA) -> + try + Res = erlang:apply(M, F, A), + case Res =:= ok of + true -> + ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => ok}); + false -> + ?SLOG(error, #{msg => "failed to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}) + end, + Res + catch + C : E -> + ?SLOG(critical, #{msg => "crash to apply MFA", tnx_id => TnxId, mfa => MFA, exception => C, reason => E}), + {error, lists:flatten(io_lib:format("TnxId(~p) apply MFA(~p) crash", [TnxId, MFA]))} + end. diff --git a/apps/emqx/src/emqx_cluster_rpc_handler.erl b/apps/emqx/src/emqx_cluster_rpc_handler.erl new file mode 100644 index 000000000..a51ef3d74 --- /dev/null +++ b/apps/emqx/src/emqx_cluster_rpc_handler.erl @@ -0,0 +1,94 @@ +%%-------------------------------------------------------------------- +%% 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_cluster_rpc_handler). + +-behaviour(gen_server). + +-include("emqx.hrl"). +-include("logger.hrl"). +-include("emqx_cluster_rpc.hrl"). + +-export([start_link/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-define(MFA_HISTORY_LEN, 100). + +start_link() -> + gen_server:start_link(?MODULE, [], []). + +%%%=================================================================== +%%% Spawning and gen_server implementation +%%%=================================================================== + +init([]) -> + _ = emqx_misc:rand_seed(), + {ok, ensure_timer(#{timer => undefined})}. + +handle_call(Req, _From, State) -> + ?LOG(error, "unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Msg, State) -> + ?LOG(error, "unexpected msg: ~p", [Msg]), + {noreply, State}. + +handle_info({timeout, TRef, del_stale_mfa}, State = #{timer := TRef}) -> + case ekka_mnesia:transaction(?COMMON_SHARD, fun del_stale_mfa/0, []) of + {atomic, ok} -> ok; + Error -> ?LOG(error, "del_stale_cluster_rpc_mfa error:~p", [Error]) + end, + {noreply, ensure_timer(State), hibernate}; + +handle_info(Info, State) -> + ?LOG(error, "unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, #{timer := TRef}) -> + emqx_misc:cancel_timer(TRef). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +-ifdef(TEST). +ensure_timer(State) -> + State#{timer := emqx_misc:start_timer(timer:seconds(1), del_stale_mfa)}. +-else. +ensure_timer(State) -> + Ms = timer:minutes(5) + rand:uniform(5000), + State#{timer := emqx_misc:start_timer(Ms, del_stale_mfa)}. +-endif. + + +%% @doc Keep the latest completed 100 records for querying and troubleshooting. +del_stale_mfa() -> + DoneId = + mnesia:foldl(fun(Rec, Min) -> min(Rec#cluster_rpc_commit.tnx_id, Min) end, + infinity, ?CLUSTER_COMMIT), + delete_stale_mfa(mnesia:last(?CLUSTER_MFA), DoneId, ?MFA_HISTORY_LEN). + +delete_stale_mfa('$end_of_table', _DoneId, _Count) -> ok; +delete_stale_mfa(CurrId, DoneId, Count) when CurrId > DoneId -> + delete_stale_mfa(mnesia:prev(?CLUSTER_MFA, CurrId), DoneId, Count); +delete_stale_mfa(CurrId, DoneId, Count) when Count > 0 -> + delete_stale_mfa(mnesia:prev(?CLUSTER_MFA, CurrId), DoneId, Count - 1); +delete_stale_mfa(CurrId, DoneId, Count) when Count =< 0 -> + mnesia:delete(?CLUSTER_MFA, CurrId, write), + delete_stale_mfa(mnesia:prev(?CLUSTER_MFA, CurrId), DoneId, Count - 1). diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index defe96182..39a8d4fba 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -29,6 +29,8 @@ init([]) -> {ok, {{one_for_one, 10, 100}, %% always start emqx_config_handler first to load the emqx.conf to emqx_config [ child_spec(emqx_config_handler, worker) + , child_spec(emqx_cluster_rpc, worker) + , child_spec(emqx_cluster_rpc_handler, worker) , child_spec(emqx_pool_sup, supervisor) , child_spec(emqx_hooks, worker) , child_spec(emqx_stats, worker) diff --git a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl new file mode 100644 index 000000000..6cdb34c6c --- /dev/null +++ b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl @@ -0,0 +1,247 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2018-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_cluster_rpc_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(NODE1, emqx_cluster_rpc). +-define(NODE2, emqx_cluster_rpc2). +-define(NODE3, emqx_cluster_rpc3). + +all() -> [ + t_base_test, + t_commit_fail_test, + t_commit_crash_test, + t_commit_ok_but_apply_fail_on_other_node, + t_commit_ok_apply_fail_on_other_node_then_recover, + t_del_stale_mfa +]. +suite() -> [{timetrap, {minutes, 3}}]. +groups() -> []. + +init_per_suite(Config) -> + application:load(emqx), + ok = ekka:start(), + emqx_cluster_rpc:mnesia(copy), + %%dbg:tracer(), + %%dbg:p(all, c), + %%dbg:tpl(emqx_cluster_rpc, cx), + %%dbg:tpl(gen_statem, loop_receive, cx), + %%dbg:tpl(gen_statem, loop_state_callback, cx), + %%dbg:tpl(gen_statem, loop_callback_mode_result, cx), + Config. + +end_per_suite(_Config) -> + ekka:stop(), + ekka_mnesia:ensure_stopped(), + ekka_mnesia:delete_schema(), + %%dbg:stop(), + ok. + +init_per_testcase(_TestCase, Config) -> + start(), + Config. + +end_per_testcase(_Config) -> + stop(), + ok. + +t_base_test(_Config) -> + ?assertEqual(emqx_cluster_rpc:status(), {atomic, []}), + Pid = self(), + MFA = {M, F, A} = {?MODULE, echo, [Pid, test]}, + {ok, TnxId} = emqx_cluster_rpc:multicall(M, F, A), + {atomic, Query} = emqx_cluster_rpc:query(TnxId), + ?assertEqual(MFA, maps:get(mfa, Query)), + ?assertEqual(node(), maps:get(initiator, Query)), + ?assert(maps:is_key(created_at, Query)), + ?assertEqual(ok, receive_msg(3, test)), + SysStatus = lists:last(lists:last(element(4,sys:get_status(?NODE1)))), + ?assertEqual(#{node => node(), opt => normal, state => realtime}, SysStatus), + sleep(400), + {atomic, Status} = emqx_cluster_rpc:status(), + ?assertEqual(3, length(Status)), + ?assert(lists:all(fun(I) -> maps:get(tnx_id, I) =:= 1 end, Status)), + ok. + +t_commit_fail_test(_Config) -> + emqx_cluster_rpc:reset(), + {atomic, []} = emqx_cluster_rpc:status(), + {M, F, A} = {?MODULE, failed_on_node, [erlang:whereis(?NODE2)]}, + {error, "MFA return not ok"} = emqx_cluster_rpc:multicall(M, F, A), + ?assertEqual({atomic, []}, emqx_cluster_rpc:status()), + ok. + +t_commit_crash_test(_Config) -> + emqx_cluster_rpc:reset(), + {atomic, []} = emqx_cluster_rpc:status(), + {M, F, A} = {?MODULE, no_exist_function, []}, + Error = emqx_cluster_rpc:multicall(M, F, A), + ?assertEqual({error, "TnxId(1) apply MFA({emqx_cluster_rpc_SUITE,no_exist_function,[]}) crash"}, Error), + ?assertEqual({atomic, []}, emqx_cluster_rpc:status()), + ok. + +t_commit_ok_but_apply_fail_on_other_node(_Config) -> + emqx_cluster_rpc:reset(), + {atomic, []} = emqx_cluster_rpc:status(), + MFA = {M, F, A} = {?MODULE, failed_on_node, [erlang:whereis(?NODE1)]}, + {ok, _} = emqx_cluster_rpc:multicall(M, F, A), + {atomic, [Status]} = emqx_cluster_rpc:status(), + ?assertEqual(MFA, maps:get(mfa, Status)), + ?assertEqual(node(), maps:get(node, Status)), + ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), + ?assertEqual(catch_up, element(1, sys:get_state(?NODE2))), + ?assertEqual(catch_up, element(1, sys:get_state(?NODE3))), + erlang:send(?NODE2, test), + Res = gen_statem:call(?NODE2, {initiate, {M, F, A}}), + ?assertEqual({error, "There are still transactions that have not been executed."}, Res), + ok. + +t_catch_up_status_handle_next_commit(_Config) -> + emqx_cluster_rpc:reset(), + {atomic, []} = emqx_cluster_rpc:status(), + {M, F, A} = {?MODULE, failed_on_node_by_odd, [erlang:whereis(?NODE1)]}, + {ok, _} = emqx_cluster_rpc:multicall(M, F, A), + ?assertEqual(catch_up, element(1, sys:get_state(?NODE2))), + {ok, 2} = gen_statem:call(?NODE2, {initiate, {M, F, A}}), + ok. + +t_commit_ok_apply_fail_on_other_node_then_recover(_Config) -> + emqx_cluster_rpc:reset(), + {atomic, []} = emqx_cluster_rpc:status(), + Now = erlang:system_time(second), + {M, F, A} = {?MODULE, failed_on_other_recover_after_5_second, [erlang:whereis(?NODE1), Now]}, + {ok, _} = emqx_cluster_rpc:multicall(M, F, A), + {ok, _} = emqx_cluster_rpc:multicall(io, format, ["test"]), + {atomic, [Status]} = emqx_cluster_rpc:status(), + ?assertEqual({io, format, ["test"]}, maps:get(mfa, Status)), + ?assertEqual(node(), maps:get(node, Status)), + ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), + ?assertEqual(catch_up, element(1, sys:get_state(?NODE2))), + ?assertEqual(catch_up, element(1, sys:get_state(?NODE3))), + sleep(4000), + {atomic, [Status1]} = emqx_cluster_rpc:status(), + ?assertEqual(Status, Status1), + sleep(1600), + {atomic, NewStatus} = emqx_cluster_rpc:status(), + ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), + ?assertEqual(realtime, element(1, sys:get_state(?NODE2))), + ?assertEqual(realtime, element(1, sys:get_state(?NODE3))), + ?assertEqual(3, length(NewStatus)), + Pid = self(), + MFAEcho = {M1, F1, A1} = {?MODULE, echo, [Pid, test]}, + {ok, TnxId} = emqx_cluster_rpc:multicall(M1, F1, A1), + {atomic, Query} = emqx_cluster_rpc:query(TnxId), + ?assertEqual(MFAEcho, maps:get(mfa, Query)), + ?assertEqual(node(), maps:get(initiator, Query)), + ?assert(maps:is_key(created_at, Query)), + ?assertEqual(ok, receive_msg(3, test)), + ok. + +t_del_stale_mfa(_Config) -> + emqx_cluster_rpc:reset(), + {atomic, []} = emqx_cluster_rpc:status(), + MFA = {M, F, A} = {io, format, ["test"]}, + Keys = lists:seq(1, 50), + Keys2 = lists:seq(51, 150), + Ids = + [begin + {ok, TnxId} = emqx_cluster_rpc:multicall(M, F, A), + TnxId end || _ <- Keys], + ?assertEqual(Keys, Ids), + Ids2 = + [begin + {ok, TnxId} = emqx_cluster_rpc:multicall(M, F, A), + TnxId end || _ <- Keys2], + ?assertEqual(Keys2, Ids2), + sleep(1200), + [begin + ?assertEqual({aborted, not_found}, emqx_cluster_rpc:query(I)) + end || I <- lists:seq(1, 50)], + [begin + {atomic, Map} = emqx_cluster_rpc:query(I), + ?assertEqual(MFA, maps:get(mfa, Map)), + ?assertEqual(node(), maps:get(initiator, Map)), + ?assert(maps:is_key(created_at, Map)) + end || I <- lists:seq(51, 150)], + ok. + +start() -> + {ok, Pid1} = emqx_cluster_rpc:start_link(), + {ok, Pid2} = emqx_cluster_rpc:start_link({node(), ?NODE2}, ?NODE2), + {ok, Pid3} = emqx_cluster_rpc:start_link({node(), ?NODE3}, ?NODE3), + {ok, Pid4} = emqx_cluster_rpc_handler:start_link(), + {ok, [Pid1, Pid2, Pid3, Pid4]}. + +stop() -> + [begin + case erlang:whereis(N) of + undefined -> ok; + P -> + erlang:unlink(P), + erlang:exit(P, kill) + end end || N <- [?NODE1, ?NODE2, ?NODE3]]. + +receive_msg(0, _Msg) -> ok; +receive_msg(Count, Msg) when Count > 0 -> + receive Msg -> + receive_msg(Count - 1, Msg) + after 800 -> + timeout + end. + +echo(Pid, Msg) -> + erlang:send(Pid, Msg), + ok. + +failed_on_node(Pid) -> + case Pid =:= self() of + true -> ok; + false -> "MFA return not ok" + end. + +failed_on_node_by_odd(Pid) -> + case Pid =:= self() of + true -> ok; + false -> + catch ets:new(test, [named_table, set, public]), + Num = ets:update_counter(test, self(), {2, 1}, {self(), 1}), + case Num rem 2 =:= 0 of + false -> "MFA return not ok"; + true -> ok + end + end. + +failed_on_other_recover_after_5_second(Pid, CreatedAt) -> + Now = erlang:system_time(second), + case Pid =:= self() of + true -> ok; + false -> + case Now < CreatedAt + 5 of + true -> "MFA return not ok"; + false -> ok + end + end. + +sleep(Second) -> + receive _ -> ok + after Second -> timeout + end. From 11e87ab9a37282f9c10f82fb19458737d4102529 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Thu, 19 Aug 2021 23:15:27 +0800 Subject: [PATCH 131/306] chore(CI): DIAGNOSTIC=1 for more error message --- .github/workflows/run_test_cases.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index fcd5e8374..b97c3c003 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -98,8 +98,8 @@ jobs: - name: run cover run: | printenv > .env - docker exec -i ${{ matrix.otp_release }} bash -c "make cover" - docker exec --env-file .env -i ${{ matrix.otp_release }} bash -c "make coveralls" + docker exec -i ${{ matrix.otp_release }} bash -c "DIAGNOSTIC=1 make cover" + docker exec --env-file .env -i ${{ matrix.otp_release }} bash -c "DIAGNOSTIC=1 make coveralls" - name: cat rebar.crashdump if: failure() run: if [ -f 'rebar3.crashdump' ];then cat 'rebar3.crashdump'; fi From e5129ead6de223eb0e25b56b4270aada2ef36d92 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 20 Aug 2021 08:31:44 +0800 Subject: [PATCH 132/306] fix(CI): Error detected: 'Invalid reference to group api in emqx_rule_engine_SUITE:all/0' --- apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a056d0c26..bf4dcb30e 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -30,7 +30,7 @@ all() -> [ {group, engine} , {group, actions} - , {group, api} +%% , {group, api} , {group, cli} , {group, funcs} , {group, registry} From 765c94152bb2cbdfc339ea49e77ab601345e5a75 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 20 Aug 2021 11:46:58 +0800 Subject: [PATCH 133/306] feat: add cluster_call.retry_interval/mfa_max_history/mfa_cleanup_interval config --- apps/emqx/etc/emqx.conf | 3 +- apps/emqx/src/emqx_cluster_rpc.erl | 105 +++++++++++---------- apps/emqx/src/emqx_cluster_rpc_handler.erl | 33 +++---- apps/emqx/src/emqx_schema.erl | 7 ++ apps/emqx/test/emqx_cluster_rpc_SUITE.erl | 13 ++- 5 files changed, 88 insertions(+), 73 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index c85e7aa29..e3c701e28 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -490,6 +490,7 @@ listeners.wss.default { ## Websocket options ## See ${example_common_websocket_options} for more information websocket.idle_timeout = 86400s + } ## Enable per connection statistics. @@ -1071,7 +1072,7 @@ broker { ## 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. + ## It requires all nodes to be stopped before changing it. ## ## @doc broker.perf.trie_compaction ## ValueType: Boolean diff --git a/apps/emqx/src/emqx_cluster_rpc.erl b/apps/emqx/src/emqx_cluster_rpc.erl index 301603c63..30f9e3234 100644 --- a/apps/emqx/src/emqx_cluster_rpc.erl +++ b/apps/emqx/src/emqx_cluster_rpc.erl @@ -26,7 +26,7 @@ -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). --export([start_link/2]). +-export([start_link/3]). -endif. -boot_mnesia({mnesia, [boot]}). @@ -41,7 +41,7 @@ -define(CATCH_UP, catch_up). -define(REALTIME, realtime). --define(CATCH_UP_AFTER(_Sec_), {state_timeout, timer:seconds(_Sec_), catch_up_delay}). +-define(CATCH_UP_AFTER(_Ms_), {state_timeout, _Ms_, catch_up_delay}). %%%=================================================================== %%% API @@ -64,9 +64,10 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(cluster_rpc_commit, disc_copies). start_link() -> - start_link(node(), ?MODULE). -start_link(Node, Name) -> - gen_statem:start_link({local, Name}, ?MODULE, [Node], []). + RetryMs = emqx:get_config([broker, cluster_call, retry_interval]), + start_link(node(), ?MODULE, RetryMs). +start_link(Node, Name, RetryMs) -> + gen_statem:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). multicall(M, F, A) -> multicall(M, F, A, timer:minutes(2)). @@ -91,14 +92,14 @@ multicall(M, F, A, Timeout) -> -spec query(pos_integer()) -> {'atomic', map()} | {'aborted', Reason :: term()}. query(TnxId) -> - Fun = fun() -> - case mnesia:read(?CLUSTER_MFA, TnxId) of - [] -> mnesia:abort(not_found); - [#cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt}] -> - #{tnx_id => TnxId, mfa => MFA, initiator => InitNode, created_at => CreatedAt} - end - end, - transaction(Fun). + transaction(fun do_query/1, [TnxId]). + +do_query(TnxId) -> + case mnesia:read(?CLUSTER_MFA, TnxId) of + [] -> mnesia:abort(not_found); + [#cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt}] -> + #{tnx_id => TnxId, mfa => MFA, initiator => InitNode, created_at => CreatedAt} + end. -spec reset() -> reset. reset() -> gen_statem:call(?MODULE, reset). @@ -128,77 +129,77 @@ status() -> %%%=================================================================== %% @private -init([Node]) -> +init([Node, RetryMs]) -> {ok, _} = mnesia:subscribe({table, ?CLUSTER_MFA, simple}), - {ok, ?CATCH_UP, Node, ?CATCH_UP_AFTER(0)}. + {ok, ?CATCH_UP, #{node => Node, retry_interval => RetryMs}, ?CATCH_UP_AFTER(0)}. callback_mode() -> handle_event_function. %% @private -format_status(Opt, [_PDict, StateName, Node]) -> - #{state => StateName, node => Node, opt => Opt}. +format_status(Opt, [_PDict, StateName, Data]) -> + #{state => StateName, data => Data , opt => Opt}. %% @private -handle_event(state_timeout, catch_up_delay, _State, Node) -> - catch_up(Node); +handle_event(state_timeout, catch_up_delay, _State, Data) -> + catch_up(Data); -handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{} = MFARec, _AId}}, ?REALTIME, Node) -> - handle_mfa_write_event(MFARec, Node); -handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{}, _ActivityId}}, ?CATCH_UP, _Node) -> +handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{} = MFARec, _AId}}, ?REALTIME, Data) -> + handle_mfa_write_event(MFARec, Data); +handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{}, _ActivityId}}, ?CATCH_UP, _Data) -> {keep_state_and_data, [postpone, ?CATCH_UP_AFTER(0)]}; %% we should catch up as soon as possible when we reset all. -handle_event(info, {mnesia_table_event, {delete,{schema, ?CLUSTER_MFA}, _Tid}}, _, _Node) -> +handle_event(info, {mnesia_table_event, {delete,{schema, ?CLUSTER_MFA}, _Tid}}, _, _Data) -> {keep_state_and_data, [?CATCH_UP_AFTER(0)]}; -handle_event({call, From}, reset, _State, _Node) -> +handle_event({call, From}, reset, _State, _Data) -> _ = ekka_mnesia:clear_table(?CLUSTER_COMMIT), _ = ekka_mnesia:clear_table(?CLUSTER_MFA), {keep_state_and_data, [{reply, From, ok}, ?CATCH_UP_AFTER(0)]}; -handle_event({call, From}, {initiate, MFA}, ?REALTIME, Node) -> +handle_event({call, From}, {initiate, MFA}, ?REALTIME, Data = #{node := Node}) -> case transaction(fun() -> init_mfa(Node, MFA) end) of {atomic, {ok, TnxId}} -> - {keep_state, Node, [{reply, From, {ok, TnxId}}]}; + {keep_state, Data, [{reply, From, {ok, TnxId}}]}; {aborted, Reason} -> - {keep_state, Node, [{reply, From, {error, Reason}}]} + {keep_state, Data, [{reply, From, {error, Reason}}]} end; -handle_event({call, From}, {initiate, _MFA}, ?CATCH_UP, Node) -> - case catch_up(Node) of - {next_state, ?REALTIME, Node} -> - {next_state, ?REALTIME, Node, [{postpone, true}]}; +handle_event({call, From}, {initiate, _MFA}, ?CATCH_UP, Data = #{retry_interval := RetryMs}) -> + case catch_up(Data) of + {next_state, ?REALTIME, Data} -> + {next_state, ?REALTIME, Data, [{postpone, true}]}; _ -> Reason = "There are still transactions that have not been executed.", - {keep_state, Node, [{reply, From, {error, Reason}}, ?CATCH_UP_AFTER(1)]} + {keep_state_and_data, [{reply, From, {error, Reason}}, ?CATCH_UP_AFTER(RetryMs)]} end; -handle_event(_EventType, _EventContent, ?CATCH_UP, _Node) -> - {keep_state_and_data, [?CATCH_UP_AFTER(10)]}; -handle_event(_EventType, _EventContent, _StateName, _Node) -> +handle_event(_EventType, _EventContent, ?CATCH_UP, #{retry_interval := RetryMs}) -> + {keep_state_and_data, [?CATCH_UP_AFTER(RetryMs)]}; +handle_event(_EventType, _EventContent, _StateName, _Data) -> keep_state_and_data. -terminate(_Reason, _StateName, _Node) -> +terminate(_Reason, _StateName, _Data) -> ok. -code_change(_OldVsn, StateName, Node, _Extra) -> - {ok, StateName, Node}. +code_change(_OldVsn, StateName, Data, _Extra) -> + {ok, StateName, Data}. %%%=================================================================== %%% Internal functions %%%=================================================================== -catch_up(Node) -> +catch_up(#{node := Node, retry_interval := RetryMs} = Data) -> case get_next_mfa(Node) of - {atomic, caught_up} -> {next_state, ?REALTIME, Node}; + {atomic, caught_up} -> {next_state, ?REALTIME, Data}; {atomic, {still_lagging, NextId, MFA}} -> case apply_mfa(NextId, MFA) of ok -> case transaction(fun() -> commit(Node, NextId) end) of - {atomic, ok} -> catch_up(Node); - _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + {atomic, ok} -> catch_up(Data); + _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} end; - _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} end; - {aborted, _Reason} -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + {aborted, _Reason} -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} end. get_next_mfa(Node) -> @@ -252,17 +253,18 @@ get_latest_id() -> Id -> Id end. -handle_mfa_write_event(#cluster_rpc_mfa{tnx_id = EventId, mfa = MFA}, Node) -> +handle_mfa_write_event(#cluster_rpc_mfa{tnx_id = EventId, mfa = MFA}, Data) -> + #{node := Node, retry_interval := RetryMs} = Data, {atomic, LastAppliedId} = transaction(fun() -> get_last_applied_id(Node, EventId - 1) end), if LastAppliedId + 1 =:= EventId -> case apply_mfa(EventId, MFA) of ok -> case transaction(fun() -> commit(Node, EventId) end) of {atomic, ok} -> - {next_state, ?REALTIME, Node}; - _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + {next_state, ?REALTIME, Data}; + _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} end; - _ -> {next_state, ?CATCH_UP, Node, [?CATCH_UP_AFTER(1)]} + _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} end; LastAppliedId >= EventId -> %% It's means the initiator receive self event or other receive stale event. keep_state_and_data; @@ -301,8 +303,11 @@ do_catch_up_in_one_trans(LatestId, Node) -> {error, Reason} -> mnesia:abort(Reason) end. -transaction(Fun) -> - ekka_mnesia:transaction(?COMMON_SHARD, Fun). +transaction(Func, Args) -> + ekka_mnesia:transaction(?COMMON_SHARD, Func, Args). + +transaction(Func) -> + ekka_mnesia:transaction(?COMMON_SHARD, Func). apply_mfa(TnxId, {M, F, A} = MFA) -> try diff --git a/apps/emqx/src/emqx_cluster_rpc_handler.erl b/apps/emqx/src/emqx_cluster_rpc_handler.erl index a51ef3d74..5f548e7fd 100644 --- a/apps/emqx/src/emqx_cluster_rpc_handler.erl +++ b/apps/emqx/src/emqx_cluster_rpc_handler.erl @@ -21,22 +21,27 @@ -include("logger.hrl"). -include("emqx_cluster_rpc.hrl"). --export([start_link/0]). +-export([start_link/0, start_link/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(MFA_HISTORY_LEN, 100). start_link() -> - gen_server:start_link(?MODULE, [], []). + MaxHistory = emqx:get_config([broker, cluster_call, mfa_max_history]), + CleanupMs = emqx:get_config([broker, cluster_call, mfa_cleanup_interval]), + start_link(MaxHistory, CleanupMs). + +start_link(MaxHistory, CleanupMs) -> + State = #{max_history => MaxHistory, cleanup_ms => CleanupMs, timer => undefined}, + gen_server:start_link(?MODULE, [State], []). %%%=================================================================== %%% Spawning and gen_server implementation %%%=================================================================== -init([]) -> - _ = emqx_misc:rand_seed(), - {ok, ensure_timer(#{timer => undefined})}. +init([State]) -> + {ok, ensure_timer(State)}. handle_call(Req, _From, State) -> ?LOG(error, "unexpected call: ~p", [Req]), @@ -46,8 +51,8 @@ handle_cast(Msg, State) -> ?LOG(error, "unexpected msg: ~p", [Msg]), {noreply, State}. -handle_info({timeout, TRef, del_stale_mfa}, State = #{timer := TRef}) -> - case ekka_mnesia:transaction(?COMMON_SHARD, fun del_stale_mfa/0, []) of +handle_info({timeout, TRef, del_stale_mfa}, State = #{timer := TRef, max_history := MaxHistory}) -> + case ekka_mnesia:transaction(?COMMON_SHARD, fun del_stale_mfa/1, [MaxHistory]) of {atomic, ok} -> ok; Error -> ?LOG(error, "del_stale_cluster_rpc_mfa error:~p", [Error]) end, @@ -66,23 +71,15 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- - --ifdef(TEST). -ensure_timer(State) -> - State#{timer := emqx_misc:start_timer(timer:seconds(1), del_stale_mfa)}. --else. -ensure_timer(State) -> - Ms = timer:minutes(5) + rand:uniform(5000), +ensure_timer(State = #{cleanup_ms := Ms}) -> State#{timer := emqx_misc:start_timer(Ms, del_stale_mfa)}. --endif. - %% @doc Keep the latest completed 100 records for querying and troubleshooting. -del_stale_mfa() -> +del_stale_mfa(MaxHistory) -> DoneId = mnesia:foldl(fun(Rec, Min) -> min(Rec#cluster_rpc_commit.tnx_id, Min) end, infinity, ?CLUSTER_COMMIT), - delete_stale_mfa(mnesia:last(?CLUSTER_MFA), DoneId, ?MFA_HISTORY_LEN). + delete_stale_mfa(mnesia:last(?CLUSTER_MFA), DoneId, MaxHistory). delete_stale_mfa('$end_of_table', _DoneId, _Count) -> ok; delete_stale_mfa(CurrId, DoneId, Count) when CurrId > DoneId -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 3bbeb1d07..cb250f575 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -293,6 +293,7 @@ fields("broker") -> , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} , {"route_batch_clean", t(boolean(), undefined, true)} , {"perf", ref("perf")} + , {"cluster_call", ref("cluster_call")} ]; fields("perf") -> @@ -325,6 +326,12 @@ fields("sysmon_os") -> , {"procmem_high_watermark", t(percent(), undefined, "5%")} ]; +fields("cluster_call") -> + [{"retry_interval", t(duration(), undefined, "2s")} + , {"mfa_max_history", t(range(1, 500), undefined, 50)} + , {"mfa_cleanup_interval", t(duration(), undefined, "5m")} + ]; + fields("alarm") -> [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} , {"size_limit", t(integer(), undefined, 1000)} diff --git a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl index 6cdb34c6c..8e8f55834 100644 --- a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl @@ -41,6 +41,11 @@ init_per_suite(Config) -> application:load(emqx), ok = ekka:start(), emqx_cluster_rpc:mnesia(copy), + emqx_config:put([broker, cluster_call], #{ + mfa_max_history => 100, + mfa_cleanup_interval => 1000, + retry_interval => 900 + }), %%dbg:tracer(), %%dbg:p(all, c), %%dbg:tpl(emqx_cluster_rpc, cx), @@ -75,7 +80,7 @@ t_base_test(_Config) -> ?assert(maps:is_key(created_at, Query)), ?assertEqual(ok, receive_msg(3, test)), SysStatus = lists:last(lists:last(element(4,sys:get_status(?NODE1)))), - ?assertEqual(#{node => node(), opt => normal, state => realtime}, SysStatus), + ?assertEqual(#{data => #{node => node(),retry_interval => 900}, opt => normal, state => realtime}, SysStatus), sleep(400), {atomic, Status} = emqx_cluster_rpc:status(), ?assertEqual(3, length(Status)), @@ -186,9 +191,9 @@ t_del_stale_mfa(_Config) -> start() -> {ok, Pid1} = emqx_cluster_rpc:start_link(), - {ok, Pid2} = emqx_cluster_rpc:start_link({node(), ?NODE2}, ?NODE2), - {ok, Pid3} = emqx_cluster_rpc:start_link({node(), ?NODE3}, ?NODE3), - {ok, Pid4} = emqx_cluster_rpc_handler:start_link(), + {ok, Pid2} = emqx_cluster_rpc:start_link({node(), ?NODE2}, ?NODE2, 500), + {ok, Pid3} = emqx_cluster_rpc:start_link({node(), ?NODE3}, ?NODE3, 500), + {ok, Pid4} = emqx_cluster_rpc_handler:start_link(100, 500), {ok, [Pid1, Pid2, Pid3, Pid4]}. stop() -> From d55ba6b2e83fc2301aaf74dd1520c1565c9fe619 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 20 Aug 2021 13:57:32 +0800 Subject: [PATCH 134/306] chore: transaction without nnonymous function --- apps/emqx/src/emqx_cluster_rpc.erl | 91 +++++++++++------------ apps/emqx/test/emqx_cluster_rpc_SUITE.erl | 3 +- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/apps/emqx/src/emqx_cluster_rpc.erl b/apps/emqx/src/emqx_cluster_rpc.erl index 30f9e3234..a4c54d01e 100644 --- a/apps/emqx/src/emqx_cluster_rpc.erl +++ b/apps/emqx/src/emqx_cluster_rpc.erl @@ -92,37 +92,14 @@ multicall(M, F, A, Timeout) -> -spec query(pos_integer()) -> {'atomic', map()} | {'aborted', Reason :: term()}. query(TnxId) -> - transaction(fun do_query/1, [TnxId]). - -do_query(TnxId) -> - case mnesia:read(?CLUSTER_MFA, TnxId) of - [] -> mnesia:abort(not_found); - [#cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt}] -> - #{tnx_id => TnxId, mfa => MFA, initiator => InitNode, created_at => CreatedAt} - end. + transaction(fun trans_query/1, [TnxId]). -spec reset() -> reset. reset() -> gen_statem:call(?MODULE, reset). -spec status() -> {'atomic', [map()]} | {'aborted', Reason :: term()}. status() -> - Fun = fun() -> - mnesia:foldl(fun(Rec, Acc) -> - #cluster_rpc_commit{node = Node, tnx_id = TnxId} = Rec, - case mnesia:read(?CLUSTER_MFA, TnxId) of - [MFARec] -> - #cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt} = MFARec, - [#{ - node => Node, - tnx_id => TnxId, - initiator => InitNode, - mfa => MFA, - created_at => CreatedAt - } | Acc]; - [] -> Acc - end end, [], ?CLUSTER_COMMIT) - end, - transaction(Fun). + transaction(fun trans_status/0, []). %%%=================================================================== %%% gen_statem callbacks @@ -158,7 +135,7 @@ handle_event({call, From}, reset, _State, _Data) -> {keep_state_and_data, [{reply, From, ok}, ?CATCH_UP_AFTER(0)]}; handle_event({call, From}, {initiate, MFA}, ?REALTIME, Data = #{node := Node}) -> - case transaction(fun() -> init_mfa(Node, MFA) end) of + case transaction(fun init_mfa/2, [Node, MFA]) of {atomic, {ok, TnxId}} -> {keep_state, Data, [{reply, From, {ok, TnxId}}]}; {aborted, Reason} -> @@ -188,12 +165,12 @@ code_change(_OldVsn, StateName, Data, _Extra) -> %%% Internal functions %%%=================================================================== catch_up(#{node := Node, retry_interval := RetryMs} = Data) -> - case get_next_mfa(Node) of + case transaction(fun get_next_mfa/1, [Node]) of {atomic, caught_up} -> {next_state, ?REALTIME, Data}; {atomic, {still_lagging, NextId, MFA}} -> case apply_mfa(NextId, MFA) of ok -> - case transaction(fun() -> commit(Node, NextId) end) of + case transaction(fun commit/2, [Node, NextId]) of {atomic, ok} -> catch_up(Data); _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} end; @@ -203,24 +180,20 @@ catch_up(#{node := Node, retry_interval := RetryMs} = Data) -> end. get_next_mfa(Node) -> - Fun = - fun() -> - NextId = - case mnesia:wread({?CLUSTER_COMMIT, Node}) of - [] -> - LatestId = get_latest_id(), - TnxId = max(LatestId - 1, 0), - commit(Node, TnxId), - ?LOG(notice, "New node(~p) first catch up and start commit at ~p", [Node, TnxId]), - TnxId; - [#cluster_rpc_commit{tnx_id = LastAppliedID}] -> LastAppliedID + 1 - end, - case mnesia:read(?CLUSTER_MFA, NextId) of - [] -> caught_up; - [#cluster_rpc_mfa{mfa = MFA}] -> {still_lagging, NextId, MFA} - end + NextId = + case mnesia:wread({?CLUSTER_COMMIT, Node}) of + [] -> + LatestId = get_latest_id(), + TnxId = max(LatestId - 1, 0), + commit(Node, TnxId), + ?LOG(notice, "New node(~p) first catch up and start commit at ~p", [Node, TnxId]), + TnxId; + [#cluster_rpc_commit{tnx_id = LastAppliedID}] -> LastAppliedID + 1 end, - transaction(Fun). + case mnesia:read(?CLUSTER_MFA, NextId) of + [] -> caught_up; + [#cluster_rpc_mfa{mfa = MFA}] -> {still_lagging, NextId, MFA} + end. do_catch_up(ToTnxId, Node) -> case mnesia:wread({?CLUSTER_COMMIT, Node}) of @@ -255,11 +228,11 @@ get_latest_id() -> handle_mfa_write_event(#cluster_rpc_mfa{tnx_id = EventId, mfa = MFA}, Data) -> #{node := Node, retry_interval := RetryMs} = Data, - {atomic, LastAppliedId} = transaction(fun() -> get_last_applied_id(Node, EventId - 1) end), + {atomic, LastAppliedId} = transaction(fun get_last_applied_id/2, [Node, EventId - 1]), if LastAppliedId + 1 =:= EventId -> case apply_mfa(EventId, MFA) of ok -> - case transaction(fun() -> commit(Node, EventId) end) of + case transaction(fun commit/2, [Node, EventId]) of {atomic, ok} -> {next_state, ?REALTIME, Data}; _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} @@ -306,8 +279,28 @@ do_catch_up_in_one_trans(LatestId, Node) -> transaction(Func, Args) -> ekka_mnesia:transaction(?COMMON_SHARD, Func, Args). -transaction(Func) -> - ekka_mnesia:transaction(?COMMON_SHARD, Func). +trans_status() -> + mnesia:foldl(fun(Rec, Acc) -> + #cluster_rpc_commit{node = Node, tnx_id = TnxId} = Rec, + case mnesia:read(?CLUSTER_MFA, TnxId) of + [MFARec] -> + #cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt} = MFARec, + [#{ + node => Node, + tnx_id => TnxId, + initiator => InitNode, + mfa => MFA, + created_at => CreatedAt + } | Acc]; + [] -> Acc + end end, [], ?CLUSTER_COMMIT). + +trans_query(TnxId) -> + case mnesia:read(?CLUSTER_MFA, TnxId) of + [] -> mnesia:abort(not_found); + [#cluster_rpc_mfa{mfa = MFA, initiator = InitNode, created_at = CreatedAt}] -> + #{tnx_id => TnxId, mfa => MFA, initiator => InitNode, created_at => CreatedAt} + end. apply_mfa(TnxId, {M, F, A} = MFA) -> try diff --git a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl index 8e8f55834..5106183bb 100644 --- a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl @@ -136,7 +136,8 @@ t_commit_ok_apply_fail_on_other_node_then_recover(_Config) -> {M, F, A} = {?MODULE, failed_on_other_recover_after_5_second, [erlang:whereis(?NODE1), Now]}, {ok, _} = emqx_cluster_rpc:multicall(M, F, A), {ok, _} = emqx_cluster_rpc:multicall(io, format, ["test"]), - {atomic, [Status]} = emqx_cluster_rpc:status(), + {atomic, [Status|L]} = emqx_cluster_rpc:status(), + ?assertEqual([], L), ?assertEqual({io, format, ["test"]}, maps:get(mfa, Status)), ?assertEqual(node(), maps:get(node, Status)), ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), From 2c1b1fbfa87c5a86c6760bd1aa38b08dcf289db1 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 20 Aug 2021 14:31:47 +0800 Subject: [PATCH 135/306] chore(config): rename cluster_call to hot_config_loader --- apps/emqx/etc/emqx.conf | 2 ++ apps/emqx/src/emqx_cluster_rpc.erl | 2 +- apps/emqx/src/emqx_cluster_rpc_handler.erl | 4 ++-- apps/emqx/src/emqx_schema.erl | 6 +++--- apps/emqx/test/emqx_cluster_rpc_SUITE.erl | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index e3c701e28..7c404ff2d 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -222,6 +222,8 @@ listeners.quic.default { ## If not set, the global configs are used for this listener. ## ## See `zones.` for more details. + ## NOTE: This is a cluster-wide configuration. + ## It requires all nodes to be stopped before changing it. ## ## @doc listeners.quic..zone ## ValueType: String diff --git a/apps/emqx/src/emqx_cluster_rpc.erl b/apps/emqx/src/emqx_cluster_rpc.erl index a4c54d01e..5ddfe5742 100644 --- a/apps/emqx/src/emqx_cluster_rpc.erl +++ b/apps/emqx/src/emqx_cluster_rpc.erl @@ -64,7 +64,7 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(cluster_rpc_commit, disc_copies). start_link() -> - RetryMs = emqx:get_config([broker, cluster_call, retry_interval]), + RetryMs = emqx:get_config([broker, hot_config_loader, retry_interval]), start_link(node(), ?MODULE, RetryMs). start_link(Node, Name, RetryMs) -> gen_statem:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). diff --git a/apps/emqx/src/emqx_cluster_rpc_handler.erl b/apps/emqx/src/emqx_cluster_rpc_handler.erl index 5f548e7fd..4da219165 100644 --- a/apps/emqx/src/emqx_cluster_rpc_handler.erl +++ b/apps/emqx/src/emqx_cluster_rpc_handler.erl @@ -28,8 +28,8 @@ -define(MFA_HISTORY_LEN, 100). start_link() -> - MaxHistory = emqx:get_config([broker, cluster_call, mfa_max_history]), - CleanupMs = emqx:get_config([broker, cluster_call, mfa_cleanup_interval]), + MaxHistory = emqx:get_config([broker, hot_config_loader, mfa_max_history]), + CleanupMs = emqx:get_config([broker, hot_config_loader, mfa_cleanup_interval]), start_link(MaxHistory, CleanupMs). start_link(MaxHistory, CleanupMs) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index cb250f575..3fb986f1e 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -293,7 +293,7 @@ fields("broker") -> , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} , {"route_batch_clean", t(boolean(), undefined, true)} , {"perf", ref("perf")} - , {"cluster_call", ref("cluster_call")} + , {"hot_config_loader", ref("hot_config_loader")} ]; fields("perf") -> @@ -326,8 +326,8 @@ fields("sysmon_os") -> , {"procmem_high_watermark", t(percent(), undefined, "5%")} ]; -fields("cluster_call") -> - [{"retry_interval", t(duration(), undefined, "2s")} +fields("hot_config_loader") -> + [{"retry_interval", t(duration(), undefined, "1s")} , {"mfa_max_history", t(range(1, 500), undefined, 50)} , {"mfa_cleanup_interval", t(duration(), undefined, "5m")} ]; diff --git a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl index 5106183bb..b03137796 100644 --- a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx/test/emqx_cluster_rpc_SUITE.erl @@ -41,7 +41,7 @@ init_per_suite(Config) -> application:load(emqx), ok = ekka:start(), emqx_cluster_rpc:mnesia(copy), - emqx_config:put([broker, cluster_call], #{ + emqx_config:put([broker, hot_config_loader], #{ mfa_max_history => 100, mfa_cleanup_interval => 1000, retry_interval => 900 From 60c1c4edba0a6a492e959e861375eb609e1cd4a8 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 24 Aug 2021 10:56:11 +0800 Subject: [PATCH 136/306] feat: move cluster_call to emqx_machine --- apps/emqx/src/emqx_kernel_sup.erl | 2 -- apps/emqx/src/emqx_schema.erl | 7 ------ apps/emqx_machine/etc/emqx_machine.conf | 23 +++++++++++++++++++ .../include/emqx_cluster_rpc.hrl | 0 .../src/emqx_cluster_rpc.erl | 12 +++++++--- .../src/emqx_cluster_rpc_handler.erl | 10 ++++---- apps/emqx_machine/src/emqx_machine_schema.erl | 8 +++++++ apps/emqx_machine/src/emqx_machine_sup.erl | 4 +++- .../test/emqx_cluster_rpc_SUITE.erl | 11 ++++----- 9 files changed, 52 insertions(+), 25 deletions(-) rename apps/{emqx => emqx_machine}/include/emqx_cluster_rpc.hrl (100%) rename apps/{emqx => emqx_machine}/src/emqx_cluster_rpc.erl (97%) rename apps/{emqx => emqx_machine}/src/emqx_cluster_rpc_handler.erl (92%) rename apps/{emqx => emqx_machine}/test/emqx_cluster_rpc_SUITE.erl (96%) diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index 39a8d4fba..defe96182 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -29,8 +29,6 @@ init([]) -> {ok, {{one_for_one, 10, 100}, %% always start emqx_config_handler first to load the emqx.conf to emqx_config [ child_spec(emqx_config_handler, worker) - , child_spec(emqx_cluster_rpc, worker) - , child_spec(emqx_cluster_rpc_handler, worker) , child_spec(emqx_pool_sup, supervisor) , child_spec(emqx_hooks, worker) , child_spec(emqx_stats, worker) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 3fb986f1e..3bbeb1d07 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -293,7 +293,6 @@ fields("broker") -> , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} , {"route_batch_clean", t(boolean(), undefined, true)} , {"perf", ref("perf")} - , {"hot_config_loader", ref("hot_config_loader")} ]; fields("perf") -> @@ -326,12 +325,6 @@ fields("sysmon_os") -> , {"procmem_high_watermark", t(percent(), undefined, "5%")} ]; -fields("hot_config_loader") -> - [{"retry_interval", t(duration(), undefined, "1s")} - , {"mfa_max_history", t(range(1, 500), undefined, 50)} - , {"mfa_cleanup_interval", t(duration(), undefined, "5m")} - ]; - fields("alarm") -> [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} , {"size_limit", t(integer(), undefined, 1000)} diff --git a/apps/emqx_machine/etc/emqx_machine.conf b/apps/emqx_machine/etc/emqx_machine.conf index d21a19bc2..3ec09f2d4 100644 --- a/apps/emqx_machine/etc/emqx_machine.conf +++ b/apps/emqx_machine/etc/emqx_machine.conf @@ -89,6 +89,29 @@ node { ## Default: 23 backtrace_depth = 23 + cluster_call { + ## Time interval to retry after a failed call + ## + ## @doc node.cluster_call.retry_interval + ## ValueType: Duration + ## Default: 1s + retry_interval = 1s + ## Retain the maximum number of completed transactions (for queries) + ## + ## @doc node.cluster_call.max_history + ## ValueType: Integer + ## Range: [1, 500] + ## Default: 100 + max_history = 100 + ## Time interval to clear completed but stale transactions. + ## Ensure that the number of completed transactions is less than the max_history + ## + ## @doc node.cluster_call.cleanup_interval + ## ValueType: Duration + ## Default: 5m + cleanup_interval = 5m + } + } ##================================================================== diff --git a/apps/emqx/include/emqx_cluster_rpc.hrl b/apps/emqx_machine/include/emqx_cluster_rpc.hrl similarity index 100% rename from apps/emqx/include/emqx_cluster_rpc.hrl rename to apps/emqx_machine/include/emqx_cluster_rpc.hrl diff --git a/apps/emqx/src/emqx_cluster_rpc.erl b/apps/emqx_machine/src/emqx_cluster_rpc.erl similarity index 97% rename from apps/emqx/src/emqx_cluster_rpc.erl rename to apps/emqx_machine/src/emqx_cluster_rpc.erl index 5ddfe5742..b988d55fb 100644 --- a/apps/emqx/src/emqx_cluster_rpc.erl +++ b/apps/emqx_machine/src/emqx_cluster_rpc.erl @@ -32,8 +32,8 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --include("emqx.hrl"). --include("logger.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). -include("emqx_cluster_rpc.hrl"). -rlog_shard({?COMMON_SHARD, ?CLUSTER_MFA}). @@ -64,11 +64,17 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(cluster_rpc_commit, disc_copies). start_link() -> - RetryMs = emqx:get_config([broker, hot_config_loader, retry_interval]), + RetryMs = application:get_env(emqx_machine, cluster_call_retry_interval, 1000), start_link(node(), ?MODULE, RetryMs). start_link(Node, Name, RetryMs) -> gen_statem:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). +-spec multicall(Module, Function, Args) -> {ok, TnxId} | {error, Reason} when + Module :: module(), + Function :: atom(), + Args :: [term()], + TnxId :: pos_integer(), + Reason :: term(). multicall(M, F, A) -> multicall(M, F, A, timer:minutes(2)). diff --git a/apps/emqx/src/emqx_cluster_rpc_handler.erl b/apps/emqx_machine/src/emqx_cluster_rpc_handler.erl similarity index 92% rename from apps/emqx/src/emqx_cluster_rpc_handler.erl rename to apps/emqx_machine/src/emqx_cluster_rpc_handler.erl index 4da219165..803b7f9fc 100644 --- a/apps/emqx/src/emqx_cluster_rpc_handler.erl +++ b/apps/emqx_machine/src/emqx_cluster_rpc_handler.erl @@ -17,19 +17,17 @@ -behaviour(gen_server). --include("emqx.hrl"). --include("logger.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). -include("emqx_cluster_rpc.hrl"). -export([start_link/0, start_link/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(MFA_HISTORY_LEN, 100). - start_link() -> - MaxHistory = emqx:get_config([broker, hot_config_loader, mfa_max_history]), - CleanupMs = emqx:get_config([broker, hot_config_loader, mfa_cleanup_interval]), + MaxHistory = application:get_env(emqx_machine, cluster_call_max_history, 100), + CleanupMs = application:get_env(emqx_machine, cluster_call_cleanup_interval, 5*60*1000), start_link(MaxHistory, CleanupMs). start_link(MaxHistory, CleanupMs) -> diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 7dd193e63..2dde8aae3 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -139,6 +139,14 @@ fields("node") -> , {"dist_listen_min", t(range(1024, 65535), "kernel.inet_dist_listen_min", 6369)} , {"dist_listen_max", t(range(1024, 65535), "kernel.inet_dist_listen_max", 6369)} , {"backtrace_depth", t(integer(), "emqx_machine.backtrace_depth", 23)} + , {"cluster_call", ref("cluster_call")} + ]; + + +fields("cluster_call") -> + [ {"retry_interval", t(emqx_schema:duration(), "emqx_machine.retry_interval", "1s")} + , {"max_history", t(range(1, 500), "emqx_machine.max_history", 100)} + , {"cleanup_interval", t(emqx_schema:duration(), "emqx_machine.cleanup_interval", "5m")} ]; fields("rpc") -> diff --git a/apps/emqx_machine/src/emqx_machine_sup.erl b/apps/emqx_machine/src/emqx_machine_sup.erl index 0810eb267..798beee1c 100644 --- a/apps/emqx_machine/src/emqx_machine_sup.erl +++ b/apps/emqx_machine/src/emqx_machine_sup.erl @@ -31,7 +31,9 @@ start_link() -> init([]) -> GlobalGC = child_worker(emqx_global_gc, [], permanent), Terminator = child_worker(emqx_machine_terminator, [], transient), - Children = [GlobalGC, Terminator], + ClusterRpc = child_worker(emqx_cluster_rpc, [], permanent), + ClusterHandler = child_worker(emqx_cluster_rpc_handler, [], permanent), + Children = [GlobalGC, Terminator, ClusterRpc, ClusterHandler], SupFlags = #{strategy => one_for_one, intensity => 100, period => 10 diff --git a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl similarity index 96% rename from apps/emqx/test/emqx_cluster_rpc_SUITE.erl rename to apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl index b03137796..92a89790a 100644 --- a/apps/emqx/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl @@ -39,13 +39,12 @@ groups() -> []. init_per_suite(Config) -> application:load(emqx), + application:load(emqx_machine), ok = ekka:start(), - emqx_cluster_rpc:mnesia(copy), - emqx_config:put([broker, hot_config_loader], #{ - mfa_max_history => 100, - mfa_cleanup_interval => 1000, - retry_interval => 900 - }), + ok = ekka_rlog:wait_for_shards([emqx_common_shard], infinity), + application:set_env(emqx_machine, cluster_call_max_history, 100), + application:set_env(emqx_machine, cluster_call_clean_interval, 1000), + application:set_env(emqx_machine, cluster_call_retry_interval, 900), %%dbg:tracer(), %%dbg:p(all, c), %%dbg:tpl(emqx_cluster_rpc, cx), From 45285086209ab7f0a7da40cd1d596f294ec543e3 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Wed, 25 Aug 2021 16:38:01 +0800 Subject: [PATCH 137/306] feat: replace gen_statem by gen_server --- apps/emqx_machine/src/emqx_cluster_rpc.erl | 163 ++++++++---------- .../test/emqx_cluster_rpc_SUITE.erl | 14 +- 2 files changed, 69 insertions(+), 108 deletions(-) diff --git a/apps/emqx_machine/src/emqx_cluster_rpc.erl b/apps/emqx_machine/src/emqx_cluster_rpc.erl index b988d55fb..f7dc1eef9 100644 --- a/apps/emqx_machine/src/emqx_cluster_rpc.erl +++ b/apps/emqx_machine/src/emqx_cluster_rpc.erl @@ -14,14 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- -module(emqx_cluster_rpc). --behaviour(gen_statem). +-behaviour(gen_server). %% API -export([start_link/0, mnesia/1]). -export([multicall/3, multicall/4, query/1, reset/0, status/0]). --export([init/1, format_status/2, handle_event/4, terminate/3, - code_change/4, callback_mode/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + handle_continue/2, code_change/3]). -ifdef(TEST). -compile(export_all). @@ -40,8 +40,7 @@ -rlog_shard({?COMMON_SHARD, ?CLUSTER_COMMIT}). -define(CATCH_UP, catch_up). --define(REALTIME, realtime). --define(CATCH_UP_AFTER(_Ms_), {state_timeout, _Ms_, catch_up_delay}). +-define(TIMEOUT, timer:minutes(1)). %%%=================================================================== %%% API @@ -49,14 +48,14 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?CLUSTER_MFA, [ {type, ordered_set}, + {rlog_shard, ?COMMON_SHARD}, {disc_copies, [node()]}, - {local_content, true}, {record_name, cluster_rpc_mfa}, {attributes, record_info(fields, cluster_rpc_mfa)}]), ok = ekka_mnesia:create_table(?CLUSTER_COMMIT, [ {type, set}, + {rlog_shard, ?COMMON_SHARD}, {disc_copies, [node()]}, - {local_content, true}, {record_name, cluster_rpc_commit}, {attributes, record_info(fields, cluster_rpc_commit)}]); mnesia(copy) -> @@ -66,15 +65,16 @@ mnesia(copy) -> start_link() -> RetryMs = application:get_env(emqx_machine, cluster_call_retry_interval, 1000), start_link(node(), ?MODULE, RetryMs). + start_link(Node, Name, RetryMs) -> - gen_statem:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). + gen_server:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). -spec multicall(Module, Function, Args) -> {ok, TnxId} | {error, Reason} when Module :: module(), Function :: atom(), Args :: [term()], TnxId :: pos_integer(), - Reason :: term(). + Reason :: string(). multicall(M, F, A) -> multicall(M, F, A, timer:minutes(2)). @@ -84,14 +84,17 @@ multicall(M, F, A) -> Args :: [term()], TnxId :: pos_integer(), Timeout :: timeout(), - Reason :: term(). + Reason :: string(). multicall(M, F, A, Timeout) -> MFA = {initiate, {M, F, A}}, case ekka_rlog:role() of - core -> gen_statem:call(?MODULE, MFA, Timeout); + core -> gen_server:call(?MODULE, MFA, Timeout); replicant -> + %% the initiate transaction must happened on core node + %% make sure MFA(in the transaction) and the transaction on the same node + %% don't need rpc again inside transaction. case ekka_rlog_status:upstream_node(?COMMON_SHARD) of - {ok, Node} -> gen_statem:call({?MODULE, Node}, MFA, Timeout); + {ok, Node} -> gen_server:call({?MODULE, Node}, MFA, Timeout); disconnected -> {error, disconnected} end end. @@ -101,7 +104,7 @@ query(TnxId) -> transaction(fun trans_query/1, [TnxId]). -spec reset() -> reset. -reset() -> gen_statem:call(?MODULE, reset). +reset() -> gen_server:call(?MODULE, reset). -spec status() -> {'atomic', [map()]} | {'aborted', Reason :: term()}. status() -> @@ -114,75 +117,67 @@ status() -> %% @private init([Node, RetryMs]) -> {ok, _} = mnesia:subscribe({table, ?CLUSTER_MFA, simple}), - {ok, ?CATCH_UP, #{node => Node, retry_interval => RetryMs}, ?CATCH_UP_AFTER(0)}. - -callback_mode() -> - handle_event_function. + {ok, #{node => Node, retry_interval => RetryMs}, {continue, ?CATCH_UP}}. %% @private -format_status(Opt, [_PDict, StateName, Data]) -> - #{state => StateName, data => Data , opt => Opt}. +handle_continue(?CATCH_UP, State) -> + {noreply, State, catch_up(State)}. -%% @private -handle_event(state_timeout, catch_up_delay, _State, Data) -> - catch_up(Data); - -handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{} = MFARec, _AId}}, ?REALTIME, Data) -> - handle_mfa_write_event(MFARec, Data); -handle_event(info, {mnesia_table_event, {write, #cluster_rpc_mfa{}, _ActivityId}}, ?CATCH_UP, _Data) -> - {keep_state_and_data, [postpone, ?CATCH_UP_AFTER(0)]}; -%% we should catch up as soon as possible when we reset all. -handle_event(info, {mnesia_table_event, {delete,{schema, ?CLUSTER_MFA}, _Tid}}, _, _Data) -> - {keep_state_and_data, [?CATCH_UP_AFTER(0)]}; - -handle_event({call, From}, reset, _State, _Data) -> +handle_call(reset, _From, State) -> _ = ekka_mnesia:clear_table(?CLUSTER_COMMIT), _ = ekka_mnesia:clear_table(?CLUSTER_MFA), - {keep_state_and_data, [{reply, From, ok}, ?CATCH_UP_AFTER(0)]}; + {reply, ok, State, {continue, ?CATCH_UP}}; -handle_event({call, From}, {initiate, MFA}, ?REALTIME, Data = #{node := Node}) -> +handle_call({initiate, MFA}, _From, State = #{node := Node}) -> case transaction(fun init_mfa/2, [Node, MFA]) of {atomic, {ok, TnxId}} -> - {keep_state, Data, [{reply, From, {ok, TnxId}}]}; + {reply, {ok, TnxId}, State, {continue, ?CATCH_UP}}; {aborted, Reason} -> - {keep_state, Data, [{reply, From, {error, Reason}}]} - end; -handle_event({call, From}, {initiate, _MFA}, ?CATCH_UP, Data = #{retry_interval := RetryMs}) -> - case catch_up(Data) of - {next_state, ?REALTIME, Data} -> - {next_state, ?REALTIME, Data, [{postpone, true}]}; - _ -> - Reason = "There are still transactions that have not been executed.", - {keep_state_and_data, [{reply, From, {error, Reason}}, ?CATCH_UP_AFTER(RetryMs)]} + {reply, {error, Reason}, State, {continue, ?CATCH_UP}} end; +handle_call(_, _From, State) -> + {reply, ok, State, catch_up(State)}. -handle_event(_EventType, _EventContent, ?CATCH_UP, #{retry_interval := RetryMs}) -> - {keep_state_and_data, [?CATCH_UP_AFTER(RetryMs)]}; -handle_event(_EventType, _EventContent, _StateName, _Data) -> - keep_state_and_data. +handle_cast(_, State) -> + {noreply, State, catch_up(State)}. -terminate(_Reason, _StateName, _Data) -> +handle_info({mnesia_table_event, _}, State) -> + {noreply, State, catch_up(State)}; +handle_info(_, State) -> + {noreply, State, catch_up(State)}. + +terminate(_Reason, _Data) -> ok. -code_change(_OldVsn, StateName, Data, _Extra) -> - {ok, StateName, Data}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -catch_up(#{node := Node, retry_interval := RetryMs} = Data) -> +catch_up(#{node := Node, retry_interval := RetryMs} = State) -> case transaction(fun get_next_mfa/1, [Node]) of - {atomic, caught_up} -> {next_state, ?REALTIME, Data}; + {atomic, caught_up} -> ?TIMEOUT; {atomic, {still_lagging, NextId, MFA}} -> case apply_mfa(NextId, MFA) of ok -> case transaction(fun commit/2, [Node, NextId]) of - {atomic, ok} -> catch_up(Data); - _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} + {atomic, ok} -> catch_up(State); + Error -> + ?SLOG(error, #{ + msg => "mnesia write transaction failed", + node => Node, + nextId => NextId, + error => Error}), + RetryMs end; - _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} + _Error -> RetryMs end; - {aborted, _Reason} -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} + {aborted, Reason} -> + ?SLOG(error, #{ + msg => "get_next_mfa transaction failed", + node => Node, error => Reason}), + RetryMs end. get_next_mfa(Node) -> @@ -192,7 +187,9 @@ get_next_mfa(Node) -> LatestId = get_latest_id(), TnxId = max(LatestId - 1, 0), commit(Node, TnxId), - ?LOG(notice, "New node(~p) first catch up and start commit at ~p", [Node, TnxId]), + ?SLOG(notice, #{ + msg => "New node first catch up and start commit.", + node => Node, tnx_id => TnxId}), TnxId; [#cluster_rpc_commit{tnx_id = LastAppliedID}] -> LastAppliedID + 1 end, @@ -216,10 +213,15 @@ do_catch_up(ToTnxId, Node) -> {error, Reason} -> mnesia:abort(Reason); Other -> mnesia:abort(Other) end; - [#cluster_rpc_commit{tnx_id = LastAppliedId}] -> + [#cluster_rpc_commit{tnx_id = LastAppliedId}] -> Reason = lists:flatten(io_lib:format("~p catch up failed by LastAppliedId(~p) > ToTnxId(~p)", [Node, LastAppliedId, ToTnxId])), - ?LOG(error, Reason), + ?SLOG(error, #{ + msg => "catch up failed!", + last_applied_id => LastAppliedId, + node => Node, + to_tnx_id => ToTnxId + }), {error, Reason} end. @@ -232,35 +234,6 @@ get_latest_id() -> Id -> Id end. -handle_mfa_write_event(#cluster_rpc_mfa{tnx_id = EventId, mfa = MFA}, Data) -> - #{node := Node, retry_interval := RetryMs} = Data, - {atomic, LastAppliedId} = transaction(fun get_last_applied_id/2, [Node, EventId - 1]), - if LastAppliedId + 1 =:= EventId -> - case apply_mfa(EventId, MFA) of - ok -> - case transaction(fun commit/2, [Node, EventId]) of - {atomic, ok} -> - {next_state, ?REALTIME, Data}; - _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} - end; - _ -> {next_state, ?CATCH_UP, Data, [?CATCH_UP_AFTER(RetryMs)]} - end; - LastAppliedId >= EventId -> %% It's means the initiator receive self event or other receive stale event. - keep_state_and_data; - true -> - ?LOG(error, "LastAppliedId+1 - case mnesia:wread({?CLUSTER_COMMIT, Node}) of - [#cluster_rpc_commit{tnx_id = TnxId}] -> TnxId; - [] -> - commit(Node, Default), - Default - end. - init_mfa(Node, MFA) -> mnesia:write_lock_table(?CLUSTER_MFA), LatestId = get_latest_id(), @@ -311,12 +284,12 @@ trans_query(TnxId) -> apply_mfa(TnxId, {M, F, A} = MFA) -> try Res = erlang:apply(M, F, A), - case Res =:= ok of - true -> - ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => ok}); - false -> - ?SLOG(error, #{msg => "failed to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}) - end, + case Res =:= ok of + true -> + ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => ok}); + false -> + ?SLOG(error, #{msg => "failed to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}) + end, Res catch C : E -> diff --git a/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl index 92a89790a..b91131b93 100644 --- a/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl @@ -78,8 +78,6 @@ t_base_test(_Config) -> ?assertEqual(node(), maps:get(initiator, Query)), ?assert(maps:is_key(created_at, Query)), ?assertEqual(ok, receive_msg(3, test)), - SysStatus = lists:last(lists:last(element(4,sys:get_status(?NODE1)))), - ?assertEqual(#{data => #{node => node(),retry_interval => 900}, opt => normal, state => realtime}, SysStatus), sleep(400), {atomic, Status} = emqx_cluster_rpc:status(), ?assertEqual(3, length(Status)), @@ -111,12 +109,9 @@ t_commit_ok_but_apply_fail_on_other_node(_Config) -> {atomic, [Status]} = emqx_cluster_rpc:status(), ?assertEqual(MFA, maps:get(mfa, Status)), ?assertEqual(node(), maps:get(node, Status)), - ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), - ?assertEqual(catch_up, element(1, sys:get_state(?NODE2))), - ?assertEqual(catch_up, element(1, sys:get_state(?NODE3))), erlang:send(?NODE2, test), Res = gen_statem:call(?NODE2, {initiate, {M, F, A}}), - ?assertEqual({error, "There are still transactions that have not been executed."}, Res), + ?assertEqual({error, "MFA return not ok"}, Res), ok. t_catch_up_status_handle_next_commit(_Config) -> @@ -124,7 +119,6 @@ t_catch_up_status_handle_next_commit(_Config) -> {atomic, []} = emqx_cluster_rpc:status(), {M, F, A} = {?MODULE, failed_on_node_by_odd, [erlang:whereis(?NODE1)]}, {ok, _} = emqx_cluster_rpc:multicall(M, F, A), - ?assertEqual(catch_up, element(1, sys:get_state(?NODE2))), {ok, 2} = gen_statem:call(?NODE2, {initiate, {M, F, A}}), ok. @@ -139,17 +133,11 @@ t_commit_ok_apply_fail_on_other_node_then_recover(_Config) -> ?assertEqual([], L), ?assertEqual({io, format, ["test"]}, maps:get(mfa, Status)), ?assertEqual(node(), maps:get(node, Status)), - ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), - ?assertEqual(catch_up, element(1, sys:get_state(?NODE2))), - ?assertEqual(catch_up, element(1, sys:get_state(?NODE3))), sleep(4000), {atomic, [Status1]} = emqx_cluster_rpc:status(), ?assertEqual(Status, Status1), sleep(1600), {atomic, NewStatus} = emqx_cluster_rpc:status(), - ?assertEqual(realtime, element(1, sys:get_state(?NODE1))), - ?assertEqual(realtime, element(1, sys:get_state(?NODE2))), - ?assertEqual(realtime, element(1, sys:get_state(?NODE3))), ?assertEqual(3, length(NewStatus)), Pid = self(), MFAEcho = {M1, F1, A1} = {?MODULE, echo, [Pid, test]}, From 73238ed81f3a861aea66eaf1eb454421ac066421 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Thu, 26 Aug 2021 16:54:04 +0800 Subject: [PATCH 138/306] feat: emqx_resource support cluster_call --- .../src/simple_authn/emqx_authn_http.erl | 8 ++-- .../src/simple_authn/emqx_authn_mongodb.erl | 4 +- .../src/simple_authn/emqx_authn_mysql.erl | 6 +-- .../src/simple_authn/emqx_authn_pgsql.erl | 6 +-- .../src/simple_authn/emqx_authn_redis.erl | 6 +-- apps/emqx_authz/src/emqx_authz.erl | 4 +- .../src/emqx_data_bridge_api.erl | 4 +- .../src/emqx_data_bridge_monitor.erl | 2 +- apps/emqx_machine/src/emqx_cluster_rpc.erl | 41 +++++++++++-------- .../test/emqx_cluster_rpc_SUITE.erl | 20 ++++----- .../include/emqx_resource_utils.hrl | 28 +------------ apps/emqx_resource/src/emqx_resource.erl | 14 +++++-- .../src/emqx_resource_instance.erl | 2 +- apps/emqx_retainer/src/emqx_retainer.erl | 4 +- apps/emqx_rule_engine/include/rule_engine.hrl | 16 -------- .../emqx_rule_engine/src/emqx_rule_engine.erl | 18 ++++---- .../src/emqx_rule_registry.erl | 9 ++-- apps/emqx_rule_engine/src/emqx_rule_utils.erl | 6 +++ .../test/emqx_rule_engine_SUITE.erl | 4 ++ .../test/emqx_rule_monitor_SUITE.erl | 2 + 20 files changed, 93 insertions(+), 111 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 026df2415..43af1d9b4 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -89,7 +89,7 @@ headers(_) -> undefined. headers_no_content_type(type) -> map(); headers_no_content_type(converter) -> fun(Headers) -> - maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) end; headers_no_content_type(default) -> default_headers_no_content_type(); headers_no_content_type(_) -> undefined. @@ -129,9 +129,9 @@ create(#{ method := Method emqx_connector_http, Config#{base_url => maps:remove(query, URIMap), pool_type => random}) of - {ok, _} -> + {ok, already_created} -> {ok, State}; - {error, already_created} -> + {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} @@ -296,4 +296,4 @@ parse_body(<<"application/json">>, Body) -> parse_body(<<"application/x-www-form-urlencoded">>, Body) -> {ok, maps:from_list(cow_qs:parse_qs(Body))}; parse_body(ContentType, _) -> - {error, {unsupported_content_type, ContentType}}. \ No newline at end of file + {error, {unsupported_content_type, ContentType}}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 56ced0104..1ce145f35 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -106,9 +106,9 @@ create(#{ selector := Selector , '_unique'], Config), NState = State#{selector => NSelector}, case emqx_resource:create_local(Unique, emqx_connector_mongo, Config) of - {ok, _} -> + {ok, already_created} -> {ok, NState}; - {error, already_created} -> + {ok, _} -> {ok, NState}; {error, Reason} -> {error, Reason} 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 75a3392ec..59afa9671 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -83,9 +83,9 @@ create(#{ password_hash_algorithm := Algorithm query_timeout => QueryTimeout, '_unique' => Unique}, case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of - {ok, _} -> + {ok, already_created} -> {ok, State}; - {error, already_created} -> + {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} @@ -131,7 +131,7 @@ authenticate(#{password := Password} = Credential, destroy(#{'_unique' := Unique}) -> _ = emqx_resource:remove_local(Unique), ok. - + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ 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 44c7f7185..cce9ebd6f 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -71,9 +71,9 @@ create(#{ query := Query0 salt_position => SaltPosition, '_unique' => Unique}, case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of - {ok, _} -> + {ok, already_created} -> {ok, State}; - {error, already_created} -> + {ok, _} -> {ok, State}; {error, Reason} -> {error, Reason} @@ -119,7 +119,7 @@ authenticate(#{password := Password} = Credential, destroy(#{'_unique' := Unique}) -> _ = emqx_resource:remove_local(Unique), ok. - + %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 0c2696c0e..6eff345ed 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -89,9 +89,9 @@ create(#{ query := Query , '_unique'], Config), NState = State#{query => NQuery}, case emqx_resource:create_local(Unique, emqx_connector_redis, Config) of - {ok, _} -> + {ok, already_created} -> {ok, NState}; - {error, already_created} -> + {ok, _} -> {ok, NState}; {error, Reason} -> {error, Reason} @@ -176,7 +176,7 @@ check_fields(["superuser" | More], HasPassHash) -> check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). - + parse_key(Key) -> Tokens = re:split(Key, "(" ++ ?RE_PLACEHOLDER ++ ")", [{return, binary}, group, trim]), parse_key(Tokens, []). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f158322e1..bbe4caa6b 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -216,8 +216,8 @@ create_resource(#{type := DB, Config, []) of + {ok, already_created} -> ResourceID; {ok, _} -> ResourceID; - {error, already_created} -> ResourceID; {error, Reason} -> {error, Reason} end; create_resource(#{type := DB, @@ -228,8 +228,8 @@ create_resource(#{type := DB, list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), Config) of + {ok, already_created} -> ResourceID; {ok, _} -> ResourceID; - {error, already_created} -> ResourceID; {error, Reason} -> {error, Reason} end. diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl index dea3dcae8..6fe75e4ce 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl @@ -77,10 +77,10 @@ create_bridge(#{name := Name}, Params) -> case emqx_resource:check_and_create( emqx_data_bridge:name_to_resource_id(Name), emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of + {ok, already_created} -> + {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}}; {ok, Data} -> update_config_and_reply(Name, BridgeType, Config, Data); - {error, already_created} -> - {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}}; {error, Reason0} -> Reason = emqx_resource_api:stringnify(Reason0), {500, #{code => 102, message => <<"create bridge ", Name/binary, diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl index d408a8062..4917833ec 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl @@ -73,8 +73,8 @@ load_bridge(#{name := Name, type := Type, config := Config}) -> case emqx_resource:create_local( emqx_data_bridge:name_to_resource_id(Name), emqx_data_bridge:resource_type(Type), Config) of + {ok, already_created} -> ok; {ok, _} -> ok; - {error, already_created} -> ok; {error, Reason} -> error({load_bridge, Reason}) end. diff --git a/apps/emqx_machine/src/emqx_cluster_rpc.erl b/apps/emqx_machine/src/emqx_cluster_rpc.erl index f7dc1eef9..4c7576ff3 100644 --- a/apps/emqx_machine/src/emqx_cluster_rpc.erl +++ b/apps/emqx_machine/src/emqx_cluster_rpc.erl @@ -130,8 +130,8 @@ handle_call(reset, _From, State) -> handle_call({initiate, MFA}, _From, State = #{node := Node}) -> case transaction(fun init_mfa/2, [Node, MFA]) of - {atomic, {ok, TnxId}} -> - {reply, {ok, TnxId}, State, {continue, ?CATCH_UP}}; + {atomic, {ok, TnxId, Result}} -> + {reply, {ok, TnxId, Result}, State, {continue, ?CATCH_UP}}; {aborted, Reason} -> {reply, {error, Reason}, State, {continue, ?CATCH_UP}} end; @@ -159,8 +159,9 @@ catch_up(#{node := Node, retry_interval := RetryMs} = State) -> case transaction(fun get_next_mfa/1, [Node]) of {atomic, caught_up} -> ?TIMEOUT; {atomic, {still_lagging, NextId, MFA}} -> - case apply_mfa(NextId, MFA) of - ok -> + {Succeed, _} = apply_mfa(NextId, MFA), + case Succeed of + true -> case transaction(fun commit/2, [Node, NextId]) of {atomic, ok} -> catch_up(State); Error -> @@ -171,7 +172,7 @@ catch_up(#{node := Node, retry_interval := RetryMs} = State) -> error => Error}), RetryMs end; - _Error -> RetryMs + false -> RetryMs end; {aborted, Reason} -> ?SLOG(error, #{ @@ -209,9 +210,8 @@ do_catch_up(ToTnxId, Node) -> CurTnxId = LastAppliedId + 1, [#cluster_rpc_mfa{mfa = MFA}] = mnesia:read(?CLUSTER_MFA, CurTnxId), case apply_mfa(CurTnxId, MFA) of - ok -> ok = commit(Node, CurTnxId); - {error, Reason} -> mnesia:abort(Reason); - Other -> mnesia:abort(Other) + {true, _Result} -> ok = commit(Node, CurTnxId); + {false, Error} -> mnesia:abort(Error) end; [#cluster_rpc_commit{tnx_id = LastAppliedId}] -> Reason = lists:flatten(io_lib:format("~p catch up failed by LastAppliedId(~p) > ToTnxId(~p)", @@ -243,9 +243,8 @@ init_mfa(Node, MFA) -> ok = mnesia:write(?CLUSTER_MFA, MFARec, write), ok = commit(Node, TnxId), case apply_mfa(TnxId, MFA) of - ok -> {ok, TnxId}; - {error, Reason} -> mnesia:abort(Reason); - Other -> mnesia:abort(Other) + {true, Result} -> {ok, TnxId, Result}; + {false, Error} -> mnesia:abort(Error) end. do_catch_up_in_one_trans(LatestId, Node) -> @@ -284,15 +283,21 @@ trans_query(TnxId) -> apply_mfa(TnxId, {M, F, A} = MFA) -> try Res = erlang:apply(M, F, A), - case Res =:= ok of - true -> - ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => ok}); - false -> - ?SLOG(error, #{msg => "failed to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}) + Succeed = + case Res of + ok -> + ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}), + true; + {ok, _} -> + ?SLOG(notice, #{msg => "succeeded to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}), + true; + _ -> + ?SLOG(error, #{msg => "failed to apply MFA", tnx_id => TnxId, mfa => MFA, result => Res}), + false end, - Res + {Succeed, Res} catch C : E -> ?SLOG(critical, #{msg => "crash to apply MFA", tnx_id => TnxId, mfa => MFA, exception => C, reason => E}), - {error, lists:flatten(io_lib:format("TnxId(~p) apply MFA(~p) crash", [TnxId, MFA]))} + {false, lists:flatten(io_lib:format("TnxId(~p) apply MFA(~p) crash", [TnxId, MFA]))} end. diff --git a/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl index b91131b93..26ad28f3e 100644 --- a/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx_machine/test/emqx_cluster_rpc_SUITE.erl @@ -72,7 +72,7 @@ t_base_test(_Config) -> ?assertEqual(emqx_cluster_rpc:status(), {atomic, []}), Pid = self(), MFA = {M, F, A} = {?MODULE, echo, [Pid, test]}, - {ok, TnxId} = emqx_cluster_rpc:multicall(M, F, A), + {ok, TnxId, ok} = emqx_cluster_rpc:multicall(M, F, A), {atomic, Query} = emqx_cluster_rpc:query(TnxId), ?assertEqual(MFA, maps:get(mfa, Query)), ?assertEqual(node(), maps:get(initiator, Query)), @@ -105,7 +105,7 @@ t_commit_ok_but_apply_fail_on_other_node(_Config) -> emqx_cluster_rpc:reset(), {atomic, []} = emqx_cluster_rpc:status(), MFA = {M, F, A} = {?MODULE, failed_on_node, [erlang:whereis(?NODE1)]}, - {ok, _} = emqx_cluster_rpc:multicall(M, F, A), + {ok, _, ok} = emqx_cluster_rpc:multicall(M, F, A), {atomic, [Status]} = emqx_cluster_rpc:status(), ?assertEqual(MFA, maps:get(mfa, Status)), ?assertEqual(node(), maps:get(node, Status)), @@ -118,7 +118,7 @@ t_catch_up_status_handle_next_commit(_Config) -> emqx_cluster_rpc:reset(), {atomic, []} = emqx_cluster_rpc:status(), {M, F, A} = {?MODULE, failed_on_node_by_odd, [erlang:whereis(?NODE1)]}, - {ok, _} = emqx_cluster_rpc:multicall(M, F, A), + {ok, _, ok} = emqx_cluster_rpc:multicall(M, F, A), {ok, 2} = gen_statem:call(?NODE2, {initiate, {M, F, A}}), ok. @@ -127,21 +127,21 @@ t_commit_ok_apply_fail_on_other_node_then_recover(_Config) -> {atomic, []} = emqx_cluster_rpc:status(), Now = erlang:system_time(second), {M, F, A} = {?MODULE, failed_on_other_recover_after_5_second, [erlang:whereis(?NODE1), Now]}, - {ok, _} = emqx_cluster_rpc:multicall(M, F, A), - {ok, _} = emqx_cluster_rpc:multicall(io, format, ["test"]), + {ok, _, ok} = emqx_cluster_rpc:multicall(M, F, A), + {ok, _, ok} = emqx_cluster_rpc:multicall(io, format, ["test"]), {atomic, [Status|L]} = emqx_cluster_rpc:status(), ?assertEqual([], L), ?assertEqual({io, format, ["test"]}, maps:get(mfa, Status)), ?assertEqual(node(), maps:get(node, Status)), - sleep(4000), + sleep(3000), {atomic, [Status1]} = emqx_cluster_rpc:status(), ?assertEqual(Status, Status1), - sleep(1600), + sleep(2600), {atomic, NewStatus} = emqx_cluster_rpc:status(), ?assertEqual(3, length(NewStatus)), Pid = self(), MFAEcho = {M1, F1, A1} = {?MODULE, echo, [Pid, test]}, - {ok, TnxId} = emqx_cluster_rpc:multicall(M1, F1, A1), + {ok, TnxId, ok} = emqx_cluster_rpc:multicall(M1, F1, A1), {atomic, Query} = emqx_cluster_rpc:query(TnxId), ?assertEqual(MFAEcho, maps:get(mfa, Query)), ?assertEqual(node(), maps:get(initiator, Query)), @@ -157,12 +157,12 @@ t_del_stale_mfa(_Config) -> Keys2 = lists:seq(51, 150), Ids = [begin - {ok, TnxId} = emqx_cluster_rpc:multicall(M, F, A), + {ok, TnxId, ok} = emqx_cluster_rpc:multicall(M, F, A), TnxId end || _ <- Keys], ?assertEqual(Keys, Ids), Ids2 = [begin - {ok, TnxId} = emqx_cluster_rpc:multicall(M, F, A), + {ok, TnxId, ok} = emqx_cluster_rpc:multicall(M, F, A), TnxId end || _ <- Keys2], ?assertEqual(Keys2, Ids2), sleep(1200), diff --git a/apps/emqx_resource/include/emqx_resource_utils.hrl b/apps/emqx_resource/include/emqx_resource_utils.hrl index a20a17e89..4c3a2a749 100644 --- a/apps/emqx_resource/include/emqx_resource_utils.hrl +++ b/apps/emqx_resource/include/emqx_resource_utils.hrl @@ -13,32 +13,6 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --define(CLUSTER_CALL(Func, Args), ?CLUSTER_CALL(Func, Args, ok)). - --define(CLUSTER_CALL(Func, Args, ResParttern), -%% ekka_mnesia:running_nodes() - fun() -> - case LocalResult = erlang:apply(?MODULE, Func, Args) of - ResParttern -> - case rpc:multicall(nodes(), ?MODULE, Func, Args, 5000) of - {ResL, []} -> - Filter = fun - (ResParttern) -> false; - ({badrpc, {'EXIT', {undef, [{?MODULE, Func0, _, []}]}}}) - when Func0 =:= Func -> false; - (_) -> true - end, - case lists:filter(Filter, ResL) of - [] -> LocalResult; - ErrL -> {error, ErrL} - end; - {ResL, BadNodes} -> - {error, {failed_on_nodes, BadNodes, ResL}} - end; - ErrorResult -> - {error, ErrorResult} - end - end()). -define(SAFE_CALL(_EXP_), ?SAFE_CALL(_EXP_, _ = do_nothing)). @@ -50,4 +24,4 @@ _EXP_ON_FAIL_, {error, {_EXCLASS_, _EXCPTION_, _ST_}} end - end()). \ No newline at end of file + end()). diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index 1fce5e122..4bc1d20f0 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -157,7 +157,7 @@ query_failed({_, {OnFailed, Args}}) -> -spec create(instance_id(), resource_type(), resource_config()) -> {ok, resource_data()} | {error, Reason :: term()}. create(InstId, ResourceType, Config) -> - ?CLUSTER_CALL(create_local, [InstId, ResourceType, Config], {ok, _}). + cluster_call(create_local, [InstId, ResourceType, Config]). -spec create_local(instance_id(), resource_type(), resource_config()) -> {ok, resource_data()} | {error, Reason :: term()}. @@ -167,7 +167,7 @@ create_local(InstId, ResourceType, Config) -> -spec create_dry_run(instance_id(), resource_type(), resource_config()) -> ok | {error, Reason :: term()}. create_dry_run(InstId, ResourceType, Config) -> - ?CLUSTER_CALL(create_dry_run_local, [InstId, ResourceType, Config]). + cluster_call(create_dry_run_local, [InstId, ResourceType, Config]). -spec create_dry_run_local(instance_id(), resource_type(), resource_config()) -> ok | {error, Reason :: term()}. @@ -177,7 +177,7 @@ create_dry_run_local(InstId, ResourceType, Config) -> -spec update(instance_id(), resource_type(), resource_config(), term()) -> {ok, resource_data()} | {error, Reason :: term()}. update(InstId, ResourceType, Config, Params) -> - ?CLUSTER_CALL(update_local, [InstId, ResourceType, Config, Params], {ok, _}). + cluster_call(update_local, [InstId, ResourceType, Config, Params]). -spec update_local(instance_id(), resource_type(), resource_config(), term()) -> {ok, resource_data()} | {error, Reason :: term()}. @@ -186,7 +186,7 @@ update_local(InstId, ResourceType, Config, Params) -> -spec remove(instance_id()) -> ok | {error, Reason :: term()}. remove(InstId) -> - ?CLUSTER_CALL(remove_local, [InstId]). + cluster_call(remove_local, [InstId]). -spec remove_local(instance_id()) -> ok | {error, Reason :: term()}. remove_local(InstId) -> @@ -335,3 +335,9 @@ safe_apply(Func, Args) -> str(S) when is_binary(S) -> binary_to_list(S); str(S) when is_list(S) -> S. + +cluster_call(Func, Args) -> + case emqx_cluster_rpc:multicall(?MODULE, Func, Args) of + {ok, _TxnId, Result} -> Result; + Failed -> Failed + end. diff --git a/apps/emqx_resource/src/emqx_resource_instance.erl b/apps/emqx_resource/src/emqx_resource_instance.erl index 8e0624c75..84b5a1f7c 100644 --- a/apps/emqx_resource/src/emqx_resource_instance.erl +++ b/apps/emqx_resource/src/emqx_resource_instance.erl @@ -162,7 +162,7 @@ do_update(InstId, ResourceType, NewConfig, Params) -> do_create(InstId, ResourceType, Config) -> case lookup(InstId) of - {ok, _} -> {error, already_created}; + {ok, _} -> {ok, already_created}; _ -> case emqx_resource:call_start(InstId, ResourceType, Config) of {ok, ResourceState} -> diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 7a37abbee..3fab5958d 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -443,9 +443,9 @@ create_resource(Context, #{type := DB} = Config) -> ResourceID, list_to_existing_atom(io_lib:format("~s_~s", [emqx_connector, DB])), Config) of - {ok, _} -> + {ok, already_created} -> Context#{resource_id => ResourceID}; - {error, already_created} -> + {ok, _} -> Context#{resource_id => ResourceID}; {error, Reason} -> error({load_config_error, Reason}) diff --git a/apps/emqx_rule_engine/include/rule_engine.hrl b/apps/emqx_rule_engine/include/rule_engine.hrl index 568724263..760495f6b 100644 --- a/apps/emqx_rule_engine/include/rule_engine.hrl +++ b/apps/emqx_rule_engine/include/rule_engine.hrl @@ -155,22 +155,6 @@ end end()). --define(CLUSTER_CALL(Func, Args), ?CLUSTER_CALL(Func, Args, ok)). - --define(CLUSTER_CALL(Func, Args, ResParttern), - fun() -> case rpc:multicall(ekka_mnesia:running_nodes(), ?MODULE, Func, Args, 5000) of - {ResL, []} -> - case lists:filter(fun(ResParttern) -> false; (_) -> true end, ResL) of - [] -> ResL; - ErrL -> - ?LOG(error, "cluster_call error found, ResL: ~p", [ResL]), - throw({Func, ErrL}) - end; - {ResL, BadNodes} -> - ?LOG(error, "cluster_call bad nodes found: ~p, ResL: ~p", [BadNodes, ResL]), - throw({Func, {failed_on_nodes, BadNodes}}) - end end()). - %% Tables -define(RULE_TAB, emqx_rule). -define(ACTION_TAB, emqx_rule_action). diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index c2ccf2c29..0b3ffb603 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -216,7 +216,7 @@ delete_rule(RuleId) -> case emqx_rule_registry:get_rule(RuleId) of {ok, Rule = #rule{actions = Actions}} -> try - _ = ?CLUSTER_CALL(clear_rule, [Rule]), + _ = emqx_rule_utils:cluster_call(?MODULE, clear_rule, [Rule]), ok = emqx_rule_registry:remove_rule(Rule) catch Error:Reason:ST -> @@ -242,7 +242,7 @@ create_resource(#{type := Type, config := Config0} = Params) -> ok = emqx_rule_registry:add_resource(Resource), %% Note that we will return OK in case of resource creation failure, %% A timer is started to re-start the resource later. - catch _ = ?CLUSTER_CALL(init_resource, [M, F, ResId, Config]), + catch _ = emqx_rule_utils:cluster_call(?MODULE, init_resource, [M, F, ResId, Config]), {ok, Resource}; not_found -> {error, {resource_type_not_found, Type}} @@ -280,7 +280,7 @@ do_check_and_update_resource(#{id := Id, type := Type, description := NewDescrip Config = emqx_rule_validator:validate_params(NewConfig, ParamSpec), case test_resource(#{type => Type, config => NewConfig}) of ok -> - _ = ?CLUSTER_CALL(init_resource, [Module, Create, Id, Config]), + _ = emqx_rule_utils:cluster_call(?MODULE, init_resource, [Module, Create, Id, Config]), emqx_rule_registry:add_resource(#resource{ id = Id, type = Type, @@ -319,8 +319,8 @@ test_resource(#{type := Type, config := Config0}) -> Config = emqx_rule_validator:validate_params(Config0, ParamSpec), ResId = resource_id(), try - _ = ?CLUSTER_CALL(init_resource, [ModC, Create, ResId, Config]), - _ = ?CLUSTER_CALL(clear_resource, [ModD, Destroy, ResId]), + _ = emqx_rule_utils:cluster_call(?MODULE, init_resource, [ModC, Create, ResId, Config]), + _ = emqx_rule_utils:cluster_call(?MODULE, clear_resource, [ModD, Destroy, ResId]), ok catch throw:Reason -> {error, Reason} @@ -359,7 +359,7 @@ delete_resource(ResId) -> try case emqx_rule_registry:remove_resource(ResId) of ok -> - _ = ?CLUSTER_CALL(clear_resource, [ModD, Destroy, ResId]), + _ = emqx_rule_utils:cluster_call(?MODULE, clear_resource, [ModD, Destroy, ResId]), ok; {error, _} = R -> R end @@ -426,7 +426,7 @@ prepare_action(#{name := Name, args := Args0} = Action, NeedInit) -> ActionInstId = maps:get(id, Action, action_instance_id(Name)), case NeedInit of true -> - _ = ?CLUSTER_CALL(init_action, [Mod, Create, ActionInstId, + _ = emqx_rule_utils:cluster_call(?MODULE, init_action, [Mod, Create, ActionInstId, with_resource_params(Args)]), ok; false -> ok @@ -485,7 +485,7 @@ may_update_rule_params(Rule, Params = #{on_action_failed := OnFailed}) -> may_update_rule_params(Rule = #rule{actions = OldActions}, Params = #{actions := Actions}) -> %% prepare new actions before removing old ones NewActions = prepare_actions(Actions, maps:get(enabled, Params, true)), - _ = ?CLUSTER_CALL(clear_actions, [OldActions]), + _ = emqx_rule_utils:cluster_call(?MODULE, clear_actions, [OldActions]), may_update_rule_params(Rule#rule{actions = NewActions}, maps:remove(actions, Params)); may_update_rule_params(Rule, _Params) -> %% ignore all the unsupported params Rule. @@ -631,7 +631,7 @@ refresh_actions(Actions, Pred) -> true -> {ok, #action{module = Mod, on_create = Create}} = emqx_rule_registry:find_action(ActName), - _ = ?CLUSTER_CALL(init_action, [Mod, Create, Id, with_resource_params(Args)]), + _ = emqx_rule_utils:cluster_call(?MODULE, init_action, [Mod, Create, Id, with_resource_params(Args)]), refresh_actions(Fallbacks, Pred); false -> ok end diff --git a/apps/emqx_rule_engine/src/emqx_rule_registry.erl b/apps/emqx_rule_engine/src/emqx_rule_registry.erl index c0bd5de7b..096534585 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_registry.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_registry.erl @@ -221,7 +221,7 @@ remove_rules(Rules) -> %% @private insert_rule(Rule) -> - _ = ?CLUSTER_CALL(load_hooks_for_rule, [Rule]), + _ = emqx_rule_utils:cluster_call(?MODULE, load_hooks_for_rule, [Rule]), mnesia:write(?RULE_TAB, Rule, write). %% @private @@ -231,7 +231,7 @@ delete_rule(RuleId) when is_binary(RuleId) -> not_found -> ok end; delete_rule(Rule) -> - _ = ?CLUSTER_CALL(unload_hooks_for_rule, [Rule]), + _ = emqx_rule_utils:cluster_call(?MODULE, unload_hooks_for_rule, [Rule]), mnesia:delete_object(?RULE_TAB, Rule, write). load_hooks_for_rule(#rule{for = Topics}) -> @@ -476,10 +476,11 @@ code_change(_OldVsn, State, _Extra) -> get_all_records(Tab) -> %mnesia:dirty_match_object(Tab, mnesia:table_info(Tab, wild_pattern)). - %% Wrapping ets to a r/o transaction to avoid reading inconsistent + %% Wrapping ets to a transaction to avoid reading inconsistent + %% ( nest cluster_call transaction, no a r/o transaction) %% data during shard bootstrap {atomic, Ret} = - ekka_mnesia:ro_transaction(?RULE_ENGINE_SHARD, + ekka_mnesia:transaction(?RULE_ENGINE_SHARD, fun() -> ets:tab2list(Tab) end), diff --git a/apps/emqx_rule_engine/src/emqx_rule_utils.erl b/apps/emqx_rule_engine/src/emqx_rule_utils.erl index 3791b1386..2d978ee0d 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_utils.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_utils.erl @@ -55,6 +55,8 @@ , can_topic_match_oneof/2 ]). +-export([cluster_call/3]). + -compile({no_auto_import, [ float/1 ]}). @@ -356,3 +358,7 @@ can_topic_match_oneof(Topic, Filters) -> lists:any(fun(Fltr) -> emqx_topic:match(Topic, Fltr) end, Filters). + +cluster_call(Module, Func, Args) -> + {ok, _TnxId, Result} = emqx_cluster_rpc:multicall(Module, Func, Args), + Result. 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 bf4dcb30e..47eb4faad 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -148,6 +148,7 @@ groups() -> %%------------------------------------------------------------------------------ init_per_suite(Config) -> + application:load(emqx_machine), ok = ekka_mnesia:start(), ok = emqx_rule_registry:mnesia(boot), ok = emqx_ct_helpers:start_apps([emqx_rule_engine], fun set_special_configs/1), @@ -181,6 +182,7 @@ end_per_group(_Groupname, _Config) -> %%------------------------------------------------------------------------------ init_per_testcase(t_events, Config) -> + {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), ok = emqx_rule_engine:load_providers(), init_events_counters(), ok = emqx_rule_registry:register_resource_types([make_simple_resource_type(simple_resource_type)]), @@ -214,6 +216,7 @@ init_per_testcase(Test, Config) ;Test =:= t_sqlselect_multi_actoins_3_1 ;Test =:= t_sqlselect_multi_actoins_4 -> + emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), ok = emqx_rule_engine:load_providers(), ok = emqx_rule_registry:add_action( #action{name = 'crash_action', app = ?APP, @@ -252,6 +255,7 @@ init_per_testcase(Test, Config) {connsql, SQL} | Config]; init_per_testcase(_TestCase, Config) -> + emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), ok = emqx_rule_registry:register_resource_types( [#resource_type{ name = built_in, diff --git a/apps/emqx_rule_engine/test/emqx_rule_monitor_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_monitor_SUITE.erl index 67c59e26c..62f538f43 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_monitor_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_monitor_SUITE.erl @@ -39,6 +39,7 @@ groups() -> ]. init_per_suite(Config) -> + application:load(emqx_machine), ok = ekka_mnesia:start(), ok = emqx_rule_registry:mnesia(boot), Config. @@ -65,6 +66,7 @@ end_per_testcase(_, Config) -> t_restart_resource(_) -> {ok, _} = emqx_rule_monitor:start_link(), + emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc,1000), ok = emqx_rule_registry:register_resource_types( [#resource_type{ name = test_res_1, From e92255114f6be852895a051774649489fae7088b Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Thu, 26 Aug 2021 17:22:42 +0800 Subject: [PATCH 139/306] fix: update delayed config --- apps/emqx_modules/src/emqx_delayed_api.erl | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 24f4822ab..7b694bb31 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -92,7 +92,7 @@ status_api() -> <<"200">> => schema(conf_schema(), <<"Enable or disable delayed successfully">>), <<"400">> => - error_schema(<<"Already disabled or enabled">>, [?ALREADY_ENABLED, ?ALREADY_DISABLED]) + error_schema(<<"Max limit illegality">>, [?BAD_REQUEST]) } } }, @@ -174,7 +174,7 @@ update_config(Config) -> {400, #{code => Code, message => Message}} end. generate_config(Config) -> - generate_config(Config, [fun generate_enable/1, fun generate_max_delayed_messages/1]). + generate_config(Config, [fun generate_max_delayed_messages/1]). generate_config(Config, []) -> {ok, Config}; @@ -186,18 +186,6 @@ generate_config(Config, [Fun | Tail]) -> {error, CodeMessage} end. -generate_enable(Config = #{<<"enable">> := Enable}) -> - case {Enable =:= maps:get(enable, get_status()), Enable} of - {true, true} -> - {error, {?ALREADY_ENABLED, <<"Delayed message status is already enabled">>}}; - {true, false} -> - {error, {?ALREADY_DISABLED, <<"Delayed message status is already disable">>}}; - _ -> - {ok, Config} - end; -generate_enable(Config) -> - {ok, Config}. - generate_max_delayed_messages(Config = #{<<"max_delayed_messages">> := Max}) when Max >= 0 -> {ok, Config}; generate_max_delayed_messages(#{<<"max_delayed_messages">> := Max}) when Max < 0 -> From 436dba83b872b9e4bdb2560a0d34658c80f13c20 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 26 Aug 2021 17:54:16 +0800 Subject: [PATCH 140/306] feat(logger): update configs for logger at runtime --- apps/emqx/src/emqx_alarm.erl | 7 +- apps/emqx/src/emqx_config.erl | 2 +- apps/emqx/src/emqx_config_handler.erl | 37 +++++++--- apps/emqx/src/emqx_kernel_sup.erl | 1 + apps/emqx/src/emqx_logger.erl | 74 +++++++++++++++++++ apps/emqx/src/emqx_map_lib.erl | 12 +-- apps/emqx_authn/src/emqx_authn.erl | 16 ++-- apps/emqx_authz/src/emqx_authz.erl | 20 ++--- .../src/emqx_mgmt_api_configs.erl | 18 ++--- 9 files changed, 138 insertions(+), 49 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 8f3e1c568..11a2805f3 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -28,7 +28,7 @@ -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). --export([post_config_update/3]). +-export([post_config_update/4]). -export([ start_link/0 , stop/0 @@ -148,7 +148,7 @@ get_alarms(activated) -> get_alarms(deactivated) -> gen_server:call(?MODULE, {get_alarms, deactivated}). -post_config_update(_, #{validity_period := Period0}, _OldConf) -> +post_config_update(_, #{validity_period := Period0}, _OldConf, _AppEnv) -> ?MODULE ! {update_timer, Period0}, ok. @@ -179,7 +179,7 @@ format(_) -> init([]) -> deactivate_all_alarms(), - emqx_config_handler:add_handler([alarm], ?MODULE), + ok = 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. @@ -255,6 +255,7 @@ handle_info(Info, State) -> {noreply, State}. terminate(_Reason, _State) -> + ok = emqx_config_handler:remove_handler([alarm]), ok. code_change(_OldVsn, State, _Extra) -> diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 516831600..e995f1d74 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -80,7 +80,7 @@ error:badarg -> EXP_ON_FAIL end). --export_type([update_request/0, raw_config/0, config/0, +-export_type([update_request/0, raw_config/0, config/0, app_envs/0, update_opts/0, update_cmd/0, update_args/0, update_error/0, update_result/0]). diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index 7c66656ce..b45f89538 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -24,6 +24,7 @@ %% API functions -export([ start_link/0 , add_handler/2 + , remove_handler/1 , update_config/3 , merge_to_old_config/2 ]). @@ -49,14 +50,15 @@ -type handlers() :: #{emqx_config:config_key() => handlers(), ?MOD => handler_name()}. -optional_callbacks([ pre_config_update/2 - , post_config_update/3 + , post_config_update/4 ]). -callback pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> {ok, emqx_config:update_request()} | {error, term()}. -callback post_config_update(emqx_config:update_request(), emqx_config:config(), - emqx_config:config()) -> ok | {ok, Result::any()} | {error, Reason::term()}. + emqx_config:config(), emqx_config:app_envs()) -> + ok | {ok, Result::any()} | {error, Reason::term()}. -type state() :: #{ handlers := handlers(), @@ -76,6 +78,10 @@ update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> add_handler(ConfKeyPath, HandlerName) -> gen_server:call(?MODULE, {add_child, ConfKeyPath, HandlerName}). +-spec remove_handler(emqx_config:config_key_path()) -> ok. +remove_handler(ConfKeyPath) -> + gen_server:call(?MODULE, {remove_child, ConfKeyPath}). + %%============================================================================ -spec init(term()) -> {ok, state()}. @@ -87,6 +93,11 @@ handle_call({add_child, ConfKeyPath, HandlerName}, _From, {reply, ok, State#{handlers => emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; +handle_call({remove_child, ConfKeyPath}, _From, + State = #{handlers := Handlers}) -> + {reply, ok, State#{handlers => + emqx_map_lib:deep_remove(ConfKeyPath, Handlers)}}; + handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> Reply = try @@ -152,7 +163,7 @@ check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, Override FullRawConf = with_full_raw_confs(NewRawConf), {AppEnvs, CheckedConf} = emqx_config:check_config(SchemaModule, FullRawConf), NewConf = maps:with(maps:keys(OldConf), CheckedConf), - case do_post_config_update(ConfKeyPath, Handlers, OldConf, NewConf, UpdateArgs, #{}) of + case do_post_config_update(ConfKeyPath, Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, #{}) of {ok, Result0} -> case save_configs(ConfKeyPath, AppEnvs, NewConf, NewRawConf, OverrideConf, UpdateArgs) of @@ -163,16 +174,18 @@ check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf, Override Error -> Error end. -do_post_config_update([], Handlers, OldConf, NewConf, UpdateArgs, Result) -> - call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs), Result); -do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, UpdateArgs, Result) -> +do_post_config_update([], Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, Result) -> + call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, up_req(UpdateArgs), Result); +do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEnvs, UpdateArgs, + Result) -> SubOldConf = get_sub_config(ConfKey, OldConf), SubNewConf = get_sub_config(ConfKey, NewConf), SubHandlers = maps:get(ConfKey, Handlers, #{}), - case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, UpdateArgs, - Result) of + case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, AppEnvs, + UpdateArgs, Result) of {ok, Result1} -> - call_post_config_update(Handlers, OldConf, NewConf, up_req(UpdateArgs), Result1); + call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, up_req(UpdateArgs), + Result1); Error -> Error end. @@ -192,11 +205,11 @@ call_pre_config_update(Handlers, OldRawConf, UpdateReq) -> false -> merge_to_old_config(UpdateReq, OldRawConf) end. -call_post_config_update(Handlers, OldConf, NewConf, UpdateReq, Result) -> +call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, UpdateReq, Result) -> HandlerName = maps:get(?MOD, Handlers, undefined), - case erlang:function_exported(HandlerName, post_config_update, 3) of + case erlang:function_exported(HandlerName, post_config_update, 4) of true -> - case HandlerName:post_config_update(UpdateReq, NewConf, OldConf) of + case HandlerName:post_config_update(UpdateReq, NewConf, OldConf, AppEnvs) of ok -> {ok, Result}; {ok, Result1} -> {ok, Result#{HandlerName => Result1}}; {error, Reason} -> {error, {post_config_update, HandlerName, Reason}} diff --git a/apps/emqx/src/emqx_kernel_sup.erl b/apps/emqx/src/emqx_kernel_sup.erl index defe96182..b854c60b7 100644 --- a/apps/emqx/src/emqx_kernel_sup.erl +++ b/apps/emqx/src/emqx_kernel_sup.erl @@ -34,6 +34,7 @@ init([]) -> , child_spec(emqx_stats, worker) , child_spec(emqx_metrics, worker) , child_spec(emqx_ctl, worker) + , child_spec(emqx_logger, worker) ]}}. child_spec(M, Type) -> diff --git a/apps/emqx/src/emqx_logger.erl b/apps/emqx/src/emqx_logger.erl index 986ba11e0..85450a22a 100644 --- a/apps/emqx/src/emqx_logger.erl +++ b/apps/emqx/src/emqx_logger.erl @@ -18,6 +18,19 @@ -compile({no_auto_import, [error/1]}). +-behaviour(gen_server). +-behaviour(emqx_config_handler). + +%% gen_server callbacks +-export([ start_link/0 + , init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + %% Logs -export([ debug/1 , debug/2 @@ -47,6 +60,7 @@ ]). -export([ get_primary_log_level/0 + , tune_primary_log_level/0 , get_log_handlers/0 , get_log_handlers/1 , get_log_handler/1 @@ -56,6 +70,8 @@ , stop_log_handler/1 ]). +-export([post_config_update/4]). + -type(peername_str() :: list()). -type(logger_dst() :: file:filename() | console | unknown). -type(logger_handler_info() :: #{ @@ -66,6 +82,54 @@ }). -define(stopped_handlers, {?MODULE, stopped_handlers}). +-define(CONF_PATH, [log]). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- +init([]) -> + ok = emqx_config_handler:add_handler(?CONF_PATH, ?MODULE), + {ok, #{}}. + +handle_call({update_config, AppEnvs}, _From, State) -> + io:format("----new appenvs: ~p~n", [AppEnvs]), + OldEnvs = application:get_env(kernel, logger, []), + NewEnvs = proplists:get_value(logger, proplists:get_value(kernel, AppEnvs, []), []), + io:format("----new logger configs: ~p~n", [NewEnvs]), + ok = application:set_env(kernel, logger, NewEnvs), + io:format("----1~n", []), + _ = [logger:remove_handler(HandlerId) || {handler, HandlerId, _Mod, _Conf} <- OldEnvs], + io:format("----2~n", []), + _ = [logger:add_handler(HandlerId, Mod, Conf) || {handler, HandlerId, Mod, Conf} <- NewEnvs], + io:format("----3~n", []), + ok = tune_primary_log_level(), + {reply, ok, State}; + +handle_call(_Req, _From, State) -> + {reply, ignored, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok = emqx_config_handler:remove_handler(?CONF_PATH), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%%-------------------------------------------------------------------- +%% emqx_config_handler callbacks +%%-------------------------------------------------------------------- +post_config_update(_Req, _NewConf, _OldConf, AppEnvs) -> + gen_server:call(?MODULE, {update_config, AppEnvs}, 5000). %%-------------------------------------------------------------------- %% APIs @@ -159,6 +223,16 @@ get_primary_log_level() -> #{level := Level} = logger:get_primary_config(), Level. +-spec tune_primary_log_level() -> ok. +tune_primary_log_level() -> + LowestLevel = lists:foldl(fun(#{level := Level}, OldLevel) -> + case logger:compare_levels(Level, OldLevel) of + lt -> Level; + _ -> OldLevel + end + end, get_primary_log_level(), get_log_handlers()), + set_primary_log_level(LowestLevel). + -spec(set_primary_log_level(logger:level()) -> ok | {error, term()}). set_primary_log_level(Level) -> logger:set_primary_config(level, Level). diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 468553193..0486c10da 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -62,12 +62,12 @@ 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) -> - 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), +deep_put([], Map, Data) when is_map(Map) -> + Data; +deep_put([], _Map, Data) -> %% not map, replace it + Data; +deep_put([Key | KeyPath], Map, Data) -> + SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Data), Map#{Key => SubMap}. -spec deep_remove(config_key_path(), map()) -> map(). diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 84629be78..1034682e5 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -24,7 +24,7 @@ -include_lib("emqx/include/logger.hrl"). -export([ pre_config_update/2 - , post_config_update/3 + , post_config_update/4 , update_config/2 ]). @@ -137,11 +137,11 @@ pre_config_update({move_authenticator, ID, Position}, OldConfig) -> end end. -post_config_update({enable, true}, _NewConfig, _OldConfig) -> +post_config_update({enable, true}, _NewConfig, _OldConfig, _AppEnvs) -> emqx_authn:enable(); -post_config_update({enable, false}, _NewConfig, _OldConfig) -> +post_config_update({enable, false}, _NewConfig, _OldConfig, _AppEnvs) -> emqx_authn:disable(); -post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> +post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> case lists:filter( fun(#{name := N}) -> N =:= Name @@ -151,12 +151,12 @@ post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _Ol [_Config | _] -> {error, name_has_be_used} end; -post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig) -> +post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig, _AppEnvs) -> case delete_authenticator(?CHAIN, ID) of ok -> ok; {error, Reason} -> throw(Reason) end; -post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> +post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> case lists:filter( fun(#{name := N}) -> N =:= Name @@ -166,7 +166,7 @@ post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, [_Config | _] -> {error, name_has_be_used} end; -post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) -> +post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> case lists:filter( fun(#{name := N}) -> N =:= Name @@ -176,7 +176,7 @@ post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, [_Config | _] -> {error, name_has_be_used} end; -post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig) -> +post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig, _AppEnvs) -> NPosition = case Position of <<"top">> -> top; <<"bottom">> -> bottom; diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f158322e1..3905f0138 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -34,7 +34,7 @@ , authorize/5 ]). --export([post_config_update/3, pre_config_update/2]). +-export([post_config_update/4, pre_config_update/2]). -define(CONF_KEY_PATH, [authorization_rules, rules]). @@ -107,23 +107,23 @@ pre_config_update({_, Rules}, _Conf) when is_list(Rules)-> %% overwrite the entire config! {ok, Rules}. -post_config_update(_, undefined, _Conf) -> +post_config_update(_, undefined, _Conf, _AppEnvs) -> ok; -post_config_update({move, Id, <<"top">>}, _NewRules, _OldRules) -> +post_config_update({move, Id, <<"top">>}, _NewRules, _OldRules, _AppEnvs) -> InitedRules = lookup(), {Index, Rule} = find_rule_by_id(Id, InitedRules), {Rules1, Rules2 } = lists:split(Index, InitedRules), Rules3 = [Rule] ++ lists:droplast(Rules1) ++ Rules2, ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, <<"bottom">>}, _NewRules, _OldRules) -> +post_config_update({move, Id, <<"bottom">>}, _NewRules, _OldRules, _AppEnvs) -> InitedRules = lookup(), {Index, Rule} = find_rule_by_id(Id, InitedRules), {Rules1, Rules2 } = lists:split(Index, InitedRules), Rules3 = lists:droplast(Rules1) ++ Rules2 ++ [Rule], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewRules, _OldRules) -> +post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewRules, _OldRules, _AppEnvs) -> InitedRules = lookup(), {_, Rule0} = find_rule_by_id(Id, InitedRules), {Index, Rule1} = find_rule_by_id(BeforeId, InitedRules), @@ -134,7 +134,7 @@ post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewRules, _OldRules ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewRules, _OldRules) -> +post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewRules, _OldRules, _AppEnvs) -> InitedRules = lookup(), {_, Rule} = find_rule_by_id(Id, InitedRules), {Index, _} = find_rule_by_id(AfterId, InitedRules), @@ -145,17 +145,17 @@ post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewRules, _OldRules) ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({head, Rules}, _NewRules, _OldConf) -> +post_config_update({head, Rules}, _NewRules, _OldConf, _AppEnvs) -> InitedRules = [init_provider(R) || R <- check_rules(Rules)], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules ++ lookup()]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({tail, Rules}, _NewRules, _OldConf) -> +post_config_update({tail, Rules}, _NewRules, _OldConf, _AppEnvs) -> InitedRules = [init_provider(R) || R <- check_rules(Rules)], emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedRules]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({{replace_once, Id}, Rule}, _NewRules, _OldConf) when is_map(Rule) -> +post_config_update({{replace_once, Id}, Rule}, _NewRules, _OldConf, _AppEnvs) when is_map(Rule) -> OldInitedRules = lookup(), {Index, OldRule} = find_rule_by_id(Id, OldInitedRules), case maps:get(type, OldRule, undefined) of @@ -169,7 +169,7 @@ post_config_update({{replace_once, Id}, Rule}, _NewRules, _OldConf) when is_map( ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldRules1) ++ InitedRules ++ OldRules2]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update(_, NewRules, _OldConf) -> +post_config_update(_, NewRules, _OldConf, _AppEnvs) -> %% overwrite the entire config! OldInitedRules = lookup(), InitedRules = [init_provider(Rule) || Rule <- NewRules], diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 9a5c3e361..d335afbb2 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -46,7 +46,13 @@ -define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))). --define(CORE_CONFS, [node, log, alarm, zones, cluster, rpc, broker, sysmon, +-define(CORE_CONFS, [ + %% from emqx_machine_schema + log, rpc, + %% from emqx_schema + zones, mqtt, flapping_detect, force_shutdown, force_gc, conn_congestion, rate_limit, quota, + broker, alarm, sysmon, + %% from other apps emqx_dashboard, emqx_management]). api_spec() -> @@ -109,9 +115,9 @@ config(get, _Params, Req) -> {404, #{code => 'NOT_FOUND', message => <<"Config cannot found">>}} end; -config(put, _Params, Req) -> +config(put, #{body := Body}, Req) -> Path = conf_path(Req), - {ok, #{raw_config := RawConf}} = emqx:update_config(Path, http_body(Req), + {ok, #{raw_config := RawConf}} = emqx:update_config(Path, Body, #{rawconf_with_defaults => true}), {200, emqx_map_lib:jsonable_map(RawConf)}. @@ -142,12 +148,6 @@ conf_path_reset(Req) -> <<"/api/v5", ?PREFIX_RESET, Path/binary>> = cowboy_req:path(Req), string:lexemes(Path, "/ "). -http_body(Req) -> - {ok, Body, _} = cowboy_req:read_body(Req), - try jsx:decode(Body, [{return_maps, true}]) - catch error:badarg -> Body - end. - get_conf_schema(Conf, MaxDepth) -> get_conf_schema([], maps:to_list(Conf), [], MaxDepth). From 4c1e0802d982c4c1fa2f8f22a375269662bbf639 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 26 Aug 2021 18:34:12 +0800 Subject: [PATCH 141/306] fix(logger): remove debug code from emqx_logger --- apps/emqx/src/emqx_logger.erl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/emqx/src/emqx_logger.erl b/apps/emqx/src/emqx_logger.erl index 85450a22a..29f5bd597 100644 --- a/apps/emqx/src/emqx_logger.erl +++ b/apps/emqx/src/emqx_logger.erl @@ -95,16 +95,11 @@ init([]) -> {ok, #{}}. handle_call({update_config, AppEnvs}, _From, State) -> - io:format("----new appenvs: ~p~n", [AppEnvs]), OldEnvs = application:get_env(kernel, logger, []), NewEnvs = proplists:get_value(logger, proplists:get_value(kernel, AppEnvs, []), []), - io:format("----new logger configs: ~p~n", [NewEnvs]), ok = application:set_env(kernel, logger, NewEnvs), - io:format("----1~n", []), _ = [logger:remove_handler(HandlerId) || {handler, HandlerId, _Mod, _Conf} <- OldEnvs], - io:format("----2~n", []), _ = [logger:add_handler(HandlerId, Mod, Conf) || {handler, HandlerId, Mod, Conf} <- NewEnvs], - io:format("----3~n", []), ok = tune_primary_log_level(), {reply, ok, State}; From c1c24af002a7e4b51ff42c5f335a42c3693e22dd Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Thu, 26 Aug 2021 18:12:21 +0800 Subject: [PATCH 142/306] fix: dialyzer warning --- apps/emqx_authz/src/emqx_authz.erl | 1 - apps/emqx_machine/src/emqx_cluster_rpc.erl | 4 ++-- apps/emqx_resource/src/emqx_resource.erl | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index bbe4caa6b..e082b9995 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -216,7 +216,6 @@ create_resource(#{type := DB, Config, []) of - {ok, already_created} -> ResourceID; {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} end; diff --git a/apps/emqx_machine/src/emqx_cluster_rpc.erl b/apps/emqx_machine/src/emqx_cluster_rpc.erl index 4c7576ff3..c2b2d086d 100644 --- a/apps/emqx_machine/src/emqx_cluster_rpc.erl +++ b/apps/emqx_machine/src/emqx_cluster_rpc.erl @@ -69,7 +69,7 @@ start_link() -> start_link(Node, Name, RetryMs) -> gen_server:start_link({local, Name}, ?MODULE, [Node, RetryMs], []). --spec multicall(Module, Function, Args) -> {ok, TnxId} | {error, Reason} when +-spec multicall(Module, Function, Args) -> {ok, TnxId, term()} | {error, Reason} when Module :: module(), Function :: atom(), Args :: [term()], @@ -78,7 +78,7 @@ start_link(Node, Name, RetryMs) -> multicall(M, F, A) -> multicall(M, F, A, timer:minutes(2)). --spec multicall(Module, Function, Args, Timeout) -> {ok, TnxId} |{error, Reason} when +-spec multicall(Module, Function, Args, Timeout) -> {ok, TnxId, term()} |{error, Reason} when Module :: module(), Function :: atom(), Args :: [term()], diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index 4bc1d20f0..cad32bcb2 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -155,12 +155,12 @@ query_failed({_, {OnFailed, Args}}) -> %% APIs for resource instances %% ================================================================================= -spec create(instance_id(), resource_type(), resource_config()) -> - {ok, resource_data()} | {error, Reason :: term()}. + {ok, resource_data() |'already_created'} | {error, Reason :: term()}. create(InstId, ResourceType, Config) -> cluster_call(create_local, [InstId, ResourceType, Config]). -spec create_local(instance_id(), resource_type(), resource_config()) -> - {ok, resource_data()} | {error, Reason :: term()}. + {ok, resource_data() | 'already_created'} | {error, Reason :: term()}. create_local(InstId, ResourceType, Config) -> call_instance(InstId, {create, InstId, ResourceType, Config}). @@ -285,7 +285,7 @@ check_config(ResourceType, RawConfigTerm) -> end. -spec check_and_create(instance_id(), resource_type(), raw_resource_config()) -> - {ok, resource_data()} | {error, term()}. + {ok, resource_data() |'already_created'} | {error, term()}. check_and_create(InstId, ResourceType, RawConfig) -> check_and_do(ResourceType, RawConfig, fun(InstConf) -> create(InstId, ResourceType, InstConf) end). From e35a6c73504c096c1ed08797e16543664151d3c5 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 27 Aug 2021 11:15:46 +0800 Subject: [PATCH 143/306] chore: cluster_call early aborted --- apps/emqx_machine/src/emqx_cluster_rpc.erl | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/apps/emqx_machine/src/emqx_cluster_rpc.erl b/apps/emqx_machine/src/emqx_cluster_rpc.erl index c2b2d086d..346ca5025 100644 --- a/apps/emqx_machine/src/emqx_cluster_rpc.erl +++ b/apps/emqx_machine/src/emqx_cluster_rpc.erl @@ -156,7 +156,7 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%=================================================================== catch_up(#{node := Node, retry_interval := RetryMs} = State) -> - case transaction(fun get_next_mfa/1, [Node]) of + case transaction(fun read_next_mfa/1, [Node]) of {atomic, caught_up} -> ?TIMEOUT; {atomic, {still_lagging, NextId, MFA}} -> {Succeed, _} = apply_mfa(NextId, MFA), @@ -166,22 +166,19 @@ catch_up(#{node := Node, retry_interval := RetryMs} = State) -> {atomic, ok} -> catch_up(State); Error -> ?SLOG(error, #{ - msg => "mnesia write transaction failed", - node => Node, - nextId => NextId, + msg => "failed to commit applied call", + applied_id => NextId, error => Error}), RetryMs end; false -> RetryMs end; {aborted, Reason} -> - ?SLOG(error, #{ - msg => "get_next_mfa transaction failed", - node => Node, error => Reason}), + ?SLOG(error, #{msg => "read_next_mfa transaction failed", error => Reason}), RetryMs end. -get_next_mfa(Node) -> +read_next_mfa(Node) -> NextId = case mnesia:wread({?CLUSTER_COMMIT, Node}) of [] -> @@ -219,10 +216,9 @@ do_catch_up(ToTnxId, Node) -> ?SLOG(error, #{ msg => "catch up failed!", last_applied_id => LastAppliedId, - node => Node, to_tnx_id => ToTnxId }), - {error, Reason} + mnesia:abort(Reason) end. commit(Node, TnxId) -> @@ -250,8 +246,7 @@ init_mfa(Node, MFA) -> do_catch_up_in_one_trans(LatestId, Node) -> case do_catch_up(LatestId, Node) of caught_up -> ok; - ok -> do_catch_up_in_one_trans(LatestId, Node); - {error, Reason} -> mnesia:abort(Reason) + ok -> do_catch_up_in_one_trans(LatestId, Node) end. transaction(Func, Args) -> From 86231f795d71588c021d4cee39ac334feb7d4c89 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 27 Aug 2021 13:46:39 +0800 Subject: [PATCH 144/306] refactor: close managment http 8081 (#5564) --- .ci/build_packages/tests.sh | 6 +- .ci/docker-compose-file/haproxy/haproxy.cfg | 6 +- .github/workflows/build_packages.yaml | 2 +- .github/workflows/build_slim_packages.yaml | 2 +- apps/emqx/rebar.config | 2 +- .../emqx_hocon_plugin/rebar.config | 2 +- .../emqx_mini_plugin/rebar.config | 2 +- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 33 ++- .../src/emqx_dashboard_schema.erl | 2 +- apps/emqx_machine/src/emqx_machine_schema.erl | 1 - apps/emqx_management/etc/emqx_management.conf | 42 --- apps/emqx_management/include/emqx_mgmt.hrl | 46 +--- .../src/emqx_management_schema.erl | 34 +-- apps/emqx_management/src/emqx_mgmt.erl | 4 +- .../src/emqx_mgmt_api_apps.erl | 214 --------------- .../src/emqx_mgmt_api_authz.erl | 47 ---- .../src/emqx_mgmt_api_banned.erl | 166 ------------ .../src/emqx_mgmt_api_brokers.erl | 47 ---- .../src/emqx_mgmt_api_plugins.erl | 113 -------- .../src/emqx_mgmt_api_pubsub.erl | 247 ------------------ apps/emqx_management/src/emqx_mgmt_app.erl | 4 +- apps/emqx_management/src/emqx_mgmt_auth.erl | 216 --------------- apps/emqx_management/src/emqx_mgmt_cli.erl | 50 ---- apps/emqx_management/src/emqx_mgmt_http.erl | 136 ---------- .../test/emqx_mgmt_alarms_api_SUITE.erl | 15 +- .../test/emqx_mgmt_api_test_util.erl | 36 ++- .../test/emqx_mgmt_apps_api_SUITE.erl | 110 -------- .../test/emqx_mgmt_clients_api_SUITE.erl | 15 +- .../test/emqx_mgmt_listeners_api_SUITE.erl | 13 +- .../test/emqx_mgmt_metrics_api_SUITE.erl | 13 +- .../test/emqx_mgmt_nodes_api_SUITE.erl | 13 +- .../test/emqx_mgmt_publish_api_SUITE.erl | 15 +- .../test/emqx_mgmt_routes_api_SUITE.erl | 15 +- .../test/emqx_mgmt_stats_api_SUITE.erl | 13 +- .../test/emqx_mgmt_subscription_api_SUITE.erl | 15 +- deploy/charts/emqx/templates/StatefulSet.yaml | 4 +- deploy/charts/emqx/templates/service.yaml | 13 - 37 files changed, 88 insertions(+), 1626 deletions(-) delete mode 100644 apps/emqx_management/src/emqx_mgmt_api_apps.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_api_authz.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_api_banned.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_api_brokers.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_api_plugins.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_api_pubsub.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_auth.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_http.erl delete mode 100644 apps/emqx_management/test/emqx_mgmt_apps_api_SUITE.erl diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index d3a2a6858..87c19621a 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/api/v5/status >/dev/null 2>&1; do + while ! curl http://localhost:18083/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx running error" @@ -139,7 +139,7 @@ EOF exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do + while ! curl http://localhost:18083/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx running error" @@ -168,7 +168,7 @@ EOF exit 1 fi IDLE_TIME=0 - while ! curl http://localhost:8081/api/v5/status >/dev/null 2>&1; do + while ! curl http://localhost:18083/api/v5/status >/dev/null 2>&1; do if [ $IDLE_TIME -gt 10 ] then echo "emqx service error" diff --git a/.ci/docker-compose-file/haproxy/haproxy.cfg b/.ci/docker-compose-file/haproxy/haproxy.cfg index 4361ccadb..b658789da 100644 --- a/.ci/docker-compose-file/haproxy/haproxy.cfg +++ b/.ci/docker-compose-file/haproxy/haproxy.cfg @@ -33,7 +33,7 @@ defaults frontend emqx_mgmt mode tcp option tcplog - bind *:8081 + bind *:18083 default_backend emqx_mgmt_back frontend emqx_dashboard @@ -45,8 +45,8 @@ frontend emqx_dashboard backend emqx_mgmt_back mode http # balance static-rr - server emqx-1 node1.emqx.io:8081 - server emqx-2 node2.emqx.io:8081 + server emqx-1 node1.emqx.io:18083 + server emqx-2 node2.emqx.io:18083 backend emqx_dashboard_back mode http diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index f42835a18..0c6d065a9 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -197,7 +197,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/api/v5/status > /dev/null; then + if curl -fs 127.0.0.1:18083/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 9e19889fc..9578c6f9d 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -113,7 +113,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/api/v5/status > /dev/null; then + if curl -fs 127.0.0.1:18083/api/v5/status > /dev/null; then ready='yes' break fi diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 60f257f30..c2229ce0f 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -28,7 +28,7 @@ [{deps, [ meck , {bbmustache,"1.10.0"} - , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers", {branch,"hocon"}}} + , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config index 888a03bc4..57bf1245c 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/rebar.config @@ -17,7 +17,7 @@ {profiles, [{test, [ - {deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}} + {deps, [{emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} ]} ]} ]}. diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config index 4c49da1dc..b2bf39c55 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config @@ -17,7 +17,7 @@ {profiles, [{test, [ - {deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}} + {deps, [{emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} ]} ]} ]}. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 56d0170c1..b88afc57b 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -33,7 +33,7 @@ , auth_header/2 ]). --define(HOST, "http://127.0.0.1:8081/"). +-define(HOST, "http://127.0.0.1:18083/"). -define(API_VERSION, "v5"). -define(BASE_PATH, "api"). @@ -100,11 +100,9 @@ init_per_suite(Config) -> meck:expect(emqx_resource, health_check, fun(_) -> ok end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), - ekka_mnesia:start(), - emqx_mgmt_auth:mnesia(boot), - ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_management, emqx_authz], fun set_special_configs/1), + + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -112,13 +110,20 @@ init_per_suite(Config) -> end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), - emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_management]), + emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]), meck:unload(emqx_resource), ok. -set_special_configs(emqx_management) -> - emqx_config:put([emqx_management], #{listeners => [#{protocol => http, port => 8081}], - applications =>[#{id => "admin", secret => "public"}]}), +set_special_configs(emqx_dashboard) -> + Config = #{ + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, + emqx_config:put([emqx_dashboard], Config), ok; set_special_configs(emqx_authz) -> emqx_config:put([authorization_rules], #{rules => []}), @@ -225,8 +230,8 @@ t_move_rule(_) -> request(Method, Url, Body) -> Request = case Body of - [] -> {Url, [auth_header("admin", "public")]}; - _ -> {Url, [auth_header("admin", "public")], "application/json", jsx:encode(Body)} + [] -> {Url, [auth_header_()]}; + _ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)} end, ct:pal("Method: ~p, Request: ~p", [Method, Request]), case httpc:request(Method, Request, [], [{body_format, binary}]) of @@ -245,3 +250,9 @@ uri(Parts) when is_list(Parts) -> get_rules(Result) -> maps:get(<<"rules">>, jsx:decode(Result), []). + +auth_header_() -> + Username = <<"admin">>, + Password = <<"public">>, + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 6cca17390..018061ff6 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -33,7 +33,7 @@ fields("emqx_dashboard") -> fields("http") -> [ {"protocol", hoconsc:enum([http, https])} - , {"port", emqx_schema:t(integer(), undefined, 8081)} + , {"port", emqx_schema:t(integer(), undefined, 18083)} , {"num_acceptors", emqx_schema:t(integer(), undefined, 4)} , {"max_connections", emqx_schema:t(integer(), undefined, 512)} , {"backlog", emqx_schema:t(integer(), undefined, 1024)} diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 7dd193e63..a8cba047d 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -51,7 +51,6 @@ , emqx_auto_subscribe_schema , emqx_bridge_mqtt_schema , emqx_modules_schema - , emqx_management_schema , emqx_dashboard_schema , emqx_gateway_schema , emqx_prometheus_schema diff --git a/apps/emqx_management/etc/emqx_management.conf b/apps/emqx_management/etc/emqx_management.conf index 8517aec7f..e69de29bb 100644 --- a/apps/emqx_management/etc/emqx_management.conf +++ b/apps/emqx_management/etc/emqx_management.conf @@ -1,42 +0,0 @@ -emqx_management { - applications = [ - { - id = "admin", - 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 = true - inet6 = false - ipv6_v6only = false - } -## , -## { -## protocol: https -## port: 8081 -## acceptors: 2 -## backlog: 512 -## send_timeout: 15s -## send_timeout_close: true -## 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/include/emqx_mgmt.hrl b/apps/emqx_management/include/emqx_mgmt.hrl index 40baec4e1..ea8687dcd 100644 --- a/apps/emqx_management/include/emqx_mgmt.hrl +++ b/apps/emqx_management/include/emqx_mgmt.hrl @@ -14,50 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% Return Codes --define(SUCCESS, 0). %% Success --define(ERROR1, 101). %% badrpc --define(ERROR2, 102). %% Unknown error --define(ERROR3, 103). %% Username or password error --define(ERROR4, 104). %% Empty username or password --define(ERROR5, 105). %% User does not exist --define(ERROR6, 106). %% Admin can not be deleted --define(ERROR7, 107). %% Missing request parameter --define(ERROR8, 108). %% Request parameter type error --define(ERROR9, 109). %% Request parameter is not a json --define(ERROR10, 110). %% Plugin has been loaded --define(ERROR11, 111). %% Plugin has been unloaded --define(ERROR12, 112). %% Client not online --define(ERROR13, 113). %% User already exist --define(ERROR14, 114). %% OldPassword error --define(ERROR15, 115). %% bad topic - --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]). +-define(MAX_ROW_LIMIT, 100). diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl index f9543697f..a0da91d86 100644 --- a/apps/emqx_management/src/emqx_management_schema.erl +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -22,36 +22,6 @@ -export([ structs/0 , fields/1]). -structs() -> ["emqx_management"]. +structs() -> []. -fields("emqx_management") -> - [ {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", 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)} - , {"backlog", emqx_schema:t(integer(), undefined, 1024)} - , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "15s")} - , {"send_timeout_close", emqx_schema:t(boolean(), undefined, true)} - , {"inet6", emqx_schema:t(boolean(), undefined, false)} - , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} - ]; - -fields("https") -> - emqx_schema:ssl(#{enable => true}) ++ fields("http"). - -max_row_limit(type) -> integer(); -max_row_limit(default) -> 1000; -max_row_limit(nullable) -> false; -max_row_limit(_) -> undefined. +fields(_) -> []. diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 4a7fefb2d..29f8de0d3 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -114,8 +114,6 @@ -export([ return/0 , return/1]). --define(MAX_ROW_LIMIT, 10000). - -define(APP, emqx_management). %% TODO: remove these function after all api use minirest version 1.X @@ -590,7 +588,7 @@ check_row_limit([Tab|Tables], Limit) -> end. max_row_limit() -> - emqx:get_config([?APP, max_row_limit], ?MAX_ROW_LIMIT). + ?MAX_ROW_LIMIT. table_size(Tab) -> ets:info(Tab, size). diff --git a/apps/emqx_management/src/emqx_mgmt_api_apps.erl b/apps/emqx_management/src/emqx_mgmt_api_apps.erl deleted file mode 100644 index 64a84b381..000000000 --- a/apps/emqx_management/src/emqx_mgmt_api_apps.erl +++ /dev/null @@ -1,214 +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_apps). - --behaviour(minirest_api). - --import(emqx_mgmt_util, [ schema/1 - , schema/2 - , object_schema/1 - , object_schema/2 - , object_array_schema/2 - , error_schema/1 - , error_schema/2 - , properties/1 - ]). - --export([api_spec/0]). - --export([ apps/2 - , app/2]). - --define(BAD_APP_ID, 'BAD_APP_ID'). --define(APP_ID_NOT_FOUND, <<"{\"code\": \"BAD_APP_ID\", \"reason\": \"App id not found\"}">>). - -api_spec() -> - { - [apps_api(), app_api()], - [] - }. - -properties() -> - properties([ - {app_id, string, <<"App ID">>}, - {secret, string, <<"App Secret">>}, - {name, string, <<"Dsiplay name">>}, - {desc, string, <<"App description">>}, - {status, boolean, <<"Enable or disable">>}, - {expired, integer, <<"Expired time">>} - ]). - -%% not export schema -app_without_secret_schema() -> - maps:without([secret], properties()). - -apps_api() -> - Metadata = #{ - get => #{ - description => <<"List EMQ X apps">>, - responses => #{ - <<"200">> => - object_array_schema(app_without_secret_schema(), <<"All apps">>) - } - }, - post => #{ - description => <<"EMQ X create apps">>, - 'requestBody' => schema(app), - responses => #{ - <<"200">> => - schema(app_secret, <<"Create apps">>), - <<"400">> => - error_schema(<<"App ID already exist">>, [?BAD_APP_ID]) - } - } - }, - {"/apps", Metadata, apps}. - -app_api() -> - Metadata = #{ - get => #{ - description => <<"EMQ X apps">>, - parameters => [#{ - name => app_id, - in => path, - required => true, - schema => #{type => string}}], - responses => #{ - <<"404">> => - error_schema(<<"App id not found">>), - <<"200">> => - object_schema(app_without_secret_schema(), <<"Get App">>)}}, - delete => #{ - description => <<"EMQ X apps">>, - parameters => [#{ - name => app_id, - in => path, - required => true, - schema => #{type => string} - }], - responses => #{ - <<"200">> => schema(<<"Remove app ok">>)}}, - put => #{ - description => <<"EMQ X update apps">>, - parameters => [#{ - name => app_id, - in => path, - required => true, - schema => #{type => string} - }], - 'requestBody' => object_schema(app_without_secret_schema()), - responses => #{ - <<"404">> => - error_schema(<<"App id not found">>, [?BAD_APP_ID]), - <<"200">> => - object_schema(app_without_secret_schema(), <<"Update ok">>)}}}, - {"/apps/:app_id", Metadata, app}. - -%%%============================================================================================== -%% parameters trans -apps(get, _Params) -> - list(#{}); - -apps(post, #{body := Data}) -> - Parameters = #{ - app_id => maps:get(<<"app_id">>, Data), - name => maps:get(<<"name">>, Data), - secret => maps:get(<<"secret">>, Data), - desc => maps:get(<<"desc">>, Data), - status => maps:get(<<"status">>, Data), - expired => maps:get(<<"expired">>, Data, undefined) - }, - create(Parameters). - -app(get, #{bindings := #{app_id := AppID}}) -> - lookup(#{app_id => AppID}); - -app(delete, #{bindings := #{app_id := AppID}}) -> - delete(#{app_id => AppID}); - -app(put, #{bindings := #{app_id := AppID}, body := Data}) -> - Parameters = #{ - app_id => AppID, - name => maps:get(<<"name">>, Data), - desc => maps:get(<<"desc">>, Data), - status => maps:get(<<"status">>, Data), - expired => maps:get(<<"expired">>, Data, undefined) - }, - update(Parameters). - - -%%%============================================================================================== -%% api apply -list(_) -> - {200, [format_without_app_secret(Apps) || Apps <- emqx_mgmt_auth:list_apps()]}. - -create(#{app_id := AppID, name := Name, secret := Secret, - desc := Desc, status := Status, expired := Expired}) -> - case emqx_mgmt_auth:add_app(AppID, Name, Secret, Desc, Status, Expired) of - {ok, AppSecret} -> - {200, #{secret => AppSecret}}; - {error, alread_existed} -> - Message = list_to_binary(io_lib:format("appid ~p already existed", [AppID])), - {400, #{code => 'BAD_APP_ID', message => Message}}; - {error, Reason} -> - Response = #{code => 'UNKNOW_ERROR', - message => list_to_binary(io_lib:format("~p", [Reason]))}, - {500, Response} - end. - -lookup(#{app_id := AppID}) -> - case emqx_mgmt_auth:lookup_app(AppID) of - undefined -> - {404, ?APP_ID_NOT_FOUND}; - App -> - Response = format_with_app_secret(App), - {200, Response} - end. - -delete(#{app_id := AppID}) -> - _ = emqx_mgmt_auth:del_app(AppID), - {200}. - -update(App = #{app_id := AppID, name := Name, desc := Desc, status := Status, expired := Expired}) -> - case emqx_mgmt_auth:update_app(AppID, Name, Desc, Status, Expired) of - ok -> - {200, App}; - {error, not_found} -> - {404, ?APP_ID_NOT_FOUND}; - {error, Reason} -> - Response = #{code => 'UNKNOW_ERROR', message => list_to_binary(io_lib:format("~p", [Reason]))}, - {500, Response} - end. - -%%%============================================================================================== -%% format -format_without_app_secret(App) -> - format_without([secret], App). - -format_with_app_secret(App) -> - format_without([], App). - -format_without(List, {AppID, AppSecret, Name, Desc, Status, Expired}) -> - Data = #{ - app_id => AppID, - secret => AppSecret, - name => Name, - desc => Desc, - status => Status, - expired => Expired - }, - maps:without(List, Data). diff --git a/apps/emqx_management/src/emqx_mgmt_api_authz.erl b/apps/emqx_management/src/emqx_mgmt_api_authz.erl deleted file mode 100644 index 6da16ff30..000000000 --- a/apps/emqx_management/src/emqx_mgmt_api_authz.erl +++ /dev/null @@ -1,47 +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_mgmt_api_authz). - --include("emqx_mgmt.hrl"). - --rest_api(#{name => clean_authz_cache_all, - method => 'DELETE', - path => "/authz-cache", - func => clean_all, - descr => "Clean authz cache on all nodes"}). - --rest_api(#{name => clean_authz_cache_node, - method => 'DELETE', - path => "nodes/:atom:node/authz-cache", - func => clean_node, - descr => "Clean authz cache on specific node"}). - --export([ clean_all/2 - , clean_node/2 - ]). - -clean_all(_Bindings, _Params) -> - case emqx_mgmt:clean_authz_cache_all() of - ok -> emqx_mgmt:return(); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) - end. - -clean_node(#{node := Node}, _Params) -> - case emqx_mgmt:clean_authz_cache_all(Node) of - ok -> emqx_mgmt:return(); - {error, Reason} -> emqx_mgmt:return({error, ?ERROR1, Reason}) - end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl deleted file mode 100644 index bdd43b35c..000000000 --- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl +++ /dev/null @@ -1,166 +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_banned). - --include_lib("emqx/include/emqx.hrl"). - --include("emqx_mgmt.hrl"). - --rest_api(#{name => list_banned, - method => 'GET', - path => "/banned/", - func => list, - descr => "List banned"}). - --rest_api(#{name => create_banned, - method => 'POST', - path => "/banned/", - func => create, - descr => "Create banned"}). - --rest_api(#{name => delete_banned, - method => 'DELETE', - path => "/banned/:as/:who", - func => delete, - descr => "Delete banned"}). - --export([ list/2 - , create/2 - , delete/2 - ]). - -list(_Bindings, Params) -> - emqx_mgmt:return({ok, emqx_mgmt_api:paginate(emqx_banned, Params, fun format/1)}). - -create(_Bindings, Params) -> - case pipeline([fun ensure_required/1, - fun validate_params/1], Params) of - {ok, NParams} -> - {ok, Banned} = pack_banned(NParams), - ok = emqx_mgmt:create_banned(Banned), - emqx_mgmt:return({ok, maps:from_list(Params)}); - {error, Code, Message} -> - emqx_mgmt:return({error, Code, Message}) - end. - -delete(#{as := As, who := Who}, _) -> - Params = [{<<"who">>, bin(emqx_mgmt_util:urldecode(Who))}, - {<<"as">>, bin(emqx_mgmt_util:urldecode(As))}], - case pipeline([fun ensure_required/1, - fun validate_params/1], Params) of - {ok, NParams} -> - do_delete(proplists:get_value(<<"as">>, NParams), proplists:get_value(<<"who">>, NParams)), - emqx_mgmt:return(); - {error, Code, Message} -> - emqx_mgmt:return({error, Code, Message}) - end. - -pipeline([], Params) -> - {ok, Params}; -pipeline([Fun|More], Params) -> - case Fun(Params) of - {ok, NParams} -> - pipeline(More, NParams); - {error, Code, Message} -> - {error, Code, Message} - end. - -%% Plugs -ensure_required(Params) when is_list(Params) -> - #{required_params := RequiredParams, message := Msg} = required_params(), - AllIncluded = lists:all(fun(Key) -> - lists:keymember(Key, 1, Params) - end, RequiredParams), - case AllIncluded of - true -> {ok, Params}; - false -> - {error, ?ERROR7, Msg} - end. - -validate_params(Params) -> - #{enum_values := AsEnums, message := Msg} = enum_values(as), - case lists:member(proplists:get_value(<<"as">>, Params), AsEnums) of - true -> {ok, Params}; - false -> - {error, ?ERROR8, Msg} - end. - -pack_banned(Params) -> - Now = erlang:system_time(second), - do_pack_banned(Params, #{by => <<"user">>, at => Now, until => Now + 300}). - -do_pack_banned([], #{who := Who, by := By, reason := Reason, at := At, until := Until}) -> - {ok, #banned{who = Who, by = By, reason = Reason, at = At, until = Until}}; -do_pack_banned([{<<"who">>, Who} | Params], Banned) -> - case lists:keytake(<<"as">>, 1, Params) of - {value, {<<"as">>, <<"peerhost">>}, Params2} -> - {ok, IPAddress} = inet:parse_address(str(Who)), - do_pack_banned(Params2, Banned#{who => {peerhost, IPAddress}}); - {value, {<<"as">>, <<"clientid">>}, Params2} -> - do_pack_banned(Params2, Banned#{who => {clientid, Who}}); - {value, {<<"as">>, <<"username">>}, Params2} -> - do_pack_banned(Params2, Banned#{who => {username, Who}}) - end; -do_pack_banned([P1 = {<<"as">>, _}, P2 | Params], Banned) -> - do_pack_banned([P2, P1 | Params], Banned); -do_pack_banned([{<<"by">>, By} | Params], Banned) -> - do_pack_banned(Params, Banned#{by => By}); -do_pack_banned([{<<"reason">>, Reason} | Params], Banned) -> - do_pack_banned(Params, Banned#{reason => Reason}); -do_pack_banned([{<<"at">>, At} | Params], Banned) -> - do_pack_banned(Params, Banned#{at => At}); -do_pack_banned([{<<"until">>, Until} | Params], Banned) -> - do_pack_banned(Params, Banned#{until => Until}); -do_pack_banned([_P | Params], Banned) -> %% ignore other params - do_pack_banned(Params, Banned). - -do_delete(<<"peerhost">>, Who) -> - {ok, IPAddress} = inet:parse_address(str(Who)), - emqx_mgmt:delete_banned({peerhost, IPAddress}); -do_delete(<<"username">>, Who) -> - emqx_mgmt:delete_banned({username, bin(Who)}); -do_delete(<<"clientid">>, Who) -> - emqx_mgmt:delete_banned({clientid, bin(Who)}). - -required_params() -> - #{required_params => [<<"who">>, <<"as">>], - message => <<"missing mandatory params: ['who', 'as']">> }. - -enum_values(as) -> - #{enum_values => [<<"clientid">>, <<"username">>, <<"peerhost">>], - message => <<"value of 'as' must be one of: ['clientid', 'username', 'peerhost']">> }. - -%% Internal Functions - -format(BannedList) when is_list(BannedList) -> - [format(Ban) || Ban <- BannedList]; -format(#banned{who = {As, Who}, by = By, reason = Reason, at = At, until = Until}) -> - #{who => case As of - peerhost -> bin(inet:ntoa(Who)); - _ -> Who - end, - as => As, by => By, reason => Reason, at => At, until => Until}. - -bin(L) when is_list(L) -> - list_to_binary(L); -bin(B) when is_binary(B) -> - B. - -str(B) when is_binary(B) -> - binary_to_list(B); -str(L) when is_list(L) -> - L. diff --git a/apps/emqx_management/src/emqx_mgmt_api_brokers.erl b/apps/emqx_management/src/emqx_mgmt_api_brokers.erl deleted file mode 100644 index 836f097cb..000000000 --- a/apps/emqx_management/src/emqx_mgmt_api_brokers.erl +++ /dev/null @@ -1,47 +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_brokers). - --include("emqx_mgmt.hrl"). - --rest_api(#{name => list_brokers, - method => 'GET', - path => "/brokers/", - func => list, - descr => "A list of brokers in the cluster"}). - --rest_api(#{name => get_broker, - method => 'GET', - path => "/brokers/:atom:node", - func => get, - descr => "Get broker info of a node"}). - --export([ list/2 - , get/2 - ]). - -list(_Bindings, _Params) -> - emqx_mgmt:return({ok, [Info || {_Node, Info} <- emqx_mgmt:list_brokers()]}). - -get(#{node := Node}, _Params) -> - case emqx_mgmt:lookup_broker(Node) of - {error, Reason} -> - emqx_mgmt:return({error, ?ERROR2, Reason}); - Info -> - emqx_mgmt:return({ok, Info}) - end. - diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl deleted file mode 100644 index fda7151d7..000000000 --- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl +++ /dev/null @@ -1,113 +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_plugins). - --include("emqx_mgmt.hrl"). - --include_lib("emqx/include/emqx.hrl"). - --rest_api(#{name => list_all_plugins, - method => 'GET', - path => "/plugins/", - func => list, - descr => "List all plugins in the cluster"}). - --rest_api(#{name => list_node_plugins, - method => 'GET', - path => "/nodes/:atom:node/plugins/", - func => list, - descr => "List all plugins on a node"}). - --rest_api(#{name => load_node_plugin, - method => 'PUT', - path => "/nodes/:atom:node/plugins/:atom:plugin/load", - func => load, - descr => "Load a plugin"}). - --rest_api(#{name => unload_node_plugin, - method => 'PUT', - path => "/nodes/:atom:node/plugins/:atom:plugin/unload", - func => unload, - descr => "Unload a plugin"}). - --rest_api(#{name => reload_node_plugin, - method => 'PUT', - path => "/nodes/:atom:node/plugins/:atom:plugin/reload", - func => reload, - descr => "Reload a plugin"}). - --rest_api(#{name => unload_plugin, - method => 'PUT', - path => "/plugins/:atom:plugin/unload", - func => unload, - descr => "Unload a plugin in the cluster"}). - --rest_api(#{name => reload_plugin, - method => 'PUT', - path => "/plugins/:atom:plugin/reload", - func => reload, - descr => "Reload a plugin in the cluster"}). - --export([ list/2 - , load/2 - , unload/2 - , reload/2 - ]). - -list(#{node := Node}, _Params) -> - emqx_mgmt:return({ok, [format(Plugin) || Plugin <- emqx_mgmt:list_plugins(Node)]}); - -list(_Bindings, _Params) -> - emqx_mgmt:return({ok, [format({Node, Plugins}) || {Node, Plugins} <- emqx_mgmt:list_plugins()]}). - -load(#{node := Node, plugin := Plugin}, _Params) -> - emqx_mgmt:return(emqx_mgmt:load_plugin(Node, Plugin)). - -unload(#{node := Node, plugin := Plugin}, _Params) -> - 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 - [] -> - emqx_mgmt:return(ok); - Errors -> - emqx_mgmt:return(lists:last(Errors)) - end. - -reload(#{node := Node, plugin := Plugin}, _Params) -> - 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 - [] -> - emqx_mgmt:return(ok); - Errors -> - emqx_mgmt:return(lists:last(Errors)) - end. - -format({Node, Plugins}) -> - #{node => Node, plugins => [format(Plugin) || Plugin <- Plugins]}; - -format(#plugin{name = Name, - descr = Descr, - active = Active}) -> - #{name => Name, - description => iolist_to_binary(Descr), - active => Active}. - diff --git a/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl b/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl deleted file mode 100644 index 28e67c9f1..000000000 --- a/apps/emqx_management/src/emqx_mgmt_api_pubsub.erl +++ /dev/null @@ -1,247 +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_pubsub). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include("emqx_mgmt.hrl"). - --rest_api(#{name => mqtt_subscribe, - method => 'POST', - path => "/mqtt/subscribe", - func => subscribe, - descr => "Subscribe a topic"}). - --rest_api(#{name => mqtt_publish, - method => 'POST', - path => "/mqtt/publish", - func => publish, - descr => "Publish a MQTT message"}). - --rest_api(#{name => mqtt_unsubscribe, - method => 'POST', - path => "/mqtt/unsubscribe", - func => unsubscribe, - descr => "Unsubscribe a topic"}). - --rest_api(#{name => mqtt_subscribe_batch, - method => 'POST', - path => "/mqtt/subscribe_batch", - func => subscribe_batch, - descr => "Batch subscribes topics"}). - --rest_api(#{name => mqtt_publish_batch, - method => 'POST', - path => "/mqtt/publish_batch", - func => publish_batch, - descr => "Batch publish MQTT messages"}). - --rest_api(#{name => mqtt_unsubscribe_batch, - method => 'POST', - path => "/mqtt/unsubscribe_batch", - func => unsubscribe_batch, - descr => "Batch unsubscribes topics"}). - --export([ subscribe/2 - , publish/2 - , unsubscribe/2 - , subscribe_batch/2 - , publish_batch/2 - , unsubscribe_batch/2 - ]). - -subscribe(_Bindings, Params) -> - logger:debug("API subscribe Params:~p", [Params]), - {ClientId, Topic, QoS} = parse_subscribe_params(Params), - emqx_mgmt:return(do_subscribe(ClientId, Topic, QoS)). - -publish(_Bindings, Params) -> - logger:debug("API publish Params:~p", [Params]), - {ClientId, Topic, Qos, Retain, Payload} = parse_publish_params(Params), - case do_publish(ClientId, Topic, Qos, Retain, Payload) of - {ok, MsgIds} -> - case proplists:get_value(<<"return">>, Params, undefined) of - undefined -> emqx_mgmt:return(ok); - _Val -> - case proplists:get_value(<<"topics">>, Params, undefined) of - undefined -> emqx_mgmt:return({ok, #{msgid => lists:last(MsgIds)}}); - _ -> emqx_mgmt:return({ok, #{msgids => MsgIds}}) - end - end; - Result -> - emqx_mgmt:return(Result) - end. - -unsubscribe(_Bindings, Params) -> - logger:debug("API unsubscribe Params:~p", [Params]), - {ClientId, Topic} = parse_unsubscribe_params(Params), - emqx_mgmt:return(do_unsubscribe(ClientId, Topic)). - -subscribe_batch(_Bindings, Params) -> - logger:debug("API subscribe batch Params:~p", [Params]), - emqx_mgmt:return({ok, loop_subscribe(Params)}). - -publish_batch(_Bindings, Params) -> - logger:debug("API publish batch Params:~p", [Params]), - emqx_mgmt:return({ok, loop_publish(Params)}). - -unsubscribe_batch(_Bindings, Params) -> - logger:debug("API unsubscribe batch Params:~p", [Params]), - emqx_mgmt:return({ok, loop_unsubscribe(Params)}). - -loop_subscribe(Params) -> - loop_subscribe(Params, []). -loop_subscribe([], Result) -> - lists:reverse(Result); -loop_subscribe([Params | ParamsN], Acc) -> - {ClientId, Topic, QoS} = parse_subscribe_params(Params), - Code = case do_subscribe(ClientId, Topic, QoS) of - ok -> 0; - {_, Code0, _Reason} -> Code0 - end, - Result = #{clientid => ClientId, - topic => resp_topic(proplists:get_value(<<"topic">>, Params), proplists:get_value(<<"topics">>, Params, <<"">>)), - code => Code}, - loop_subscribe(ParamsN, [Result | Acc]). - -loop_publish(Params) -> - loop_publish(Params, []). -loop_publish([], Result) -> - lists:reverse(Result); -loop_publish([Params | ParamsN], Acc) -> - {ClientId, Topic, Qos, Retain, Payload} = parse_publish_params(Params), - Code = case do_publish(ClientId, Topic, Qos, Retain, Payload) of - {ok, _} -> 0; - {_, Code0, _} -> Code0 - end, - Result = #{topic => resp_topic(proplists:get_value(<<"topic">>, Params), proplists:get_value(<<"topics">>, Params, <<"">>)), - code => Code}, - loop_publish(ParamsN, [Result | Acc]). - -loop_unsubscribe(Params) -> - loop_unsubscribe(Params, []). -loop_unsubscribe([], Result) -> - lists:reverse(Result); -loop_unsubscribe([Params | ParamsN], Acc) -> - {ClientId, Topic} = parse_unsubscribe_params(Params), - Code = case do_unsubscribe(ClientId, Topic) of - ok -> 0; - {_, Code0, _} -> Code0 - end, - Result = #{clientid => ClientId, - topic => resp_topic(proplists:get_value(<<"topic">>, Params), proplists:get_value(<<"topics">>, Params, <<"">>)), - code => Code}, - loop_unsubscribe(ParamsN, [Result | Acc]). - -do_subscribe(_ClientId, [], _QoS) -> - {ok, ?ERROR15, bad_topic}; -do_subscribe(ClientId, Topics, QoS) -> - TopicTable = parse_topic_filters(Topics, QoS), - case emqx_mgmt:subscribe(ClientId, TopicTable) of - {error, Reason} -> {ok, ?ERROR12, Reason}; - _ -> ok - end. - -do_publish(_ClientId, [], _Qos, _Retain, _Payload) -> - {ok, ?ERROR15, bad_topic}; -do_publish(ClientId, Topics, Qos, Retain, Payload) -> - MsgIds = lists:map(fun(Topic) -> - Msg = emqx_message:make(ClientId, Qos, Topic, Payload), - _ = emqx_mgmt:publish(Msg#message{flags = #{retain => Retain}}), - emqx_guid:to_hexstr(Msg#message.id) - end, Topics), - {ok, MsgIds}. - -do_unsubscribe(ClientId, Topic) -> - case validate_by_filter(Topic) of - true -> - case emqx_mgmt:unsubscribe(ClientId, Topic) of - {error, Reason} -> {ok, ?ERROR12, Reason}; - _ -> ok - end; - false -> - {ok, ?ERROR15, bad_topic} - end. - -parse_subscribe_params(Params) -> - ClientId = proplists:get_value(<<"clientid">>, Params), - Topics = topics(filter, proplists:get_value(<<"topic">>, Params), proplists:get_value(<<"topics">>, Params, <<"">>)), - QoS = proplists:get_value(<<"qos">>, Params, 0), - {ClientId, Topics, QoS}. - -parse_publish_params(Params) -> - Topics = topics(name, proplists:get_value(<<"topic">>, Params), proplists:get_value(<<"topics">>, Params, <<"">>)), - ClientId = proplists:get_value(<<"clientid">>, Params), - Payload = decode_payload(proplists:get_value(<<"payload">>, Params, <<>>), - proplists:get_value(<<"encoding">>, Params, <<"plain">>)), - Qos = proplists:get_value(<<"qos">>, Params, 0), - Retain = proplists:get_value(<<"retain">>, Params, false), - Payload1 = maybe_maps_to_binary(Payload), - {ClientId, Topics, Qos, Retain, Payload1}. - -parse_unsubscribe_params(Params) -> - ClientId = proplists:get_value(<<"clientid">>, Params), - Topic = proplists:get_value(<<"topic">>, Params), - {ClientId, Topic}. - -topics(Type, undefined, Topics0) -> - Topics = binary:split(Topics0, <<",">>, [global]), - case Type of - name -> lists:filter(fun(T) -> validate_by_name(T) end, Topics); - filter -> lists:filter(fun(T) -> validate_by_filter(T) end, Topics) - end; - -topics(Type, Topic, _) -> - topics(Type, undefined, Topic). - -%%TODO: -% validate(qos, Qos) -> -% (Qos >= ?QOS_0) and (Qos =< ?QOS_2). - -validate_by_filter(Topic) -> - validate_topic({filter, Topic}). - -validate_by_name(Topic) -> - validate_topic({name, Topic}). - -validate_topic({Type, Topic}) -> - try emqx_topic:validate({Type, Topic}) of - true -> true - catch - error:_Reason -> false - end. - -parse_topic_filters(Topics, Qos) -> - [begin - {Topic, Opts} = emqx_topic:parse(Topic0), - {Topic, Opts#{qos => Qos}} - end || Topic0 <- Topics]. - -resp_topic(undefined, Topics) -> Topics; -resp_topic(Topic, _) -> Topic. - -decode_payload(Payload, <<"base64">>) -> base64:decode(Payload); -decode_payload(Payload, _) -> Payload. - -maybe_maps_to_binary(Payload) when is_binary(Payload) -> Payload; -maybe_maps_to_binary(Payload) -> - try - emqx_json:encode(Payload) - catch - _C : _E : S -> - error({encode_payload_fail, S}) - end. diff --git a/apps/emqx_management/src/emqx_mgmt_app.erl b/apps/emqx_management/src/emqx_mgmt_app.erl index ff85666b3..8dc1651da 100644 --- a/apps/emqx_management/src/emqx_mgmt_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_app.erl @@ -29,10 +29,8 @@ start(_Type, _Args) -> {ok, Sup} = emqx_mgmt_sup:start_link(), ok = ekka_rlog:wait_for_shards([?MANAGEMENT_SHARD], infinity), - _ = emqx_mgmt_auth:add_default_app(), - emqx_mgmt_http:start_listeners(), emqx_mgmt_cli:load(), {ok, Sup}. stop(_State) -> - emqx_mgmt_http:stop_listeners(). + ok. diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl deleted file mode 100644 index 7c0eb8e82..000000000 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ /dev/null @@ -1,216 +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_auth). - -%% Mnesia Bootstrap --export([mnesia/1]). --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - -%% APP Management API --export([ add_default_app/0 - , add_app/2 - , add_app/5 - , add_app/6 - , force_add_app/6 - , lookup_app/1 - , get_appsecret/1 - , update_app/2 - , update_app/5 - , del_app/1 - , list_apps/0 - ]). - -%% APP Auth/Authorization API --export([is_authorized/2]). - --define(APP, emqx_management). - --record(mqtt_app, {id, secret, name, desc, status, expired}). - --type(appid() :: binary()). - --type(appsecret() :: binary()). - --include("emqx_mgmt.hrl"). - -%%-------------------------------------------------------------------- -%% Mnesia Bootstrap -%%-------------------------------------------------------------------- - -mnesia(boot) -> - ok = ekka_mnesia:create_table(mqtt_app, [ - {rlog_shard, ?MANAGEMENT_SHARD}, - {disc_copies, [node()]}, - {record_name, mqtt_app}, - {attributes, record_info(fields, mqtt_app)}]); - -mnesia(copy) -> - ok = ekka_mnesia:copy_table(mqtt_app, disc_copies). - -%%-------------------------------------------------------------------- -%% Manage Apps -%%-------------------------------------------------------------------- --spec(add_default_app() -> list()). -add_default_app() -> - Apps = emqx:get_config([?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) -> - add_app(AppId, Name, <<"Application user">>, true, undefined). - --spec(add_app(appid(), binary(), binary(), boolean(), integer() | undefined) - -> {ok, appsecret()} - | {error, term()}). -add_app(AppId, Name, Desc, Status, Expired) when is_binary(AppId) -> - add_app(AppId, Name, undefined, Desc, Status, Expired). - --spec(add_app(appid(), binary(), binary() | undefined, binary(), boolean(), integer() | undefined) - -> {ok, appsecret()} - | {error, term()}). -add_app(AppId, Name, Secret, Desc, Status, Expired) when is_binary(AppId) -> - Secret1 = generate_appsecret_if_need(Secret), - App = #mqtt_app{id = AppId, - secret = Secret1, - name = Name, - desc = Desc, - status = Status, - expired = Expired}, - AddFun = fun() -> - case mnesia:wread({mqtt_app, AppId}) of - [] -> mnesia:write(App); - _ -> mnesia:abort(alread_existed) - end - end, - case ekka_mnesia:transaction(?MANAGEMENT_SHARD, AddFun) of - {atomic, ok} -> {ok, Secret1}; - {aborted, Reason} -> {error, Reason} - end. - -force_add_app(AppId, Name, Secret, Desc, Status, Expired) -> - AddFun = fun() -> - mnesia:write(#mqtt_app{id = AppId, - secret = Secret, - name = Name, - desc = Desc, - status = Status, - expired = Expired}) - end, - case ekka_mnesia:transaction(?MANAGEMENT_SHARD, AddFun) of - {atomic, ok} -> ok; - {aborted, Reason} -> {error, Reason} - end. - --spec(generate_appsecret_if_need(binary() | undefined) -> binary()). -generate_appsecret_if_need(InSecrt) when is_binary(InSecrt), byte_size(InSecrt) > 0 -> - InSecrt; -generate_appsecret_if_need(_) -> - emqx_guid:to_base62(emqx_guid:gen()). - --spec(get_appsecret(appid()) -> {appsecret() | undefined}). -get_appsecret(AppId) when is_binary(AppId) -> - case mnesia:dirty_read(mqtt_app, AppId) of - [#mqtt_app{secret = Secret}] -> Secret; - [] -> undefined - end. - --spec(lookup_app(appid()) -> undefined | {appid(), appsecret(), binary(), binary(), boolean(), integer() | undefined}). -lookup_app(AppId) when is_binary(AppId) -> - case mnesia:dirty_read(mqtt_app, AppId) of - [#mqtt_app{id = AppId, - secret = AppSecret, - name = Name, - desc = Desc, - status = Status, - expired = Expired}] -> {AppId, AppSecret, Name, Desc, Status, Expired}; - [] -> undefined - end. - --spec(update_app(appid(), boolean()) -> ok | {error, term()}). -update_app(AppId, Status) -> - case mnesia:dirty_read(mqtt_app, AppId) of - [App = #mqtt_app{}] -> - Fun = fun() -> mnesia:write(App#mqtt_app{status = Status}) end, - case ekka_mnesia:transaction(?MANAGEMENT_SHARD, Fun) of - {atomic, ok} -> ok; - {aborted, Reason} -> {error, Reason} - end; - [] -> - {error, not_found} - end. - --spec(update_app(appid(), binary(), binary(), boolean(), integer() | undefined) -> ok | {error, term()}). -update_app(AppId, Name, Desc, Status, Expired) -> - case mnesia:dirty_read(mqtt_app, AppId) of - [App = #mqtt_app{}] -> - case ekka_mnesia:transaction( - ?MANAGEMENT_SHARD, - fun() -> mnesia:write(App#mqtt_app{name = Name, - desc = Desc, - status = Status, - expired = Expired}) end) of - {atomic, ok} -> ok; - {aborted, Reason} -> {error, Reason} - end; - [] -> - {error, not_found} - end. - --spec(del_app(appid()) -> ok | {error, term()}). -del_app(AppId) when is_binary(AppId) -> - case ekka_mnesia:transaction(?MANAGEMENT_SHARD, fun mnesia:delete/1, [{mqtt_app, AppId}]) of - {atomic, Ok} -> Ok; - {aborted, Reason} -> {error, Reason} - end. - --spec(list_apps() -> [{appid(), appsecret(), binary(), binary(), boolean(), integer() | undefined}]). -list_apps() -> - [ {AppId, AppSecret, Name, Desc, Status, Expired} || #mqtt_app{id = AppId, - secret = AppSecret, - name = Name, - desc = Desc, - status = Status, - expired = Expired} <- ets:tab2list(mqtt_app) ]. -%%-------------------------------------------------------------------- -%% Authenticate App -%%-------------------------------------------------------------------- - --spec(is_authorized(appid(), appsecret()) -> boolean()). -is_authorized(AppId, AppSecret) -> - case lookup_app(AppId) of - {_, AppSecret1, _, _, Status, Expired} -> - Status andalso is_expired(Expired) andalso AppSecret =:= AppSecret1; - _ -> - false - end. - -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_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index dcaac5052..3d4dea31e 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -37,7 +37,6 @@ , mnesia/1 , trace/1 , log/1 - , mgmt/1 , authz/1 ]). @@ -61,55 +60,6 @@ load() -> is_cmd(Fun) -> not lists:member(Fun, [init, load, module_info]). -mgmt(["insert", AppId, Name]) -> - case emqx_mgmt_auth:add_app(list_to_binary(AppId), list_to_binary(Name)) of - {ok, Secret} -> - emqx_ctl:print("AppSecret: ~s~n", [Secret]); - {error, already_existed} -> - emqx_ctl:print("Error: already existed~n"); - {error, Reason} -> - emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -mgmt(["lookup", AppId]) -> - case emqx_mgmt_auth:lookup_app(list_to_binary(AppId)) of - {AppId1, AppSecret, Name, Desc, Status, Expired} -> - emqx_ctl:print("app_id: ~s~nsecret: ~s~nname: ~s~ndesc: ~s~nstatus: ~s~nexpired: ~p~n", - [AppId1, AppSecret, Name, Desc, Status, Expired]); - undefined -> - emqx_ctl:print("Not Found.~n") - end; - -mgmt(["update", AppId, Status]) -> - case emqx_mgmt_auth:update_app(list_to_binary(AppId), list_to_atom(Status)) of - ok -> - emqx_ctl:print("update successfully.~n"); - {error, Reason} -> - emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -mgmt(["delete", AppId]) -> - case emqx_mgmt_auth:del_app(list_to_binary(AppId)) of - ok -> emqx_ctl:print("ok~n"); - {error, not_found} -> - emqx_ctl:print("Error: app not found~n"); - {error, Reason} -> - emqx_ctl:print("Error: ~p~n", [Reason]) - end; - -mgmt(["list"]) -> - lists:foreach(fun({AppId, AppSecret, Name, Desc, Status, Expired}) -> - emqx_ctl:print("app_id: ~s, secret: ~s, name: ~s, desc: ~s, status: ~s, expired: ~p~n", - [AppId, AppSecret, Name, Desc, Status, Expired]) - end, emqx_mgmt_auth:list_apps()); - -mgmt(_) -> - emqx_ctl:usage([{"mgmt list", "List Applications"}, - {"mgmt insert ", "Add Application of REST API"}, - {"mgmt update ", "Update Application of REST API"}, - {"mgmt lookup ", "Get Application of REST API"}, - {"mgmt delete ", "Delete Application of REST API"}]). - %%-------------------------------------------------------------------- %% @doc Node status diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl deleted file mode 100644 index da509a36b..000000000 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ /dev/null @@ -1,136 +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_http). - --export([ start_listeners/0 - , stop_listeners/0 - , start_listener/1 - , stop_listener/1]). - -%% Authorization --export([authorize_appid/1]). - --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). - --define(APP, emqx_management). - --define(BASE_PATH, "/api/v5"). - -%%-------------------------------------------------------------------- -%% Start/Stop Listeners -%%-------------------------------------------------------------------- - -start_listeners() -> - lists:foreach(fun start_listener/1, listeners()). - -stop_listeners() -> - lists:foreach(fun stop_listener/1, listeners()). - -start_listener({Proto, Port, Options}) -> - {ok, _} = application:ensure_all_started(minirest), - Authorization = {?MODULE, authorize_appid}, - RanchOptions = ranch_opts(Port, Options), - GlobalSpec = #{ - openapi => "3.0.0", - info => #{title => "EMQ X API", version => "5.0.0"}, - servers => [#{url => ?BASE_PATH}], - tags => [#{ - name => configs, - description => <<"The query string parameter `conf_path` is of jq format.">>, - externalDocs => #{ - description => "Find out more about the path syntax in jq", - url => "https://stedolan.github.io/jq/manual/" - } - }], - components => #{ - schemas => #{}, - securitySchemes => #{ - application => #{ - type => apiKey, - name => "authorization", - in => header}}}}, - Minirest = #{ - protocol => Proto, - base_path => ?BASE_PATH, - modules => api_modules(), - authorization => Authorization, - security => [#{application => []}], - swagger_global_spec => GlobalSpec}, - MinirestOptions = maps:merge(Minirest, RanchOptions), - {ok, _} = minirest:start(listener_name(Proto), MinirestOptions), - ?ULOG("Start ~p listener on ~p successfully.~n", [listener_name(Proto), Port]). - -ranch_opts(Port, Options0) -> - 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, _}) -> - ?ULOG("Stop http:management listener on ~s successfully.~n",[format(Port)]), - minirest:stop(listener_name(Proto)). - -listeners() -> - [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} - || Map = #{protocol := Protocol,port := Port} - <- emqx:get_config([emqx_management, listeners], [])]. - -listener_name(Proto) -> - list_to_atom(atom_to_list(Proto) ++ ":management"). - -authorize_appid(Req) -> - case cowboy_req:parse_header(<<"authorization">>, Req) of - {basic, AppId, AppSecret} -> - case emqx_mgmt_auth:is_authorized(AppId, AppSecret) of - true -> ok; - false -> {401, #{<<"WWW-Authenticate">> => <<"Basic Realm=\"minirest-server\"">>}, <<"UNAUTHORIZED">>} - end; - _ -> - {401, #{<<"WWW-Authenticate">> => <<"Basic Realm=\"minirest-server\"">>}, <<"UNAUTHORIZED">>} - 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]). - -apps() -> - Apps = [App || {App, _, _} <- application:loaded_applications(), App =/= emqx_dashboard], - lists:filter(fun(App) -> - case re:run(atom_to_list(App), "^emqx") of - {match,[{0,4}]} -> true; - _ -> false - end - end, Apps). - --ifdef(TEST). -api_modules() -> - minirest_api:find_api_modules(apps()). --else. -api_modules() -> - minirest_api:find_api_modules(apps()) -- [emqx_mgmt_api_apps]. --endif. - diff --git a/apps/emqx_management/test/emqx_mgmt_alarms_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_alarms_api_SUITE.erl index 2929e961d..9293d8fe0 100644 --- a/apps/emqx_management/test/emqx_mgmt_alarms_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_alarms_api_SUITE.erl @@ -25,23 +25,14 @@ -define(DE_ACT_ALARM, test_de_act_alarm). all() -> - [t_alarms_api, t_delete_alarms_api]. + 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:init_suite(), 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:end_suite(). t_alarms_api(_) -> ok = emqx_alarm:activate(?ACT_ALARM), diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index 568ed46a6..0babef05a 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -17,9 +17,32 @@ -compile(export_all). -compile(nowarn_export_all). --define(SERVER, "http://127.0.0.1:8081"). +-define(SERVER, "http://127.0.0.1:18083"). -define(BASE_PATH, "/api/v5"). +init_suite() -> + ekka_mnesia:start(), + application:load(emqx_management), + emqx_ct_helpers:start_apps([emqx_dashboard], fun set_special_configs/1). + +end_suite() -> + application:unload(emqx_management), + emqx_ct_helpers:stop_apps([emqx_dashboard]). + +set_special_configs(emqx_dashboard) -> + Config = #{ + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, + emqx_config:put([emqx_dashboard], Config), + ok; +set_special_configs(_App) -> + ok. + request_api(Method, Url) -> request_api(Method, Url, [], auth_header_(), []). @@ -55,13 +78,10 @@ do_request_api(Method, Request)-> 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}. + Username = <<"admin">>, + Password = <<"public">>, + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. api_path(Parts)-> ?SERVER ++ filename:join([?BASE_PATH | Parts]). diff --git a/apps/emqx_management/test/emqx_mgmt_apps_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_apps_api_SUITE.erl deleted file mode 100644 index a139208a5..000000000 --- a/apps/emqx_management/test/emqx_mgmt_apps_api_SUITE.erl +++ /dev/null @@ -1,110 +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_apps_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_list_app(_) -> - Path = emqx_mgmt_api_test_util:api_path(["apps"]), - {ok, Body} = emqx_mgmt_api_test_util:request_api(get, Path), - Data = emqx_json:decode(Body, [return_maps]), - AdminApp = hd(Data), - Admin = maps:get(<<"app_id">>, AdminApp), - ?assertEqual(<<"admin">>, Admin). - -t_get_app(_) -> - Path = emqx_mgmt_api_test_util:api_path(["apps/admin"]), - {ok, Body} = emqx_mgmt_api_test_util:request_api(get, Path), - AdminApp = emqx_json:decode(Body, [return_maps]), - ?assertEqual(<<"admin">>, maps:get(<<"app_id">>, AdminApp)), - ?assertEqual(<<"public">>, maps:get(<<"secret">>, AdminApp)). - -t_add_app(_) -> - AuthHeader = emqx_mgmt_api_test_util:auth_header_(), - AppId = <<"test_app_id">>, - TestAppPath = emqx_mgmt_api_test_util:api_path(["apps", AppId]), - AppSecret = <<"test_app_secret">>, - - %% new test app - Path = emqx_mgmt_api_test_util:api_path(["apps"]), - RequestBody = #{ - app_id => AppId, - secret => AppSecret, - desc => <<"test desc">>, - name => <<"test_app_name">>, - expired => erlang:system_time(second) + 3000, - status => true - }, - {ok, Body} = emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, RequestBody), - TestAppSecret = emqx_json:decode(Body, [return_maps]), - ?assertEqual(AppSecret, maps:get(<<"secret">>, TestAppSecret)), - - %% get new test app - {ok, GetApp} = emqx_mgmt_api_test_util:request_api(get, TestAppPath), - TestApp = emqx_json:decode(GetApp, [return_maps]), - ?assertEqual(AppId, maps:get(<<"app_id">>, TestApp)), - ?assertEqual(AppSecret, maps:get(<<"secret">>, TestApp)), - - %% update app - Desc2 = <<"test desc 2">>, - Name2 = <<"test_app_name_2">>, - PutBody = #{ - desc => Desc2, - name => Name2, - expired => erlang:system_time(second) + 3000, - status => false - }, - {ok, PutApp} = emqx_mgmt_api_test_util:request_api(put, TestAppPath, "", AuthHeader, PutBody), - TestApp1 = emqx_json:decode(PutApp, [return_maps]), - ?assertEqual(Desc2, maps:get(<<"desc">>, TestApp1)), - ?assertEqual(Name2, maps:get(<<"name">>, TestApp1)), - ?assertEqual(false, maps:get(<<"status">>, TestApp1)), - - %% after update - {ok, GetApp2} = emqx_mgmt_api_test_util:request_api(get, TestAppPath), - TestApp2 = emqx_json:decode(GetApp2, [return_maps]), - ?assertEqual(Desc2, maps:get(<<"desc">>, TestApp2)), - ?assertEqual(Name2, maps:get(<<"name">>, TestApp2)), - ?assertEqual(false, maps:get(<<"status">>, TestApp2)), - - %% delete new app - {ok, _} = emqx_mgmt_api_test_util:request_api(delete, TestAppPath), - - %% after delete - ?assertEqual({error,{"HTTP/1.1",404,"Not Found"}}, - emqx_mgmt_api_test_util:request_api(get, TestAppPath)). 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 9f754b4e1..54b4f92ff 100644 --- a/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_clients_api_SUITE.erl @@ -19,26 +19,15 @@ -include_lib("eunit/include/eunit.hrl"). --define(APP, emqx_management). - 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:init_suite(), 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:end_suite(). t_clients(_) -> process_flag(trap_exit, true), diff --git a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl index 93632dda4..10e1def26 100644 --- a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl @@ -24,20 +24,11 @@ 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:init_suite(), 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:end_suite(). t_list_listeners(_) -> Path = emqx_mgmt_api_test_util:api_path(["listeners"]), diff --git a/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl index b54489e37..5e9464a1e 100644 --- a/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_metrics_api_SUITE.erl @@ -24,20 +24,11 @@ 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:init_suite(), 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:end_suite(). t_metrics_api(_) -> MetricsPath = emqx_mgmt_api_test_util:api_path(["metrics?aggregate=true"]), 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 f0829b7fb..f4c17a163 100644 --- a/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_nodes_api_SUITE.erl @@ -24,20 +24,11 @@ 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:init_suite(), 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:end_suite(). t_nodes_api(_) -> NodesPath = emqx_mgmt_api_test_util:api_path(["nodes"]), diff --git a/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl index 2f566e5f4..311a2cc97 100644 --- a/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_publish_api_SUITE.erl @@ -26,26 +26,15 @@ -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), + emqx_mgmt_api_test_util:init_suite(), 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:end_suite(). t_publish_api(_) -> {ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}), diff --git a/apps/emqx_management/test/emqx_mgmt_routes_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_routes_api_SUITE.erl index 1756f6ff5..6963e42cf 100644 --- a/apps/emqx_management/test/emqx_mgmt_routes_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_routes_api_SUITE.erl @@ -24,20 +24,11 @@ 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:init_suite(), 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:end_suite(). t_nodes_api(_) -> Topic = <<"test_topic">>, @@ -62,4 +53,4 @@ t_nodes_api(_) -> {ok, RouteResponse} = emqx_mgmt_api_test_util:request_api(get, RoutePath), RouteData = emqx_json:decode(RouteResponse, [return_maps]), ?assertEqual(Topic, maps:get(<<"topic">>, RouteData)), - ?assertEqual(atom_to_binary(node(), utf8), maps:get(<<"node">>, RouteData)). \ No newline at end of file + ?assertEqual(atom_to_binary(node(), utf8), maps:get(<<"node">>, RouteData)). diff --git a/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl index 395a0851e..11c66daa1 100644 --- a/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_stats_api_SUITE.erl @@ -24,20 +24,11 @@ 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:init_suite(), 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:end_suite(). t_stats_api(_) -> StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]), diff --git a/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl index 6d56f21c4..f2d8c6eb2 100644 --- a/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl @@ -27,26 +27,15 @@ -define(TOPIC1, <<"0000">>). -define(TOPIC2, <<"0001">>). - 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:init_suite(), 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:end_suite(). t_subscription_api(_) -> {ok, Client} = emqtt:start_link(#{username => ?USERNAME, clientid => ?CLIENTID}), diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index ac9a6bcdc..60cc15f58 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -94,8 +94,6 @@ spec: containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT | default 1883 }} - name: mqttssl containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__SSL__DEFAULT | default 8883 }} - - name: mgmt - containerPort: {{ .Values.emqxConfig.EMQX_MANAGEMENT__LISTENER__HTTP | default 8081 }} - name: ws containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WS__DEFAULT | default 8083 }} - name: wss @@ -140,7 +138,7 @@ spec: readinessProbe: httpGet: path: /api/v5/status - port: {{ .Values.emqxConfig.EMQX_MANAGEMENT__LISTENER__HTTP | default 8081 }} + port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} initialDelaySeconds: 5 periodSeconds: 5 {{- with .Values.nodeSelector }} diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 77269d31f..3e9f06b52 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -55,15 +55,6 @@ spec: {{- else if eq .Values.service.type "ClusterIP" }} nodePort: null {{- end }} - - name: mgmt - port: {{ .Values.service.mgmt | default 8081 }} - protocol: TCP - targetPort: mgmt - {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePorts.mgmt)) }} - nodePort: {{ .Values.service.nodePorts.mgmt }} - {{- else if eq .Values.service.type "ClusterIP" }} - nodePort: null - {{- end }} - name: ws port: {{ .Values.service.ws | default 8083 }} protocol: TCP @@ -136,10 +127,6 @@ spec: port: {{ .Values.service.mqttssl | default 8883 }} protocol: TCP targetPort: mqttssl - - name: mgmt - port: {{ .Values.service.mgmt | default 8081 }} - protocol: TCP - targetPort: mgmt - name: ws port: {{ .Values.service.ws | default 8083 }} protocol: TCP From 765c730b92db47016b8072a31e138b0afac1fc0b Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Aug 2021 13:44:13 +0800 Subject: [PATCH 145/306] fix(dashboard): Update dashboard version to beta.6 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d521ca179..a807dfce8 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.5 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.6 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From 69574455547d886a4ffae0ef760e1e88e7de4e12 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 24 Aug 2021 15:22:42 +0800 Subject: [PATCH 146/306] chore(recon): add observer_cli to emqx_ctl --- apps/emqx_modules/src/emqx_recon.erl | 5 ++++- rebar.config | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx_modules/src/emqx_recon.erl b/apps/emqx_modules/src/emqx_recon.erl index b16d1b051..4781bc450 100644 --- a/apps/emqx_modules/src/emqx_recon.erl +++ b/apps/emqx_modules/src/emqx_recon.erl @@ -32,6 +32,8 @@ enable() -> disable() -> emqx_ctl:unregister_command(recon). +cmd(["observer_cli"]) -> + observer_cli:start(); cmd(["memory"]) -> Print = fun(Key, Keyword) -> emqx_ctl:print("~-20s: ~w~n", [concat(Key, Keyword), recon_alloc:memory(Key, Keyword)]) @@ -56,7 +58,8 @@ cmd(["proc_count", Attr, N]) -> emqx_ctl:print("~p~n", [recon:proc_count(list_to_atom(Attr), list_to_integer(N))]); cmd(_) -> - emqx_ctl:usage([{"recon memory", "recon_alloc:memory/2"}, + emqx_ctl:usage([{"observer_cli", "observer_cli:start()"}, + {"recon memory", "recon_alloc:memory/2"}, {"recon allocated", "recon_alloc:memory(allocated_types, current|max)"}, {"recon bin_leak", "recon:bin_leak(100)"}, {"recon node_stats", "recon:node_stats(10, 1000)"}, diff --git a/rebar.config b/rebar.config index e2559c885..4b9f17fd0 100644 --- a/rebar.config +++ b/rebar.config @@ -57,8 +57,7 @@ , {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"}}} - , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 + , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.1"}}} From f4e3eeb2b27317aa2bef9ac701e78983382f7e73 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 24 Aug 2021 17:23:29 +0800 Subject: [PATCH 147/306] chore(recon): rename to emqx_ctl observer status --- apps/emqx_modules/src/emqx_recon.erl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/emqx_modules/src/emqx_recon.erl b/apps/emqx_modules/src/emqx_recon.erl index 4781bc450..b6a314da5 100644 --- a/apps/emqx_modules/src/emqx_recon.erl +++ b/apps/emqx_modules/src/emqx_recon.erl @@ -27,12 +27,14 @@ %% enable/disable %%-------------------------------------------------------------------- enable() -> - emqx_ctl:register_command(recon, {?MODULE, cmd}, []). + emqx_ctl:register_command(recon, {?MODULE, cmd}, []), + emqx_ctl:register_command(observer, {?MODULE, cmd}, []). disable() -> - emqx_ctl:unregister_command(recon). + emqx_ctl:unregister_command(recon), + emqx_ctl:unregister_command(observer). -cmd(["observer_cli"]) -> +cmd(["status"]) -> observer_cli:start(); cmd(["memory"]) -> Print = fun(Key, Keyword) -> @@ -58,7 +60,7 @@ cmd(["proc_count", Attr, N]) -> emqx_ctl:print("~p~n", [recon:proc_count(list_to_atom(Attr), list_to_integer(N))]); cmd(_) -> - emqx_ctl:usage([{"observer_cli", "observer_cli:start()"}, + emqx_ctl:usage([{"observer status", "observer_cli:start()"}, {"recon memory", "recon_alloc:memory/2"}, {"recon allocated", "recon_alloc:memory(allocated_types, current|max)"}, {"recon bin_leak", "recon:bin_leak(100)"}, From 468102f46205c4d54d5b1b05e2e3b63080183d1b Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Wed, 25 Aug 2021 13:36:27 +0800 Subject: [PATCH 148/306] chore: replace recon by obsserver_cli 1.7.0 --- apps/emqx_modules/etc/emqx_modules.conf | 2 +- apps/emqx_modules/src/emqx_modules_app.erl | 4 +- .../{emqx_recon.erl => emqx_observer_cli.erl} | 40 ++++--------------- bin/emqx | 2 +- rebar.config | 2 +- 5 files changed, 13 insertions(+), 37 deletions(-) rename apps/emqx_modules/src/{emqx_recon.erl => emqx_observer_cli.erl} (51%) diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index 3bbf8b52c..20d2672dc 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -5,7 +5,7 @@ delayed { max_delayed_messages = 0 } -recon { +observer_cli { enable = true } diff --git a/apps/emqx_modules/src/emqx_modules_app.erl b/apps/emqx_modules/src/emqx_modules_app.erl index 889c566b1..123431605 100644 --- a/apps/emqx_modules/src/emqx_modules_app.erl +++ b/apps/emqx_modules/src/emqx_modules_app.erl @@ -34,7 +34,7 @@ stop(_State) -> maybe_enable_modules() -> emqx:get_config([delayed, enable], true) andalso emqx_delayed:enable(), emqx:get_config([telemetry, enable], true) andalso emqx_telemetry:enable(), - emqx:get_config([recon, enable], true) andalso emqx_recon:enable(), + emqx:get_config([observer_cli, enable], true) andalso emqx_observer_cli:enable(), emqx_event_message:enable(), emqx_rewrite:enable(), emqx_topic_metrics:enable(). @@ -42,7 +42,7 @@ maybe_enable_modules() -> maybe_disable_modules() -> emqx:get_config([delayed, enable], true) andalso emqx_delayed:disable(), emqx:get_config([telemetry, enable], true) andalso emqx_telemetry:disable(), - emqx:get_config([recon, enable], true) andalso emqx_recon:disable(), + emqx:get_config([observer_cli, enable], true) andalso emqx_observer_cli:disable(), emqx_event_message:disable(), emqx_rewrite:disable(), emqx_topic_metrics:disable(). diff --git a/apps/emqx_modules/src/emqx_recon.erl b/apps/emqx_modules/src/emqx_observer_cli.erl similarity index 51% rename from apps/emqx_modules/src/emqx_recon.erl rename to apps/emqx_modules/src/emqx_observer_cli.erl index b6a314da5..ccde9032c 100644 --- a/apps/emqx_modules/src/emqx_recon.erl +++ b/apps/emqx_modules/src/emqx_observer_cli.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_recon). +-module(emqx_observer_cli). -export([ enable/0 , disable/0 @@ -27,51 +27,27 @@ %% enable/disable %%-------------------------------------------------------------------- enable() -> - emqx_ctl:register_command(recon, {?MODULE, cmd}, []), emqx_ctl:register_command(observer, {?MODULE, cmd}, []). disable() -> - emqx_ctl:unregister_command(recon), emqx_ctl:unregister_command(observer). cmd(["status"]) -> observer_cli:start(); -cmd(["memory"]) -> - Print = fun(Key, Keyword) -> - emqx_ctl:print("~-20s: ~w~n", [concat(Key, Keyword), recon_alloc:memory(Key, Keyword)]) - end, - [Print(Key, Keyword) || Key <- [usage, used, allocated, unused], Keyword <- [current, max]]; - -cmd(["allocated"]) -> - Print = fun(Keyword, Key, Val) -> emqx_ctl:print("~-20s: ~w~n", [concat(Key, Keyword), Val]) end, - Alloc = fun(Keyword) -> recon_alloc:memory(allocated_types, Keyword) end, - [Print(Keyword, Key, Val) || Keyword <- [current, max], {Key, Val} <- Alloc(Keyword)]; cmd(["bin_leak"]) -> [emqx_ctl:print("~p~n", [Row]) || Row <- recon:bin_leak(100)]; -cmd(["node_stats"]) -> - recon:node_stats_print(10, 1000); - -cmd(["remote_load", Mod]) -> - emqx_ctl:print("~p~n", [remote_load(list_to_atom(Mod))]); - -cmd(["proc_count", Attr, N]) -> - emqx_ctl:print("~p~n", [recon:proc_count(list_to_atom(Attr), list_to_integer(N))]); +cmd(["load", Mod]) -> + Module = list_to_existing_atom(Mod), + Nodes = nodes(), + Res = remote_load(Nodes, Module), + emqx_ctl:print("Loaded ~p module on ~p on ~n", [Mod, Nodes, Res]); cmd(_) -> emqx_ctl:usage([{"observer status", "observer_cli:start()"}, - {"recon memory", "recon_alloc:memory/2"}, - {"recon allocated", "recon_alloc:memory(allocated_types, current|max)"}, - {"recon bin_leak", "recon:bin_leak(100)"}, - {"recon node_stats", "recon:node_stats(10, 1000)"}, - {"recon remote_load Mod", "recon:remote_load(Mod)"}, - {"recon proc_count Attr N","recon:proc_count(Attr, N)"}]). - -concat(Key, Keyword) -> - lists:concat([atom_to_list(Key), "/", atom_to_list(Keyword)]). - -remote_load(Module) -> remote_load(nodes(), Module). + {"observer bin_leak", "recon:bin_leak(100)"}, + {"observer load Mod", "recon:remote_load(Mod) to all nodes"}]). %% recon:remote_load/1 has a bug, when nodes() returns [], it is %% taken by recon as a node name. diff --git a/bin/emqx b/bin/emqx index a286d2801..41f2c0db5 100755 --- a/bin/emqx +++ b/bin/emqx @@ -681,7 +681,7 @@ case "$1" in shift - relx_nodetool rpc emqx_ctl run_command "$@" + relx_nodetool rpc_infinity emqx_ctl run_command "$@" ;; rpc) assert_node_alive diff --git a/rebar.config b/rebar.config index 4b9f17fd0..3b1f97195 100644 --- a/rebar.config +++ b/rebar.config @@ -57,7 +57,7 @@ , {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"}}} - , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.x + , {observer_cli, "1.7.0"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.1"}}} From f02a55450252661ec5948abb24b7d626bff93d05 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Fri, 27 Aug 2021 10:08:06 +0800 Subject: [PATCH 149/306] fix: ctrl+c break cause badarg, bump observer_cli to 1.7.1 --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 3b1f97195..75d78b64e 100644 --- a/rebar.config +++ b/rebar.config @@ -57,7 +57,7 @@ , {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"}}} - , {observer_cli, "1.7.0"} % NOTE: depends on recon 2.5.x + , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.1"}}} From 307eaa7f1e644d63b7cf1a621720d54a9064b960 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 26 Aug 2021 20:16:56 +0800 Subject: [PATCH 150/306] feat(rewrite): update rewrite conf to array --- apps/emqx_modules/etc/emqx_modules.conf | 24 +++++++++---------- apps/emqx_modules/src/emqx_modules_schema.erl | 20 ++++++---------- apps/emqx_modules/src/emqx_rewrite.erl | 9 +++---- apps/emqx_modules/test/emqx_rewrite_SUITE.erl | 12 +++------- 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index 20d2672dc..a55a06bc5 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -23,18 +23,16 @@ event_message { # "$event/message_dropped": false } -topic_metrics { - topics = [] -} +topic_metrics: [ + #{topic: "test/1"} +] -rewrite { - rules = [ - { - action = publish - source_topic = "x/#" - re = "^x/y/(.+)$" - dest_topic = "z/y/$1" - } - ] -} +rewrite: [ + { + action = publish + source_topic = "x/#" + re = "^x/y/(.+)$" + dest_topic = "z/y/$1" + } +] diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl index 0c5e716bf..695db972f 100644 --- a/apps/emqx_modules/src/emqx_modules_schema.erl +++ b/apps/emqx_modules/src/emqx_modules_schema.erl @@ -28,8 +28,8 @@ structs() -> "recon", "telemetry", "event_message", - "rewrite", - "topic_metrics"]. + {array, "rewrite"}, + {array, "topic_metrics"}]. fields(Name) when Name =:= "recon"; Name =:= "telemetry" -> @@ -42,10 +42,12 @@ fields("delayed") -> ]; fields("rewrite") -> - [ {rules, hoconsc:array(hoconsc:ref(?MODULE, "rules"))} + [ {action, hoconsc:enum([publish, subscribe])} + , {source_topic, emqx_schema:t(binary())} + , {re, emqx_schema:t(binary())} + , {dest_topic, emqx_schema:t(binary())} ]; - fields("event_message") -> [ {"$event/client_connected", emqx_schema:t(boolean(), undefined, false)} , {"$event/client_disconnected", emqx_schema:t(boolean(), undefined, false)} @@ -57,13 +59,5 @@ fields("event_message") -> ]; fields("topic_metrics") -> - [ {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())} - ]. + [{topic, emqx_schema:t(binary())}]. diff --git a/apps/emqx_modules/src/emqx_rewrite.erl b/apps/emqx_modules/src/emqx_rewrite.erl index 9a6c0574b..4b4173156 100644 --- a/apps/emqx_modules/src/emqx_rewrite.erl +++ b/apps/emqx_modules/src/emqx_rewrite.erl @@ -43,7 +43,7 @@ %%-------------------------------------------------------------------- enable() -> - Rules = emqx:get_config([rewrite, rules], []), + Rules = emqx:get_config([rewrite], []), register_hook(Rules). disable() -> @@ -52,13 +52,10 @@ disable() -> emqx_hooks:del('message.publish', {?MODULE, rewrite_publish}). list() -> - maps:get(<<"rules">>, emqx:get_raw_config([<<"rewrite">>], #{}), []). + maps:get(<<"rules">>, emqx:get_raw_config([<<"rewrite">>], []), []). update(Rules0) -> - Rewrite = emqx:get_raw_config([<<"rewrite">>], #{}), - {ok, #{config := Config}} = emqx:update_config([rewrite], maps:put(<<"rules">>, - Rules0, Rewrite)), - Rules = maps:get(rules, maps:get(rewrite, Config, #{}), []), + {ok, #{config := Rules}} = emqx:update_config([rewrite], Rules0), case Rules of [] -> disable(); diff --git a/apps/emqx_modules/test/emqx_rewrite_SUITE.erl b/apps/emqx_modules/test/emqx_rewrite_SUITE.erl index 467fa0e45..012f1cb5b 100644 --- a/apps/emqx_modules/test/emqx_rewrite_SUITE.erl +++ b/apps/emqx_modules/test/emqx_rewrite_SUITE.erl @@ -23,8 +23,7 @@ -include_lib("eunit/include/eunit.hrl"). -define(REWRITE, <<""" -rewrite: { - rules : [ +rewrite: [ { action : publish source_topic : \"x/#\" @@ -37,7 +36,7 @@ rewrite: { re : \"^y/(.+)/z/(.+)$\" dest_topic : \"y/z/$2\" } - ]}""">>). +]""">>). all() -> emqx_ct:all(?MODULE). @@ -87,12 +86,7 @@ t_mod_rewrite(_Config) -> ok = emqx_rewrite:disable(). t_rewrite_rule(_Config) -> - {ok, Rewite} = hocon:binary(?REWRITE), - #{rewrite := #{rules := Rules}} = - hocon_schema:check_plain(emqx_modules_schema, Rewite, - #{atom_key => true}, - ["rewrite"]), - {PubRules, SubRules} = emqx_rewrite:compile(Rules), + {PubRules, SubRules} = emqx_rewrite:compile(emqx:get_config([rewrite])), ?assertEqual(<<"z/y/2">>, emqx_rewrite:match_and_rewrite(<<"x/y/2">>, PubRules)), ?assertEqual(<<"x/1/2">>, emqx_rewrite:match_and_rewrite(<<"x/1/2">>, PubRules)), ?assertEqual(<<"y/z/b">>, emqx_rewrite:match_and_rewrite(<<"y/a/z/b">>, SubRules)), From 856d394860b3c4d00e7601e5ae166cbcc12e3fb4 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 26 Aug 2021 20:17:17 +0800 Subject: [PATCH 151/306] feat(topic_metrics): update topic_metrics conf to array --- apps/emqx_modules/src/emqx_topic_metrics.erl | 15 +++++++-------- .../test/emqx_topic_metrics_SUITE.erl | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/emqx_modules/src/emqx_topic_metrics.erl b/apps/emqx_modules/src/emqx_topic_metrics.erl index 3a7e5e3c0..3829deb1a 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics.erl @@ -146,7 +146,7 @@ on_message_dropped(#message{topic = Topic}, _, _) -> end. start_link() -> - Opts = emqx:get_config([topic_metrics], #{}), + Opts = emqx:get_config([topic_metrics], []), gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). stop() -> @@ -198,7 +198,7 @@ init([Opts]) -> ok = emqx_tables:new(?TAB, [{read_concurrency, true}]), erlang:send_after(timer:seconds(?TICKING_INTERVAL), self(), ticking), Fun = - fun(Topic, CurrentSpeeds) -> + fun(#{topic := Topic}, CurrentSpeeds) -> case do_register(Topic, CurrentSpeeds) of {ok, NSpeeds} -> NSpeeds; @@ -208,7 +208,7 @@ init([Opts]) -> error("max topic metrics quota exceeded") end end, - {ok, #state{speeds = lists:foldl(Fun, #{}, maps:get(topics, Opts, []))}, hibernate}. + {ok, #state{speeds = lists:foldl(Fun, #{}, Opts)}, hibernate}. handle_call({register, Topic}, _From, State = #state{speeds = Speeds}) -> case do_register(Topic, Speeds) of @@ -348,16 +348,15 @@ format({Topic, Data}) -> end. remove_topic_config(Topic) when is_binary(Topic) -> - Topics = emqx_config:get_raw([<<"topic_metrics">>, <<"topics">>], []) -- [Topic], + Topics = emqx_config:get_raw([<<"topic_metrics">>], []) -- [#{<<"topic">> => Topic}], update_config(Topics). add_topic_config(Topic) when is_binary(Topic) -> - Topics = emqx_config:get_raw([<<"topic_metrics">>, <<"topics">>], []) ++ [Topic], - update_config(Topics). + Topics = emqx_config:get_raw([<<"topic_metrics">>], []) ++ [#{<<"topic">> => Topic}], + update_config(lists:usort(Topics)). update_config(Topics) when is_list(Topics) -> - Opts = emqx_config:get_raw([<<"topic_metrics">>], #{}), - {ok, _} = emqx:update_config([topic_metrics], maps:put(<<"topics">>, Topics, Opts)), + {ok, _} = emqx:update_config([topic_metrics], Topics), ok. try_inc(Topic, Metric) -> diff --git a/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl b/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl index 958131716..246eb2ab3 100644 --- a/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl +++ b/apps/emqx_modules/test/emqx_topic_metrics_SUITE.erl @@ -21,8 +21,7 @@ -define(TOPIC, <<""" -topic_metrics: { - topics : []}""">>). +topic_metrics: []""">>). -include_lib("eunit/include/eunit.hrl"). From cf04e5cce364756ae226bbb7f7f6428061f5d0cb Mon Sep 17 00:00:00 2001 From: William Yang Date: Sat, 7 Aug 2021 11:46:10 +0200 Subject: [PATCH 152/306] feat(quic): adapt to new quicer --- apps/emqx/src/emqx_listeners.erl | 7 ++--- apps/emqx/src/emqx_quic_connection.erl | 41 ++++++++++++++++++++++++-- apps/emqx/src/emqx_quic_stream.erl | 7 +++-- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 521366877..411caeea9 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -178,9 +178,6 @@ do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> case [ A || {quicer, _, _} = A<-application:which_applications() ] of [_] -> - %% @fixme unsure why we need reopen lib and reopen config. - quicer_nif:open_lib(), - quicer_nif:reg_open(), DefAcceptors = erlang:system_info(schedulers_online) * 8, ListenOpts = [ {cert, maps:get(certfile, Opts)} , {key, maps:get(keyfile, Opts)} @@ -189,13 +186,13 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> , {idle_timeout_ms, emqx_config:get_zone_conf(zone(Opts), [mqtt, idle_timeout])} ], - ConnectionOpts = #{conn_callback => emqx_quic_connection + ConnectionOpts = #{ conn_callback => emqx_quic_connection , peer_unidi_stream_count => 1 , peer_bidi_stream_count => 10 , zone => zone(Opts) , listener => {quic, ListenerName} }, - StreamOpts = [], + StreamOpts = [{stream_callback, emqx_quic_stream}], quicer:start_listener(listener_id(quic, ListenerName), port(ListenOn), {ListenOpts, ConnectionOpts, StreamOpts}); [] -> diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl index cd41e74a7..7ac130278 100644 --- a/apps/emqx/src/emqx_quic_connection.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -17,8 +17,43 @@ -module(emqx_quic_connection). %% Callbacks --export([ new_conn/2 +-export([ init/1 + , new_conn/2 + , connected/2 + , shutdown/2 ]). -new_conn(Conn, {_L, COpts, _S}) when is_map(COpts) -> - emqx_connection:start_link(emqx_quic_stream, Conn, COpts). +-type cb_state() :: map() | proplists:proplist(). + + +-spec init(cb_state()) -> cb_state(). +init(ConnOpts) when is_list(ConnOpts) -> + init(maps:from_list(ConnOpts)); +init(ConnOpts) when is_map(ConnOpts) -> + ConnOpts. + +-spec new_conn(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. +new_conn(Conn, S) -> + case emqx_connection:start_link(emqx_quic_stream, Conn, S) of + {ok, _Pid} -> + ok = quicer:async_handshake(Conn), + {ok, S}; + Other -> + {error, Other} + end. + +-spec connected(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. +connected(Conn, #{slow_start := false} = S) -> + case emqx_connection:start_link(emqx_quic_stream, Conn, S) of + {ok, _Pid} -> + {ok, S}; + Other -> + {error, Other} + end; +connected(_Conn, S) -> + {ok, S}. + +-spec shutdown(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. +shutdown(Conn, S) -> + quicer:async_close_connection(Conn), + {ok, S}. diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 236c11ad3..76a365340 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -88,5 +88,8 @@ ensure_ok_or_exit(Fun, Args = [Sock|_]) when is_atom(Fun), is_list(Args) -> 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. + case quicer:send(Stream, Data) of + {ok, _Len} -> ok; + Other -> + Other + end. From abd58bb23553585a139dccac6275e0efd4385a29 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 25 Aug 2021 09:22:18 +0200 Subject: [PATCH 153/306] feat(quic): idle_timeout transport idle timeout should be at least 3x mqtt idle_timeout --- apps/emqx/src/emqx_listeners.erl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 411caeea9..3c132f335 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -183,8 +183,10 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> , {key, maps:get(keyfile, Opts)} , {alpn, ["mqtt"]} , {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)} - , {idle_timeout_ms, emqx_config:get_zone_conf(zone(Opts), - [mqtt, idle_timeout])} + , {idle_timeout_ms, lists:max([ + emqx_config:get_zone_conf(zone(Opts), [mqtt, idle_timeout]) * 3 + , timer:seconds(maps:get(idle_timeout, Opts))] + )} ], ConnectionOpts = #{ conn_callback => emqx_quic_connection , peer_unidi_stream_count => 1 From 2ef2acc8506f517ca7d1062f432078601fd80c13 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 25 Aug 2021 09:27:19 +0200 Subject: [PATCH 154/306] feat(quic): adapt to quicer 0.0.8 --- apps/emqx/src/emqx_quic_connection.erl | 14 ++++++++++---- apps/emqx/src/emqx_quic_stream.erl | 13 +++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl index 7ac130278..2fe911001 100644 --- a/apps/emqx/src/emqx_quic_connection.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -34,10 +34,16 @@ init(ConnOpts) when is_map(ConnOpts) -> -spec new_conn(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. new_conn(Conn, S) -> - case emqx_connection:start_link(emqx_quic_stream, Conn, S) of - {ok, _Pid} -> - ok = quicer:async_handshake(Conn), - {ok, S}; + process_flag(trap_exit, true), + case emqx_connection:start_link(emqx_quic_stream, {self(), Conn}, S) of + {ok, Pid} -> + receive + {Pid, stream_acceptor_ready} -> + ok = quicer:async_handshake(Conn), + {ok, S}; + {'EXIT', Pid, _Reason} -> + {error, stream_accept_error} + end; Other -> {error, Other} end. diff --git a/apps/emqx/src/emqx_quic_stream.erl b/apps/emqx/src/emqx_quic_stream.erl index 76a365340..bba1876c4 100644 --- a/apps/emqx/src/emqx_quic_stream.erl +++ b/apps/emqx/src/emqx_quic_stream.erl @@ -31,8 +31,16 @@ , peercert/1 ]). -wait(Conn) -> - quicer:accept_stream(Conn, []). +wait({ConnOwner, Conn}) -> + {ok, Conn} = quicer:async_accept_stream(Conn, []), + ConnOwner ! {self(), stream_acceptor_ready}, + receive + %% from msquic + {quic, new_stream, Stream} -> + {ok, Stream}; + {'EXIT', ConnOwner, _Reason} -> + {error, enotconn} + end. type(_) -> quic. @@ -44,6 +52,7 @@ sockname(S) -> quicer:sockname(S). peercert(_S) -> + %% @todo but unsupported by msquic nossl. getstat(Socket, Stats) -> From eb7625a5956d4310c12f07e01bf585f69869e540 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 25 Aug 2021 17:04:04 +0200 Subject: [PATCH 155/306] fix: quic listener spec --- apps/emqx/src/emqx_connection.erl | 4 +++- apps/emqx/src/emqx_quic_connection.erl | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 7d7f215c2..26eb346a4 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -135,7 +135,9 @@ , system_code_change/4 ]}). --spec(start_link(esockd:transport(), esockd:socket(), emqx_channel:opts()) +-spec(start_link(esockd:transport(), + esockd:socket() | {pid(), quicer:connection_handler()}, + emqx_channel:opts()) -> {ok, pid()}). start_link(Transport, Socket, Options) -> Args = [self(), Transport, Socket, Options], diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl index 2fe911001..3de607f75 100644 --- a/apps/emqx/src/emqx_quic_connection.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -50,12 +50,8 @@ new_conn(Conn, S) -> -spec connected(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. connected(Conn, #{slow_start := false} = S) -> - case emqx_connection:start_link(emqx_quic_stream, Conn, S) of - {ok, _Pid} -> - {ok, S}; - Other -> - {error, Other} - end; + {ok, _Pid} = emqx_connection:start_link(emqx_quic_stream, Conn, S), + {ok, S}; connected(_Conn, S) -> {ok, S}. From 1b5acdb9c59fecd345c7124b78d62f5c7791b38e Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 25 Aug 2021 17:04:22 +0200 Subject: [PATCH 156/306] feat(quic): at least 8 x number of cores acceptors --- apps/emqx/src/emqx_listeners.erl | 2 +- apps/emqx/src/emqx_quic_connection.erl | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 3c132f335..2d3357f37 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -182,7 +182,7 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> ListenOpts = [ {cert, maps:get(certfile, Opts)} , {key, maps:get(keyfile, Opts)} , {alpn, ["mqtt"]} - , {conn_acceptors, maps:get(acceptors, Opts, DefAcceptors)} + , {conn_acceptors, lists:max([DefAcceptors, maps:get(acceptors, Opts, 0)])} , {idle_timeout_ms, lists:max([ emqx_config:get_zone_conf(zone(Opts), [mqtt, idle_timeout]) * 3 , timer:seconds(maps:get(idle_timeout, Opts))] diff --git a/apps/emqx/src/emqx_quic_connection.erl b/apps/emqx/src/emqx_quic_connection.erl index 3de607f75..c23aec17b 100644 --- a/apps/emqx/src/emqx_quic_connection.erl +++ b/apps/emqx/src/emqx_quic_connection.erl @@ -35,17 +35,13 @@ init(ConnOpts) when is_map(ConnOpts) -> -spec new_conn(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. new_conn(Conn, S) -> process_flag(trap_exit, true), - case emqx_connection:start_link(emqx_quic_stream, {self(), Conn}, S) of - {ok, Pid} -> - receive - {Pid, stream_acceptor_ready} -> - ok = quicer:async_handshake(Conn), - {ok, S}; - {'EXIT', Pid, _Reason} -> - {error, stream_accept_error} - end; - Other -> - {error, Other} + {ok, Pid} = emqx_connection:start_link(emqx_quic_stream, {self(), Conn}, S), + receive + {Pid, stream_acceptor_ready} -> + ok = quicer:async_handshake(Conn), + {ok, S}; + {'EXIT', Pid, _Reason} -> + {error, stream_accept_error} end. -spec connected(quicer:connection_handler(), cb_state()) -> {ok, cb_state()} | {error, any()}. From 6186fa29a093fa19a7d683438cb54aa916d69be5 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 26 Aug 2021 15:15:00 +0200 Subject: [PATCH 157/306] feat(quic): bump to emqtt 1.4.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 c2229ce0f..b5aef703a 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -29,7 +29,7 @@ [ meck , {bbmustache,"1.10.0"} , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} ]} diff --git a/rebar.config b/rebar.config index 75d78b64e..dc883f1a0 100644 --- a/rebar.config +++ b/rebar.config @@ -55,7 +55,7 @@ , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, "0.3.3"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.2"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.2"}}} , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} From dbc971f26463dd8455919297480f9fd21a62c60c Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 26 Aug 2021 16:45:03 +0200 Subject: [PATCH 158/306] feat(quic): bump quicer to 0.0.8 --- apps/emqx/rebar.config.script | 2 +- rebar.config.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/rebar.config.script b/apps/emqx/rebar.config.script index 860be0a10..a56403dec 100644 --- a/apps/emqx/rebar.config.script +++ b/apps/emqx/rebar.config.script @@ -18,7 +18,7 @@ IsQuicSupp = fun() -> 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, "0.0.7"}}}, +Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {branch, "0.0.8"}}}, ExtraDeps = fun(C) -> {deps, Deps0} = lists:keyfind(deps, 1, C), diff --git a/rebar.config.erl b/rebar.config.erl index 3a189dba0..be08dc68c 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -16,7 +16,7 @@ bcrypt() -> {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {branch, "0.6.0"}}}. quicer() -> - {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.7"}}}. + {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.8"}}}. deps(Config) -> {deps, OldDeps} = lists:keyfind(deps, 1, Config), From ab9d6b9b7ae937c7fb9ceb9a4623830a4ee04bff Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 4 Aug 2021 15:48:09 +0200 Subject: [PATCH 159/306] fix(helm-chart): start/stop pods in parallel --- deploy/charts/emqx/templates/StatefulSet.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 60cc15f58..a7ba00949 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -10,6 +10,7 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} spec: serviceName: {{ include "emqx.fullname" . }}-headless + podManagementPolicy: Parallel {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} volumeClaimTemplates: - metadata: From 3fd2447f103205bf5f5f7d8c14472f351fb0baaf Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 4 Aug 2021 23:16:18 +0200 Subject: [PATCH 160/306] fix(helm-chart): make podManagementPolicy configurable --- deploy/charts/emqx/templates/StatefulSet.yaml | 2 +- deploy/charts/emqx/values.yaml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index a7ba00949..24e805f76 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -10,7 +10,7 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} spec: serviceName: {{ include "emqx.fullname" . }}-headless - podManagementPolicy: Parallel + podManagementPolicy: {{ .Values.podManagementPolicy }} {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} volumeClaimTemplates: - metadata: diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index 963fd36c2..bbf254e0a 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -17,6 +17,11 @@ image: ## Forces the recreation of pods during helm upgrades. This can be useful to update configuration values even if the container image did not change. recreatePods: false +# Pod deployment policy +# value: OrderedReady | Parallel +# To redeploy a chart with existing PVC(s), the value must be set to Parallel to avoid deadlock +podManagementPolicy: Parallel + persistence: enabled: false size: 20Mi From 4594d2d42bea9ab1e4b7fd8c64111b28f0c96d35 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 25 Aug 2021 16:09:54 +0800 Subject: [PATCH 161/306] chore(authorization): merge authorization in emqx and emqx_authz --- apps/emqx_authz/etc/emqx_authz.conf | 10 +++++----- apps/emqx_authz/src/emqx_authz_schema.erl | 4 ++-- apps/emqx_machine/src/emqx_machine_schema.erl | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 5928b1f97..be05437c0 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,9 +1,5 @@ -authorization_rules { +authorization { rules = [ - # { - # type: file - # path: {{ platform_etc_dir }}/authorization_rules.conf - # }, # { # type: http # config: { @@ -68,6 +64,10 @@ authorization_rules { # } # collection: mqtt_authz # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } + # }, + # { + # type: file + # path: {{ platform_etc_dir }}/authorization_rules.conf # } ] } diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index a09046572..9de963972 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -17,9 +17,9 @@ , fields/1 ]). -structs() -> ["authorization_rules"]. +structs() -> ["authorization"]. -fields("authorization_rules") -> +fields("authorization") -> [ {rules, rules()} ]; fields(file) -> diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index a8cba047d..ac21ed56e 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -197,6 +197,10 @@ fields("log_burst_limit") -> , {"window_time", t(emqx_schema:duration(), undefined, "1s")} ]; +fields("authorization") -> + emqx_schema:fields("authorization") ++ + emqx_authz_schema:fields("authorization"); + fields(Name) -> find_field(Name, ?MERGED_CONFIGS). From 6f8c87001f4a0f00e7ea30f1bce9db3b0a101d7d Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 25 Aug 2021 17:21:04 +0800 Subject: [PATCH 162/306] chore(authz): add default config Signed-off-by: zhanghongtong --- apps/emqx_authz/etc/emqx_authz.conf | 8 ++++---- apps/emqx_authz/src/emqx_authz_schema.erl | 2 +- rebar.config | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index be05437c0..358831d28 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -65,9 +65,9 @@ authorization { # collection: mqtt_authz # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } # }, - # { - # type: file - # path: {{ platform_etc_dir }}/authorization_rules.conf - # } + { + type: file + path: "{{ platform_etc_dir }}/authorization_rules.conf" + } ] } diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 9de963972..ce437ab2b 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -23,7 +23,7 @@ fields("authorization") -> [ {rules, rules()} ]; fields(file) -> - [ {type, #{type => http}} + [ {type, #{type => file}} , {enable, #{type => boolean(), default => true}} , {path, #{type => string(), diff --git a/rebar.config b/rebar.config index 75d78b64e..4a6b51cfa 100644 --- a/rebar.config +++ b/rebar.config @@ -60,7 +60,7 @@ , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.1"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.13.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. From f2e29184dec5bc0cbf9bdd84cf3f79100129db9d Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 26 Aug 2021 14:30:34 +0800 Subject: [PATCH 163/306] chore(emqx_authz): use new config path --- apps/emqx_authz/src/emqx_authz.erl | 6 +++--- apps/emqx_authz/test/emqx_authz_SUITE.erl | 8 ++++---- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 4 ++-- apps/emqx_authz/test/emqx_authz_http_SUITE.erl | 2 +- apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl | 2 +- apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl | 2 +- apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl | 2 +- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 3905f0138..90e46461d 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -36,7 +36,7 @@ -export([post_config_update/4, pre_config_update/2]). --define(CONF_KEY_PATH, [authorization_rules, rules]). +-define(CONF_KEY_PATH, [authorization, rules]). -spec(register_metrics() -> ok). register_metrics() -> @@ -185,9 +185,9 @@ post_config_update(_, NewRules, _OldConf, _AppEnvs) -> %%-------------------------------------------------------------------- check_rules(RawRules) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization_rules">> => #{<<"rules">> => RawRules}}), #{format => richmap}), + {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"rules">> => RawRules}}), #{format => richmap}), CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization_rules := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), + #{authorization:= #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), Rules. find_rule_by_id(Id) -> find_rule_by_id(Id, lookup()). diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index a766ecd82..2a656f07c 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). @@ -37,7 +37,7 @@ init_per_suite(Config) -> meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_authz]), + ok = emqx_ct_helpers:start_apps([emqx_machine, emqx_authz]), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Config. @@ -111,7 +111,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update(head, [?RULE1]), {ok, _} = emqx_authz:update(tail, [?RULE3]), - ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization_rules, rules], [])), + ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization, rules], [])), [#{annotations := #{id := Id1}, type := http}, #{annotations := #{id := Id2}, type := mongo}, @@ -120,7 +120,7 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update({replace_once, Id1}, ?RULE5), {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), - ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization_rules, rules], [])), + ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization, rules], [])), [#{annotations := #{id := Id1}, type := redis}, #{annotations := #{id := Id2}, type := mongo}, diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index b88afc57b..673bc1c4e 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). -import(emqx_ct_http, [ request_api/3 , request_api/5 @@ -126,7 +126,7 @@ set_special_configs(emqx_dashboard) -> emqx_config:put([emqx_dashboard], Config), ok; set_special_configs(emqx_authz) -> - emqx_config:put([authorization_rules], #{rules => []}), + emqx_config:put([authorization], #{rules => []}), ok; set_special_configs(_App) -> ok. diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index a455d0ab8..cacf1407c 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -21,7 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index 1ad7b4f7a..dd9b2108a 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 3c0320a42..8a762e9f3 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 43ea271a6..209e76af9 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 46ed9579e..70fb2ae85 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -21,7 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization_rules: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {rules: []}">>). all() -> emqx_ct:all(?MODULE). From f03fc88161822187cebec28881262b16908a16e1 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 26 Aug 2021 17:58:59 +0800 Subject: [PATCH 164/306] chore(emqx_authz): fix test cases error Signed-off-by: zhanghongtong --- apps/emqx/rebar.config | 2 +- apps/emqx_authz/etc/authorization_rules.conf | 22 +++++++++---------- apps/emqx_authz/test/emqx_authz_SUITE.erl | 10 ++++++++- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 8 +++++++ .../emqx_authz/test/emqx_authz_http_SUITE.erl | 8 +++++++ .../test/emqx_authz_mongo_SUITE.erl | 8 +++++++ .../test/emqx_authz_mysql_SUITE.erl | 11 +++++++++- .../test/emqx_authz_pgsql_SUITE.erl | 11 +++++++++- .../test/emqx_authz_redis_SUITE.erl | 11 +++++++++- 9 files changed, 74 insertions(+), 17 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index c2229ce0f..5e545ab2c 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -15,7 +15,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.12.1"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.13.0"}}} , {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.14.1"}}} diff --git a/apps/emqx_authz/etc/authorization_rules.conf b/apps/emqx_authz/etc/authorization_rules.conf index 79493b57a..2948f2af7 100644 --- a/apps/emqx_authz/etc/authorization_rules.conf +++ b/apps/emqx_authz/etc/authorization_rules.conf @@ -1,13 +1,15 @@ %%-------------------------------------------------------------------- -%% -type(ipaddress() :: {ipaddress, string() | [string()]}) +%% -type(ipaddr() :: {ipaddr, string()}). %% -%% -type(username() :: {username, regex()}) +%% -type(ipaddrs() :: {ipaddrs, string()}). %% -%% -type(clientid() :: {clientid, regex()}) +%% -type(username() :: {username, regex()}). %% -%% -type(who() :: ipaddress() | username() | clientid() | -%% {'and', [ipaddress() | username() | clientid()]} | -%% {'or', [ipaddress() | username() | clientid()]} | +%% -type(clientid() :: {clientid, regex()}). +%% +%% -type(who() :: ipaddr() | ipaddrs() |username() | clientid() | +%% {'and', [ipaddr() | ipaddrs()| username() | clientid()]} | +%% {'or', [ipaddr() | ipaddrs()| username() | clientid()]} | %% all). %% %% -type(action() :: subscribe | publish | all). @@ -21,10 +23,6 @@ %% -type(rule() :: {permission(), who(), access(), topics()}). %%-------------------------------------------------------------------- -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. +{allow, {username, "^dashboard?"}, subscribe, ["$SYS/#"]}. -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. +{allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}. diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 2a656f07c..36e706d13 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -31,13 +31,20 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_machine, emqx_authz]), + ok = emqx_ct_helpers:start_apps([emqx_authz]), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Config. @@ -46,6 +53,7 @@ end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. init_per_testcase(_, Config) -> diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 673bc1c4e..dc321cf98 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -94,6 +94,13 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end), @@ -112,6 +119,7 @@ end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]), meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. set_special_configs(emqx_dashboard) -> diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index cacf1407c..b0525bb24 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -30,6 +30,13 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), @@ -54,6 +61,7 @@ end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index dd9b2108a..6e2f398a4 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -31,6 +31,13 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, remove, fun(_) -> ok end ), @@ -56,6 +63,7 @@ end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), meck:unload(emqx_resource), + meck:unload(emqx_schema), ok. -define(RULE1,[#{<<"topics">> => [<<"#">>], diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 8a762e9f3..dc8d99e59 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -31,6 +31,13 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), @@ -57,7 +64,9 @@ init_per_suite(Config) -> end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. -define(COLUMNS, [ <<"action">> , <<"permission">> diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 209e76af9..53a91bd35 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -31,6 +31,13 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), @@ -57,7 +64,9 @@ init_per_suite(Config) -> end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. -define(COLUMNS, [ {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"permission">>, meck, meck, meck, meck, meck, meck, meck} diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 70fb2ae85..2d1e9161e 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -30,6 +30,13 @@ groups() -> []. init_per_suite(Config) -> + meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_schema, fields, fun("authorization") -> + meck:passthrough(["authorization"]) ++ + emqx_authz_schema:fields("authorization"); + (F) -> meck:passthrough([F]) + end), + meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, remove, fun(_) -> ok end ), @@ -55,7 +62,9 @@ init_per_suite(Config) -> end_per_suite(_Config) -> {ok, _} = emqx_authz:update(replace, []), emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]), - meck:unload(emqx_resource). + meck:unload(emqx_resource), + meck:unload(emqx_schema), + ok. -define(RULE1, [<<"test/%u">>, <<"publish">>]). -define(RULE2, [<<"test/%c">>, <<"publish">>]). From 65bb71cccaf215d8b1121ed164d9ecb4bf240540 Mon Sep 17 00:00:00 2001 From: Spycsh <39623753+Spycsh@users.noreply.github.com> Date: Fri, 27 Aug 2021 14:24:12 +0800 Subject: [PATCH 165/306] fix(rebar): make compatible to Windows --- rebar.config.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rebar.config.erl b/rebar.config.erl index be08dc68c..ad24e6479 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -382,7 +382,10 @@ emqx_etc_overlay_common() -> ]. get_vsn() -> - PkgVsn = os:cmd("./pkg-vsn.sh"), + %% to make it compatible to Linux and Windows, + %% we must use bash to execute the bash file + %% because "./" will not be recognized as an internal or external command + PkgVsn = os:cmd("bash pkg-vsn.sh"), re:replace(PkgVsn, "\n", "", [{return ,list}]). maybe_dump(Config) -> From 9d9eb2095b3827a56829fec733085b07acdb94db Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 27 Aug 2021 17:13:16 +0800 Subject: [PATCH 166/306] fix: delayed message api page param & doc (#5587) --- apps/emqx_modules/src/emqx_delayed_api.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl index 7b694bb31..d78b7ddcb 100644 --- a/apps/emqx_modules/src/emqx_delayed_api.erl +++ b/apps/emqx_modules/src/emqx_delayed_api.erl @@ -18,7 +18,8 @@ -behavior(minirest_api). --import(emqx_mgmt_util, [ schema/1 +-import(emqx_mgmt_util, [ page_params/0 + , schema/1 , schema/2 , object_schema/2 , error_schema/2 @@ -102,6 +103,7 @@ delayed_messages_api() -> Metadata = #{ get => #{ description => "List delayed messages", + parameters => page_params(), responses => #{ <<"200">> => page_object_schema(properties()) } From 46fb99d44ebdfc4f4b3abbf491dcaf93a5cf309c Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 27 Aug 2021 17:15:58 +0800 Subject: [PATCH 167/306] chore(emqx_authz): rename rules to sources in emqx_authz --- apps/emqx/src/emqx_schema.erl | 1 - apps/emqx_authz/etc/emqx_authz.conf | 2 +- apps/emqx_authz/include/emqx_authz.hrl | 2 + apps/emqx_authz/src/emqx_authz.erl | 202 +++++++++--------- apps/emqx_authz/src/emqx_authz_schema.erl | 4 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 32 +-- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 20 +- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 2 +- .../test/emqx_authz_mongo_SUITE.erl | 16 +- .../test/emqx_authz_mysql_SUITE.erl | 16 +- .../test/emqx_authz_pgsql_SUITE.erl | 16 +- .../test/emqx_authz_redis_SUITE.erl | 12 +- .../emqx_authz/test/emqx_authz_rule_SUITE.erl | 58 ++--- 13 files changed, 192 insertions(+), 191 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 3bbeb1d07..34a63534d 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -80,7 +80,6 @@ fields("stats") -> fields("authorization") -> [ {"no_match", t(union(allow, deny), undefined, allow)} - , {"enable", t(boolean(), undefined, true)} , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} , {"cache", ref("authorization_cache")} ]; diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 358831d28..8eadab38b 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,5 +1,5 @@ authorization { - rules = [ + sources = [ # { # type: http # config: { diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index a4f10c5f9..83d7601c6 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -17,6 +17,8 @@ -type(rule() :: {permission(), who(), action(), list(emqx_topic:topic())}). -type(rules() :: [rule()]). +-type(sources() :: [map()]). + -define(APP, emqx_authz). -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 5393b6b33..3e60cc32e 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -36,7 +36,7 @@ -export([post_config_update/4, pre_config_update/2]). --define(CONF_KEY_PATH, [authorization, rules]). +-define(CONF_KEY_PATH, [authorization, sources]). -spec(register_metrics() -> ok). register_metrics() -> @@ -45,15 +45,15 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NRules = [init_provider(Rule) || Rule <- emqx:get_config(?CONF_KEY_PATH, [])], - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). + NSources = [init_source(Source) || Source <- emqx:get_config(?CONF_KEY_PATH, [])], + ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1). lookup() -> {_M, _F, [A]}= find_action_in_hooks(), A. lookup(Id) -> - try find_rule_by_id(Id, lookup()) of - {_, Rule} -> Rule + try find_source_by_id(Id, lookup()) of + {_, Source} -> Source catch error:Reason -> {error, Reason} end. @@ -61,23 +61,23 @@ lookup(Id) -> move(Id, Position) -> emqx:update_config(?CONF_KEY_PATH, {move, Id, Position}). -update(Cmd, Rules) -> - emqx:update_config(?CONF_KEY_PATH, {Cmd, Rules}). +update(Cmd, Sources) -> + emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}). pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> - {Index, _} = find_rule_by_id(Id), + {Index, _} = find_source_by_id(Id), {List1, List2} = lists:split(Index, Conf), {ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2}; pre_config_update({move, Id, <<"bottom">>}, Conf) when is_list(Conf) -> - {Index, _} = find_rule_by_id(Id), + {Index, _} = find_source_by_id(Id), {List1, List2} = lists:split(Index, Conf), {ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]}; pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_rule_by_id(Id), + {Index1, _} = find_source_by_id(Id), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_rule_by_id(BeforeId), + {Index2, _} = find_source_by_id(BeforeId), Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), @@ -86,117 +86,117 @@ pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Co ++ lists:delete(Conf1, List2)}; pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_rule_by_id(Id), + {Index1, _} = find_source_by_id(Id), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_rule_by_id(AfterId), + {Index2, _} = find_source_by_id(AfterId), {List1, List2} = lists:split(Index2, Conf), {ok, lists:delete(Conf1, List1) ++ [Conf1] ++ lists:delete(Conf1, List2)}; -pre_config_update({head, Rules}, Conf) when is_list(Rules), is_list(Conf) -> - {ok, Rules ++ Conf}; -pre_config_update({tail, Rules}, Conf) when is_list(Rules), is_list(Conf) -> - {ok, Conf ++ Rules}; -pre_config_update({{replace_once, Id}, Rule}, Conf) when is_map(Rule), is_list(Conf) -> - {Index, _} = find_rule_by_id(Id), +pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + {ok, Sources ++ Conf}; +pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + {ok, Conf ++ Sources}; +pre_config_update({{replace_once, Id}, Source}, Conf) when is_map(Source), is_list(Conf) -> + {Index, _} = find_source_by_id(Id), {List1, List2} = lists:split(Index, Conf), - {ok, lists:droplast(List1) ++ [Rule] ++ List2}; -pre_config_update({_, Rules}, _Conf) when is_list(Rules)-> + {ok, lists:droplast(List1) ++ [Source] ++ List2}; +pre_config_update({_, Sources}, _Conf) when is_list(Sources)-> %% overwrite the entire config! - {ok, Rules}. + {ok, Sources}. post_config_update(_, undefined, _Conf, _AppEnvs) -> ok; -post_config_update({move, Id, <<"top">>}, _NewRules, _OldRules, _AppEnvs) -> - InitedRules = lookup(), - {Index, Rule} = find_rule_by_id(Id, InitedRules), - {Rules1, Rules2 } = lists:split(Index, InitedRules), - Rules3 = [Rule] ++ lists:droplast(Rules1) ++ Rules2, - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Id, <<"top">>}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {Index, Source} = find_source_by_id(Id, InitedSources), + {Sources1, Sources2 } = lists:split(Index, InitedSources), + Sources3 = [Source] ++ lists:droplast(Sources1) ++ Sources2, + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, <<"bottom">>}, _NewRules, _OldRules, _AppEnvs) -> - InitedRules = lookup(), - {Index, Rule} = find_rule_by_id(Id, InitedRules), - {Rules1, Rules2 } = lists:split(Index, InitedRules), - Rules3 = lists:droplast(Rules1) ++ Rules2 ++ [Rule], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Id, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {Index, Source} = find_source_by_id(Id, InitedSources), + {Sources1, Sources2 } = lists:split(Index, InitedSources), + Sources3 = lists:droplast(Sources1) ++ Sources2 ++ [Source], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewRules, _OldRules, _AppEnvs) -> - InitedRules = lookup(), - {_, Rule0} = find_rule_by_id(Id, InitedRules), - {Index, Rule1} = find_rule_by_id(BeforeId, InitedRules), - {Rules1, Rules2} = lists:split(Index, InitedRules), - Rules3 = lists:delete(Rule0, lists:droplast(Rules1)) - ++ [Rule0] ++ [Rule1] - ++ lists:delete(Rule0, Rules2), - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {_, Source0} = find_source_by_id(Id, InitedSources), + {Index, Source1} = find_source_by_id(BeforeId, InitedSources), + {Sources1, Sources2} = lists:split(Index, InitedSources), + Sources3 = lists:delete(Source0, lists:droplast(Sources1)) + ++ [Source0] ++ [Source1] + ++ lists:delete(Source0, Sources2), + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewRules, _OldRules, _AppEnvs) -> - InitedRules = lookup(), - {_, Rule} = find_rule_by_id(Id, InitedRules), - {Index, _} = find_rule_by_id(AfterId, InitedRules), - {Rules1, Rules2} = lists:split(Index, InitedRules), - Rules3 = lists:delete(Rule, Rules1) - ++ [Rule] - ++ lists:delete(Rule, Rules2), - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Rules3]}, -1), +post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewSources, _OldSources, _AppEnvs) -> + InitedSources = lookup(), + {_, Source} = find_source_by_id(Id, InitedSources), + {Index, _} = find_source_by_id(AfterId, InitedSources), + {Sources1, Sources2} = lists:split(Index, InitedSources), + Sources3 = lists:delete(Source, Sources1) + ++ [Source] + ++ lists:delete(Source, Sources2), + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({head, Rules}, _NewRules, _OldConf, _AppEnvs) -> - InitedRules = [init_provider(R) || R <- check_rules(Rules)], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules ++ lookup()]}, -1), +post_config_update({head, Sources}, _NewSources, _OldConf, _AppEnvs) -> + InitedSources = [init_source(R) || R <- check_sources(Sources)], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources ++ lookup()]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({tail, Rules}, _NewRules, _OldConf, _AppEnvs) -> - InitedRules = [init_provider(R) || R <- check_rules(Rules)], - emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedRules]}, -1), +post_config_update({tail, Sources}, _NewSources, _OldConf, _AppEnvs) -> + InitedSources = [init_source(R) || R <- check_sources(Sources)], + emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({{replace_once, Id}, Rule}, _NewRules, _OldConf, _AppEnvs) when is_map(Rule) -> - OldInitedRules = lookup(), - {Index, OldRule} = find_rule_by_id(Id, OldInitedRules), - case maps:get(type, OldRule, undefined) of +post_config_update({{replace_once, Id}, Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) -> + OldInitedSources = lookup(), + {Index, OldSource} = find_source_by_id(Id, OldInitedSources), + case maps:get(type, OldSource, undefined) of undefined -> ok; _ -> - #{annotations := #{id := Id}} = OldRule, + #{annotations := #{id := Id}} = OldSource, ok = emqx_resource:remove(Id) end, - {OldRules1, OldRules2 } = lists:split(Index, OldInitedRules), - InitedRules = [init_provider(R#{annotations => #{id => Id}}) || R <- check_rules([Rule])], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldRules1) ++ InitedRules ++ OldRules2]}, -1), + {OldSources1, OldSources2 } = lists:split(Index, OldInitedSources), + InitedSources = [init_source(R#{annotations => #{id => Id}}) || R <- check_sources([Source])], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldSources1) ++ InitedSources ++ OldSources2]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update(_, NewRules, _OldConf, _AppEnvs) -> +post_config_update(_, NewSources, _OldConf, _AppEnvs) -> %% overwrite the entire config! - OldInitedRules = lookup(), - InitedRules = [init_provider(Rule) || Rule <- NewRules], - ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules]}, -1), + OldInitedSources = lookup(), + InitedSources = [init_source(Source) || Source <- NewSources], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedSources]}, -1), lists:foreach(fun (#{type := _Type, enable := true, annotations := #{id := Id}}) -> ok = emqx_resource:remove(Id); (_) -> ok - end, OldInitedRules), + end, OldInitedSources), ok = emqx_authz_cache:drain_cache(). %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -check_rules(RawRules) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"rules">> => RawRules}}), #{format => richmap}), +check_sources(RawSources) -> + {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization:= #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), - Rules. + #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), + Sources. -find_rule_by_id(Id) -> find_rule_by_id(Id, lookup()). -find_rule_by_id(Id, Rules) -> find_rule_by_id(Id, Rules, 1). -find_rule_by_id(_RuleId, [], _N) -> error(not_found_rule); -find_rule_by_id(RuleId, [ Rule = #{annotations := #{id := Id}} | Tail], N) -> - case RuleId =:= Id of - true -> {N, Rule}; - false -> find_rule_by_id(RuleId, Tail, N + 1) +find_source_by_id(Id) -> find_source_by_id(Id, lookup()). +find_source_by_id(Id, Sources) -> find_source_by_id(Id, Sources, 1). +find_source_by_id(_SourceId, [], _N) -> error(not_found_rule); +find_source_by_id(SourceId, [ Source = #{annotations := #{id := Id}} | Tail], N) -> + case SourceId =:= Id of + true -> {N, Source}; + false -> find_source_by_id(SourceId, Tail, N + 1) end. find_action_in_hooks() -> @@ -232,10 +232,10 @@ create_resource(#{type := DB, {error, Reason} -> {error, Reason} end. -init_provider(#{enable := true, +init_source(#{enable := true, type := file, path := Path - } = Rule) -> + } = Source) -> Rules = case file:consult(Path) of {ok, Terms} -> [emqx_authz_rule:compile(Term) || Term <- Terms]; @@ -249,58 +249,58 @@ init_provider(#{enable := true, ?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]), error(Reason) end, - Rule#{annotations => + Source#{annotations => #{id => gen_id(file), rules => Rules }}; -init_provider(#{enable := true, +init_source(#{enable := true, type := http, config := #{url := Url} = Config - } = Rule) -> + } = Source) -> NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), - case create_resource(Rule#{config := NConfig}) of + case create_resource(Source#{config := NConfig}) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Rule#{annotations => + Id -> Source#{annotations => #{id => Id} } end; -init_provider(#{enable := true, +init_source(#{enable := true, type := DB - } = Rule) when DB =:= redis; + } = Source) when DB =:= redis; DB =:= mongo -> - case create_resource(Rule) of + case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Rule#{annotations => + Id -> Source#{annotations => #{id => Id} } end; -init_provider(#{enable := true, +init_source(#{enable := true, type := DB, sql := SQL - } = Rule) when DB =:= mysql; + } = Source) when DB =:= mysql; DB =:= pgsql -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), - case create_resource(Rule) of + case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Rule#{annotations => + Id -> Source#{annotations => #{id => Id, sql => Mod:parse_query(SQL) } } end; -init_provider(#{enable := false} = Rule) ->Rule. +init_source(#{enable := false} = Source) ->Source. %%-------------------------------------------------------------------- %% AuthZ callbacks %%-------------------------------------------------------------------- %% @doc Check AuthZ --spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), allow | deny, rules()) +-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), allow | deny, sources()) -> {stop, allow} | {ok, deny}). authorize(#{username := Username, peerhost := IpAddress - } = Client, PubSub, Topic, DefaultResult, Rules) -> - case do_authorize(Client, PubSub, Topic, Rules) of + } = Client, PubSub, Topic, DefaultResult, Sources) -> + case do_authorize(Client, PubSub, Topic, Sources) 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_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index ce437ab2b..64ef09601 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -20,7 +20,7 @@ structs() -> ["authorization"]. fields("authorization") -> - [ {rules, rules()} + [ {sources, sources()} ]; fields(file) -> [ {type, #{type => file}} @@ -146,7 +146,7 @@ fields(eq_topic) -> union_array(Item) when is_list(Item) -> hoconsc:array(hoconsc:union(Item)). -rules() -> +sources() -> #{type => union_array( [ hoconsc:ref(?MODULE, file) , hoconsc:ref(?MODULE, http) diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 36e706d13..ef7644a65 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -60,14 +60,14 @@ init_per_testcase(_, Config) -> {ok, _} = emqx_authz:update(replace, []), Config. --define(RULE1, #{<<"type">> => <<"http">>, +-define(SOURCE1, #{<<"type">> => <<"http">>, <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, <<"headers">> => #{}, <<"method">> => <<"get">>, <<"request_timeout">> => 5000} }). --define(RULE2, #{<<"type">> => <<"mongo">>, +-define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"config">> => #{ <<"mongo_type">> => <<"single">>, <<"server">> => <<"127.0.0.1:27017">>, @@ -77,7 +77,7 @@ init_per_testcase(_, Config) -> <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }). --define(RULE3, #{<<"type">> => <<"mysql">>, +-define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -88,7 +88,7 @@ init_per_testcase(_, Config) -> <<"ssl">> => #{<<"enable">> => false}}, <<"sql">> => <<"abcb">> }). --define(RULE4, #{<<"type">> => <<"pgsql">>, +-define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -99,7 +99,7 @@ init_per_testcase(_, Config) -> <<"ssl">> => #{<<"enable">> => false}}, <<"sql">> => <<"abcb">> }). --define(RULE5, #{<<"type">> => <<"redis">>, +-define(SOURCE5, #{<<"type">> => <<"redis">>, <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -114,21 +114,21 @@ init_per_testcase(_, Config) -> %% Testcases %%------------------------------------------------------------------------------ -t_update_rule(_) -> - {ok, _} = emqx_authz:update(replace, [?RULE2]), - {ok, _} = emqx_authz:update(head, [?RULE1]), - {ok, _} = emqx_authz:update(tail, [?RULE3]), +t_update_source(_) -> + {ok, _} = emqx_authz:update(replace, [?SOURCE2]), + {ok, _} = emqx_authz:update(head, [?SOURCE1]), + {ok, _} = emqx_authz:update(tail, [?SOURCE3]), - ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization, rules], [])), + ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization, sources], [])), [#{annotations := #{id := Id1}, type := http}, #{annotations := #{id := Id2}, type := mongo}, #{annotations := #{id := Id3}, type := mysql} ] = emqx_authz:lookup(), - {ok, _} = emqx_authz:update({replace_once, Id1}, ?RULE5), - {ok, _} = emqx_authz:update({replace_once, Id3}, ?RULE4), - ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization, rules], [])), + {ok, _} = emqx_authz:update({replace_once, Id1}, ?SOURCE5), + {ok, _} = emqx_authz:update({replace_once, Id3}, ?SOURCE4), + ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization, sources], [])), [#{annotations := #{id := Id1}, type := redis}, #{annotations := #{id := Id2}, type := mongo}, @@ -137,8 +137,8 @@ t_update_rule(_) -> {ok, _} = emqx_authz:update(replace, []). -t_move_rule(_) -> - {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4, ?RULE5]), +t_move_source(_) -> + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index dc321cf98..8d92413b3 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). -import(emqx_ct_http, [ request_api/3 , request_api/5 @@ -37,14 +37,14 @@ -define(API_VERSION, "v5"). -define(BASE_PATH, "api"). --define(RULE1, #{<<"type">> => <<"http">>, +-define(SOURCE1, #{<<"type">> => <<"http">>, <<"config">> => #{ <<"url">> => <<"https://fake.com:443/">>, <<"headers">> => #{}, <<"method">> => <<"get">>, <<"request_timeout">> => 5000} }). --define(RULE2, #{<<"type">> => <<"mongo">>, +-define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"config">> => #{ <<"mongo_type">> => <<"single">>, <<"server">> => <<"127.0.0.1:27017">>, @@ -54,7 +54,7 @@ <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }). --define(RULE3, #{<<"type">> => <<"mysql">>, +-define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -65,7 +65,7 @@ <<"ssl">> => #{<<"enable">> => false}}, <<"sql">> => <<"abcb">> }). --define(RULE4, #{<<"type">> => <<"pgsql">>, +-define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -76,7 +76,7 @@ <<"ssl">> => #{<<"enable">> => false}}, <<"sql">> => <<"abcb">> }). --define(RULE5, #{<<"type">> => <<"redis">>, +-define(SOURCE5, #{<<"type">> => <<"redis">>, <<"config">> => #{ <<"server">> => <<"127.0.0.1:27017">>, <<"pool_size">> => 1, @@ -148,7 +148,7 @@ t_api(_) -> ?assertEqual([], get_rules(Result1)), lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization"]), ?RULE1) + {ok, 204, _} = request(post, uri(["authorization"]), ?SOURCE1) end, lists:seq(1, 20)), {ok, 200, Result2} = request(get, uri(["authorization"]), []), ?assertEqual(20, length(get_rules(Result2))), @@ -160,7 +160,7 @@ t_api(_) -> ?assertEqual(10, length(get_rules(Result))) end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization"]), [?RULE1, ?RULE2, ?RULE3, ?RULE4]), + {ok, 204, _} = request(put, uri(["authorization"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), {ok, 200, Result3} = request(get, uri(["authorization"]), []), Rules = get_rules(Result3), @@ -173,7 +173,7 @@ t_api(_) -> #{<<"annotations">> := #{<<"id">> := Id}} = lists:nth(2, Rules), - {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), ?RULE5), + {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), ?SOURCE5), {ok, 200, Result4} = request(get, uri(["authorization", binary_to_list(Id)]), []), ?assertMatch(#{<<"type">> := <<"redis">>}, jsx:decode(Result4)), @@ -186,7 +186,7 @@ t_api(_) -> ok. t_move_rule(_) -> - {ok, _} = emqx_authz:update(replace, [?RULE1, ?RULE2, ?RULE3, ?RULE4, ?RULE5]), + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), [#{annotations := #{id := Id1}}, #{annotations := #{id := Id2}}, #{annotations := #{id := Id3}}, diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index b0525bb24..fad5e9580 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -21,7 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index 6e2f398a4..db111ce83 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -66,16 +66,16 @@ end_per_suite(_Config) -> meck:unload(emqx_schema), ok. --define(RULE1,[#{<<"topics">> => [<<"#">>], +-define(SOURCE1,[#{<<"topics">> => [<<"#">>], <<"permission">> => <<"deny">>, <<"action">> => <<"all">>}]). --define(RULE2,[#{<<"topics">> => [<<"eq #">>], +-define(SOURCE2,[#{<<"topics">> => [<<"eq #">>], <<"permission">> => <<"allow">>, <<"action">> => <<"all">>}]). --define(RULE3,[#{<<"topics">> => [<<"test/%c">>], +-define(SOURCE3,[#{<<"topics">> => [<<"test/%c">>], <<"permission">> => <<"allow">>, <<"action">> => <<"subscribe">>}]). --define(RULE4,[#{<<"topics">> => [<<"test/%u">>], +-define(SOURCE4,[#{<<"topics">> => [<<"test/%u">>], <<"permission">> => <<"allow">>, <<"action">> => <<"publish">>}]). @@ -107,15 +107,15 @@ t_authz(_) -> ?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), + meck:expect(emqx_resource, query, fun(_, _) -> ?SOURCE1 ++ ?SOURCE2 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), + meck:expect(emqx_resource, query, fun(_, _) -> ?SOURCE2 ++ ?SOURCE1 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), + meck:expect(emqx_resource, query, fun(_, _) -> ?SOURCE3 ++ ?SOURCE4 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">>)), diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index dc8d99e59..0675e1caf 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -72,10 +72,10 @@ end_per_suite(_Config) -> , <<"permission">> , <<"topic">> ]). --define(RULE1, [[<<"all">>, <<"deny">>, <<"#">>]]). --define(RULE2, [[<<"all">>, <<"allow">>, <<"eq #">>]]). --define(RULE3, [[<<"subscribe">>, <<"allow">>, <<"test/%c">>]]). --define(RULE4, [[<<"publish">>, <<"allow">>, <<"test/%u">>]]). +-define(SOURCE1, [[<<"all">>, <<"deny">>, <<"#">>]]). +-define(SOURCE2, [[<<"all">>, <<"allow">>, <<"eq #">>]]). +-define(SOURCE3, [[<<"subscribe">>, <<"allow">>, <<"test/%c">>]]). +-define(SOURCE4, [[<<"publish">>, <<"allow">>, <<"test/%u">>]]). %%------------------------------------------------------------------------------ %% Testcases @@ -105,15 +105,15 @@ t_authz(_) -> ?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), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE1 ++ ?SOURCE2} end), ?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), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE2 ++ ?SOURCE1} end), ?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), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE3 ++ ?SOURCE4} 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">>)), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 53a91bd35..6880ab405 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -72,10 +72,10 @@ end_per_suite(_Config) -> , {column, <<"permission">>, meck, meck, meck, meck, meck, meck, meck} , {column, <<"topic">>, meck, meck, meck, meck, meck, meck, meck} ]). --define(RULE1, [{<<"all">>, <<"deny">>, <<"#">>}]). --define(RULE2, [{<<"all">>, <<"allow">>, <<"eq #">>}]). --define(RULE3, [{<<"subscribe">>, <<"allow">>, <<"test/%c">>}]). --define(RULE4, [{<<"publish">>, <<"allow">>, <<"test/%u">>}]). +-define(SOURCE1, [{<<"all">>, <<"deny">>, <<"#">>}]). +-define(SOURCE2, [{<<"all">>, <<"allow">>, <<"eq #">>}]). +-define(SOURCE3, [{<<"subscribe">>, <<"allow">>, <<"test/%c">>}]). +-define(SOURCE4, [{<<"publish">>, <<"allow">>, <<"test/%u">>}]). %%------------------------------------------------------------------------------ %% Testcases @@ -105,15 +105,15 @@ t_authz(_) -> ?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), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE1 ++ ?SOURCE2} end), ?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), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE2 ++ ?SOURCE1} end), ?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), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?SOURCE3 ++ ?SOURCE4} 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">>)), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 2d1e9161e..09682761d 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -21,7 +21,7 @@ -include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(CONF_DEFAULT, <<"authorization: {rules: []}">>). +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). all() -> emqx_ct:all(?MODULE). @@ -66,9 +66,9 @@ end_per_suite(_Config) -> meck:unload(emqx_schema), ok. --define(RULE1, [<<"test/%u">>, <<"publish">>]). --define(RULE2, [<<"test/%c">>, <<"publish">>]). --define(RULE3, [<<"#">>, <<"subscribe">>]). +-define(SOURCE1, [<<"test/%u">>, <<"publish">>]). +-define(SOURCE2, [<<"test/%c">>, <<"publish">>]). +-define(SOURCE3, [<<"#">>, <<"subscribe">>]). %%------------------------------------------------------------------------------ %% Testcases @@ -90,7 +90,7 @@ t_authz(_) -> emqx_access_control:authorize(ClientInfo, publish, <<"#">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?SOURCE1 ++ ?SOURCE2} end), % nomatch ?assertEqual(deny, emqx_access_control:authorize(ClientInfo, subscribe, <<"+">>)), @@ -103,7 +103,7 @@ t_authz(_) -> ?assertEqual(allow, emqx_access_control:authorize(ClientInfo, publish, <<"test/clientid">>)), - meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end), + meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?SOURCE3} end), ?assertEqual(allow, emqx_access_control:authorize(ClientInfo, subscribe, <<"#">>)), diff --git a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl index ff215354a..c38d99cba 100644 --- a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl @@ -22,11 +22,11 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(RULE1, {deny, all, all, ["#"]}). --define(RULE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}). --define(RULE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}). --define(RULE4, {allow, {'and', [{clientid, "^test?"}, {username, "^test?"}]}, publish, ["topic/test"]}). --define(RULE5, {allow, {'or', [{username, "^test"}, {clientid, "test?"}]}, publish, ["%u", "%c"]}). +-define(SOURCE1, {deny, all, all, ["#"]}). +-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}). +-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}). +-define(SOURCE4, {allow, {'and', [{clientid, "^test?"}, {username, "^test?"}]}, publish, ["topic/test"]}). +-define(SOURCE5, {allow, {'or', [{username, "^test"}, {clientid, "test?"}]}, publish, ["%u", "%c"]}). all() -> emqx_ct:all(?MODULE). @@ -40,28 +40,28 @@ end_per_suite(_Config) -> ok. t_compile(_) -> - ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?RULE1)), + ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)), - ?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?RULE2)), + ?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?SOURCE2)), ?assertEqual({allow, {ipaddrs,[{{127,0,0,1},{127,0,0,1},32}, {{192,168,1,0},{192,168,1,255},24}]}, subscribe, [{pattern,[<<"%c">>]}] - }, emqx_authz_rule:compile(?RULE3)), + }, emqx_authz_rule:compile(?SOURCE3)), ?assertMatch({allow, {'and', [{clientid, {re_pattern, _, _, _, _}}, {username, {re_pattern, _, _, _, _}}]}, publish, [[<<"topic">>, <<"test">>]] - }, emqx_authz_rule:compile(?RULE4)), + }, emqx_authz_rule:compile(?SOURCE4)), ?assertMatch({allow, {'or', [{username, {re_pattern, _, _, _, _}}, {clientid, {re_pattern, _, _, _, _}}]}, publish, [{pattern, [<<"%u">>]}, {pattern, [<<"%c">>]}] - }, emqx_authz_rule:compile(?RULE5)), + }, emqx_authz_rule:compile(?SOURCE5)), ok. @@ -92,47 +92,47 @@ t_match(_) -> }, ?assertEqual({matched, deny}, - emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE1))), + emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?SOURCE1))), ?assertEqual({matched, deny}, - emqx_authz_rule:match(ClientInfo2, subscribe, <<"+">>, emqx_authz_rule:compile(?RULE1))), + emqx_authz_rule:match(ClientInfo2, subscribe, <<"+">>, emqx_authz_rule:compile(?SOURCE1))), ?assertEqual({matched, deny}, - emqx_authz_rule:match(ClientInfo3, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE1))), + emqx_authz_rule:match(ClientInfo3, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE1))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE2))), + emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?SOURCE2))), ?assertEqual(nomatch, - emqx_authz_rule:match(ClientInfo1, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE2))), + emqx_authz_rule:match(ClientInfo1, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE2))), ?assertEqual(nomatch, - emqx_authz_rule:match(ClientInfo2, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE2))), + emqx_authz_rule:match(ClientInfo2, subscribe, <<"#">>, emqx_authz_rule:compile(?SOURCE2))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo1, subscribe, <<"test">>, emqx_authz_rule:compile(?RULE3))), + emqx_authz_rule:match(ClientInfo1, subscribe, <<"test">>, emqx_authz_rule:compile(?SOURCE3))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo2, subscribe, <<"test">>, emqx_authz_rule:compile(?RULE3))), + emqx_authz_rule:match(ClientInfo2, subscribe, <<"test">>, emqx_authz_rule:compile(?SOURCE3))), ?assertEqual(nomatch, - emqx_authz_rule:match(ClientInfo2, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE3))), + emqx_authz_rule:match(ClientInfo2, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE3))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo2, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + emqx_authz_rule:match(ClientInfo2, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), ?assertEqual(nomatch, - emqx_authz_rule:match(ClientInfo3, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + emqx_authz_rule:match(ClientInfo3, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), ?assertEqual(nomatch, - emqx_authz_rule:match(ClientInfo4, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))), + emqx_authz_rule:match(ClientInfo4, publish, <<"topic/test">>, emqx_authz_rule:compile(?SOURCE4))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo1, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + emqx_authz_rule:match(ClientInfo1, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo2, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + emqx_authz_rule:match(ClientInfo2, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo3, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + emqx_authz_rule:match(ClientInfo3, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo3, publish, <<"fake">>, emqx_authz_rule:compile(?RULE5))), + emqx_authz_rule:match(ClientInfo3, publish, <<"fake">>, emqx_authz_rule:compile(?SOURCE5))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo4, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))), + emqx_authz_rule:match(ClientInfo4, publish, <<"test">>, emqx_authz_rule:compile(?SOURCE5))), ?assertEqual({matched, allow}, - emqx_authz_rule:match(ClientInfo4, publish, <<"fake">>, emqx_authz_rule:compile(?RULE5))), + emqx_authz_rule:match(ClientInfo4, publish, <<"fake">>, emqx_authz_rule:compile(?SOURCE5))), ok. From 9893c0263ad4d2c1bbd437b4354aba0996de054c Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 27 Aug 2021 18:21:49 +0800 Subject: [PATCH 168/306] refactor: banned api (#5578) --- apps/emqx/include/emqx.hrl | 3 +- apps/emqx/src/emqx_banned.erl | 31 +++- .../src/emqx_mgmt_api_banned.erl | 144 ++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 apps/emqx_management/src/emqx_mgmt_api_banned.erl diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 58fdc5f98..633527b57 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -126,8 +126,7 @@ -record(banned, { who :: {clientid, binary()} | {peerhost, inet:ip_address()} - | {username, binary()} - | {ip_address, inet:ip_address()}, + | {username, binary()}, by :: binary(), reason :: binary(), at :: integer(), diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index c143a20a6..715548d41 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -33,8 +33,10 @@ -export([ check/1 , create/1 + , look_up/1 , delete/1 , info/1 + , format/1 ]). %% gen_server callbacks @@ -90,7 +92,31 @@ do_check(Who) when is_tuple(Who) -> Until > erlang:system_time(second) end. --spec(create(emqx_types:banned()) -> ok). +format(#banned{who = Who0, + by = By, + reason = Reason, + at = At, + until = Until}) -> + {As, Who} = maybe_format_host(Who0), + #{ + as => As, + who => Who, + by => By, + reason => Reason, + at => to_rfc3339(At), + until => to_rfc3339(Until) + }. + +maybe_format_host({peerhost, Host}) -> + AddrBinary = list_to_binary(inet:ntoa(Host)), + {peerhost, AddrBinary}; +maybe_format_host({As, Who}) -> + {As, Who}. + +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])). + +-spec(create(emqx_types:banned() | map()) -> ok). create(#{who := Who, by := By, reason := Reason, @@ -104,6 +130,9 @@ create(#{who := Who, create(Banned) when is_record(Banned, banned) -> ekka_mnesia:dirty_write(?BANNED_TAB, Banned). +look_up(Who) -> + mnesia:dirty_read(?BANNED_TAB, Who). + -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl new file mode 100644 index 000000000..18abbd7e1 --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl @@ -0,0 +1,144 @@ +%%-------------------------------------------------------------------- +%% 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_banned). + +-include_lib("emqx/include/emqx.hrl"). + +-include("emqx_mgmt.hrl"). + +-behaviour(minirest_api). + +-export([api_spec/0]). + +-export([ banned/2 + , delete_banned/2 + ]). + +-import(emqx_mgmt_util, [ page_params/0 + , schema/1 + , object_schema/1 + , page_object_schema/1 + , properties/1 + , error_schema/1 + ]). + +-export([format/1]). + +-define(TAB, emqx_banned). + +api_spec() -> + {[banned_api(), delete_banned_api()], []}. + +-define(BANNED_TYPES, [clientid, username, peerhost]). + +properties() -> + properties([ + {as, string, <<"Banned type clientid, username, peerhost">>, [clientid, username, peerhost]}, + {who, string, <<"Client info as banned type">>}, + {by, integer, <<"Commander">>}, + {reason, string, <<"Banned reason">>}, + {at, integer, <<"Create banned time. Nullable, rfc3339, default is now">>}, + {until, string, <<"Cancel banned time. Nullable, rfc3339, default is now + 5 minute">>} + ]). + +banned_api() -> + Path = "/banned", + MetaData = #{ + get => #{ + description => <<"List banned">>, + parameters => page_params(), + responses => #{ + <<"200">> => + page_object_schema(properties())}}, + post => #{ + description => <<"Create banned">>, + 'requestBody' => object_schema(properties()), + responses => #{ + <<"200">> => schema(<<"Create success">>)}}}, + {Path, MetaData, banned}. + +delete_banned_api() -> + Path = "/banned/:as/:who", + MetaData = #{ + delete => #{ + description => <<"Delete banned">>, + parameters => [ + #{ + name => as, + in => path, + required => true, + description => <<"Banned type">>, + schema => #{type => string, enum => ?BANNED_TYPES} + }, + #{ + name => who, + in => path, + required => true, + description => <<"Client info as banned type">>, + schema => #{type => string} + } + ], + responses => #{ + <<"200">> => schema(<<"Delete banned success">>), + <<"404">> => error_schema(<<"Banned not found">>)}}}, + {Path, MetaData, delete_banned}. + +banned(get, #{query_string := Params}) -> + Response = emqx_mgmt_api:paginate(?TAB, Params, fun format/1), + {200, Response}; +banned(post, #{body := Body}) -> + Banned = trans_param(Body), + _ = emqx_banned:create(Banned), + {200}. + +delete_banned(delete, #{bindings := Params}) -> + Who = trans_who(Params), + case emqx_banned:look_up(Who) of + [] -> + As0 = maps:get(as, Params), + Who0 = maps:get(who, Params), + Message = list_to_binary(io_lib:format("~p: ~p not found", [As0, Who0])), + {404, #{code => 'RESOURCE_NOT_FOUND', message => Message}}; + _ -> + ok = emqx_banned:delete(Who), + {200} + end. + +trans_param(Params) -> + Who = trans_who(Params), + By = maps:get(<<"by">>, Params, <<"mgmt_api">>), + Reason = maps:get(<<"reason">>, Params, <<"">>), + At = maps:get(<<"at">>, Params, erlang:system_time(second)), + Until = maps:get(<<"until">>, Params, At + 5 * 60), + #banned{ + who = Who, + by = By, + reason = Reason, + at = At, + until = Until + }. + +trans_who(#{as := As, who := Who}) -> + trans_who(#{<<"as">> => As, <<"who">> => Who}); +trans_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> + {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), + {peerhost, Peerhost}; +trans_who(#{<<"as">> := As, <<"who">> := Who}) -> + {binary_to_atom(As, utf8), Who}. + +format(Banned) -> + emqx_banned:format(Banned). From 75dc4ea9a22965fcd03efeeff53efffd3ae85619 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 20 Aug 2021 09:46:06 +0800 Subject: [PATCH 169/306] feat(gw): add the http api skeleton --- apps/emqx_gateway/src/emqx_gateway_api.erl | 389 ++++++++++++++++++ .../src/emqx_gateway_api_authn.erl | 25 ++ .../src/emqx_gateway_api_client.erl | 25 ++ 3 files changed, 439 insertions(+) create mode 100644 apps/emqx_gateway/src/emqx_gateway_api.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_api_authn.erl create mode 100644 apps/emqx_gateway/src/emqx_gateway_api_client.erl diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl new file mode 100644 index 000000000..0aca3c8f3 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -0,0 +1,389 @@ +%%-------------------------------------------------------------------- +%% 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_api). + +-behaviour(minirest_api). + +-compile(nowarn_unused_function). + +%% minirest behaviour callbacks +-export([api_spec/0]). + +%% http handlers +-export([ gateway/2 + , gateway_insta/2 + ]). + +-define(EXAMPLE_GATEWAY_LIST, + [ #{ name => <<"lwm2m">> + , status => <<"running">> + , started_at => <<"2021-08-19T11:45:56.006373+08:00">> + , max_connection => 1024000 + , current_connection => 12 + , listeners => [ + #{name => <<"lw-udp-1">>, status => <<"activing">>}, + #{name => <<"lw-udp-2">>, status => <<"inactived">>} + ] + } + ]). + +-define(EXAMPLE_STOMP_GATEWAY_CONF, #{ + frame => #{ + max_headers => 10, + max_headers_length => 1024, + max_body_length => 8192 + }, + listener => #{ + tcp => #{<<"default-stomp-listener">> => #{ + bind => <<"61613">> + }} + } + }). + +-define(EXAMPLE_MQTTSN_GATEWAY_CONF, #{ + }). + +-define(EXAMPLE_GATEWAY_STATS, #{ + max_connection => 10240000, + current_connection => 1000, + messages_in => 100.24, + messages_out => 32.5 + }). + +%%-------------------------------------------------------------------- +%% minirest behaviour callbacks +%%-------------------------------------------------------------------- + +api_spec() -> + {apis(), schemas()}. + +apis() -> + [ {"/gateway", metadata(gateway), gateway} + , {"/gateway/:name", metadata(gateway_insta), gateway_insta} + , {"/gateway/:name/stats", metadata(gateway_insta_stats), gateway_insta_stats} + ]. + +metadata(gateway) -> + #{get => #{ + description => <<"Get gateway list">>, + parameters => [ + #{name => status, + in => query, + schema => #{type => string}, + required => false + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"gateway_overrview">>), + examples => #{ + simple => #{ + summary => <<"Gateway List Example">>, + value => emqx_json:encode(?EXAMPLE_GATEWAY_LIST) + } + } + } + } + } + } + }}; + +metadata(gateway_insta) -> + UriNameParamDef = #{name => name, + in => path, + schema => #{type => string}, + required => true + }, + NameNotFoundRespDef = + #{description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>), + examples => #{ + simple => #{ + summary => <<"Not Found">>, + value => #{ + code => <<"NOT_FOUND">>, + message => <<"gateway xxx not found">> + } + } + } + } + }}, + #{delete => #{ + description => <<"Delete/Unload the gateway">>, + parameters => [UriNameParamDef], + responses => #{ + <<"404">> => NameNotFoundRespDef, + <<"204">> => #{description => <<"No Content">>} + } + }, + get => #{ + description => <<"Get the gateway configurations">>, + parameters => [UriNameParamDef], + responses => #{ + <<"404">> => NameNotFoundRespDef, + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"gateway_conf">>), + examples => #{ + simple1 => #{ + summary => <<"Stomp Gateway">>, + value => emqx_json:encode(?EXAMPLE_STOMP_GATEWAY_CONF) + }, + simple2 => #{ + summary => <<"MQTT-SN Gateway">>, + value => emqx_json:encode(?EXAMPLE_MQTTSN_GATEWAY_CONF) + } + } + } + } + } + } + }, + put => #{ + description => <<"Update the gateway configurations/status">>, + parameters => [UriNameParamDef], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"gateway_conf">>), + examples => #{ + simple1 => #{ + summary => <<"Stom Gateway">>, + value => emqx_json:encode(?EXAMPLE_STOMP_GATEWAY_CONF) + }, + simple2 => #{ + summary => <<"MQTT-SN Gateway">>, + value => emqx_json:encode(?EXAMPLE_MQTTSN_GATEWAY_CONF) + } + } + } + } + }, + responses => #{ + <<"404">> => NameNotFoundRespDef, + <<"204">> => #{description => <<"Created">>} + } + } + }; + +metadata(gateway_insta_stats) -> + #{get => #{ + description => <<"Get gateway Statistic">>, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"gateway_stats">>), + examples => #{ + simple => #{ + summary => <<"Gateway Statistic">>, + value => emqx_json:encode(?EXAMPLE_GATEWAY_STATS) + } + } + } + } + } + } + }}. + +schemas() -> + [ #{<<"gateway_overrview">> => schema_for_gateway_overrview()} + , #{<<"gateway_conf">> => schema_for_gateway_conf()} + , #{<<"gateway_stats">> => schema_for_gateway_stats()} + ]. + +schema_for_gateway_overrview() -> + #{type => array, + items => #{ + type => object, + properties => #{ + name => #{ + type => string, + example => <<"lwm2m">> + }, + status => #{ + type => string, + enum => [<<"running">>, <<"stopped">>, <<"unloaded">>], + example => <<"running">> + }, + started_at => #{ + type => string, + example => <<"2021-08-19T11:45:56.006373+08:00">> + }, + max_connection => #{ + type => integer, + example => 1024000 + }, + current_connection => #{ + type => integer, + example => 1000 + }, + listeners => #{ + type => array, + items => #{ + type => object, + properties => #{ + name => #{ + type => string, + example => <<"lw-udp">> + }, + status => #{ + type => string, + enum => [<<"activing">>, <<"inactived">>] + } + } + } + } + } + } + }. + +schema_for_gateway_conf() -> + #{oneOf => + [ schema_for_gateway_conf_stomp() + , schema_for_gateway_conf_mqttsn() + , schema_for_gateway_conf_coap() + , schema_for_gateway_conf_lwm2m() + , schema_for_gateway_conf_exproto() + ]}. + +schema_for_clientinfo_override() -> + #{type => object, + properties => #{ + clientid => #{type => string}, + username => #{type => string}, + password => #{type => string} + }}. + +schema_for_authenticator() -> + %% TODO. + #{type => object, properties => #{ + a_key => #{type => string} + }}. + +schema_for_tcp_listener() -> + %% TODO. + #{type => object, properties => #{ + a_key => #{type => string} + }}. + +schema_for_udp_listener() -> + %% TODO. + #{type => object, properties => #{ + a_key => #{type => string} + }}. + +%% It should be generated by _schema.erl module +%% and emqx_gateway.conf +schema_for_gateway_conf_stomp() -> + #{type => object, + properties => #{ + frame => #{ + type => object, + properties => #{ + max_headers => #{type => integer}, + max_headers_length => #{type => integer}, + max_body_length => #{type => integer} + } + }, + clientinfo_override => schema_for_clientinfo_override(), + authenticator => schema_for_authenticator(), + listener => schema_for_tcp_listener() + } + }. + +schema_for_gateway_conf_mqttsn() -> + #{type => object, + properties => #{ + gateway_id => #{type => integer}, + broadcast => #{type => boolean}, + enable_stats => #{type => boolean}, + enable_qos3 => #{type => boolean}, + idle_timeout => #{type => integer}, + predefined => #{ + type => array, + items => #{ + type => object, + properties => #{ + id => #{type => integer}, + topic => #{type => string} + } + } + }, + clientinfo_override => schema_for_clientinfo_override(), + authenticator => schema_for_authenticator(), + listener => schema_for_udp_listener() + }}. + + +schema_for_gateway_conf_coap() -> + #{type => object, + properties => #{ + clientinfo_override => schema_for_clientinfo_override(), + authenticator => schema_for_authenticator(), + listener => schema_for_udp_listener() + }}. + +schema_for_gateway_conf_lwm2m() -> + #{type => object, + properties => #{ + clientinfo_override => schema_for_clientinfo_override(), + authenticator => schema_for_authenticator(), + listener => schema_for_udp_listener() + }}. + +schema_for_gateway_conf_exproto() -> + #{type => object, + properties => #{ + clientinfo_override => schema_for_clientinfo_override(), + authenticator => schema_for_authenticator(), + listener => #{oneOf => [schema_for_tcp_listener(), + schema_for_udp_listener() + ] + } + }}. + +schema_for_gateway_stats() -> + #{type => object, + properties => #{ + a_key => #{type => string} + }}. + +%%-------------------------------------------------------------------- +%% http handlers + +gateway(get, _Request) -> + {200, ok}. + +gateway_insta(delete, _Request) -> + {200, ok}; +gateway_insta(get, _Request) -> + {200, ok}; +gateway_insta(put, _Request) -> + {200, ok}. + +gateway_insta_stats(get, _Req) -> + {401, <<"Implement it later (maybe 5.1)">>}. + diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl new file mode 100644 index 000000000..85eb4ddc7 --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl @@ -0,0 +1,25 @@ +%%-------------------------------------------------------------------- +%% 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_api_authn). + +-behaviour(minirest_api). + +%% minirest behaviour callbacks +-export([api_spec/0]). + +api_spec() -> + {[], []}. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_client.erl new file mode 100644 index 000000000..03fb056ad --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_api_client.erl @@ -0,0 +1,25 @@ +%%-------------------------------------------------------------------- +%% 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_api_client). + +-behaviour(minirest_api). + +%% minirest behaviour callbacks +-export([api_spec/0]). + +api_spec() -> + {[], []}. From 6de250741eeb896311b9be440bdade4995ee4733 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 20 Aug 2021 10:04:29 +0800 Subject: [PATCH 170/306] chore(gw): add started_at/created_at field --- apps/emqx_gateway/include/emqx_gateway.hrl | 18 ++++++++---- apps/emqx_gateway/src/emqx_gateway.erl | 7 +++-- apps/emqx_gateway/src/emqx_gateway_api.erl | 2 +- .../src/emqx_gateway_insta_sup.erl | 28 +++++++++++-------- .../src/emqx_gateway_registry.erl | 4 +-- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl index 9099ecd4d..d959eac8b 100644 --- a/apps/emqx_gateway/include/emqx_gateway.hrl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. @@ -17,17 +17,23 @@ -ifndef(EMQX_GATEWAY_HRL). -define(EMQX_GATEWAY_HRL, 1). --type instance_id() :: atom(). -type gateway_name() :: atom(). +%% The RawConf got from emqx:get_config/1 +-type rawconf() :: map(). + %% @doc The Gateway defination -type gateway() :: #{ name := gateway_name() , descr => binary() | undefined - %% Appears only in creating or detailed info - , rawconf => map() - %% Appears only in getting gateway status/info - , status => stopped | running + %% Appears only in getting gateway info + , status => stopped | running | unloaded + %% Timestamp in millisecond + , created_at => integer() + %% Timestamp in millisecond + , started_at => integer() + %% Appears only in getting gateway info + , rawconf => rawconf() }. -endif. diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 01d5897ff..3462f4d11 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -41,7 +41,7 @@ registered_gateway() -> list() -> emqx_gateway_sup:list_gateway_insta(). --spec load(gateway_name(), map()) +-spec load(gateway_name(), rawconf()) -> {ok, pid()} | {error, any()}. load(Name, RawConf) -> @@ -59,8 +59,9 @@ unload(Name) -> lookup(Name) -> emqx_gateway_sup:lookup_gateway(Name). --spec update(gateway()) -> ok | {error, any()}. -update(NewGateway) -> +-spec update(gateway_name(), rawconf()) -> ok | {error, any()}. +update(Name, RawConf) -> + NewGateway = #{name => Name, rawconf => RawConf}, emqx_gateway_sup:update_gateway(NewGateway). -spec start(gateway_name()) -> ok | {error, any()}. diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 0aca3c8f3..eea26b3a0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -33,7 +33,7 @@ , status => <<"running">> , started_at => <<"2021-08-19T11:45:56.006373+08:00">> , max_connection => 1024000 - , current_connection => 12 + , current_connection => 1000 , listeners => [ #{name => <<"lw-udp-1">>, status => <<"activing">>}, #{name => <<"lw-udp-2">>, status => <<"inactived">>} diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 2edccd033..7bb3069d5 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -21,7 +21,6 @@ -include("include/emqx_gateway.hrl"). - %% APIs -export([ start_link/3 , info/1 @@ -40,11 +39,13 @@ ]). -record(state, { - gw :: gateway(), - ctx :: emqx_gateway_ctx:context(), - status :: stopped | running, + gw :: gateway(), + ctx :: emqx_gateway_ctx:context(), + status :: stopped | running, child_pids :: [pid()], - gw_state :: emqx_gateway_impl:state() | undefined + gw_state :: emqx_gateway_impl:state() | undefined, + created_at :: integer(), + started_at :: integer() | undefined }). %%-------------------------------------------------------------------- @@ -92,7 +93,8 @@ init([Gateway, Ctx0, _GwDscrptr]) -> gw = Gateway, ctx = Ctx, child_pids = [], - status = stopped + status = stopped, + created_at = erlang:system_time(millisecond) }, case cb_gateway_load(State) of {error, Reason} -> @@ -116,8 +118,12 @@ do_deinit_context(Ctx) -> cleanup_authenticators_for_gateway_insta(maps:get(auth, Ctx)), ok. -handle_call(info, _From, State = #state{gw = Gateway, status = Status}) -> - {reply, Gateway#{status => Status}, State}; +handle_call(info, _From, State = #state{gw = Gateway}) -> + GwInfo = Gateway#{status => State#state.status, + created_at => State#state.created_at, + started_at => State#state.started_at + }, + {reply, GwInfo, State}; handle_call(disable, _From, State = #state{status = Status}) -> case Status of @@ -159,7 +165,7 @@ handle_call({update, NewGateway}, _From, State = #state{ %% Running -> update handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, - status = running}) -> + status = running}) -> case maps:get(name, NewGateway, undefined) == maps:get(name, Gateway, undefined) of true -> @@ -279,7 +285,8 @@ cb_gateway_load(State = #state{gw = Gateway = #{name := GwName}, {ok, State#state{ status = running, child_pids = ChildPids, - gw_state = GwState + gw_state = GwState, + started_at = erlang:system_time(millisecond) }} end catch @@ -303,7 +310,6 @@ cb_gateway_update(NewGateway, %% XXX: Hot-upgrade ??? ChildPids = start_child_process(ChildPidOrSpecs), {ok, State#state{ - status = running, child_pids = ChildPids, gw_state = NGwState }} diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl index b311073a9..cfa6d424a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -19,10 +19,9 @@ -include("include/emqx_gateway.hrl"). - -behavior(gen_server). -%% APIs for Impl. +%% APIs -export([ reg/2 , unreg/1 , list/0 @@ -100,7 +99,6 @@ call(Req) -> %%-------------------------------------------------------------------- init([]) -> - %% TODO: Metrics ??? process_flag(trap_exit, true), {ok, #state{reged = #{}}}. From eb8ec65162a78a22b8cfd3fc5a94ae728ea468ec Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 20 Aug 2021 16:05:07 +0800 Subject: [PATCH 171/306] refactor(gw): refactor authentication to authenticator --- apps/emqx_gateway/etc/emqx_gateway.conf | 34 ++++++------------ apps/emqx_gateway/src/emqx_gateway.app.src | 2 +- apps/emqx_gateway/src/emqx_gateway.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 6 ++-- .../src/emqx_gateway_insta_sup.erl | 36 +++++++++---------- apps/emqx_gateway/src/emqx_gateway_schema.erl | 35 ++++++++++++------ 6 files changed, 57 insertions(+), 58 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index c4b732c39..41d1d10de 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -17,16 +17,12 @@ gateway.stomp { password = "${Packet.headers.passcode}" } - authentication { - enable = true - authenticators = [ - { - name = "authenticator1" - mechanism = password-based - server_type = built-in-database - user_id_type = clientid - } - ] + authenticator { + #enable = true + name = "authenticator1" + mechanism = password-based + server_type = built-in-database + user_id_type = clientid } listener.tcp.1 { @@ -42,17 +38,11 @@ gateway.coap { enable_stats = false - #authentication.enable: false - authentication { - enable = true - authenticators = [ - { - name = "authenticator1" - mechanism = password-based - server_type = built-in-database - user_id_type = clientid - } - ] + authenticator { + name = "authenticator1" + mechanism = password-based + server_type = built-in-database + user_id_type = clientid } heartbeat = 30s @@ -123,8 +113,6 @@ gateway.exproto { #ssl.cacertfile: } - authentication.enable = false - listener.tcp.1 { bind = 7993 acceptors = 8 diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index e25b767cc..2fc329711 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {emqx_gateway_app, []}}, - {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx, emqx_authn]}, + {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx]}, {env, []}, {modules, []}, {licenses, ["Apache 2.0"]}, diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 3462f4d11..d2ab66362 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -23,7 +23,7 @@ , load/2 , unload/1 , lookup/1 - , update/1 + , update/2 , start/1 , stop/1 , list/0 diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index eea26b3a0..9b77cc643 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -72,7 +72,7 @@ api_spec() -> {apis(), schemas()}. apis() -> - [ {"/gateway", metadata(gateway), gateway} + [ {"/gateway", metadata(gateway), gateway} , {"/gateway/:name", metadata(gateway_insta), gateway_insta} , {"/gateway/:name/stats", metadata(gateway_insta_stats), gateway_insta_stats} ]. @@ -100,7 +100,7 @@ metadata(gateway) -> } } } - } + } } } }}; @@ -203,7 +203,7 @@ metadata(gateway_insta_stats) -> } } } - } + } } } }}. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 7bb3069d5..c32f10df6 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -105,10 +105,15 @@ init([Gateway, Ctx0, _GwDscrptr]) -> end. do_init_context(GwName, RawConf, Ctx) -> - Auth = case maps:get(authentication, RawConf, #{enable => false}) of - #{enable := true, - authenticators := AuthCfgs} when is_list(AuthCfgs) -> - create_authenticators_for_gateway_insta(GwName, AuthCfgs); + Auth = case maps:get(authenticators, RawConf, #{enable => false}) of + #{enable := false} -> undefined; + AuthCfg when is_map(AuthCfg) -> + case maps:get(enable, AuthCfg, true) of + false -> + undefined; + _ -> + create_authenticator_for_gateway_insta(GwName, AuthCfg) + end; _ -> undefined end, @@ -220,25 +225,16 @@ code_change(_OldVsn, State, _Extra) -> %% Internal funcs %%-------------------------------------------------------------------- -%% @doc AuthCfgs is a array of authenticatior configurations, -%% see: emqx_authn_schema:authenticators/1 -create_authenticators_for_gateway_insta(GwName, AuthCfgs) -> +create_authenticator_for_gateway_insta(GwName, AuthCfg) -> ChainId = atom_to_binary(GwName, utf8), case emqx_authn:create_chain(#{id => ChainId}) of {ok, _ChainInfo} -> - Results = lists:map(fun(AuthCfg = #{name := Name}) -> - case emqx_authn:create_authenticator( - ChainId, - AuthCfg) of - {ok, _AuthInfo} -> ok; - {error, Reason} -> {Name, Reason} - end - end, AuthCfgs), - NResults = [ E || E <- Results, E /= ok], - NResults /= [] andalso begin - logger:error("Failed to create authenticators: ~p", [NResults]), - throw({bad_autheticators, NResults}) - end, ChainId; + case emqx_authn:create_authenticator(ChainId, AuthCfg) of + {ok, _} -> ChainId; + {error, Reason} -> + logger:error("Failed to create authenticator ~p", [Reason]), + throw({bad_autheticator, Reason}) + end; {error, Reason} -> logger:error("Failed to create authentication chain: ~p", [Reason]), throw({bad_chain, {ChainId, Reason}}) diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 938da15ba..facbe9026 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -42,7 +42,7 @@ fields("gateway") -> fields(stomp_structs) -> [ {frame, t(ref(stomp_frame))} , {clientinfo_override, t(ref(clientinfo_override))} - , {authentication, t(ref(authentication))} + , {authenticator, t(authenticator(), undefined, undefined)} , {listener, t(ref(tcp_listener_group))} ]; @@ -60,7 +60,7 @@ fields(mqttsn_structs) -> , {idle_timeout, t(duration())} , {predefined, hoconsc:array(ref(mqttsn_predefined))} , {clientinfo_override, t(ref(clientinfo_override))} - , {authentication, t(ref(authentication))} + , {authenticator, t(authenticator(), undefined, undefined)} , {listener, t(ref(udp_listener_group))} ]; @@ -79,14 +79,14 @@ fields(lwm2m_structs) -> , {mountpoint, t(string())} , {update_msg_publish_condition, t(union([always, contains_object_list]))} , {translators, t(ref(translators))} - , {authentication, t(ref(authentication))} + , {authenticator, t(authenticator(), undefined, undefined)} , {listener, t(ref(udp_listener_group))} ]; fields(exproto_structs) -> [ {server, t(ref(exproto_grpc_server))} , {handler, t(ref(exproto_grpc_handler))} - , {authentication, t(ref(authentication))} + , {authenticator, t(authenticator(), undefined, undefined)} , {listener, t(ref(udp_tcp_listener_group))} ]; @@ -100,11 +100,6 @@ fields(exproto_grpc_handler) -> %% TODO: ssl ]; -fields(authentication) -> - [ {enable, #{type => boolean(), default => false}} - , {authenticators, fun emqx_authn_schema:authenticators/1} - ]; - fields(clientinfo_override) -> [ {username, t(string())} , {password, t(string())} @@ -207,7 +202,7 @@ fields(coap_structs) -> , {notify_type, t(union([non, con, qos]), undefined, qos)} , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {authentication, t(ref(authentication))} + , {authenticator, t(authenticator(), undefined, undefined)} , {listener, t(ref(udp_listener_group))} ]; @@ -215,6 +210,26 @@ fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). +authenticator() -> + hoconsc:union( + [ undefined + , hoconsc:ref(emqx_authn_mnesia, config) + , hoconsc:ref(emqx_authn_mysql, config) + , hoconsc:ref(emqx_authn_pgsql, config) + , hoconsc:ref(emqx_authn_mongodb, standalone) + , hoconsc:ref(emqx_authn_mongodb, 'replica-set') + , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') + , hoconsc:ref(emqx_authn_redis, standalone) + , hoconsc:ref(emqx_authn_redis, cluster) + , hoconsc:ref(emqx_authn_redis, sentinel) + , hoconsc:ref(emqx_authn_http, get) + , hoconsc:ref(emqx_authn_http, post) + , hoconsc:ref(emqx_authn_jwt, 'hmac-based') + , hoconsc:ref(emqx_authn_jwt, 'public-key') + , hoconsc:ref(emqx_authn_jwt, 'jwks') + , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) + ]). + %translations() -> []. % %translations(_) -> []. From f333a0b888d543b505abd25fb668eb64a330699b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 20 Aug 2021 16:59:53 +0800 Subject: [PATCH 172/306] chore(gw): rename functions --- apps/emqx_gateway/src/emqx_gateway_insta_sup.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index c32f10df6..ff49b5736 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -105,7 +105,7 @@ init([Gateway, Ctx0, _GwDscrptr]) -> end. do_init_context(GwName, RawConf, Ctx) -> - Auth = case maps:get(authenticators, RawConf, #{enable => false}) of + Auth = case maps:get(authenticator, RawConf, #{enable => false}) of #{enable := false} -> undefined; AuthCfg when is_map(AuthCfg) -> case maps:get(enable, AuthCfg, true) of @@ -120,7 +120,7 @@ do_init_context(GwName, RawConf, Ctx) -> Ctx#{auth => Auth}. do_deinit_context(Ctx) -> - cleanup_authenticators_for_gateway_insta(maps:get(auth, Ctx)), + cleanup_authenticator_for_gateway_insta(maps:get(auth, Ctx)), ok. handle_call(info, _From, State = #state{gw = Gateway}) -> @@ -240,9 +240,9 @@ create_authenticator_for_gateway_insta(GwName, AuthCfg) -> throw({bad_chain, {ChainId, Reason}}) end. -cleanup_authenticators_for_gateway_insta(undefined) -> +cleanup_authenticator_for_gateway_insta(undefined) -> ok; -cleanup_authenticators_for_gateway_insta(ChainId) -> +cleanup_authenticator_for_gateway_insta(ChainId) -> case emqx_authn:delete_chain(ChainId) of ok -> ok; {error, {not_found, _}} -> From 914c375d9e3b88202bc1f4b8e9516b5bf1f9d20a Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 23 Aug 2021 11:48:08 +0800 Subject: [PATCH 173/306] chore(gw): adjust the configuration format --- apps/emqx_gateway/etc/emqx_gateway.conf | 18 +++---- apps/emqx_gateway/include/emqx_gateway.hrl | 9 +++- .../src/coap/emqx_coap_channel.erl | 11 +++- apps/emqx_gateway/src/emqx_gateway.app.src | 2 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 52 +++++++++++++------ apps/emqx_gateway/src/emqx_gateway_utils.erl | 15 ++---- 6 files changed, 66 insertions(+), 41 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 41d1d10de..15aeb2e29 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -6,6 +6,7 @@ ## In the final version, it will be commented out. gateway.stomp { + frame { max_headers = 10 max_headers_length = 1024 @@ -18,14 +19,13 @@ gateway.stomp { } authenticator { - #enable = true name = "authenticator1" mechanism = password-based server_type = built-in-database user_id_type = clientid } - listener.tcp.1 { + listeners.tcp.default { bind = 61613 acceptors = 16 max_connections = 1024000 @@ -49,7 +49,7 @@ gateway.coap { notify_type = qos subscribe_qos = qos0 publish_qos = qos1 - listener.udp.1 { + listeners.udp.default { bind = 5683 } } @@ -90,7 +90,7 @@ gateway.mqttsn { password = "abc" } - listener.udp.1 { + listeners.udp.default { bind = 1884 max_connections = 10240000 max_conn_rate = 1000 @@ -113,16 +113,16 @@ gateway.exproto { #ssl.cacertfile: } - listener.tcp.1 { + listeners.tcp.default { bind = 7993 acceptors = 8 max_connections = 10240 max_conn_rate = 1000 } - #listener.ssl.1: {} - #listener.udp.1: {} - #listener.dtls.1: {} + #listeners.ssl.default: {} + #listeners.udp.default: {} + #listeners.dtls.default: {} } gateway.lwm2m { @@ -147,7 +147,7 @@ gateway.lwm2m { update = "up/resp" } - listener.udp.1 { + listeners.udp.default { bind = 5783 } } diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl index d959eac8b..baa7a1ce7 100644 --- a/apps/emqx_gateway/include/emqx_gateway.hrl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -19,8 +19,15 @@ -type gateway_name() :: atom(). +-type listener() :: #{}. + %% The RawConf got from emqx:get_config/1 --type rawconf() :: map(). +-type rawconf() :: + #{ clientinfo_override => map() + , authenticator => map() + , listeners => listener() + , atom() => any() + }. %% @doc The Gateway defination -type gateway() :: diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 760a832ae..ccf42343c 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -106,7 +106,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), Mountpoint = maps:get(mountpoint, Config, undefined), - EnableAuth = maps:get(enable, maps:get(authentication, Config)), + EnableAuth = is_authenticator_enabled(Config), ClientInfo = set_peercert_infos( Peercert, #{ zone => default @@ -134,6 +134,13 @@ init(ConnInfo = #{peername := {PeerHost, _}, , keepalive = emqx_keepalive:init(maps:get(heartbeat, Config)) }. +is_authenticator_enabled(Cfg) -> + case maps:get(authenticator, Cfg, #{enable => false}) of + AuthCfg when is_map(AuthCfg) -> + maps:get(enable, AuthCfg, true); + _ -> false + end. + validator(Type, Topic, #exec_ctx{ctx = Ctx, clientinfo = ClientInfo}) -> emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). @@ -290,7 +297,7 @@ handle_result(_, _, _, Channel) -> {ok, Channel}. check_auth_state(Msg, #channel{config = Cfg} = Channel) -> - #{authentication := #{enable := Enable}} = Cfg, + Enable = is_authenticator_enabled(Cfg), check_token(Enable, Msg, Channel). check_token(true, diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index 2fc329711..e25b767cc 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {emqx_gateway_app, []}}, - {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx]}, + {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx, emqx_authn]}, {env, []}, {modules, []}, {licenses, ["Apache 2.0"]}, diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index facbe9026..9a0e75a37 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -1,5 +1,23 @@ +%%-------------------------------------------------------------------- +%% 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_schema). +-behaviour(hocon_schema). + -dialyzer(no_return). -dialyzer(no_match). -dialyzer(no_contracts). @@ -8,17 +26,16 @@ -include_lib("typerefl/include/types.hrl"). +-type ip_port() :: tuple(). -type duration() :: integer(). -type bytesize() :: integer(). -type comma_separated_list() :: list(). --type ip_port() :: tuple(). +-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}). -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). +-typerefl_from_string({comma_separated_list/0, emqx_schema, + to_comma_separated_list}). -reflect_type([ duration/0 , bytesize/0 @@ -27,11 +44,15 @@ ]). -export([structs/0 , fields/1]). + -export([t/1, t/3, t/4, ref/1]). -structs() -> ["gateway"]. +%%-------------------------------------------------------------------- +%% Structs -fields("gateway") -> +structs() -> [gateway]. + +fields(gateway) -> [{stomp, t(ref(stomp_structs))}, {mqttsn, t(ref(mqttsn_structs))}, {coap, t(ref(coap_structs))}, @@ -43,7 +64,7 @@ fields(stomp_structs) -> [ {frame, t(ref(stomp_frame))} , {clientinfo_override, t(ref(clientinfo_override))} , {authenticator, t(authenticator(), undefined, undefined)} - , {listener, t(ref(tcp_listener_group))} + , {listeners, t(ref(tcp_listener_group))} ]; fields(stomp_frame) -> @@ -61,11 +82,10 @@ fields(mqttsn_structs) -> , {predefined, hoconsc:array(ref(mqttsn_predefined))} , {clientinfo_override, t(ref(clientinfo_override))} , {authenticator, t(authenticator(), undefined, undefined)} - , {listener, t(ref(udp_listener_group))} + , {listeners, t(ref(udp_listener_group))} ]; fields(mqttsn_predefined) -> - %% FIXME: How to check the $id is a integer ??? [ {id, t(integer())} , {topic, t(string())} ]; @@ -80,18 +100,18 @@ fields(lwm2m_structs) -> , {update_msg_publish_condition, t(union([always, contains_object_list]))} , {translators, t(ref(translators))} , {authenticator, t(authenticator(), undefined, undefined)} - , {listener, t(ref(udp_listener_group))} + , {listeners, t(ref(udp_listener_group))} ]; fields(exproto_structs) -> [ {server, t(ref(exproto_grpc_server))} , {handler, t(ref(exproto_grpc_handler))} , {authenticator, t(authenticator(), undefined, undefined)} - , {listener, t(ref(udp_tcp_listener_group))} + , {listeners, t(ref(udp_tcp_listener_group))} ]; fields(exproto_grpc_server) -> - [ {bind, t(integer())} + [ {bind, t(union(ip_port(), integer()))} %% TODO: ssl options ]; @@ -139,9 +159,7 @@ fields(dtls_listener) -> [ {"$name", t(ref(dtls_listener_settings))}]; fields(listener_settings) -> - % FIXME: - %[ {"bind", t(union(ip_port(), integer()))} - [ {bind, t(integer())} + [ {bind, t(union(ip_port(), integer()))} , {acceptors, t(integer(), undefined, 8)} , {max_connections, t(integer(), undefined, 1024)} , {max_conn_rate, t(integer())} @@ -203,7 +221,7 @@ fields(coap_structs) -> , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} , {authenticator, t(authenticator(), undefined, undefined)} - , {listener, t(ref(udp_listener_group))} + , {listeners, t(ref(udp_listener_group))} ]; fields(ExtraField) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 8a4d24691..d8c2b9be7 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -17,6 +17,8 @@ %% @doc Utils funcs for emqx-gateway -module(emqx_gateway_utils). +-include("emqx_gateway.hrl"). + -export([ childspec/2 , childspec/3 , childspec/4 @@ -105,15 +107,6 @@ format_listenon({Addr, Port}) when is_list(Addr) -> format_listenon({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). --type listener() :: #{}. - --type rawconf() :: - #{ clientinfo_override => #{} - , authenticators := list() - , listeners => listener() - , atom() => any() - }. - -spec normalize_rawconf(rawconf()) -> list({ Type :: udp | tcp | ssl | dtls , ListenOn :: esockd:listen_on() @@ -121,8 +114,8 @@ format_listenon({Addr, Port}) when is_tuple(Addr) -> , Cfg :: map() }). normalize_rawconf(RawConf) -> - LisMap = maps:get(listener, RawConf, #{}), - Cfg0 = maps:without([listener], RawConf), + LisMap = maps:get(listeners, RawConf, #{}), + Cfg0 = maps:without([listeners], RawConf), lists:append(maps:fold(fun(Type, Liss, AccIn1) -> Listeners = maps:fold(fun(_Name, Confs, AccIn2) -> From 836ca38c2b83101f4bffc22e8d3ec10f2e859428 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 24 Aug 2021 17:59:20 +0800 Subject: [PATCH 174/306] chore(gw): append the protoname into listener name --- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 36 +++++++++---------- apps/emqx_gateway/src/emqx_gateway_utils.erl | 5 +-- .../src/exproto/emqx_exproto_impl.erl | 36 +++++++++---------- .../src/lwm2m/emqx_lwm2m_impl.erl | 36 +++++++++---------- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 36 +++++++++---------- .../src/stomp/emqx_stomp_impl.erl | 36 +++++++++---------- 6 files changed, 93 insertions(+), 92 deletions(-) diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index b3f7b640a..cf181f07e 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -85,21 +85,21 @@ on_gateway_unload(_Gateway = #{ name := GwName, %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]), + ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, LisName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_coap_frame, @@ -114,21 +114,21 @@ do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). -name(GwName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type])). +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). -stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, LisName, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index d8c2b9be7..593729d67 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -109,6 +109,7 @@ format_listenon({Addr, Port}) when is_tuple(Addr) -> -spec normalize_rawconf(rawconf()) -> list({ Type :: udp | tcp | ssl | dtls + , Name :: atom() , ListenOn :: esockd:listen_on() , SocketOpts :: esockd:option() , Cfg :: map() @@ -118,14 +119,14 @@ normalize_rawconf(RawConf) -> Cfg0 = maps:without([listeners], RawConf), lists:append(maps:fold(fun(Type, Liss, AccIn1) -> Listeners = - maps:fold(fun(_Name, Confs, AccIn2) -> + 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] + [{Type, Name, ListenOn, SocketOpts, Cfg}|AccIn2] end, [], Liss), [Listeners|AccIn1] end, [], LisMap)). diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 8c500be82..c17a6e766 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -139,21 +139,21 @@ pool_name(GwName) -> %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]), + ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, LisName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_exproto_frame, @@ -172,8 +172,8 @@ do_start_listener(udp, Name, ListenOn, Opts, MFA) -> do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). -name(GwName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type])). +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> @@ -196,18 +196,18 @@ merge_default_by_type(Type, Options) when Type =:= udp; [{udp_options, Default} | Options] end. -stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, LisName, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index d5fb0d06e..d1afbc6b2 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -103,21 +103,21 @@ on_gateway_unload(_Gateway = #{ name := GwName, %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]), + ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, udp), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, LisName, udp), NCfg = Cfg#{ctx => Ctx}, NSocketOpts = merge_default(SocketOpts), Options = [{config, NCfg}|NSocketOpts], @@ -128,8 +128,8 @@ start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> lwm2m_coap_server:start_dtls(Name, ListenOn, Options) end. -name(GwName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type])). +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -141,20 +141,20 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, LisName, Type), case Type of udp -> lwm2m_coap_server:stop_udp(Name, ListenOn); diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index d35228e1f..9ea3ca3ae 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -104,21 +104,21 @@ on_gateway_unload(_Insta = #{ name := GwName, %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]), + ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, LisName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_sn_frame, @@ -127,8 +127,8 @@ start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type])). +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -140,18 +140,18 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, LisName, Type), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 19bfc16ab..d8b4bfcff 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -89,21 +89,21 @@ on_gateway_unload(_Gateway = #{ name := GwName, %% Internal funcs %%-------------------------------------------------------------------- -start_listener(GwName, Ctx, {Type, ListenOn, SocketOpts, Cfg}) -> +start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), - case start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) of + case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]), + ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]), + ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. -start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, Type), +start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> + Name = name(GwName, LisName, Type), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_stomp_frame, @@ -112,8 +112,8 @@ start_listener(GwName, Ctx, Type, ListenOn, SocketOpts, Cfg) -> esockd:open(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type])). +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), @@ -125,18 +125,18 @@ merge_default(Options) -> [{tcp_options, Default} | Options] end. -stop_listener(GwName, {Type, ListenOn, SocketOpts, Cfg}) -> - StopRet = stop_listener(GwName, Type, ListenOn, SocketOpts, Cfg), +stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> + StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s listener on ~s successfully.~n", - [GwName, Type, ListenOnStr]); + ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s listener on ~s: ~0p~n", - [GwName, Type, ListenOnStr, Reason]) + ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. -stop_listener(GwName, Type, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, Type), +stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> + Name = name(GwName, LisName, Type), esockd:close(Name, ListenOn). From e239fb07cdd9521bf76db6405009ed5c97d55db0 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 25 Aug 2021 10:40:52 +0800 Subject: [PATCH 175/306] chore(gw): add http-api for gateway summary lists --- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 9 ++- apps/emqx_gateway/src/emqx_gateway_intr.erl | 72 +++++++++++++++++++ .../src/exproto/emqx_exproto_impl.erl | 2 +- .../src/lwm2m/emqx_lwm2m_impl.erl | 2 +- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 2 +- .../src/stomp/emqx_stomp_impl.erl | 2 +- 7 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 apps/emqx_gateway/src/emqx_gateway_intr.erl diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index cf181f07e..f150f6f81 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -115,7 +115,7 @@ do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 9b77cc643..e617af65e 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -374,8 +374,13 @@ schema_for_gateway_stats() -> %%-------------------------------------------------------------------- %% http handlers -gateway(get, _Request) -> - {200, ok}. +gateway(get, Request) -> + Params = cowboy_req:parse_qs(Request), + Status = case proplists:get_value(<<"status">>, Params) of + undefined -> all; + S0 -> binary_to_existing_atom(S0, utf8) + end, + {200, emqx_gateway_intr:gateways(Status)}. gateway_insta(delete, _Request) -> {200, ok}; diff --git a/apps/emqx_gateway/src/emqx_gateway_intr.erl b/apps/emqx_gateway/src/emqx_gateway_intr.erl new file mode 100644 index 000000000..bafdb233e --- /dev/null +++ b/apps/emqx_gateway/src/emqx_gateway_intr.erl @@ -0,0 +1,72 @@ +%%-------------------------------------------------------------------- +%% 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 Gateway Interface Module for HTTP-APIs +-module(emqx_gateway_intr). + +-export([ gateways/1 + ]). + +-type gateway_summary() :: + #{ name := binary() + , status := running | stopped | unloaded + , started_at => binary() + , max_connection => integer() + , current_connect => integer() + , listeners => [] + }. + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +-spec gateways(Status :: all | running | stopped | unloaded) + -> [gateway_summary()]. +gateways(Status) -> + Gateways = lists:map(fun({GwName, _}) -> + case emqx_gateway:lookup(GwName) of + undefined -> #{name => GwName, status => unloaded}; + GwInfo = #{rawconf := RawConf} -> + GwInfo1 = maps:with( + [name, started_at, craeted_at, status], GwInfo), + GwInfo1#{listeners => get_listeners_status(GwName, RawConf)} + + end + end, emqx_gateway_registry:list()), + case Status of + all -> Gateways; + _ -> + [Gw || Gw = #{status := S} <- Gateways, S == Status] + end. + +%% @private +get_listeners_status(GwName, RawConf) -> + Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + lists:map(fun({Type, LisName, ListenOn, _, _}) -> + Name0 = listener_name(GwName, Type, LisName), + Name = {Name0, ListenOn}, + case catch esockd:listener(Name) of + _Pid when is_pid(_Pid) -> + #{Name0 => <<"activing">>}; + _ -> + #{Name0 => <<"inactived">>} + + end + end, Listeners). + +%% @private +listener_name(GwName, Type, LisName) -> + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index c17a6e766..a3b484fad 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -173,7 +173,7 @@ do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index d1afbc6b2..66a7bd9e2 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -129,7 +129,7 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> end. name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 9ea3ca3ae..0eeb1b952 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -128,7 +128,7 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> {emqx_gateway_conn, start_link, [NCfg]}). name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index d8b4bfcff..28dedac26 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -113,7 +113,7 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> {emqx_gateway_conn, start_link, [NCfg]}). name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", LisName, ":", Type])). + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), From ef372e415d78e04bb0a1f9244f20f282443d385c Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 25 Aug 2021 11:21:44 +0800 Subject: [PATCH 176/306] chore(gw): fix the time format --- .../src/emqx_gateway_insta_sup.erl | 11 +++++++--- apps/emqx_gateway/src/emqx_gateway_intr.erl | 21 +++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index ff49b5736..47f0104a9 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -45,7 +45,8 @@ child_pids :: [pid()], gw_state :: emqx_gateway_impl:state() | undefined, created_at :: integer(), - started_at :: integer() | undefined + started_at :: integer() | undefined, + stopped_at :: integer() | undefined }). %%-------------------------------------------------------------------- @@ -126,7 +127,8 @@ do_deinit_context(Ctx) -> handle_call(info, _From, State = #state{gw = Gateway}) -> GwInfo = Gateway#{status => State#state.status, created_at => State#state.created_at, - started_at => State#state.started_at + started_at => State#state.started_at, + stopped_at => State#state.stopped_at }, {reply, GwInfo, State}; @@ -259,8 +261,10 @@ cb_gateway_unload(State = #state{gw = Gateway = #{name := GwName}, #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), CbMod:on_gateway_unload(Gateway, GwState), {ok, State#state{child_pids = [], + status = stopped, gw_state = undefined, - status = stopped}} + started_at = undefined, + stopped_at = erlang:system_time(millisecond)}} catch Class : Reason : Stk -> logger:error("Failed to unload gateway (~0p, ~0p) crashed: " @@ -282,6 +286,7 @@ cb_gateway_load(State = #state{gw = Gateway = #{name := GwName}, status = running, child_pids = ChildPids, gw_state = GwState, + stopped_at = undefined, started_at = erlang:system_time(millisecond) }} end diff --git a/apps/emqx_gateway/src/emqx_gateway_intr.erl b/apps/emqx_gateway/src/emqx_gateway_intr.erl index bafdb233e..b2a8b0484 100644 --- a/apps/emqx_gateway/src/emqx_gateway_intr.erl +++ b/apps/emqx_gateway/src/emqx_gateway_intr.erl @@ -40,8 +40,14 @@ gateways(Status) -> case emqx_gateway:lookup(GwName) of undefined -> #{name => GwName, status => unloaded}; GwInfo = #{rawconf := RawConf} -> - GwInfo1 = maps:with( - [name, started_at, craeted_at, status], GwInfo), + GwInfo0 = unix_ts_to_rfc3339( + [created_at, started_at, stopped_at], + GwInfo), + GwInfo1 = maps:with([name, + status, + created_at, + started_at, + stopped_at], GwInfo0), GwInfo1#{listeners => get_listeners_status(GwName, RawConf)} end @@ -70,3 +76,14 @@ get_listeners_status(GwName, RawConf) -> %% @private listener_name(GwName, Type, LisName) -> list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + +%% @private +unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) -> + lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys); +unix_ts_to_rfc3339(Key, Map) -> + case maps:get(Key, Map, undefined) of + undefined -> Map; + Ts -> + Map#{Key => + emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} + end. From 9c855ba8c26f41f002740b9675fd82da825a722d Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 25 Aug 2021 11:43:33 +0800 Subject: [PATCH 177/306] chore(gw): cleanup the massive schema defination --- apps/emqx_gateway/etc/emqx_gateway.conf | 130 +++++--- .../src/coap/emqx_coap_channel.erl | 8 +- apps/emqx_gateway/src/emqx_gateway.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 290 ++++++++---------- .../src/emqx_gateway_insta_sup.erl | 16 +- apps/emqx_gateway/src/emqx_gateway_intr.erl | 13 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 78 +++-- apps/emqx_gateway/src/emqx_gateway_sup.erl | 5 +- apps/emqx_gateway/src/emqx_gateway_utils.erl | 11 + .../src/lwm2m/emqx_lwm2m_xml_object_db.erl | 9 +- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 2 +- apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 38 ++- .../test/emqx_sn_protocol_SUITE.erl | 39 ++- apps/emqx_gateway/test/emqx_stomp_SUITE.erl | 18 +- 14 files changed, 338 insertions(+), 321 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 15aeb2e29..206c54b93 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -7,6 +7,17 @@ gateway.stomp { + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + frame { max_headers = 10 max_headers_length = 1024 @@ -18,7 +29,7 @@ gateway.stomp { password = "${Packet.headers.passcode}" } - authenticator { + authentication { name = "authenticator1" mechanism = password-based server_type = built-in-database @@ -36,42 +47,57 @@ gateway.stomp { gateway.coap { - enable_stats = false + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s - authenticator { + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + + heartbeat = 30s + notify_type = qos + subscribe_qos = qos0 + publish_qos = qos1 + + authentication { name = "authenticator1" mechanism = password-based server_type = built-in-database user_id_type = clientid } - heartbeat = 30s - notify_type = qos - subscribe_qos = qos0 - publish_qos = qos1 listeners.udp.default { bind = 5683 } } gateway.mqttsn { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + ## 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. @@ -97,7 +123,55 @@ gateway.mqttsn { } } +gateway.lwm2m { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "lwm2m/%e/" + + xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" + + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + + ## always | contains_object_list + update_msg_publish_condition = contains_object_list + + translators { + command = "dn/#" + response = "up/resp" + notify = "up/notify" + register = "up/resp" + update = "up/resp" + } + + listeners.udp.default { + bind = 5783 + } +} + gateway.exproto { + + ## How long time the connection will be disconnected if the + ## connection is established but no bytes received + idle_timeout = 30s + + ## To control whether write statistics data into ETS table + ## for dashbord to read. + enable_stats = true + + ## When publishing or subscribing, prefix all topics with a mountpoint string. + mountpoint = "" + ## The gRPC server to accept requests server { bind = 9100 @@ -119,35 +193,7 @@ gateway.exproto { max_connections = 10240 max_conn_rate = 1000 } - #listeners.ssl.default: {} #listeners.udp.default: {} #listeners.dtls.default: {} } - -gateway.lwm2m { - - xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" - - lifetime_min = 1s - lifetime_max = 86400s - qmode_time_windonw = 22 - auto_observe = false - - mountpoint = "lwm2m/%e/" - - ## always | contains_object_list - update_msg_publish_condition = contains_object_list - - translators { - command = "dn/#" - response = "up/resp" - notify = "up/notify" - register = "up/resp" - update = "up/resp" - } - - listeners.udp.default { - bind = 5783 - } -} diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index ccf42343c..510432441 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -106,7 +106,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), Mountpoint = maps:get(mountpoint, Config, undefined), - EnableAuth = is_authenticator_enabled(Config), + EnableAuth = is_authentication_enabled(Config), ClientInfo = set_peercert_infos( Peercert, #{ zone => default @@ -134,8 +134,8 @@ init(ConnInfo = #{peername := {PeerHost, _}, , keepalive = emqx_keepalive:init(maps:get(heartbeat, Config)) }. -is_authenticator_enabled(Cfg) -> - case maps:get(authenticator, Cfg, #{enable => false}) of +is_authentication_enabled(Cfg) -> + case maps:get(authentication, Cfg, #{enable => false}) of AuthCfg when is_map(AuthCfg) -> maps:get(enable, AuthCfg, true); _ -> false @@ -297,7 +297,7 @@ handle_result(_, _, _, Channel) -> {ok, Channel}. check_auth_state(Msg, #channel{config = Cfg} = Channel) -> - Enable = is_authenticator_enabled(Cfg), + Enable = is_authentication_enabled(Cfg), check_token(Enable, Msg, Channel). check_token(true, diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index d2ab66362..4f80bfe3b 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -51,7 +51,7 @@ load(Name, RawConf) -> }, emqx_gateway_sup:load_gateway(Gateway). --spec unload(gateway_name()) -> ok | {error, any()}. +-spec unload(gateway_name()) -> ok | {error, not_found}. unload(Name) -> emqx_gateway_sup:unload_gateway(Name). diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index e617af65e..28cb45b47 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -20,6 +20,9 @@ -compile(nowarn_unused_function). +-import(emqx_mgmt_util, [ schema/1 + ]). + %% minirest behaviour callbacks -export([api_spec/0]). @@ -41,21 +44,109 @@ } ]). --define(EXAMPLE_STOMP_GATEWAY_CONF, #{ - frame => #{ - max_headers => 10, - max_headers_length => 1024, - max_body_length => 8192 - }, - listener => #{ - tcp => #{<<"default-stomp-listener">> => #{ - bind => <<"61613">> - }} - } - }). +%% XXX: This is whole confs for all type gateways. It is used to fill the +%% default configurations and generate the swagger-schema +%% +%% NOTE: It is a temporary measure to generate swagger-schema +-define(COAP_GATEWAY_CONFS, +#{<<"authentication">> => + #{<<"mechanism">> => <<"password-based">>, + <<"name">> => <<"authenticator1">>, + <<"server_type">> => <<"built-in-database">>, + <<"user_id_type">> => <<"clientid">>}, + <<"enable">> => true, + <<"enable_stats">> => true,<<"heartbeat">> => <<"30s">>, + <<"idle_timeout">> => <<"30s">>, + <<"listeners">> => + #{<<"udp">> => #{<<"default">> => #{<<"bind">> => 5683}}}, + <<"mountpoint">> => <<>>,<<"notify_type">> => <<"qos">>, + <<"publish_qos">> => <<"qos1">>, + <<"subscribe_qos">> => <<"qos0">>} +). --define(EXAMPLE_MQTTSN_GATEWAY_CONF, #{ - }). +-define(EXPROTO_GATEWAY_CONFS, +#{<<"enable">> => true, + <<"enable_stats">> => true, + <<"handler">> => + #{<<"address">> => <<"http://127.0.0.1:9001">>}, + <<"idle_timeout">> => <<"30s">>, + <<"listeners">> => + #{<<"tcp">> => + #{<<"default">> => + #{<<"acceptors">> => 8,<<"bind">> => 7993, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}}}, + <<"mountpoint">> => <<>>, + <<"server">> => #{<<"bind">> => 9100}} +). + +-define(LWM2M_GATEWAY_CONFS, +#{<<"auto_observe">> => false, + <<"enable">> => true, + <<"enable_stats">> => true, + <<"idle_timeout">> => <<"30s">>, + <<"lifetime_max">> => <<"86400s">>, + <<"lifetime_min">> => <<"1s">>, + <<"listeners">> => + #{<<"udp">> => #{<<"default">> => #{<<"bind">> => 5783}}}, + <<"mountpoint">> => <<"lwm2m/%e/">>, + <<"qmode_time_windonw">> => 22, + <<"translators">> => + #{<<"command">> => <<"dn/#">>,<<"notify">> => <<"up/notify">>, + <<"register">> => <<"up/resp">>, + <<"response">> => <<"up/resp">>, + <<"update">> => <<"up/resp">>}, + <<"update_msg_publish_condition">> => + <<"contains_object_list">>, + <<"xml_dir">> => <<"etc/lwm2m_xml">>} +). + +-define(MQTTSN_GATEWAY_CONFS, +#{<<"broadcast">> => true, + <<"clientinfo_override">> => + #{<<"password">> => <<"abc">>, + <<"username">> => <<"mqtt_sn_user">>}, + <<"enable">> => true, + <<"enable_qos3">> => true,<<"enable_stats">> => true, + <<"gateway_id">> => 1,<<"idle_timeout">> => <<"30s">>, + <<"listeners">> => + #{<<"udp">> => + #{<<"default">> => + #{<<"bind">> => 1884,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240000}}}, + <<"mountpoint">> => <<>>, + <<"predefined">> => + [#{<<"id">> => 1, + <<"topic">> => <<"/predefined/topic/name/hello">>}, + #{<<"id">> => 2, + <<"topic">> => <<"/predefined/topic/name/nice">>}]} +). + +-define(STOMP_GATEWAY_CONFS, +#{<<"authentication">> => + #{<<"mechanism">> => <<"password-based">>, + <<"name">> => <<"authenticator1">>, + <<"server_type">> => <<"built-in-database">>, + <<"user_id_type">> => <<"clientid">>}, + <<"clientinfo_override">> => + #{<<"password">> => <<"${Packet.headers.passcode}">>, + <<"username">> => <<"${Packet.headers.login}">>}, + <<"enable">> => true, + <<"enable_stats">> => true, + <<"frame">> => + #{<<"max_body_length">> => 8192,<<"max_headers">> => 10, + <<"max_headers_length">> => 1024}, + <<"idle_timeout">> => <<"30s">>, + <<"listeners">> => + #{<<"tcp">> => + #{<<"default">> => + #{<<"acceptors">> => 16,<<"active_n">> => 100, + <<"bind">> => 61613,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 1024000}}}, + <<"mountpoint">> => <<>>} +). + +%% --- END -define(EXAMPLE_GATEWAY_STATS, #{ max_connection => 10240000, @@ -121,7 +212,7 @@ metadata(gateway_insta) -> summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"gateway xxx not found">> + message => <<"The gateway not found">> } } } @@ -140,46 +231,13 @@ metadata(gateway_insta) -> parameters => [UriNameParamDef], responses => #{ <<"404">> => NameNotFoundRespDef, - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"gateway_conf">>), - examples => #{ - simple1 => #{ - summary => <<"Stomp Gateway">>, - value => emqx_json:encode(?EXAMPLE_STOMP_GATEWAY_CONF) - }, - simple2 => #{ - summary => <<"MQTT-SN Gateway">>, - value => emqx_json:encode(?EXAMPLE_MQTTSN_GATEWAY_CONF) - } - } - } - } - } + <<"200">> => schema(schema_for_gateway_conf()) } }, put => #{ description => <<"Update the gateway configurations/status">>, parameters => [UriNameParamDef], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"gateway_conf">>), - examples => #{ - simple1 => #{ - summary => <<"Stom Gateway">>, - value => emqx_json:encode(?EXAMPLE_STOMP_GATEWAY_CONF) - }, - simple2 => #{ - summary => <<"MQTT-SN Gateway">>, - value => emqx_json:encode(?EXAMPLE_MQTTSN_GATEWAY_CONF) - } - } - } - } - }, + requestBody => schema(schema_for_gateway_conf()), responses => #{ <<"404">> => NameNotFoundRespDef, <<"204">> => #{description => <<"Created">>} @@ -210,7 +268,6 @@ metadata(gateway_insta_stats) -> schemas() -> [ #{<<"gateway_overrview">> => schema_for_gateway_overrview()} - , #{<<"gateway_conf">> => schema_for_gateway_conf()} , #{<<"gateway_stats">> => schema_for_gateway_stats()} ]. @@ -262,109 +319,13 @@ schema_for_gateway_overrview() -> schema_for_gateway_conf() -> #{oneOf => - [ schema_for_gateway_conf_stomp() - , schema_for_gateway_conf_mqttsn() - , schema_for_gateway_conf_coap() - , schema_for_gateway_conf_lwm2m() - , schema_for_gateway_conf_exproto() + [ emqx_mgmt_api_configs:gen_schema(?STOMP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?MQTTSN_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?COAP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?LWM2M_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?EXPROTO_GATEWAY_CONFS) ]}. -schema_for_clientinfo_override() -> - #{type => object, - properties => #{ - clientid => #{type => string}, - username => #{type => string}, - password => #{type => string} - }}. - -schema_for_authenticator() -> - %% TODO. - #{type => object, properties => #{ - a_key => #{type => string} - }}. - -schema_for_tcp_listener() -> - %% TODO. - #{type => object, properties => #{ - a_key => #{type => string} - }}. - -schema_for_udp_listener() -> - %% TODO. - #{type => object, properties => #{ - a_key => #{type => string} - }}. - -%% It should be generated by _schema.erl module -%% and emqx_gateway.conf -schema_for_gateway_conf_stomp() -> - #{type => object, - properties => #{ - frame => #{ - type => object, - properties => #{ - max_headers => #{type => integer}, - max_headers_length => #{type => integer}, - max_body_length => #{type => integer} - } - }, - clientinfo_override => schema_for_clientinfo_override(), - authenticator => schema_for_authenticator(), - listener => schema_for_tcp_listener() - } - }. - -schema_for_gateway_conf_mqttsn() -> - #{type => object, - properties => #{ - gateway_id => #{type => integer}, - broadcast => #{type => boolean}, - enable_stats => #{type => boolean}, - enable_qos3 => #{type => boolean}, - idle_timeout => #{type => integer}, - predefined => #{ - type => array, - items => #{ - type => object, - properties => #{ - id => #{type => integer}, - topic => #{type => string} - } - } - }, - clientinfo_override => schema_for_clientinfo_override(), - authenticator => schema_for_authenticator(), - listener => schema_for_udp_listener() - }}. - - -schema_for_gateway_conf_coap() -> - #{type => object, - properties => #{ - clientinfo_override => schema_for_clientinfo_override(), - authenticator => schema_for_authenticator(), - listener => schema_for_udp_listener() - }}. - -schema_for_gateway_conf_lwm2m() -> - #{type => object, - properties => #{ - clientinfo_override => schema_for_clientinfo_override(), - authenticator => schema_for_authenticator(), - listener => schema_for_udp_listener() - }}. - -schema_for_gateway_conf_exproto() -> - #{type => object, - properties => #{ - clientinfo_override => schema_for_clientinfo_override(), - authenticator => schema_for_authenticator(), - listener => #{oneOf => [schema_for_tcp_listener(), - schema_for_udp_listener() - ] - } - }}. - schema_for_gateway_stats() -> #{type => object, properties => #{ @@ -382,13 +343,26 @@ gateway(get, Request) -> end, {200, emqx_gateway_intr:gateways(Status)}. -gateway_insta(delete, _Request) -> - {200, ok}; -gateway_insta(get, _Request) -> - {200, ok}; -gateway_insta(put, _Request) -> +gateway_insta(delete, Request) -> + Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), + case emqx_gateway:unload(Name) of + ok -> + {200, ok}; + {error, not_found} -> + {404, <<"Not Found">>}; + {error, Reason} -> + {500, Reason} + end; +gateway_insta(get, Request) -> + Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), + case emqx_gateway:lookup(Name) of + #{rawconf := RawConf} -> + {200, RawConf}; + undefined -> + {404, <<"Not Found">>} + end; +gateway_insta(post, _Request) -> {200, ok}. gateway_insta_stats(get, _Req) -> {401, <<"Implement it later (maybe 5.1)">>}. - diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 47f0104a9..0abcb6426 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -106,14 +106,14 @@ init([Gateway, Ctx0, _GwDscrptr]) -> end. do_init_context(GwName, RawConf, Ctx) -> - Auth = case maps:get(authenticator, RawConf, #{enable => false}) of + Auth = case maps:get(authentication, RawConf, #{enable => false}) of #{enable := false} -> undefined; AuthCfg when is_map(AuthCfg) -> case maps:get(enable, AuthCfg, true) of false -> undefined; _ -> - create_authenticator_for_gateway_insta(GwName, AuthCfg) + create_authentication_for_gateway_insta(GwName, AuthCfg) end; _ -> undefined @@ -121,7 +121,7 @@ do_init_context(GwName, RawConf, Ctx) -> Ctx#{auth => Auth}. do_deinit_context(Ctx) -> - cleanup_authenticator_for_gateway_insta(maps:get(auth, Ctx)), + cleanup_authentication_for_gateway_insta(maps:get(auth, Ctx)), ok. handle_call(info, _From, State = #state{gw = Gateway}) -> @@ -227,24 +227,24 @@ code_change(_OldVsn, State, _Extra) -> %% Internal funcs %%-------------------------------------------------------------------- -create_authenticator_for_gateway_insta(GwName, AuthCfg) -> +create_authentication_for_gateway_insta(GwName, AuthCfg) -> ChainId = atom_to_binary(GwName, utf8), case emqx_authn:create_chain(#{id => ChainId}) of {ok, _ChainInfo} -> case emqx_authn:create_authenticator(ChainId, AuthCfg) of {ok, _} -> ChainId; {error, Reason} -> - logger:error("Failed to create authenticator ~p", [Reason]), - throw({bad_autheticator, Reason}) + logger:error("Failed to create authentication ~p", [Reason]), + throw({bad_authentication, Reason}) end; {error, Reason} -> logger:error("Failed to create authentication chain: ~p", [Reason]), throw({bad_chain, {ChainId, Reason}}) end. -cleanup_authenticator_for_gateway_insta(undefined) -> +cleanup_authentication_for_gateway_insta(undefined) -> ok; -cleanup_authenticator_for_gateway_insta(ChainId) -> +cleanup_authentication_for_gateway_insta(ChainId) -> case emqx_authn:delete_chain(ChainId) of ok -> ok; {error, {not_found, _}} -> diff --git a/apps/emqx_gateway/src/emqx_gateway_intr.erl b/apps/emqx_gateway/src/emqx_gateway_intr.erl index b2a8b0484..2a0c15646 100644 --- a/apps/emqx_gateway/src/emqx_gateway_intr.erl +++ b/apps/emqx_gateway/src/emqx_gateway_intr.erl @@ -40,7 +40,7 @@ gateways(Status) -> case emqx_gateway:lookup(GwName) of undefined -> #{name => GwName, status => unloaded}; GwInfo = #{rawconf := RawConf} -> - GwInfo0 = unix_ts_to_rfc3339( + GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339( [created_at, started_at, stopped_at], GwInfo), GwInfo1 = maps:with([name, @@ -76,14 +76,3 @@ get_listeners_status(GwName, RawConf) -> %% @private listener_name(GwName, Type, LisName) -> list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - -%% @private -unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) -> - lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys); -unix_ts_to_rfc3339(Key, Map) -> - case maps:get(Key, Map, undefined) of - undefined -> Map; - Ts -> - Map#{Key => - emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} - end. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 9a0e75a37..0abeb1354 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -62,10 +62,8 @@ fields(gateway) -> fields(stomp_structs) -> [ {frame, t(ref(stomp_frame))} - , {clientinfo_override, t(ref(clientinfo_override))} - , {authenticator, t(authenticator(), undefined, undefined)} , {listeners, t(ref(tcp_listener_group))} - ]; + ] ++ gateway_common_options(); fields(stomp_frame) -> [ {max_headers, t(integer(), undefined, 10)} @@ -76,39 +74,40 @@ fields(stomp_frame) -> 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))} - , {authenticator, t(authenticator(), undefined, undefined)} , {listeners, t(ref(udp_listener_group))} - ]; + ] ++ gateway_common_options(); fields(mqttsn_predefined) -> [ {id, t(integer())} - , {topic, t(string())} + , {topic, t(binary())} ]; +fields(coap_structs) -> + [ {heartbeat, t(duration(), undefined, "30s")} + , {notify_type, t(union([non, con, qos]), undefined, qos)} + , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {listeners, t(ref(udp_listener_group))} + ] ++ gateway_common_options(); + fields(lwm2m_structs) -> - [ {xml_dir, t(string())} + [ {xml_dir, t(binary())} , {lifetime_min, t(duration())} , {lifetime_max, t(duration())} , {qmode_time_windonw, t(integer())} , {auto_observe, t(boolean())} - , {mountpoint, t(string())} , {update_msg_publish_condition, t(union([always, contains_object_list]))} , {translators, t(ref(translators))} - , {authenticator, t(authenticator(), undefined, undefined)} , {listeners, t(ref(udp_listener_group))} - ]; + ] ++ gateway_common_options(); fields(exproto_structs) -> [ {server, t(ref(exproto_grpc_server))} , {handler, t(ref(exproto_grpc_handler))} - , {authenticator, t(authenticator(), undefined, undefined)} , {listeners, t(ref(udp_tcp_listener_group))} - ]; + ] ++ gateway_common_options(); fields(exproto_grpc_server) -> [ {bind, t(union(ip_port(), integer()))} @@ -116,18 +115,18 @@ fields(exproto_grpc_server) -> ]; fields(exproto_grpc_handler) -> - [ {address, t(string())} + [ {address, t(binary())} %% TODO: ssl ]; fields(clientinfo_override) -> - [ {username, t(string())} - , {password, t(string())} - , {clientid, t(string())} + [ {username, t(binary())} + , {password, t(binary())} + , {clientid, t(binary())} ]; fields(translators) -> - [{"$name", t(string())}]; + [{"$name", t(binary())}]; fields(udp_listener_group) -> [ {udp, t(ref(udp_listener))} @@ -164,7 +163,6 @@ fields(listener_settings) -> , {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(boolean())} @@ -208,27 +206,14 @@ fields(dtls_listener_settings) -> , reuse_sessions => true}) ++ fields(listener_settings); fields(access) -> - [ {"$id", #{type => string(), + [ {"$id", #{type => binary(), nullable => true}}]; -fields(coap) -> - [{"$id", t(ref(coap_structs))}]; - -fields(coap_structs) -> - [ {enable_stats, t(boolean(), undefined, true)} - , {heartbeat, t(duration(), undefined, "30s")} - , {notify_type, t(union([non, con, qos]), undefined, qos)} - , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {authenticator, t(authenticator(), undefined, undefined)} - , {listeners, t(ref(udp_listener_group))} - ]; - fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). -authenticator() -> +authentication() -> hoconsc:union( [ undefined , hoconsc:ref(emqx_authn_mnesia, config) @@ -252,6 +237,15 @@ authenticator() -> % %translations(_) -> []. +gateway_common_options() -> + [ {enable, t(boolean(), undefined, true)} + , {enable_stats, t(boolean(), undefined, true)} + , {idle_timeout, t(duration(), undefined, "30s")} + , {mountpoint, t(binary())} + , {clientinfo_override, t(ref(clientinfo_override))} + , {authentication, t(authentication(), undefined, undefined)} + ]. + %%-------------------------------------------------------------------- %% Helpers @@ -289,9 +283,9 @@ ssl(Mapping, Defaults) -> end end, D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, [ {"enable", t(boolean(), 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"))} + , {"cacertfile", t(binary(), M("cacertfile"), D("cacertfile"))} + , {"certfile", t(binary(), M("certfile"), D("certfile"))} + , {"keyfile", t(binary(), 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(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} @@ -299,12 +293,12 @@ ssl(Mapping, Defaults) -> , {"honor_cipher_order", t(boolean(), 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"), + , {"password", hoconsc:t(binary(), #{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"), + , {"dhfile", t(binary(), M("dhfile"), D("dhfile"))} + , {"server_name_indication", t(union(disable, binary()), 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"))} diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index 87e41d93b..57ac7e7c7 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -52,7 +52,10 @@ load_gateway(Gateway = #{name := GwName}) -> emqx_gateway_gw_sup:create_insta(GwSup, Gateway, GwDscrptr) end. --spec unload_gateway(gateway_name()) -> ok | {error, not_found}. +-spec unload_gateway(gateway_name()) + -> ok + | {error, not_found} + | {error, any()}. unload_gateway(GwName) -> case lists:keyfind(GwName, 1, supervisor:which_children(?MODULE)) of false -> {error, not_found}; diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 593729d67..a4978ee8b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -28,6 +28,7 @@ -export([ apply/2 , format_listenon/1 + , unix_ts_to_rfc3339/2 ]). -export([ normalize_rawconf/1 @@ -107,6 +108,16 @@ format_listenon({Addr, Port}) when is_list(Addr) -> format_listenon({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). +unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) -> + lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys); +unix_ts_to_rfc3339(Key, Map) -> + case maps:get(Key, Map, undefined) of + undefined -> Map; + Ts -> + Map#{Key => + emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} + end. + -spec normalize_rawconf(rawconf()) -> list({ Type :: udp | tcp | ssl | dtls , Name :: atom() diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl index ea5f878d3..1d7fb6d5e 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl @@ -49,6 +49,7 @@ %% API Function Definitions %% ------------------------------------------------------------------ +-spec start_link(binary() | string()) -> {ok, pid()} | ignore | {error, any()}. start_link(XmlDir) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []). @@ -85,10 +86,10 @@ stop() -> %% gen_server Function Definitions %% ------------------------------------------------------------------ -init([XmlDir]) -> +init([XmlDir0]) -> _ = ets:new(?LWM2M_OBJECT_DEF_TAB, [set, named_table, protected]), _ = ets:new(?LWM2M_OBJECT_NAME_TO_ID_TAB, [set, named_table, protected]), - load(XmlDir), + load(to_list(XmlDir0)), {ok, #state{}}. handle_call(_Request, _From, State) -> @@ -140,3 +141,7 @@ load_xml(FileName) -> [ObjectXml] = xmerl_xpath:string("/LWM2M/Object", Xml), ObjectXml. +to_list(B) when is_binary(B) -> + binary_to_list(B); +to_list(S) when is_list(S) -> + S. diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 0b241a5c8..4902aacf5 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -70,7 +70,7 @@ set_special_cfg(emqx_gateway) -> #{authentication => #{enable => false}, server => #{bind => 9100}, handler => #{address => "http://127.0.0.1:9001"}, - listener => listener_confs(LisType) + listeners => listener_confs(LisType) }); set_special_cfg(_App) -> ok. diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index 2e3e5a040..79664928d 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -29,26 +29,24 @@ -include_lib("common_test/include/ct.hrl"). -define(CONF_DEFAULT, <<" -gateway: { - lwm2m: { - xml_dir: \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" - lifetime_min: 1s - lifetime_max: 86400s - qmode_time_windonw: 22 - auto_observe: false - mountpoint: \"lwm2m/%e/\" - update_msg_publish_condition: contains_object_list - translators: { - command: \"dn/#\" - response: \"up/resp\" - notify: \"up/notify\" - register: \"up/resp\" - update: \"up/resp\" - } - listener.udp.1 { - bind: 5783 - } - } +gateway.lwm2m { + xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + mountpoint = \"lwm2m/%e/\" + update_msg_publish_condition = contains_object_list + translators { + command = \"dn/#\" + response = \"up/resp\" + notify = \"up/notify\" + register = \"up/resp\" + update = \"up/resp\" + } + listeners.udp.default { + bind = 5783 + } } ">>). diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 2b8f62f58..2fbd031ff 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -52,26 +52,25 @@ integer_to_list(erlang:system_time())])). -define(CONF_DEFAULT, <<" -gateway: { - mqttsn: { - gateway_id: 1 - broadcast: true - enable_stats: true - enable_qos3: true - predefined: [ - {id: 1, topic: \"/predefined/topic/name/hello\"}, - {id: 2, topic: \"/predefined/topic/name/nice\"} - ] - clientinfo_override: { - username: \"user1\" - password: \"pw123\" - } - listener.udp.1: { - bind: 1884 - max_connections: 10240000 - max_conn_rate: 1000 - } +gateway.mqttsn { + gateway_id = 1 + broadcast = true + enable_qos3 = true + predefined = [ + { id = 1, + topic = \"/predefined/topic/name/hello\" + }, + { id = 2, + topic = \"/predefined/topic/name/nice\" } + ] + clientinfo_override { + username = \"user1\" + password = \"pw123\" + } + listeners.udp.default { + bind = 1884 + } } ">>). @@ -98,7 +97,7 @@ end_per_suite(_) -> %% Connect t_connect(_) -> - SockName = {'mqttsn:udp', 1884}, + SockName = {'mqttsn:udp:default', 1884}, ?assertEqual(true, lists:keymember(SockName, 1, esockd:listeners())), {ok, Socket} = gen_udp:open(0, [binary]), diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index 328e9fa79..75f6dadc3 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -24,16 +24,14 @@ -define(HEARTBEAT, <<$\n>>). -define(CONF_DEFAULT, <<" -gateway: { - stomp: { - clientinfo_override: { - username: \"${Packet.headers.login}\" - password: \"${Packet.headers.passcode}\" - } - listener.tcp.1: { - bind: 61613 - } - } +gateway.stomp { + clientinfo_override { + username = \"${Packet.headers.login}\" + password = \"${Packet.headers.passcode}\" + } + listeners.tcp.default { + bind = 61613 + } } ">>). From 86e28d5abbf3e02d73c46304434db795f81ed1d3 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 25 Aug 2021 16:58:56 +0800 Subject: [PATCH 178/306] chore(gw): rename rawconf to config --- apps/emqx_gateway/include/emqx_gateway.hrl | 10 +-------- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 8 +++---- apps/emqx_gateway/src/emqx_gateway.erl | 21 +++++++++++++------ apps/emqx_gateway/src/emqx_gateway_api.erl | 5 +++-- .../src/emqx_gateway_insta_sup.erl | 8 +++---- apps/emqx_gateway/src/emqx_gateway_intr.erl | 8 +++---- apps/emqx_gateway/src/emqx_gateway_schema.erl | 3 ++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 6 +++--- .../src/exproto/emqx_exproto_impl.erl | 18 ++++++++-------- .../src/lwm2m/emqx_lwm2m_impl.erl | 10 ++++----- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 18 ++++++++-------- .../src/stomp/emqx_stomp_impl.erl | 10 ++++----- 12 files changed, 64 insertions(+), 61 deletions(-) diff --git a/apps/emqx_gateway/include/emqx_gateway.hrl b/apps/emqx_gateway/include/emqx_gateway.hrl index baa7a1ce7..5c0893cb2 100644 --- a/apps/emqx_gateway/include/emqx_gateway.hrl +++ b/apps/emqx_gateway/include/emqx_gateway.hrl @@ -21,14 +21,6 @@ -type listener() :: #{}. -%% The RawConf got from emqx:get_config/1 --type rawconf() :: - #{ clientinfo_override => map() - , authenticator => map() - , listeners => listener() - , atom() => any() - }. - %% @doc The Gateway defination -type gateway() :: #{ name := gateway_name() @@ -40,7 +32,7 @@ %% Timestamp in millisecond , started_at => integer() %% Appears only in getting gateway info - , rawconf => rawconf() + , config => emqx_config:config() }. -endif. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index f150f6f81..aacb98546 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -49,9 +49,9 @@ unreg() -> %%-------------------------------------------------------------------- on_gateway_load(_Gateway = #{name := GwName, - rawconf := RawConf + config := Config }, Ctx) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), ListenerPids = lists:map(fun(Lis) -> start_listener(GwName, Ctx, Lis) end, Listeners), @@ -74,9 +74,9 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> end. on_gateway_unload(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) end, Listeners). diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 4f80bfe3b..213208f19 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -29,6 +29,8 @@ , list/0 ]). +-export([update_rawconf/2]). + -spec registered_gateway() -> [{gateway_name(), emqx_gateway_registry:descriptor()}]. registered_gateway() -> @@ -41,13 +43,13 @@ registered_gateway() -> list() -> emqx_gateway_sup:list_gateway_insta(). --spec load(gateway_name(), rawconf()) +-spec load(gateway_name(), emqx_config:config()) -> {ok, pid()} | {error, any()}. -load(Name, RawConf) -> +load(Name, Config) -> Gateway = #{ name => Name , descr => undefined - , rawconf => RawConf + , config => Config }, emqx_gateway_sup:load_gateway(Gateway). @@ -59,9 +61,9 @@ unload(Name) -> lookup(Name) -> emqx_gateway_sup:lookup_gateway(Name). --spec update(gateway_name(), rawconf()) -> ok | {error, any()}. -update(Name, RawConf) -> - NewGateway = #{name => Name, rawconf => RawConf}, +-spec update(gateway_name(), emqx_config:config()) -> ok | {error, any()}. +update(Name, Config) -> + NewGateway = #{name => Name, config => Config}, emqx_gateway_sup:update_gateway(NewGateway). -spec start(gateway_name()) -> ok | {error, any()}. @@ -72,6 +74,13 @@ start(Name) -> stop(Name) -> emqx_gateway_sup:stop_gateway_insta(Name). +-spec update_rawconf(gateway_name(), emqx_config:raw_config()) + -> ok + | {error, any()}. +update_rawconf(_Name, _RawConf) -> + %% TODO: + ok. + %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 28cb45b47..19ea97e9e 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -356,8 +356,9 @@ gateway_insta(delete, Request) -> gateway_insta(get, Request) -> Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), case emqx_gateway:lookup(Name) of - #{rawconf := RawConf} -> - {200, RawConf}; + #{config := Config} -> + %% TODO: ??? RawConf or Config ?? + {200, Config}; undefined -> {404, <<"Not Found">>} end; diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 0abcb6426..1eae5f54c 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -88,8 +88,8 @@ call(Pid, Req) -> init([Gateway, Ctx0, _GwDscrptr]) -> process_flag(trap_exit, true), - #{name := GwName, rawconf := RawConf} = Gateway, - Ctx = do_init_context(GwName, RawConf, Ctx0), + #{name := GwName, config := Config } = Gateway, + Ctx = do_init_context(GwName, Config, Ctx0), State = #state{ gw = Gateway, ctx = Ctx, @@ -105,8 +105,8 @@ init([Gateway, Ctx0, _GwDscrptr]) -> {ok, NState} end. -do_init_context(GwName, RawConf, Ctx) -> - Auth = case maps:get(authentication, RawConf, #{enable => false}) of +do_init_context(GwName, Config, Ctx) -> + Auth = case maps:get(authentication, Config, #{enable => false}) of #{enable := false} -> undefined; AuthCfg when is_map(AuthCfg) -> case maps:get(enable, AuthCfg, true) of diff --git a/apps/emqx_gateway/src/emqx_gateway_intr.erl b/apps/emqx_gateway/src/emqx_gateway_intr.erl index 2a0c15646..add37e1c5 100644 --- a/apps/emqx_gateway/src/emqx_gateway_intr.erl +++ b/apps/emqx_gateway/src/emqx_gateway_intr.erl @@ -39,7 +39,7 @@ gateways(Status) -> Gateways = lists:map(fun({GwName, _}) -> case emqx_gateway:lookup(GwName) of undefined -> #{name => GwName, status => unloaded}; - GwInfo = #{rawconf := RawConf} -> + GwInfo = #{config := Config} -> GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339( [created_at, started_at, stopped_at], GwInfo), @@ -48,7 +48,7 @@ gateways(Status) -> created_at, started_at, stopped_at], GwInfo0), - GwInfo1#{listeners => get_listeners_status(GwName, RawConf)} + GwInfo1#{listeners => get_listeners_status(GwName, Config)} end end, emqx_gateway_registry:list()), @@ -59,8 +59,8 @@ gateways(Status) -> end. %% @private -get_listeners_status(GwName, RawConf) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), +get_listeners_status(GwName, Config) -> + Listeners = emqx_gateway_utils:normalize_config(Config), lists:map(fun({Type, LisName, ListenOn, _, _}) -> Name0 = listener_name(GwName, Type, LisName), Name = {Name0, ListenOn}, diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 0abeb1354..d161c499c 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -158,7 +158,8 @@ fields(dtls_listener) -> [ {"$name", t(ref(dtls_listener_settings))}]; fields(listener_settings) -> - [ {bind, t(union(ip_port(), integer()))} + [ {enable, t(boolean(), undefined, true)} + , {bind, t(union(ip_port(), integer()))} , {acceptors, t(integer(), undefined, 8)} , {max_connections, t(integer(), undefined, 1024)} , {max_conn_rate, t(integer())} diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index a4978ee8b..82ace4b3d 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -31,7 +31,7 @@ , unix_ts_to_rfc3339/2 ]). --export([ normalize_rawconf/1 +-export([ normalize_config/1 ]). %% Common Envs @@ -118,14 +118,14 @@ unix_ts_to_rfc3339(Key, Map) -> emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} end. --spec normalize_rawconf(rawconf()) +-spec normalize_config(emqx_config:config()) -> list({ Type :: udp | tcp | ssl | dtls , Name :: atom() , ListenOn :: esockd:listen_on() , SocketOpts :: esockd:option() , Cfg :: map() }). -normalize_rawconf(RawConf) -> +normalize_config(RawConf) -> LisMap = maps:get(listeners, RawConf, #{}), Cfg0 = maps:without([listeners], RawConf), lists:append(maps:fold(fun(Type, Liss, AccIn1) -> diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index a3b484fad..2821e9d15 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -84,7 +84,7 @@ start_grpc_client_channel(GwName, Options = #{address := UriStr}) -> grpc_client_sup:create_channel_pool(GwName, SvrAddr, ClientOpts). on_gateway_load(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, Ctx) -> %% XXX: How to monitor it ? %% Start grpc client pool & client channel @@ -92,17 +92,17 @@ on_gateway_load(_Gateway = #{ name := GwName, PoolSize = emqx_vm:schedulers() * 2, {ok, _} = emqx_pool_sup:start_link(PoolName, hash, PoolSize, {emqx_exproto_gcli, start_link, []}), - _ = start_grpc_client_channel(GwName, maps:get(handler, RawConf, undefined)), + _ = start_grpc_client_channel(GwName, maps:get(handler, Config, undefined)), %% XXX: How to monitor it ? - _ = start_grpc_server(GwName, maps:get(server, RawConf, undefined)), + _ = start_grpc_server(GwName, maps:get(server, Config, undefined)), - NRawConf = maps:without( + NConfig = maps:without( [server, handler], - RawConf#{pool_name => PoolName} + Config#{pool_name => PoolName} ), - Listeners = emqx_gateway_utils:normalize_rawconf( - NRawConf#{handler => GwName} + Listeners = emqx_gateway_utils:normalize_config( + NConfig#{handler => GwName} ), ListenerPids = lists:map(fun(Lis) -> start_listener(GwName, Ctx, Lis) @@ -125,9 +125,9 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> end. on_gateway_unload(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) end, Listeners). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index 66a7bd9e2..c00f76532 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -48,7 +48,7 @@ unreg() -> %%-------------------------------------------------------------------- on_gateway_load(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, Ctx) -> %% Handler @@ -58,13 +58,13 @@ on_gateway_load(_Gateway = #{ name := GwName, emqx_lwm2m_coap_resource, undefined ), %% Xml registry - {ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, RawConf)), + {ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, Config)), %% XXX: Self managed table? %% TODO: Improve it later {ok, _} = emqx_lwm2m_cm:start_link(), - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), ListenerPids = lists:map(fun(Lis) -> start_listener(GwName, Ctx, Lis) end, Listeners), @@ -86,7 +86,7 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> end. on_gateway_unload(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, _GwState) -> %% XXX: lwm2m_coap_server_registry:remove_handler( @@ -94,7 +94,7 @@ on_gateway_unload(_Gateway = #{ name := GwName, emqx_lwm2m_coap_resource, undefined ), - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) end, Listeners). diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 0eeb1b952..196818478 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -48,29 +48,29 @@ unreg() -> %%-------------------------------------------------------------------- on_gateway_load(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, Ctx) -> %% We Also need to start `emqx_sn_broadcast` & %% `emqx_sn_registry` process - case maps:get(broadcast, RawConf, false) of + case maps:get(broadcast, Config, false) of false -> ok; true -> %% FIXME: Port = 1884, - SnGwId = maps:get(gateway_id, RawConf, undefined), + SnGwId = maps:get(gateway_id, Config, undefined), _ = emqx_sn_broadcast:start_link(SnGwId, Port), ok end, - PredefTopics = maps:get(predefined, RawConf, []), + PredefTopics = maps:get(predefined, Config, []), {ok, RegistrySvr} = emqx_sn_registry:start_link(GwName, PredefTopics), - NRawConf = maps:without( + NConfig = maps:without( [broadcast, predefined], - RawConf#{registry => emqx_sn_registry:lookup_name(RegistrySvr)} + Config#{registry => emqx_sn_registry:lookup_name(RegistrySvr)} ), - Listeners = emqx_gateway_utils:normalize_rawconf(NRawConf), + Listeners = emqx_gateway_utils:normalize_config(NConfig), ListenerPids = lists:map(fun(Lis) -> start_listener(GwName, Ctx, Lis) @@ -93,9 +93,9 @@ on_gateway_update(NewGateway = #{name := GwName}, OldGateway, end. on_gateway_unload(_Insta = #{ name := GwName, - rawconf := RawConf + config := Config }, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) end, Listeners). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 28dedac26..fd7c0427a 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -50,10 +50,10 @@ unreg() -> %%-------------------------------------------------------------------- on_gateway_load(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, Ctx) -> - %% Step1. Fold the rawconfs to listeners - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + %% Step1. Fold the config to listeners + Listeners = emqx_gateway_utils:normalize_config(Config), %% Step2. Start listeners or escokd:specs ListenerPids = lists:map(fun(Lis) -> start_listener(GwName, Ctx, Lis) @@ -78,9 +78,9 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> end. on_gateway_unload(_Gateway = #{ name := GwName, - rawconf := RawConf + config := Config }, _GwState) -> - Listeners = emqx_gateway_utils:normalize_rawconf(RawConf), + Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) end, Listeners). From bce130d9f959ad8736057ee4987f38af5c46a9bb Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 26 Aug 2021 11:34:12 +0800 Subject: [PATCH 179/306] chore(gw): integrate config-handler --- apps/emqx_gateway/src/emqx_gateway.erl | 8 ++--- apps/emqx_gateway/src/emqx_gateway_api.erl | 16 ++++++++-- apps/emqx_gateway/src/emqx_gateway_app.erl | 31 ++++++++++++++++++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 12 +++---- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 213208f19..d8b4125ce 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -74,13 +74,13 @@ start(Name) -> stop(Name) -> emqx_gateway_sup:stop_gateway_insta(Name). --spec update_rawconf(gateway_name(), emqx_config:raw_config()) +-spec update_rawconf(binary(), emqx_config:raw_config()) -> ok | {error, any()}. -update_rawconf(_Name, _RawConf) -> - %% TODO: - ok. +update_rawconf(RawName, RawConfDiff) -> + emqx:update_config([gateway], {RawName, RawConfDiff}). %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- + diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 19ea97e9e..89b17a6b9 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -357,13 +357,23 @@ gateway_insta(get, Request) -> Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), case emqx_gateway:lookup(Name) of #{config := Config} -> - %% TODO: ??? RawConf or Config ?? + %% TODO: ??? RawConf or Config or RunningState ??? {200, Config}; undefined -> {404, <<"Not Found">>} end; -gateway_insta(post, _Request) -> - {200, ok}. +gateway_insta(post, Request) -> + Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), + {ok, RawConf, _NRequest} = cowboy_req:read_body(Request), + %% XXX: Consistence ?? + case emqx_gateway:update_rawconf(Name, RawConf) of + ok -> + {200, ok}; + {error, not_found} -> + {404, <<"Not Found">>}; + {error, Reason} -> + {500, Reason} + end. gateway_insta_stats(get, _Req) -> {401, <<"Implement it later (maybe 5.1)">>}. diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index adc546767..8af5a1026 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -17,23 +17,52 @@ -module(emqx_gateway_app). -behaviour(application). +-behaviour(emqx_config_handler). -include_lib("emqx/include/logger.hrl"). - -export([start/2, stop/1]). +-export([ pre_config_update/2 + , post_config_update/3 + ]). + start(_StartType, _StartArgs) -> {ok, Sup} = emqx_gateway_sup:start_link(), emqx_gateway_cli:load(), load_default_gateway_applications(), load_gateway_by_default(), + emqx_config_handler:add_handler([gateway], ?MODULE), {ok, Sup}. stop(_State) -> emqx_gateway_cli:unload(), + %% XXX: No api now + %emqx_config_handler:remove_handler([gateway], ?MODULE), ok. +%%-------------------------------------------------------------------- +%% Config Handler + +%% All of update_request is created by emqx_gateway_xx_api.erl module + +-spec pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> + {ok, emqx_config:update_request()} | {error, term()}. +pre_config_update({RawName, RawConfDiff}, RawConf) -> + {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. + +-spec post_config_update(emqx_config:update_request(), emqx_config:config(), + emqx_config:config()) -> ok | {ok, Result::any()} | {error, Reason::term()}. +post_config_update({RawName, _}, NewConfig, OldConfig) -> + GwName = binary_to_existing_atom(RawName), + SubConf = maps:get(GwName, NewConfig), + case maps:get(GwName, OldConfig, undefined) of + undefined -> + emqx_gateway:load(GwName, SubConf); + _ -> + emqx_gateway:update(GwName, SubConf) + end. + %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index d161c499c..f313058c1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -85,7 +85,7 @@ fields(mqttsn_predefined) -> ]; fields(coap_structs) -> - [ {heartbeat, t(duration(), undefined, "30s")} + [ {heartbeat, t(duration(), undefined, <<"30s">>)} , {notify_type, t(union([non, con, qos]), undefined, qos)} , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} @@ -169,12 +169,12 @@ fields(listener_settings) -> , {proxy_protocol, t(boolean())} , {proxy_protocol_timeout, t(duration())} , {backlog, t(integer(), undefined, 1024)} - , {send_timeout, t(duration(), undefined, "15s")} %% FIXME: mapping it + , {send_timeout, t(duration(), undefined, <<"15s">>)} , {send_timeout_close, t(boolean(), undefined, true)} , {recbuf, t(bytesize())} , {sndbuf, t(bytesize())} , {buffer, t(bytesize())} - , {high_watermark, t(bytesize(), undefined, "1MB")} + , {high_watermark, t(bytesize(), undefined, <<"1MB">>)} , {tune_buffer, t(boolean())} , {nodelay, t(boolean())} , {reuseaddr, t(boolean())} @@ -189,7 +189,7 @@ fields(ssl_listener_settings) -> [ %% some special confs for ssl listener ] ++ - ssl(undefined, #{handshake_timeout => "15s" + ssl(undefined, #{handshake_timeout => <<"15s">> , depth => 10 , reuse_sessions => true}) ++ fields(listener_settings); @@ -202,7 +202,7 @@ fields(dtls_listener_settings) -> [ %% some special confs for dtls listener ] ++ - ssl(undefined, #{handshake_timeout => "15s" + ssl(undefined, #{handshake_timeout => <<"15s">> , depth => 10 , reuse_sessions => true}) ++ fields(listener_settings); @@ -241,7 +241,7 @@ authentication() -> gateway_common_options() -> [ {enable, t(boolean(), undefined, true)} , {enable_stats, t(boolean(), undefined, true)} - , {idle_timeout, t(duration(), undefined, "30s")} + , {idle_timeout, t(duration(), undefined, <<"30s">>)} , {mountpoint, t(binary())} , {clientinfo_override, t(ref(clientinfo_override))} , {authentication, t(authentication(), undefined, undefined)} From 8886d607203e57e9cb0a6eb712f5c10ddeb06902 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 26 Aug 2021 14:45:56 +0800 Subject: [PATCH 180/306] refactor(gw): change the on_gateway_update/3 callback params --- .../src/bhvrs/emqx_gateway_impl.erl | 4 +- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 8 +-- apps/emqx_gateway/src/emqx_gateway.erl | 3 +- apps/emqx_gateway/src/emqx_gateway_gw_sup.erl | 9 +-- .../src/emqx_gateway_insta_sup.erl | 66 +++++++------------ apps/emqx_gateway/src/emqx_gateway_schema.erl | 4 -- apps/emqx_gateway/src/emqx_gateway_sup.erl | 8 +-- .../src/exproto/emqx_exproto_impl.erl | 8 +-- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 14 ++-- .../src/stomp/emqx_stomp_impl.erl | 8 +-- 10 files changed, 55 insertions(+), 77 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl index 6906043d9..ac3289dfa 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_impl.erl @@ -30,8 +30,8 @@ | {ok, [Childspec :: supervisor:child_spec()], GwState :: state()}. %% @doc --callback on_gateway_update(NewGateway :: gateway(), - OldGateway :: gateway(), +-callback on_gateway_update(Config :: emqx_config:config(), + Gateway :: gateway(), GwState :: state()) -> ok | {ok, [ChildPid :: pid()], NGwState :: state()} diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index aacb98546..da2f2b8e9 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -58,13 +58,13 @@ on_gateway_load(_Gateway = #{name := GwName, {ok, ListenerPids, #{ctx => Ctx}}. -on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwName = maps:get(name, NewGateway), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_gateway_unload(OldGateway, GwState), - on_gateway_load(NewGateway, Ctx) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> logger:error("Failed to update ~s; " diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index d8b4125ce..aab0dab55 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -63,8 +63,7 @@ lookup(Name) -> -spec update(gateway_name(), emqx_config:config()) -> ok | {error, any()}. update(Name, Config) -> - NewGateway = #{name => Name, config => Config}, - emqx_gateway_sup:update_gateway(NewGateway). + emqx_gateway_sup:update_gateway(Name, Config). -spec start(gateway_name()) -> ok | {error, any()}. start(Name) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl index bfde2b562..d56c8783b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -29,7 +29,7 @@ -export([ create_insta/3 , remove_insta/2 - , update_insta/2 + , update_insta/3 , start_insta/2 , stop_insta/2 , list_insta/1 @@ -72,12 +72,13 @@ remove_insta(Sup, Name) -> ok = supervisor:delete_child(Sup, Name) end. --spec update_insta(pid(), NewGateway :: gateway()) -> ok | {error, any()}. -update_insta(Sup, NewGateway = #{name := Name}) -> +-spec update_insta(pid(), gateway_name(), emqx_config:config()) + -> ok | {error, any()}. +update_insta(Sup, Name, Config) -> case emqx_gateway_utils:find_sup_child(Sup, Name) of false -> {error, not_found}; {ok, GwInstaPid} -> - emqx_gateway_insta_sup:update(GwInstaPid, NewGateway) + emqx_gateway_insta_sup:update(GwInstaPid, Config) end. -spec start_insta(pid(), gateway_name()) -> ok | {error, any()}. diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 1eae5f54c..74419914e 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -69,15 +69,15 @@ info(Pid) -> disable(Pid) -> call(Pid, disable). -%% @doc Start gateway +%% @doc Start gateway -spec enable(pid()) -> ok | {error, any()}. enable(Pid) -> call(Pid, enable). %% @doc Update the gateway configurations --spec update(pid(), gateway()) -> ok | {error, any()}. -update(Pid, NewGateway) -> - call(Pid, {update, NewGateway}). +-spec update(pid(), emqx_config:config()) -> ok | {error, any()}. +update(Pid, Config) -> + call(Pid, {update, Config}). call(Pid, Req) -> gen_server:call(Pid, Req, 5000). @@ -125,12 +125,7 @@ do_deinit_context(Ctx) -> ok. handle_call(info, _From, State = #state{gw = Gateway}) -> - GwInfo = Gateway#{status => State#state.status, - created_at => State#state.created_at, - started_at => State#state.started_at, - stopped_at => State#state.stopped_at - }, - {reply, GwInfo, State}; + {reply, state2info(Gateway), State}; handle_call(disable, _From, State = #state{status = Status}) -> case Status of @@ -158,32 +153,12 @@ handle_call(enable, _From, State = #state{status = Status}) -> {reply, {error, already_started}, State} end; -%% Stopped -> update -handle_call({update, NewGateway}, _From, State = #state{ - gw = Gateway, - status = stopped}) -> - case maps:get(name, NewGateway, undefined) - == maps:get(name, Gateway, undefined) of - true -> - {reply, ok, State#state{gw = NewGateway}}; - false -> - {reply, {error, gateway_name_not_match}, State} - end; - -%% Running -> update -handle_call({update, NewGateway}, _From, State = #state{gw = Gateway, - status = running}) -> - case maps:get(name, NewGateway, undefined) - == maps:get(name, Gateway, undefined) of - true -> - case cb_gateway_update(NewGateway, State) of - {ok, NState} -> - {reply, ok, NState}; - {error, Reason} -> - {reply, {error, Reason}, State} - end; - false -> - {reply, {error, gateway_name_not_match}, State} +handle_call({update, Config}, _From, State) -> + case cb_gateway_update(Config, State) of + {ok, NState} -> + {reply, ok, NState}; + {error, Reason} -> + {reply, {error, Reason}, State} end; handle_call(_Request, _From, State) -> @@ -223,6 +198,14 @@ terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +state2info(State = #state{gw = Gateway}) -> + Gateway#{ + status => State#state.status, + created_at => State#state.created_at, + started_at => State#state.started_at, + stopped_at => State#state.stopped_at + }. + %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- @@ -299,13 +282,12 @@ cb_gateway_load(State = #state{gw = Gateway = #{name := GwName}, {error, {Class, Reason1, Stk}} end. -cb_gateway_update(NewGateway, - State = #state{gw = Gateway = #{name := GwName}, - ctx = Ctx, +cb_gateway_update(Config, + State = #state{gw = #{name := GwName}, gw_state = GwState}) -> try #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), - case CbMod:on_gateway_update(NewGateway, Gateway, GwState) of + case CbMod:on_gateway_update(Config, state2info(State), GwState) of {error, Reason} -> throw({callback_return_error, Reason}); {ok, ChildPidOrSpecs, NGwState} -> %% XXX: Hot-upgrade ??? @@ -317,9 +299,9 @@ cb_gateway_update(NewGateway, end catch Class : Reason1 : Stk -> - logger:error("Failed to update gateway (~0p, ~0p, ~0p) crashed: " + logger:error("Failed to update ~s gateway to config: ~0p crashed: " "{~p, ~p}, stacktrace: ~0p", - [NewGateway, Gateway, Ctx, + [GwName, Config, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index f313058c1..8db75e504 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -234,10 +234,6 @@ authentication() -> , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) ]). -%translations() -> []. -% -%translations(_) -> []. - gateway_common_options() -> [ {enable, t(boolean(), undefined, true)} , {enable_stats, t(boolean(), undefined, true)} diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index 57ac7e7c7..09b74450a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -26,7 +26,7 @@ -export([ load_gateway/1 , unload_gateway/1 , lookup_gateway/1 - , update_gateway/1 + , update_gateway/2 , start_gateway_insta/1 , stop_gateway_insta/1 , list_gateway_insta/0 @@ -74,13 +74,13 @@ lookup_gateway(GwName) -> undefined end. --spec update_gateway(gateway()) +-spec update_gateway(gateway_name(), emqx_config:config()) -> ok | {error, any()}. -update_gateway(NewGateway = #{name := GwName}) -> +update_gateway(GwName, Config) -> case emqx_gateway_utils:find_sup_child(?MODULE, GwName) of {ok, GwSup} -> - emqx_gateway_gw_sup:update_insta(GwSup, NewGateway); + emqx_gateway_gw_sup:update_insta(GwSup, GwName, Config); _ -> {error, not_found} end. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 2821e9d15..8131f2d0c 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -109,13 +109,13 @@ on_gateway_load(_Gateway = #{ name := GwName, end, Listeners), {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwName = maps:get(name, NewGateway), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_gateway_unload(OldGateway, GwState), - on_gateway_load(NewGateway, Ctx) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> logger:error("Failed to update ~s; " diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 196818478..039b23924 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -77,13 +77,13 @@ on_gateway_load(_Gateway = #{ name := GwName, end, Listeners), {ok, ListenerPids, _InstaState = #{ctx => Ctx}}. -on_gateway_update(NewGateway = #{name := GwName}, OldGateway, - GwState = #{ctx := Ctx}) -> +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old instance ??? - on_gateway_unload(OldGateway, GwState), - on_gateway_load(NewGateway, Ctx) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> logger:error("Failed to update ~s; " @@ -92,9 +92,9 @@ on_gateway_update(NewGateway = #{name := GwName}, OldGateway, {error, {Class, Reason}} end. -on_gateway_unload(_Insta = #{ name := GwName, - config := Config - }, _GwState) -> +on_gateway_unload(_Gateway = #{ name := GwName, + config := Config + }, _GwState) -> Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index fd7c0427a..593b71289 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -62,13 +62,13 @@ on_gateway_load(_Gateway = #{ name := GwName, %% FIXME: Assign ctx to GwState {ok, ListenerPids, _GwState = #{ctx => Ctx}}. -on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> - GwName = maps:get(name, NewGateway), +on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) -> + GwName = maps:get(name, Gateway), try %% XXX: 1. How hot-upgrade the changes ??? %% XXX: 2. Check the New confs first before destroy old state??? - on_gateway_unload(OldGateway, GwState), - on_gateway_load(NewGateway, Ctx) + on_gateway_unload(Gateway, GwState), + on_gateway_load(Gateway#{config => Config}, Ctx) catch Class : Reason : Stk -> logger:error("Failed to update ~s; " From dacc53facf8921c13f9ee191eccdc28ec30335b1 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 27 Aug 2021 09:43:49 +0800 Subject: [PATCH 181/306] refactor(gw): suppport the hot upgrade gateway instance --- apps/emqx_gateway/src/emqx_gateway.erl | 31 +++ apps/emqx_gateway/src/emqx_gateway_api.erl | 6 +- apps/emqx_gateway/src/emqx_gateway_app.erl | 29 +-- .../src/emqx_gateway_insta_sup.erl | 200 +++++++++++------- 4 files changed, 162 insertions(+), 104 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index aab0dab55..81d9b593c 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -16,8 +16,15 @@ -module(emqx_gateway). +-behaviour(emqx_config_handler). + -include("include/emqx_gateway.hrl"). +%% callbacks for emqx_config_handler +-export([ pre_config_update/2 + , post_config_update/3 + ]). + %% APIs -export([ registered_gateway/0 , load/2 @@ -31,6 +38,10 @@ -export([update_rawconf/2]). +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + -spec registered_gateway() -> [{gateway_name(), emqx_gateway_registry:descriptor()}]. registered_gateway() -> @@ -79,6 +90,26 @@ stop(Name) -> update_rawconf(RawName, RawConfDiff) -> emqx:update_config([gateway], {RawName, RawConfDiff}). +%%-------------------------------------------------------------------- +%% Config Handler + +-spec pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> + {ok, emqx_config:update_request()} | {error, term()}. +pre_config_update({RawName, RawConfDiff}, RawConf) -> + {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. + +-spec post_config_update(emqx_config:update_request(), emqx_config:config(), + emqx_config:config()) -> ok | {ok, Result::any()} | {error, Reason::term()}. +post_config_update({RawName, _}, NewConfig, OldConfig) -> + GwName = binary_to_existing_atom(RawName), + SubConf = maps:get(GwName, NewConfig), + case maps:get(GwName, OldConfig, undefined) of + undefined -> + emqx_gateway:load(GwName, SubConf); + _ -> + emqx_gateway:update(GwName, SubConf) + end. + %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 89b17a6b9..270a8b332 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -349,9 +349,7 @@ gateway_insta(delete, Request) -> ok -> {200, ok}; {error, not_found} -> - {404, <<"Not Found">>}; - {error, Reason} -> - {500, Reason} + {404, <<"Not Found">>} end; gateway_insta(get, Request) -> Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), @@ -363,7 +361,7 @@ gateway_insta(get, Request) -> {404, <<"Not Found">>} end; gateway_insta(post, Request) -> - Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), + Name = cowboy_req:binding(name, Request), {ok, RawConf, _NRequest} = cowboy_req:read_body(Request), %% XXX: Consistence ?? case emqx_gateway:update_rawconf(Name, RawConf) of diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index 8af5a1026..1ecd9cf26 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -17,22 +17,19 @@ -module(emqx_gateway_app). -behaviour(application). --behaviour(emqx_config_handler). -include_lib("emqx/include/logger.hrl"). -export([start/2, stop/1]). --export([ pre_config_update/2 - , post_config_update/3 - ]). +-define(CONF_CALLBACK_MODULE, emqx_gateway). start(_StartType, _StartArgs) -> {ok, Sup} = emqx_gateway_sup:start_link(), emqx_gateway_cli:load(), load_default_gateway_applications(), load_gateway_by_default(), - emqx_config_handler:add_handler([gateway], ?MODULE), + emqx_config_handler:add_handler([gateway], ?CONF_CALLBACK_MODULE), {ok, Sup}. stop(_State) -> @@ -41,28 +38,6 @@ stop(_State) -> %emqx_config_handler:remove_handler([gateway], ?MODULE), ok. -%%-------------------------------------------------------------------- -%% Config Handler - -%% All of update_request is created by emqx_gateway_xx_api.erl module - --spec pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> - {ok, emqx_config:update_request()} | {error, term()}. -pre_config_update({RawName, RawConfDiff}, RawConf) -> - {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. - --spec post_config_update(emqx_config:update_request(), emqx_config:config(), - emqx_config:config()) -> ok | {ok, Result::any()} | {error, Reason::term()}. -post_config_update({RawName, _}, NewConfig, OldConfig) -> - GwName = binary_to_existing_atom(RawName), - SubConf = maps:get(GwName, NewConfig), - case maps:get(GwName, OldConfig, undefined) of - undefined -> - emqx_gateway:load(GwName, SubConf); - _ -> - emqx_gateway:update(GwName, SubConf) - end. - %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 74419914e..d61d2c479 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -20,6 +20,7 @@ -behaviour(gen_server). -include("include/emqx_gateway.hrl"). +-include_lib("emqx/include/logger.hrl"). %% APIs -export([ start_link/3 @@ -39,7 +40,8 @@ ]). -record(state, { - gw :: gateway(), + name :: gateway_name(), + config :: emqx_config:config(), ctx :: emqx_gateway_ctx:context(), status :: stopped | running, child_pids :: [pid()], @@ -86,48 +88,29 @@ call(Pid, Req) -> %% gen_server callbacks %%-------------------------------------------------------------------- -init([Gateway, Ctx0, _GwDscrptr]) -> +init([Gateway, Ctx, _GwDscrptr]) -> process_flag(trap_exit, true), #{name := GwName, config := Config } = Gateway, - Ctx = do_init_context(GwName, Config, Ctx0), State = #state{ - gw = Gateway, - ctx = Ctx, + ctx = Ctx, + name = GwName, + config = Config, child_pids = [], status = stopped, created_at = erlang:system_time(millisecond) }, case cb_gateway_load(State) of {error, Reason} -> - do_deinit_context(Ctx), {stop, {load_gateway_failure, Reason}}; {ok, NState} -> {ok, NState} end. -do_init_context(GwName, Config, Ctx) -> - Auth = case maps:get(authentication, Config, #{enable => false}) of - #{enable := false} -> undefined; - AuthCfg when is_map(AuthCfg) -> - case maps:get(enable, AuthCfg, true) of - false -> - undefined; - _ -> - create_authentication_for_gateway_insta(GwName, AuthCfg) - end; - _ -> - undefined - end, - Ctx#{auth => Auth}. - -do_deinit_context(Ctx) -> - cleanup_authentication_for_gateway_insta(maps:get(auth, Ctx)), - ok. - -handle_call(info, _From, State = #state{gw = Gateway}) -> - {reply, state2info(Gateway), State}; +handle_call(info, _From, State) -> + {reply, detailed_gateway_info(State), State}; handle_call(disable, _From, State = #state{status = Status}) -> + %% XXX: The `disable` opertaion is not persist to config database case Status of running -> case cb_gateway_unload(State) of @@ -154,10 +137,11 @@ handle_call(enable, _From, State = #state{status = Status}) -> end; handle_call({update, Config}, _From, State) -> - case cb_gateway_update(Config, State) of + case do_update_one_by_one(Config, State) of {ok, NState} -> {reply, ok, NState}; {error, Reason} -> + %% If something wrong, nothing to update {reply, {error, Reason}, State} end; @@ -171,10 +155,10 @@ handle_cast(_Msg, 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]), + ?LOG(error, "Child process ~p exited: ~0p.", [Pid, Reason]), case Pids -- [Pid]of [] -> - logger:error("All child process exited!"), + ?LOG(error, "All child process exited!"), {noreply, State#state{status = stopped, child_pids = [], gw_state = undefined}}; @@ -182,24 +166,25 @@ handle_info({'EXIT', Pid, Reason}, State = #state{child_pids = Pids}) -> {noreply, State#state{child_pids = RemainPids}} end; _ -> - logger:error("Unknown process exited ~p:~0p", [Pid, Reason]), + ?LOG(error, "Unknown process exited ~p:~0p", [Pid, Reason]), {noreply, State} end; handle_info(Info, State) -> - logger:warning("Unexcepted info: ~p", [Info]), + ?LOG(warning, "Unexcepted info: ~p", [Info]), {noreply, State}. terminate(_Reason, State = #state{ctx = Ctx, child_pids = Pids}) -> Pids /= [] andalso (_ = cb_gateway_unload(State)), - _ = do_deinit_context(Ctx), + _ = do_deinit_authn(maps:get(auth, Ctx, undefined)), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -state2info(State = #state{gw = Gateway}) -> - Gateway#{ +detailed_gateway_info(State) -> + #{name => State#state.name, + config => State#state.config, status => State#state.status, created_at => State#state.created_at, started_at => State#state.started_at, @@ -210,36 +195,94 @@ state2info(State = #state{gw = Gateway}) -> %% Internal funcs %%-------------------------------------------------------------------- -create_authentication_for_gateway_insta(GwName, AuthCfg) -> - ChainId = atom_to_binary(GwName, utf8), - case emqx_authn:create_chain(#{id => ChainId}) of - {ok, _ChainInfo} -> - case emqx_authn:create_authenticator(ChainId, AuthCfg) of - {ok, _} -> ChainId; - {error, Reason} -> - logger:error("Failed to create authentication ~p", [Reason]), - throw({bad_authentication, Reason}) +do_init_authn(GwName, Config) -> + case maps:get(authentication, Config, #{enable => false}) of + #{enable := false} -> undefined; + AuthCfg when is_map(AuthCfg) -> + case maps:get(enable, AuthCfg, true) of + false -> + undefined; + _ -> + %% TODO: Implement Authentication + GwName + %case emqx_authn:create_chain(#{id => ChainId}) of + % {ok, _ChainInfo} -> + % case emqx_authn:create_authenticator(ChainId, AuthCfg) of + % {ok, _} -> ChainId; + % {error, Reason} -> + % ?LOG(error, "Failed to create authentication ~p", [Reason]), + % throw({bad_authentication, Reason}) + % end; + % {error, Reason} -> + % ?LOG(error, "Failed to create authentication chain: ~p", [Reason]), + % throw({bad_chain, {ChainId, Reason}}) + %end. end; - {error, Reason} -> - logger:error("Failed to create authentication chain: ~p", [Reason]), - throw({bad_chain, {ChainId, Reason}}) + _ -> + undefined end. -cleanup_authentication_for_gateway_insta(undefined) -> +do_deinit_authn(undefined) -> ok; -cleanup_authentication_for_gateway_insta(ChainId) -> - case emqx_authn:delete_chain(ChainId) of - ok -> ok; - {error, {not_found, _}} -> - logger:warning("Failed to clean authentication chain: ~s, " - "reason: not_found", [ChainId]); - {error, Reason} -> - logger:error("Failed to clean authentication chain: ~s, " - "reason: ~p", [ChainId, Reason]) +do_deinit_authn(AuthnRef) -> + %% TODO: + ?LOG(error, "Failed to clean authn ~p, not suppported now", [AuthnRef]). + %case emqx_authn:delete_chain(AuthnRef) of + % ok -> ok; + % {error, {not_found, _}} -> + % ?LOG(warning, "Failed to clean authentication chain: ~s, " + % "reason: not_found", [AuthnRef]); + % {error, Reason} -> + % ?LOG(error, "Failed to clean authentication chain: ~s, " + % "reason: ~p", [AuthnRef, Reason]) + %end. + +do_update_one_by_one(NCfg0, State = #state{ + ctx = Ctx, + config = OCfg, + status = Status}) -> + + NCfg = emqx_map_lib:deep_merge(OCfg, NCfg0), + + OEnable = maps:get(enable, OCfg, true), + NEnable = maps:get(enable, NCfg0, OEnable), + + OAuth = maps:get(authentication, OCfg, undefined), + NAuth = maps:get(authentication, NCfg0, OAuth), + + if + Status == stopped, NEnable == true -> + NState = State#state{config = NCfg}, + cb_gateway_load(NState); + Status == stopped, NEnable == false -> + {ok, State#state{config = NCfg}}; + Status == running, NEnable == true -> + NState = case NAuth == OAuth of + true -> State; + false -> + %% Reset Authentication first + _ = do_deinit_authn(maps:get(auth, Ctx, undefined)), + NCtx = Ctx#{ + auth => do_init_authn( + State#state.name, + NCfg + ) + }, + State#state{ctx = NCtx} + end, + cb_gateway_update(NCfg, NState); + Status == running, NEnable == false -> + case cb_gateway_unload(State) of + {ok, NState} -> {ok, NState#state{config = NCfg}}; + {error, Reason} -> {error, Reason} + end; + true -> + throw(nomatch) end. -cb_gateway_unload(State = #state{gw = Gateway = #{name := GwName}, +cb_gateway_unload(State = #state{name = GwName, gw_state = GwState}) -> + Gateway = detailed_gateway_info(State), try #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), CbMod:on_gateway_unload(Gateway, GwState), @@ -250,22 +293,33 @@ cb_gateway_unload(State = #state{gw = Gateway = #{name := GwName}, stopped_at = erlang:system_time(millisecond)}} catch Class : Reason : Stk -> - logger:error("Failed to unload gateway (~0p, ~0p) crashed: " - "{~p, ~p}, stacktrace: ~0p", - [Gateway, GwState, + ?LOG(error, "Failed to unload gateway (~0p, ~0p) crashed: " + "{~p, ~p}, stacktrace: ~0p", + [GwName, GwState, Class, Reason, Stk]), {error, {Class, Reason, Stk}} end. -cb_gateway_load(State = #state{gw = Gateway = #{name := GwName}, +%% @doc 1. Create Authentcation Context +%% 2. Callback to Mod:on_gateway_load/2 +%% +%% Notes: If failed, rollback +cb_gateway_load(State = #state{name = GwName, + config = Config, ctx = Ctx}) -> + Gateway = detailed_gateway_info(State), try + AuthnRef = do_init_authn(GwName, Config), + NCtx = Ctx#{auth => AuthnRef}, #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), - case CbMod:on_gateway_load(Gateway, Ctx) of - {error, Reason} -> throw({callback_return_error, Reason}); + case CbMod:on_gateway_load(Gateway, NCtx) of + {error, Reason} -> + do_deinit_authn(AuthnRef), + throw({callback_return_error, Reason}); {ok, ChildPidOrSpecs, GwState} -> ChildPids = start_child_process(ChildPidOrSpecs), {ok, State#state{ + ctx = NCtx, status = running, child_pids = ChildPids, gw_state = GwState, @@ -275,34 +329,34 @@ cb_gateway_load(State = #state{gw = Gateway = #{name := GwName}, end catch Class : Reason1 : Stk -> - logger:error("Failed to load ~s gateway (~0p, ~0p) crashed: " - "{~p, ~p}, stacktrace: ~0p", + ?LOG(error, "Failed to load ~s gateway (~0p, ~0p) crashed: " + "{~p, ~p}, stacktrace: ~0p", [GwName, Gateway, Ctx, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. cb_gateway_update(Config, - State = #state{gw = #{name := GwName}, - gw_state = GwState}) -> + State = #state{name = GwName, + gw_state = GwState}) -> try #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), - case CbMod:on_gateway_update(Config, state2info(State), GwState) of + case CbMod:on_gateway_update(Config, detailed_gateway_info(State), GwState) of {error, Reason} -> throw({callback_return_error, Reason}); {ok, ChildPidOrSpecs, NGwState} -> %% XXX: Hot-upgrade ??? ChildPids = start_child_process(ChildPidOrSpecs), {ok, State#state{ + config = Config, child_pids = ChildPids, gw_state = NGwState }} end catch Class : Reason1 : Stk -> - logger:error("Failed to update ~s gateway to config: ~0p crashed: " - "{~p, ~p}, stacktrace: ~0p", - [GwName, Config, - Class, Reason1, Stk]), + ?LOG(error, "Failed to update ~s gateway to config: ~0p crashed: " + "{~p, ~p}, stacktrace: ~0p", + [GwName, Config, Class, Reason1, Stk]), {error, {Class, Reason1, Stk}} end. From fdb41fe13778116b9f9ff73c6e696ad0a0299350 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 27 Aug 2021 11:25:00 +0800 Subject: [PATCH 182/306] chore(gw): adapt the lates minirest and emqx-config --- apps/emqx_gateway/src/emqx_gateway.erl | 12 ++- apps/emqx_gateway/src/emqx_gateway_api.erl | 39 ++++++---- .../src/emqx_gateway_insta_sup.erl | 77 +++++++++++-------- apps/emqx_gateway/src/emqx_gateway_utils.erl | 7 ++ 4 files changed, 80 insertions(+), 55 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 81d9b593c..79ea5d8a4 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -22,7 +22,7 @@ %% callbacks for emqx_config_handler -export([ pre_config_update/2 - , post_config_update/3 + , post_config_update/4 ]). %% APIs @@ -88,7 +88,10 @@ stop(Name) -> -> ok | {error, any()}. update_rawconf(RawName, RawConfDiff) -> - emqx:update_config([gateway], {RawName, RawConfDiff}). + case emqx:update_config([gateway], {RawName, RawConfDiff}) of + {ok, _Result} -> ok; + {error, Reason} -> {error, Reason} + end. %%-------------------------------------------------------------------- %% Config Handler @@ -99,8 +102,9 @@ pre_config_update({RawName, RawConfDiff}, RawConf) -> {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. -spec post_config_update(emqx_config:update_request(), emqx_config:config(), - emqx_config:config()) -> ok | {ok, Result::any()} | {error, Reason::term()}. -post_config_update({RawName, _}, NewConfig, OldConfig) -> + emqx_config:config(), emqx_config:app_envs()) + -> ok | {ok, Result::any()} | {error, Reason::term()}. +post_config_update({RawName, _}, NewConfig, OldConfig, _AppEnvs) -> GwName = binary_to_existing_atom(RawName), SubConf = maps:get(GwName, NewConfig), case maps:get(GwName, OldConfig, undefined) of diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 270a8b332..f38624ed9 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -29,6 +29,7 @@ %% http handlers -export([ gateway/2 , gateway_insta/2 + , gateway_insta_stats/2 ]). -define(EXAMPLE_GATEWAY_LIST, @@ -240,7 +241,7 @@ metadata(gateway_insta) -> requestBody => schema(schema_for_gateway_conf()), responses => #{ <<"404">> => NameNotFoundRespDef, - <<"204">> => #{description => <<"Created">>} + <<"200">> => #{description => <<"Changed">>} } } }; @@ -336,41 +337,45 @@ schema_for_gateway_stats() -> %% http handlers gateway(get, Request) -> - Params = cowboy_req:parse_qs(Request), - Status = case proplists:get_value(<<"status">>, Params) of + Params = maps:get(query_string, Request, #{}), + Status = case maps:get(<<"status">>, Params, undefined) of undefined -> all; S0 -> binary_to_existing_atom(S0, utf8) end, {200, emqx_gateway_intr:gateways(Status)}. -gateway_insta(delete, Request) -> - Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), +gateway_insta(delete, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), case emqx_gateway:unload(Name) of ok -> {200, ok}; {error, not_found} -> {404, <<"Not Found">>} end; -gateway_insta(get, Request) -> - Name = binary_to_existing_atom(cowboy_req:binding(name, Request)), +gateway_insta(get, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), case emqx_gateway:lookup(Name) of - #{config := Config} -> - %% TODO: ??? RawConf or Config or RunningState ??? - {200, Config}; + #{config := _Config} -> + %% FIXME: Got the parsed config, but we should return rawconfig to + %% frontend + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw([<<"gateway">>]) + ), + {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; undefined -> {404, <<"Not Found">>} end; -gateway_insta(post, Request) -> - Name = cowboy_req:binding(name, Request), - {ok, RawConf, _NRequest} = cowboy_req:read_body(Request), - %% XXX: Consistence ?? - case emqx_gateway:update_rawconf(Name, RawConf) of +gateway_insta(put, #{body := RawConfsIn, + bindings := #{name := Name} + }) -> + %% FIXME: Cluster Consistence ?? + case emqx_gateway:update_rawconf(Name, RawConfsIn) of ok -> - {200, ok}; + {200, <<"Changed">>}; {error, not_found} -> {404, <<"Not Found">>}; {error, Reason} -> - {500, Reason} + {500, emqx_gateway_utils:stringfy(Reason)} end. gateway_insta_stats(get, _Req) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index d61d2c479..39115f114 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -183,13 +183,15 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. detailed_gateway_info(State) -> - #{name => State#state.name, - config => State#state.config, - status => State#state.status, - created_at => State#state.created_at, - started_at => State#state.started_at, - stopped_at => State#state.stopped_at - }. + maps:filter( + fun(_, V) -> V =/= undefined end, + #{name => State#state.name, + config => State#state.config, + status => State#state.status, + created_at => State#state.created_at, + started_at => State#state.started_at, + stopped_at => State#state.stopped_at + }). %%-------------------------------------------------------------------- %% Internal funcs @@ -226,7 +228,7 @@ do_deinit_authn(undefined) -> ok; do_deinit_authn(AuthnRef) -> %% TODO: - ?LOG(error, "Failed to clean authn ~p, not suppported now", [AuthnRef]). + ?LOG(warning, "Failed to clean authn ~p, not suppported now", [AuthnRef]). %case emqx_authn:delete_chain(AuthnRef) of % ok -> ok; % {error, {not_found, _}} -> @@ -307,33 +309,40 @@ cb_gateway_unload(State = #state{name = GwName, cb_gateway_load(State = #state{name = GwName, config = Config, ctx = Ctx}) -> + Gateway = detailed_gateway_info(State), - try - AuthnRef = do_init_authn(GwName, Config), - NCtx = Ctx#{auth => AuthnRef}, - #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), - case CbMod:on_gateway_load(Gateway, NCtx) of - {error, Reason} -> - do_deinit_authn(AuthnRef), - throw({callback_return_error, Reason}); - {ok, ChildPidOrSpecs, GwState} -> - ChildPids = start_child_process(ChildPidOrSpecs), - {ok, State#state{ - ctx = NCtx, - status = running, - child_pids = ChildPids, - gw_state = GwState, - stopped_at = undefined, - started_at = erlang:system_time(millisecond) - }} - end - catch - Class : Reason1 : Stk -> - ?LOG(error, "Failed to load ~s gateway (~0p, ~0p) crashed: " - "{~p, ~p}, stacktrace: ~0p", - [GwName, Gateway, Ctx, - Class, Reason1, Stk]), - {error, {Class, Reason1, Stk}} + + case maps:get(enable, Config, true) of + false -> + ?LOG(info, "Skipp to start ~s gateway due to disabled", [GwName]); + true -> + try + AuthnRef = do_init_authn(GwName, Config), + NCtx = Ctx#{auth => AuthnRef}, + #{cbkmod := CbMod} = emqx_gateway_registry:lookup(GwName), + case CbMod:on_gateway_load(Gateway, NCtx) of + {error, Reason} -> + do_deinit_authn(AuthnRef), + throw({callback_return_error, Reason}); + {ok, ChildPidOrSpecs, GwState} -> + ChildPids = start_child_process(ChildPidOrSpecs), + {ok, State#state{ + ctx = NCtx, + status = running, + child_pids = ChildPids, + gw_state = GwState, + stopped_at = undefined, + started_at = erlang:system_time(millisecond) + }} + end + catch + Class : Reason1 : Stk -> + ?LOG(error, "Failed to load ~s gateway (~0p, ~0p) " + "crashed: {~p, ~p}, stacktrace: ~0p", + [GwName, Gateway, Ctx, + Class, Reason1, Stk]), + {error, {Class, Reason1, Stk}} + end end. cb_gateway_update(Config, diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 82ace4b3d..dc4e38e7d 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -31,6 +31,9 @@ , unix_ts_to_rfc3339/2 ]). +-export([ stringfy/1 + ]). + -export([ normalize_config/1 ]). @@ -118,6 +121,10 @@ unix_ts_to_rfc3339(Key, Map) -> emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} end. +-spec stringfy(term()) -> binary(). +stringfy(T) -> + iolist_to_binary(io_lib:format("~0p", [T])). + -spec normalize_config(emqx_config:config()) -> list({ Type :: udp | tcp | ssl | dtls , Name :: atom() From e6ee8ec1400395dc0ea9e35c69a72ec551c4dd49 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 27 Aug 2021 16:30:43 +0800 Subject: [PATCH 183/306] refactor(listener): rewrite the code for managing listeners --- apps/emqx_management/src/emqx_mgmt.erl | 4 - .../src/emqx_mgmt_api_listeners.erl | 294 ++++++++---------- 2 files changed, 135 insertions(+), 163 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 29f8de0d3..02bb8662c 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -87,7 +87,6 @@ %% Listeners -export([ list_listeners/0 , list_listeners/1 - , list_listeners/2 , list_listeners_by_id/1 , get_listener/2 , manage_listener/2 @@ -473,9 +472,6 @@ reload_plugin(Node, Plugin) -> list_listeners() -> lists:append([list_listeners(Node) || Node <- ekka_mnesia:running_nodes()]). -list_listeners(Node, Identifier) -> - listener_id_filter(Identifier, list_listeners(Node)). - list_listeners(Node) when Node =:= node() -> [{Id, maps:put(node, Node, Conf)} || {Id, Conf} <- emqx_listeners:list()]; diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index fb098634f..e13549a86 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -20,14 +20,15 @@ -export([api_spec/0]). --export([ listeners/2 - , listener/2 +-export([ list_listeners/2 + , list_listeners_by_id/2 + , list_listeners_on_node/2 + , get_listener_by_id_on_node/2 , manage_listeners/2]). -import(emqx_mgmt_util, [ schema/1 , object_schema/2 , object_array_schema/2 - , error_schema/1 , error_schema/2 , properties/1 ]). @@ -36,15 +37,18 @@ -include_lib("emqx/include/emqx.hrl"). +-define(NODE_LISTENER_NOT_FOUND, <<"Node name or listener id not found">>). +-define(LISTENER_NOT_FOUND, <<"Listener id not found">>). + api_spec() -> { [ - listeners_api(), - listener_api(), - nodes_listeners_api(), - nodes_listener_api(), - manage_listeners_api(), - manage_nodes_listeners_api() + api_list_listeners(), + api_list_listeners_by_id(), + api_manage_listeners(), + api_list_listeners_on_node(), + api_get_listener_by_id_on_node(), + api_manage_listeners_on_node() ], [] }. @@ -61,86 +65,74 @@ properties() -> {auth, boolean, <<"Has auth">>} ]). -listeners_api() -> +api_list_listeners() -> Metadata = #{ get => #{ - description => <<"List listeners in cluster">>, + description => <<"List listeners from all nodes in the cluster">>, responses => #{ <<"200">> => - object_array_schema(properties(), <<"List all listeners">>)}}}, - {"/listeners", Metadata, listeners}. + object_array_schema(properties(), <<"List listeners successfully">>)}}}, + {"/listeners", Metadata, list_listeners}. -listener_api() -> +api_list_listeners_by_id() -> Metadata = #{ get => #{ - description => <<"List listeners by listener ID">>, + description => <<"List listeners by a given Id from all nodes in the cluster">>, parameters => [param_path_id()], responses => #{ <<"404">> => - error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), + error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - object_array_schema(properties(), <<"List listener info ok">>)}}}, - {"/listeners/:id", Metadata, listener}. + object_array_schema(properties(), <<"List listeners successfully">>)}}}, + {"/listeners/:id", Metadata, list_listeners_by_id}. -manage_listeners_api() -> - Metadata = #{ - get => #{ - description => <<"Restart listeners in cluster">>, - parameters => [ - param_path_id(), - param_path_operation()], - responses => #{ - <<"500">> => - error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), - <<"404">> => - error_schema(<<"Listener id not found">>, ['BAD_LISTENER_ID']), - <<"400">> => - error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), - <<"200">> => schema(<<"Operation success">>)}}}, - {"/listeners/:id/:operation", Metadata, manage_listeners}. - -manage_nodes_listeners_api() -> - Metadata = #{ - put => #{ - description => <<"Restart listeners in cluster">>, - parameters => [ - param_path_node(), - param_path_id(), - param_path_operation()], - responses => #{ - <<"500">> => - error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), - <<"404">> => - error_schema(<<"Bad node or Listener id not found">>, - ['BAD_NODE_NAME','BAD_LISTENER_ID']), - <<"400">> => - error_schema(<<"Listener id not found">>, ['BAD_REQUEST']), - <<"200">> => - schema(<<"Operation success">>)}}}, - {"/node/:node/listeners/:id/:operation", Metadata, manage_listeners}. - -nodes_listeners_api() -> - Metadata = #{ - get => #{ - description => <<"Get listener info in one node">>, - parameters => [param_path_node(), param_path_id()], - responses => #{ - <<"404">> => - error_schema(<<"Node name or listener id not found">>, - ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), - <<"200">> => - object_schema(properties(), <<"Get listener info ok">>)}}}, - {"/nodes/:node/listeners/:id", Metadata, listener}. - -nodes_listener_api() -> +api_list_listeners_on_node() -> Metadata = #{ get => #{ description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"404">> => error_schema(<<"Listener id not found">>), - <<"200">> => object_schema(properties(), <<"Get listener info ok">>)}}}, - {"/nodes/:node/listeners", Metadata, listeners}. + <<"200">> => object_schema(properties(), <<"List listeners successfully">>)}}}, + {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. + +api_get_listener_by_id_on_node() -> + Metadata = #{ + get => #{ + description => <<"Get a listener by a given Id on a specific node">>, + parameters => [param_path_node(), param_path_id()], + responses => #{ + <<"404">> => + error_schema(?NODE_LISTENER_NOT_FOUND, + ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), + <<"200">> => + object_schema(properties(), <<"Get listener successfully">>)}}}, + {"/nodes/:node/listeners/:id", Metadata, get_listener_by_id_on_node}. + +api_manage_listeners() -> + Metadata = #{ + get => #{ + description => <<"Restart listeners on all nodes in the cluster">>, + parameters => [ + param_path_id(), + param_path_operation()], + responses => #{ + <<"500">> => error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + <<"200">> => schema(<<"Operation success">>)}}}, + {"/listeners/:id/:operation", Metadata, manage_listeners}. + +api_manage_listeners_on_node() -> + Metadata = #{ + put => #{ + description => <<"Restart listeners on all nodes in the cluster">>, + parameters => [ + param_path_node(), + param_path_id(), + param_path_operation()], + responses => #{ + <<"500">> => error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + <<"200">> => schema(<<"Operation success">>)}}}, + {"/nodes/:node/listeners/:id/:operation", Metadata, manage_listeners}. + %%%============================================================================================== %% parameters param_path_node() -> @@ -173,102 +165,80 @@ param_path_operation()-> %%%============================================================================================== %% api -listeners(get, _Request) -> - list(). - -listener(get, #{bindings := Bindings}) -> - get_listeners(Bindings). - -manage_listeners(_, #{bindings := Bindings}) -> - manage(Bindings). - -%%%============================================================================================== - -%% List listeners in the cluster. -list() -> +list_listeners(get, _Request) -> {200, format(emqx_mgmt:list_listeners())}. -get_listeners(Param) -> - case list_listener(Param) of - {error, not_found} -> - ID = b2a(maps:get(id, Param)), - Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), - {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; - {error, nodedown} -> - Node = b2a(maps:get(node, Param)), - Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), - Response = #{code => 'BAD_NODE_NAME', message => Reason}, - {404, Response}; +list_listeners_by_id(get, #{bindings := #{id := Id}}) -> + case [L || L = {Id0, _Conf} <- emqx_mgmt:list_listeners(), + atom_to_binary(Id0, latin1) =:= Id] of [] -> - ID = b2a(maps:get(id, Param)), - Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), - {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; - Data -> - {200, Data} + {400, #{code => 'RESOURCE_NOT_FOUND', message => ?LISTENER_NOT_FOUND}}; + Listeners -> + {200, format(Listeners)} end. -manage(Param) -> - OperationMap = #{start => start_listener, - stop => stop_listener, - restart => restart_listener}, - Operation = maps:get(b2a(maps:get(operation, Param)), OperationMap), - case list_listener(Param) of - {error, not_found} -> - ID = b2a(maps:get(id, Param)), - Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), - {404, #{code => 'BAD_LISTENER_ID', message => Reason}}; - {error, nodedown} -> - Node = b2a(maps:get(node, Param)), - Reason = iolist_to_binary(io_lib:format("Node ~p rpc failed", [Node])), - Response = #{code => 'BAD_NODE_NAME', message => Reason}, - {404, Response}; - [] -> - ID = b2a(maps:get(id, Param)), - Reason = iolist_to_binary(io_lib:format("Error listener id ~p", [ID])), - {404, #{code => 'RESOURCE_NOT_FOUND', message => Reason}}; - ListenersOrSingleListener -> - manage_(Operation, ListenersOrSingleListener) +list_listeners_on_node(get, #{bindings := #{node := Node}}) -> + case emqx_mgmt:list_listeners(atom(Node)) of + {error, Reason} -> + {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}}; + Listener -> + {200, format(Listener)} end. -manage_(Operation, Listener) when is_map(Listener) -> - manage_(Operation, [Listener]); -manage_(Operation, Listeners) when is_list(Listeners) -> - Results = [emqx_mgmt:manage_listener(Operation, Listener) || Listener <- Listeners], - case lists:filter(fun(Result) -> Result =/= ok end, Results) of - [] -> - {200}; - Errors -> - case lists:filter(fun({error, {already_started, _}}) -> false; (_) -> true end, Results) of - [] -> - ID = maps:get(id, hd(Listeners)), - Message = iolist_to_binary(io_lib:format("Already Started: ~s", [ID])), - {400, #{code => 'BAD_REQUEST', message => Message}}; - _ -> - case lists:filter(fun({error,not_found}) -> false; (_) -> true end, Results) of - [] -> - ID = maps:get(id, hd(Listeners)), - Message = iolist_to_binary(io_lib:format("Already Stopped: ~s", [ID])), - {400, #{code => 'BAD_REQUEST', message => Message}}; - _ -> - Reason = iolist_to_binary(io_lib:format("~p", [Errors])), - {500, #{code => 'UNKNOW_ERROR', message => Reason}} - end - end +get_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> + case emqx_mgmt:get_listener(atom(Node), atom(Id)) of + {error, not_found} -> + {404, #{code => 'RESOURCE_NOT_FOUND', message => ?NODE_LISTENER_NOT_FOUND}}; + {error, Reason} -> + {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}}; + Listener -> + {200, format(Listener)} + end. + +manage_listeners(_, #{bindings := #{id := Id, operation := Oper, node := Node}}) -> + {_, Result} = do_manage_listeners(Node, Id, Oper), + Result; + +manage_listeners(_, #{bindings := #{id := Id, operation := Oper}}) -> + Results = [do_manage_listeners(Node, Id, Oper) || Node <- ekka_mnesia:running_nodes()], + case lists:filter(fun({_, {200}}) -> false; (_) -> true end, Results) of + [] -> {200}; + Errors -> {500, #{code => 'UNKNOW_ERROR', message => manage_listeners_err(Errors)}} end. %%%============================================================================================== -%% util function -list_listener(Params) -> - format(list_listener_(Params)). +%% util functions -list_listener_(#{node := Node, id := Identifier}) -> - emqx_mgmt:get_listener(b2a(Node), b2a(Identifier)); -list_listener_(#{id := Identifier}) -> - emqx_mgmt:list_listeners_by_id(b2a(Identifier)); -list_listener_(#{node := Node}) -> - emqx_mgmt:list_listeners(b2a(Node)); -list_listener_(#{}) -> - emqx_mgmt:list_listeners(). +do_manage_listeners(Node, Id, Oper) -> + Param = #{node => atom(Node), id => atom(Id)}, + {Node, do_manage_listeners2(Oper, Param)}. + +do_manage_listeners2(<<"start">>, Param) -> + case emqx_mgmt:manage_listener(start_listener, Param) of + ok -> {200}; + {error, {already_started, _}} -> {200}; + {error, Reason} -> + {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} + end; +do_manage_listeners2(<<"stop">>, Param) -> + case emqx_mgmt:manage_listener(stop_listener, Param) of + ok -> {200}; + {error, not_found} -> {200}; + {error, Reason} -> + {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} + end; +do_manage_listeners2(<<"restart">>, Param) -> + case emqx_mgmt:manage_listener(restart_listener, Param) of + ok -> {200}; + {error, not_found} -> do_manage_listeners2(<<"start">>, Param); + {error, Reason} -> + {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} + end. + +manage_listeners_err(Errors) -> + list_to_binary(lists:foldl(fun({Node, Err}, Str) -> + err_msg_str(#{node => Node, error => Err}) ++ "; " ++ Str + end, "", Errors)). format(Listeners) when is_list(Listeners) -> [format(Listener) || Listener <- Listeners]; @@ -295,6 +265,12 @@ trans_running(Conf) -> Running end. +atom(B) when is_binary(B) -> binary_to_atom(B, utf8); +atom(S) when is_list(S) -> list_to_atom(S); +atom(A) when is_atom(A) -> A. -b2a(B) when is_binary(B) -> binary_to_atom(B, utf8); -b2a(Any) -> Any. +err_msg(Reason) -> + list_to_binary(err_msg_str(Reason)). + +err_msg_str(Reason) -> + io_lib:format("~p", [Reason]). From a4717d22091489d8a73c6e8b3b7a4d96d3a52c9f Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 27 Aug 2021 17:18:23 +0800 Subject: [PATCH 184/306] fix(config): move config_reset under tags 'configs' --- apps/emqx_management/src/emqx_mgmt_api_configs.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index d335afbb2..ba864fa89 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -89,6 +89,7 @@ config_api(ConfPath, Schema) -> config_reset_api() -> Metadata = #{ post => #{ + tags => [configs], description => <<"Reset the config entry specified by the query string parameter `conf_path`.
- For a config entry that has default value, this resets it to the default value; - For a config entry that has no default value, an error 400 will be returned">>, From 56f7d64d79d394b788230537680a99b7040a473a Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Aug 2021 18:35:29 +0800 Subject: [PATCH 185/306] fix(dashboard): Update dashboard version to 5.0-beta.7 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a807dfce8..7d7f3da8d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.6 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.7 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From 8f058a768093f746098e3530859e6ad48ab6c0e9 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Aug 2021 18:36:04 +0800 Subject: [PATCH 186/306] fix(rewrite): fix get topic rewrite list fail --- apps/emqx_modules/src/emqx_rewrite.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_modules/src/emqx_rewrite.erl b/apps/emqx_modules/src/emqx_rewrite.erl index 4b4173156..ae83339da 100644 --- a/apps/emqx_modules/src/emqx_rewrite.erl +++ b/apps/emqx_modules/src/emqx_rewrite.erl @@ -52,7 +52,7 @@ disable() -> emqx_hooks:del('message.publish', {?MODULE, rewrite_publish}). list() -> - maps:get(<<"rules">>, emqx:get_raw_config([<<"rewrite">>], []), []). + emqx:get_raw_config([<<"rewrite">>], []). update(Rules0) -> {ok, #{config := Rules}} = emqx:update_config([rewrite], Rules0), From 329cac6623cea49a0865fea8e63c4e2375944e48 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Aug 2021 18:49:59 +0800 Subject: [PATCH 187/306] fix(rewrite): del topic rewrite conf --- apps/emqx_modules/etc/emqx_modules.conf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx_modules/etc/emqx_modules.conf b/apps/emqx_modules/etc/emqx_modules.conf index a55a06bc5..56970bb0e 100644 --- a/apps/emqx_modules/etc/emqx_modules.conf +++ b/apps/emqx_modules/etc/emqx_modules.conf @@ -28,11 +28,11 @@ topic_metrics: [ ] rewrite: [ - { - action = publish - source_topic = "x/#" - re = "^x/y/(.+)$" - dest_topic = "z/y/$1" - } + # { + # action = publish + # source_topic = "x/#" + # re = "^x/y/(.+)$" + # dest_topic = "z/y/$1" + # } ] From 5a0516735f6926b12ea693462263f7abfaf61afd Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Aug 2021 19:28:03 +0800 Subject: [PATCH 188/306] fix(dashboard): Update dashboard version to 5.0-beta.8 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7d7f3da8d..46dcf0366 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.7 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.8 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From f482ff81163780fa92f19977bb951f05576a865a Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Aug 2021 21:02:03 +0800 Subject: [PATCH 189/306] fix(dashboard): Update dashboard version to 5.0-beta.9 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 46dcf0366..52b0ea209 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.8 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.9 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From eb495535d27949b96d0fb4869f419dcc8d0ece3a Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 27 Aug 2021 21:19:53 +0800 Subject: [PATCH 190/306] 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 85ba805bc..572d23155 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-alpha.4"}). +-define(EMQX_RELEASE, {opensource, "5.0-alpha.5"}). -else. From 9e4c7b5f76ad5bde7bc25bc0c21530042770c229 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 27 Aug 2021 10:04:22 +0200 Subject: [PATCH 191/306] chore(config): upgrade to hocon 0.14.0 --- 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 b3fba9146..0ee94328b 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -15,7 +15,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.13.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}} , {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.14.1"}}} diff --git a/rebar.config b/rebar.config index 65ecd8bf9..c1710bda2 100644 --- a/rebar.config +++ b/rebar.config @@ -60,7 +60,7 @@ , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.13.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. From 5165fd6b30a6b5767e215a4a550ed4d4b3cd79ae Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 27 Aug 2021 10:07:15 +0200 Subject: [PATCH 192/306] refactor(schema): implement new hocon_schema callbacks --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_config.erl | 8 +++----- apps/emqx/src/emqx_schema.erl | 9 +++++---- .../src/emqx_hocon_plugin_schema.erl | 4 ++-- apps/emqx_authn/src/emqx_authn_schema.erl | 4 ++-- .../emqx_enhanced_authn_scram_mnesia.erl | 4 ++-- .../emqx_authn/src/simple_authn/emqx_authn_http.erl | 8 +++----- apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl | 8 +++----- .../src/simple_authn/emqx_authn_mnesia.erl | 6 +++--- .../src/simple_authn/emqx_authn_mongodb.erl | 8 +++----- .../src/simple_authn/emqx_authn_mysql.erl | 4 ++-- .../src/simple_authn/emqx_authn_pgsql.erl | 4 ++-- .../src/simple_authn/emqx_authn_redis.erl | 8 +++----- apps/emqx_authz/src/emqx_authz_schema.erl | 6 +++--- .../src/emqx_auto_subscribe_schema.erl | 4 ++-- .../src/emqx_bridge_mqtt_schema.erl | 6 ++++-- apps/emqx_connector/src/emqx_connector_http.erl | 8 +++----- apps/emqx_connector/src/emqx_connector_ldap.erl | 10 +++++----- apps/emqx_connector/src/emqx_connector_mongo.erl | 9 ++++----- apps/emqx_connector/src/emqx_connector_mysql.erl | 8 +++----- apps/emqx_connector/src/emqx_connector_pgsql.erl | 7 +++---- apps/emqx_connector/src/emqx_connector_redis.erl | 9 ++++----- .../src/emqx_connector_schema_lib.erl | 4 ++-- apps/emqx_dashboard/src/emqx_dashboard_schema.erl | 4 ++-- .../src/emqx_data_bridge_schema.erl | 6 +++--- apps/emqx_exhook/src/emqx_exhook_schema.erl | 4 ++-- apps/emqx_gateway/src/emqx_gateway_schema.erl | 8 ++------ apps/emqx_machine/src/emqx_machine_schema.erl | 13 ++++++++----- apps/emqx_management/src/emqx_management_schema.erl | 4 ++-- apps/emqx_modules/src/emqx_modules_schema.erl | 9 +++++---- apps/emqx_prometheus/src/emqx_prometheus_schema.erl | 4 ++-- apps/emqx_retainer/src/emqx_retainer_schema.erl | 4 ++-- .../src/emqx_rule_engine_schema.erl | 4 ++-- apps/emqx_statsd/src/emqx_statsd_schema.erl | 4 ++-- rebar.config.erl | 3 +-- 35 files changed, 100 insertions(+), 115 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 0ee94328b..271558f6d 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -28,7 +28,7 @@ [{deps, [ meck , {bbmustache,"1.10.0"} - , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {branch,"hocon"}}} + , {emqx_ct_helpers, {git,"https://github.com/emqx/emqx-ct-helpers.git", {tag,"2.1.0"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} ]}, {extra_src_dirs, [{"test",[recursive]}]} diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index e995f1d74..2f5bc9551 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -298,10 +298,11 @@ read_override_conf() -> -spec save_schema_mod_and_names(module()) -> ok. save_schema_mod_and_names(SchemaMod) -> - RootNames = SchemaMod:structs(), + RootNames = hocon_schema:root_names(SchemaMod), OldMods = get_schema_mod(), OldNames = get_root_names(), - NewMods = maps:from_list([{root_bin(Name), SchemaMod} || Name <- RootNames]), + %% map from root name to schema module name + NewMods = maps:from_list([{Name, SchemaMod} || Name <- RootNames]), persistent_term:put(?PERSIS_SCHEMA_MODS, #{ mods => maps:merge(OldMods, NewMods), names => lists:usort(OldNames ++ RootNames) @@ -442,6 +443,3 @@ conf_key(?CONF, RootName) -> atom(RootName); conf_key(?RAW_CONF, RootName) -> bin(RootName). - -root_bin({array, Bin}) -> bin(Bin); -root_bin(Bin) -> bin(Bin). diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 34a63534d..7d1e39510 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -65,14 +65,15 @@ cipher/0, comma_separated_atoms/0]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -export([t/1, t/3, t/4, ref/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([ssl/1]). -structs() -> ["zones", "mqtt", "flapping_detect", "force_shutdown", "force_gc", - "conn_congestion", "rate_limit", "quota", "listeners", "broker", "plugins", - "stats", "sysmon", "alarm", "authorization"]. +roots() -> + ["zones", "mqtt", "flapping_detect", "force_shutdown", "force_gc", + "conn_congestion", "rate_limit", "quota", "listeners", "broker", "plugins", + "stats", "sysmon", "alarm", "authorization"]. fields("stats") -> [ {"enable", t(boolean(), undefined, true)} diff --git a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl index fab74b5e8..8e333a3e0 100644 --- a/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl +++ b/apps/emqx/test/emqx_plugins_SUITE_data/emqx_hocon_plugin/src/emqx_hocon_plugin_schema.erl @@ -2,11 +2,11 @@ -include_lib("typerefl/include/types.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -behaviour(hocon_schema). -structs() -> ["emqx_hocon_plugin"]. +roots() -> ["emqx_hocon_plugin"]. fields("emqx_hocon_plugin") -> [{name, fun name/1}]. diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 6a834df1f..de0de9fcc 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -21,7 +21,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 ]). @@ -32,7 +32,7 @@ -export([ authenticators/1 ]). -structs() -> [ "authentication" ]. +roots() -> [ "authentication" ]. fields("authentication") -> [ {enable, fun enable/1} 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 index f4fede9f5..0ca281aa0 100644 --- 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 @@ -21,7 +21,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 ]). @@ -74,7 +74,7 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +roots() -> [config]. fields(config) -> [ {name, fun emqx_authn_schema:authenticator_name/1} 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 43af1d9b4..c5cbc0f02 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -22,7 +22,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 , validations/0 ]). @@ -37,13 +37,11 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. - -fields("") -> +roots() -> [ {config, {union, [ hoconsc:t(get) , hoconsc:t(post) ]}} - ]; + ]. fields(get) -> [ {method, #{type => get, 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 74aa9e8f6..bc26bf70e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -20,7 +20,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 ]). @@ -34,14 +34,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. - -fields("") -> +roots() -> [ {config, {union, [ hoconsc:t('hmac-based') , hoconsc:t('public-key') , hoconsc:t('jwks') ]}} - ]; + ]. fields('hmac-based') -> [ {use_jwks, {enum, [false]}} 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 9bbf3239c..c525efbf1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -21,7 +21,7 @@ -behaviour(hocon_schema). --export([ structs/0, fields/1 ]). +-export([ roots/0, fields/1 ]). -export([ create/1 , update/2 @@ -79,7 +79,7 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +roots() -> [config]. fields(config) -> [ {name, fun emqx_authn_schema:authenticator_name/1} @@ -391,4 +391,4 @@ to_binary(L) when is_list(L) -> iolist_to_binary(L). serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> - #{user_id => UserID, superuser => Superuser}. \ No newline at end of file + #{user_id => UserID, superuser => Superuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 1ce145f35..11411b70f 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -22,7 +22,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 ]). @@ -36,14 +36,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. - -fields("") -> +roots() -> [ {config, {union, [ hoconsc:t(standalone) , hoconsc:t('replica-set') , hoconsc:t('sharded-cluster') ]}} - ]; + ]. fields(standalone) -> common_fields() ++ emqx_connector_mongo:fields(single); 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 59afa9671..3cafdb94e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -22,7 +22,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 ]). @@ -36,7 +36,7 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +roots() -> [config]. fields(config) -> [ {name, fun emqx_authn_schema:authenticator_name/1} 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 cce9ebd6f..5c21d3d6c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -23,7 +23,7 @@ -behaviour(hocon_schema). --export([ structs/0, fields/1 ]). +-export([ roots/0, fields/1 ]). -export([ create/1 , update/2 @@ -35,7 +35,7 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [config]. +roots() -> [config]. fields(config) -> [ {name, fun emqx_authn_schema:authenticator_name/1} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 6eff345ed..1b090b007 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -22,7 +22,7 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1 ]). @@ -36,14 +36,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -structs() -> [""]. - -fields("") -> +roots() -> [ {config, {union, [ hoconsc:t(standalone) , hoconsc:t(cluster) , hoconsc:t(sentinel) ]}} - ]; + ]. fields(standalone) -> common_fields() ++ emqx_connector_redis:fields(single); diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 64ef09601..7fb60bae2 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -13,11 +13,11 @@ -type permission() :: allow | deny. -type url() :: emqx_http_lib:uri_map(). --export([ structs/0 +-export([ roots/0 , fields/1 ]). -structs() -> ["authorization"]. +roots() -> ["authorization"]. fields("authorization") -> [ {sources, sources()} @@ -180,4 +180,4 @@ connector_fields(DB) -> [ {type, #{type => DB}} , {enable, #{type => boolean(), default => true}} - ] ++ Mod:fields(""). + ] ++ Mod:roots(). diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl index c3621f3a4..73ae262a1 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -19,10 +19,10 @@ -include_lib("typerefl/include/types.hrl"). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> +roots() -> ["auto_subscribe"]. fields("auto_subscribe") -> 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 02078fac0..925bfa403 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -20,10 +20,12 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> [{array, "bridge_mqtt"}]. +roots() -> [array("bridge_mqtt")]. + +array(Name) -> {Name, hoconsc:array(hoconsc:ref(Name))}. fields("bridge_mqtt") -> [ {name, emqx_schema:t(string(), undefined, true)} diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 11860f32d..572b2a4e8 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -32,7 +32,7 @@ -reflect_type([url/0]). -typerefl_from_string({url/0, emqx_http_lib, uri_parse}). --export([ structs/0 +-export([ roots/0 , fields/1 , validations/0]). @@ -47,10 +47,8 @@ %%===================================================================== %% Hocon schema -structs() -> [""]. - -fields("") -> - [{config, #{type => hoconsc:ref(?MODULE, config)}}]; +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> [ {base_url, fun base_url/1} diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index 8c0504d53..fadf7f56f 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -19,7 +19,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -35,11 +35,11 @@ -export([search/4]). %%===================================================================== -structs() -> [""]. +roots() -> + ldap_fields() ++ emqx_connector_schema_lib:ssl_fields(). -fields("") -> - ldap_fields() ++ - emqx_connector_schema_lib:ssl_fields(). +%% this schema has no sub-structs +fields(_) -> []. on_jsonify(Config) -> Config. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index c4953c3fb..88dfb2b72 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -33,19 +33,18 @@ -export([connect/1]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -export([mongo_query/5]). %%===================================================================== -structs() -> [""]. - -fields("") -> +roots() -> [ {config, #{type => hoconsc:union( [ hoconsc:ref(?MODULE, single) , hoconsc:ref(?MODULE, rs) , hoconsc:ref(?MODULE, sharded) ])}} - ]; + ]. + fields(single) -> [ {mongo_type, #{type => single, default => single}} diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 6a5d93ca2..9dc194c55 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -28,16 +28,14 @@ -export([connect/1]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -export([do_health_check/1]). %%===================================================================== %% Hocon schema -structs() -> [""]. - -fields("") -> - [{config, #{type => hoconsc:ref(?MODULE, config)}}]; +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> emqx_connector_schema_lib:relational_db_fields() ++ diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index e89ab7401..8472c661e 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -18,7 +18,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -35,10 +35,9 @@ -export([do_health_check/1]). %%===================================================================== -structs() -> [""]. -fields("") -> - [{config, #{type => hoconsc:ref(?MODULE, config)}}]; +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> emqx_connector_schema_lib:relational_db_fields() ++ diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 60087188f..4fe26381e 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -23,7 +23,7 @@ -reflect_type([server/0]). -typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %% callbacks of behaviour emqx_resource -export([ on_start/2 @@ -40,16 +40,15 @@ -export([cmd/3]). %%===================================================================== -structs() -> [""]. - -fields("") -> +roots() -> [ {config, #{type => hoconsc:union( [ hoconsc:ref(?MODULE, cluster) , hoconsc:ref(?MODULE, single) , hoconsc:ref(?MODULE, sentinel) ])} } - ]; + ]. + fields(single) -> [ {server, #{type => server()}} , {redis_type, #{type => hoconsc:enum([single]), diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index d0b314077..5f9472cca 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -51,9 +51,9 @@ , servers/0 ]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -structs() -> [ssl_on, ssl_off]. +roots() -> [ssl_on, ssl_off]. fields(ssl_on) -> [ {enable, #{type => true}} diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 018061ff6..e0ff21ada 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -17,10 +17,10 @@ -include_lib("typerefl/include/types.hrl"). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> ["emqx_dashboard"]. +roots() -> ["emqx_dashboard"]. fields("emqx_dashboard") -> [ {listeners, hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"), 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 066d72096..69f53d6c1 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -1,6 +1,6 @@ -module(emqx_data_bridge_schema). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). %%====================================================================================== %% Hocon Schema Definitions @@ -8,7 +8,7 @@ -define(TYPES, [mysql, pgsql, mongo, redis, ldap]). -define(BRIDGES, [hoconsc:ref(?MODULE, T) || T <- ?TYPES]). -structs() -> ["emqx_data_bridge"]. +roots() -> ["emqx_data_bridge"]. fields("emqx_data_bridge") -> [{bridges, #{type => hoconsc:array(hoconsc:union(?BRIDGES)), @@ -23,4 +23,4 @@ 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(""). + {type, #{type => DB}}] ++ Mod:roots(). diff --git a/apps/emqx_exhook/src/emqx_exhook_schema.erl b/apps/emqx_exhook/src/emqx_exhook_schema.erl index 852a210fe..16fd93fa0 100644 --- a/apps/emqx_exhook/src/emqx_exhook_schema.erl +++ b/apps/emqx_exhook/src/emqx_exhook_schema.erl @@ -32,11 +32,11 @@ -reflect_type([duration/0]). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -export([t/1, t/3, t/4, ref/1]). -structs() -> [exhook]. +roots() -> [exhook]. fields(exhook) -> [ {request_failed_action, t(union([deny, ignore]), undefined, deny)} diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 8db75e504..9371f8c6b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -43,14 +43,10 @@ , ip_port/0 ]). --export([structs/0 , fields/1]). - +-export([roots/0 , fields/1]). -export([t/1, t/3, t/4, ref/1]). -%%-------------------------------------------------------------------- -%% Structs - -structs() -> [gateway]. +roots() -> [gateway]. fields(gateway) -> [{stomp, t(ref(stomp_structs))}, diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index ee8cc4978..c25ab8139 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -34,7 +34,7 @@ file/0, cipher/0]). --export([structs/0, fields/1, translations/0, translation/1]). +-export([roots/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]). @@ -59,9 +59,9 @@ ]). %% TODO: add a test case to ensure the list elements are unique -structs() -> +roots() -> ["cluster", "node", "rpc", "log"] - ++ lists:flatmap(fun(Mod) -> Mod:structs() end, ?MERGED_CONFIGS). + ++ lists:flatmap(fun(Mod) -> Mod:roots() end, ?MERGED_CONFIGS). fields("cluster") -> [ {"name", t(atom(), "ekka.cluster_name", emqxcl)} @@ -215,8 +215,7 @@ fields(Name) -> find_field(Name, []) -> error({unknown_config_struct_field, Name}); find_field(Name, [SchemaModule | Rest]) -> - case lists:member(Name, SchemaModule:structs()) orelse - lists:keymember(Name, 2, SchemaModule:structs()) of + case lists:member(bin(Name), hocon_schema:root_names(SchemaModule)) of true -> SchemaModule:fields(Name); false -> find_field(Name, Rest) end. @@ -475,3 +474,7 @@ to_atom(Str) when is_list(Str) -> list_to_atom(Str); to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). + +bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); +bin(Bin) when is_binary(Bin) -> Bin; +bin(L) when is_list(L) -> iolist_to_binary(L). diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl index a0da91d86..d21f0e106 100644 --- a/apps/emqx_management/src/emqx_management_schema.erl +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -19,9 +19,9 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> []. +roots() -> []. fields(_) -> []. diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl index 695db972f..7a6b72a8a 100644 --- a/apps/emqx_modules/src/emqx_modules_schema.erl +++ b/apps/emqx_modules/src/emqx_modules_schema.erl @@ -20,16 +20,16 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> +roots() -> ["delayed", "recon", "telemetry", "event_message", - {array, "rewrite"}, - {array, "topic_metrics"}]. + array("rewrite"), + array("topic_metrics")]. fields(Name) when Name =:= "recon"; Name =:= "telemetry" -> @@ -61,3 +61,4 @@ fields("event_message") -> fields("topic_metrics") -> [{topic, emqx_schema:t(binary())}]. +array(Name) -> {Name, hoconsc:array(hoconsc:ref(Name))}. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index fa41154d3..47630b58d 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -19,10 +19,10 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> ["prometheus"]. +roots() -> ["prometheus"]. fields("prometheus") -> [ {push_gateway_server, emqx_schema:t(string())} diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl index df31f647f..e2acc7fe7 100644 --- a/apps/emqx_retainer/src/emqx_retainer_schema.erl +++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl @@ -2,11 +2,11 @@ -include_lib("typerefl/include/types.hrl"). --export([structs/0, fields/1]). +-export([roots/0, fields/1]). -define(TYPE(Type), hoconsc:t(Type)). -structs() -> ["emqx_retainer"]. +roots() -> ["emqx_retainer"]. fields("emqx_retainer") -> [ {enable, t(boolean(), false)} diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl index f7658c208..a9646ae1e 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -20,10 +20,10 @@ -behaviour(hocon_schema). --export([ structs/0 +-export([ roots/0 , fields/1]). -structs() -> ["emqx_rule_engine"]. +roots() -> ["emqx_rule_engine"]. fields("emqx_rule_engine") -> [{ignore_sys_message, emqx_schema:t(boolean(), undefined, true)}]. diff --git a/apps/emqx_statsd/src/emqx_statsd_schema.erl b/apps/emqx_statsd/src/emqx_statsd_schema.erl index 0e96c37d0..906f55a4c 100644 --- a/apps/emqx_statsd/src/emqx_statsd_schema.erl +++ b/apps/emqx_statsd/src/emqx_statsd_schema.erl @@ -6,12 +6,12 @@ -export([to_ip_port/1]). --export([ structs/0 +-export([ roots/0 , fields/1]). -typerefl_from_string({ip_port/0, emqx_statsd_schema, to_ip_port}). -structs() -> ["statsd"]. +roots() -> ["statsd"]. fields("statsd") -> [ {enable, emqx_schema:t(boolean(), undefined, false)} diff --git a/rebar.config.erl b/rebar.config.erl index ad24e6479..aac6e0085 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -127,8 +127,7 @@ test_plugins() -> test_deps() -> [ {bbmustache, "1.10.0"} - %, {emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "2.0.0"}}} - , {emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {branch, "hocon"}}} + , {emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "2.1.0"}}} , meck ]. From ce1772c2b53e81228aa4e1c7f5d86d12bcc367a4 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Sun, 29 Aug 2021 22:15:45 +0200 Subject: [PATCH 193/306] fix(emqx_authz): fix typo in authz annotation key rule -> rules --- apps/emqx_authz/src/emqx_authz.erl | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 3e60cc32e..d935bc4a1 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -314,22 +314,20 @@ authorize(#{username := Username, {stop, DefaultResult} end. -do_authorize(Client, PubSub, Topic, - [#{type := file, - enable := true, - annotations := #{rule := Rules} - } | Tail] ) -> +do_authorize(_Client, _PubSub, _Topic, []) -> + nomatch; +do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) -> + do_authorize(Client, PubSub, Topic, Rest); +do_authorize(Client, PubSub, Topic, [#{type := file} = F | Tail]) -> + #{annotations := #{rules := Rules}} = F, case emqx_authz_rule:match(Client, PubSub, Topic, Rules) of nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched end; do_authorize(Client, PubSub, Topic, - [Connector = #{type := Type, - enable := true - } | Tail] ) -> - Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, Type])), + [Connector = #{type := Type} | Tail] ) -> + Mod = list_to_existing_atom(io_lib:format("emqx_authz_~s",[Type])), case Mod:authorize(Client, PubSub, Topic, Connector) of nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched - end; -do_authorize(_Client, _PubSub, _Topic, []) -> nomatch. + end. From 84ed368d41779a36f756355b2f60428b1a4fcd66 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Mon, 30 Aug 2021 00:05:20 +0200 Subject: [PATCH 194/306] refactor(emqx_authz): use module name builder functions --- apps/emqx_authz/src/emqx_authz.erl | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index d935bc4a1..21e5ceb87 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -210,23 +210,14 @@ gen_id(Type) -> create_resource(#{type := DB, config := Config, annotations := #{id := ResourceID}}) -> - case emqx_resource:update( - ResourceID, - list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - Config, - []) - of + case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} end; create_resource(#{type := DB, config := Config}) -> ResourceID = gen_id(DB), - case emqx_resource:create( - ResourceID, - list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - Config) - of + case emqx_resource:create(ResourceID, connector_module(DB), Config) of {ok, already_created} -> ResourceID; {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} @@ -279,7 +270,7 @@ init_source(#{enable := true, sql := SQL } = Source) when DB =:= mysql; DB =:= pgsql -> - Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), + Mod = authz_module(DB), case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); Id -> Source#{annotations => @@ -326,8 +317,14 @@ do_authorize(Client, PubSub, Topic, [#{type := file} = F | Tail]) -> end; do_authorize(Client, PubSub, Topic, [Connector = #{type := Type} | Tail] ) -> - Mod = list_to_existing_atom(io_lib:format("emqx_authz_~s",[Type])), + Mod = authz_module(Type), case Mod:authorize(Client, PubSub, Topic, Connector) of nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched end. + +authz_module(Type) -> + list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)). + +connector_module(Type) -> + list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)). From 77aca28d87d5da1957ab1a11d07729a63600581a Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Mon, 30 Aug 2021 01:11:16 +0200 Subject: [PATCH 195/306] fix(emqx_authz): call matches with rules input --- apps/emqx_authz/src/emqx_authz.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 21e5ceb87..3a39a2984 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -311,7 +311,7 @@ do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) -> do_authorize(Client, PubSub, Topic, Rest); do_authorize(Client, PubSub, Topic, [#{type := file} = F | Tail]) -> #{annotations := #{rules := Rules}} = F, - case emqx_authz_rule:match(Client, PubSub, Topic, Rules) of + case emqx_authz_rule:matches(Client, PubSub, Topic, Rules) of nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched end; From 100e550491937c960f0066ba3122037d089a4dc4 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Mon, 30 Aug 2021 14:21:15 +0800 Subject: [PATCH 196/306] fix: topic metrics api path & params (#5599) --- apps/emqx_modules/src/emqx_topic_metrics.erl | 2 +- .../src/emqx_topic_metrics_api.erl | 114 +++++++++--------- 2 files changed, 55 insertions(+), 61 deletions(-) diff --git a/apps/emqx_modules/src/emqx_topic_metrics.erl b/apps/emqx_modules/src/emqx_topic_metrics.erl index 3829deb1a..00e1e4bbc 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics.erl @@ -226,7 +226,7 @@ handle_call({deregister, all}, _From, State) -> handle_call({deregister, Topic}, _From, State = #state{speeds = Speeds}) -> case is_registered(Topic) of false -> - {reply, ok, State}; + {reply, {error, topic_not_found}, State}; true -> true = ets:delete(?TAB, Topic), NSpeeds = lists:foldl(fun(Metric, Acc) -> diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index e63af61f6..e22f5750f 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -20,6 +20,7 @@ -import(emqx_mgmt_util, [ properties/1 , schema/1 + , object_schema/1 , object_schema/2 , object_array_schema/2 , error_schema/2 @@ -27,10 +28,8 @@ -export([api_spec/0]). --export([ list_topic_metrics/2 +-export([ topic_metrics/2 , operate_topic_metrics/2 - , reset_all_topic_metrics/2 - , reset_topic_metrics/2 ]). -define(ERROR_TOPIC, 'ERROR_TOPIC'). @@ -40,15 +39,10 @@ -define(BAD_REQUEST, 'BAD_REQUEST'). api_spec() -> - { - [ - list_topic_metrics_api(), - get_topic_metrics_api(), - reset_all_topic_metrics_api(), - reset_topic_metrics_api() - ], - [] - }. + {[ + topic_metrics_api(), + operation_topic_metrics_api() + ],[]}. properties() -> properties([ @@ -75,61 +69,57 @@ properties() -> {'messages.qos2.out.rate', number}]} ]). -list_topic_metrics_api() -> +topic_metrics_api() -> MetaData = #{ get => #{ description => <<"List topic metrics">>, responses => #{ <<"200">> => object_array_schema(properties(), <<"List topic metrics">>) } - } - }, - {"/mqtt/topic_metrics", MetaData, list_topic_metrics}. - -get_topic_metrics_api() -> - MetaData = #{ - get => #{ - description => <<"List topic metrics">>, - parameters => [topic_param()], - responses => #{ - <<"200">> => object_schema(properties(), <<"List topic metrics">>)}}, + }, put => #{ - description => <<"Register topic metrics">>, - parameters => [topic_param()], + description => <<"Reset topic metrics by topic name, or all">>, + 'requestBody' => object_schema(properties([ + {topic, string, <<"no topic will reset all">>}, + {action, string, <<"Action, default reset">>, [reset]} + ])), responses => #{ - <<"200">> => schema(<<"Register topic metrics">>), + <<"200">> => schema(<<"Reset topic metrics success">>), + <<"404">> => error_schema(<<"Topic not found">>, [?ERROR_TOPIC]) + } + }, + post => #{ + description => <<"Create topic metrics">>, + 'requestBody' => object_schema(properties([{topic, string}])), + responses => #{ + <<"200">> => schema(<<"Create topic metrics success">>), <<"409">> => error_schema(<<"Topic metrics max limit">>, [?EXCEED_LIMIT]), <<"400">> => error_schema(<<"Topic metrics already exist">>, [?BAD_REQUEST]) } - }, + } + }, + {"/mqtt/topic_metrics", MetaData, topic_metrics}. + +operation_topic_metrics_api() -> + MetaData = #{ + get => #{ + description => <<"Get topic metrics">>, + parameters => [topic_param()], + responses => #{ + <<"200">> => object_schema(properties(), <<"Topic metrics">>), + <<"404">> => error_schema(<<"Topic not found">>, [?ERROR_TOPIC]) + }}, delete => #{ description => <<"Deregister topic metrics">>, parameters => [topic_param()], - responses => #{ <<"200">> => schema(<<"Deregister topic metrics">>)} + responses => #{ + <<"200">> => schema(<<"Deregister topic metrics">>), + <<"404">> => error_schema(<<"Topic not found">>, [?ERROR_TOPIC]) + } } }, {"/mqtt/topic_metrics/:topic", MetaData, operate_topic_metrics}. -reset_all_topic_metrics_api() -> - MetaData = #{ - put => #{ - description => <<"Reset all topic metrics">>, - responses => #{<<"200">> => schema(<<"Reset all topic metrics">>)} - } - }, - {"/mqtt/topic_metrics/reset", MetaData, reset_all_topic_metrics}. - -reset_topic_metrics_api() -> - Path = "/mqtt/topic_metrics/:topic/reset", - MetaData = #{ - put => #{ - description => <<"Reset topic metrics">>, - parameters => [topic_param()], - responses => #{<<"200">> => schema(<<"Reset topic metrics">>)} - } - }, - {Path, MetaData, reset_topic_metrics}. - topic_param() -> #{ name => topic, @@ -141,8 +131,14 @@ topic_param() -> %%-------------------------------------------------------------------- %% api callback -list_topic_metrics(get, _) -> - list_metrics(). +topic_metrics(get, _) -> + list_metrics(); +topic_metrics(put, #{body := #{<<"topic">> := Topic, <<"action">> := <<"reset">>}}) -> + reset(Topic); +topic_metrics(put, #{body := #{<<"action">> := <<"reset">>}}) -> + reset(); +topic_metrics(post, #{body := #{<<"topic">> := Topic}}) -> + register(Topic). operate_topic_metrics(Method, #{bindings := #{topic := Topic0}}) -> Topic = decode_topic(Topic0), @@ -155,13 +151,6 @@ operate_topic_metrics(Method, #{bindings := #{topic := Topic0}}) -> deregister(Topic) end. -reset_all_topic_metrics(put, _) -> - reset(). - -reset_topic_metrics(put, #{bindings := #{topic := Topic0}}) -> - Topic = decode_topic(Topic0), - reset(Topic). - decode_topic(Topic) -> uri_string:percent_decode(Topic). @@ -184,8 +173,13 @@ register(Topic) -> end. deregister(Topic) -> - _ = emqx_topic_metrics:deregister(Topic), - {200}. + case emqx_topic_metrics:deregister(Topic) of + {error, topic_not_found} -> + Message = list_to_binary(io_lib:format("Topic ~p not found", [Topic])), + {404, #{code => ?ERROR_TOPIC, message => Message}}; + ok -> + {200} + end. get_metrics(Topic) -> case emqx_topic_metrics:metrics(Topic) of From 5b75fdd120eb5955957bb3f4417d573f04aadec9 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:41:00 +0800 Subject: [PATCH 197/306] fix: some API code format & api doc code format (#5601) --- .../src/emqx_mgmt_api_clients.erl | 179 +++------ .../src/emqx_mgmt_api_metrics.erl | 340 +++++------------- .../src/emqx_mgmt_api_stats.erl | 92 ++--- 3 files changed, 164 insertions(+), 447 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index b006a4a49..a4e307114 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -78,143 +78,64 @@ schemas() -> Client = #{ client => #{ type => object, - properties => #{ - 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">>} - } + properties => emqx_mgmt_util:properties(properties(client)) } - }, + }, AuthzCache = #{ authz_cache => #{ type => object, - properties => #{ - 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">>} - } + properties => emqx_mgmt_util:properties(properties(authz_cache)) } }, [Client, AuthzCache]. +properties(client) -> + [ + {awaiting_rel_cnt, integer, <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>}, + {awaiting_rel_max, integer, <<"v4 api name [max_awaiting_rel]. Maximum allowed number of awaiting PUBREC packet">>}, + {clean_start, boolean, <<"Indicate whether the client is using a brand new session">>}, + {clientid, string , <<"Client identifier">>}, + {connected, boolean, <<"Whether the client is connected">>}, + {connected_at, string , <<"Client connection time">>}, + {created_at, string , <<"Session creation time">>}, + {disconnected_at, string , <<"Client offline time, This field is only valid and returned when connected is false">>}, + {expiry_interval, integer, <<"Session expiration interval, with the unit of second">>}, + {heap_size, integer, <<"Process heap size with the unit of byte">>}, + {inflight_cnt, integer, <<"Current length of inflight">>}, + {inflight_max, integer, <<"v4 api name [max_inflight]. Maximum length of inflight">>}, + {ip_address, string , <<"Client's IP address">>}, + {is_bridge, boolean, <<"Indicates whether the client is connectedvia bridge">>}, + {keepalive, integer, <<"keepalive time, with the unit of second">>}, + {mailbox_len, integer, <<"Process mailbox size">>}, + {mqueue_dropped, integer, <<"Number of messages dropped by the message queue due to exceeding the length">>}, + {mqueue_len, integer, <<"Current length of message queue">>}, + {mqueue_max, integer, <<"v4 api name [max_mqueue]. Maximum length of message queue">>}, + {node, string , <<"Name of the node to which the client is connected">>}, + {proto_name, string , <<"Client protocol name">>}, + {proto_ver, integer, <<"Protocol version used by the client">>}, + {recv_cnt, integer, <<"Number of TCP packets received">>}, + {recv_msg, integer, <<"Number of PUBLISH packets received">>}, + {recv_oct, integer, <<"Number of bytes received by EMQ X Broker (the same below)">>}, + {recv_pkt, integer, <<"Number of MQTT packets received">>}, + {reductions, integer, <<"Erlang reduction">>}, + {send_cnt, integer, <<"Number of TCP packets sent">>}, + {send_msg, integer, <<"Number of PUBLISH packets sent">>}, + {send_oct, integer, <<"Number of bytes sent">>}, + {send_pkt, integer, <<"Number of MQTT packets sent">>}, + {subscriptions_cnt, integer, <<"Number of subscriptions established by this client.">>}, + {subscriptions_max, integer, <<"v4 api name [max_subscriptions] Maximum number of subscriptions allowed by this client">>}, + {username, string , <<"User name of client when connecting">>}, + {will_msg, string , <<"Client will message">>}, + {zone, string , <<"Indicate the configuration group used by the client">>} + ]; +properties(authz_cache) -> + [ + {access, string, <<"Access type">>}, + {result, string, <<"Allow or deny">>}, + {topic, string, <<"Topic name">>}, + {updated_time, integer, <<"Update time">>} + ]. + clients_api() -> Metadata = #{ get => #{ diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl index eae9cd76b..2795fe342 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl @@ -28,271 +28,107 @@ api_spec() -> metrics_schema() -> Metric = #{ type => object, - properties => properties() + properties => emqx_mgmt_util:properties(properties()) }, Metrics = #{ type => array, items => #{ type => object, - properties => properties() + properties => emqx_mgmt_util:properties([{node, string} | properties()]) } }, MetricsInfo = #{ - oneOf => [ minirest:ref(<<"metric">>) - , minirest:ref(<<"metrics">>) + oneOf => [ minirest:ref(metric) + , minirest:ref(metrics) ] }, #{metric => Metric, metrics => Metrics, metrics_info => MetricsInfo}. properties() -> - #{ - '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_authz' => #{ - type => integer, - description => <<"Number of Authorization 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 Authorization 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 Authorization 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">>} - }. + [ + {'actions.failure', integer, <<"Number of failure executions of the rule engine action">>}, + {'actions.success', integer, <<"Number of successful executions of the rule engine action">>}, + {'bytes.received', integer, <<"Number of bytes received by EMQ X Broker">>}, + {'bytes.sent', integer, <<"Number of bytes sent by EMQ X Broker on this connection">>}, + {'client.auth.anonymous', integer, <<"Number of clients who log in anonymously">>}, + {'client.authenticate', integer, <<"Number of client authentications">>}, + {'client.check_authz', integer, <<"Number of Authorization rule checks">>}, + {'client.connack', integer, <<"Number of CONNACK packet sent">>}, + {'client.connect', integer, <<"Number of client connections">>}, + {'client.connected', integer, <<"Number of successful client connections">>}, + {'client.disconnected', integer, <<"Number of client disconnects">>}, + {'client.subscribe', integer, <<"Number of client subscriptions">>}, + {'client.unsubscribe', integer, <<"Number of client unsubscriptions">>}, + {'delivery.dropped', integer, <<"Total number of discarded messages when sending">>}, + {'delivery.dropped.expired', integer, <<"Number of messages dropped due to message expiration on sending">>}, + {'delivery.dropped.no_local', integer, <<"Number of messages that were dropped due to the No Local subscription option when sending">>}, + {'delivery.dropped.qos0_msg', integer, <<"Number of messages with QoS 0 that were dropped because the message queue was full when sending">>}, + {'delivery.dropped.queue_full', integer, <<"Number of messages with a non-zero QoS that were dropped because the message queue was full when sending">>}, + {'delivery.dropped.too_large', integer, <<"The number of messages that were dropped because the length exceeded the limit when sending">>}, + {'messages.acked', integer, <<"Number of received PUBACK and PUBREC packet">>}, + {'messages.delayed', integer, <<"Number of delay- published messages stored by EMQ X Broker">>}, + {'messages.delivered', integer, <<"Number of messages forwarded to the subscription process internally by EMQ X Broker">>}, + {'messages.dropped', integer, <<"Total number of messages dropped by EMQ X Broker before forwarding to the subscription process">>}, + {'messages.dropped.expired', integer, <<"Number of messages dropped due to message expiration when receiving">>}, + {'messages.dropped.no_subscribers', integer, <<"Number of messages dropped due to no subscribers">>}, + {'messages.forward', integer, <<"Number of messages forwarded to other nodes">>}, + {'messages.publish', integer, <<"Number of messages published in addition to system messages">>}, + {'messages.qos0.received', integer, <<"Number of QoS 0 messages received from clients">>}, + {'messages.qos0.sent', integer, <<"Number of QoS 0 messages sent to clients">>}, + {'messages.qos1.received', integer, <<"Number of QoS 1 messages received from clients">>}, + {'messages.qos1.sent', integer, <<"Number of QoS 1 messages sent to clients">>}, + {'messages.qos2.received', integer, <<"Number of QoS 2 messages received from clients">>}, + {'messages.qos2.sent', integer, <<"Number of QoS 2 messages sent to clients">>}, + {'messages.received', integer, <<"Number of messages received from the client, equal to the sum of messages.qos0.received\fmessages.qos1.received and messages.qos2.received">>}, + {'messages.retained', integer, <<"Number of retained messages stored by EMQ X Broker">>}, + {'messages.sent', integer, <<"Number of messages sent to the client, equal to the sum of messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent">>}, + {'packets.auth.received', integer, <<"Number of received AUTH packet">>}, + {'packets.auth.sent', integer, <<"Number of sent AUTH packet">>}, + {'packets.connack.auth_error', integer, <<"Number of received CONNECT packet with failed authentication">>}, + {'packets.connack.error', integer, <<"Number of received CONNECT packet with unsuccessful connections">>}, + {'packets.connack.sent', integer, <<"Number of sent CONNACK packet">>}, + {'packets.connect.received', integer, <<"Number of received CONNECT packet">>}, + {'packets.disconnect.received', integer, <<"Number of received DISCONNECT packet">>}, + {'packets.disconnect.sent', integer, <<"Number of sent DISCONNECT packet">>}, + {'packets.pingreq.received', integer, <<"Number of received PINGREQ packet">>}, + {'packets.pingresp.sent', integer, <<"Number of sent PUBRESP packet">>}, + {'packets.puback.inuse', integer, <<"Number of received PUBACK packet with occupied identifiers">>}, + {'packets.puback.missed', integer, <<"Number of received packet with identifiers.">>}, + {'packets.puback.received', integer, <<"Number of received PUBACK packet">>}, + {'packets.puback.sent', integer, <<"Number of sent PUBACK packet">>}, + {'packets.pubcomp.inuse', integer, <<"Number of received PUBCOMP packet with occupied identifiers">>}, + {'packets.pubcomp.missed', integer, <<"Number of missed PUBCOMP packet">>}, + {'packets.pubcomp.received', integer, <<"Number of received PUBCOMP packet">>}, + {'packets.pubcomp.sent', integer, <<"Number of sent PUBCOMP packet">>}, + {'packets.publish.auth_error', integer, <<"Number of received PUBLISH packets with failed the Authorization check">>}, + {'packets.publish.dropped', integer, <<"Number of messages discarded due to the receiving limit">>}, + {'packets.publish.error', integer, <<"Number of received PUBLISH packet that cannot be published">>}, + {'packets.publish.inuse', integer, <<"Number of received PUBLISH packet with occupied identifiers">>}, + {'packets.publish.received', integer, <<"Number of received PUBLISH packet">>}, + {'packets.publish.sent', integer, <<"Number of sent PUBLISH packet">>}, + {'packets.pubrec.inuse', integer, <<"Number of received PUBREC packet with occupied identifiers">>}, + {'packets.pubrec.missed', integer, <<"Number of received PUBREC packet with unknown identifiers">>}, + {'packets.pubrec.received', integer, <<"Number of received PUBREC packet">>}, + {'packets.pubrec.sent', integer, <<"Number of sent PUBREC packet">>}, + {'packets.pubrel.missed', integer, <<"Number of received PUBREC packet with unknown identifiers">>}, + {'packets.pubrel.received', integer, <<"Number of received PUBREL packet">>}, + {'packets.pubrel.sent', integer, <<"Number of sent PUBREL packet">>}, + {'packets.received', integer, <<"Number of received packet">>}, + {'packets.sent', integer, <<"Number of sent packet">>}, + {'packets.suback.sent', integer, <<"Number of sent SUBACK packet">>}, + {'packets.subscribe.auth_error', integer, <<"Number of received SUBACK packet with failed Authorization check">>}, + {'packets.subscribe.error', integer, <<"Number of received SUBSCRIBE packet with failed subscriptions">>}, + {'packets.subscribe.received', integer, <<"Number of received SUBSCRIBE packet">>}, + {'packets.unsuback.sent', integer, <<"Number of sent UNSUBACK packet">>}, + {'packets.unsubscribe.error', integer, <<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>}, + {'packets.unsubscribe.received', integer, <<"Number of received UNSUBSCRIBE packet">>}, + {'rules.matched', integer, <<"Number of rule matched">>}, + {'session.created', integer, <<"Number of sessions created">>}, + {'session.discarded', integer, <<"Number of sessions dropped because Clean Session or Clean Start is true">>}, + {'session.resumed', integer, <<"Number of sessions resumed because Clean Session or Clean Start is false">>}, + {'session.takeovered', integer, <<"Number of sessions takeovered because Clean Session or Clean Start is false">>}, + {'session.terminated', integer, <<"Number of terminated sessions">>} + ]. metrics_api() -> Metadata = #{ diff --git a/apps/emqx_management/src/emqx_mgmt_api_stats.erl b/apps/emqx_management/src/emqx_mgmt_api_stats.erl index d01e4f0c0..470b5fda1 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_stats.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_stats.erl @@ -29,83 +29,43 @@ stats_schema() -> type => array, items => #{ type => object, - properties => maps:put('node', #{type => string, description => <<"Node">>}, properties()) + properties => emqx_mgmt_util:properties([{'node', string} | properties()]) } }, Stat = #{ type => object, - properties => properties() + properties => emqx_mgmt_util:properties(properties()) }, StatsInfo =#{ - oneOf => [ minirest:ref(<<"stats">>) - , minirest:ref(<<"stat">>) + oneOf => [ minirest:ref(stats) + , minirest:ref(stat) ] }, [#{stats => Stats, stat => Stat, stats_info => StatsInfo}]. properties() -> - #{ - '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">>} - }. + [ + {'channels.count', integer, <<"sessions.count">>}, + {'channels.max', integer, <<"session.max">>}, + {'connections.count', integer, <<"Number of current connections">>}, + {'connections.max', integer, <<"Historical maximum number of connections">>}, + {'retained.count', integer, <<"Number of currently retained messages">>}, + {'retained.max', integer, <<"Historical maximum number of retained messages">>}, + {'routes.count', integer, <<"Number of current routes">>}, + {'routes.max', integer, <<"Historical maximum number of routes">>}, + {'sessions.count', integer, <<"Number of current sessions">>}, + {'sessions.max', integer, <<"Historical maximum number of sessions">>}, + {'suboptions.count', integer, <<"subscriptions.count">>}, + {'suboptions.max', integer, <<"subscriptions.max">>}, + {'subscribers.count', integer, <<"Number of current subscribers">>}, + {'subscribers.max', integer, <<"Historical maximum number of subscribers">>}, + {'subscriptions.count', integer, <<"Number of current subscriptions, including shared subscriptions">>}, + {'subscriptions.max', integer, <<"Historical maximum number of subscriptions">>}, + {'subscriptions.shared.count', integer, <<"Number of current shared subscriptions">>}, + {'subscriptions.shared.max', integer, <<"Historical maximum number of shared subscriptions">>}, + {'topics.count', integer, <<"Number of current topics">>}, + {'topics.max', integer, <<"Historical maximum number of topics">>} + ]. stats_api() -> Metadata = #{ From caef8cb38146f10d07e60dbcfb43ff03e866e960 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:24:17 +0800 Subject: [PATCH 198/306] fix: retainer message format time by rfc3339 (#5607) * fix: retainer message format time by rfc3339 --- apps/emqx_retainer/src/emqx_retainer_api.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 313ae9b02..503827d4b 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -46,9 +46,9 @@ message_props() -> {topic, string, <<"MQTT Topic">>}, {qos, string, <<"MQTT QoS">>}, {payload, string, <<"MQTT Payload">>}, - {publish_at, string, <<"publish datetime">>}, - {from_clientid, string, <<"publisher ClientId">>}, - {from_username, string, <<"publisher Username">>} + {publish_at, string, <<"Publish datetime, in RFC 3339 format">>}, + {from_clientid, string, <<"Publisher ClientId">>}, + {from_username, string, <<"Publisher Username">>} ]). parameters() -> @@ -170,7 +170,7 @@ format_message(#message{id = ID, qos = Qos, topic = Topic, from = From, timestam #{msgid => emqx_guid:to_hexstr(ID), qos => Qos, topic => Topic, - publish_at => erlang:list_to_binary(emqx_mgmt_util:strftime(Timestamp div 1000)), + publish_at => list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, millisecond}])), from_clientid => to_bin_string(From), from_username => maps:get(username, Headers, <<>>) }. From 6b313a60d4d9920494ceeed908905725e9945b56 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 30 Aug 2021 17:42:56 +0800 Subject: [PATCH 199/306] refactor: refactor emqx_authz Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 206 ++++++++++-------- apps/emqx_authz/src/emqx_authz_api.erl | 195 ++++++++--------- apps/emqx_authz/src/emqx_authz_api_schema.erl | 10 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 192 ++++++++-------- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 192 ++++++++-------- 5 files changed, 415 insertions(+), 380 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 3a39a2984..4a6d7033e 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -51,33 +51,41 @@ init() -> lookup() -> {_M, _F, [A]}= find_action_in_hooks(), A. -lookup(Id) -> - try find_source_by_id(Id, lookup()) of +lookup(Type) -> + try find_source_by_type(atom(Type), lookup()) of {_, Source} -> Source catch error:Reason -> {error, Reason} end. -move(Id, Position) -> - emqx:update_config(?CONF_KEY_PATH, {move, Id, Position}). +move(Type, #{<<"before">> := Before}) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}); +move(Type, #{<<"after">> := After}) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}); +move(Type, Position) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}). +update({replace_once, Type}, Sources) -> + emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}); +update({delete_once, Type}, Sources) -> + emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}); update(Cmd, Sources) -> emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}). -pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> - {Index, _} = find_source_by_id(Id), +pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), {ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2}; -pre_config_update({move, Id, <<"bottom">>}, Conf) when is_list(Conf) -> - {Index, _} = find_source_by_id(Id), +pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), {ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]}; -pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_source_by_id(Id), +pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) -> + {Index1, _} = find_source_by_type(Type), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_source_by_id(BeforeId), + {Index2, _} = find_source_by_type(Before), Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), @@ -85,10 +93,10 @@ pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Co ++ [Conf1] ++ [Conf2] ++ lists:delete(Conf1, List2)}; -pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_source_by_id(Id), +pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) -> + {Index1, _} = find_source_by_type(Type), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_source_by_id(AfterId), + {Index2, _} = find_source_by_type(After), {List1, List2} = lists:split(Index2, Conf), {ok, lists:delete(Conf1, List1) @@ -99,34 +107,37 @@ pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) -> {ok, Sources ++ Conf}; pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) -> {ok, Conf ++ Sources}; -pre_config_update({{replace_once, Id}, Source}, Conf) when is_map(Source), is_list(Conf) -> - {Index, _} = find_source_by_id(Id), +pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), {ok, lists:droplast(List1) ++ [Source] ++ List2}; +pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) -> + {_, Source} = find_source_by_type(Type), + {ok, lists:delete(Source, Conf)}; pre_config_update({_, Sources}, _Conf) when is_list(Sources)-> %% overwrite the entire config! {ok, Sources}. post_config_update(_, undefined, _Conf, _AppEnvs) -> ok; -post_config_update({move, Id, <<"top">>}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, <<"top">>}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {Index, Source} = find_source_by_id(Id, InitedSources), + {Index, Source} = find_source_by_type(Type, InitedSources), {Sources1, Sources2 } = lists:split(Index, InitedSources), Sources3 = [Source] ++ lists:droplast(Sources1) ++ Sources2, ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {Index, Source} = find_source_by_id(Id, InitedSources), + {Index, Source} = find_source_by_type(Type, InitedSources), {Sources1, Sources2 } = lists:split(Index, InitedSources), Sources3 = lists:droplast(Sources1) ++ Sources2 ++ [Source], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, #{<<"before">> := Before}}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {_, Source0} = find_source_by_id(Id, InitedSources), - {Index, Source1} = find_source_by_id(BeforeId, InitedSources), + {_, Source0} = find_source_by_type(Type, InitedSources), + {Index, Source1} = find_source_by_type(Before, InitedSources), {Sources1, Sources2} = lists:split(Index, InitedSources), Sources3 = lists:delete(Source0, lists:droplast(Sources1)) ++ [Source0] ++ [Source1] @@ -134,10 +145,10 @@ post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSou ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, #{<<"after">> := After}}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {_, Source} = find_source_by_id(Id, InitedSources), - {Index, _} = find_source_by_id(AfterId, InitedSources), + {_, Source} = find_source_by_type(Type, InitedSources), + {Index, _} = find_source_by_type(After, InitedSources), {Sources1, Sources2} = lists:split(Index, InitedSources), Sources3 = lists:delete(Source, Sources1) ++ [Source] @@ -155,9 +166,9 @@ post_config_update({tail, Sources}, _NewSources, _OldConf, _AppEnvs) -> emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({{replace_once, Id}, Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) -> +post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) -> OldInitedSources = lookup(), - {Index, OldSource} = find_source_by_id(Id, OldInitedSources), + {Index, OldSource} = find_source_by_type(Type, OldInitedSources), case maps:get(type, OldSource, undefined) of undefined -> ok; _ -> @@ -165,10 +176,19 @@ post_config_update({{replace_once, Id}, Source}, _NewSources, _OldConf, _AppEnvs ok = emqx_resource:remove(Id) end, {OldSources1, OldSources2 } = lists:split(Index, OldInitedSources), - InitedSources = [init_source(R#{annotations => #{id => Id}}) || R <- check_sources([Source])], + InitedSources = [init_source(R) || R <- check_sources([Source])], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldSources1) ++ InitedSources ++ OldSources2]}, -1), ok = emqx_authz_cache:drain_cache(); - +post_config_update({{delete_once, Type}, _Source}, _NewSources, _OldConf, _AppEnvs) -> + OldInitedSources = lookup(), + {_, OldSource} = find_source_by_type(Type, OldInitedSources), + case OldSource of + #{annotations := #{id := Id}} -> + ok = emqx_resource:remove(Id); + _ -> ok + end, + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:delete(OldSource, OldInitedSources)]}, -1), + ok = emqx_authz_cache:drain_cache(); post_config_update(_, NewSources, _OldConf, _AppEnvs) -> %% overwrite the entire config! OldInitedSources = lookup(), @@ -181,52 +201,13 @@ post_config_update(_, NewSources, _OldConf, _AppEnvs) -> ok = emqx_authz_cache:drain_cache(). %%-------------------------------------------------------------------- -%% Internal functions +%% Initialize source %%-------------------------------------------------------------------- -check_sources(RawSources) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), - Sources. - -find_source_by_id(Id) -> find_source_by_id(Id, lookup()). -find_source_by_id(Id, Sources) -> find_source_by_id(Id, Sources, 1). -find_source_by_id(_SourceId, [], _N) -> error(not_found_rule); -find_source_by_id(SourceId, [ Source = #{annotations := #{id := Id}} | Tail], N) -> - case SourceId =:= Id of - true -> {N, Source}; - false -> find_source_by_id(SourceId, Tail, N + 1) - end. - -find_action_in_hooks() -> - Callbacks = emqx_hooks:lookup('client.authorize'), - [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], - Action. - -gen_id(Type) -> - iolist_to_binary([io_lib:format("~s_~s",[?APP, Type]), "_", integer_to_list(erlang:system_time())]). - -create_resource(#{type := DB, - config := Config, - annotations := #{id := ResourceID}}) -> - case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of - {ok, _} -> ResourceID; - {error, Reason} -> {error, Reason} - end; -create_resource(#{type := DB, - config := Config}) -> - ResourceID = gen_id(DB), - case emqx_resource:create(ResourceID, connector_module(DB), Config) of - {ok, already_created} -> ResourceID; - {ok, _} -> ResourceID; - {error, Reason} -> {error, Reason} - end. - init_source(#{enable := true, - type := file, - path := Path - } = Source) -> + type := file, + path := Path + } = Source) -> Rules = case file:consult(Path) of {ok, Terms} -> [emqx_authz_rule:compile(Term) || Term <- Terms]; @@ -240,35 +221,28 @@ init_source(#{enable := true, ?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]), error(Reason) end, - Source#{annotations => - #{id => gen_id(file), - rules => Rules - }}; + Source#{annotations => #{rules => Rules}}; init_source(#{enable := true, - type := http, - config := #{url := Url} = Config - } = Source) -> + type := http, + config := #{url := Url} = Config + } = Source) -> NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), case create_resource(Source#{config := NConfig}) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => - #{id => Id} - } + Id -> Source#{annotations => #{id => Id}} end; init_source(#{enable := true, - type := DB - } = Source) when DB =:= redis; + type := DB + } = Source) when DB =:= redis; DB =:= mongo -> case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => - #{id => Id} - } + Id -> Source#{annotations => #{id => Id}} end; init_source(#{enable := true, - type := DB, - sql := SQL - } = Source) when DB =:= mysql; + type := DB, + sql := SQL + } = Source) when DB =:= mysql; DB =:= pgsql -> Mod = authz_module(DB), case create_resource(Source) of @@ -323,8 +297,58 @@ do_authorize(Client, PubSub, Topic, Matched -> Matched end. +%%-------------------------------------------------------------------- +%% Internal function +%%-------------------------------------------------------------------- + +check_sources(RawSources) -> + {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), + CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), + #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), + Sources. + +find_source_by_type(Type) -> find_source_by_type(Type, lookup()). +find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1). +find_source_by_type(_, [], _N) -> error(not_found_rule); +find_source_by_type(Type, [ Source = #{type := T} | Tail], N) -> + case Type =:= T of + true -> {N, Source}; + false -> find_source_by_type(Type, Tail, N + 1) + end. + +find_action_in_hooks() -> + Callbacks = emqx_hooks:lookup('client.authorize'), + [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], + Action. + +gen_id(Type) -> + iolist_to_binary([io_lib:format("~s_~s",[?APP, Type])]). + +create_resource(#{type := DB, + config := Config, + annotations := #{id := ResourceID}}) -> + case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of + {ok, _} -> ResourceID; + {error, Reason} -> {error, Reason} + end; +create_resource(#{type := DB, + config := Config}) -> + ResourceID = gen_id(DB), + case emqx_resource:create(ResourceID, connector_module(DB), Config) of + {ok, already_created} -> ResourceID; + {ok, _} -> ResourceID; + {error, Reason} -> {error, Reason} + end. + authz_module(Type) -> list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)). connector_module(Type) -> list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)). + +atom(B) when is_binary(B) -> + try binary_to_existing_atom(B, utf8) + catch + _ -> binary_to_atom(B) + end; +atom(A) when is_atom(A) -> A. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 1646a9af2..ff5217426 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -30,7 +30,7 @@ -define(EXAMPLE_RETURNED_RULES, - #{rules => [?EXAMPLE_RETURNED_RULE1 + #{sources => [?EXAMPLE_RETURNED_RULE1 ] }). @@ -40,23 +40,23 @@ topics => [<<"#">>]}). -export([ api_spec/0 - , rules/2 - , rule/2 - , move_rule/2 + , sources/2 + , source/2 + , move_source/2 ]). api_spec() -> - {[ rules_api() - , rule_api() - , move_rule_api() + {[ sources_api() + , source_api() + , move_source_api() ], definitions()}. definitions() -> emqx_authz_api_schema:definitions(). -rules_api() -> +sources_api() -> Metadata = #{ get => #{ - description => "List authorization rules", + description => "List authorization sources", parameters => [ #{ name => page, @@ -82,16 +82,16 @@ rules_api() -> 'application/json' => #{ schema => #{ type => object, - required => [rules], - properties => #{rules => #{ + required => [sources], + properties => #{sources => #{ type => array, - items => minirest:ref(<<"returned_rules">>) + items => minirest:ref(<<"returned_sources">>) } } }, examples => #{ - rules => #{ - summary => <<"Rules">>, + sources => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RETURNED_RULES) } } @@ -101,14 +101,14 @@ rules_api() -> } }, post => #{ - description => "Add new rule", + description => "Add new source", requestBody => #{ content => #{ 'application/json' => #{ - schema => minirest:ref(<<"rules">>), + schema => minirest:ref(<<"sources">>), examples => #{ - simple_rule => #{ - summary => <<"Rules">>, + simple_source => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RULE1) } } @@ -138,17 +138,17 @@ rules_api() -> }, put => #{ - description => "Update all rules", + description => "Update all sources", requestBody => #{ content => #{ 'application/json' => #{ schema => #{ type => array, - items => minirest:ref(<<"returned_rules">>) + items => minirest:ref(<<"returned_sources">>) }, examples => #{ - rules => #{ - summary => <<"Rules">>, + sources => #{ + summary => <<"Sources">>, value => jsx:encode([?EXAMPLE_RULE1]) } } @@ -177,15 +177,15 @@ rules_api() -> } } }, - {"/authorization", Metadata, rules}. + {"/authorization", Metadata, sources}. -rule_api() -> +source_api() -> Metadata = #{ get => #{ - description => "List authorization rules", + description => "List authorization sources", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -198,10 +198,10 @@ rule_api() -> description => <<"OK">>, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"returned_rules">>), + schema => minirest:ref(<<"returned_sources">>), examples => #{ - rules => #{ - summary => <<"Rules">>, + sources => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RETURNED_RULE1) } } @@ -218,7 +218,7 @@ rule_api() -> summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> + message => <<"source xxx not found">> } } } @@ -228,10 +228,10 @@ rule_api() -> } }, put => #{ - description => "Update rule", + description => "Update source", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -242,10 +242,10 @@ rule_api() -> requestBody => #{ content => #{ 'application/json' => #{ - schema => minirest:ref(<<"rules">>), + schema => minirest:ref(<<"sources">>), examples => #{ - simple_rule => #{ - summary => <<"Rules">>, + simple_source => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RULE1) } } @@ -264,7 +264,7 @@ rule_api() -> summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> + message => <<"source xxx not found">> } } } @@ -291,7 +291,7 @@ rule_api() -> } }, delete => #{ - description => "Delete rule", + description => "Delete source", parameters => [ #{ name => id, @@ -324,15 +324,15 @@ rule_api() -> } } }, - {"/authorization/:id", Metadata, rule}. + {"/authorization/:type", Metadata, source}. -move_rule_api() -> +move_source_api() -> Metadata = #{ post => #{ - description => "Change the order of rules", + description => "Change the order of sources", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -381,15 +381,13 @@ move_rule_api() -> }, <<"404">> => #{ description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), + content => #{ 'application/json' => #{ schema => minirest:ref(<<"error">>), examples => #{ example1 => #{ summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> + message => <<"source xxx not found">> } } } @@ -416,56 +414,54 @@ move_rule_api() -> } } }, - {"/authorization/:id/move", Metadata, move_rule}. + {"/authorization/:type/move", Metadata, move_source}. -rules(get, #{query_string := Query}) -> - Rules = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Rule, AccIn) -> - NRule = case emqx_resource:health_check(Id) of +sources(get, #{query_string := Query}) -> + Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> + NSource = case emqx_resource:health_check(Id) of ok -> - Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}; + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => healthy}}; _ -> - Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}} + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => unhealthy}} end, - lists:append(AccIn, [NRule]); - (#{type := _Type, enable := true, annotations := #{id := Id}} = Rule, AccIn) -> - NRule = case emqx_resource:health_check(Id) of + lists:append(AccIn, [NSource]); + (#{type := _Type, enable := true, annotations := #{id := Id}} = Source, AccIn) -> + NSource = case emqx_resource:health_check(Id) of ok -> - Rule#{annotations => #{id => Id, - status => healthy}}; + Source#{annotations => #{status => healthy}}; _ -> - Rule#{annotations => #{id => Id, - status => unhealthy}} + Source#{annotations => #{status => unhealthy}} end, - lists:append(AccIn, [NRule]); - (Rule, AccIn) -> - lists:append(AccIn, [Rule]) + lists:append(AccIn, [NSource]); + (Source, AccIn) -> + lists:append(AccIn, [Source]) end, [], emqx_authz:lookup()), case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of true -> Page = maps:get(<<"page">>, Query), Limit = maps:get(<<"limit">>, Query), Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), - {_, Rules1} = lists:split(Index, Rules), - case binary_to_integer(Limit) < length(Rules1) of + {_, Sources1} = lists:split(Index, Sources), + case binary_to_integer(Limit) < length(Sources1) of true -> - {Rules2, _} = lists:split(binary_to_integer(Limit), Rules1), - {200, #{rules => Rules2}}; - false -> {200, #{rules => Rules1}} + {Sources2, _} = lists:split(binary_to_integer(Limit), Sources1), + {200, #{sources => Sources2}}; + false -> {200, #{sources => Sources1}} end; - false -> {200, #{rules => Rules}} + false -> {200, #{sources => Sources}} end; -rules(post, #{body := RawConfig}) -> +sources(post, #{body := RawConfig}) -> case emqx_authz:update(head, [RawConfig]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rules(put, #{body := RawConfig}) -> +sources(put, #{body := RawConfig}) -> case emqx_authz:update(replace, RawConfig) of {ok, _} -> {204}; {error, Reason} -> @@ -473,56 +469,57 @@ rules(put, #{body := RawConfig}) -> messgae => atom_to_binary(Reason)}} end. -rule(get, #{bindings := #{id := Id}}) -> - case emqx_authz:lookup(Id) of +source(get, #{bindings := #{type := Type}}) -> + case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{type := file} = Rule -> {200, Rule}; - #{config := #{server := Server} = Config} = Rule -> + #{enable := false} = Source -> {200, Source}; + #{type := file} = Source -> {200, Source}; + #{config := #{server := Server, + annotations := #{id := Id} + } = Config} = Source -> case emqx_resource:health_check(Id) of ok -> - {200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}}; + {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{status => healthy}}}; _ -> - {200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}}} + {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{status => unhealthy}}} end; - Rule -> + #{config := #{annotations := #{id := Id}}} = Source -> case emqx_resource:health_check(Id) of ok -> - {200, Rule#{annotations => #{id => Id, - status => healthy}}}; + {200, Source#{annotations => #{status => healthy}}}; _ -> - {200, Rule#{annotations => #{id => Id, - status => unhealthy}}} + {200, Source#{annotations => #{status => unhealthy}}} end end; -rule(put, #{bindings := #{id := RuleId}, body := RawConfig}) -> - case emqx_authz:update({replace_once, RuleId}, RawConfig) of +source(put, #{bindings := #{type := Type}, body := RawConfig}) -> + case emqx_authz:update({replace_once, Type}, RawConfig) of {ok, _} -> {204}; - {error, not_found_rule} -> + {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, - messgae => <<"rule ", RuleId/binary, " not found">>}}; + messgae => <<"source ", Type/binary, " not found">>}}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rule(delete, #{bindings := #{id := RuleId}}) -> - case emqx_authz:update({replace_once, RuleId}, #{}) of +source(delete, #{bindings := #{type := Type}}) -> + case emqx_authz:update({delete_once, Type}, #{}) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. -move_rule(post, #{bindings := #{id := RuleId}, body := Body}) -> - #{<<"position">> := Position} = Body, - case emqx_authz:move(RuleId, Position) of +move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) -> + case emqx_authz:move(Type, Position) of {ok, _} -> {204}; - {error, not_found_rule} -> + {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, - messgae => <<"rule ", RuleId/binary, " not found">>}}; + messgae => <<"source ", Type/binary, " not found">>}}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. + + + diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 64ecc58eb..1bc316986 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -34,11 +34,11 @@ definitions() -> } } } - , minirest:ref(<<"rules">>) + , minirest:ref(<<"sources">>) ] }, Rules = #{ - oneOf => [ minirest:ref(<<"simple_rule">>) + oneOf => [ minirest:ref(<<"simple_source">>) % , minirest:ref(<<"connector_redis">>) ] }, @@ -144,9 +144,9 @@ definitions() -> } } }, - [ #{<<"returned_rules">> => RetruenedRules} - , #{<<"rules">> => Rules} - , #{<<"simple_rule">> => SimpleRule} + [ #{<<"returned_sources">> => RetruenedRules} + , #{<<"sources">> => Rules} + , #{<<"simple_source">> => SimpleRule} , #{<<"principal">> => Principal} , #{<<"principal_username">> => PrincipalUsername} , #{<<"principal_clientid">> => PrincipalClientid} diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index ef7644a65..cee83cd30 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -61,120 +61,132 @@ init_per_testcase(_, Config) -> Config. -define(SOURCE1, #{<<"type">> => <<"http">>, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000} + }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, - <<"collection">> => <<"fake">>, - <<"find">> => #{<<"a">> => <<"b">>} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE5, #{<<"type">> => <<"redis">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"cmd">> => <<"HGETALL mqtt_authz:%u">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ t_update_source(_) -> - {ok, _} = emqx_authz:update(replace, [?SOURCE2]), + {ok, _} = emqx_authz:update(replace, [?SOURCE3]), + {ok, _} = emqx_authz:update(head, [?SOURCE2]), {ok, _} = emqx_authz:update(head, [?SOURCE1]), - {ok, _} = emqx_authz:update(tail, [?SOURCE3]), + {ok, _} = emqx_authz:update(tail, [?SOURCE4]), + {ok, _} = emqx_authz:update(tail, [?SOURCE5]), - ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization, sources], [])), + ?assertMatch([ #{type := http, enable := true} + , #{type := mongo, enable := true} + , #{type := mysql, enable := true} + , #{type := pgsql, enable := true} + , #{type := redis, enable := true} + ], emqx:get_config([authorization, sources], [])), - [#{annotations := #{id := Id1}, type := http}, - #{annotations := #{id := Id2}, type := mongo}, - #{annotations := #{id := Id3}, type := mysql} - ] = emqx_authz:lookup(), + {ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, mongo}, ?SOURCE2#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}), - {ok, _} = emqx_authz:update({replace_once, Id1}, ?SOURCE5), - {ok, _} = emqx_authz:update({replace_once, Id3}, ?SOURCE4), - ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization, sources], [])), - - [#{annotations := #{id := Id1}, type := redis}, - #{annotations := #{id := Id2}, type := mongo}, - #{annotations := #{id := Id3}, type := pgsql} - ] = emqx_authz:lookup(), + ?assertMatch([ #{type := http, enable := false} + , #{type := mongo, enable := false} + , #{type := mysql, enable := false} + , #{type := pgsql, enable := false} + , #{type := redis, enable := false} + ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update(replace, []). t_move_source(_) -> {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), - [#{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}} - ] = emqx_authz:lookup(), - - {ok, _} = emqx_authz:move(Id4, <<"top">>), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}} + ?assertMatch([ #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := pgsql} + , #{type := redis} ], emqx_authz:lookup()), - {ok, _} = emqx_authz:move(Id1, <<"bottom">>), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + {ok, _} = emqx_authz:move(pgsql, <<"top">>), + ?assertMatch([ #{type := pgsql} + , #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} ], emqx_authz:lookup()), - {ok, _} = emqx_authz:move(Id3, #{<<"before">> => Id4}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + {ok, _} = emqx_authz:move(http, <<"bottom">>), + ?assertMatch([ #{type := pgsql} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), - {ok, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}} + {ok, _} = emqx_authz:move(mysql, #{<<"before">> => pgsql}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := mongo} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), + + {ok, _} = emqx_authz:move(mongo, #{<<"after">> => http}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := http} + , #{type := mongo} + ], emqx_authz:lookup()), + ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 8d92413b3..c8901af77 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -38,54 +38,59 @@ -define(BASE_PATH, "api"). -define(SOURCE1, #{<<"type">> => <<"http">>, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000} + }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, - <<"collection">> => <<"fake">>, - <<"find">> => #{<<"a">> => <<"b">>} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE5, #{<<"type">> => <<"redis">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"cmd">> => <<"HGETALL mqtt_authz:%u">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). all() -> emqx_ct:all(?MODULE). @@ -134,7 +139,7 @@ set_special_configs(emqx_dashboard) -> emqx_config:put([emqx_dashboard], Config), ok; set_special_configs(emqx_authz) -> - emqx_config:put([authorization], #{rules => []}), + emqx_config:put([authorization], #{sources => []}), ok; set_special_configs(_App) -> ok. @@ -145,89 +150,86 @@ set_special_configs(_App) -> t_api(_) -> {ok, 200, Result1} = request(get, uri(["authorization"]), []), - ?assertEqual([], get_rules(Result1)), + ?assertEqual([], get_sources(Result1)), lists:foreach(fun(_) -> {ok, 204, _} = request(post, uri(["authorization"]), ?SOURCE1) end, lists:seq(1, 20)), {ok, 200, Result2} = request(get, uri(["authorization"]), []), - ?assertEqual(20, length(get_rules(Result2))), + ?assertEqual(20, length(get_sources(Result2))), lists:foreach(fun(Page) -> Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", Url = uri(["authorization" ++ Query]), {ok, 200, Result} = request(get, Url, []), - ?assertEqual(10, length(get_rules(Result))) + ?assertEqual(10, length(get_sources(Result))) end, lists:seq(1, 2)), {ok, 204, _} = request(put, uri(["authorization"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), {ok, 200, Result3} = request(get, uri(["authorization"]), []), - Rules = get_rules(Result3), - ?assertEqual(4, length(Rules)), + Sources = get_sources(Result3), ?assertMatch([ #{<<"type">> := <<"http">>} , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} , #{<<"type">> := <<"pgsql">>} - ], Rules), + ], Sources), - #{<<"annotations">> := #{<<"id">> := Id}} = lists:nth(2, Rules), + {ok, 204, _} = request(put, uri(["authorization", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), ?SOURCE5), + {ok, 200, Result4} = request(get, uri(["authorization", "http"]), []), + ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), - {ok, 200, Result4} = request(get, uri(["authorization", binary_to_list(Id)]), []), - ?assertMatch(#{<<"type">> := <<"redis">>}, jsx:decode(Result4)), - - lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id0}}) -> - {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id0)]), []) - end, Rules), + lists:foreach(fun(#{<<"type">> := Type}) -> + {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Type)]), []) + end, Sources), {ok, 200, Result5} = request(get, uri(["authorization"]), []), - ?assertEqual([], get_rules(Result5)), + ?assertEqual([], get_sources(Result5)), ok. -t_move_rule(_) -> +t_move_source(_) -> {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), - [#{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}} - ] = emqx_authz:lookup(), + ?assertMatch([ #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := pgsql} + , #{type := redis} + ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id4, "move"]), + {ok, 204, _} = request(post, uri(["authorization", "pgsql", "move"]), #{<<"position">> => <<"top">>}), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}} + ?assertMatch([ #{type := pgsql} + , #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id1, "move"]), + {ok, 204, _} = request(post, uri(["authorization", "http", "move"]), #{<<"position">> => <<"bottom">>}), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + ?assertMatch([ #{type := pgsql} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id3, "move"]), - #{<<"position">> => #{<<"before">> => Id4}}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + {ok, 204, _} = request(post, uri(["authorization", "mysql", "move"]), + #{<<"position">> => #{<<"before">> => <<"pgsql">>}}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := mongo} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id2, "move"]), - #{<<"position">> => #{<<"after">> => Id1}}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}} + {ok, 204, _} = request(post, uri(["authorization", "mongo", "move"]), + #{<<"position">> => #{<<"after">> => <<"http">>}}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := http} + , #{type := mongo} ], emqx_authz:lookup()), ok. @@ -256,8 +258,8 @@ uri(Parts) when is_list(Parts) -> NParts = [E || E <- Parts], ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). -get_rules(Result) -> - maps:get(<<"rules">>, jsx:decode(Result), []). +get_sources(Result) -> + maps:get(<<"sources">>, jsx:decode(Result), []). auth_header_() -> Username = <<"admin">>, From c0eaa30064502880557970f4c0c57be182c49f96 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Tue, 31 Aug 2021 10:29:28 +0800 Subject: [PATCH 200/306] chore(emqx_authz): change api path --- apps/emqx_authz/src/emqx_authz_api.erl | 7 ++--- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 28 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index ff5217426..dc8694c3f 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -137,7 +137,6 @@ sources_api() -> } }, put => #{ - description => "Update all sources", requestBody => #{ content => #{ @@ -177,7 +176,7 @@ sources_api() -> } } }, - {"/authorization", Metadata, sources}. + {"/authorization/sources", Metadata, sources}. source_api() -> Metadata = #{ @@ -324,7 +323,7 @@ source_api() -> } } }, - {"/authorization/:type", Metadata, source}. + {"/authorization/sources/:type", Metadata, source}. move_source_api() -> Metadata = #{ @@ -414,7 +413,7 @@ move_source_api() -> } } }, - {"/authorization/:type/move", Metadata, move_source}. + {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, #{query_string := Query}) -> Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index c8901af77..946b1a30b 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -149,25 +149,25 @@ set_special_configs(_App) -> %%------------------------------------------------------------------------------ t_api(_) -> - {ok, 200, Result1} = request(get, uri(["authorization"]), []), + {ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []), ?assertEqual([], get_sources(Result1)), lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization"]), ?SOURCE1) + {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1) end, lists:seq(1, 20)), - {ok, 200, Result2} = request(get, uri(["authorization"]), []), + {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), lists:foreach(fun(Page) -> Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", - Url = uri(["authorization" ++ Query]), + Url = uri(["authorization/sources" ++ Query]), {ok, 200, Result} = request(get, Url, []), ?assertEqual(10, length(get_sources(Result))) end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), - {ok, 200, Result3} = request(get, uri(["authorization"]), []), + {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), Sources = get_sources(Result3), ?assertMatch([ #{<<"type">> := <<"http">>} , #{<<"type">> := <<"mongo">>} @@ -175,15 +175,15 @@ t_api(_) -> , #{<<"type">> := <<"pgsql">>} ], Sources), - {ok, 204, _} = request(put, uri(["authorization", "http"]), ?SOURCE1#{<<"enable">> := false}), + {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "http"]), []), + {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), lists:foreach(fun(#{<<"type">> := Type}) -> - {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Type)]), []) + {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result5} = request(get, uri(["authorization"]), []), + {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), ?assertEqual([], get_sources(Result5)), ok. @@ -196,7 +196,7 @@ t_move_source(_) -> , #{type := redis} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "pgsql", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "pgsql", "move"]), #{<<"position">> => <<"top">>}), ?assertMatch([ #{type := pgsql} , #{type := http} @@ -205,7 +205,7 @@ t_move_source(_) -> , #{type := redis} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "http", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]), #{<<"position">> => <<"bottom">>}), ?assertMatch([ #{type := pgsql} , #{type := mongo} @@ -214,7 +214,7 @@ t_move_source(_) -> , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "mysql", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]), #{<<"position">> => #{<<"before">> => <<"pgsql">>}}), ?assertMatch([ #{type := mysql} , #{type := pgsql} @@ -223,7 +223,7 @@ t_move_source(_) -> , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "mongo", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "mongo", "move"]), #{<<"position">> => #{<<"after">> => <<"http">>}}), ?assertMatch([ #{type := mysql} , #{type := pgsql} From ca327b7c5570a45265336c8edf7cff63c7906353 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sun, 29 Aug 2021 15:23:18 +0800 Subject: [PATCH 201/306] refactor(listener): GET /listeners API returns full config of listeners --- apps/emqx/src/emqx_map_lib.erl | 46 +++++++---- .../src/emqx_mgmt_api_configs.erl | 2 +- .../src/emqx_mgmt_api_listeners.erl | 82 ++++++++++--------- 3 files changed, 74 insertions(+), 56 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 0486c10da..529c22816 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -24,13 +24,16 @@ , safe_atom_key_map/1 , unsafe_atom_key_map/1 , jsonable_map/1 - , jsonable_value/1 - , deep_convert/2 + , jsonable_map/2 + , binary_string/1 + , deep_convert/3 ]). -export_type([config_key/0, config_key_path/0]). -type config_key() :: atom() | binary(). -type config_key_path() :: [config_key()]. +-type convert_fun() :: fun((K::any(), V::any(), Args::list()) -> + {K1::any(), V1::any()} | drop). %%----------------------------------------------------------------- -spec deep_get(config_key_path(), map()) -> term(). @@ -100,15 +103,17 @@ deep_merge(BaseMap, NewMap) -> end, #{}, BaseMap), maps:merge(MergedBase, maps:with(NewKeys, NewMap)). --spec deep_convert(map(), fun((K::any(), V::any()) -> {K1::any(), V1::any()})) -> map(). -deep_convert(Map, ConvFun) when is_map(Map) -> +-spec deep_convert(map(), convert_fun(), Args::list()) -> map(). +deep_convert(Map, ConvFun, Args) when is_map(Map) -> maps:fold(fun(K, V, Acc) -> - {K1, V1} = ConvFun(K, deep_convert(V, ConvFun)), - Acc#{K1 => V1} + case apply(ConvFun, [K, deep_convert(V, ConvFun, Args) | Args]) of + drop -> Acc; + {K1, V1} -> Acc#{K1 => V1} + end end, #{}, Map); -deep_convert(ListV, ConvFun) when is_list(ListV) -> - [deep_convert(V, ConvFun) || V <- ListV]; -deep_convert(Val, _) -> Val. +deep_convert(ListV, ConvFun, Args) when is_list(ListV) -> + [deep_convert(V, ConvFun, Args) || V <- ListV]; +deep_convert(Val, _, _Args) -> Val. -spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. unsafe_atom_key_map(Map) -> @@ -120,17 +125,24 @@ safe_atom_key_map(Map) -> -spec jsonable_map(map() | list()) -> map() | list(). jsonable_map(Map) -> - deep_convert(Map, fun(K, V) -> - {jsonable_value(K), jsonable_value(V)} - end). + jsonable_map(Map, fun(K, V) -> {K, V} end). -jsonable_value([]) -> []; -jsonable_value(Val) when is_list(Val) -> +jsonable_map(Map, JsonableFun) -> + deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). + +binary_string_kv(K, V, JsonableFun) -> + case JsonableFun(K, V) of + drop -> drop; + {K1, V1} -> {binary_string(K1), binary_string(V1)} + end. + +binary_string([]) -> []; +binary_string(Val) when is_list(Val) -> case io_lib:printable_unicode_list(Val) of true -> unicode:characters_to_binary(Val); - false -> Val + false -> [binary_string(V) || V <- Val] end; -jsonable_value(Val) -> +binary_string(Val) -> Val. %%--------------------------------------------------------------------------- @@ -138,4 +150,4 @@ covert_keys_to_atom(BinKeyMap, Conv) -> deep_convert(BinKeyMap, fun (K, V) when is_atom(K) -> {K, V}; (K, V) when is_binary(K) -> {Conv(K), V} - end). + end, []). diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index ba864fa89..a859f2002 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -188,7 +188,7 @@ gen_schema(_Conf) -> #{type => string}. with_default_value(Type, Value) -> - Type#{example => emqx_map_lib:jsonable_value(Value)}. + Type#{example => emqx_map_lib:binary_string(Value)}. path_join(Path) -> path_join(Path, "/"). diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index e13549a86..4bad6a26b 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -24,14 +24,9 @@ , list_listeners_by_id/2 , list_listeners_on_node/2 , get_listener_by_id_on_node/2 - , manage_listeners/2]). - --import(emqx_mgmt_util, [ schema/1 - , object_schema/2 - , object_array_schema/2 - , error_schema/2 - , properties/1 - ]). + , manage_listeners/2 + , jsonable_resp/2 + ]). -export([format/1]). @@ -53,17 +48,23 @@ api_spec() -> [] }. -properties() -> - properties([ - {node, string, <<"Node">>}, - {id, string, <<"Identifier">>}, - {acceptors, integer, <<"Number of Acceptor process">>}, - {max_conn, integer, <<"Maximum number of allowed connection">>}, - {type, string, <<"Listener type">>}, - {listen_on, string, <<"Listener port">>}, - {running, boolean, <<"Open or close">>}, - {auth, boolean, <<"Has auth">>} - ]). +-define(TYPES, [tcp, ssl, ws, wss, quic]). +req_schema() -> + Schema = [emqx_mgmt_api_configs:gen_schema( + emqx:get_raw_config([listeners, T, default], #{})) + || T <- ?TYPES], + #{oneOf => Schema}. + +resp_schema() -> + #{oneOf := Schema} = req_schema(), + AddMetadata = fun(Prop) -> + Prop#{running => #{type => boolean}, + id => #{type => string}, + node => #{type => string}} + end, + Schema1 = [S#{properties => AddMetadata(Prop)} + || S = #{properties := Prop} <- Schema], + #{oneOf => Schema1}. api_list_listeners() -> Metadata = #{ @@ -71,7 +72,7 @@ api_list_listeners() -> description => <<"List listeners from all nodes in the cluster">>, responses => #{ <<"200">> => - object_array_schema(properties(), <<"List listeners successfully">>)}}}, + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/listeners", Metadata, list_listeners}. api_list_listeners_by_id() -> @@ -81,9 +82,9 @@ api_list_listeners_by_id() -> parameters => [param_path_id()], responses => #{ <<"404">> => - error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - object_array_schema(properties(), <<"List listeners successfully">>)}}}, + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/listeners/:id", Metadata, list_listeners_by_id}. api_list_listeners_on_node() -> @@ -92,7 +93,7 @@ api_list_listeners_on_node() -> description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"200">> => object_schema(properties(), <<"List listeners successfully">>)}}}, + <<"200">> => emqx_mgmt_util:object_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. api_get_listener_by_id_on_node() -> @@ -102,10 +103,10 @@ api_get_listener_by_id_on_node() -> parameters => [param_path_node(), param_path_id()], responses => #{ <<"404">> => - error_schema(?NODE_LISTENER_NOT_FOUND, + emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - object_schema(properties(), <<"Get listener successfully">>)}}}, + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}}, {"/nodes/:node/listeners/:id", Metadata, get_listener_by_id_on_node}. api_manage_listeners() -> @@ -116,9 +117,9 @@ api_manage_listeners() -> param_path_id(), param_path_operation()], responses => #{ - <<"500">> => error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), - <<"200">> => schema(<<"Operation success">>)}}}, - {"/listeners/:id/:operation", Metadata, manage_listeners}. + <<"500">> => emqx_mgmt_util:error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + <<"200">> => emqx_mgmt_util:schema(<<"Operation success">>)}}}, + {"/listeners/:id/operation/:operation", Metadata, manage_listeners}. api_manage_listeners_on_node() -> Metadata = #{ @@ -129,9 +130,9 @@ api_manage_listeners_on_node() -> param_path_id(), param_path_operation()], responses => #{ - <<"500">> => error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), - <<"200">> => schema(<<"Operation success">>)}}}, - {"/nodes/:node/listeners/:id/:operation", Metadata, manage_listeners}. + <<"500">> => emqx_mgmt_util:error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + <<"200">> => emqx_mgmt_util:schema(<<"Operation success">>)}}}, + {"/nodes/:node/listeners/:id/operation/:operation", Metadata, manage_listeners}. %%%============================================================================================== %% parameters @@ -247,16 +248,12 @@ format({error, Reason}) -> {error, Reason}; format({ID, Conf}) -> - {Type, _Name} = emqx_listeners:parse_listener_id(ID), - #{ + emqx_map_lib:jsonable_map(Conf#{ id => ID, node => maps:get(node, Conf), - acceptors => maps:get(acceptors, Conf), - max_conn => maps:get(max_connections, Conf), - type => Type, - listen_on => list_to_binary(esockd:to_string(maps:get(bind, Conf))), running => trans_running(Conf) - }. + }, fun ?MODULE:jsonable_resp/2). + trans_running(Conf) -> case maps:get(running, Conf) of {error, _} -> @@ -265,6 +262,15 @@ trans_running(Conf) -> Running end. +jsonable_resp(bind, Port) when is_integer(Port) -> + {bind, Port}; +jsonable_resp(bind, {Addr, Port}) when is_tuple(Addr); is_integer(Port)-> + {bind, inet:ntoa(Addr) ++ ":" ++ integer_to_list(Port)}; +jsonable_resp(user_lookup_fun, _) -> + drop; +jsonable_resp(K, V) -> + {K, V}. + atom(B) when is_binary(B) -> binary_to_atom(B, utf8); atom(S) when is_list(S) -> list_to_atom(S); atom(A) when is_atom(A) -> A. From 05fc6d9e45f8b30d37072d291d73ab9f7fd749dd Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 09:48:20 +0800 Subject: [PATCH 202/306] fix(dialyzer): bad function spec for emqx_map_lib:deep_convert/3 --- apps/emqx/src/emqx_map_lib.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 529c22816..de2b41b32 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -32,8 +32,7 @@ -export_type([config_key/0, config_key_path/0]). -type config_key() :: atom() | binary(). -type config_key_path() :: [config_key()]. --type convert_fun() :: fun((K::any(), V::any(), Args::list()) -> - {K1::any(), V1::any()} | drop). +-type convert_fun() :: fun((...) -> {K1::any(), V1::any()} | drop). %%----------------------------------------------------------------- -spec deep_get(config_key_path(), map()) -> term(). From 8c36b7879f08594d3683928ff906014b11b25210 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sun, 29 Aug 2021 16:03:17 +0800 Subject: [PATCH 203/306] feat(listeners): APIs for updating the listener --- apps/emqx/src/emqx_listeners.erl | 17 +++-- apps/emqx_management/src/emqx_mgmt.erl | 14 +++- .../src/emqx_mgmt_api_listeners.erl | 70 +++++++++++++------ 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 2d3357f37..d9670b858 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -265,12 +265,12 @@ format_addr({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). listener_id(Type, ListenerName) -> - list_to_atom(lists:append([atom_to_list(Type), ":", atom_to_list(ListenerName)])). + list_to_atom(lists:append([str(Type), ":", str(ListenerName)])). parse_listener_id(Id) -> try - [Zone, Listen] = string:split(atom_to_list(Id), ":", leading), - {list_to_existing_atom(Zone), list_to_existing_atom(Listen)} + [Type, Name] = string:split(str(Id), ":", leading), + {list_to_existing_atom(Type), list_to_atom(Name)} catch _ : _ -> error({invalid_listener_id, Id}) end. @@ -291,8 +291,8 @@ tcp_opts(Opts) -> foreach_listeners(Do) -> lists:foreach( - fun({ZoneName, LName, LConf}) -> - Do(ZoneName, LName, LConf) + fun({Type, LName, LConf}) -> + Do(Type, LName, LConf) end, do_list()). has_enabled_listener_conf_by_type(Type) -> @@ -307,3 +307,10 @@ apply_on_listener(ListenerId, Do) -> {not_found, _, _} -> error({listener_config_not_found, Type, ListenerName}); {ok, Conf} -> Do(Type, ListenerName, Conf) end. + +str(A) when is_atom(A) -> + atom_to_list(A); +str(B) when is_binary(B) -> + binary_to_list(B); +str(S) when is_list(S) -> + S. diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 02bb8662c..4a628d994 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -90,6 +90,8 @@ , list_listeners_by_id/1 , get_listener/2 , manage_listener/2 + , update_listener/2 + , update_listener/3 ]). %% Alarms @@ -473,7 +475,7 @@ list_listeners() -> lists:append([list_listeners(Node) || Node <- ekka_mnesia:running_nodes()]). list_listeners(Node) when Node =:= node() -> - [{Id, maps:put(node, Node, Conf)} || {Id, Conf} <- emqx_listeners:list()]; + [Conf#{node => Node, id => Id} || {Id, Conf} <- emqx_listeners:list()]; list_listeners(Node) -> rpc_call(Node, list_listeners, [Node]). @@ -501,6 +503,16 @@ manage_listener(Operation, #{id := ID, node := Node}) when Node =:= node()-> manage_listener(Operation, Param = #{node := Node}) -> rpc_call(Node, manage_listener, [Operation, Param]). +update_listener(Id, Config) -> + [update_listener(Node, Id, Config) || Node <- ekka_mnesia:running_nodes()]. + +update_listener(Node, Id, Config) when Node =:= node() -> + {Type, Name} = emqx_listeners:parse_listener_id(Id), + {ok, #{raw_config := RawConf}} = emqx:update_config([listeners, Type, Name], Config, #{}), + RawConf#{node => Node, id => Id, running => true}; +update_listener(Node, Id, Config) -> + rpc_call(Node, update_listener, [Node, Id, Config]). + %%-------------------------------------------------------------------- %% Get Alarms %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 4bad6a26b..f1749b1d9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -21,9 +21,9 @@ -export([api_spec/0]). -export([ list_listeners/2 - , list_listeners_by_id/2 + , list_update_listeners_by_id/2 , list_listeners_on_node/2 - , get_listener_by_id_on_node/2 + , get_update_listener_by_id_on_node/2 , manage_listeners/2 , jsonable_resp/2 ]). @@ -39,10 +39,10 @@ api_spec() -> { [ api_list_listeners(), - api_list_listeners_by_id(), + api_list_update_listeners_by_id(), api_manage_listeners(), api_list_listeners_on_node(), - api_get_listener_by_id_on_node(), + api_get_update_listener_by_id_on_node(), api_manage_listeners_on_node() ], [] @@ -75,7 +75,7 @@ api_list_listeners() -> emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/listeners", Metadata, list_listeners}. -api_list_listeners_by_id() -> +api_list_update_listeners_by_id() -> Metadata = #{ get => #{ description => <<"List listeners by a given Id from all nodes in the cluster">>, @@ -84,8 +84,18 @@ api_list_listeners_by_id() -> <<"404">> => emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, - {"/listeners/:id", Metadata, list_listeners_by_id}. + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}, + put => #{ + description => <<"Create or update listeners by a given Id to all nodes in the cluster">>, + parameters => [param_path_id()], + requestBody => emqx_mgmt_util:schema(req_schema(), <<"Listener Config">>), + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}} + }, + {"/listeners/:id", Metadata, list_update_listeners_by_id}. api_list_listeners_on_node() -> Metadata = #{ @@ -96,7 +106,7 @@ api_list_listeners_on_node() -> <<"200">> => emqx_mgmt_util:object_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. -api_get_listener_by_id_on_node() -> +api_get_update_listener_by_id_on_node() -> Metadata = #{ get => #{ description => <<"Get a listener by a given Id on a specific node">>, @@ -106,8 +116,19 @@ api_get_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}}, - {"/nodes/:node/listeners/:id", Metadata, get_listener_by_id_on_node}. + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + put => #{ + description => <<"Create or update a listener by a given Id on a specific node">>, + parameters => [param_path_node(), param_path_id()], + requestBody => emqx_mgmt_util:schema(req_schema(), <<"Listener Config">>), + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, + ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}} + }, + {"/nodes/:node/listeners/:id", Metadata, get_update_listener_by_id_on_node}. api_manage_listeners() -> Metadata = #{ @@ -169,14 +190,16 @@ param_path_operation()-> list_listeners(get, _Request) -> {200, format(emqx_mgmt:list_listeners())}. -list_listeners_by_id(get, #{bindings := #{id := Id}}) -> - case [L || L = {Id0, _Conf} <- emqx_mgmt:list_listeners(), +list_update_listeners_by_id(get, #{bindings := #{id := Id}}) -> + case [L || L = #{id := Id0} <- emqx_mgmt:list_listeners(), atom_to_binary(Id0, latin1) =:= Id] of [] -> {400, #{code => 'RESOURCE_NOT_FOUND', message => ?LISTENER_NOT_FOUND}}; Listeners -> {200, format(Listeners)} - end. + end; +list_update_listeners_by_id(put, #{bindings := #{id := Id}, body := Conf}) -> + return_listeners(emqx_mgmt:update_listener(Id, Conf)). list_listeners_on_node(get, #{bindings := #{node := Node}}) -> case emqx_mgmt:list_listeners(atom(Node)) of @@ -186,7 +209,7 @@ list_listeners_on_node(get, #{bindings := #{node := Node}}) -> {200, format(Listener)} end. -get_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> +get_update_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> case emqx_mgmt:get_listener(atom(Node), atom(Id)) of {error, not_found} -> {404, #{code => 'RESOURCE_NOT_FOUND', message => ?NODE_LISTENER_NOT_FOUND}}; @@ -194,7 +217,9 @@ get_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}}; Listener -> {200, format(Listener)} - end. + end; +get_update_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node, body := Conf}}) -> + return_listeners(emqx_mgmt:update_listener(atom(Node), Id, Conf)). manage_listeners(_, #{bindings := #{id := Id, operation := Oper, node := Node}}) -> {_, Result} = do_manage_listeners(Node, Id, Oper), @@ -236,6 +261,13 @@ do_manage_listeners2(<<"restart">>, Param) -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} end. +return_listeners(Listeners) -> + Results = format(Listeners), + case lists:filter(fun({error, _}) -> true; (_) -> false end, Results) of + [] -> {200, Results}; + Errors -> {500, #{code => 'UNKNOW_ERROR', message => manage_listeners_err(Errors)}} + end. + manage_listeners_err(Errors) -> list_to_binary(lists:foldl(fun({Node, Err}, Str) -> err_msg_str(#{node => Node, error => Err}) ++ "; " ++ Str @@ -247,12 +279,10 @@ format(Listeners) when is_list(Listeners) -> format({error, Reason}) -> {error, Reason}; -format({ID, Conf}) -> +format(#{node := _Node, id := _Id} = Conf) when is_map(Conf) -> emqx_map_lib:jsonable_map(Conf#{ - id => ID, - node => maps:get(node, Conf), - running => trans_running(Conf) - }, fun ?MODULE:jsonable_resp/2). + running => trans_running(Conf) + }, fun ?MODULE:jsonable_resp/2). trans_running(Conf) -> case maps:get(running, Conf) of From 7390d2bb366cf3a29053b801079c7232efbcae29 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 17:54:01 +0800 Subject: [PATCH 204/306] fix(listeners): test case emqx_mgmt_listeners_api_SUITE failed --- apps/emqx_management/src/emqx_mgmt.erl | 13 ++++++------- .../src/emqx_mgmt_api_listeners.erl | 2 +- .../test/emqx_mgmt_api_test_util.erl | 14 ++++++++++++-- .../test/emqx_mgmt_listeners_api_SUITE.erl | 10 ++++------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 4a628d994..eb4166675 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -480,20 +480,19 @@ list_listeners(Node) when Node =:= node() -> list_listeners(Node) -> rpc_call(Node, list_listeners, [Node]). -list_listeners_by_id(Identifier) -> - listener_id_filter(Identifier, list_listeners()). +list_listeners_by_id(Id) -> + listener_id_filter(Id, list_listeners()). -get_listener(Node, Identifier) -> - case listener_id_filter(Identifier, list_listeners(Node)) of +get_listener(Node, Id) -> + case listener_id_filter(Id, list_listeners(Node)) of [] -> {error, not_found}; [Listener] -> Listener end. -listener_id_filter(Identifier, Listeners) -> - Filter = - fun({Id, _}) -> Id =:= Identifier end, +listener_id_filter(Id, Listeners) -> + Filter = fun(#{id := Id0}) -> Id0 =:= Id end, lists:filter(Filter, Listeners). -spec manage_listener(Operation :: start_listener|stop_listener|restart_listener, Param :: map()) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index f1749b1d9..4b8e132e7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -132,7 +132,7 @@ api_get_update_listener_by_id_on_node() -> api_manage_listeners() -> Metadata = #{ - get => #{ + post => #{ description => <<"Restart listeners on all nodes in the cluster">>, parameters => [ param_path_id(), diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index 0babef05a..fa924a71c 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -52,13 +52,23 @@ request_api(Method, Url, Auth) -> request_api(Method, Url, QueryParams, Auth) -> request_api(Method, Url, QueryParams, Auth, []). -request_api(Method, Url, QueryParams, Auth, []) -> +request_api(Method, Url, QueryParams, Auth, []) + when (Method =:= options) orelse + (Method =:= get) orelse + (Method =:= put) orelse + (Method =:= head) orelse + (Method =:= delete) orelse + (Method =:= trace) -> NewUrl = case QueryParams of "" -> Url; _ -> Url ++ "?" ++ QueryParams end, do_request_api(Method, {NewUrl, [Auth]}); -request_api(Method, Url, QueryParams, Auth, Body) -> +request_api(Method, Url, QueryParams, Auth, Body) + when (Method =:= post) orelse + (Method =:= patch) orelse + (Method =:= put) orelse + (Method =:= delete) -> NewUrl = case QueryParams of "" -> Url; _ -> Url ++ "?" ++ QueryParams diff --git a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl index 10e1def26..0dd00b38e 100644 --- a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl @@ -58,8 +58,8 @@ t_manage_listener(_) -> manage_listener(ID, "restart", true). manage_listener(ID, Operation, Running) -> - Path = emqx_mgmt_api_test_util:api_path(["listeners", ID, Operation]), - {ok, _} = emqx_mgmt_api_test_util:request_api(get, Path), + Path = emqx_mgmt_api_test_util:api_path(["listeners", ID, "operation", Operation]), + {ok, _} = emqx_mgmt_api_test_util:request_api(post, Path), timer:sleep(500), GetPath = emqx_mgmt_api_test_util:api_path(["listeners", ID]), {ok, ListenersResponse} = emqx_mgmt_api_test_util:request_api(get, GetPath), @@ -106,10 +106,8 @@ comparison_listener(Local, Response) -> ?assertEqual(maps:get(id, Local), binary_to_atom(maps:get(<<"id">>, Response))), ?assertEqual(maps:get(node, Local), binary_to_atom(maps:get(<<"node">>, Response))), ?assertEqual(maps:get(acceptors, Local), maps:get(<<"acceptors">>, Response)), - ?assertEqual(maps:get(max_conn, Local), maps:get(<<"max_conn">>, Response)), - ?assertEqual(maps:get(listen_on, Local), maps:get(<<"listen_on">>, Response)), ?assertEqual(maps:get(running, Local), maps:get(<<"running">>, Response)). -listener_stats(Listener, Stats) -> - ?assertEqual(maps:get(<<"running">>, Listener), Stats). +listener_stats(Listener, ExpectedStats) -> + ?assertEqual(ExpectedStats, maps:get(<<"running">>, Listener)). From 50ccaec4b0a8e055ec0096494a211f580c5c5361 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:49:58 +0800 Subject: [PATCH 205/306] fix(emqx_schema): define bind as a mandatory config of listener --- apps/emqx/src/emqx_schema.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 7d1e39510..812e52f9b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -342,7 +342,7 @@ mqtt_listener() -> ]. base_listener() -> - [ {"bind", t(union(ip_port(), integer()))} + [ {"bind", hoconsc:t(union(ip_port(), integer()), #{nullable => false})} , {"acceptors", t(integer(), undefined, 16)} , {"max_connections", maybe_infinity(integer(), infinity)} , {"mountpoint", t(binary(), undefined, <<>>)} From 4da413c4530ed27d2314f51ffad857057f5599f0 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:51:30 +0800 Subject: [PATCH 206/306] fix(APIs): clarify the error message when update listener failed --- apps/emqx_management/src/emqx_mgmt.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index eb4166675..8216e2c3f 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -507,8 +507,12 @@ update_listener(Id, Config) -> update_listener(Node, Id, Config) when Node =:= node() -> {Type, Name} = emqx_listeners:parse_listener_id(Id), - {ok, #{raw_config := RawConf}} = emqx:update_config([listeners, Type, Name], Config, #{}), - RawConf#{node => Node, id => Id, running => true}; + case emqx:update_config([listeners, Type, Name], Config, #{}) of + {ok, #{raw_config := RawConf}} -> + RawConf#{node => Node, id => Id, running => true}; + {error, Reason} -> + error(Reason) + end; update_listener(Node, Id, Config) -> rpc_call(Node, update_listener, [Node, Id, Config]). From e6306bccd8cc92ca879be5c5ae960db757989837 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:53:02 +0800 Subject: [PATCH 207/306] feat(map_lib): add emqx_map_lib:diff_maps/2 --- apps/emqx/src/emqx_map_lib.erl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index de2b41b32..2ed25d22d 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -27,6 +27,7 @@ , jsonable_map/2 , binary_string/1 , deep_convert/3 + , diff_maps/2 ]). -export_type([config_key/0, config_key_path/0]). @@ -129,6 +130,27 @@ jsonable_map(Map) -> jsonable_map(Map, JsonableFun) -> deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). +-spec diff_maps(map(), map()) -> + #{added := [map()], identical := [map()], removed := [map()], + changed := [#{any() => {OldValue::any(), NewValue::any()}}]}. +diff_maps(NewMap, OldMap) -> + InitR = #{identical => [], changed => [], removed => []}, + {Result, RemInNew} = + lists:foldl(fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D}, + RemNewMap}) -> + Result1 = case maps:find(OldK, NewMap) of + error -> + Result0#{removed => [#{OldK => OldV} | D]}; + {ok, NewV} when NewV == OldV -> + Result0#{identical => [#{OldK => OldV} | I]}; + {ok, NewV} -> + Result0#{changed => [#{OldK => {OldV, NewV}} | U]} + end, + {Result1, maps:remove(OldK, RemNewMap)} + end, {InitR, NewMap}, maps:to_list(OldMap)), + Result#{added => RemInNew}. + + binary_string_kv(K, V, JsonableFun) -> case JsonableFun(K, V) of drop -> drop; From 0d1bc6d6895d7e72a6798551be7b9b0c78248f7e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:53:36 +0800 Subject: [PATCH 208/306] feat(listeners): add config handler for listeners --- apps/emqx/src/emqx_listeners.erl | 67 ++++++++++++++++++++++++++++---- apps/emqx/src/emqx_map_lib.erl | 12 +++--- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index d9670b858..399bd3d08 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -41,6 +41,10 @@ , parse_listener_id/1 ]). +-export([post_config_update/4]). + +-define(CONF_KEY_PATH, [listeners]). + %% @doc List configured listeners. -spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]). list() -> @@ -88,6 +92,9 @@ is_running(quic, _ListenerId, _Conf)-> %% @doc Start all listeners. -spec(start() -> ok). start() -> + %% The ?MODULE:start/0 will be called by emqx_app when emqx get started, + %% so we install the config handler here. + ok = emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), foreach_listeners(fun start_listener/3). -spec start_listener(atom()) -> ok | {error, term()}. @@ -102,7 +109,7 @@ start_listener(Type, ListenerName, #{bind := Bind} = Conf) -> console_print("- Skip - starting listener ~s on ~s ~n due to ~p", [listener_id(Type, ListenerName), format_addr(Bind), Reason]); {ok, _} -> - console_print("Start listener ~s on ~s successfully.~n", + console_print("Listener ~s on ~s started.~n", [listener_id(Type, ListenerName), format_addr(Bind)]); {error, {already_started, Pid}} -> {error, {already_started, Pid}}; @@ -122,27 +129,47 @@ restart_listener(ListenerId) -> apply_on_listener(ListenerId, fun restart_listener/3). -spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}). +restart_listener(Type, ListenerName, {OldConf, NewConf}) -> + restart_listener(Type, ListenerName, OldConf, NewConf); restart_listener(Type, ListenerName, Conf) -> - case stop_listener(Type, ListenerName, Conf) of - ok -> start_listener(Type, ListenerName, Conf); + restart_listener(Type, ListenerName, Conf, Conf). + +restart_listener(Type, ListenerName, OldConf, NewConf) -> + case stop_listener(Type, ListenerName, OldConf) of + ok -> start_listener(Type, ListenerName, NewConf); Error -> Error end. %% @doc Stop all listeners. -spec(stop() -> ok). stop() -> + %% The ?MODULE:stop/0 will be called by emqx_app when emqx is going to shutdown, + %% so we uninstall the config handler here. + _ = emqx_config_handler:remove_handler(?CONF_KEY_PATH), foreach_listeners(fun stop_listener/3). -spec(stop_listener(atom()) -> ok | {error, term()}). stop_listener(ListenerId) -> apply_on_listener(ListenerId, fun stop_listener/3). --spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). -stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl -> +stop_listener(Type, ListenerName, #{bind := Bind} = Conf) -> + case do_stop_listener(Type, ListenerName, Conf) of + ok -> + console_print("Listener ~s on ~s stopped.~n", + [listener_id(Type, ListenerName), format_addr(Bind)]), + ok; + {error, Reason} -> + ?ELOG("Failed to stop listener ~s on ~s: ~0p~n", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]), + {error, Reason} + end. + +-spec(do_stop_listener(atom(), atom(), map()) -> ok | {error, term()}). +do_stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl -> esockd:close(listener_id(Type, ListenerName), ListenOn); -stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss -> +do_stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss -> cowboy:stop_listener(listener_id(Type, ListenerName)); -stop_listener(quic, ListenerName, _Conf) -> +do_stop_listener(quic, ListenerName, _Conf) -> quicer:stop_listener(listener_id(quic, ListenerName)). -ifndef(TEST). @@ -201,6 +228,32 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> {ok, {skipped, quic_app_missing}} end. +%% Update the listeners at runtime +post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) -> + #{added := Added, removed := Removed, changed := Updated} + = diff_listeners(NewListeners, OldListeners), + perform_listener_changes(fun stop_listener/3, Removed), + perform_listener_changes(fun start_listener/3, Added), + perform_listener_changes(fun restart_listener/3, Updated). + +perform_listener_changes(Action, MapConfs) -> + lists:foreach(fun + ({Id, Conf}) -> + {Type, Name} = parse_listener_id(Id), + Action(Type, Name, Conf) + end, maps:to_list(MapConfs)). + +diff_listeners(NewListeners, OldListeners) -> + emqx_map_lib:diff_maps(flatten_listeners(NewListeners), flatten_listeners(OldListeners)). + +flatten_listeners(Conf0) -> + maps:from_list( + lists:append([do_flatten_listeners(Type, Conf) + || {Type, Conf} <- maps:to_list(Conf0)])). + +do_flatten_listeners(Type, Conf0) -> + [{listener_id(Type, Name), Conf} || {Name, Conf} <- maps:to_list(Conf0)]. + esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), Opts2 = case emqx_config:get_zone_conf(zone(Opts0), [rate_limit, max_conn_rate]) of diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 2ed25d22d..d5e851971 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -131,20 +131,20 @@ jsonable_map(Map, JsonableFun) -> deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). -spec diff_maps(map(), map()) -> - #{added := [map()], identical := [map()], removed := [map()], - changed := [#{any() => {OldValue::any(), NewValue::any()}}]}. + #{added := map(), identical := map(), removed := map(), + changed := #{any() => {OldValue::any(), NewValue::any()}}}. diff_maps(NewMap, OldMap) -> - InitR = #{identical => [], changed => [], removed => []}, + InitR = #{identical => #{}, changed => #{}, removed => #{}}, {Result, RemInNew} = lists:foldl(fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D}, RemNewMap}) -> Result1 = case maps:find(OldK, NewMap) of error -> - Result0#{removed => [#{OldK => OldV} | D]}; + Result0#{removed => D#{OldK => OldV}}; {ok, NewV} when NewV == OldV -> - Result0#{identical => [#{OldK => OldV} | I]}; + Result0#{identical => I#{OldK => OldV}}; {ok, NewV} -> - Result0#{changed => [#{OldK => {OldV, NewV}} | U]} + Result0#{changed => U#{OldK => {OldV, NewV}}} end, {Result1, maps:remove(OldK, RemNewMap)} end, {InitR, NewMap}, maps:to_list(OldMap)), From 0af39e88a4b33671c5fb289e70ce93d49f68959c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 31 Aug 2021 14:08:49 +0800 Subject: [PATCH 209/306] feat(listeners): add DELETE APIs for removing the listeners --- apps/emqx/src/emqx_config.erl | 4 +- apps/emqx/src/emqx_config_handler.erl | 11 ++-- apps/emqx_management/src/emqx_mgmt.erl | 15 ++++++ .../src/emqx_mgmt_api_listeners.erl | 53 ++++++++++++++----- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 2f5bc9551..ddedef024 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -94,8 +94,8 @@ -type update_stage() :: pre_config_update | post_config_update. -type update_error() :: {update_stage(), module(), term()} | {save_configs, term()} | term(). -type update_result() :: #{ - config := emqx_config:config(), - raw_config := emqx_config:raw_config(), + config => emqx_config:config(), + raw_config => emqx_config:raw_config(), post_config_update => #{module() => any()} }. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index b45f89538..a86efb2bc 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -217,10 +217,9 @@ call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, UpdateReq, Result) false -> {ok, Result} end. -save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, {_Cmd, Opts}) -> +save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs) -> case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of - ok -> {ok, #{config => emqx_config:get(ConfKeyPath), - raw_config => return_rawconf(ConfKeyPath, Opts)}}; + ok -> {ok, return_change_result(ConfKeyPath, UpdateArgs)}; {error, Reason} -> {error, {save_configs, Reason}} end. @@ -241,6 +240,12 @@ update_override_config(RawConf) -> up_req({remove, _Opts}) -> '$remove'; up_req({{update, Req}, _Opts}) -> Req. +return_change_result(ConfKeyPath, {{update, _Req}, Opts}) -> + #{config => emqx_config:get(ConfKeyPath), + raw_config => return_rawconf(ConfKeyPath, Opts)}; +return_change_result(_ConfKeyPath, {remove, _Opts}) -> + #{}. + return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) -> FullRawConf = emqx_config:fill_defaults(emqx_config:get_raw([])), emqx_map_lib:deep_get(bin_path(ConfKeyPath), FullRawConf); diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 8216e2c3f..3cc31b47f 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -92,6 +92,8 @@ , manage_listener/2 , update_listener/2 , update_listener/3 + , remove_listener/1 + , remove_listener/2 ]). %% Alarms @@ -516,6 +518,19 @@ update_listener(Node, Id, Config) when Node =:= node() -> update_listener(Node, Id, Config) -> rpc_call(Node, update_listener, [Node, Id, Config]). +remove_listener(Id) -> + [remove_listener(Node, Id) || Node <- ekka_mnesia:running_nodes()]. + +remove_listener(Node, Id) when Node =:= node() -> + {Type, Name} = emqx_listeners:parse_listener_id(Id), + case emqx:remove_config([listeners, Type, Name], #{}) of + {ok, _} -> ok; + {error, Reason} -> + error(Reason) + end; +remove_listener(Node, Id) -> + rpc_call(Node, remove_listener, [Node, Id]). + %%-------------------------------------------------------------------- %% Get Alarms %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 4b8e132e7..51487fb2a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -21,9 +21,9 @@ -export([api_spec/0]). -export([ list_listeners/2 - , list_update_listeners_by_id/2 + , crud_listeners_by_id/2 , list_listeners_on_node/2 - , get_update_listener_by_id_on_node/2 + , crud_listener_by_id_on_node/2 , manage_listeners/2 , jsonable_resp/2 ]). @@ -86,16 +86,24 @@ api_list_update_listeners_by_id() -> <<"200">> => emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}, put => #{ - description => <<"Create or update listeners by a given Id to all nodes in the cluster">>, + description => <<"Create or update a listener by a given Id to all nodes in the cluster">>, parameters => [param_path_id()], requestBody => emqx_mgmt_util:schema(req_schema(), <<"Listener Config">>), responses => #{ <<"404">> => emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}} + emqx_mgmt_util:array_schema(resp_schema(), <<"Create or update listener successfully">>)}}, + delete => #{ + description => <<"Delete a listener by a given Id to all nodes in the cluster">>, + parameters => [param_path_id()], + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:schema(<<"Delete listener successfully">>)}} }, - {"/listeners/:id", Metadata, list_update_listeners_by_id}. + {"/listeners/:id", Metadata, crud_listeners_by_id}. api_list_listeners_on_node() -> Metadata = #{ @@ -126,9 +134,17 @@ api_get_update_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}} + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + delete => #{ + description => <<"Delete a listener by a given Id to all nodes in the cluster">>, + parameters => [param_path_node(), param_path_id()], + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:schema(<<"Delete listener successfully">>)}} }, - {"/nodes/:node/listeners/:id", Metadata, get_update_listener_by_id_on_node}. + {"/nodes/:node/listeners/:id", Metadata, crud_listener_by_id_on_node}. api_manage_listeners() -> Metadata = #{ @@ -190,7 +206,7 @@ param_path_operation()-> list_listeners(get, _Request) -> {200, format(emqx_mgmt:list_listeners())}. -list_update_listeners_by_id(get, #{bindings := #{id := Id}}) -> +crud_listeners_by_id(get, #{bindings := #{id := Id}}) -> case [L || L = #{id := Id0} <- emqx_mgmt:list_listeners(), atom_to_binary(Id0, latin1) =:= Id] of [] -> @@ -198,8 +214,14 @@ list_update_listeners_by_id(get, #{bindings := #{id := Id}}) -> Listeners -> {200, format(Listeners)} end; -list_update_listeners_by_id(put, #{bindings := #{id := Id}, body := Conf}) -> - return_listeners(emqx_mgmt:update_listener(Id, Conf)). +crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Conf}) -> + return_listeners(emqx_mgmt:update_listener(Id, Conf)); +crud_listeners_by_id(delete, #{bindings := #{id := Id}}) -> + Results = emqx_mgmt:remove_listener(Id), + case lists:filter(fun({error, _}) -> true; (_) -> false end, Results) of + [] -> {200}; + Errors -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Errors)}} + end. list_listeners_on_node(get, #{bindings := #{node := Node}}) -> case emqx_mgmt:list_listeners(atom(Node)) of @@ -209,7 +231,7 @@ list_listeners_on_node(get, #{bindings := #{node := Node}}) -> {200, format(Listener)} end. -get_update_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> +crud_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> case emqx_mgmt:get_listener(atom(Node), atom(Id)) of {error, not_found} -> {404, #{code => 'RESOURCE_NOT_FOUND', message => ?NODE_LISTENER_NOT_FOUND}}; @@ -218,8 +240,13 @@ get_update_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) Listener -> {200, format(Listener)} end; -get_update_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node, body := Conf}}) -> - return_listeners(emqx_mgmt:update_listener(atom(Node), Id, Conf)). +crud_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node, body := Conf}}) -> + return_listeners(emqx_mgmt:update_listener(atom(Node), Id, Conf)); +crud_listener_by_id_on_node(delete, #{bindings := #{id := Id, node := Node}}) -> + case emqx_mgmt:remove_listener(atom(Node), Id) of + ok -> {200}; + {error, Reason} -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} + end. manage_listeners(_, #{bindings := #{id := Id, operation := Oper, node := Node}}) -> {_, Result} = do_manage_listeners(Node, Id, Oper), From 7e53469bb8c531b61e3c4d6a0aeae3e244c00fa5 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 31 Aug 2021 14:21:53 +0800 Subject: [PATCH 210/306] fix(listeners): update the testcases for listeners --- apps/emqx/src/emqx_config_handler.erl | 4 ++++ apps/emqx/test/emqx_listeners_SUITE.erl | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index a86efb2bc..f16f8a97a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -23,6 +23,7 @@ %% API functions -export([ start_link/0 + , stop/0 , add_handler/2 , remove_handler/1 , update_config/3 @@ -68,6 +69,9 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []). +stop() -> + gen_server:stop(?MODULE). + -spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) -> {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index a8760c7e8..a3bfb2d47 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -37,6 +37,14 @@ end_per_suite(_Config) -> application:stop(esockd), application:stop(cowboy). +init_per_testcase(_, Config) -> + {ok, _} = emqx_config_handler:start_link(), + Config. + +end_per_testcase(_, _Config) -> + _ = emqx_config_handler:stop(), + ok. + t_start_stop_listeners(_) -> ok = emqx_listeners:start(), ?assertException(error, _, emqx_listeners:start_listener({ws,{"127.0.0.1", 8083}, []})), From 4c468b383acd501963c39ee6ce818f177734853f Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:04:38 +0800 Subject: [PATCH 211/306] fix: delayed math string (#5609) * fix: delayed ms --- apps/emqx_modules/src/emqx_delayed.erl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index b773f04ac..0b1f00e14 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -52,6 +52,10 @@ -record(delayed_message, {key, delayed, msg}). +%% sync ms with record change +-define(QUERY_MS(Id), [{{delayed_message, {'_', Id}, '_', '_'}, [], ['$_']}]). +-define(DELETE_MS(Id), [{{delayed_message, {'$1', Id}, '_', '_'}, [], ['$1']}]). + -define(TAB, ?MODULE). -define(SERVER, ?MODULE). -define(MAX_INTERVAL, 4294967). @@ -161,8 +165,7 @@ to_rfc3339(Timestamp) -> get_delayed_message(Id0) -> Id = emqx_guid:from_hexstr(Id0), - Ms = [{{delayed_message,{'_',Id},'_'},[],['$_']}], - case ets:select(?TAB, Ms) of + case ets:select(?TAB, ?QUERY_MS(Id)) of [] -> {error, not_found}; Rows -> @@ -172,8 +175,7 @@ get_delayed_message(Id0) -> delete_delayed_message(Id0) -> Id = emqx_guid:from_hexstr(Id0), - Ms = [{{delayed_message, {'$1', Id}, '_'}, [], ['$1']}], - case ets:select(?TAB, Ms) of + case ets:select(?TAB, ?DELETE_MS(Id)) of [] -> {error, not_found}; Rows -> From 00d469976f16094ac7f716083f965b50ff17ecf8 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:04:52 +0800 Subject: [PATCH 212/306] fix: subscriptions api share param name (#5610) --- apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 62514e314..058d824ac 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -37,6 +37,7 @@ [ {<<"clientid">>, binary} , {<<"topic">>, binary} , {<<"share">>, binary} + , {<<"share_group">>, binary} , {<<"qos">>, integer} , {<<"match_topic">>, binary}]}). @@ -87,10 +88,10 @@ parameters() -> schema => #{type => integer, enum => [0, 1, 2]} }, #{ - name => share, + name => share_group, in => query, - description => <<"Shared subscription">>, - schema => #{type => boolean} + description => <<"Shared subscription group name">>, + schema => #{type => string} }, #{ name => topic, @@ -183,7 +184,7 @@ update_ms(clientid, X, {{Pid, Topic}, Opts}) -> {{Pid, Topic}, Opts#{subid => X}}; update_ms(topic, X, {{Pid, _Topic}, Opts}) -> {{Pid, X}, Opts}; -update_ms(share, X, {{Pid, Topic}, Opts}) -> +update_ms(share_group, X, {{Pid, Topic}, Opts}) -> {{Pid, Topic}, Opts#{share => X}}; update_ms(qos, X, {{Pid, Topic}, Opts}) -> {{Pid, Topic}, Opts#{qos => X}}. From 8d2b72c278416002d642371c264a2e6dd01a83d5 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:12:27 +0800 Subject: [PATCH 213/306] fix: alarms api return time (#5612) --- apps/emqx/src/emqx_alarm.erl | 6 ++++++ apps/emqx_management/src/emqx_mgmt_api_alarms.erl | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 11a2805f3..b43f5c52e 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -159,6 +159,7 @@ format(#activated_alarm{name = Name, message = Message, activate_at = At, detail name => Name, message => Message, duration => (Now - At) div 1000, %% to millisecond + activate_at => to_rfc3339(At), details => Details }; format(#deactivated_alarm{name = Name, message = Message, activate_at = At, details = Details, @@ -168,11 +169,16 @@ format(#deactivated_alarm{name = Name, message = Message, activate_at = At, deta name => Name, message => Message, duration => DAt - At, + activate_at => to_rfc3339(At), + deactivate_at => to_rfc3339(DAt), details => Details }; format(_) -> {error, unknow_alarm}. +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp div 1000, [{unit, millisecond}])). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 40956fd11..1adb5fce3 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -42,7 +42,9 @@ properties() -> {name, string, <<"Alarm name">>}, {message, string, <<"Alarm readable information">>}, {details, object}, - {duration, integer, <<"Alarms duration time; UNIX time stamp">>} + {duration, integer, <<"Alarms duration time; UNIX time stamp, millisecond">>}, + {activate_at, string, <<"Alarms activate time, RFC 3339">>}, + {deactivate_at, string, <<"Nullable, alarms deactivate time, RFC 3339">>} ]). alarms_api() -> From 560f415964c28fe9cf7ef35f9b980b794c49153c Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:27:08 +0800 Subject: [PATCH 214/306] fix: auto sub api doc & null body check (#5613) * fix: auto sub api doc & null body check --- apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl index d55444dba..7eeef52ff 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -23,6 +23,7 @@ -export([auto_subscribe/2]). -define(EXCEED_LIMIT, 'EXCEED_LIMIT'). +-define(BAD_REQUEST, 'BAD_REQUEST'). api_spec() -> {[auto_subscribe_api()], []}. @@ -43,6 +44,8 @@ auto_subscribe_api() -> 'requestBody' => schema(), responses => #{ <<"200">> => schema(), + <<"400">> => emqx_mgmt_util:error_schema( + <<"Request body required">>, [?BAD_REQUEST]), <<"409">> => emqx_mgmt_util:error_schema( <<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}} }, @@ -53,6 +56,8 @@ auto_subscribe_api() -> auto_subscribe(get, _) -> {200, emqx_auto_subscribe:list()}; +auto_subscribe(put, #{body := #{}}) -> + {400, #{code => ?BAD_REQUEST, message => <<"Request body required">>}}; auto_subscribe(put, #{body := Params}) -> case emqx_auto_subscribe:update(Params) of {error, quota_exceeded} -> From 7075f3260e4cc86a774d3afea6cc649797104372 Mon Sep 17 00:00:00 2001 From: lafirest Date: Tue, 24 Aug 2021 17:12:09 +0800 Subject: [PATCH 215/306] refactor(emqx_lwm2m): port lwm2m into emqx_gateway framework --- apps/emqx_gateway/etc/emqx_gateway.conf | 32 +- apps/emqx_gateway/src/coap/README.md | 31 + apps/emqx_gateway/src/coap/doc/flow.png | Bin 111145 -> 76789 bytes .../src/coap/emqx_coap_channel.erl | 316 ++--- .../emqx_gateway/src/coap/emqx_coap_frame.erl | 48 +- .../src/coap/emqx_coap_medium.erl | 107 ++ .../src/coap/emqx_coap_message.erl | 93 +- .../src/coap/emqx_coap_resource.erl | 37 - .../src/coap/emqx_coap_session.erl | 155 +- apps/emqx_gateway/src/coap/emqx_coap_tm.erl | 341 +++-- .../src/coap/emqx_coap_transport.erl | 178 ++- .../coap/handler/emqx_coap_mqtt_handler.erl | 21 +- .../coap/handler/emqx_coap_pubsub_handler.erl | 80 +- .../src/coap/include/emqx_coap.hrl | 17 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 35 +- .../emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl | 105 +- .../src/lwm2m/emqx_lwm2m_channel.erl | 459 ++++++ apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl | 153 -- .../emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl | 410 ++++++ .../src/lwm2m/emqx_lwm2m_cmd_handler.erl | 310 ---- .../src/lwm2m/emqx_lwm2m_coap_resource.erl | 386 ----- .../src/lwm2m/emqx_lwm2m_impl.erl | 55 +- .../src/lwm2m/emqx_lwm2m_json.erl | 351 ----- .../src/lwm2m/emqx_lwm2m_protocol.erl | 560 -------- .../src/lwm2m/emqx_lwm2m_session.erl | 773 ++++++++++ .../src/lwm2m/emqx_lwm2m_timer.erl | 47 - .../src/lwm2m/emqx_lwm2m_xml_object.erl | 6 +- .../src/lwm2m/emqx_lwm2m_xml_object_db.erl | 34 +- .../src/lwm2m/include/emqx_lwm2m.hrl | 11 +- apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 1263 +++++++++-------- 30 files changed, 3243 insertions(+), 3171 deletions(-) create mode 100644 apps/emqx_gateway/src/coap/emqx_coap_medium.erl delete mode 100644 apps/emqx_gateway/src/coap/emqx_coap_resource.erl create mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl create mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl create mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 206c54b93..5134246cd 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -134,7 +134,7 @@ gateway.lwm2m { enable_stats = true ## When publishing or subscribing, prefix all topics with a mountpoint string. - mountpoint = "lwm2m/%e/" + mountpoint = "lwm2m" xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" @@ -146,12 +146,32 @@ gateway.lwm2m { ## always | contains_object_list update_msg_publish_condition = contains_object_list + translators { - command = "dn/#" - response = "up/resp" - notify = "up/notify" - register = "up/resp" - update = "up/resp" + command { + topic = "dn/#" + qos = 0 + } + + response { + topic = "up/resp" + qos = 0 + } + + notify { + topic = "up/notify" + qos = 0 + } + + register { + topic = "up/resp" + qos = 0 + } + + update { + topic = "up/resp" + qos = 0 + } } listeners.udp.default { diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index 12b5ac5b7..88f657537 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -9,6 +9,7 @@ 4. [Query String](#org9a6b996) 2. [Implementation](#org9985dfe) 1. [Request/Response flow](#orge94210c) + 3. [Example](#ref_example) @@ -401,3 +402,33 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. + + + +## Example +1. Create Connection +``` +coap-client -m post -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public" +``` +Server will return token **X** in payload + +2. Update Connection +``` +coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" +``` + +3. Publish +``` +coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +``` +if you want to publish with auth, you must first establish a connection, and then post publish request on the same socket, so libcoap client can't simulation publish with a token + +4. Subscribe +``` +coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +``` + +5. Close Connection +``` +coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" +``` \ No newline at end of file diff --git a/apps/emqx_gateway/src/coap/doc/flow.png b/apps/emqx_gateway/src/coap/doc/flow.png index 5c72883487b21f467864698eaeca248bfbefd839..bb9b775a5f14ce2319605f3888685ece2b62fa8b 100644 GIT binary patch literal 76789 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 literal 111145 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 z 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 diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 510432441..24f06549b 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -22,17 +22,10 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). %% API --export([]). - -export([ info/1 , info/2 , stats/1 - , validator/3 - , get_clientinfo/1 - , get_config/2 - , get_config/3 - , result_keys/0 - , transfer_result/3]). + , validator/4]). -export([ init/2 , handle_in/2 @@ -61,20 +54,17 @@ keepalive :: emqx_keepalive:keepalive() | undefined, %% Timer timers :: #{atom() => disable | undefined | reference()}, - token :: binary() | undefined, - config :: hocon:config() + + conn_state :: idle | connected, + + token :: binary() | undefined }). -%% the execuate context for session call --record(exec_ctx, { config :: hocon:config(), - ctx :: emqx_gateway_ctx:context(), - clientinfo :: emqx_types:clientinfo() - }). - -type channel() :: #channel{}. --define(DISCONNECT_WAIT_TIME, timer:seconds(10)). +-define(TOKEN_MAXIMUM, 4294967295). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). +-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- @@ -87,8 +77,8 @@ info(Keys, Channel) when is_list(Keys) -> info(conninfo, #channel{conninfo = ConnInfo}) -> ConnInfo; -info(conn_state, _) -> - connected; +info(conn_state, #channel{conn_state = CState}) -> + CState; info(clientinfo, #channel{clientinfo = ClientInfo}) -> ClientInfo; info(session, #channel{session = Session}) -> @@ -106,18 +96,13 @@ init(ConnInfo = #{peername := {PeerHost, _}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), Mountpoint = maps:get(mountpoint, Config, undefined), - EnableAuth = is_authentication_enabled(Config), ClientInfo = set_peercert_infos( Peercert, #{ zone => default , protocol => 'coap' , peerhost => PeerHost , sockport => SockPort - , clientid => if EnableAuth -> - undefined; - true -> - emqx_guid:to_base62(emqx_guid:gen()) - end + , clientid => emqx_guid:to_base62(emqx_guid:gen()) , username => undefined , is_bridge => false , is_superuser => false @@ -125,56 +110,29 @@ init(ConnInfo = #{peername := {PeerHost, _}, } ), + Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), #channel{ ctx = Ctx , conninfo = ConnInfo , clientinfo = ClientInfo , timers = #{} - , config = Config , session = emqx_coap_session:new() - , keepalive = emqx_keepalive:init(maps:get(heartbeat, Config)) + , keepalive = emqx_keepalive:init(Heartbeat) + , conn_state = idle }. -is_authentication_enabled(Cfg) -> - case maps:get(authentication, Cfg, #{enable => false}) of - AuthCfg when is_map(AuthCfg) -> - maps:get(enable, AuthCfg, true); - _ -> false - end. - -validator(Type, Topic, #exec_ctx{ctx = Ctx, - clientinfo = ClientInfo}) -> +validator(Type, Topic, Ctx, ClientInfo) -> emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). -get_clientinfo(#exec_ctx{clientinfo = ClientInfo}) -> - ClientInfo. - -get_config(Key, Ctx) -> - get_config(Key, Ctx, undefined). - -get_config(Key, #exec_ctx{config = Cfg}, Def) -> - maps:get(Key, Cfg, Def). - -result_keys() -> - [out, connection]. - -transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT(From, Value, Result). - %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- handle_in(Msg, ChannleT) -> Channel = ensure_keepalive_timer(ChannleT), - case convert_queries(Msg) of - {ok, Msg2} -> - case emqx_coap_message:is_request(Msg2) of - true -> - check_auth_state(Msg2, Channel); - _ -> - call_session(handle_response, Msg2, Channel) - end; + case emqx_coap_message:is_request(Msg) of + true -> + check_auth_state(Msg, Channel); _ -> - response({error, bad_request}, <<"bad uri_query">>, Msg, Channel) + call_session(handle_response, Msg, Channel) end. %%-------------------------------------------------------------------- @@ -258,94 +216,57 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> ensure_keepalive_timer(Channel) -> ensure_keepalive_timer(fun ensure_timer/4, Channel). -ensure_keepalive_timer(Fun, #channel{config = Cfg} = Channel) -> - Interval = maps:get(heartbeat, Cfg), - Fun(keepalive, Interval, keepalive, Channel). +ensure_keepalive_timer(Fun, Channel) -> + Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), + Fun(keepalive, Heartbeat, keepalive, Channel). -call_session(Fun, - Msg, - #channel{session = Session} = Channel) -> - Ctx = new_exec_ctx(Channel), - Result = erlang:apply(emqx_coap_session, Fun, [Msg, Ctx, Session]), - process_result([session, connection, out], Result, Msg, Channel). - -process_result([Key | T], Result, Msg, Channel) -> - case handle_result(Key, Result, Msg, Channel) of - {ok, Channel2} -> - process_result(T, Result, Msg, Channel2); - Other -> - Other - end; - -process_result(_, _, _, Channel) -> - {ok, Channel}. - -handle_result(session, #{session := Session}, _, Channel) -> - {ok, Channel#channel{session = Session}}; - -handle_result(connection, #{connection := open}, Msg, Channel) -> - do_connect(Msg, Channel); - -handle_result(connection, #{connection := close}, Msg, Channel) -> - Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), - {shutdown, close, {outgoing, Reply}, Channel}; - -handle_result(out, #{out := Out}, _, Channel) -> - {ok, {outgoing, Out}, Channel}; - -handle_result(_, _, _, Channel) -> - {ok, Channel}. - -check_auth_state(Msg, #channel{config = Cfg} = Channel) -> - Enable = is_authentication_enabled(Cfg), +check_auth_state(Msg, Channel) -> + Enable = emqx:get_config([gateway, coap, enable_stats]), check_token(Enable, Msg, Channel). check_token(true, - #coap_message{options = Options} = Msg, + Msg, #channel{token = Token, - clientinfo = ClientInfo} = Channel) -> + clientinfo = ClientInfo, + conn_state = CState} = Channel) -> #{clientid := ClientId} = ClientInfo, - case maps:get(uri_query, Options, undefined) of + case emqx_coap_message:get_option(uri_query, Msg) of #{<<"clientid">> := ClientId, <<"token">> := Token} -> call_session(handle_request, Msg, Channel); #{<<"clientid">> := DesireId} -> - try_takeover(ClientId, DesireId, Msg, Channel); + try_takeover(CState, DesireId, Msg, Channel); _ -> - response({error, unauthorized}, Msg, Channel) + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Msg} end; -check_token(false, - #coap_message{options = Options} = Msg, - Channel) -> - case maps:get(uri_query, Options, undefined) of +check_token(false, Msg, Channel) -> + case emqx_coap_message:get_option(uri_query, Msg) of #{<<"clientid">> := _} -> - response({error, unauthorized}, Msg, Channel); + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Msg}; #{<<"token">> := _} -> - response({error, unauthorized}, Msg, Channel); + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Msg}; _ -> call_session(handle_request, Msg, Channel) end. -response(Method, Req, Channel) -> - response(Method, <<>>, Req, Channel). - -response(Method, Payload, Req, Channel) -> - Reply = emqx_coap_message:piggyback(Method, Payload, Req), - call_session(handle_out, Reply, Channel). - -try_takeover(undefined, - DesireId, - #coap_message{options = Opts} = Msg, - Channel) -> - case maps:get(uri_path, Opts, []) of - [<<"mqtt">>, <<"connection">> | _] -> +try_takeover(idle, DesireId, Msg, Channel) -> + case emqx_coap_message:get_option(uri_path, Msg, []) of + [<<"mqtt">>, <<"connection">> | _] -> %% may be is a connect request %% TODO need check repeat connect, unless we implement the %% udp connection baseon the clientid call_session(handle_request, Msg, Channel); _ -> - do_takeover(DesireId, Msg, Channel) + case emqx:get_config([gateway, coap, authentication], undefined) of + undefined -> + call_session(handle_request, Msg, Channel); + _ -> + do_takeover(DesireId, Msg, Channel) + end end; try_takeover(_, DesireId, Msg, Channel) -> @@ -354,31 +275,7 @@ try_takeover(_, DesireId, Msg, Channel) -> do_takeover(_DesireId, Msg, Channel) -> %% TODO completed the takeover, now only reset the message Reset = emqx_coap_message:reset(Msg), - call_session(handle_out, Reset, Channel). - -new_exec_ctx(#channel{config = Cfg, - ctx = Ctx, - clientinfo = ClientInfo}) -> - #exec_ctx{config = Cfg, - ctx = Ctx, - clientinfo = ClientInfo}. - -do_connect(#coap_message{options = Opts} = Req, Channel) -> - Queries = maps:get(uri_query, Opts), - case emqx_misc:pipeline( - [ fun run_conn_hooks/2 - , fun enrich_clientinfo/2 - , fun set_log_meta/2 - , fun auth_connect/2 - ], - {Queries, Req}, - Channel) of - {ok, _Input, NChannel} -> - process_connect(ensure_connected(NChannel), Req); - {error, ReasonCode, NChannel} -> - ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), - response({error, bad_request}, ErrMsg, Req, NChannel) - end. + {ok, {outgoing, Reset}, Channel}. run_conn_hooks(Input, Channel = #channel{ctx = Ctx, conninfo = ConnInfo}) -> @@ -439,11 +336,11 @@ ensure_connected(Channel = #channel{ctx = Ctx, ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), Channel#channel{conninfo = NConnInfo}. -process_connect(Channel = #channel{ctx = Ctx, - session = Session, - conninfo = ConnInfo, - clientinfo = ClientInfo}, - Msg) -> +process_connect(#channel{ctx = Ctx, + session = Session, + conninfo = ConnInfo, + clientinfo = ClientInfo} = Channel, + Msg, Result, Iter) -> %% inherit the old session SessFun = fun(_,_) -> Session end, case emqx_gateway_ctx:open_session( @@ -455,10 +352,14 @@ process_connect(Channel = #channel{ctx = Ctx, emqx_coap_session ) of {ok, _Sess} -> - response({ok, created}, <<"connected">>, Msg, Channel); + RandVal = rand:uniform(?TOKEN_MAXIMUM), + Token = erlang:list_to_binary(erlang:integer_to_list(RandVal)), + iter(Iter, + reply({ok, created}, Token, Msg, Result), + Channel#channel{token = Token}); {error, Reason} -> ?LOG(error, "Failed to open session du to ~p", [Reason]), - response({error, bad_request}, Msg, Channel) + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) end. run_hooks(Ctx, Name, Args) -> @@ -469,20 +370,93 @@ run_hooks(Ctx, Name, Args, Acc) -> emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_hooks:run_fold(Name, Args, Acc). -convert_queries(#coap_message{options = Opts} = Msg) -> - case maps:get(uri_query, Opts, undefined) of - undefined -> - {ok, Msg#coap_message{options = Opts#{uri_query => #{}}}}; - Queries -> - convert_queries(Queries, #{}, Msg) - end. +%%-------------------------------------------------------------------- +%% Call Chain +%%-------------------------------------------------------------------- +call_session(Fun, + Msg, + #channel{session = Session} = Channel) -> + iter([ session, fun process_session/4 + , proto, fun process_protocol/4 + , reply, fun process_reply/4 + , out, fun process_out/4 + , fun process_nothing/3 + ], + emqx_coap_session:Fun(Msg, Session), + Channel). -convert_queries([H | T], Queries, Msg) -> - case re:split(H, "=") of - [Key, Val] -> - convert_queries(T, Queries#{Key => Val}, Msg); - _ -> - error +call_handler(request, Msg, Result, + #channel{ctx = Ctx, + clientinfo = ClientInfo} = Channel, Iter) -> + HandlerResult = + case emqx_coap_message:get_option(uri_path, Msg) of + [<<"ps">> | RestPath] -> + emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx, ClientInfo); + [<<"mqtt">> | RestPath] -> + emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx, ClientInfo); + _ -> + reply({error, bad_request}, Msg) + end, + iter([ connection, fun process_connection/4 + , subscribe, fun process_subscribe/4 | Iter], + maps:merge(Result, HandlerResult), + Channel); + +call_handler(_, _, Result, Channel, Iter) -> + iter(Iter, Result, Channel). + +process_session(Session, Result, Channel, Iter) -> + iter(Iter, Result, Channel#channel{session = Session}). + +process_protocol({Type, Msg}, Result, Channel, Iter) -> + call_handler(Type, Msg, Result, Channel, Iter). + +%% leaf node +process_out(Outs, Result, Channel, _) -> + Outs2 = lists:reverse(Outs), + Outs3 = case maps:get(reply, Result, undefined) of + undefined -> + Outs2; + Reply -> + [Reply | Outs2] + end, + {ok, {outgoing, Outs3}, Channel}. + +%% leaf node +process_nothing(_, _, Channel) -> + {ok, Channel}. + +process_connection({open, Req}, Result, Channel, Iter) -> + Queries = emqx_coap_message:get_option(uri_query, Req), + case emqx_misc:pipeline( + [ fun run_conn_hooks/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + , fun auth_connect/2 + ], + {Queries, Req}, + Channel) of + {ok, _Input, NChannel} -> + process_connect(ensure_connected(NChannel), Req, Result, Iter); + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + Payload = erlang:list_to_binary(lists:flatten(ErrMsg)), + iter(Iter, + reply({error, bad_request}, Payload, Req, Result), + NChannel) end; -convert_queries([], Queries, #coap_message{options = Opts} = Msg) -> - {ok, Msg#coap_message{options = Opts#{uri_query => Queries}}}. + +process_connection({close, Msg}, _, Channel, _) -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, Reply, Channel}. + +process_subscribe({Sub, Msg}, Result, #channel{session = Session} = Channel, Iter) -> + Result2 = emqx_coap_session:process_subscribe(Sub, Msg, Result, Session), + iter([session, fun process_session/4 | Iter], Result2, Channel). + +%% leaf node +process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> + Session2 = emqx_coap_session:set_reply(Reply, Session), + Outs = maps:get(out, Result, []), + Outs2 = lists:reverse(Outs), + {ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl index c1bc08928..4d12997a7 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl @@ -103,11 +103,7 @@ flatten_options([{OptId, OptVal} | T], Acc) -> false -> [encode_option(OptId, OptVal) | Acc]; _ -> - lists:foldl(fun(undefined, InnerAcc) -> - InnerAcc; - (E, InnerAcc) -> - [encode_option(OptId, E) | InnerAcc] - end, Acc, OptVal) + try_encode_repeatable(OptId, OptVal) ++ Acc end); flatten_options([], Acc) -> @@ -141,6 +137,19 @@ encode_option_list([], _LastNum, Acc, <<>>) -> encode_option_list([], _, Acc, Payload) -> <>. +try_encode_repeatable(uri_query, Val) when is_map(Val) -> + maps:fold(fun(K, V, Acc) -> + [encode_option(uri_query, <>) | Acc] + end, + [], Val); + +try_encode_repeatable(K, Val) -> + lists:foldr(fun(undefined, Acc) -> + Acc; + (E, Acc) -> + [encode_option(K, E) | Acc] + end, [], Val). + %% RFC 7252 encode_option(if_match, OptVal) -> {?OPTION_IF_MATCH, OptVal}; encode_option(uri_host, OptVal) -> {?OPTION_URI_HOST, OptVal}; @@ -188,6 +197,8 @@ content_format_to_code(<<"application/octet-stream">>) -> 42; content_format_to_code(<<"application/exi">>) -> 47; content_format_to_code(<<"application/json">>) -> 50; content_format_to_code(<<"application/cbor">>) -> 60; +content_format_to_code(<<"application/vnd.oma.lwm2m+tlv">>) -> 11542; +content_format_to_code(<<"application/vnd.oma.lwm2m+json">>) -> 11543; content_format_to_code(_) -> 42. %% use octet-stream as default method_to_class_code(get) -> {0, 01}; @@ -235,12 +246,7 @@ parse(< {Options, Payload} = decode_option_list(Tail), Options2 = maps:fold(fun(K, V, Acc) -> - case is_repeatable_option(K) of - true -> - Acc#{K => lists:reverse(V)}; - _ -> - Acc#{K => V} - end + Acc#{K => get_option_val(K, V)} end, #{}, Options), @@ -255,6 +261,24 @@ parse(<>, ParseState}. +get_option_val(uri_query, V) -> + KVList = lists:foldl(fun(E, Acc) -> + [Key, Val] = re:split(E, "="), + [{Key, Val} | Acc] + + end, + [], + V), + maps:from_list(KVList); + +get_option_val(K, V) -> + case is_repeatable_option(K) of + true -> + lists:reverse(V); + _ -> + V + end. + -spec decode_type(X) -> message_type() when X :: 0 .. 3. decode_type(0) -> con; @@ -359,6 +383,8 @@ content_code_to_format(42) -> <<"application/octet-stream">>; content_code_to_format(47) -> <<"application/exi">>; content_code_to_format(50) -> <<"application/json">>; content_code_to_format(60) -> <<"application/cbor">>; +content_code_to_format(11542) -> <<"application/vnd.oma.lwm2m+tlv">>; +content_code_to_format(11543) -> <<"application/vnd.oma.lwm2m+json">>; content_code_to_format(_) -> <<"application/octet-stream">>. %% use octet as default %% RFC 7252 diff --git a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl new file mode 100644 index 000000000..ae5763179 --- /dev/null +++ b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl @@ -0,0 +1,107 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +%% Simplified semi-automatic CPS mode tree for coap +%% The tree must have a terminal leaf node, and it's return is the result of the entire tree. +%% This module currently only supports simple linear operation + +-module(emqx_coap_medium). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). + +%% API +-export([ empty/0, reset/1, reset/2 + , out/1, out/2, proto_out/1 + , proto_out/2, iter/3, iter/4 + , reply/2, reply/3, reply/4]). + +%%-type result() :: map() | empty. +-define(DEFINE_DEF(Name), Name(Msg) -> Name(Msg, #{})). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +empty() -> #{}. + +?DEFINE_DEF(reset). + +reset(Msg, Result) -> + out(emqx_coap_message:reset(Msg), Result). + +out(Msg) -> + #{out => [Msg]}. + +out(Msg, #{out := Outs} = Result) -> + Result#{out := [Msg | Outs]}; + +out(Msg, Result) -> + Result#{out => [Msg]}. + +?DEFINE_DEF(proto_out). + +proto_out(Proto, Resut) -> + Resut#{proto => Proto}. + +reply(Method, Req) when not is_record(Method, coap_message) -> + reply(Method, <<>>, Req); + +reply(Reply, Result) -> + Result#{reply => Reply}. + +reply(Method, Req, Result) when is_record(Req, coap_message) -> + reply(Method, <<>>, Req, Result); + +reply(Method, Payload, Req) -> + reply(Method, Payload, Req, #{}). + +reply(Method, Payload, Req, Result) -> + Result#{reply => emqx_coap_message:piggyback(Method, Payload, Req)}. + +%% run a tree +iter([Key, Fun | T], Input, State) -> + case maps:get(Key, Input, undefined) of + undefined -> + iter(T, Input, State); + Val -> + Fun(Val, maps:remove(Key, Input), State, T) + %% reserved + %% if is_function(Fun) -> + %% Fun(Val, maps:remove(Key, Input), State, T); + %% true -> + %% %% switch to sub branch + %% [FunH | FunT] = Fun, + %% FunH(Val, maps:remove(Key, Input), State, FunT) + %% end + end; + +%% terminal node +iter([Fun], Input, State) -> + Fun(undefined, Input, State). + +%% run a tree with argument +iter([Key, Fun | T], Input, Arg, State) -> + case maps:get(Key, Input, undefined) of + undefined -> + iter(T, Input, Arg, State); + Val -> + Fun(Val, maps:remove(Key, Input), Arg, State, T) + end; + +iter([Fun], Input, Arg, State) -> + Fun(undefined, Input, Arg, State). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_message.erl b/apps/emqx_gateway/src/coap/emqx_coap_message.erl index 2e9fb144e..3851b3428 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_message.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_message.erl @@ -31,7 +31,8 @@ -export([is_request/1]). --export([set/3, set_payload/2, get_content/1, set_content/2, set_content/3, get_option/2]). +-export([ set/3, set_payload/2, get_option/2 + , get_option/3, set_payload_block/3, set_payload_block/4]). -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). @@ -42,11 +43,10 @@ request(Type, Method, Payload) -> request(Type, Method, Payload, []). request(Type, Method, Payload, Options) when is_binary(Payload) -> - #coap_message{type = Type, method = Method, payload = Payload, options = Options}; - -request(Type, Method, Content=#coap_content{}, Options) -> - set_content(Content, - #coap_message{type = Type, method = Method, options = Options}). + #coap_message{type = Type, + method = Method, + payload = Payload, + options = to_options(Options)}. ack(#coap_message{id = Id}) -> #coap_message{type = ack, id = Id}. @@ -55,20 +55,20 @@ reset(#coap_message{id = Id}) -> #coap_message{type = reset, id = Id}. %% just make a response -response(#coap_message{type = Type, - id = Id, - token = Token}) -> - #coap_message{type = Type, - id = Id, - token = Token}. +response(Request) -> + response(undefined, Request). response(Method, Request) -> - set_method(Method, response(Request)). + response(Method, <<>>, Request). -response(Method, Payload, Request) -> - set_method(Method, - set_payload(Payload, - response(Request))). +response(Method, Payload, #coap_message{type = Type, + id = Id, + token = Token}) -> + #coap_message{type = Type, + id = Id, + token = Token, + method = Method, + payload = Payload}. %% make a response which maybe is a piggyback ack piggyback(Method, Request) -> @@ -90,14 +90,11 @@ set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg; set(Option, Value, Msg = #coap_message{options = Options}) -> Msg#coap_message{options = Options#{Option => Value}}. -get_option(Option, #coap_message{options = Options}) -> - maps:get(Option, Options, undefined). +get_option(Option, Msg) -> + get_option(Option, Msg, undefined). -set_method(Method, Msg) -> - Msg#coap_message{method = Method}. - -set_payload(Payload = #coap_content{}, Msg) -> - set_content(Payload, undefined, Msg); +get_option(Option, #coap_message{options = Options}, Def) -> + maps:get(Option, Options, Def). set_payload(Payload, Msg) when is_binary(Payload) -> Msg#coap_message{payload = Payload}; @@ -105,49 +102,6 @@ set_payload(Payload, Msg) when is_binary(Payload) -> set_payload(Payload, Msg) when is_list(Payload) -> Msg#coap_message{payload = list_to_binary(Payload)}. -get_content(#coap_message{options = Options, payload = Payload}) -> - #coap_content{etag = maps:get(etag, Options, undefined), - max_age = maps:get(max_age, Options, ?DEFAULT_MAX_AGE), - format = maps:get(content_format, Options, undefined), - location_path = maps:get(location_path, Options, []), - payload = Payload}. - -set_content(Content, Msg) -> - set_content(Content, undefined, Msg). - -%% segmentation not requested and not required -set_content(#coap_content{etag = ETag, - max_age = MaxAge, - format = Format, - location_path = LocPath, - payload = Payload}, - undefined, - Msg) - when byte_size(Payload) =< ?MAX_BLOCK_SIZE -> - #coap_message{options = Options} = Msg2 = set_payload(Payload, Msg), - Options2 = Options#{etag => [ETag], - max_age => MaxAge, - content_format => Format, - location_path => LocPath}, - Msg2#coap_message{options = Options2}; - -%% segmentation not requested, but required (late negotiation) -set_content(Content, undefined, Msg) -> - set_content(Content, {0, true, ?MAX_BLOCK_SIZE}, Msg); - -%% segmentation requested (early negotiation) -set_content(#coap_content{etag = ETag, - max_age = MaxAge, - format = Format, - payload = Payload}, - Block, - Msg) -> - #coap_message{options = Options} = Msg2 = set_payload_block(Payload, Block, Msg), - Options2 = Options#{etag => [ETag], - max => MaxAge, - content_format => Format}, - Msg2#coap_message{options = Options2}. - set_payload_block(Content, Block, Msg = #coap_message{method = Method}) when is_atom(Method) -> set_payload_block(Content, block1, Block, Msg); @@ -172,3 +126,8 @@ is_request(#coap_message{method = Method}) when is_atom(Method) -> is_request(_) -> false. + +to_options(Opts) when is_map(Opts) -> + Opts; +to_options(Opts) -> + maps:from_list(Opts). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_resource.erl b/apps/emqx_gateway/src/coap/emqx_coap_resource.erl deleted file mode 100644 index 93fe82aba..000000000 --- a/apps/emqx_gateway/src/coap/emqx_coap_resource.erl +++ /dev/null @@ -1,37 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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_coap_resource). - --include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). - --type context() :: any(). --type topic() :: binary(). --type token() :: token(). - --type register() :: {topic(), token()} - | topic() - | undefined. - --type result() :: emqx_coap_message() - | {has_sub, emqx_coap_message(), register()}. - --callback init(hocon:confg()) -> context(). --callback stop(context()) -> ok. --callback get(emqx_coap_message(), hocon:config()) -> result(). --callback put(emqx_coap_message(), hocon:config()) -> result(). --callback post(emqx_coap_message(), hocon:config()) -> result(). --callback delete(emqx_coap_message(), hocon:config()) -> result(). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index 50e91797b..b7e6c53f4 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -21,24 +21,25 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). %% API --export([new/0, transfer_result/3]). +-export([ new/0 + , process_subscribe/4]). -export([ info/1 , info/2 , stats/1 ]). --export([ handle_request/3 - , handle_response/3 - , handle_out/3 - , deliver/3 - , timeout/3]). +-export([ handle_request/2 + , handle_response/2 + , handle_out/2 + , set_reply/2 + , deliver/2 + , timeout/2]). -export_type([session/0]). -record(session, { transport_manager :: emqx_coap_tm:manager() , observe_manager :: emqx_coap_observe_res:manager() - , next_msg_id :: coap_message_id() , created_at :: pos_integer() }). @@ -64,6 +65,8 @@ awaiting_rel_max ]). +-import(emqx_coap_medium, [iter/3]). + %%%------------------------------------------------------------------- %%% API %%%------------------------------------------------------------------- @@ -72,7 +75,6 @@ new() -> _ = emqx_misc:rand_seed(), #session{ transport_manager = emqx_coap_tm:new() , observe_manager = emqx_coap_observe_res:new_manager() - , next_msg_id = rand:uniform(?MAX_MESSAGE_ID) , created_at = erlang:system_time(millisecond)}. %%-------------------------------------------------------------------- @@ -110,8 +112,8 @@ info(mqueue_max, _) -> 0; info(mqueue_dropped, _) -> 0; -info(next_pkt_id, #session{next_msg_id = PacketId}) -> - PacketId; +info(next_pkt_id, _) -> + 0; info(awaiting_rel, _) -> #{}; info(awaiting_rel_cnt, _) -> @@ -130,105 +132,87 @@ stats(Session) -> info(?STATS_KEYS, Session). %%%------------------------------------------------------------------- %%% Process Message %%%------------------------------------------------------------------- -handle_request(Msg, Ctx, Session) -> +handle_request(Msg, Session) -> call_transport_manager(?FUNCTION_NAME, Msg, - Ctx, - [fun process_tm/3, fun process_subscribe/3], Session). -handle_response(Msg, Ctx, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session). +handle_response(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Session). -handle_out(Msg, Ctx, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session). +handle_out(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Session). -deliver(Delivers, Ctx, Session) -> - Fun = fun({_, Topic, Message}, - #{out := OutAcc, - session := #session{observe_manager = OM, - next_msg_id = MsgId, - transport_manager = TM} = SAcc} = Acc) -> - case emqx_coap_observe_res:res_changed(Topic, OM) of +set_reply(Msg, #session{transport_manager = TM} = Session) -> + TM2 = emqx_coap_tm:set_reply(Msg, TM), + Session#session{transport_manager = TM2}. + +deliver(Delivers, #session{observe_manager = OM, + transport_manager = TM} = Session) -> + Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) -> + case emqx_coap_observe_res:res_changed(Topic, OMAcc) of undefined -> Acc; {Token, SeqId, OM2} -> - Msg = mqtt_to_coap(Message, MsgId, Token, SeqId, Ctx), - SAcc2 = SAcc#session{next_msg_id = next_msg_id(MsgId, TM), - observe_manager = OM2}, - #{out := Out} = Result = handle_out(Msg, Ctx, SAcc2), - Result#{out := [Out | OutAcc]} + Msg = mqtt_to_coap(Message, Token, SeqId), + #{out := Out, tm := TM2} = emqx_coap_tm:handle_out(Msg, TMAcc), + {Out ++ OutAcc, OM2, TM2} end end, - lists:foldl(Fun, - #{out => [], session => Session}, - lists:reverse(Delivers)). + {Outs, OM2, TM2} = lists:foldl(Fun, {[], OM, TM}, lists:reverse(Delivers)), -timeout(Timer, Ctx, Session) -> - call_transport_manager(?FUNCTION_NAME, Timer, Ctx, [fun process_tm/3], Session). + #{out => lists:reverse(Outs), + session => Session#session{observe_manager = OM2, + transport_manager = TM2}}. -result_keys() -> - [tm, subscribe] ++ emqx_coap_channel:result_keys(). - -transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT(From, Value, Result). +timeout(Timer, Session) -> + call_transport_manager(?FUNCTION_NAME, Timer, Session). %%%------------------------------------------------------------------- %%% Internal functions %%%------------------------------------------------------------------- call_transport_manager(Fun, Msg, - Ctx, - Processor, #session{transport_manager = TM} = Session) -> - try - Result = emqx_coap_tm:Fun(Msg, Ctx, TM), - {ok, Result2, Session2} = pipeline(Processor, - Result, - Msg, - Session), - emqx_coap_channel:transfer_result(session, Session2, Result2) - catch Type:Reason:Stack -> - ?ERROR("process transmission with, message:~p failed~nType:~p,Reason:~p~n,StackTrace:~p~n", - [Msg, Type, Reason, Stack]), - ?REPLY({error, internal_server_error}, Msg) - end. + Result = emqx_coap_tm:Fun(Msg, TM), + iter([tm, fun process_tm/4, fun process_session/3], + Result, + Session). -process_tm(#{tm := TM}, _, Session) -> - {ok, Session#session{transport_manager = TM}}; -process_tm(_, _, Session) -> - {ok, Session}. +process_tm(TM, Result, Session, Cursor) -> + iter(Cursor, Result, Session#session{transport_manager = TM}). -process_subscribe(#{subscribe := Sub} = Result, - Msg, - #session{observe_manager = OM} = Session) -> +process_session(_, Result, Session) -> + Result#{session => Session}. + +process_subscribe(Sub, Msg, Result, + #session{observe_manager = OM} = Session) -> case Sub of undefined -> - {ok, Result, Session}; + Result; {Topic, Token} -> {SeqId, OM2} = emqx_coap_observe_res:insert(Topic, Token, OM), Replay = emqx_coap_message:piggyback({ok, content}, Msg), Replay2 = Replay#coap_message{options = #{observe => SeqId}}, - {ok, Result#{reply => Replay2}, Session#session{observe_manager = OM2}}; + Result#{reply => Replay2, + session => Session#session{observe_manager = OM2}}; Topic -> OM2 = emqx_coap_observe_res:remove(Topic, OM), Replay = emqx_coap_message:piggyback({ok, nocontent}, Msg), - {ok, Result#{reply => Replay}, Session#session{observe_manager = OM2}} - end; -process_subscribe(Result, _, Session) -> - {ok, Result, Session}. + Result#{reply => Replay, + session => Session#session{observe_manager = OM2}} + end. -mqtt_to_coap(MQTT, MsgId, Token, SeqId, Ctx) -> +mqtt_to_coap(MQTT, Token, SeqId) -> #message{payload = Payload} = MQTT, - #coap_message{type = get_notify_type(MQTT, Ctx), + #coap_message{type = get_notify_type(MQTT), method = {ok, content}, - id = MsgId, token = Token, payload = Payload, options = #{observe => SeqId}}. -get_notify_type(#message{qos = Qos}, Ctx) -> - case emqx_coap_channel:get_config(notify_type, Ctx) of +get_notify_type(#message{qos = Qos}) -> + case emqx:get_config([gateway, coap, notify_qos], non) of qos -> case Qos of ?QOS_0 -> @@ -239,32 +223,3 @@ get_notify_type(#message{qos = Qos}, Ctx) -> Other -> Other end. - -next_msg_id(MsgId, TM) -> - next_msg_id(MsgId + 1, MsgId, TM). - -next_msg_id(MsgId, MsgId, _) -> - erlang:throw("too many message in delivering"); -next_msg_id(MsgId, BeginId, TM) when MsgId >= ?MAX_MESSAGE_ID -> - check_is_inused(1, BeginId, TM); -next_msg_id(MsgId, BeginId, TM) -> - check_is_inused(MsgId, BeginId, TM). - -check_is_inused(NewMsgId, BeginId, TM) -> - case emqx_coap_tm:is_inused(out, NewMsgId, TM) of - false -> - NewMsgId; - _ -> - next_msg_id(NewMsgId + 1, BeginId, TM) - end. - -pipeline([Fun | T], Result, Msg, Session) -> - case Fun(Result, Msg, Session) of - {ok, Session2} -> - pipeline(T, Result, Msg, Session2); - {ok, Result2, Session2} -> - pipeline(T, Result2, Msg, Session2) - end; - -pipeline([], Result, _, Session) -> - {ok, Result, Session}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl index 5a664b0f2..bdc061b1d 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl @@ -18,11 +18,12 @@ -module(emqx_coap_tm). -export([ new/0 - , handle_request/3 - , handle_response/3 + , handle_request/2 + , handle_response/2 + , handle_out/2 , handle_out/3 - , timeout/3 - , is_inused/3]). + , set_reply/2 + , timeout/2]). -export_type([manager/0, event_result/1]). @@ -30,17 +31,28 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). -type direction() :: in | out. --type state_machine_id() :: {direction(), non_neg_integer()}. --record(state_machine, { id :: state_machine_id() +-record(state_machine, { seq_id :: seq_id() + , id :: state_machine_key() + , token :: token() | undefined + , observe :: 0 | 1 | undefined | observed , state :: atom() , timers :: maps:map() , transport :: emqx_coap_transport:transport()}). -type state_machine() :: #state_machine{}. -type message_id() :: 0 .. ?MAX_MESSAGE_ID. +-type token_key() :: {token, token()}. +-type state_machine_key() :: {direction(), message_id()}. +-type seq_id() :: pos_integer(). +-type manager_key() :: token_key() | state_machine_key() | seq_id(). --type manager() :: #{message_id() => state_machine()}. +-type manager() :: #{ seq_id => seq_id() + , next_msg_id => coap_message_id() + , token_key() => seq_id() + , state_machine_key() => seq_id() + , seq_id() => state_machine() + }. -type ttimeout() :: {state_timeout, pos_integer(), any()} | {stop_timeout, pos_integer()}. @@ -48,6 +60,7 @@ -type topic() :: binary(). -type token() :: binary(). -type sub_register() :: {topic(), token()} | topic(). + -type event_result(State) :: #{next => State, outgoing => emqx_coap_message(), @@ -55,108 +68,161 @@ has_sub => undefined | sub_register(), transport => emqx_coap_transport:transprot()}. +-define(TOKEN_ID(T), {token, T}). + +-import(emqx_coap_medium, [empty/0, iter/4, reset/1, proto_out/2]). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- new() -> - #{}. + #{ seq_id => 1 + , next_msg_id => rand:uniform(?MAX_MESSAGE_ID) + }. -handle_request(#coap_message{id = MsgId} = Msg, Ctx, TM) -> +%% client request +handle_request(#coap_message{id = MsgId} = Msg, TM) -> Id = {in, MsgId}, - case maps:get(Id, TM, undefined) of + case find_machine(Id, TM) of undefined -> - Transport = emqx_coap_transport:new(), - Machine = new_state_machine(Id, Transport), - process_event(in, Msg, TM, Ctx, Machine); + {Machine, TM2} = new_in_machine(Id, TM), + process_event(in, Msg, TM2, Machine); Machine -> - process_event(in, Msg, TM, Ctx, Machine) + process_event(in, Msg, TM, Machine) end. -handle_response(#coap_message{type = Type, id = MsgId} = Msg, Ctx, TM) -> +%% client response +handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM) -> Id = {out, MsgId}, - case maps:get(Id, TM, undefined) of + TokenId = ?TOKEN_ID(Token), + case find_machine_by_keys([Id, TokenId], TM) of undefined -> case Type of reset -> - ?EMPTY_RESULT; + empty(); _ -> - ?RESET(Msg) + reset(Msg) end; Machine -> - process_event(in, Msg, TM, Ctx, Machine) + process_event(in, Msg, TM, Machine) end. -handle_out(#coap_message{id = MsgId} = Msg, Ctx, TM) -> +%% send to a client, msg can be request/piggyback/separate/notify +handle_out(Msg, TM) -> + handle_out(Msg, undefined, TM). + +handle_out(#coap_message{token = Token} = MsgT, Ctx, TM) -> + {MsgId, TM2} = alloc_message_id(TM), + Msg = MsgT#coap_message{id = MsgId}, Id = {out, MsgId}, - case maps:get(Id, TM, undefined) of + TokenId = ?TOKEN_ID(Token), + %% TODO why find by token ? + case find_machine_by_keys([Id, TokenId], TM2) of undefined -> - Transport = emqx_coap_transport:new(), - Machine = new_state_machine(Id, Transport), - process_event(out, Msg, TM, Ctx, Machine); + {Machine, TM3} = new_out_machine(Id, Msg, TM), + process_event(out, {Ctx, Msg}, TM3, Machine); _ -> %% ignore repeat send - ?EMPTY_RESULT + empty() end. -timeout({Id, Type, Msg}, Ctx, TM) -> - case maps:get(Id, TM, undefined) of +set_reply(#coap_message{id = MsgId} = Msg, TM) -> + Id = {in, MsgId}, + case find_machine(Id, TM) of undefined -> - ?EMPTY_RESULT; + TM; + #state_machine{transport = Transport, + seq_id = SeqId} = Machine -> + Transport2 = emqx_coap_transport:set_cache(Msg, Transport), + Machine2 = Machine#state_machine{transport = Transport2}, + TM#{SeqId => Machine2} + end. + +timeout({SeqId, Type, Msg}, TM) -> + case maps:get(SeqId, TM, undefined) of + undefined -> + empty(); #state_machine{timers = Timers} = Machine -> %% maybe timer has been canceled case maps:is_key(Type, Timers) of true -> - process_event(Type, Msg, TM, Ctx, Machine); + process_event(Type, Msg, TM, Machine); _ -> - ?EMPTY_RESULT + empty() end end. --spec is_inused(direction(), message_id(), manager()) -> boolean(). -is_inused(Dir, Msg, Manager) -> - maps:is_key({Dir, Msg}, Manager). - %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -new_state_machine(Id, Transport) -> - #state_machine{id = Id, - state = idle, - timers = #{}, - transport = Transport}. +process_event(stop_timeout, _, TM, Machine) -> + process_manager(stop, #{}, Machine, TM); -process_event(stop_timeout, - _, - TM, - _, - #state_machine{id = Id, - timers = Timers}) -> - lists:foreach(fun({_, Ref}) -> - emqx_misc:cancel_timer(Ref) - end, - maps:to_list(Timers)), - #{tm => maps:remove(Id, TM)}; +process_event(Event, Msg, TM, #state_machine{state = State, + transport = Transport} = Machine) -> + Result = emqx_coap_transport:State(Event, Msg, Transport), + iter([ proto, fun process_observe_response/5 + , next, fun process_state_change/5 + , transport, fun process_transport_change/5 + , timeouts, fun process_timeouts/5 + , fun process_manager/4], + Result, + Machine, + TM). -process_event(Event, - Msg, - TM, - Ctx, - #state_machine{id = Id, - state = State, - transport = Transport} = Machine) -> - Result = emqx_coap_transport:State(Event, Msg, Ctx, Transport), - {ok, _, Machine2} = emqx_misc:pipeline([fun process_state_change/2, - fun process_transport_change/2, - fun process_timeouts/2], - Result, - Machine), - TM2 = TM#{Id => Machine2}, - emqx_coap_session:transfer_result(tm, TM2, Result). +process_observe_response({response, {_, Msg}} = Response, + Result, + #state_machine{observe = 0} = Machine, + TM, + Iter) -> + Result2 = proto_out(Response, Result), + case Msg#coap_message.method of + {ok, _} -> + iter(Iter, + Result2#{next => observe}, + Machine#state_machine{observe = observed}, + TM); + _ -> + iter(Iter, Result2, Machine, TM) + end; -process_state_change(#{next := Next}, Machine) -> - {ok, cancel_state_timer(Machine#state_machine{state = Next})}; -process_state_change(_, Machine) -> - {ok, Machine}. +process_observe_response(Proto, Result, Machine, TM, Iter) -> + iter(Iter, proto_out(Proto, Result), Machine, TM). + +process_state_change(Next, Result, Machine, TM, Iter) -> + case Next of + stop -> + process_manager(stop, Result, Machine, TM); + _ -> + iter(Iter, + Result, + cancel_state_timer(Machine#state_machine{state = Next}), + TM) + end. + +process_transport_change(Transport, Result, Machine, TM, Iter) -> + iter(Iter, Result, Machine#state_machine{transport = Transport}, TM). + +process_timeouts([], Result, Machine, TM, Iter) -> + iter(Iter, Result, Machine, TM); + +process_timeouts(Timeouts, Result, + #state_machine{seq_id = SeqId, + timers = Timers} = Machine, TM, Iter) -> + NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) -> + process_timer(SeqId, Timer, Acc); + ({stop_timeout, I}, Acc) -> + process_timer(SeqId, {stop_timeout, I, stop}, Acc) + end, + Timers, + Timeouts), + iter(Iter, Result, Machine#state_machine{timers = NewTimers}, TM). + +process_manager(stop, Result, #state_machine{seq_id = SeqId}, TM) -> + Result#{tm => delete_machine(SeqId, TM)}; + +process_manager(_, Result, #state_machine{seq_id = SeqId} = Machine2, TM) -> + Result#{tm => TM#{SeqId => Machine2}}. cancel_state_timer(#state_machine{timers = Timers} = Machine) -> case maps:get(state_timer, Timers, undefined) of @@ -167,27 +233,118 @@ cancel_state_timer(#state_machine{timers = Timers} = Machine) -> Machine#state_machine{timers = maps:remove(state_timer, Timers)} end. -process_transport_change(#{transport := Transport}, Machine) -> - {ok, Machine#state_machine{transport = Transport}}; -process_transport_change(_, Machine) -> - {ok, Machine}. - -process_timeouts(#{timeouts := []}, Machine) -> - {ok, Machine}; -process_timeouts(#{timeouts := Timeouts}, - #state_machine{id = Id, timers = Timers} = Machine) -> - NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) -> - process_timer(Id, Timer, Acc); - ({stop_timeout, I}, Acc) -> - process_timer(Id, {stop_timeout, I, stop}, Acc) - end, - Timers, - Timeouts), - {ok, Machine#state_machine{timers = NewTimers}}; - -process_timeouts(_, Machine) -> - {ok, Machine}. - -process_timer(Id, {Type, Interval, Msg}, Timers) -> - Ref = emqx_misc:start_timer(Interval, {state_machine, {Id, Type, Msg}}), +process_timer(SeqId, {Type, Interval, Msg}, Timers) -> + Ref = emqx_misc:start_timer(Interval, {state_machine, {SeqId, Type, Msg}}), Timers#{Type => Ref}. + +-spec delete_machine(manager_key(), manager()) -> manager(). +delete_machine(Id, Manager) -> + case find_machine(Id, Manager) of + undefined -> + Manager; + #state_machine{seq_id = SeqId, + id = MachineId, + token = Token, + timers = Timers} -> + lists:foreach(fun({_, Ref}) -> + emqx_misc:cancel_timer(Ref) + end, + maps:to_list(Timers)), + maps:without([SeqId, MachineId, ?TOKEN_ID(Token)], Manager) + end. + +-spec find_machine(manager_key(), manager()) -> state_machine() | undefined. +find_machine({_, _} = Id, Manager) -> + find_machine_by_seqid(maps:get(Id, Manager, undefined), Manager); +find_machine(SeqId, Manager) -> + find_machine_by_seqid(SeqId, Manager). + +-spec find_machine_by_seqid(seq_id() | undefined, manager()) -> + state_machine() | undefined. +find_machine_by_seqid(SeqId, Manager) -> + maps:get(SeqId, Manager, undefined). + +-spec find_machine_by_keys(list(manager_key()), + manager()) -> state_machine() | undefined. +find_machine_by_keys([H | T], Manager) -> + case H of + ?TOKEN_ID(<<>>) -> + find_machine_by_keys(T, Manager); + _ -> + case find_machine(H, Manager) of + undefined -> + find_machine_by_keys(T, Manager); + Machine -> + Machine + end + end; +find_machine_by_keys(_, _) -> + undefined. + +-spec new_in_machine(state_machine_key(), manager()) -> + {state_machine(), manager()}. +new_in_machine(MachineId, #{seq_id := SeqId} = Manager) -> + Machine = #state_machine{ seq_id = SeqId + , id = MachineId + , state = idle + , timers = #{} + , transport = emqx_coap_transport:new()}, + {Machine, Manager#{seq_id := SeqId + 1, + SeqId => Machine, + MachineId => SeqId}}. + +-spec new_out_machine(state_machine_key(), emqx_coap_message(), manager()) -> + {state_machine(), manager()}. +new_out_machine(MachineId, + #coap_message{type = Type, token = Token, options = Opts}, + #{seq_id := SeqId} = Manager) -> + Observe = maps:get(observe, Opts, undefined), + Machine = #state_machine{ seq_id = SeqId + , id = MachineId + , token = Token + , observe = Observe + , state = idle + , timers = #{} + , transport = emqx_coap_transport:new()}, + + Manager2 = Manager#{seq_id := SeqId + 1, + SeqId => Machine, + MachineId => SeqId}, + {Machine, + if Token =:= <<>> -> + Manager2; + Observe =:= 1 -> + TokenId = ?TOKEN_ID(Token), + delete_machine(TokenId, Manager2); + Type =:= con orelse Observe =:= 0 -> + TokenId = ?TOKEN_ID(Token), + case maps:get(TokenId, Manager, undefined) of + undefined -> + Manager2#{TokenId => SeqId}; + _ -> + throw("token conflict") + end; + true -> + Manager2 + end + }. + +alloc_message_id(#{next_msg_id := MsgId} = TM) -> + alloc_message_id(MsgId, TM). + +alloc_message_id(MsgId, TM) -> + Id = {out, MsgId}, + case maps:get(Id, TM, undefined) of + undefined -> + {MsgId, TM#{next_msg_id => next_message_id(MsgId)}}; + _ -> + alloc_message_id(next_message_id(MsgId), TM) + end. + +next_message_id(MsgId) -> + Next = MsgId + 1, + if Next >= ?MAX_MESSAGE_ID -> + 1; + true -> + Next + end. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl index 2c2aaab2e..eb7ce9bd4 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl @@ -9,19 +9,27 @@ -define(EXCHANGE_LIFETIME, 247000). -define(NON_LIFETIME, 145000). +-type request_context() :: any(). + -record(transport, { cache :: undefined | emqx_coap_message() + , req_context :: request_context() , retry_interval :: non_neg_integer() , retry_count :: non_neg_integer() + , observe :: non_neg_integer() | undefined }). -type transport() :: #transport{}. --export([ new/0, idle/4, maybe_reset/4 - , maybe_resend/4, wait_ack/4, until_stop/4]). +-export([ new/0, idle/3, maybe_reset/3, set_cache/2 + , maybe_resend_4request/3, wait_ack/3, until_stop/3 + , observe/3, maybe_resend_4response/3]). -export_type([transport/0]). -import(emqx_coap_message, [reset/1]). +-import(emqx_coap_medium, [ empty/0, reset/2, proto_out/2 + , out/1, out/2, proto_out/1 + , reply/2]). -spec new() -> transport(). new() -> @@ -31,96 +39,152 @@ new() -> idle(in, #coap_message{type = non, method = Method} = Msg, - Ctx, _) -> - Ret = #{next => until_stop, - timeouts => [{stop_timeout, ?NON_LIFETIME}]}, case Method of undefined -> - ?RESET(Msg); + reset(Msg, #{next => stop}); _ -> - Result = call_handler(Msg, Ctx), - maps:merge(Ret, Result) + proto_out({request, Msg}, + #{next => until_stop, + timeouts => + [{stop_timeout, ?NON_LIFETIME}]}) end; idle(in, #coap_message{type = con, method = Method} = Msg, - Ctx, - Transport) -> - Ret = #{next => maybe_resend, - timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}, + _) -> case Method of undefined -> - ResetMsg = reset(Msg), - Ret#{transport => Transport#transport{cache = ResetMsg}, - out => ResetMsg}; + reset(Msg, #{next => stop}); _ -> - Result = call_handler(Msg, Ctx), - maps:merge(Ret, Result) + proto_out({request, Msg}, + #{next => maybe_resend_4request, + timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}) end; -idle(out, #coap_message{type = non} = Msg, _, _) -> - #{next => maybe_reset, - out => Msg, - timeouts => [{stop_timeout, ?NON_LIFETIME}]}; +idle(out, {Ctx, Msg}, Transport) -> + idle(out, Msg, Transport#transport{req_context = Ctx}); -idle(out, Msg, _, Transport) -> +idle(out, #coap_message{type = non} = Msg, _) -> + out(Msg, #{next => maybe_reset, + timeouts => [{stop_timeout, ?NON_LIFETIME}]}); + +idle(out, Msg, Transport) -> _ = emqx_misc:rand_seed(), Timeout = ?ACK_TIMEOUT + rand:uniform(?ACK_RANDOM_FACTOR), - #{next => wait_ack, - transport => Transport#transport{cache = Msg}, - out => Msg, - timeouts => [ {state_timeout, Timeout, ack_timeout} - , {stop_timeout, ?EXCHANGE_LIFETIME}]}. + out(Msg, #{next => wait_ack, + transport => Transport#transport{cache = Msg}, + timeouts => [ {state_timeout, Timeout, ack_timeout} + , {stop_timeout, ?EXCHANGE_LIFETIME}]}). -maybe_reset(in, Message, _, _) -> - case Message of - #coap_message{type = reset} -> - ?INFO("Reset Message:~p~n", [Message]); +maybe_resend_4request(in, Msg, Transport) -> + maybe_resend(Msg, true, Transport). + +maybe_resend_4response(in, Msg, Transport) -> + maybe_resend(Msg, false, Transport). + +maybe_resend(Msg, IsExpecteReq, #transport{cache = Cache}) -> + IsExpected = emqx_coap_message:is_request(Msg) =:= IsExpecteReq, + case IsExpected of + true -> + case Cache of + undefined -> + %% handler in processing, ignore + empty(); + _ -> + out(Cache) + end; _ -> - ok - end, - ?EMPTY_RESULT. + reset(Msg, #{next => stop}) + end. -maybe_resend(in, _, _, #transport{cache = Cache}) -> - #{out => Cache}. +maybe_reset(in, #coap_message{type = Type, method = Method} = Message, + #transport{req_context = Ctx} = Transport) -> + Ret = #{next => stop}, + CtxMsg = {Ctx, Message}, + if Type =:= reset -> + proto_out({reset, CtxMsg}, Ret); + is_tuple(Method) -> + on_response(Message, + Transport, + if Type =:= non -> until_stop; + true -> maybe_resend_4response + end); + true -> + reset(Message, Ret) + end. -wait_ack(in, #coap_message{type = Type}, _, _) -> +wait_ack(in, #coap_message{type = Type, method = Method} = Msg, #transport{req_context = Ctx}) -> + CtxMsg = {Ctx, Msg}, case Type of - ack -> - #{next => until_stop}; reset -> - #{next => until_stop}; + proto_out({reset, CtxMsg}, #{next => stop}); _ -> - ?EMPTY_RESULT + case Method of + undefined -> + %% empty ack, keep transport to recv response + proto_out({ack, CtxMsg}); + {_, _} -> + %% ack with payload + proto_out({response, CtxMsg}, #{next => stop}); + _ -> + reset(Msg, #{next => stop}) + end end; wait_ack(state_timeout, ack_timeout, - _, #transport{cache = Msg, retry_interval = Timeout, retry_count = Count} =Transport) -> case Count < ?MAX_RETRANSMIT of true -> Timeout2 = Timeout * 2, - #{transport => Transport#transport{retry_interval = Timeout2, - retry_count = Count + 1}, - out => Msg, - timeouts => [{state_timeout, Timeout2, ack_timeout}]}; + out(Msg, + #{transport => Transport#transport{retry_interval = Timeout2, + retry_count = Count + 1}, + timeouts => [{state_timeout, Timeout2, ack_timeout}]}); _ -> - #{next_state => until_stop} + proto_out({ack_failure, Msg}, #{next_state => stop}) end. -until_stop(_, _, _, _) -> - ?EMPTY_RESULT. - -call_handler(#coap_message{options = Opts} = Msg, Ctx) -> - case maps:get(uri_path, Opts, undefined) of - [<<"ps">> | RestPath] -> - emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx); - [<<"mqtt">> | RestPath] -> - emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx); +observe(in, + #coap_message{method = Method} = Message, + #transport{observe = Observe} = Transport) -> + case Method of + {ok, _} -> + case emqx_coap_message:get_option(observe, Message, Observe) of + Observe -> + %% repeatd notify, ignore + empty(); + NewObserve -> + on_response(Message, + Transport#transport{observe = NewObserve}, + ?FUNCTION_NAME) + end; + {error, _} -> + #{next => stop}; _ -> - ?REPLY({error, bad_request}, Msg) + reset(Message) + end. + +until_stop(_, _, _) -> + empty(). + +set_cache(Cache, Transport) -> + Transport#transport{cache = Cache}. + +on_response(#coap_message{type = Type} = Message, + #transport{req_context = Ctx} = Transport, + NextState) -> + CtxMsg = {Ctx, Message}, + if Type =:= non -> + proto_out({response, CtxMsg}, #{next => NextState}); + Type =:= con -> + Ack = emqx_coap_message:ack(Message), + proto_out({response, CtxMsg}, + out(Ack, #{next => NextState, + transport => Transport#transport{cache = Ack}})); + true -> + reset(Message) end. diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl index 88a4a2310..47bf14d9b 100644 --- a/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl @@ -18,23 +18,24 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). --export([handle_request/3]). +-export([handle_request/4]). -import(emqx_coap_message, [response/2, response/3]). +-import(emqx_coap_medium, [reply/2]). -handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _) -> +handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _Ctx, _CInfo) -> handle_method(Method, Msg); -handle_request(_, Msg, _) -> - ?REPLY({error, bad_request}, Msg). +handle_request(_, Msg, _, _) -> + reply({error, bad_request}, Msg). handle_method(put, Msg) -> - ?REPLY({ok, changed}, Msg); + reply({ok, changed}, Msg); -handle_method(post, _) -> - #{connection => open}; +handle_method(post, Msg) -> + #{connection => {open, Msg}}; -handle_method(delete, _) -> - #{connection => close}; +handle_method(delete, Msg) -> + #{connection => {close, Msg}}; handle_method(_, Msg) -> - ?REPLY({error, method_not_allowed}, Msg). + reply({error, method_not_allowed}, Msg). diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl index e6886a559..ca734993a 100644 --- a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl @@ -20,48 +20,48 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). --export([handle_request/3]). +-export([handle_request/4]). -import(emqx_coap_message, [response/2, response/3]). +-import(emqx_coap_medium, [reply/2, reply/3]). --define(UNSUB(Topic), #{subscribe => Topic}). --define(SUB(Topic, Token), #{subscribe => {Topic, Token}}). +-define(UNSUB(Topic, Msg), #{subscribe => {Topic, Msg}}). +-define(SUB(Topic, Token, Msg), #{subscribe => {{Topic, Token}, Msg}}). -define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}). -handle_request(Path, #coap_message{method = Method} = Msg, Ctx) -> +handle_request(Path, #coap_message{method = Method} = Msg, Ctx, CInfo) -> case check_topic(Path) of {ok, Topic} -> - handle_method(Method, Topic, Msg, Ctx); + handle_method(Method, Topic, Msg, Ctx, CInfo); _ -> - ?REPLY({error, bad_request}, <<"invalid topic">>, Msg) + reply({error, bad_request}, <<"invalid topic">>, Msg) end. -handle_method(get, Topic, #coap_message{options = Opts} = Msg, Ctx) -> - case maps:get(observe, Opts, undefined) of +handle_method(get, Topic, Msg, Ctx, CInfo) -> + case emqx_coap_message:get_option(observe, Msg) of 0 -> - subscribe(Msg, Topic, Ctx); + subscribe(Msg, Topic, Ctx, CInfo); 1 -> - unsubscribe(Topic, Ctx); + unsubscribe(Msg, Topic, CInfo); _ -> - ?REPLY({error, bad_request}, <<"invalid observe value">>, Msg) + reply({error, bad_request}, <<"invalid observe value">>, Msg) end; -handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx) -> - case emqx_coap_channel:validator(publish, Topic, Ctx) of +handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) -> + case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of allow -> - ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), - #{clientid := ClientId} = ClientInfo, - QOS = get_publish_qos(Msg, Ctx), + #{clientid := ClientId} = CInfo, + QOS = get_publish_qos(Msg), MQTTMsg = emqx_message:make(ClientId, QOS, Topic, Payload), MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), _ = emqx_broker:publish(MQTTMsg2), - ?REPLY({ok, changed}, Msg); + reply({ok, changed}, Msg); _ -> - ?REPLY({error, unauthorized}, Msg) + reply({error, unauthorized}, Msg) end; -handle_method(_, _, Msg, _) -> - ?REPLY({error, method_not_allowed}, Msg). +handle_method(_, _, Msg, _, _) -> + reply({error, method_not_allowed}, Msg). check_topic([]) -> error; @@ -76,13 +76,13 @@ check_topic(Path) -> <<>>, Path))}. -get_sub_opts(#coap_message{options = Opts} = Msg, Ctx) -> +get_sub_opts(#coap_message{options = Opts} = Msg) -> SubOpts = maps:fold(fun parse_sub_opts/3, #{}, Opts), case SubOpts of #{qos := _} -> maps:merge(SubOpts, ?SUBOPTS); _ -> - CfgType = emqx_coap_channel:get_config(subscribe_qos, Ctx), + CfgType = emqx:get_config([gateway, coap, subscribe_qos], ?QOS_0), maps:merge(SubOpts, ?SUBOPTS#{qos => type_to_qos(CfgType, Msg)}) end. @@ -106,16 +106,16 @@ type_to_qos(coap, #coap_message{type = Type}) -> ?QOS_1 end. -get_publish_qos(#coap_message{options = Opts} = Msg, Ctx) -> - case maps:get(uri_query, Opts) of +get_publish_qos(Msg) -> + case emqx_coap_message:get_option(uri_query, Msg) of #{<<"qos">> := QOS} -> erlang:binary_to_integer(QOS); _ -> - CfgType = emqx_coap_channel:get_config(publish_qos, Ctx), + CfgType = emqx:get_config([gateway, coap, publish_qos], ?QOS_0), type_to_qos(CfgType, Msg) end. -apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) -> +apply_publish_opts(Msg, MQTTMsg) -> maps:fold(fun(<<"retain">>, V, Acc) -> Val = erlang:binary_to_atom(V), emqx_message:set_flag(retain, Val, Acc); @@ -129,27 +129,25 @@ apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) -> Acc end, MQTTMsg, - maps:get(uri_query, Opts)). + emqx_coap_message:get_option(uri_query, Msg)). -subscribe(#coap_message{token = <<>>} = Msg, _, _) -> - ?REPLY({error, bad_request}, <<"observe without token">>, Msg); +subscribe(#coap_message{token = <<>>} = Msg, _, _, _) -> + reply({error, bad_request}, <<"observe without token">>, Msg); -subscribe(#coap_message{token = Token} = Msg, Topic, Ctx) -> - case emqx_coap_channel:validator(subscribe, Topic, Ctx) of +subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) -> + case emqx_coap_channel:validator(subscribe, Topic, Ctx, CInfo) of allow -> - ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), - #{clientid := ClientId} = ClientInfo, - SubOpts = get_sub_opts(Msg, Ctx), + #{clientid := ClientId} = CInfo, + SubOpts = get_sub_opts(Msg), emqx_broker:subscribe(Topic, ClientId, SubOpts), emqx_hooks:run('session.subscribed', - [ClientInfo, Topic, SubOpts]), - ?SUB(Topic, Token); + [CInfo, Topic, SubOpts]), + ?SUB(Topic, Token, Msg); _ -> - ?REPLY({error, unauthorized}, Msg) + reply({error, unauthorized}, Msg) end. -unsubscribe(Topic, Ctx) -> - ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), +unsubscribe(Msg, Topic, CInfo) -> emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, ?SUBOPTS]), - ?UNSUB(Topic). + emqx_hooks:run('session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), + ?UNSUB(Topic, Msg). diff --git a/apps/emqx_gateway/src/coap/include/emqx_coap.hrl b/apps/emqx_gateway/src/coap/include/emqx_coap.hrl index 3b0268abb..d47dd17fd 100644 --- a/apps/emqx_gateway/src/coap/include/emqx_coap.hrl +++ b/apps/emqx_gateway/src/coap/include/emqx_coap.hrl @@ -22,18 +22,6 @@ -define(DEFAULT_MAX_AGE, 60). -define(MAXIMUM_MAX_AGE, 4294967295). --define(EMPTY_RESULT, #{}). --define(TRANSFER_RESULT(From, Value, R1), - begin - Keys = result_keys(), - R2 = maps:with(Keys, R1), - R2#{From => Value} - end). - --define(RESET(Msg), #{out => emqx_coap_message:reset(Msg)}). --define(REPLY(Resp, Payload, Msg), #{out => emqx_coap_message:piggyback(Resp, Payload, Msg)}). --define(REPLY(Resp, Msg), ?REPLY(Resp, <<>>, Msg)). - -type coap_message_id() :: 1 .. ?MAX_MESSAGE_ID. -type message_type() :: con | non | ack | reset. -type max_age() :: 1 .. ?MAXIMUM_MAX_AGE. @@ -66,7 +54,7 @@ , uri_path => list(binary()) , content_format => 0 .. 65535 , max_age => non_neg_integer() - , uri_query => list(binary()) + , uri_query => list(binary()) | map() , 'accept' => 0 .. 65535 , location_query => list(binary()) , proxy_uri => binary() @@ -85,7 +73,4 @@ , options = #{} , payload = <<>>}). --record(coap_content, {etag, max_age = ?DEFAULT_MAX_AGE, format, location_path = [], payload = <<>>}). - -type emqx_coap_message() :: #coap_message{}. --type coap_content() :: #coap_content{}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 9371f8c6b..9ab26e480 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -94,6 +94,7 @@ fields(lwm2m_structs) -> , {lifetime_max, t(duration())} , {qmode_time_windonw, t(integer())} , {auto_observe, t(boolean())} + , {mountpoint, t(string())} , {update_msg_publish_condition, t(union([always, contains_object_list]))} , {translators, t(ref(translators))} , {listeners, t(ref(udp_listener_group))} @@ -122,7 +123,17 @@ fields(clientinfo_override) -> ]; fields(translators) -> - [{"$name", t(binary())}]; + [ {command, t(ref(translator))} + , {response, t(ref(translator))} + , {notify, t(ref(translator))} + , {register, t(ref(translator))} + , {update, t(ref(translator))} + ]; + +fields(translator) -> + [ {topic, t(binary())} + , {qos, t(range(0, 2))} + ]; fields(udp_listener_group) -> [ {udp, t(ref(udp_listener))} @@ -160,7 +171,7 @@ fields(listener_settings) -> , {max_connections, t(integer(), undefined, 1024)} , {max_conn_rate, t(integer())} , {active_n, t(integer(), undefined, 100)} - %, {rate_limit, t(comma_separated_list())} + %, {rate_limit, t(comma_separated_list())} , {access, t(ref(access))} , {proxy_protocol, t(boolean())} , {proxy_protocol_timeout, t(duration())} @@ -183,24 +194,24 @@ fields(tcp_listener_settings) -> fields(ssl_listener_settings) -> [ - %% some special confs for ssl listener + %% some special confs for ssl listener ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ssl(undefined, #{handshake_timeout => <<"15s">> + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); fields(udp_listener_settings) -> [ - %% some special confs for udp listener + %% some special confs for udp listener ] ++ fields(listener_settings); fields(dtls_listener_settings) -> [ - %% some special confs for dtls listener + %% some special confs for dtls listener ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ssl(undefined, #{handshake_timeout => <<"15s">> + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); fields(access) -> [ {"$id", #{type => binary(), @@ -270,7 +281,7 @@ ref(Field) -> %% ... ssl(Mapping, Defaults) -> M = fun (Field) -> - case (Mapping) of + case (Mapping) of undefined -> undefined; _ -> Mapping ++ "." ++ Field end end, diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl index 80449238c..03c3a6bc2 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl @@ -55,7 +55,8 @@ list(#{node := Node }, Params) -> end; list(#{}, _Params) -> - Channels = emqx_lwm2m_cm:all_channels(), + %% Channels = emqx_lwm2m_cm:all_channels(), + Channels = [], return({ok, format(Channels)}). lookup_cmd(#{ep := Ep, node := Node}, Params) -> @@ -64,26 +65,27 @@ lookup_cmd(#{ep := Ep, node := Node}, Params) -> _ -> rpc_call(Node, lookup_cmd, [#{ep => Ep}, Params]) end; -lookup_cmd(#{ep := Ep}, Params) -> - MsgType = proplists:get_value(<<"msgType">>, Params), - Path0 = proplists:get_value(<<"path">>, Params), - case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of - [] -> return({ok, []}); - [{_, undefined} | _] -> return({ok, []}); - [{{IMEI, Path, MsgType}, undefined}] -> - return({ok, [{imei, IMEI}, - {'msgType', IMEI}, - {'code', <<"6.01">>}, - {'codeMsg', <<"reply_not_received">>}, - {'path', Path}]}); - [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> - Payload1 = format_cmd_content(Content, MsgType), - return({ok, [{imei, IMEI}, - {'msgType', IMEI}, - {'code', Code}, - {'codeMsg', CodeMsg}, - {'path', Path}] ++ Payload1}) - end. +lookup_cmd(#{ep := _Ep}, Params) -> + _MsgType = proplists:get_value(<<"msgType">>, Params), + _Path0 = proplists:get_value(<<"path">>, Params), + %% case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of + %% [] -> return({ok, []}); + %% [{_, undefined} | _] -> return({ok, []}); + %% [{{IMEI, Path, MsgType}, undefined}] -> + %% return({ok, [{imei, IMEI}, + %% {'msgType', IMEI}, + %% {'code', <<"6.01">>}, + %% {'codeMsg', <<"reply_not_received">>}, + %% {'path', Path}]}); + %% [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> + %% Payload1 = format_cmd_content(Content, MsgType), + %% return({ok, [{imei, IMEI}, + %% {'msgType', IMEI}, + %% {'code', Code}, + %% {'codeMsg', CodeMsg}, + %% {'path', Path}] ++ Payload1}) + %% end. + return({ok, []}). rpc_call(Node, Fun, Args) -> case rpc:call(Node, ?MODULE, Fun, Args) of @@ -115,36 +117,37 @@ format(Channels) -> {'objectList', ObjectList}] end, Channels). -format_cmd_content(undefined, _MsgType) -> []; -format_cmd_content(Content, <<"discover">>) -> - [H | Content1] = Content, - {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), - [ObjId | _]= path_list(HObjId), - ObjectList = case Content1 of - [Content2 | _] -> - {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), - ObjL; - [] -> [] - end, - R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of - {error, _} -> - lists:map(fun(Object) -> {Object, Object} end, ObjectList); - ObjDefinition -> - lists:map(fun(Object) -> - [_, _, ResId| _] = path_list(Object), - Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of - "E" -> [{operations, list_to_binary("E")}]; - Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, - {operations, list_to_binary(Oper)}] - end, - [{path, Object}, - {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} - ] ++ Operations - end, ObjectList) - end, - [{content, R}]; -format_cmd_content(Content, _) -> - [{content, Content}]. +%% format_cmd_content(undefined, _MsgType) -> []; +%% format_cmd_content(_Content, <<"discover">>) -> +%% %% [H | Content1] = Content, +%% %% {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), +%% %% [ObjId | _]= path_list(HObjId), +%% %% ObjectList = case Content1 of +%% %% [Content2 | _] -> +%% %% {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), +%% %% ObjL; +%% %% [] -> [] +%% %% end, +%% %% R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of +%% %% {error, _} -> +%% %% lists:map(fun(Object) -> {Object, Object} end, ObjectList); +%% %% ObjDefinition -> +%% %% lists:map(fun(Object) -> +%% %% [_, _, ResId| _] = path_list(Object), +%% %% Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of +%% %% "E" -> [{operations, list_to_binary("E")}]; +%% %% Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, +%% %% {operations, list_to_binary(Oper)}] +%% %% end, +%% %% [{path, Object}, +%% %% {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} +%% %% ] ++ Operations +%% %% end, ObjectList) +%% %% end, +%% %% [{content, R}]; +%% []; +%% format_cmd_content(Content, _) -> +%% [{content, Content}]. ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl new file mode 100644 index 000000000..80078407b --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -0,0 +1,459 @@ +%%-------------------------------------------------------------------- +%% 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_lwm2m_channel). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). + +%% API +-export([ info/1 + , info/2 + , stats/1 + , validator/2 + , validator/4 + , do_takeover/3]). + +-export([ init/2 + , handle_in/2 + , handle_deliver/2 + , handle_timeout/3 + , terminate/2 + ]). + +-export([ handle_call/2 + , handle_cast/2 + , handle_info/2 + ]). + +-record(channel, { + %% Context + ctx :: emqx_gateway_ctx:context(), + %% Connection Info + conninfo :: emqx_types:conninfo(), + %% Client Info + clientinfo :: emqx_types:clientinfo(), + %% Session + session :: emqx_lwm2m_session:session() | undefined, + + %% Timer + timers :: #{atom() => disable | undefined | reference()}, + + validator :: function() + }). + +-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). + +-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +info(Channel) -> + maps:from_list(info(?INFO_KEYS, Channel)). + +info(Keys, Channel) when is_list(Keys) -> + [{Key, info(Key, Channel)} || Key <- Keys]; + +info(conninfo, #channel{conninfo = ConnInfo}) -> + ConnInfo; +info(conn_state, _) -> + connected; +info(clientinfo, #channel{clientinfo = ClientInfo}) -> + ClientInfo; +info(session, #channel{session = Session}) -> + emqx_misc:maybe_apply(fun emqx_session:info/1, Session); +info(clientid, #channel{clientinfo = #{clientid := ClientId}}) -> + ClientId; +info(ctx, #channel{ctx = Ctx}) -> + Ctx. + +stats(_) -> + []. + +init(ConnInfo = #{peername := {PeerHost, _}, + sockname := {_, SockPort}}, + #{ctx := Ctx} = Config) -> + Peercert = maps:get(peercert, ConnInfo, undefined), + Mountpoint = maps:get(mountpoint, Config, undefined), + ClientInfo = set_peercert_infos( + Peercert, + #{ zone => default + , protocol => lwm2m + , peerhost => PeerHost + , sockport => SockPort + , username => undefined + , clientid => undefined + , is_bridge => false + , is_superuser => false + , mountpoint => Mountpoint + } + ), + + #channel{ ctx = Ctx + , conninfo = ConnInfo + , clientinfo = ClientInfo + , timers = #{} + , session = emqx_lwm2m_session:new() + , validator = validator(Ctx, ClientInfo) + }. + +validator(_Type, _Topic, _Ctx, _ClientInfo) -> + allow. + %emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). + +validator(Ctx, ClientInfo) -> + fun(Type, Topic) -> + validator(Type, Topic, Ctx, ClientInfo) + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- +handle_in(Msg, ChannleT) -> + Channel = update_life_timer(ChannleT), + call_session(handle_coap_in, Msg, Channel). + +%%-------------------------------------------------------------------- +%% Handle Delivers from broker to client +%%-------------------------------------------------------------------- +handle_deliver(Delivers, Channel) -> + call_session(handle_deliver, Delivers, Channel). + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- +handle_timeout(_, lifetime, Channel) -> + {shutdown, timeout, Channel}; + +handle_timeout(_, {transport, _} = Msg, Channel) -> + call_session(timeout, Msg, Channel); + +handle_timeout(_, disconnect, Channel) -> + {shutdown, normal, Channel}; + +handle_timeout(_, _, Channel) -> + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- +handle_call(Req, Channel) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Cast +%%-------------------------------------------------------------------- +handle_cast(Req, Channel) -> + ?LOG(error, "Unexpected cast: ~p", [Req]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Info +%%-------------------------------------------------------------------- +handle_info(Info, Channel) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- +terminate(_Reason, #channel{session = Session}) -> + emqx_lwm2m_session:on_close(Session). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +set_peercert_infos(NoSSL, ClientInfo) + when NoSSL =:= nossl; + NoSSL =:= undefined -> + ClientInfo; +set_peercert_infos(Peercert, ClientInfo) -> + {DN, CN} = {esockd_peercert:subject(Peercert), + esockd_peercert:common_name(Peercert)}, + ClientInfo#{dn => DN, cn => CN}. + +make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> + TRef = emqx_misc:start_timer(Time, Msg), + Channel#channel{timers = Timers#{Name => TRef}}. + +update_life_timer(#channel{session = Session, timers = Timers} = Channel) -> + LifeTime = emqx_lwm2m_session:info(lifetime, Session), + _ = case maps:get(lifetime, Timers, undefined) of + undefined -> ok; + Ref -> erlang:cancel_timer(Ref) + end, + make_timer(lifetime, LifeTime, lifetime, Channel). + +check_location(Location, #channel{session = Session}) -> + SLocation = emqx_lwm2m_session:info(location_path, Session), + Location =:= SLocation. + +do_takeover(_DesireId, Msg, Channel) -> + %% TODO completed the takeover, now only reset the message + Reset = emqx_coap_message:reset(Msg), + call_session(handle_out, Reset, Channel). + +do_connect(Req, Result, Channel, Iter) -> + case emqx_misc:pipeline( + [ fun check_lwm2m_version/2 + , fun run_conn_hooks/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + , fun auth_connect/2 + ], + Req, + Channel) of + {ok, _Input, #channel{session = Session, + validator = Validator} = NChannel} -> + case emqx_lwm2m_session:info(reg_info, Session) of + undefined -> + process_connect(ensure_connected(NChannel), Req, Result, Iter); + _ -> + NewResult = emqx_lwm2m_session:reregister(Req, Validator, Session), + iter(Iter, maps:merge(Result, NewResult), NChannel) + end; + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + Payload = erlang:list_to_binary(lists:flatten(ErrMsg)), + iter(Iter, + reply({error, bad_request}, Payload, Req, Result), + NChannel) + end. + +check_lwm2m_version(#coap_message{options = Opts}, + #channel{conninfo = ConnInfo} = Channel) -> + Ver = gets([uri_query, <<"lwm2m">>], Opts), + IsValid = case Ver of + <<"1.0">> -> + true; + <<"1">> -> + true; + <<"1.1">> -> + true; + _ -> + false + end, + if IsValid -> + NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) + , proto_name => <<"lwm2m">> + , proto_ver => Ver + }, + {ok, Channel#channel{conninfo = NConnInfo}}; + true -> + ?LOG(error, "Reject REGISTER due to unsupported version: ~0p", [Ver]), + {error, "invalid lwm2m version", Channel} + end. + +run_conn_hooks(Input, Channel = #channel{ctx = Ctx, + conninfo = ConnInfo}) -> + ConnProps = #{}, + case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of + Error = {error, _Reason} -> Error; + _NConnProps -> + {ok, Input, Channel} + end. + +enrich_clientinfo(#coap_message{options = Options} = Msg, + Channel = #channel{clientinfo = ClientInfo0}) -> + Query = maps:get(uri_query, Options, #{}), + case Query of + #{<<"ep">> := Epn} -> + UserName = maps:get(<<"imei">>, Query, undefined), + Password = maps:get(<<"password">>, Query, undefined), + ClientId = maps:get(<<"device_id">>, Query, Epn), + ClientInfo = + ClientInfo0#{username => UserName, + password => Password, + clientid => ClientId}, + {ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo), + {ok, Channel#channel{clientinfo = NClientInfo}}; + _ -> + ?LOG(error, "Reject REGISTER due to wrong parameters, Query=~p", [Query]), + {error, "invalid queries", Channel} + end. + +set_log_meta(_Input, #channel{clientinfo = #{clientid := ClientId}}) -> + emqx_logger:set_metadata_clientid(ClientId), + ok. + +auth_connect(_Input, 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, + validator = validator(Ctx, ClientInfo)}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, Reason} + end. + +fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> + {ok, ClientInfo}; +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}}. + +ensure_connected(Channel = #channel{ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + ok = run_hooks(Ctx, 'client.connected', [ClientInfo, ConnInfo]), + Channel. + +process_connect(Channel = #channel{ctx = Ctx, + session = Session, + conninfo = ConnInfo, + clientinfo = ClientInfo, + validator = Validator}, + Msg, Result, Iter) -> + %% inherit the old session + SessFun = fun(_,_) -> #{} end, + case emqx_gateway_ctx:open_session( + Ctx, + true, + ClientInfo, + ConnInfo, + SessFun, + emqx_lwm2m_session + ) of + {ok, _} -> + NewResult = emqx_lwm2m_session:init(Msg, Validator, Session), + iter(Iter, maps:merge(Result, NewResult), Channel); + {error, Reason} -> + ?LOG(error, "Failed to open session du to ~p", [Reason]), + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) + end. + +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). + +gets(_, undefined) -> + undefined; +gets([H | T], Map) -> + gets(T, maps:get(H, Map, undefined)); +gets([], Val) -> + Val. + +%%-------------------------------------------------------------------- +%% Call Chain +%%-------------------------------------------------------------------- +call_session(Fun, + Msg, + #channel{session = Session, + validator = Validator} = Channel) -> + iter([ session, fun process_session/4 + , proto, fun process_protocol/4 + , return, fun process_return/4 + , lifetime, fun process_lifetime/4 + , reply, fun process_reply/4 + , out, fun process_out/4 + , fun process_nothing/3 + ], + emqx_lwm2m_session:Fun(Msg, Validator, Session), + Channel). + +process_session(Session, Result, Channel, Iter) -> + iter(Iter, Result, Channel#channel{session = Session}). + +process_protocol({request, Msg}, Result, Channel, Iter) -> + #coap_message{method = Method} = Msg, + handle_request_protocol(Method, Msg, Result, Channel, Iter); + +process_protocol(Msg, Result, + #channel{validator = Validator, session = Session} = Channel, Iter) -> + ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, Validator, Session), + iter(Iter, maps:merge(Result, ProtoResult), Channel). + +handle_request_protocol(post, #coap_message{options = Opts} = Msg, + Result, Channel, Iter) -> + case Opts of + #{uri_path := [?REG_PREFIX]} -> + do_connect(Msg, Result, Channel, Iter); + #{uri_path := Location} -> + do_update(Location, Msg, Result, Channel, Iter); + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end; + +handle_request_protocol(delete, #coap_message{options = Opts} = Msg, + Result, Channel, Iter) -> + case Opts of + #{uri_path := Location} -> + case check_location(Location, Channel) of + true -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, Reply, Channel}; + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end; + _ -> + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) + end. + +do_update(Location, Msg, Result, + #channel{session = Session, validator = Validator} = Channel, Iter) -> + case check_location(Location, Channel) of + true -> + NewResult = emqx_lwm2m_session:update(Msg, Validator, Session), + iter(Iter, maps:merge(Result, NewResult), Channel); + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end. + +process_return({Outs, Session}, Result, Channel, Iter) -> + OldOuts = maps:get(out, Result, []), + iter(Iter, + Result#{out => Outs ++ OldOuts}, + Channel#channel{session = Session}). + +process_out(Outs, Result, Channel, _) -> + Outs2 = lists:reverse(Outs), + Outs3 = case maps:get(reply, Result, undefined) of + undefined -> + Outs2; + Reply -> + [Reply | Outs2] + end, + %% emqx_gateway_conn bug, work around + case Outs3 of + [] -> + {ok, Channel}; + _ -> + {ok, {outgoing, Outs3}, Channel} + end. + +process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> + Session2 = emqx_lwm2m_session:set_reply(Reply, Session), + Outs = maps:get(out, Result, []), + Outs2 = lists:reverse(Outs), + {ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}. + +process_lifetime(_, Result, Channel, Iter) -> + iter(Iter, Result, update_life_timer(Channel)). + +process_nothing(_, _, Channel) -> + {ok, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl deleted file mode 100644 index 16e938b84..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl +++ /dev/null @@ -1,153 +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. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_cm). - --export([start_link/0]). - --export([ register_channel/5 - , update_reg_info/2 - , unregister_channel/1 - ]). - --export([ lookup_channel/1 - , all_channels/0 - ]). - --export([ register_cmd/3 - , register_cmd/4 - , lookup_cmd/3 - , lookup_cmd_by_imei/1 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-CM: " ++ Format, Args)). - -%% Server name --define(CM, ?MODULE). - --define(LWM2M_CHANNEL_TAB, emqx_lwm2m_channel). --define(LWM2M_CMD_TAB, emqx_lwm2m_cmd). - -%% Batch drain --define(BATCH_SIZE, 100000). - -%% @doc Start the channel manager. -start_link() -> - gen_server:start_link({local, ?CM}, ?MODULE, [], []). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -register_channel(IMEI, RegInfo, LifeTime, Ver, Peername) -> - Info = #{ - reg_info => RegInfo, - lifetime => LifeTime, - version => Ver, - peername => Peername - }, - true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, Info}), - cast({registered, {IMEI, self()}}). - -update_reg_info(IMEI, RegInfo) -> - case lookup_channel(IMEI) of - [{_, RegInfo0}] -> - true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, RegInfo0#{reg_info => RegInfo}}), - ok; - [] -> - ok - end. - -unregister_channel(IMEI) when is_binary(IMEI) -> - true = ets:delete(?LWM2M_CHANNEL_TAB, IMEI), - ok. - -lookup_channel(IMEI) -> - ets:lookup(?LWM2M_CHANNEL_TAB, IMEI). - -all_channels() -> - ets:tab2list(?LWM2M_CHANNEL_TAB). - -register_cmd(IMEI, Path, Type) -> - true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, undefined}). - -register_cmd(_IMEI, undefined, _Type, _Result) -> - ok; -register_cmd(IMEI, Path, Type, Result) -> - true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, Result}). - -lookup_cmd(IMEI, Path, Type) -> - ets:lookup(?LWM2M_CMD_TAB, {IMEI, Path, Type}). - -lookup_cmd_by_imei(IMEI) -> - ets:select(?LWM2M_CHANNEL_TAB, [{{{IMEI, '_', '_'}, '$1'}, [], ['$_']}]). - -%% @private -cast(Msg) -> gen_server:cast(?CM, Msg). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([]) -> - TabOpts = [public, {write_concurrency, true}, {read_concurrency, true}], - ok = emqx_tables:new(?LWM2M_CHANNEL_TAB, [set, compressed | TabOpts]), - ok = emqx_tables:new(?LWM2M_CMD_TAB, [set, compressed | TabOpts]), - {ok, #{chan_pmon => emqx_pmon:new()}}. - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast({registered, {IMEI, ChanPid}}, State = #{chan_pmon := PMon}) -> - PMon1 = emqx_pmon:monitor(ChanPid, IMEI, PMon), - {noreply, State#{chan_pmon := PMon1}}; - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) -> - ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], - {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), - ok = emqx_pool:async_submit(fun lists:foreach/2, [fun clean_down/1, Items]), - {noreply, State#{chan_pmon := PMon1}}; - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - emqx_stats:cancel_update(chan_stats). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -clean_down({_ChanPid, IMEI}) -> - unregister_channel(IMEI). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl new file mode 100644 index 000000000..925ca1d94 --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -0,0 +1,410 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2016-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% 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_lwm2m_cmd). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). + +-export([ mqtt_to_coap/2 + , coap_to_mqtt/4 + , empty_ack_to_mqtt/1 + , coap_failure_to_mqtt/2 + ]). + +-export([path_list/1, extract_path/1]). + +-define(STANDARD, 1). + +%-type msg_type() :: <<"create">> +% | <<"delete">> +% | <<"read">> +% | <<"write">> +% | <<"execute">> +% | <<"discover">> +% | <<"write-attr">> +% | <<"observe">> +% | <<"cancel-observe">>. +% + %-type cmd() :: #{ <<"msgType">> := msg_type() + % , <<"data">> := maps() + % %% more keys? + % }. + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"basePath">>, Data, <<"/">>)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)), + Payload = emqx_lwm2m_tlv:encode(TlvData), + CoapRequest = emqx_coap_message:request(con, post, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]), + {CoapRequest, InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, delete, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) -> + CoapRequest = + case maps:get(<<"basePath">>, Data, <<"/">>) of + <<"/">> -> + single_write_request(AlternatePath, Data); + BasePath -> + batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data)) + end, + {CoapRequest, InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + Args = + case maps:get(<<"args">>, Data, <<>>) of + <<"undefined">> -> <<>>; + undefined -> <<>>; + Arg1 -> Arg1 + end, + {emqx_coap_message:request(con, post, Args, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"text/plain">>}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + Query = attr_query_list(Data), + {emqx_coap_message:request(con, put, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {uri_query, Query}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {observe, 0}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {observe, 1}]), InputCmd}. + +coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) -> + make_response(Code, Ref); + +coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) -> + make_response(Code, Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) -> + read_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) -> + write_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) -> + execute_resp_to_mqtt(Method, Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) -> + discover_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) -> + writeattr_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) -> + observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) -> + cancel_observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref). + +read_resp_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) -> + make_response(ErrorCode, Ref); + +read_resp_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) -> + try + Result = content_to_mqtt(CoapPayload, Format, Ref), + make_response(SuccessCode, Ref, Format, Result) + catch + error:not_implemented -> make_response(not_implemented, Ref); + _:Ex:_ST -> + ?LOG(error, "~0p, bad payload format: ~0p", [Ex, CoapPayload]), + make_response(bad_request, Ref) + end. + +empty_ack_to_mqtt(Ref) -> + make_base_response(maps:put(<<"msgType">>, <<"ack">>, Ref)). + +coap_failure_to_mqtt(Ref, MsgType) -> + make_base_response(maps:put(<<"msgType">>, MsgType, Ref)). + +content_to_mqtt(CoapPayload, <<"text/plain">>, Ref) -> + emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/octet-stream">>, Ref) -> + emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) -> + emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) -> + emqx_lwm2m_message:translate_json(CoapPayload). + +write_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +write_resp_to_mqtt({ok, content}, CoapPayload, Ref) when CoapPayload =:= <<>> -> + make_response(method_not_allowed, Ref); + +write_resp_to_mqtt({ok, content}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +write_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +execute_resp_to_mqtt({ok, changed}, Ref) -> + make_response(changed, Ref); + +execute_resp_to_mqtt({error, Error}, Ref) -> + make_response(Error, Ref). + +discover_resp_to_mqtt({ok, content}, CoapPayload, Ref) -> + Links = binary:split(CoapPayload, <<",">>, [global]), + make_response(content, Ref, <<"application/link-format">>, Links); + +discover_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +writeattr_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +writeattr_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) -> + make_response(Error, Ref); + +observe_resp_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref); + +observe_resp_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref#{<<"seqNum">> => ObserveSeqNum}). + +cancel_observe_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref); + +cancel_observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) -> + make_response(Error, Ref). + +make_response(Code, Ref=#{}) -> + BaseRsp = make_base_response(Ref), + make_data_response(BaseRsp, Code). + +make_response(Code, Ref=#{}, _Format, Result) -> + BaseRsp = make_base_response(Ref), + make_data_response(BaseRsp, Code, _Format, Result). + +%% The base response format is what included in the request: +%% +%% #{ +%% <<"seqNum">> => SeqNum, +%% <<"imsi">> => maps:get(<<"imsi">>, Ref, null), +%% <<"imei">> => maps:get(<<"imei">>, Ref, null), +%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null), +%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null), +%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null) +%% } + +make_base_response(Ref=#{}) -> + remove_tmp_fields(Ref). + +make_data_response(BaseRsp, Code) -> + BaseRsp#{ + <<"data">> => #{ + <<"reqPath">> => extract_path(BaseRsp), + <<"code">> => code(Code), + <<"codeMsg">> => Code + } + }. + +make_data_response(BaseRsp, Code, _Format, Result) -> + BaseRsp#{ + <<"data">> => + #{ + <<"reqPath">> => extract_path(BaseRsp), + <<"code">> => code(Code), + <<"codeMsg">> => Code, + <<"content">> => Result + } + }. + +remove_tmp_fields(Ref) -> + maps:remove(observe_type, Ref). + +-spec path_list(Path::binary()) -> {[PathWord::binary()], [Query::binary()]}. +path_list(Path) -> + case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of + [ObjId, ObjInsId, ResId, LastPart] -> + {ResInstId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId, ResId, ResInstId], QueryList}; + [ObjId, ObjInsId, LastPart] -> + {ResId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId, ResId], QueryList}; + [ObjId, LastPart] -> + {ObjInsId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId], QueryList}; + [LastPart] -> + {ObjId, QueryList} = query_list(LastPart), + {[ObjId], QueryList} + end. + +query_list(PathWithQuery) -> + case binary:split(PathWithQuery, [<<$?>>], []) of + [Path] -> {Path, []}; + [Path, Querys] -> + {Path, binary:split(Querys, [<<$&>>], [global])} + end. + +attr_query_list(Data) -> + attr_query_list(Data, valid_attr_keys(), []). + +attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) -> + maps:fold( + fun + (_K, null, Acc) -> Acc; + (K, V, Acc) -> + case lists:member(K, ValidAttrKeys) of + true -> + KV = <>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>]. + +data_format(Options) -> + maps:get(content_format, Options, <<"text/plain">>). + +observe_seq(Options) -> + maps:get(observe, Options, rand:uniform(1000000) + 1 ). + +add_alternate_path_prefix(<<"/">>, PathList) -> + PathList; + +add_alternate_path_prefix(AlternatePath, PathList) -> + [binary_util:trim(AlternatePath, $/) | PathList]. + +extract_path(Ref = #{}) -> + drop_query( + case Ref of + #{<<"data">> := Data} -> + case maps:get(<<"path">>, Data, nil) of + nil -> maps:get(<<"basePath">>, Data, undefined); + Path -> Path + end; + #{<<"path">> := Path} -> + Path + end). + + +batch_write_request(AlternatePath, BasePath, Content) -> + {PathList, QueryList} = path_list(BasePath), + Method = case length(PathList) of + 2 -> post; + 3 -> put + end, + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content), + Payload = emqx_lwm2m_tlv:encode(TlvData), + emqx_coap_message:request(con, Method, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). + +single_write_request(AlternatePath, Data) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + %% TO DO: handle write to resource instance, e.g. /4/0/1/0 + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, [Data]), + Payload = emqx_lwm2m_tlv:encode(TlvData), + emqx_coap_message:request(con, put, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). + +drop_query(Path) -> + case binary:split(Path, [<<$?>>]) of + [Path] -> Path; + [PathOnly, _Query] -> PathOnly + end. + +code(get) -> <<"0.01">>; +code(post) -> <<"0.02">>; +code(put) -> <<"0.03">>; +code(delete) -> <<"0.04">>; +code(created) -> <<"2.01">>; +code(deleted) -> <<"2.02">>; +code(valid) -> <<"2.03">>; +code(changed) -> <<"2.04">>; +code(content) -> <<"2.05">>; +code(continue) -> <<"2.31">>; +code(bad_request) -> <<"4.00">>; +code(unauthorized) -> <<"4.01">>; +code(bad_option) -> <<"4.02">>; +code(forbidden) -> <<"4.03">>; +code(not_found) -> <<"4.04">>; +code(method_not_allowed) -> <<"4.05">>; +code(not_acceptable) -> <<"4.06">>; +code(request_entity_incomplete) -> <<"4.08">>; +code(precondition_failed) -> <<"4.12">>; +code(request_entity_too_large) -> <<"4.13">>; +code(unsupported_content_format) -> <<"4.15">>; +code(internal_server_error) -> <<"5.00">>; +code(not_implemented) -> <<"5.01">>; +code(bad_gateway) -> <<"5.02">>; +code(service_unavailable) -> <<"5.03">>; +code(gateway_timeout) -> <<"5.04">>; +code(proxying_not_supported) -> <<"5.05">>. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl deleted file mode 100644 index 318328e3c..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl +++ /dev/null @@ -1,310 +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_lwm2m_cmd_handler). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --include_lib("lwm2m_coap/include/coap.hrl"). - --export([ mqtt2coap/2 - , coap2mqtt/4 - , ack2mqtt/1 - , extract_path/1 - ]). - --export([path_list/1]). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-CMD: " ++ Format, Args)). - -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) -> - PathList = path_list(maps:get(<<"basePath">>, Data, <<"/">>)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)), - Payload = emqx_lwm2m_tlv:encode(TlvData), - CoapRequest = lwm2m_coap_message:request(con, post, Payload, [{uri_path, FullPathList}, - {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]), - {CoapRequest, InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - {lwm2m_coap_message:request(con, delete, <<>>, [{uri_path, FullPathList}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) -> - Encoding = maps:get(<<"encoding">>, InputCmd, <<"plain">>), - CoapRequest = - case maps:get(<<"basePath">>, Data, <<"/">>) of - <<"/">> -> - single_write_request(AlternatePath, Data, Encoding); - BasePath -> - batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data), Encoding) - end, - {CoapRequest, InputCmd}; - -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - Args = - case maps:get(<<"args">>, Data, <<>>) of - <<"undefined">> -> <<>>; - undefined -> <<>>; - Arg1 -> Arg1 - end, - {lwm2m_coap_message:request(con, post, Args, [{uri_path, FullPathList}, {content_format, <<"text/plain">>}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - Query = attr_query_list(Data), - {lwm2m_coap_message:request(con, put, <<>>, [{uri_path, FullPathList}, {uri_query, Query}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) -> - PathList = path_list(maps:get(<<"path">>, Data)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 0}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) -> - PathList = path_list(maps:get(<<"path">>, Data)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 1}]), InputCmd}. - -coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) -> - make_response(Code, Ref); -coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) -> - make_response(Code, Ref); -coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) -> - coap_read_to_mqtt(Method, CoapPayload, data_format(Options), Ref); -coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) -> - coap_write_to_mqtt(Method, Ref); -coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) -> - coap_execute_to_mqtt(Method, Ref); -coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) -> - coap_discover_to_mqtt(Method, CoapPayload, Ref); -coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) -> - coap_writeattr_to_mqtt(Method, CoapPayload, Ref); -coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) -> - coap_observe_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref); -coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) -> - coap_cancel_observe_to_mqtt(Method, CoapPayload, data_format(Options), Ref). - -coap_read_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) -> - make_response(ErrorCode, Ref); -coap_read_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) -> - try - Result = coap_content_to_mqtt_payload(CoapPayload, Format, Ref), - make_response(SuccessCode, Ref, Format, Result) - catch - error:not_implemented -> make_response(not_implemented, Ref); - C:R:Stack -> - ?LOG(error, "~p, bad payload format: ~p, stacktrace: ~p", [{C, R}, CoapPayload, Stack]), - make_response(bad_request, Ref) - end. - -ack2mqtt(Ref) -> - make_base_response(Ref). - -coap_content_to_mqtt_payload(CoapPayload, <<"text/plain">>, Ref) -> - emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload); -coap_content_to_mqtt_payload(CoapPayload, <<"application/octet-stream">>, Ref) -> - emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload); -coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) -> - emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload); -coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) -> - emqx_lwm2m_message:translate_json(CoapPayload). - -coap_write_to_mqtt({ok, changed}, Ref) -> - make_response(changed, Ref); -coap_write_to_mqtt({error, Error}, Ref) -> - make_response(Error, Ref). - -coap_execute_to_mqtt({ok, changed}, Ref) -> - make_response(changed, Ref); -coap_execute_to_mqtt({error, Error}, Ref) -> - make_response(Error, Ref). - -coap_discover_to_mqtt({ok, content}, CoapPayload, Ref) -> - Links = binary:split(CoapPayload, <<",">>), - make_response(content, Ref, <<"application/link-format">>, Links); -coap_discover_to_mqtt({error, Error}, _CoapPayload, Ref) -> - make_response(Error, Ref). - -coap_writeattr_to_mqtt({ok, changed}, _CoapPayload, Ref) -> - make_response(changed, Ref); -coap_writeattr_to_mqtt({error, Error}, _CoapPayload, Ref) -> - make_response(Error, Ref). - -coap_observe_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) -> - make_response(Error, Ref); -coap_observe_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) -> - coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref); -coap_observe_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) -> - RefWithObserve = maps:put(<<"seqNum">>, ObserveSeqNum, Ref), - RefNotify = maps:put(<<"msgType">>, <<"notify">>, RefWithObserve), - coap_read_to_mqtt({ok, content}, CoapPayload, Format, RefNotify). - -coap_cancel_observe_to_mqtt({ok, content}, CoapPayload, Format, Ref) -> - coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref); -coap_cancel_observe_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) -> - make_response(Error, Ref). - -make_response(Code, Ref=#{}) -> - BaseRsp = make_base_response(Ref), - make_data_response(BaseRsp, Code). -make_response(Code, Ref=#{}, _Format, Result) -> - BaseRsp = make_base_response(Ref), - make_data_response(BaseRsp, Code, _Format, Result). - -%% The base response format is what included in the request: -%% -%% #{ -%% <<"seqNum">> => SeqNum, -%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null), -%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null), -%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null) -%% } - -make_base_response(Ref=#{}) -> - remove_tmp_fields(Ref). - -make_data_response(BaseRsp, Code) -> - BaseRsp#{ - <<"data">> => #{ - <<"reqPath">> => extract_path(BaseRsp), - <<"code">> => code(Code), - <<"codeMsg">> => Code - } - }. -make_data_response(BaseRsp, Code, _Format, Result) -> - BaseRsp#{ - <<"data">> => #{ - <<"reqPath">> => extract_path(BaseRsp), - <<"code">> => code(Code), - <<"codeMsg">> => Code, - <<"content">> => Result - } - }. - -remove_tmp_fields(Ref) -> - maps:remove(observe_type, Ref). - -path_list(Path) -> - case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of - [ObjId, ObjInsId, ResId, ResInstId] -> [ObjId, ObjInsId, ResId, ResInstId]; - [ObjId, ObjInsId, ResId] -> [ObjId, ObjInsId, ResId]; - [ObjId, ObjInsId] -> [ObjId, ObjInsId]; - [ObjId] -> [ObjId] - end. - -attr_query_list(Data) -> - attr_query_list(Data, valid_attr_keys(), []). -attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) -> - maps:fold( - fun - (_K, null, Acc) -> Acc; - (K, V, Acc) -> - case lists:member(K, ValidAttrKeys) of - true -> - Val = bin(V), - KV = <>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>]. - -data_format(Options) -> - proplists:get_value(content_format, Options, <<"text/plain">>). -observe_seq(Options) -> - proplists:get_value(observe, Options, rand:uniform(1000000) + 1 ). - -add_alternate_path_prefix(<<"/">>, PathList) -> - PathList; -add_alternate_path_prefix(AlternatePath, PathList) -> - [binary_util:trim(AlternatePath, $/) | PathList]. - -extract_path(Ref = #{}) -> - case Ref of - #{<<"data">> := Data} -> - case maps:get(<<"path">>, Data, nil) of - nil -> maps:get(<<"basePath">>, Data, undefined); - Path -> Path - end; - #{<<"path">> := Path} -> - Path - end. - -batch_write_request(AlternatePath, BasePath, Content, Encoding) -> - PathList = path_list(BasePath), - Method = case length(PathList) of - 2 -> post; - 3 -> put - end, - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - Content1 = decoding(Content, Encoding), - TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content1), - Payload = emqx_lwm2m_tlv:encode(TlvData), - lwm2m_coap_message:request(con, Method, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). - -single_write_request(AlternatePath, Data, Encoding) -> - PathList = path_list(maps:get(<<"path">>, Data)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - Datas = decoding([Data], Encoding), - TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Datas), - Payload = emqx_lwm2m_tlv:encode(TlvData), - lwm2m_coap_message:request(con, put, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). - - -code(get) -> <<"0.01">>; -code(post) -> <<"0.02">>; -code(put) -> <<"0.03">>; -code(delete) -> <<"0.04">>; -code(created) -> <<"2.01">>; -code(deleted) -> <<"2.02">>; -code(valid) -> <<"2.03">>; -code(changed) -> <<"2.04">>; -code(content) -> <<"2.05">>; -code(continue) -> <<"2.31">>; -code(bad_request) -> <<"4.00">>; -code(uauthorized) -> <<"4.01">>; -code(bad_option) -> <<"4.02">>; -code(forbidden) -> <<"4.03">>; -code(not_found) -> <<"4.04">>; -code(method_not_allowed) -> <<"4.05">>; -code(not_acceptable) -> <<"4.06">>; -code(request_entity_incomplete) -> <<"4.08">>; -code(precondition_failed) -> <<"4.12">>; -code(request_entity_too_large) -> <<"4.13">>; -code(unsupported_content_format) -> <<"4.15">>; -code(internal_server_error) -> <<"5.00">>; -code(not_implemented) -> <<"5.01">>; -code(bad_gateway) -> <<"5.02">>; -code(service_unavailable) -> <<"5.03">>; -code(gateway_timeout) -> <<"5.04">>; -code(proxying_not_supported) -> <<"5.05">>. - -bin(Bin) when is_binary(Bin) -> Bin; -bin(Str) when is_list(Str) -> list_to_binary(Str); -bin(Int) when is_integer(Int) -> integer_to_binary(Int); -bin(Float) when is_float(Float) -> float_to_binary(Float). - -decoding(Datas, <<"hex">>) -> - lists:map(fun(Data = #{<<"value">> := Value}) -> - Data#{<<"value">> => emqx_misc:hexstr2bin(Value)} - end, Datas); -decoding(Datas, _) -> - Datas. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl deleted file mode 100644 index 588dd523e..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl +++ /dev/null @@ -1,386 +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_lwm2m_coap_resource). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("emqx/include/emqx_mqtt.hrl"). - --include_lib("lwm2m_coap/include/coap.hrl"). - -% -behaviour(lwm2m_coap_resource). - --export([ coap_discover/2 - , coap_get/5 - , coap_post/5 - , coap_put/5 - , coap_delete/4 - , coap_observe/5 - , coap_unobserve/1 - , coap_response/7 - , coap_ack/3 - , handle_info/2 - , handle_call/3 - , handle_cast/2 - , terminate/2 - ]). - --export([parse_object_list/1]). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --define(PREFIX, <<"rd">>). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-RESOURCE: " ++ Format, Args)). - --dialyzer([{nowarn_function, [coap_discover/2]}]). -% we use {'absolute', list(binary()), [{atom(), binary()}]} as coap_uri() -% https://github.com/emqx/lwm2m-coap/blob/258e9bd3762124395e83c1e68a1583b84718230f/src/lwm2m_coap_resource.erl#L61 -% resource operations -coap_discover(_Prefix, _Args) -> - [{absolute, [<<"mqtt">>], []}]. - -coap_get(ChId, [?PREFIX], Query, Content, Lwm2mState) -> - ?LOG(debug, "~p ~p GET Query=~p, Content=~p", [self(),ChId, Query, Content]), - {ok, #coap_content{}, Lwm2mState}; -coap_get(ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "ignore bad put request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -% LWM2M REGISTER COMMAND -coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = [?PREFIX]}, Lwm2mState) -> - ?LOG(debug, "~p ~p REGISTER command Query=~p, Content=~p", [self(), ChId, Query, Content]), - case parse_options(Query) of - {error, {bad_opt, _CustomOption}} -> - ?LOG(error, "Reject REGISTER from ~p due to wrong option", [ChId]), - {error, bad_request, Lwm2mState}; - {ok, LwM2MQuery} -> - process_register(ChId, LwM2MQuery, Content#coap_content.payload, Lwm2mState) - end; - -% LWM2M UPDATE COMMAND -coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = LocationPath}, Lwm2mState) -> - ?LOG(debug, "~p ~p UPDATE command location=~p, Query=~p, Content=~p", [self(), ChId, LocationPath, Query, Content]), - case parse_options(Query) of - {error, {bad_opt, _CustomOption}} -> - ?LOG(error, "Reject UPDATE from ~p due to wrong option, Query=~p", [ChId, Query]), - {error, bad_request, Lwm2mState}; - {ok, LwM2MQuery} -> - process_update(ChId, LwM2MQuery, LocationPath, Content#coap_content.payload, Lwm2mState) - end; - -coap_post(ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "bad post request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -coap_put(_ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "put has error, Prefix=~p, Query=~p, Content=~p", [Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -% LWM2M DE-REGISTER COMMAND -coap_delete(ChId, [?PREFIX], #coap_content{uri_path = Location}, Lwm2mState) -> - LocationPath = binary_util:join_path(Location), - ?LOG(debug, "~p ~p DELETE command location=~p", [self(), ChId, LocationPath]), - case get(lwm2m_context) of - #lwm2m_context{location = LocationPath} -> - lwm2m_coap_responder:stop(deregister), - {ok, Lwm2mState}; - undefined -> - ?LOG(error, "Reject DELETE from ~p, Location: ~p not found", [ChId, Location]), - {error, forbidden, Lwm2mState}; - TrueLocation -> - ?LOG(error, "Reject DELETE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]), - {error, not_found, Lwm2mState} - end; -coap_delete(_ChId, _Prefix, _Content, Lwm2mState) -> - {error, forbidden, Lwm2mState}. - -coap_observe(ChId, Prefix, Name, Ack, Lwm2mState) -> - ?LOG(error, "unsupported observe request ChId=~p, Prefix=~p, Name=~p, Ack=~p", [ChId, Prefix, Name, Ack]), - {error, method_not_allowed, Lwm2mState}. - -coap_unobserve(Lwm2mState) -> - ?LOG(error, "unsupported unobserve request: ~p", [Lwm2mState]), - {ok, Lwm2mState}. - -coap_response(ChId, Ref, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Lwm2mState) -> - ?LOG(info, "~p, RCV CoAP response, CoapMsgType: ~p, CoapMsgMethod: ~p, CoapMsgPayload: ~p, - CoapMsgOpts: ~p, Ref: ~p", - [ChId, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref]), - MqttPayload = emqx_lwm2m_cmd_handler:coap2mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref), - Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState), - {noreply, Lwm2mState2}. - -coap_ack(_ChId, Ref, Lwm2mState) -> - ?LOG(info, "~p, RCV CoAP Empty ACK, Ref: ~p", [_ChId, Ref]), - AckRef = maps:put(<<"msgType">>, <<"ack">>, Ref), - MqttPayload = emqx_lwm2m_cmd_handler:ack2mqtt(AckRef), - Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState), - {ok, Lwm2mState2}. - -%% Batch deliver -handle_info({deliver, Topic, Msgs}, Lwm2mState) when is_list(Msgs) -> - {noreply, lists:foldl(fun(Msg, NewState) -> - element(2, handle_info({deliver, Topic, Msg}, NewState)) - end, Lwm2mState, Msgs)}; -%% Handle MQTT Message -handle_info({deliver, _Topic, MqttMsg}, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:deliver(MqttMsg, Lwm2mState), - {noreply, Lwm2mState2}; - -%% Deliver Coap Message to Device -handle_info({deliver_to_coap, CoapRequest, Ref}, Lwm2mState) -> - {send_request, CoapRequest, Ref, Lwm2mState}; - -handle_info({'EXIT', _Pid, Reason}, Lwm2mState) -> - ?LOG(info, "~p, received exit from: ~p, reason: ~p, quit now!", [self(), _Pid, Reason]), - {stop, Reason, Lwm2mState}; - -handle_info(post_init, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:post_init(Lwm2mState), - {noreply, Lwm2mState2}; - -handle_info(auto_observe, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:auto_observe(Lwm2mState), - {noreply, Lwm2mState2}; - -handle_info({life_timer, expired}, Lwm2mState) -> - ?LOG(debug, "lifetime expired, shutdown", []), - {stop, life_timer_expired, Lwm2mState}; - -handle_info({shutdown, Error}, Lwm2mState) -> - {stop, Error, Lwm2mState}; - -handle_info({shutdown, conflict, {ClientId, NewPid}}, Lwm2mState) -> - ?LOG(warning, "lwm2m '~s' conflict with ~p, shutdown", [ClientId, NewPid]), - {stop, conflict, Lwm2mState}; - -handle_info({suback, _MsgId, [_GrantedQos]}, Lwm2mState) -> - {noreply, Lwm2mState}; - -handle_info(emit_stats, Lwm2mState) -> - {noreply, Lwm2mState}; - -handle_info(Message, Lwm2mState) -> - ?LOG(error, "Unknown Message ~p", [Message]), - {noreply, Lwm2mState}. - - -handle_call(info, _From, Lwm2mState) -> - {Info, Lwm2mState2} = emqx_lwm2m_protocol:get_info(Lwm2mState), - {reply, Info, Lwm2mState2}; - -handle_call(stats, _From, Lwm2mState) -> - {Stats, Lwm2mState2} = emqx_lwm2m_protocol:get_stats(Lwm2mState), - {reply, Stats, Lwm2mState2}; - -handle_call(kick, _From, Lwm2mState) -> - {stop, kick, Lwm2mState}; - -handle_call({set_rate_limit, _Rl}, _From, Lwm2mState) -> - ?LOG(error, "set_rate_limit is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(get_rate_limit, _From, Lwm2mState) -> - ?LOG(error, "get_rate_limit is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(session, _From, Lwm2mState) -> - ?LOG(error, "get_session is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(Request, _From, Lwm2mState) -> - ?LOG(error, "adapter unexpected call ~p", [Request]), - {reply, ok, Lwm2mState}. - -handle_cast(Msg, Lwm2mState) -> - ?LOG(error, "unexpected cast ~p", [Msg]), - {noreply, Lwm2mState, hibernate}. - -terminate(Reason, Lwm2mState) -> - emqx_lwm2m_protocol:terminate(Reason, Lwm2mState). - -%%%%%%%%%%%%%%%%%%%%%% -%% Internal Functions -%%%%%%%%%%%%%%%%%%%%%% -process_register(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState) -> - Epn = maps:get(<<"ep">>, LwM2MQuery, undefined), - LifeTime = maps:get(<<"lt">>, LwM2MQuery, undefined), - Ver = maps:get(<<"lwm2m">>, LwM2MQuery, undefined), - case check_lwm2m_version(Ver) of - false -> - ?LOG(error, "Reject REGISTER from ~p due to unsupported version: ~p", [ChId, Ver]), - lwm2m_coap_responder:stop(invalid_version), - {error, precondition_failed, Lwm2mState}; - true -> - case check_epn(Epn) andalso check_lifetime(LifeTime) of - true -> - init_lwm2m_emq_client(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState); - false -> - ?LOG(error, "Reject REGISTER from ~p due to wrong parameters, epn=~p, lifetime=~p", [ChId, Epn, LifeTime]), - lwm2m_coap_responder:stop(invalid_query_params), - {error, bad_request, Lwm2mState} - end - end. - -process_update(ChId, LwM2MQuery, Location, LwM2MPayload, Lwm2mState) -> - LocationPath = binary_util:join_path(Location), - case get(lwm2m_context) of - #lwm2m_context{location = LocationPath} -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - Lwm2mState2 = emqx_lwm2m_protocol:update_reg_info(RegInfo, Lwm2mState), - ?LOG(info, "~p, UPDATE Success, assgined location: ~p", [ChId, LocationPath]), - {ok, changed, #coap_content{}, Lwm2mState2}; - undefined -> - ?LOG(error, "Reject UPDATE from ~p, Location: ~p not found", [ChId, Location]), - {error, forbidden, Lwm2mState}; - TrueLocation -> - ?LOG(error, "Reject UPDATE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]), - {error, not_found, Lwm2mState} - end. - -init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, _Lwm2mState = undefined) -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - case emqx_lwm2m_protocol:init(self(), Epn, ChId, RegInfo) of - {ok, Lwm2mState} -> - LocationPath = assign_location_path(Epn), - ?LOG(info, "~p, REGISTER Success, assgined location: ~p", [ChId, LocationPath]), - {ok, created, #coap_content{location_path = LocationPath}, Lwm2mState}; - {error, Error} -> - lwm2m_coap_responder:stop(Error), - ?LOG(error, "~p, REGISTER Failed, error: ~p", [ChId, Error]), - {error, forbidden, undefined} - end; -init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, Lwm2mState) -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - LocationPath = assign_location_path(Epn), - ?LOG(info, "~p, RE-REGISTER Success, location: ~p", [ChId, LocationPath]), - Lwm2mState2 = emqx_lwm2m_protocol:replace_reg_info(RegInfo, Lwm2mState), - {ok, created, #coap_content{location_path = LocationPath}, Lwm2mState2}. - -append_object_list(LwM2MQuery, <<>>) when map_size(LwM2MQuery) == 0 -> #{}; -append_object_list(LwM2MQuery, <<>>) -> LwM2MQuery; -append_object_list(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) -> - {AlterPath, ObjList} = parse_object_list(LwM2MPayload), - LwM2MQuery#{ - <<"alternatePath">> => AlterPath, - <<"objectList">> => ObjList - }. - -parse_options(InputQuery) -> - parse_options(InputQuery, maps:new()). - -parse_options([], Query) -> {ok, Query}; -parse_options([<<"ep=", Epn/binary>>|T], Query) -> - parse_options(T, maps:put(<<"ep">>, Epn, Query)); -parse_options([<<"lt=", Lt/binary>>|T], Query) -> - parse_options(T, maps:put(<<"lt">>, binary_to_integer(Lt), Query)); -parse_options([<<"lwm2m=", Ver/binary>>|T], Query) -> - parse_options(T, maps:put(<<"lwm2m">>, Ver, Query)); -parse_options([<<"b=", Binding/binary>>|T], Query) -> - parse_options(T, maps:put(<<"b">>, Binding, Query)); -parse_options([CustomOption|T], Query) -> - case binary:split(CustomOption, <<"=">>) of - [OptKey, OptValue] when OptKey =/= <<>> -> - ?LOG(debug, "non-standard option: ~p", [CustomOption]), - parse_options(T, maps:put(OptKey, OptValue, Query)); - _BadOpt -> - ?LOG(error, "bad option: ~p", [CustomOption]), - {error, {bad_opt, CustomOption}} - end. - -parse_object_list(<<>>) -> {<<"/">>, <<>>}; -parse_object_list(ObjLinks) when is_binary(ObjLinks) -> - parse_object_list(binary:split(ObjLinks, <<",">>, [global])); - -parse_object_list(FullObjLinkList) when is_list(FullObjLinkList) -> - case drop_attr(FullObjLinkList) of - {<<"/">>, _} = RootPrefixedLinks -> - RootPrefixedLinks; - {AlterPath, ObjLinkList} -> - LenAlterPath = byte_size(AlterPath), - WithOutPrefix = - lists:map( - fun - (<>) when Prefix =:= AlterPath -> - trim(Link); - (Link) -> Link - end, ObjLinkList), - {AlterPath, WithOutPrefix} - end. - -drop_attr(LinkList) -> - lists:foldr( - fun(Link, {AlternatePath, LinkAcc}) -> - {MainLink, LinkAttrs} = parse_link(Link), - case is_alternate_path(LinkAttrs) of - false -> {AlternatePath, [MainLink | LinkAcc]}; - true -> {MainLink, LinkAcc} - end - end, {<<"/">>, []}, LinkList). - -is_alternate_path(#{<<"rt">> := ?OMA_ALTER_PATH_RT}) -> true; -is_alternate_path(_) -> false. - -parse_link(Link) -> - [MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]), - {delink(trim(MainLink)), parse_link_attrs(Attrs)}. - -parse_link_attrs(LinkAttrs) when is_list(LinkAttrs) -> - lists:foldl( - fun(Attr, Acc) -> - case binary:split(trim(Attr), <<"=">>) of - [AttrKey, AttrValue] when AttrKey =/= <<>> -> - maps:put(AttrKey, AttrValue, Acc); - _BadAttr -> throw({bad_attr, _BadAttr}) - end - end, maps:new(), LinkAttrs). - -trim(Str)-> binary_util:trim(Str, $ ). -delink(Str) -> - Ltrim = binary_util:ltrim(Str, $<), - binary_util:rtrim(Ltrim, $>). - -check_lwm2m_version(<<"1">>) -> true; -check_lwm2m_version(<<"1.", _PatchVerNum/binary>>) -> true; -check_lwm2m_version(_) -> false. - -check_epn(undefined) -> false; -check_epn(_) -> true. - -check_lifetime(undefined) -> false; -check_lifetime(LifeTime0) when is_integer(LifeTime0) -> - LifeTime = timer:seconds(LifeTime0), - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Max = maps:get(lifetime_max, Envs, 315360000), - Min = maps:get(lifetime_min, Envs, 0), - - if - LifeTime >= Min, LifeTime =< Max -> - true; - true -> - false - end; -check_lifetime(_) -> false. - - -assign_location_path(Epn) -> - %Location = list_to_binary(io_lib:format("~.16B", [rand:uniform(65535)])), - %LocationPath = <<"/rd/", Location/binary>>, - Location = [<<"rd">>, Epn], - put(lwm2m_context, #lwm2m_context{epn = Epn, location = binary_util:join_path(Location)}), - Location. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index c00f76532..0a96e98e1 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -50,24 +50,13 @@ unreg() -> on_gateway_load(_Gateway = #{ name := GwName, config := Config }, Ctx) -> - - %% Handler - _ = lwm2m_coap_server:start_registry(), - lwm2m_coap_server_registry:add_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), %% Xml registry {ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, Config)), - %% XXX: Self managed table? - %% TODO: Improve it later - {ok, _} = emqx_lwm2m_cm:start_link(), - Listeners = emqx_gateway_utils:normalize_config(Config), ListenerPids = lists:map(fun(Lis) -> - start_listener(GwName, Ctx, Lis) - end, Listeners), + start_listener(GwName, Ctx, Lis) + end, Listeners), {ok, ListenerPids, _GwState = #{ctx => Ctx}}. on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> @@ -88,12 +77,6 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> on_gateway_unload(_Gateway = #{ name := GwName, config := Config }, _GwState) -> - %% XXX: - lwm2m_coap_server_registry:remove_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), - Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) @@ -118,18 +101,13 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> Name = name(GwName, LisName, udp), - NCfg = Cfg#{ctx => Ctx}, + NCfg = Cfg#{ ctx => Ctx + , frame_mod => emqx_coap_frame + , chann_mod => emqx_lwm2m_channel + }, NSocketOpts = merge_default(SocketOpts), - Options = [{config, NCfg}|NSocketOpts], - case Type of - udp -> - lwm2m_coap_server:start_udp(Name, ListenOn, Options); - dtls -> - lwm2m_coap_server:start_dtls(Name, ListenOn, Options) - end. - -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + MFA = {emqx_gateway_conn, start_link, [NCfg]}, + do_start_listener(Type, Name, ListenOn, NSocketOpts, MFA). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -141,6 +119,16 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + +do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> + esockd:open_udp(Name, ListenOn, SocketOpts, MFA); + +do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> + esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). + + stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), @@ -155,9 +143,4 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> Name = name(GwName, LisName, Type), - case Type of - udp -> - lwm2m_coap_server:stop_udp(Name, ListenOn); - dtls -> - lwm2m_coap_server:stop_dtls(Name, ListenOn) - end. + esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl deleted file mode 100644 index 295c68085..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl +++ /dev/null @@ -1,351 +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_lwm2m_json). - --export([ tlv_to_json/2 - , json_to_tlv/2 - , text_to_json/2 - , opaque_to_json/2 - ]). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)). - -tlv_to_json(BaseName, TlvData) -> - DecodedTlv = emqx_lwm2m_tlv:parse(TlvData), - ObjectId = object_id(BaseName), - ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true), - case DecodedTlv of - [#{tlv_resource_with_value:=Id, value:=Value}] -> - TrueBaseName = basename(BaseName, undefined, undefined, Id, 3), - encode_json(TrueBaseName, tlv_single_resource(Id, Value, ObjDefinition)); - List1 = [#{tlv_resource_with_value:=_Id}, _|_] -> - TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2), - encode_json(TrueBaseName, tlv_level2(<<>>, List1, ObjDefinition, [])); - List2 = [#{tlv_multiple_resource:=_Id}|_] -> - TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2), - encode_json(TrueBaseName, tlv_level2(<<>>, List2, ObjDefinition, [])); - [#{tlv_object_instance:=Id, value:=Value}] -> - TrueBaseName = basename(BaseName, undefined, Id, undefined, 2), - encode_json(TrueBaseName, tlv_level2(<<>>, Value, ObjDefinition, [])); - List3=[#{tlv_object_instance:=Id, value:=_Value}, _|_] -> - TrueBaseName = basename(BaseName, Id, undefined, undefined, 1), - encode_json(TrueBaseName, tlv_level1(List3, ObjDefinition, [])) - end. - - -tlv_level1([], _ObjDefinition, Acc) -> - Acc; -tlv_level1([#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) -> - New = tlv_level2(integer_to_binary(Id), Value, ObjDefinition, []), - tlv_level1(T, ObjDefinition, Acc++New). - -tlv_level2(_, [], _, Acc) -> - Acc; -tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) -> - {K, V} = value(Value, ResourceId, ObjDefinition), - Name = name(RelativePath, ResourceId), - New = #{n => Name, K => V}, - tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]); -tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) -> - NewRelativePath = name(RelativePath, ResourceId), - SubList = tlv_level3(NewRelativePath, Value, ResourceId, ObjDefinition, []), - tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList). - -tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) -> - lists:reverse(Acc); -tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) -> - {K, V} = value(Value, ResourceId, ObjDefinition), - Name = name(RelativePath, InsId), - New = #{n => Name, K => V}, - tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]). - -tlv_single_resource(Id, Value, ObjDefinition) -> - {K, V} = value(Value, Id, ObjDefinition), - [#{K=>V}]. - -basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 3) -> - ?LOG(debug, "basename3 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]), - case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of - [ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>; - [ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>; - [ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>> - end; -basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 2) -> - ?LOG(debug, "basename2 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]), - case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of - [ObjId, ObjInsId, _ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>; - [ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>; - [ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary>> - end; -basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 1) -> - ?LOG(debug, "basename1 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]), - case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of - [ObjId, _ObjInsId, _ResId] -> <<$/, ObjId/binary>>; - [ObjId, _ObjInsId] -> <<$/, ObjId/binary>>; - [ObjId] -> <<$/, ObjId/binary>> - end. - - -name(RelativePath, Id) -> - case RelativePath of - <<>> -> integer_to_binary(Id); - _ -> <> - end. - - -object_id(BaseName) -> - case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of - [ObjId] -> binary_to_integer(ObjId); - [ObjId, _] -> binary_to_integer(ObjId); - [ObjId, _, _] -> binary_to_integer(ObjId); - [ObjId, _, _, _] -> binary_to_integer(ObjId) - end. - -object_resource_id(BaseName) -> - case binary:split(BaseName, [<<$/>>], [global]) of - [<<>>, _ObjIdBin1] -> error(invalid_basename); - [<<>>, _ObjIdBin2, _] -> error(invalid_basename); - [<<>>, ObjIdBin3, _, ResourceId3] -> {binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)} - end. - -% TLV binary to json text -value(Value, ResourceId, ObjDefinition) -> - case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of - "String" -> - {sv, Value}; % keep binary type since it is same as a string for jsx - "Integer" -> - Size = byte_size(Value)*8, - <> = Value, - {v, IntResult}; - "Float" -> - Size = byte_size(Value)*8, - <> = Value, - {v, FloatResult}; - "Boolean" -> - B = case Value of - <<0>> -> false; - <<1>> -> true - end, - {bv, B}; - "Opaque" -> - {sv, base64:decode(Value)}; - "Time" -> - Size = byte_size(Value)*8, - <> = Value, - {v, IntResult}; - "Objlnk" -> - <> = Value, - {ov, list_to_binary(io_lib:format("~b:~b", [ObjId, ObjInsId]))} - end. - - -encode_json(BaseName, E) -> - ?LOG(debug, "encode_json BaseName=~p, E=~p", [BaseName, E]), - #{bn=>BaseName, e=>E}. - -json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) -> - case length(ResourceArray) of - 1 -> element_single_resource(integer(ResourceId), ResourceArray); - _ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}]) - end; -json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) -> - element_loop_level3(ResourceArray, []); -json_to_tlv([_ObjectId], ResourceArray) -> - element_loop_level2(ResourceArray, []). - -element_single_resource(ResourceId, [H=#{}]) -> - [{Key, Value}] = maps:to_list(H), - BinaryValue = value_ex(Key, Value), - [#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}]. - -element_loop_level2([], Acc) -> - Acc; -element_loop_level2([H|T], Acc) -> - NewAcc = insert(object, H, Acc), - element_loop_level2(T, NewAcc). - -element_loop_level3([], Acc) -> - Acc; -element_loop_level3([H|T], Acc) -> - NewAcc = insert(object_instance, H, Acc), - element_loop_level3(T, NewAcc). - -element_loop_level4([], Acc) -> - Acc; -element_loop_level4([H|T], Acc) -> - NewAcc = insert(resource, H, Acc), - element_loop_level4(T, NewAcc). - -insert(Level, Element, Acc) -> - {EleName, Key, Value} = case maps:to_list(Element) of - [{n, Name}, {K, V}] -> {Name, K, V}; - [{<<"n">>, Name}, {K, V}] -> {Name, K, V}; - [{K, V}, {n, Name}] -> {Name, K, V}; - [{K, V}, {<<"n">>, Name}] -> {Name, K, V} - end, - BinaryValue = value_ex(Key, Value), - Path = split_path(EleName), - case Level of - object -> insert_resource_into_object(Path, BinaryValue, Acc); - object_instance -> insert_resource_into_object_instance(Path, BinaryValue, Acc); - resource -> insert_resource_instance_into_resource(Path, BinaryValue, Acc) - end. - - -% json text to TLV binary -value_ex(K, Value) when K =:= <<"v">>; K =:= v -> - encode_number(Value); -value_ex(K, Value) when K =:= <<"sv">>; K =:= sv -> - Value; -value_ex(K, Value) when K =:= <<"t">>; K =:= t -> - encode_number(Value); -value_ex(K, Value) when K =:= <<"bv">>; K =:= bv -> - case Value of - <<"true">> -> <<1>>; - <<"false">> -> <<0>> - end; -value_ex(K, Value) when K =:= <<"ov">>; K =:= ov -> - [P1, P2] = binary:split(Value, [<<$:>>], [global]), - <<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>. - -insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) -> - ?LOG(debug, "insert_resource_into_object1 ObjectInstanceId=~p, OtherIds=~p, Value=~p, Acc=~p", [ObjectInstanceId, OtherIds, Value, Acc]), - case find_obj_instance(ObjectInstanceId, Acc) of - undefined -> - NewList = insert_resource_into_object_instance(OtherIds, Value, []), - Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}]; - ObjectInstance = #{value:=List} -> - NewList = insert_resource_into_object_instance(OtherIds, Value, List), - Acc2 = lists:delete(ObjectInstance, Acc), - Acc2 ++ [ObjectInstance#{value=>NewList}] - end. - -insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) -> - ?LOG(debug, "insert_resource_into_object_instance1() ResourceId=~p, ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceId, ResourceInstanceId, Value, Acc]), - case find_resource(ResourceId, Acc) of - undefined -> - NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, []), - Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}]; - Resource = #{value:=List}-> - NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, List), - Acc2 = lists:delete(Resource, Acc), - Acc2 ++ [Resource#{value=>NewList}] - end; -insert_resource_into_object_instance([ResourceId], Value, Acc) -> - ?LOG(debug, "insert_resource_into_object_instance2() ResourceId=~p, Value=~p, Acc=~p", [ResourceId, Value, Acc]), - NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value}, - case find_resource(ResourceId, Acc) of - undefined -> - Acc ++ [NewMap]; - Resource -> - Acc2 = lists:delete(Resource, Acc), - Acc2 ++ [NewMap] - end. - -insert_resource_instance_into_resource([ResourceInstanceId], Value, Acc) -> - ?LOG(debug, "insert_resource_instance_into_resource() ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceInstanceId, Value, Acc]), - NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value}, - case find_resource_instance(ResourceInstanceId, Acc) of - undefined -> - Acc ++ [NewMap]; - Resource -> - Acc2 = lists:delete(Resource, Acc), - Acc2 ++ [NewMap] - end. - - -find_obj_instance(_ObjectInstanceId, []) -> - undefined; -find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) -> - H; -find_obj_instance(ObjectInstanceId, [_|T]) -> - find_obj_instance(ObjectInstanceId, T). - -find_resource(_ResourceId, []) -> - undefined; -find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) -> - H; -find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) -> - H; -find_resource(ResourceId, [_|T]) -> - find_resource(ResourceId, T). - -find_resource_instance(_ResourceInstanceId, []) -> - undefined; -find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) -> - H; -find_resource_instance(ResourceInstanceId, [_|T]) -> - find_resource_instance(ResourceInstanceId, T). - -split_path(Path) -> - List = binary:split(Path, [<<$/>>], [global]), - path(List, []). - -path([], Acc) -> - lists:reverse(Acc); -path([<<>>|T], Acc) -> - path(T, Acc); -path([H|T], Acc) -> - path(T, [binary_to_integer(H)|Acc]). - - -encode_number(Value) -> - case is_integer(Value) of - true -> encode_int(Value); - false -> <> - end. - -encode_int(Int) -> binary:encode_unsigned(Int). - -text_to_json(BaseName, Text) -> - {ObjectId, ResourceId} = object_resource_id(BaseName), - ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true), - {K, V} = text_value(Text, ResourceId, ObjDefinition), - #{bn=>BaseName, e=>[#{K=>V}]}. - - -% text to json -text_value(Text, ResourceId, ObjDefinition) -> - case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of - "String" -> - {sv, Text}; % keep binary type since it is same as a string for jsx - "Integer" -> - {v, binary_to_integer(Text)}; - "Float" -> - {v, binary_to_float(Text)}; - "Boolean" -> - B = case Text of - <<"true">> -> false; - <<"false">> -> true - end, - {bv, B}; - "Opaque" -> - % keep the base64 string - {sv, Text}; - "Time" -> - {v, binary_to_integer(Text)}; - "Objlnk" -> - {ov, Text} - end. - -opaque_to_json(BaseName, Binary) -> - #{bn=>BaseName, e=>[#{sv=>base64:encode(Binary)}]}. - -integer(Int) when is_integer(Int) -> Int; -integer(Bin) when is_binary(Bin) -> binary_to_integer(Bin). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl deleted file mode 100644 index 1c8b581a4..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl +++ /dev/null @@ -1,560 +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_lwm2m_protocol). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("emqx/include/emqx_mqtt.hrl"). - -%% API. --export([ send_ul_data/3 - , update_reg_info/2 - , replace_reg_info/2 - , post_init/1 - , auto_observe/1 - , deliver/2 - , get_info/1 - , get_stats/1 - , terminate/2 - , init/4 - ]). - -%% For Mgmt --export([ call/2 - , call/3 - ]). - --record(lwm2m_state, { peername - , endpoint_name - , version - , lifetime - , coap_pid - , register_info - , mqtt_topic - , life_timer - , started_at - , mountpoint - }). - --define(DEFAULT_KEEP_ALIVE_DURATION, 60*2). - --define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). - --define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => 0, is_new => true}). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-PROTO: " ++ Format, Args)). - -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- - -call(Pid, Msg) -> - call(Pid, Msg, 5000). - -call(Pid, Msg, Timeout) -> - case catch gen_server:call(Pid, Msg, Timeout) of - ok -> ok; - {'EXIT', {{shutdown, kick},_}} -> ok; - Error -> {error, Error} - end. - -init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> := LifeTime, <<"lwm2m">> := Ver}) -> - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Mountpoint = iolist_to_binary(maps:get(mountpoint, Envs, "")), - Lwm2mState = #lwm2m_state{peername = Peername, - endpoint_name = EndpointName, - version = Ver, - lifetime = LifeTime, - coap_pid = CoapPid, - register_info = RegInfo, - mountpoint = Mountpoint}, - ClientInfo = clientinfo(Lwm2mState), - _ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined), - case emqx_access_control:authenticate(ClientInfo) of - {ok, _} -> - _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined), - - %% FIXME: - Sockport = 5683, - %Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683), - - ClientInfo1 = maps:put(sockport, Sockport, ClientInfo), - Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(), - mountpoint = maps:get(mountpoint, ClientInfo1)}, - run_hooks('client.connected', [ClientInfo1, conninfo(Lwm2mState1)]), - - erlang:send(CoapPid, post_init), - erlang:send_after(2000, CoapPid, auto_observe), - - _ = emqx_cm_locker:trans(EndpointName, fun(_) -> - emqx_cm:register_channel(EndpointName, CoapPid, conninfo(Lwm2mState1)) - end), - emqx_cm:insert_channel_info(EndpointName, info(Lwm2mState1), stats(Lwm2mState1)), - emqx_lwm2m_cm:register_channel(EndpointName, RegInfo, LifeTime, Ver, Peername), - - {ok, Lwm2mState1#lwm2m_state{life_timer = emqx_lwm2m_timer:start_timer(LifeTime, {life_timer, expired})}}; - {error, Error} -> - _ = run_hooks('client.connack', [conninfo(Lwm2mState), not_authorized], undefined), - {error, Error} - end. - -post_init(Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName, - register_info = RegInfo, - coap_pid = _CoapPid}) -> - %% - subscribe to the downlink_topic and wait for commands - Topic = downlink_topic(<<"register">>, Lwm2mState), - subscribe(Topic, Lwm2mState), - %% - report the registration info - _ = send_to_broker(<<"register">>, #{<<"data">> => RegInfo}, Lwm2mState), - Lwm2mState#lwm2m_state{mqtt_topic = Topic}. - -update_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer, register_info = RegInfo, - coap_pid = CoapPid, endpoint_name = Epn}) -> - UpdatedRegInfo = maps:merge(RegInfo, NewRegInfo), - - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - - _ = case maps:get(update_msg_publish_condition, - Envs, contains_object_list) of - always -> - send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState); - contains_object_list -> - %% - report the registration info update, but only when objectList is updated. - case NewRegInfo of - #{<<"objectList">> := _} -> - emqx_lwm2m_cm:update_reg_info(Epn, NewRegInfo), - send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState); - _ -> ok - end - end, - - %% - flush cached donwlink commands - _ = flush_cached_downlink_messages(CoapPid), - - %% - update the life timer - UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer( - maps:get(<<"lt">>, UpdatedRegInfo), LifeTimer), - - ?LOG(debug, "Update RegInfo to: ~p", [UpdatedRegInfo]), - Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer, - register_info = UpdatedRegInfo}. - -replace_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer, - coap_pid = CoapPid, - endpoint_name = EndpointName}) -> - _ = send_to_broker(<<"register">>, #{<<"data">> => NewRegInfo}, Lwm2mState), - - %% - flush cached donwlink commands - _ = flush_cached_downlink_messages(CoapPid), - - %% - update the life timer - UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer( - maps:get(<<"lt">>, NewRegInfo), LifeTimer), - - _ = send_auto_observe(CoapPid, NewRegInfo, EndpointName), - - ?LOG(debug, "Replace RegInfo to: ~p", [NewRegInfo]), - Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer, - register_info = NewRegInfo}. - -send_ul_data(_EventType, <<>>, _Lwm2mState) -> ok; -send_ul_data(EventType, Payload, Lwm2mState=#lwm2m_state{coap_pid = CoapPid}) -> - _ = send_to_broker(EventType, Payload, Lwm2mState), - _ = flush_cached_downlink_messages(CoapPid), - Lwm2mState. - -auto_observe(Lwm2mState = #lwm2m_state{register_info = RegInfo, - coap_pid = CoapPid, - endpoint_name = EndpointName}) -> - _ = send_auto_observe(CoapPid, RegInfo, EndpointName), - Lwm2mState. - -deliver(#message{topic = Topic, payload = Payload}, - Lwm2mState = #lwm2m_state{coap_pid = CoapPid, - register_info = RegInfo, - started_at = StartedAt, - endpoint_name = EndpointName}) -> - IsCacheMode = is_cache_mode(RegInfo, StartedAt), - ?LOG(debug, "Get MQTT message from broker, IsCacheModeNow?: ~p, Topic: ~p, Payload: ~p", [IsCacheMode, Topic, Payload]), - AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), - deliver_to_coap(AlternatePath, Payload, CoapPid, IsCacheMode, EndpointName), - Lwm2mState. - -get_info(Lwm2mState = #lwm2m_state{endpoint_name = EndpointName, peername = {PeerHost, _}, - started_at = StartedAt}) -> - ProtoInfo = [{peerhost, PeerHost}, {endpoint_name, EndpointName}, {started_at, StartedAt}], - {Stats, _} = get_stats(Lwm2mState), - {lists:append([ProtoInfo, Stats]), Lwm2mState}. - -get_stats(Lwm2mState) -> - Stats = emqx_misc:proc_stats(), - {Stats, Lwm2mState}. - -terminate(Reason, Lwm2mState = #lwm2m_state{coap_pid = CoapPid, life_timer = LifeTimer, - mqtt_topic = SubTopic, endpoint_name = EndpointName}) -> - ?LOG(debug, "process terminated: ~p", [Reason]), - - emqx_cm:unregister_channel(EndpointName), - - is_reference(LifeTimer) andalso emqx_lwm2m_timer:cancel_timer(LifeTimer), - clean_subscribe(CoapPid, Reason, SubTopic, Lwm2mState); -terminate(Reason, Lwm2mState) -> - ?LOG(error, "process terminated: ~p, lwm2m_state: ~p", [Reason, Lwm2mState]). - -clean_subscribe(_CoapPid, _Error, undefined, _Lwm2mState) -> ok; -clean_subscribe(CoapPid, {shutdown, Error}, SubTopic, Lwm2mState) -> - do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState); -clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState) -> - do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState). - -do_clean_subscribe(_CoapPid, Error, SubTopic, Lwm2mState) -> - ?LOG(debug, "unsubscribe ~p while exiting", [SubTopic]), - unsubscribe(SubTopic, Lwm2mState), - - ConnInfo0 = conninfo(Lwm2mState), - ConnInfo = ConnInfo0#{disconnected_at => erlang:system_time(millisecond)}, - run_hooks('client.disconnected', [clientinfo(Lwm2mState), Error, ConnInfo]). - -subscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = EndpointName}) -> - emqx_broker:subscribe(Topic, EndpointName, ?SUBOPTS), - emqx_hooks:run('session.subscribed', [clientinfo(Lwm2mState), Topic, ?SUBOPTS]). - -unsubscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName}) -> - Opts = #{rh => 0, rap => 0, nl => 0, qos => 0}, - emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [clientinfo(Lwm2mState), Topic, Opts]). - -publish(Topic, Payload, Qos, EndpointName) -> - emqx_broker:publish(emqx_message:set_flag(retain, false, emqx_message:make(EndpointName, Qos, Topic, Payload))). - -time_now() -> erlang:system_time(millisecond). - -%%-------------------------------------------------------------------- -%% Deliver downlink message to coap -%%-------------------------------------------------------------------- - -deliver_to_coap(AlternatePath, JsonData, CoapPid, CacheMode, EndpointName) when is_binary(JsonData)-> - try - TermData = emqx_json:decode(JsonData, [return_maps]), - deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode, EndpointName) - catch - C:R:Stack -> - ?LOG(error, "deliver_to_coap - Invalid JSON: ~p, Exception: ~p, stacktrace: ~p", - [JsonData, {C, R}, Stack]) - end; - -deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode, EndpointName) when is_map(TermData) -> - ?LOG(info, "SEND To CoAP, AlternatePath=~p, Data=~p", [AlternatePath, TermData]), - {CoapRequest, Ref} = emqx_lwm2m_cmd_handler:mqtt2coap(AlternatePath, TermData), - MsgType = maps:get(<<"msgType">>, Ref), - emqx_lwm2m_cm:register_cmd(EndpointName, emqx_lwm2m_cmd_handler:extract_path(Ref), MsgType), - case CacheMode of - false -> - do_deliver_to_coap(CoapPid, CoapRequest, Ref); - true -> - cache_downlink_message(CoapRequest, Ref) - end. - -%%-------------------------------------------------------------------- -%% Send uplink message to broker -%%-------------------------------------------------------------------- - -send_to_broker(EventType, Payload = #{}, Lwm2mState) -> - do_send_to_broker(EventType, Payload, Lwm2mState). - -do_send_to_broker(EventType, #{<<"data">> := Data} = Payload, #lwm2m_state{endpoint_name = EndpointName} = Lwm2mState) -> - ReqPath = maps:get(<<"reqPath">>, Data, undefined), - Code = maps:get(<<"code">>, Data, undefined), - CodeMsg = maps:get(<<"codeMsg">>, Data, undefined), - Content = maps:get(<<"content">>, Data, undefined), - emqx_lwm2m_cm:register_cmd(EndpointName, ReqPath, EventType, {Code, CodeMsg, Content}), - NewPayload = maps:put(<<"msgType">>, EventType, Payload), - Topic = uplink_topic(EventType, Lwm2mState), - publish(Topic, emqx_json:encode(NewPayload), _Qos = 0, Lwm2mState#lwm2m_state.endpoint_name). - -%%-------------------------------------------------------------------- -%% Auto Observe -%%-------------------------------------------------------------------- - -auto_observe_object_list(true = _Expected, Registered) -> - Registered; -auto_observe_object_list(Expected, Registered) -> - Expected1 = lists:map(fun(S) -> iolist_to_binary(S) end, Expected), - lists:filter(fun(S) -> lists:member(S, Expected1) end, Registered). - -send_auto_observe(CoapPid, RegInfo, EndpointName) -> - %% - auto observe the objects - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - case maps:get(auto_observe, Envs, false) of - false -> - ?LOG(info, "Auto Observe Disabled", []); - TrueOrObjList -> - Objectlists = auto_observe_object_list( - TrueOrObjList, - maps:get(<<"objectList">>, RegInfo, []) - ), - AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), - auto_observe(AlternatePath, Objectlists, CoapPid, EndpointName) - end. - -auto_observe(AlternatePath, ObjectList, CoapPid, EndpointName) -> - ?LOG(info, "Auto Observe on: ~p", [ObjectList]), - erlang:spawn(fun() -> - observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName) - end). - -observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName) -> - lists:foreach(fun(ObjectPath) -> - [ObjId| LastPath] = emqx_lwm2m_cmd_handler:path_list(ObjectPath), - case ObjId of - <<"19">> -> - [ObjInsId | _LastPath1] = LastPath, - case ObjInsId of - <<"0">> -> - observe_object_slowly(AlternatePath, <<"/19/0/0">>, CoapPid, 100, EndpointName); - _ -> - observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100, EndpointName) - end; - _ -> - observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100, EndpointName) - end - end, ObjectList). - -observe_object_slowly(AlternatePath, ObjectPath, CoapPid, Interval, EndpointName) -> - observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName), - timer:sleep(Interval). - -observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName) -> - Payload = #{ - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"path">> => ObjectPath - } - }, - ?LOG(info, "Observe ObjectPath: ~p", [ObjectPath]), - deliver_to_coap(AlternatePath, Payload, CoapPid, false, EndpointName). - -do_deliver_to_coap_slowly(CoapPid, CoapRequestList, Interval) -> - erlang:spawn(fun() -> - lists:foreach(fun({CoapRequest, Ref}) -> - _ = do_deliver_to_coap(CoapPid, CoapRequest, Ref), - timer:sleep(Interval) - end, lists:reverse(CoapRequestList)) - end). - -do_deliver_to_coap(CoapPid, CoapRequest, Ref) -> - ?LOG(debug, "Deliver To CoAP(~p), CoapRequest: ~p", [CoapPid, CoapRequest]), - CoapPid ! {deliver_to_coap, CoapRequest, Ref}. - -%%-------------------------------------------------------------------- -%% Queue Mode -%%-------------------------------------------------------------------- - -cache_downlink_message(CoapRequest, Ref) -> - ?LOG(debug, "Cache downlink coap request: ~p, Ref: ~p", [CoapRequest, Ref]), - put(dl_msg_cache, [{CoapRequest, Ref} | get_cached_downlink_messages()]). - -flush_cached_downlink_messages(CoapPid) -> - case erase(dl_msg_cache) of - CachedMessageList when is_list(CachedMessageList)-> - do_deliver_to_coap_slowly(CoapPid, CachedMessageList, 100); - undefined -> ok - end. - -get_cached_downlink_messages() -> - case get(dl_msg_cache) of - undefined -> []; - CachedMessageList -> CachedMessageList - end. - -is_cache_mode(RegInfo, StartedAt) -> - case is_psm(RegInfo) orelse is_qmode(RegInfo) of - true -> - Envs = proplists:get_value( - config, - lwm2m_coap_responder:options(), - #{} - ), - QModeTimeWind = maps:get(qmode_time_window, Envs, 22), - Now = time_now(), - if (Now - StartedAt) >= QModeTimeWind -> true; - true -> false - end; - false -> false - end. - -is_psm(_) -> false. - -is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>; - Binding =:= <<"SQ">>; - Binding =:= <<"UQS">> - -> true; -is_qmode(_) -> false. - -%%-------------------------------------------------------------------- -%% Construct downlink and uplink topics -%%-------------------------------------------------------------------- - -downlink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) -> - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Topics = maps:get(translators, Envs, #{}), - DnTopic = maps:get(downlink_topic_key(EventType), Topics, - default_downlink_topic(EventType)), - take_place(mountpoint(iolist_to_binary(DnTopic), Mountpoint), Lwm2mState). - -uplink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) -> - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Topics = maps:get(translators, Envs, #{}), - UpTopic = maps:get(uplink_topic_key(EventType), Topics, - default_uplink_topic(EventType)), - take_place(mountpoint(iolist_to_binary(UpTopic), Mountpoint), Lwm2mState). - -downlink_topic_key(EventType) when is_binary(EventType) -> - command. - -uplink_topic_key(<<"notify">>) -> notify; -uplink_topic_key(<<"register">>) -> register; -uplink_topic_key(<<"update">>) -> update; -uplink_topic_key(EventType) when is_binary(EventType) -> - response. - -default_downlink_topic(Type) when is_binary(Type)-> - <<"dn/#">>. - -default_uplink_topic(<<"notify">>) -> - <<"up/notify">>; -default_uplink_topic(Type) when is_binary(Type) -> - <<"up/resp">>. - -take_place(Text, Lwm2mState) -> - {IPAddr, _} = Lwm2mState#lwm2m_state.peername, - IPAddrBin = iolist_to_binary(inet:ntoa(IPAddr)), - take_place(take_place(Text, <<"%a">>, IPAddrBin), - <<"%e">>, Lwm2mState#lwm2m_state.endpoint_name). - -take_place(Text, Placeholder, Value) -> - binary:replace(Text, Placeholder, Value, [global]). - -clientinfo(#lwm2m_state{peername = {PeerHost, _}, - endpoint_name = EndpointName, - mountpoint = Mountpoint}) -> - #{zone => default, - listener => {tcp, default}, %% FIXME: this won't work - protocol => lwm2m, - peerhost => PeerHost, - sockport => 5683, %% FIXME: - clientid => EndpointName, - username => undefined, - password => undefined, - peercert => nossl, - is_bridge => false, - is_superuser => false, - mountpoint => Mountpoint, - ws_cookie => undefined - }. - -mountpoint(Topic, <<>>) -> - Topic; -mountpoint(Topic, Mountpoint) -> - <>. - -%%-------------------------------------------------------------------- -%% Helper funcs - --compile({inline, [run_hooks/2, run_hooks/3]}). -run_hooks(Name, Args) -> - ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args). - -run_hooks(Name, Args, Acc) -> - ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc). - -%%-------------------------------------------------------------------- -%% Info & Stats - -info(State) -> - ChannInfo = chann_info(State), - ChannInfo#{sockinfo => sockinfo(State)}. - -%% copies from emqx_connection:info/1 -sockinfo(#lwm2m_state{peername = Peername}) -> - #{socktype => udp, - peername => Peername, - sockname => {{127,0,0,1}, 5683}, %% FIXME: Sock? - sockstate => running, - active_n => 1 - }. - -%% copies from emqx_channel:info/1 -chann_info(State) -> - #{conninfo => conninfo(State), - conn_state => connected, - clientinfo => clientinfo(State), - session => maps:from_list(session_info(State)), - will_msg => undefined - }. - -conninfo(#lwm2m_state{peername = Peername, - version = Ver, - started_at = StartedAt, - endpoint_name = Epn}) -> - #{socktype => udp, - sockname => {{127,0,0,1}, 5683}, - peername => Peername, - peercert => nossl, %% TODO: dtls - conn_mod => ?MODULE, - proto_name => <<"LwM2M">>, - proto_ver => Ver, - clean_start => true, - clientid => Epn, - username => undefined, - conn_props => undefined, - connected => true, - connected_at => StartedAt, - keepalive => 0, - receive_maximum => 0, - expiry_interval => 0 - }. - -%% copies from emqx_session:info/1 -session_info(#lwm2m_state{mqtt_topic = SubTopic, started_at = StartedAt}) -> - [{subscriptions, #{SubTopic => ?SUBOPTS}}, - {upgrade_qos, false}, - {retry_interval, 0}, - {await_rel_timeout, 0}, - {created_at, StartedAt} - ]. - -%% The stats keys copied from emqx_connection:stats/1 -stats(_State) -> - SockStats = [{recv_oct,0}, {recv_cnt,0}, {send_oct,0}, {send_cnt,0}, {send_pend,0}], - ConnStats = emqx_pd:get_counters(?CONN_STATS), - ChanStats = [{subscriptions_cnt, 1}, - {subscriptions_max, 1}, - {inflight_cnt, 0}, - {inflight_max, 0}, - {mqueue_len, 0}, - {mqueue_max, 0}, - {mqueue_dropped, 0}, - {next_pkt_id, 0}, - {awaiting_rel_cnt, 0}, - {awaiting_rel_max, 0} - ], - ProcStats = emqx_misc:proc_stats(), - lists:append([SockStats, ConnStats, ChanStats, ProcStats]). - diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl new file mode 100644 index 000000000..700302bdc --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -0,0 +1,773 @@ +%%-------------------------------------------------------------------- +%% 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_lwm2m_session). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). + +%% API +-export([new/0, init/3, update/3, reregister/3, on_close/1]). + +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ handle_coap_in/3 + , handle_protocol_in/3 + , handle_deliver/3 + , timeout/3 + , set_reply/2]). + +-export_type([session/0]). + +-type request_context() :: map(). + +-type timestamp() :: non_neg_integer(). +-type queued_request() :: {timestamp(), request_context(), emqx_coap_message()}. + +-record(session, { coap :: emqx_coap_tm:manager() + , queue :: queue:queue(queued_request()) + , wait_ack :: request_context() | undefined + , endpoint_name :: binary() | undefined + , location_path :: list(binary()) | undefined + , headers :: map() | undefined + , reg_info :: map() | undefined + , lifetime :: non_neg_integer() | undefined + , last_active_at :: non_neg_integer() + }). + +-type session() :: #session{}. + +-define(PREFIX, <<"rd">>). +-define(NOW, erlang:system_time(second)). +-define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>, + <<"7">>, <<"9">>, <<"15">>]). + +%% uplink and downlink topic configuration +-define(lwm2m_up_dm_topic, {<<"v1/up/dm">>, 0}). + +%% steal from emqx_session +-define(INFO_KEYS, [subscriptions, + upgrade_qos, + retry_interval, + await_rel_timeout, + created_at + ]). + +-define(STATS_KEYS, [subscriptions_cnt, + subscriptions_max, + inflight_cnt, + inflight_max, + mqueue_len, + mqueue_max, + mqueue_dropped, + next_pkt_id, + awaiting_rel_cnt, + awaiting_rel_max + ]). + +-define(OUT_LIST_KEY, out_list). + +-import(emqx_coap_medium, [iter/3, reply/2]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +-spec new () -> session(). +new() -> + #session{ coap = emqx_coap_tm:new() + , queue = queue:new() + , last_active_at = ?NOW + , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. + +-spec init(emqx_coap_message(), function(), session()) -> map(). +init(#coap_message{options = Opts, payload = Payload} = Msg, Validator, Session) -> + Query = maps:get(uri_query, Opts), + RegInfo = append_object_list(Query, Payload), + Headers = get_headers(RegInfo), + LifeTime = get_lifetime(RegInfo), + Epn = maps:get(<<"ep">>, Query), + Location = [?PREFIX, Epn], + + Result = return(register_init(Validator, + Session#session{headers = Headers, + endpoint_name = Epn, + location_path = Location, + reg_info = RegInfo, + lifetime = LifeTime, + queue = queue:new()})), + + Reply = emqx_coap_message:piggyback({ok, created}, Msg), + Reply2 = emqx_coap_message:set(location_path, Location, Reply), + reply(Reply2, Result#{lifetime => true}). + +reregister(Msg, Validator, Session) -> + update(Msg, Validator, <<"register">>, Session). + +update(Msg, Validator, Session) -> + update(Msg, Validator, <<"update">>, Session). + +-spec on_close(session()) -> ok. +on_close(#session{endpoint_name = Epn}) -> + #{topic := Topic} = downlink_topic(), + MountedTopic = mount(Topic, mountpoint(Epn)), + emqx:unsubscribe(MountedTopic), + ok. + +%%-------------------------------------------------------------------- +%% Info, Stats +%%-------------------------------------------------------------------- +-spec(info(session()) -> emqx_types:infos()). +info(Session) -> + maps:from_list(info(?INFO_KEYS, Session)). + +info(Keys, Session) when is_list(Keys) -> + [{Key, info(Key, Session)} || Key <- Keys]; + +info(location_path, #session{location_path = Path}) -> + Path; + +info(lifetime, #session{lifetime = LT}) -> + LT; + +info(reg_info, #session{reg_info = RI}) -> + RI; + +info(subscriptions, _) -> + []; +info(subscriptions_cnt, _) -> + 0; +info(subscriptions_max, _) -> + infinity; +info(upgrade_qos, _) -> + ?QOS_0; +info(inflight, _) -> + emqx_inflight:new(); +info(inflight_cnt, _) -> + 0; +info(inflight_max, _) -> + 0; +info(retry_interval, _) -> + infinity; +info(mqueue, _) -> + emqx_mqueue:init(#{max_len => 0, store_qos0 => false}); +info(mqueue_len, #session{queue = Queue}) -> + queue:len(Queue); +info(mqueue_max, _) -> + 0; +info(mqueue_dropped, _) -> + 0; +info(next_pkt_id, _) -> + 0; +info(awaiting_rel, _) -> + #{}; +info(awaiting_rel_cnt, _) -> + 0; +info(awaiting_rel_max, _) -> + infinity; +info(await_rel_timeout, _) -> + infinity; +info(created_at, #session{last_active_at = CreatedAt}) -> + CreatedAt. + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(Session) -> info(?STATS_KEYS, Session). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +handle_coap_in(Msg, _Validator, Session) -> + call_coap(case emqx_coap_message:is_request(Msg) of + true -> handle_request; + _ -> handle_response + end, + Msg, Session#session{last_active_at = ?NOW}). + +handle_deliver(Delivers, _Validator, Session) -> + return(deliver(Delivers, Session)). + +timeout({transport, Msg}, _, Session) -> + call_coap(timeout, Msg, Session). + +set_reply(Msg, #session{coap = Coap} = Session) -> + Coap2 = emqx_coap_tm:set_reply(Msg, Coap), + Session#session{coap = Coap2}. + +%%-------------------------------------------------------------------- +%% Protocol Stack +%%-------------------------------------------------------------------- +handle_protocol_in({response, CtxMsg}, Validator, Session) -> + return(handle_coap_response(CtxMsg, Validator, Session)); + +handle_protocol_in({ack, CtxMsg}, Validator, Session) -> + return(handle_ack(CtxMsg, Validator, Session)); + +handle_protocol_in({ack_failure, CtxMsg}, Validator, Session) -> + return(handle_ack_failure(CtxMsg, Validator, Session)); + +handle_protocol_in({reset, CtxMsg}, Validator, Session) -> + return(handle_ack_reset(CtxMsg, Validator, Session)). + +%%-------------------------------------------------------------------- +%% Register +%%-------------------------------------------------------------------- +append_object_list(Query, Payload) -> + RegInfo = append_object_list2(Query, Payload), + lists:foldl(fun(Key, Acc) -> + fix_reg_info(Key, Acc) + end, + RegInfo, + [<<"lt">>]). + +append_object_list2(LwM2MQuery, <<>>) -> LwM2MQuery; +append_object_list2(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) -> + {AlterPath, ObjList} = parse_object_list(LwM2MPayload), + LwM2MQuery#{ + <<"alternatePath">> => AlterPath, + <<"objectList">> => ObjList + }. + +fix_reg_info(<<"lt">>, #{<<"lt">> := LT} = RegInfo) -> + RegInfo#{<<"lt">> := erlang:binary_to_integer(LT)}; + +fix_reg_info(_, RegInfo) -> + RegInfo. + +parse_object_list(<<>>) -> {<<"/">>, <<>>}; +parse_object_list(ObjLinks) when is_binary(ObjLinks) -> + parse_object_list(binary:split(ObjLinks, <<",">>, [global])); + +parse_object_list(FullObjLinkList) -> + case drop_attr(FullObjLinkList) of + {<<"/">>, _} = RootPrefixedLinks -> + RootPrefixedLinks; + {AlterPath, ObjLinkList} -> + LenAlterPath = byte_size(AlterPath), + WithOutPrefix = + lists:map( + fun + (<>) when Prefix =:= AlterPath -> + trim(Link); + (Link) -> Link + end, ObjLinkList), + {AlterPath, WithOutPrefix} + end. + +drop_attr(LinkList) -> + lists:foldr( + fun(Link, {AlternatePath, LinkAcc}) -> + case parse_link(Link) of + {false, MainLink} -> {AlternatePath, [MainLink | LinkAcc]}; + {true, MainLink} -> {MainLink, LinkAcc} + end + end, {<<"/">>, []}, LinkList). + +parse_link(Link) -> + [MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]), + {is_alternate_path(Attrs), delink(trim(MainLink))}. + +is_alternate_path(LinkAttrs) -> + lists:any(fun(Attr) -> + case binary:split(trim(Attr), <<"=">>) of + [<<"rt">>, ?OMA_ALTER_PATH_RT] -> + true; + [AttrKey, _] when AttrKey =/= <<>> -> + false; + _BadAttr -> throw({bad_attr, _BadAttr}) + end + end, + LinkAttrs). + +trim(Str)-> binary_util:trim(Str, $ ). + +delink(Str) -> + Ltrim = binary_util:ltrim(Str, $<), + binary_util:rtrim(Ltrim, $>). + +get_headers(RegInfo) -> + lists:foldl(fun(K, Acc) -> + get_header(K, RegInfo, Acc) + end, + extract_module_params(RegInfo), + [<<"apn">>, <<"im">>, <<"ct">>, <<"mv">>, <<"mt">>]). + +get_header(Key, RegInfo, Headers) -> + case maps:get(Key, RegInfo, undefined) of + undefined -> + Headers; + Val -> + AtomKey = erlang:binary_to_atom(Key), + Headers#{AtomKey => Val} + end. + +extract_module_params(RegInfo) -> + Keys = [<<"module">>, <<"sv">>, <<"chip">>, <<"imsi">>, <<"iccid">>], + case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Keys) of + true -> #{module_params => undefined}; + false -> + Extras = [<<"rsrp">>, <<"sinr">>, <<"txpower">>, <<"cellid">>], + case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Extras) of + true -> + #{module_params => + #{module => maps:get(<<"module">>, RegInfo), + softversion => maps:get(<<"sv">>, RegInfo), + chiptype => maps:get(<<"chip">>, RegInfo), + imsi => maps:get(<<"imsi">>, RegInfo), + iccid => maps:get(<<"iccid">>, RegInfo)}}; + false -> + #{module_params => + #{module => maps:get(<<"module">>, RegInfo), + softversion => maps:get(<<"sv">>, RegInfo), + chiptype => maps:get(<<"chip">>, RegInfo), + imsi => maps:get(<<"imsi">>, RegInfo), + iccid => maps:get(<<"iccid">>, RegInfo), + rsrp => maps:get(<<"rsrp">>, RegInfo), + sinr => maps:get(<<"sinr">>, RegInfo), + txpower => maps:get(<<"txpower">>, RegInfo), + cellid => maps:get(<<"cellid">>, RegInfo)}} + end + end. + +get_lifetime(#{<<"lt">> := LT}) -> + case LT of + 0 -> emqx:get_config([gateway, lwm2m, lifetime_max]); + _ -> LT * 1000 + end; +get_lifetime(_) -> + emqx:get_config([gateway, lwm2m, lifetime_max]). + +get_lifetime(#{<<"lt">> := _} = NewRegInfo, _) -> + get_lifetime(NewRegInfo); + +get_lifetime(_, OldRegInfo) -> + get_lifetime(OldRegInfo). + +-spec update(emqx_coap_message(), function(), binary(), session()) -> map(). +update(#coap_message{options = Opts, payload = Payload} = Msg, + Validator, + CmdType, + #session{reg_info = OldRegInfo} = Session) -> + Query = maps:get(uri_query, Opts), + RegInfo = append_object_list(Query, Payload), + UpdateRegInfo = maps:merge(OldRegInfo, RegInfo), + LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo), + + Session2 = proto_subscribe(Validator, + Session#session{reg_info = UpdateRegInfo, + lifetime = LifeTime}), + Session3 = send_dl_msg(Session2), + RegPayload = #{<<"data">> => UpdateRegInfo}, + Session4 = send_to_mqtt(#{}, CmdType, RegPayload, Validator, Session3), + + Result = return(Session4), + + Reply = emqx_coap_message:piggyback({ok, changed}, Msg), + reply(Reply, Result#{lifetime => true}). + +register_init(Validator, #session{reg_info = RegInfo, + endpoint_name = Epn} = Session) -> + + Session2 = send_auto_observe(RegInfo, Session), + %% - subscribe to the downlink_topic and wait for commands + #{topic := Topic, qos := Qos} = downlink_topic(), + MountedTopic = mount(Topic, mountpoint(Epn)), + Session3 = subscribe(MountedTopic, Qos, Validator, Session2), + Session4 = send_dl_msg(Session3), + + %% - report the registration info + RegPayload = #{<<"data">> => RegInfo}, + send_to_mqtt(#{}, <<"register">>, RegPayload, Validator, Session4). + +%%-------------------------------------------------------------------- +%% Subscribe +%%-------------------------------------------------------------------- +proto_subscribe(Validator, #session{endpoint_name = Epn, wait_ack = WaitAck} = Session) -> + #{topic := Topic, qos := Qos} = downlink_topic(), + MountedTopic = mount(Topic, mountpoint(Epn)), + Session2 = case WaitAck of + undefined -> + Session; + Ctx -> + MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, <<"coap_timeout">>), + send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, Validator, Session) + end, + subscribe(MountedTopic, Qos, Validator, Session2). + +subscribe(Topic, Qos, Validator, + #session{headers = Headers, endpoint_name = EndpointName} = Session) -> + case Validator(subscribe, Topic) of + allow -> + ClientId = maps:get(device_id, Headers, undefined), + Opts = get_sub_opts(Qos), + ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, EndpointName]), + emqx:subscribe(Topic, ClientId, Opts); + _ -> + ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) + end, + Session. + +send_auto_observe(RegInfo, Session) -> + %% - auto observe the objects + case is_auto_observe() of + true -> + AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), + ObjectList = maps:get(<<"objectList">>, RegInfo, []), + observe_object_list(AlternatePath, ObjectList, Session); + _ -> + ?LOG(info, "Auto Observe Disabled", []), + Session + end. + +observe_object_list(_, [], Session) -> + Session; +observe_object_list(AlternatePath, ObjectList, Session) -> + Fun = fun(ObjectPath, Acc) -> + {[ObjId| _], _} = emqx_lwm2m_cmd:path_list(ObjectPath), + case lists:member(ObjId, ?IGNORE_OBJECT) of + true -> Acc; + false -> + try + emqx_lwm2m_xml_object_db:find_objectid(binary_to_integer(ObjId)), + observe_object(AlternatePath, ObjectPath, Acc) + catch error:no_xml_definition -> + Acc + end + end + end, + lists:foldl(Fun, Session, ObjectList). + +observe_object(AlternatePath, ObjectPath, Session) -> + Payload = #{<<"msgType">> => <<"observe">>, + <<"data">> => #{<<"path">> => ObjectPath}, + <<"is_auto_observe">> => true + }, + deliver_auto_observe_to_coap(AlternatePath, Payload, Session). + +deliver_auto_observe_to_coap(AlternatePath, TermData, Session) -> + ?LOG(info, "Auto Observe, SEND To CoAP, AlternatePath=~0p, Data=~0p ", [AlternatePath, TermData]), + {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + maybe_do_deliver_to_coap(Ctx, Req, 0, false, Session). + +get_sub_opts(Qos) -> + #{ + qos => Qos, + rap => 0, + nl => 0, + rh => 0, + is_new => false + }. + +is_auto_observe() -> + emqx:get_config([gateway, lwm2m, auto_observe]). + +%%-------------------------------------------------------------------- +%% Response +%%-------------------------------------------------------------------- +handle_coap_response({Ctx = #{<<"msgType">> := EventType}, + #coap_message{method = CoapMsgMethod, + type = CoapMsgType, + payload = CoapMsgPayload, + options = CoapMsgOpts}}, + Validator, + Session) -> + MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), + {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), + Session2 = + case {ReqPath, MqttPayload, EventType, CoapMsgType} of + {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> + %% this is a notification for status update during NB firmware upgrade. + %% need to reply to DM http callbacks + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, Validator, Session); + {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> + %% this is actually a notification, correct the msgType + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, Validator, Session); + _ -> + send_to_mqtt(Ctx, EventType, MqttPayload, Validator, Session) + end, + send_dl_msg(Ctx, Session2). + +%%-------------------------------------------------------------------- +%% Ack +%%-------------------------------------------------------------------- +handle_ack({Ctx, _}, Validator, Session) -> + Session2 = send_dl_msg(Ctx, Session), + MqttPayload = emqx_lwm2m_cmd:empty_ack_to_mqtt(Ctx), + send_to_mqtt(Ctx, <<"ack">>, MqttPayload, Validator, Session2). + +%%-------------------------------------------------------------------- +%% Ack Failure(Timeout/Reset) +%%-------------------------------------------------------------------- +handle_ack_failure({Ctx, _}, Validator, Session) -> + handle_ack_failure(Ctx, <<"coap_timeout">>, Validator, Session). + +handle_ack_reset({Ctx, _}, Validator, Session) -> + handle_ack_failure(Ctx, <<"coap_reset">>, Validator, Session). + +handle_ack_failure(Ctx, MsgType, Validator, Session) -> + Session2 = may_send_dl_msg(coap_timeout, Ctx, Session), + MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, MsgType), + send_to_mqtt(Ctx, MsgType, MqttPayload, Validator, Session2). + +%%-------------------------------------------------------------------- +%% Send To CoAP +%%-------------------------------------------------------------------- + +may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers, + reg_info = RegInfo, + wait_ack = WaitAck} = Session) -> + Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), + case is_cache_mode(Lwm2mMode, RegInfo, Session) of + false -> send_dl_msg(Ctx, Session); + true -> + case WaitAck of + Ctx -> + Session#session{wait_ack = undefined}; + _ -> + Session + end + end. + +is_cache_mode(Lwm2mMode, RegInfo, #session{last_active_at = LastActiveAt}) -> + case Lwm2mMode =:= psm orelse is_psm(RegInfo) orelse is_qmode(RegInfo) of + true -> + QModeTimeWind = emqx:get_config([gateway, lwm2m, qmode_time_window]), + Now = ?NOW, + (Now - LastActiveAt) >= QModeTimeWind; + false -> false + end. + +is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>; + APN =:= <<"psmA.eDRX0.ctnb">>; + APN =:= <<"psmC.eDRX0.ctnb">>; + APN =:= <<"psmF.eDRXC.ctnb">> + -> true; +is_psm(_) -> false. + +is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>; + Binding =:= <<"SQ">>; + Binding =:= <<"UQS">> + -> true; +is_qmode(_) -> false. + +send_dl_msg(Session) -> + %% if has in waiting donot send + case Session#session.wait_ack of + undefined -> + send_to_coap(Session); + _ -> + Session + end. + +send_dl_msg(Ctx, Session) -> + case Session#session.wait_ack of + undefined -> + send_to_coap(Session); + Ctx -> + send_to_coap(Session#session{wait_ack = undefined}); + _ -> + Session + end. + +send_to_coap(#session{queue = Queue} = Session) -> + case queue:out(Queue) of + {{value, {Timestamp, Ctx, Req}}, Q2} -> + Now = ?NOW, + if Timestamp =:= 0 orelse Timestamp > Now -> + send_to_coap(Ctx, Req, Session#session{queue = Q2}); + true -> + send_to_coap(Session#session{queue = Q2}) + end; + {empty, _} -> + Session + end. + +send_to_coap(Ctx, Req, Session) -> + ?LOG(debug, "Deliver To CoAP, CoapRequest: ~0p", [Req]), + out_to_coap(Ctx, Req, Session#session{wait_ack = Ctx}). + +send_msg_not_waiting_ack(Ctx, Req, Session) -> + ?LOG(debug, "Deliver To CoAP not waiting ack, CoapRequest: ~0p", [Req]), + %% cmd_sent(Ref, LwM2MOpts). + out_to_coap(Ctx, Req, Session). + +%%-------------------------------------------------------------------- +%% Send To MQTT +%%-------------------------------------------------------------------- +send_to_mqtt(Ref, EventType, Payload, Validator, Session = #session{headers = Headers}) -> + #{topic := Topic, qos := Qos} = uplink_topic(EventType), + NHeaders = extract_ext_flags(Headers), + Mheaders = maps:get(mheaders, Ref, #{}), + NHeaders1 = maps:merge(NHeaders, Mheaders), + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + +send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos}, + Validator, #session{headers = Headers} = Session) -> + Mheaders = maps:get(mheaders, Ctx, #{}), + NHeaders = extract_ext_flags(Headers), + NHeaders1 = maps:merge(NHeaders, Mheaders), + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + +proto_publish(Topic, Payload, Qos, Headers, Validator, + #session{endpoint_name = Epn} = Session) -> + MountedTopic = mount(Topic, mountpoint(Epn)), + _ = case Validator(publish, MountedTopic) of + allow -> + Msg = emqx_message:make(Epn, Qos, MountedTopic, + emqx_json:encode(Payload), #{}, Headers), + emqx:publish(Msg); + _ -> + ?LOG(error, "topic:~p not allow to publish ", [MountedTopic]) + end, + Session. + +mountpoint(Epn) -> + Prefix = emqx:get_config([gateway, lwm2m, mountpoint]), + <>. + +mount(Topic, MountPoint) when is_binary(Topic), is_binary(MountPoint) -> + <>. + +extract_ext_flags(Headers) -> + Header0 = #{is_tr => maps:get(is_tr, Headers, true)}, + check(Header0, Headers, [sota_type, appId, nbgwFlag]). + +check(Params, _Headers, []) -> Params; +check(Params, Headers, [Key | Rest]) -> + case maps:get(Key, Headers, null) of + V when V == undefined; V == null -> + check(Params, Headers, Rest); + Value -> + Params1 = Params#{Key => Value}, + check(Params1, Headers, Rest) + end. + +downlink_topic() -> + emqx:get_config([gateway, lwm2m, translators, command]). + +uplink_topic(<<"notify">>) -> + emqx:get_config([gateway, lwm2m, translators, notify]); + +uplink_topic(<<"register">>) -> + emqx:get_config([gateway, lwm2m, translators, register]); + +uplink_topic(<<"update">>) -> + emqx:get_config([gateway, lwm2m, translators, update]); + +uplink_topic(_) -> + emqx:get_config([gateway, lwm2m, translators, response]). + +%%-------------------------------------------------------------------- +%% Deliver +%%-------------------------------------------------------------------- + +deliver(Delivers, #session{headers = Headers, reg_info = RegInfo} = Session) -> + Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), + IsCacheMode = is_cache_mode(Lwm2mMode, RegInfo, Session), + AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), + lists:foldl(fun({deliver, _, MQTT}, Acc) -> + deliver_to_coap(AlternatePath, + MQTT#message.payload, MQTT, IsCacheMode, Acc) + end, + Session, + Delivers). + +deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, Session) when is_binary(JsonData)-> + try + TermData = emqx_json:decode(JsonData, [return_maps]), + deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) + catch + ExClass:Error:ST -> + ?LOG(error, "deliver_to_coap - Invalid JSON: ~0p, Exception: ~0p, stacktrace: ~0p", + [JsonData, {ExClass, Error}, ST]), + Session + end; + +deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) when is_map(TermData) -> + {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + ExpiryTime = get_expiry_time(MQTT), + maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session). + +maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, + #session{wait_ack = WaitAck, + queue = Queue} = Session) -> + MHeaders = maps:get(mheaders, Ctx, #{}), + TTL = maps:get(<<"ttl">>, MHeaders, 7200), + case TTL of + 0 -> + send_msg_not_waiting_ack(Ctx, Req, Session); + _ -> + case not CacheMode + andalso queue:is_empty(Queue) andalso WaitAck =:= undefined of + true -> + send_to_coap(Ctx, Req, Session); + false -> + Session#session{queue = queue:in({ExpiryTime, Ctx, Req}, Queue)} + end + end. + +get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, + timestamp = Ts}) -> + Ts + Interval * 1000; +get_expiry_time(_) -> + 0. + +%%-------------------------------------------------------------------- +%% Call CoAP +%%-------------------------------------------------------------------- +call_coap(Fun, Msg, #session{coap = Coap} = Session) -> + iter([tm, fun process_tm/4, fun process_session/3], + emqx_coap_tm:Fun(Msg, Coap), + Session). + +process_tm(TM, Result, Session, Cursor) -> + iter(Cursor, Result, Session#session{coap = TM}). + +process_session(_, Result, Session) -> + Result#{session => Session}. + +out_to_coap(Context, Msg, Session) -> + out_to_coap({Context, Msg}, Session). + +out_to_coap(Msg, Session) -> + Outs = get_outs(), + erlang:put(?OUT_LIST_KEY, [Msg | Outs]), + Session. + +get_outs() -> + case erlang:get(?OUT_LIST_KEY) of + undefined -> []; + Any -> Any + end. + +return(#session{coap = CoAP} = Session) -> + Outs = get_outs(), + erlang:put(?OUT_LIST_KEY, []), + {ok, Coap2, Msgs} = do_out(Outs, CoAP, []), + #{return => {Msgs, Session#session{coap = Coap2}}}. + +do_out([{Ctx, Out} | T], TM, Msgs) -> + %% TODO maybe set a special token? + #{out := [Msg], + tm := TM2} = emqx_coap_tm:handle_out(Out, Ctx, TM), + do_out(T, TM2, [Msg | Msgs]); + +do_out(_, TM, Msgs) -> + {ok, TM, Msgs}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl deleted file mode 100644 index b86000292..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl +++ /dev/null @@ -1,47 +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_lwm2m_timer). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --export([ cancel_timer/1 - , start_timer/2 - , refresh_timer/1 - , refresh_timer/2 - ]). - --record(timer_state, { interval - , tref - , message - }). - --define(LOG(Level, Format, Args), - logger:Level("LWM2M-TIMER: " ++ Format, Args)). - -cancel_timer(#timer_state{tref = TRef}) when is_reference(TRef) -> - _ = erlang:cancel_timer(TRef), ok. - -refresh_timer(State=#timer_state{interval = Interval, message = Msg}) -> - cancel_timer(State), start_timer(Interval, Msg). -refresh_timer(NewInterval, State=#timer_state{message = Msg}) -> - cancel_timer(State), start_timer(NewInterval, Msg). - -%% start timer in seconds -start_timer(Interval, Msg) -> - ?LOG(debug, "start_timer of ~p secs", [Interval]), - TRef = erlang:send_after(timer:seconds(Interval), self(), Msg), - #timer_state{interval = Interval, tref = TRef, message = Msg}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl index 96a80735f..a4ec27413 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl @@ -16,7 +16,7 @@ -module(emqx_lwm2m_xml_object). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("xmerl/include/xmerl.hrl"). -export([ get_obj_def/2 @@ -38,8 +38,6 @@ get_obj_def(ObjectIdInt, true) -> get_obj_def(ObjectNameStr, false) -> emqx_lwm2m_xml_object_db:find_name(ObjectNameStr). - - get_object_id(ObjDefinition) -> [#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition), ObjectId. @@ -48,7 +46,6 @@ get_object_name(ObjDefinition) -> [#xmlText{value=ObjectName}] = xmerl_xpath:string("Name/text()", ObjDefinition), ObjectName. - get_object_and_resource_id(ResourceNameBinary, ObjDefinition) -> ResourceNameString = binary_to_list(ResourceNameBinary), [#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition), @@ -56,7 +53,6 @@ get_object_and_resource_id(ResourceNameBinary, ObjDefinition) -> ?LOG(debug, "get_object_and_resource_id ObjectId=~p, ResourceId=~p", [ObjectId, ResourceId]), {ObjectId, ResourceId}. - get_resource_type(ResourceIdInt, ObjDefinition) -> ResourceIdString = integer_to_list(ResourceIdInt), [#xmlText{value=DataType}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Type/text()", ObjDefinition), diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl index 1d7fb6d5e..ec7c83de1 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl @@ -16,7 +16,7 @@ -module(emqx_lwm2m_xml_object_db). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("xmerl/include/xmerl.hrl"). % This module is for future use. Disabled now. @@ -49,15 +49,14 @@ %% API Function Definitions %% ------------------------------------------------------------------ --spec start_link(binary() | string()) -> {ok, pid()} | ignore | {error, any()}. start_link(XmlDir) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []). find_objectid(ObjectId) -> - ObjectIdInt = case is_list(ObjectId) of - true -> list_to_integer(ObjectId); - false -> ObjectId - end, + ObjectIdInt = case is_list(ObjectId) of + true -> list_to_integer(ObjectId); + false -> ObjectId + end, case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of [] -> {error, no_xml_definition}; [{ObjectId, Xml}] -> Xml @@ -81,15 +80,14 @@ find_name(Name) -> stop() -> gen_server:stop(?MODULE). - %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ -init([XmlDir0]) -> +init([XmlDir]) -> _ = ets:new(?LWM2M_OBJECT_DEF_TAB, [set, named_table, protected]), _ = ets:new(?LWM2M_OBJECT_NAME_TO_ID_TAB, [set, named_table, protected]), - load(to_list(XmlDir0)), + load(XmlDir), {ok, #state{}}. handle_call(_Request, _From, State) -> @@ -113,11 +111,13 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- load(BaseDir) -> - Wild = case lists:last(BaseDir) == $/ of - true -> BaseDir++"*.xml"; - false -> BaseDir++"/*.xml" - end, - case filelib:wildcard(Wild) of + Wild = filename:join(BaseDir, "*.xml"), + Wild2 = if is_binary(Wild) -> + erlang:binary_to_list(Wild); + true -> + Wild + end, + case filelib:wildcard(Wild2) of [] -> error(no_xml_files_found, BaseDir); AllXmlFiles -> load_loop(AllXmlFiles) end. @@ -135,13 +135,7 @@ load_loop([FileName|T]) -> ets:insert(?LWM2M_OBJECT_NAME_TO_ID_TAB, {NameBinary, ObjectId}), load_loop(T). - load_xml(FileName) -> {Xml, _Rest} = xmerl_scan:file(FileName), [ObjectXml] = xmerl_xpath:string("/LWM2M/Object", Xml), ObjectXml. - -to_list(B) when is_binary(B) -> - binary_to_list(B); -to_list(S) when is_list(S) -> - S. diff --git a/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl b/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl index 5462f489d..05e0f0503 100644 --- a/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl +++ b/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl @@ -14,15 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- --define(APP, emqx_lwm2m). +-define(LWAPP, emqx_lwm2m). --record(coap_mqtt_auth, { clientid - , username - , password - }). --record(lwm2m_context, { epn - , location - }). -define(OMA_ALTER_PATH_RT, <<"\"oma.lwm2m\"">>). @@ -42,7 +35,7 @@ -define(ERR_NOT_FOUND, <<"Not Found">>). -define(ERR_UNAUTHORIZED, <<"Unauthorized">>). -define(ERR_BAD_REQUEST, <<"Bad Request">>). - +-define(REG_PREFIX, <<"rd">>). -define(LWM2M_FORMAT_PLAIN_TEXT, 0). -define(LWM2M_FORMAT_LINK, 40). diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index 79664928d..e355e05cf 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. @@ -23,7 +23,7 @@ -define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("lwm2m_coap/include/coap.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -35,14 +35,14 @@ gateway.lwm2m { lifetime_max = 86400s qmode_time_windonw = 22 auto_observe = false - mountpoint = \"lwm2m/%e/\" + mountpoint = \"lwm2m\" update_msg_publish_condition = contains_object_list translators { - command = \"dn/#\" - response = \"up/resp\" - notify = \"up/notify\" - register = \"up/resp\" - update = \"up/resp\" + command = {topic = \"dn/#\", qos = 0} + response = {topic = \"up/resp\", qos = 0} + notify = {topic = \"up/notify\", qos = 0} + register = {topic = \"up/resp\", qos = 0} + update = {topic = \"up/resp\", qos = 0} } listeners.udp.default { bind = 5783 @@ -58,11 +58,15 @@ all() -> [ {group, test_grp_0_register} , {group, test_grp_1_read} , {group, test_grp_2_write} + , {group, test_grp_create} + , {group, test_grp_delete} , {group, test_grp_3_execute} , {group, test_grp_4_discover} , {group, test_grp_5_write_attr} , {group, test_grp_6_observe} - , {group, test_grp_8_object_19} + + %% {group, test_grp_8_object_19} + %% {group, test_grp_9_psm_queue_mode} ]. suite() -> [{timetrap, {seconds, 90}}]. @@ -70,65 +74,77 @@ suite() -> [{timetrap, {seconds, 90}}]. groups() -> RepeatOpt = {repeat_until_all_ok, 1}, [ - {test_grp_0_register, [RepeatOpt], [ - case01_register, - case01_register_additional_opts, - case01_register_incorrect_opts, - case01_register_report, - case02_update_deregister, - case03_register_wrong_version, - case04_register_and_lifetime_timeout, - case05_register_wrong_epn, - case06_register_wrong_lifetime, - case07_register_alternate_path_01, - case07_register_alternate_path_02, - case08_reregister - ]}, - {test_grp_1_read, [RepeatOpt], [ - case10_read, - case10_read_separate_ack, - case11_read_object_tlv, - case11_read_object_json, - case12_read_resource_opaque, - case13_read_no_xml - ]}, - {test_grp_2_write, [RepeatOpt], [ - case20_write, - case21_write_object, - case22_write_error, - case20_single_write - ]}, - {test_grp_create, [RepeatOpt], [ - case_create_basic - ]}, - {test_grp_delete, [RepeatOpt], [ - case_delete_basic - ]}, - {test_grp_3_execute, [RepeatOpt], [ - case30_execute, case31_execute_error - ]}, - {test_grp_4_discover, [RepeatOpt], [ - case40_discover - ]}, - {test_grp_5_write_attr, [RepeatOpt], [ - case50_write_attribute - ]}, - {test_grp_6_observe, [RepeatOpt], [ - case60_observe - ]}, - {test_grp_7_block_wize_transfer, [RepeatOpt], [ - case70_read_large, case70_write_large - ]}, - {test_grp_8_object_19, [RepeatOpt], [ - case80_specail_object_19_1_0_write, - case80_specail_object_19_0_0_notify - %case80_specail_object_19_0_0_response, - %case80_normal_object_19_0_0_read - ]}, - {test_grp_9_psm_queue_mode, [RepeatOpt], [ - case90_psm_mode, - case90_queue_mode - ]} + {test_grp_0_register, [RepeatOpt], + [ + case01_register, + case01_register_additional_opts, + %% case01_register_incorrect_opts, %% TODO now we can't handle partial decode packet + case01_register_report, + case02_update_deregister, + case03_register_wrong_version, + case04_register_and_lifetime_timeout, + case05_register_wrong_epn, + %% case06_register_wrong_lifetime, %% now, will ignore wrong lifetime + case07_register_alternate_path_01, + case07_register_alternate_path_02, + case08_reregister + ]}, + {test_grp_1_read, [RepeatOpt], + [ + case10_read, + case10_read_separate_ack, + case11_read_object_tlv, + case11_read_object_json, + case12_read_resource_opaque, + case13_read_no_xml + ]}, + {test_grp_2_write, [RepeatOpt], + [ + case20_write, + case21_write_object, + case22_write_error, + case20_single_write + ]}, + {test_grp_create, [RepeatOpt], + [ + case_create_basic + ]}, + {test_grp_delete, [RepeatOpt], + [ + case_delete_basic + ]}, + {test_grp_3_execute, [RepeatOpt], + [ + case30_execute, case31_execute_error + ]}, + {test_grp_4_discover, [RepeatOpt], + [ + case40_discover + ]}, + {test_grp_5_write_attr, [RepeatOpt], + [ + case50_write_attribute + ]}, + {test_grp_6_observe, [RepeatOpt], + [ + case60_observe + ]}, + {test_grp_7_block_wize_transfer, [RepeatOpt], + [ + case70_read_large, case70_write_large + ]}, + {test_grp_8_object_19, [RepeatOpt], + [ + case80_specail_object_19_1_0_write, + case80_specail_object_19_0_0_notify, + case80_specail_object_19_0_0_response, + case80_normal_object_19_0_0_read + ]}, + {test_grp_9_psm_queue_mode, [RepeatOpt], + [ + case90_psm_mode, + case90_queue_mode + ]} ]. init_per_suite(Config) -> @@ -162,9 +178,9 @@ end_per_testcase(_AllTestCase, Config) -> %%-------------------------------------------------------------------- case01_register(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -187,13 +203,13 @@ case01_register(Config) -> ?assertNotEqual(undefined, Location), %% checkpoint 2 - verify subscribed topics - timer:sleep(50), + timer:sleep(100), ?LOGT("all topics: ~p", [test_mqtt_broker:get_subscrbied_topics()]), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -209,9 +225,9 @@ case01_register(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case01_register_additional_opts(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -239,9 +255,9 @@ case01_register_additional_opts(Config) -> true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -257,9 +273,9 @@ case01_register_additional_opts(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case01_register_incorrect_opts(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -279,9 +295,9 @@ case01_register_incorrect_opts(Config) -> ?assertEqual({error,bad_request}, Method). case01_register_report(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -320,9 +336,9 @@ case01_register_report(Config) -> }), ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -338,9 +354,9 @@ case01_register_report(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case02_update_deregister(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -373,9 +389,9 @@ case02_update_deregister(Config) -> }), ?assertEqual(Register, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % UPDATE command - % ---------------------------------------- + %%---------------------------------------- + %% UPDATE command + %%---------------------------------------- ?LOGT("start to send UPDATE command", []), MsgId2 = 27, test_send_coap_request( UdpSock, @@ -399,9 +415,9 @@ case02_update_deregister(Config) -> }), ?assertEqual(Update, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -418,9 +434,9 @@ case02_update_deregister(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case03_register_wrong_version(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -432,15 +448,15 @@ case03_register_wrong_version(Config) -> [], MsgId), #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), - ?assertEqual({error,precondition_failed}, Method), + ?assertEqual({error, bad_request}, Method), timer:sleep(50), false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case04_register_and_lifetime_timeout(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -458,17 +474,17 @@ case04_register_and_lifetime_timeout(Config) -> true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % lifetime timeout - % ---------------------------------------- + %%---------------------------------------- + %% lifetime timeout + %%---------------------------------------- timer:sleep(4000), false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case05_register_wrong_epn(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- MsgId = 12, UdpSock = ?config(sock, Config), @@ -481,29 +497,29 @@ case05_register_wrong_epn(Config) -> #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), ?assertEqual({error,bad_request}, Method). -case06_register_wrong_lifetime(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- - UdpSock = ?config(sock, Config), - Epn = "urn:oma:lwm2m:oma:3", - MsgId = 12, +%% case06_register_wrong_lifetime(Config) -> +%% %%---------------------------------------- +%% %% REGISTER command +%% %%---------------------------------------- +%% UdpSock = ?config(sock, Config), +%% Epn = "urn:oma:lwm2m:oma:3", +%% MsgId = 12, - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s&lwm2m=1", [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId), - #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), - ?assertEqual({error,bad_request}, Method), - timer:sleep(50), - ?assertEqual([], test_mqtt_broker:get_subscrbied_topics()). +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s&lwm2m=1", [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId), +%% #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), +%% ?assertEqual({error,bad_request}, Method), +%% timer:sleep(50), +%% ?assertEqual([], test_mqtt_broker:get_subscrbied_topics()). case07_register_alternate_path_01(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -516,16 +532,16 @@ case07_register_alternate_path_01(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case07_register_alternate_path_02(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -538,16 +554,16 @@ case07_register_alternate_path_02(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case08_reregister(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -560,24 +576,24 @@ case08_reregister(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), ReadResult = emqx_json:encode( - #{ - <<"msgType">> => <<"register">>, - <<"data">> => #{ - <<"alternatePath">> => <<"/lwm2m">>, - <<"ep">> => list_to_binary(Epn), - <<"lt">> => 345, - <<"lwm2m">> => <<"1">>, - <<"objectList">> => [<<"/1/0">>, <<"/2/0">>, <<"/3/0">>] - } - } - ), + #{ + <<"msgType">> => <<"register">>, + <<"data">> => #{ + <<"alternatePath">> => <<"/lwm2m">>, + <<"ep">> => list_to_binary(Epn), + <<"lt">> => 345, + <<"lwm2m">> => <<"1">>, + <<"objectList">> => [<<"/1/0">>, <<"/2/0">>, <<"/3/0">>] + } + } + ), ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)), timer:sleep(1000), @@ -586,9 +602,10 @@ case08_reregister(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId + 1), + %% verify the lwm2m client is still online ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)). @@ -599,28 +616,28 @@ case10_read(Config) -> RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), timer:sleep(200), - % step 1, device register ... + %% step 1, device register ... test_send_coap_request( UdpSock, post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId1), #coap_message{method = Method1} = test_recv_coap_response(UdpSock), ?assertEqual({ok,created}, Method1), test_recv_mqtt_response(RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -638,17 +655,17 @@ case10_read(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/0">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/0">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case10_read_separate_ack(Config) -> @@ -661,19 +678,19 @@ case10_read_separate_ack(Config) -> emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), timer:sleep(200), - % step 1, device register ... + %% step 1, device register ... std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -688,12 +705,12 @@ case10_read_separate_ack(Config) -> test_send_empty_ack(UdpSock, "127.0.0.1", ?PORT, Request2), ReadResultACK = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"ack">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"ack">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }), ?assertEqual(ReadResultACK, test_recv_mqtt_response(RespTopic)), timer:sleep(100), @@ -701,21 +718,21 @@ case10_read_separate_ack(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/0">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/0">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case11_read_object_tlv(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -726,16 +743,16 @@ case11_read_object_tlv(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 207, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -752,31 +769,31 @@ case11_read_object_tlv(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0">>, - <<"content">> => [ - #{ - path => <<"/3/0/0">>, - value => <<"Open Mobile Alliance">> - }, - #{ - path => <<"/3/0/1">>, - value => <<"Lightweight M2M Client">> - }, - #{ - path => <<"/3/0/2">>, - value => <<"345000123">> - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0">>, + <<"content">> => [ + #{ + path => <<"/3/0/0">>, + value => <<"Open Mobile Alliance">> + }, + #{ + path => <<"/3/0/1">>, + value => <<"Lightweight M2M Client">> + }, + #{ + path => <<"/3/0/2">>, + value => <<"345000123">> + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case11_read_object_json(Config) -> - % step 1, device register ... + %% step 1, device register ... UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -788,16 +805,16 @@ case11_read_object_json(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -814,31 +831,31 @@ case11_read_object_json(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0">>, - <<"content">> => [ - #{ - path => <<"/3/0/0">>, - value => <<"Open Mobile Alliance">> - }, - #{ - path => <<"/3/0/1">>, - value => <<"Lightweight M2M Client">> - }, - #{ - path => <<"/3/0/2">>, - value => <<"345000123">> - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0">>, + <<"content">> => [ + #{ + path => <<"/3/0/0">>, + value => <<"Open Mobile Alliance">> + }, + #{ + path => <<"/3/0/1">>, + value => <<"Lightweight M2M Client">> + }, + #{ + path => <<"/3/0/2">>, + value => <<"345000123">> + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case12_read_resource_opaque(Config) -> - % step 1, device register ... + %% step 1, device register ... UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -849,16 +866,16 @@ case12_read_resource_opaque(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/8">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/8">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -875,23 +892,23 @@ case12_read_resource_opaque(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/8">>, - <<"content">> => [ - #{ - path => <<"/3/0/8">>, - value => base64:encode(Opaque) - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/8">>, + <<"content">> => [ + #{ + path => <<"/3/0/8">>, + value => base64:encode(Opaque) + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case13_read_no_xml(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -902,16 +919,16 @@ case13_read_no_xml(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/9723/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/9723/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -927,17 +944,17 @@ case13_read_no_xml(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"reqPath">> => <<"/9723/0/0">>, - <<"code">> => <<"4.00">>, - <<"codeMsg">> => <<"bad_request">> - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"reqPath">> => <<"/9723/0/0">>, + <<"code">> => <<"4.00">>, + <<"codeMsg">> => <<"bad_request">> + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case20_single_write(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -948,16 +965,16 @@ case20_single_write(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"path">> => <<"/3/0/13">>, - <<"type">> => <<"Integer">>, - <<"value">> => <<"12345">> - } + <<"path">> => <<"/3/0/13">>, + <<"type">> => <<"Integer">>, + <<"value">> => <<"12345">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -975,18 +992,18 @@ case20_single_write(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/13">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/13">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case20_write(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -997,18 +1014,18 @@ case20_write(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/13">>, - <<"content">> => [#{ - type => <<"Float">>, - value => <<"12345.0">> - }] - } + <<"basePath">> => <<"/3/0/13">>, + <<"content">> => [#{ + type => <<"Float">>, + value => <<"12345.0">> + }] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1026,18 +1043,18 @@ case20_write(Config) -> timer:sleep(100), WriteResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/13">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/13">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(WriteResult, test_recv_mqtt_response(RespTopic)). case21_write_object(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1048,23 +1065,23 @@ case21_write_object(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/">>, - <<"content">> => [#{ - path => <<"13">>, - type => <<"Integer">>, - value => <<"12345">> - },#{ - path => <<"14">>, - type => <<"String">>, - value => <<"87x">> - }] - } + <<"basePath">> => <<"/3/0/">>, + <<"content">> => [#{ + path => <<"13">>, + type => <<"Integer">>, + value => <<"12345">> + },#{ + path => <<"14">>, + type => <<"String">>, + value => <<"87x">> + }] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1084,18 +1101,18 @@ case21_write_object(Config) -> ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"write">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"write">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case22_write_error(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1106,20 +1123,20 @@ case22_write_error(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/1">>, - <<"content">> => [ - #{ - type => <<"Integer">>, - value => <<"12345">> - } - ] - } + <<"basePath">> => <<"/3/0/1">>, + <<"content">> => [ + #{ + type => <<"Integer">>, + value => <<"12345">> + } + ] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1135,18 +1152,18 @@ case22_write_error(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/1">>, - <<"code">> => <<"4.00">>, - <<"codeMsg">> => <<"bad_request">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/1">>, + <<"code">> => <<"4.00">>, + <<"codeMsg">> => <<"bad_request">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case_create_basic(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1157,15 +1174,14 @@ case_create_basic(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a CREATE command to device + %% step2, send a CREATE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"create">>, - <<"data">> => #{ - <<"path">> => <<"/5">> - } - }, + Command = #{<<"msgType">> => <<"create">>, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{<<"content">> => [], + <<"basePath">> => <<"/5">> + }}, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1181,18 +1197,18 @@ case_create_basic(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/5">>, - <<"code">> => <<"2.01">>, - <<"codeMsg">> => <<"created">> - }, - <<"msgType">> => <<"create">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/5">>, + <<"code">> => <<"2.01">>, + <<"codeMsg">> => <<"created">> + }, + <<"msgType">> => <<"create">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case_delete_basic(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1203,14 +1219,14 @@ case_delete_basic(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a CREATE command to device + %% step2, send a CREATE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"delete">>, <<"data">> => #{ - <<"path">> => <<"/5/0">> - } + <<"path">> => <<"/5/0">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1227,18 +1243,18 @@ case_delete_basic(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/5/0">>, - <<"code">> => <<"2.02">>, - <<"codeMsg">> => <<"deleted">> - }, - <<"msgType">> => <<"delete">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/5/0">>, + <<"code">> => <<"2.02">>, + <<"codeMsg">> => <<"deleted">> + }, + <<"msgType">> => <<"delete">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case30_execute(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1249,16 +1265,16 @@ case30_execute(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"execute">>, <<"data">> => #{ - <<"path">> => <<"/3/0/4">>, - %% "args" should not be present for "/3/0/4", only for testing the encoding here - <<"args">> => <<"2,7">> - } + <<"path">> => <<"/3/0/4">>, + %% "args" should not be present for "/3/0/4", only for testing the encoding here + <<"args">> => <<"2,7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1275,18 +1291,18 @@ case30_execute(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/4">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"execute">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/4">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"execute">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case31_execute_error(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1297,15 +1313,15 @@ case31_execute_error(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"execute">>, <<"data">> => #{ - <<"path">> => <<"/3/0/4">>, - <<"args">> => <<"2,7">> - } + <<"path">> => <<"/3/0/4">>, + <<"args">> => <<"2,7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1322,18 +1338,18 @@ case31_execute_error(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/4">>, - <<"code">> => <<"4.01">>, - <<"codeMsg">> => <<"uauthorized">> - }, - <<"msgType">> => <<"execute">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/4">>, + <<"code">> => <<"4.01">>, + <<"codeMsg">> => <<"unauthorized">> + }, + <<"msgType">> => <<"execute">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case40_discover(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1344,14 +1360,14 @@ case40_discover(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"discover">>, <<"data">> => #{ - <<"path">> => <<"/3/0/7">> - } }, + <<"path">> => <<"/3/0/7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1374,20 +1390,20 @@ case40_discover(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"discover">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/7">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => - [<<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2">>, <<"">>] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"discover">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/7">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => + [<<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2">>, <<"">>] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case50_write_attribute(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1398,17 +1414,17 @@ case50_write_attribute(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write-attr">>, <<"data">> => #{ - <<"path">> => <<"/3/0/9">>, - <<"pmin">> => <<"1">>, - <<"pmax">> => <<"5">>, - <<"lt">> => <<"5">> - } }, + <<"path">> => <<"/3/0/9">>, + <<"pmin">> => <<"1">>, + <<"pmax">> => <<"5">>, + <<"lt">> => <<"5">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(100), @@ -1433,18 +1449,18 @@ case50_write_attribute(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/9">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write-attr">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/9">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write-attr">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case60_observe(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1457,15 +1473,15 @@ case60_observe(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a OBSERVE command to device + %% step2, send a OBSERVE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"observe">>, <<"data">> => #{ - <<"path">> => <<"/3/0/10">> - } - }, + <<"path">> => <<"/3/0/10">> + } + }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1488,18 +1504,18 @@ case60_observe(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 2048 - }] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"observe">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 2048 + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), %% step3 the notifications @@ -1515,29 +1531,29 @@ case60_observe(Config) -> #coap_message{} = test_recv_coap_response(UdpSock), ReadResult2 = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"notify">>, - <<"seqNum">> => ObSeq, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 4096 - }] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"notify">>, + <<"seqNum">> => ObSeq, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 4096 + }] + } + }), ?assertEqual(ReadResult2, test_recv_mqtt_response(RespTopicAD)), %% Step3. cancel observe CmdId3 = 308, Command3 = #{<<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, - <<"msgType">> => <<"cancel-observe">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/10">> - } - }, + <<"msgType">> => <<"cancel-observe">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/10">> + } + }, CommandJson3 = emqx_json:encode(Command3), test_mqtt_broker:publish(CommandTopic, CommandJson3, 0), timer:sleep(50), @@ -1560,143 +1576,143 @@ case60_observe(Config) -> timer:sleep(100), ReadResult3 = emqx_json:encode(#{ - <<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, - <<"msgType">> => <<"cancel-observe">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 1150 - }] - } - }), + <<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, + <<"msgType">> => <<"cancel-observe">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 1150 + }] + } + }), ?assertEqual(ReadResult3, test_recv_mqtt_response(RespTopic)). -case80_specail_object_19_0_0_notify(Config) -> - % step 1, device register, with extra register options - Epn = "urn:oma:lwm2m:oma:3", - RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", - MsgId1 = 15, - UdpSock = ?config(sock, Config), +%% case80_specail_object_19_0_0_notify(Config) -> +%% %% step 1, device register, with extra register options +%% Epn = "urn:oma:lwm2m:oma:3", +%% RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", +%% MsgId1 = 15, +%% UdpSock = ?config(sock, Config), - RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), - emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), - timer:sleep(200), +%% RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), +%% emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), +%% timer:sleep(200), - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId1), - #coap_message{method = Method1} = test_recv_coap_response(UdpSock), - ?assertEqual({ok,created}, Method1), - ReadResult = emqx_json:encode(#{ - <<"msgType">> => <<"register">>, - <<"data">> => #{ - <<"alternatePath">> => <<"/">>, - <<"ep">> => list_to_binary(Epn), - <<"lt">> => 345, - <<"lwm2m">> => <<"1">>, - <<"objectList">> => [<<"/1">>, <<"/2">>, <<"/3">>, <<"/4">>, <<"/5">>], - <<"apn">> => <<"psmA.eDRX0.ctnb">>, - <<"im">> => <<"13456">>, - <<"ct">> => <<"2.0">>, - <<"mt">> => <<"MDM9206">>, - <<"mv">> => <<"4.0">> - } - }), - ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId1), +%% #coap_message{method = Method1} = test_recv_coap_response(UdpSock), +%% ?assertEqual({ok,created}, Method1), +%% ReadResult = emqx_json:encode(#{ +%% <<"msgType">> => <<"register">>, +%% <<"data">> => #{ +%% <<"alternatePath">> => <<"/">>, +%% <<"ep">> => list_to_binary(Epn), +%% <<"lt">> => 345, +%% <<"lwm2m">> => <<"1">>, +%% <<"objectList">> => [<<"/1">>, <<"/2">>, <<"/3">>, <<"/4">>, <<"/5">>], +%% <<"apn">> => <<"psmA.eDRX0.ctnb">>, +%% <<"im">> => <<"13456">>, +%% <<"ct">> => <<"2.0">>, +%% <<"mt">> => <<"MDM9206">>, +%% <<"mv">> => <<"4.0">> +%% } +%% }), +%% ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), - % step2, send a OBSERVE command to device - CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, - CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"path">> => <<"/19/0/0">> - } - }, - CommandJson = emqx_json:encode(Command), - test_mqtt_broker:publish(CommandTopic, CommandJson, 0), - timer:sleep(50), - Request2 = test_recv_coap_request(UdpSock), - #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, - Path2 = get_coap_path(Options2), - Observe = get_coap_observe(Options2), - ?assertEqual(get, Method2), - ?assertEqual(<<"/19/0/0">>, Path2), - ?assertEqual(Observe, 0), - ?assertEqual(<<>>, Payload2), - timer:sleep(50), +%% %% step2, send a OBSERVE command to device +%% CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, +%% CmdId = 307, +%% Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"msgType">> => <<"observe">>, +%% <<"data">> => #{ +%% <<"path">> => <<"/19/0/0">> +%% } +%% }, +%% CommandJson = emqx_json:encode(Command), +%% test_mqtt_broker:publish(CommandTopic, CommandJson, 0), +%% timer:sleep(50), +%% Request2 = test_recv_coap_request(UdpSock), +%% #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, +%% Path2 = get_coap_path(Options2), +%% Observe = get_coap_observe(Options2), +%% ?assertEqual(get, Method2), +%% ?assertEqual(<<"/19/0/0">>, Path2), +%% ?assertEqual(Observe, 0), +%% ?assertEqual(<<>>, Payload2), +%% timer:sleep(50), - test_send_coap_observe_ack( UdpSock, - "127.0.0.1", - ?PORT, - {ok, content}, - #coap_content{content_format = <<"text/plain">>, payload = <<"2048">>}, - Request2), - timer:sleep(100). +%% test_send_coap_observe_ack( UdpSock, +%% "127.0.0.1", +%% ?PORT, +%% {ok, content}, +%% #coap_content{content_format = <<"text/plain">>, payload = <<"2048">>}, +%% Request2), +%% timer:sleep(100). - %% step 3, device send uplink data notifications +%% step 3, device send uplink data notifications -case80_specail_object_19_1_0_write(Config) -> - Epn = "urn:oma:lwm2m:oma:3", - RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", - MsgId1 = 15, - UdpSock = ?config(sock, Config), - RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), - emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), - timer:sleep(200), +%% case80_specail_object_19_1_0_write(Config) -> +%% Epn = "urn:oma:lwm2m:oma:3", +%% RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", +%% MsgId1 = 15, +%% UdpSock = ?config(sock, Config), +%% RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), +%% emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), +%% timer:sleep(200), - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId1), - #coap_message{method = Method1} = test_recv_coap_response(UdpSock), - ?assertEqual({ok,created}, Method1), - test_recv_mqtt_response(RespTopic), +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId1), +%% #coap_message{method = Method1} = test_recv_coap_response(UdpSock), +%% ?assertEqual({ok,created}, Method1), +%% test_recv_mqtt_response(RespTopic), - % step2, send a WRITE command to device - CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, - CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"write">>, - <<"data">> => #{ - <<"path">> => <<"/19/1/0">>, - <<"type">> => <<"Opaque">>, - <<"value">> => base64:encode(<<12345:32>>) - } - }, +%% %% step2, send a WRITE command to device +%% CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, +%% CmdId = 307, +%% Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"msgType">> => <<"write">>, +%% <<"data">> => #{ +%% <<"path">> => <<"/19/1/0">>, +%% <<"type">> => <<"Opaque">>, +%% <<"value">> => base64:encode(<<12345:32>>) +%% } +%% }, - CommandJson = emqx_json:encode(Command), - test_mqtt_broker:publish(CommandTopic, CommandJson, 0), - timer:sleep(50), - Request2 = test_recv_coap_request(UdpSock), - #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, - Path2 = get_coap_path(Options2), - ?assertEqual(put, Method2), - ?assertEqual(<<"/19/1/0">>, Path2), - ?assertEqual(<<3:2, 0:1, 0:2, 4:3, 0, 12345:32>>, Payload2), - timer:sleep(50), +%% CommandJson = emqx_json:encode(Command), +%% test_mqtt_broker:publish(CommandTopic, CommandJson, 0), +%% timer:sleep(50), +%% Request2 = test_recv_coap_request(UdpSock), +%% #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, +%% Path2 = get_coap_path(Options2), +%% ?assertEqual(put, Method2), +%% ?assertEqual(<<"/19/1/0">>, Path2), +%% ?assertEqual(<<3:2, 0:1, 0:2, 4:3, 0, 12345:32>>, Payload2), +%% timer:sleep(50), - test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, changed}, #coap_content{}, Request2, true), - timer:sleep(100), +%% test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, changed}, #coap_content{}, Request2, true), +%% timer:sleep(100), - ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/19/1/0">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), - ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). +%% ReadResult = emqx_json:encode(#{ +%% <<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"data">> => #{ +%% <<"reqPath">> => <<"/19/1/0">>, +%% <<"code">> => <<"2.04">>, +%% <<"codeMsg">> => <<"changed">> +%% }, +%% <<"msgType">> => <<"write">> +%% }), +%% ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case90_psm_mode(Config) -> server_cache_mode(Config, "ep=~s<=345&lwm2m=1&apn=psmA.eDRX0.ctnb"). @@ -1705,9 +1721,10 @@ case90_queue_mode(Config) -> server_cache_mode(Config, "ep=~s<=345&lwm2m=1&b=UQ"). server_cache_mode(Config, RegOption) -> - application:set_env(?APP, qmode_time_window, 2), - - % step 1, device register, with apn indicates "PSM" mode + #{lwm2m := LwM2M} = Gateway = emqx:get_config([gateway]), + Gateway2 = Gateway#{lwm2m := LwM2M#{qmode_time_window => 2}}, + emqx_config:put([gateway], Gateway2), + %% step 1, device register, with apn indicates "PSM" mode Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -1756,12 +1773,12 @@ send_read_command_1(CmdId, _UdpSock) -> Epn = "urn:oma:lwm2m:oma:3", CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50). @@ -1778,16 +1795,16 @@ verify_read_response_1(CmdId, UdpSock) -> test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, content}, #coap_content{content_format = <<"text/plain">>, payload = <<"EMQ">>}, Request, true), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). device_update_1(UdpSock, Location) -> From cfe4e37d50e9c19e1fc16b0825efb595578387db Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:02:47 +0800 Subject: [PATCH 216/306] fix: retainer api doc qos enum (#5614) --- apps/emqx_retainer/src/emqx_retainer_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 503827d4b..7315d5a63 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -44,7 +44,7 @@ message_props() -> properties([ {id, string, <<"Message ID">>}, {topic, string, <<"MQTT Topic">>}, - {qos, string, <<"MQTT QoS">>}, + {qos, integer, <<"MQTT QoS">>, [0, 1, 2]}, {payload, string, <<"MQTT Payload">>}, {publish_at, string, <<"Publish datetime, in RFC 3339 format">>}, {from_clientid, string, <<"Publisher ClientId">>}, From ef1b6176248d31fc014c20678230ed2b3be1d411 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Tue, 31 Aug 2021 16:43:03 +0800 Subject: [PATCH 217/306] feat(authz api): support '/authorization/settings' api and update swagger schema Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 179 ++++++---------- .../src/emqx_authz_api_settings.erl | 61 ++++++ ...thz_api.erl => emqx_authz_api_sources.erl} | 197 +++--------------- .../test/emqx_authz_api_settings_SUITE.erl | 135 ++++++++++++ ...E.erl => emqx_authz_api_sources_SUITE.erl} | 2 +- 5 files changed, 298 insertions(+), 276 deletions(-) create mode 100644 apps/emqx_authz/src/emqx_authz_api_settings.erl rename apps/emqx_authz/src/{emqx_authz_api.erl => emqx_authz_api_sources.erl} (66%) create mode 100644 apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl rename apps/emqx_authz/test/{emqx_authz_api_SUITE.erl => emqx_authz_api_sources_SUITE.erl} (99%) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 1bc316986..4c17cd0b6 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -19,17 +19,17 @@ -export([definitions/0]). definitions() -> - RetruenedRules = #{ + RetruenedSources = #{ allOf => [ #{type => object, properties => #{ annotations => #{ type => object, - required => [id], + required => [status], properties => #{ - id => #{ - type => string - }, - principal => minirest:ref(<<"principal">>) + status => #{ + type => string, + example => <<"healthy">> + } } } } @@ -37,119 +37,76 @@ definitions() -> , minirest:ref(<<"sources">>) ] }, - Rules = #{ - oneOf => [ minirest:ref(<<"simple_source">>) - % , minirest:ref(<<"connector_redis">>) + Sources = #{ + oneOf => [ minirest:ref(<<"connector_redis">>) ] }, - % ConnectorRedis = #{ - % type => object, - % required => [principal, type, enable, config, cmd] - % properties => #{ - % principal => minirest:ref(<<"principal">>), - % type => #{ - % type => string, - % enum => [<<"redis">>], - % example => <<"redis">> - % }, - % enable => #{ - % type => boolean, - % example => true - % } - % config => #{ - % type => - % } - % } - % } - SimpleRule = #{ + ConnectorRedis= #{ type => object, - required => [principal, permission, action, topics], + required => [type, enable, config, cmd], properties => #{ - action => #{ + type => #{ type => string, - enum => [<<"publish">>, <<"subscribe">>, <<"all">>], - example => <<"publish">> + enum => [<<"redis">>], + example => <<"redis">> }, - permission => #{ + enable => #{ + type => boolean, + example => true + }, + config => #{ + oneOf => [ #{type => object, + required => [server, redis_type, pool_size, auto_reconnect], + properties => #{ + server => #{type => string, example => <<"127.0.0.1:3306">>}, + redis_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => string, example => mqtt} + } + } + , #{type => object, + required => [servers, redis_type, sentinel, pool_size, auto_reconnect], + properties => #{ + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"sentinel">>], + example => <<"sentinel">>}, + sentinel => #{type => string}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => string, example => mqtt} + } + } + , #{type => object, + required => [servers, redis_type, pool_size, auto_reconnect], + properties => #{ + servers => #{type => array, + items => #{type => string, example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"cluster">>], + example => <<"cluster">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => string, example => mqtt} + } + } + ], + type => object + }, + cmd => #{ type => string, - enum => [<<"allow">>, <<"deny">>], - example => <<"allow">> - }, - topics => #{ - type => array, - items => #{ - oneOf => [ #{type => string, example => <<"#">>} - , #{type => object, - required => [eq], - properties => #{ - eq => #{type => string} - }, - example => #{eq => <<"#">>} - } - ] - } - }, - principal => minirest:ref(<<"principal">>) - } - }, - Principal = #{ - oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - , #{type => string, enum=>[<<"all">>], example => <<"all">>} - , #{type => object, - required => ['and'], - properties => #{'and' => #{type => array, - items => #{oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - ]}}}, - example => #{'and' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} - } - , #{type => object, - required => ['or'], - properties => #{'and' => #{type => array, - items => #{oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - ]}}}, - example => #{'or' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} - } - ] - }, - PrincipalUsername = #{type => object, - required => [username], - properties => #{username => #{type => string}}, - example => #{username => <<"emqx">>} - }, - PrincipalClientid = #{type => object, - required => [clientid], - properties => #{clientid => #{type => string}}, - example => #{clientid => <<"emqx">>} - }, - PrincipalIpaddress = #{type => object, - required => [ipaddress], - properties => #{ipaddress => #{type => string}}, - example => #{ipaddress => <<"127.0.0.1">>} - }, - ErrorDef = #{ - type => object, - properties => #{ - code => #{ - type => string, - example => <<"BAD_REQUEST">> - }, - message => #{ - type => string + example => <<"HGETALL mqtt_authz">> } } }, - [ #{<<"returned_sources">> => RetruenedRules} - , #{<<"sources">> => Rules} - , #{<<"simple_source">> => SimpleRule} - , #{<<"principal">> => Principal} - , #{<<"principal_username">> => PrincipalUsername} - , #{<<"principal_clientid">> => PrincipalClientid} - , #{<<"principal_ipaddress">> => PrincipalIpaddress} - , #{<<"error">> => ErrorDef} + [ #{<<"returned_sources">> => RetruenedSources} + , #{<<"sources">> => Sources} + , #{<<"connector_redis">> => ConnectorRedis} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_settings.erl b/apps/emqx_authz/src/emqx_authz_api_settings.erl new file mode 100644 index 000000000..ac48fafd9 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_api_settings.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_authz_api_settings). + +-behavior(minirest_api). + +-export([ api_spec/0 + , settings/2 + ]). + +api_spec() -> + {[settings_api()], []}. + +authorization_settings() -> + maps:remove(<<"sources">>, emqx:get_raw_config([authorization], #{})). + +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(authorization_settings()). + +settings_api() -> + Metadata = #{ + get => #{ + description => "Get authorization settings", + responses => #{<<"200">> => emqx_mgmt_util:schema(conf_schema())} + }, + put => #{ + description => "Update authorization settings", + requestBody => emqx_mgmt_util:schema(conf_schema()), + responses => #{ + <<"200">> => emqx_mgmt_util:schema(conf_schema()), + <<"400">> => emqx_mgmt_util:bad_request() + } + } + }, + {"/authorization/settings", Metadata, settings}. + +settings(get, _Params) -> + {200, authorization_settings()}; + +settings(put, #{body := #{<<"no_match">> := NoMatch, + <<"deny_action">> := DenyAction, + <<"cache">> := Cache}}) -> + {ok, _} = emqx:update_config([authorization, no_match], NoMatch), + {ok, _} = emqx:update_config([authorization, deny_action], DenyAction), + {ok, _} = emqx:update_config([authorization, cache], Cache), + ok = emqx_authz_cache:drain_cache(), + {200, authorization_settings()}. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl similarity index 66% rename from apps/emqx_authz/src/emqx_authz_api.erl rename to apps/emqx_authz/src/emqx_authz_api_sources.erl index dc8694c3f..2ad5db1da 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -14,31 +14,29 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authz_api). +-module(emqx_authz_api_sources). -behavior(minirest_api). -include("emqx_authz.hrl"). --define(EXAMPLE_RETURNED_RULE1, - #{principal => <<"all">>, - permission => <<"allow">>, - action => <<"all">>, - topics => [<<"#">>], - annotations => #{id => 1} - }). - +-define(EXAMPLE_REDIS, + #{type=> redis, + config => #{server => <<"127.0.0.1:3306">>, + redis_type => single, + pool_size => 1, + auto_reconnect => true + }, + cmd => <<"HGETALL mqtt_authz">>}). +-define(EXAMPLE_RETURNED_REDIS, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) + ). -define(EXAMPLE_RETURNED_RULES, - #{sources => [?EXAMPLE_RETURNED_RULE1 - ] + #{sources => [?EXAMPLE_RETURNED_REDIS + ] }). --define(EXAMPLE_RULE1, #{principal => <<"all">>, - permission => <<"allow">>, - action => <<"all">>, - topics => [<<"#">>]}). - -export([ api_spec/0 , sources/2 , source/2 @@ -107,9 +105,9 @@ sources_api() -> 'application/json' => #{ schema => minirest:ref(<<"sources">>), examples => #{ - simple_source => #{ - summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RULE1) + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) } } } @@ -117,23 +115,7 @@ sources_api() -> }, responses => #{ <<"204">> => #{description => <<"Created">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"400">> => emqx_mgmt_util:bad_request() } }, put => #{ @@ -146,9 +128,9 @@ sources_api() -> items => minirest:ref(<<"returned_sources">>) }, examples => #{ - sources => #{ - summary => <<"Sources">>, - value => jsx:encode([?EXAMPLE_RULE1]) + redis => #{ + summary => <<"Redis">>, + value => jsx:encode([?EXAMPLE_REDIS]) } } } @@ -156,23 +138,7 @@ sources_api() -> }, responses => #{ <<"204">> => #{description => <<"Created">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"400">> => emqx_mgmt_util:bad_request() } } }, @@ -201,29 +167,13 @@ source_api() -> examples => #{ sources => #{ summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULE1) + value => jsx:encode(?EXAMPLE_RETURNED_REDIS) } } } } }, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"source xxx not found">> - } - } - } - } - } - } + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>) } }, put => #{ @@ -243,9 +193,9 @@ source_api() -> 'application/json' => #{ schema => minirest:ref(<<"sources">>), examples => #{ - simple_source => #{ - summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RULE1) + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) } } } @@ -253,47 +203,15 @@ source_api() -> }, responses => #{ <<"204">> => #{description => <<"No Content">>}, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"source xxx not found">> - } - } - } - } - } - }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>), + <<"400">> => emqx_mgmt_util:bad_request() } }, delete => #{ description => "Delete source", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -303,23 +221,7 @@ source_api() -> ], responses => #{ <<"204">> => #{description => <<"No Content">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"400">> => emqx_mgmt_util:bad_request() } } }, @@ -378,38 +280,8 @@ move_source_api() -> <<"204">> => #{ description => <<"No Content">> }, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ 'application/json' => #{ schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"source xxx not found">> - } - } - } - } - } - }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>), + <<"400">> => emqx_mgmt_util:bad_request() } } }, @@ -519,6 +391,3 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. - - - diff --git a/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl new file mode 100644 index 000000000..1db9fff2b --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl @@ -0,0 +1,135 @@ +%%-------------------------------------------------------------------- +%% 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_api_settings_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"). + +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + +-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 + , auth_header/2 + ]). + +-define(HOST, "http://127.0.0.1:18083/"). +-define(API_VERSION, "v5"). +-define(BASE_PATH, "api"). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]), + ok. + +set_special_configs(emqx_dashboard) -> + Config = #{ + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, + emqx_config:put([emqx_dashboard], Config), + ok; +set_special_configs(_App) -> + ok. + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_api(_) -> + Settings1 = #{<<"no_match">> => <<"deny">>, + <<"deny_action">> => <<"disconnect">>, + <<"cache">> => #{ + <<"enable">> => false, + <<"max_size">> => 32, + <<"ttl">> => 60000 + } + }, + + {ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1), + {ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []), + ?assertEqual(Settings1, jsx:decode(Result1)), + + Settings2 = #{<<"no_match">> => <<"allow">>, + <<"deny_action">> => <<"ignore">>, + <<"cache">> => #{ + <<"enable">> => true, + <<"max_size">> => 32, + <<"ttl">> => 60000 + } + }, + + {ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2), + {ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []), + ?assertEqual(Settings2, jsx:decode(Result2)), + + ok. + +%%-------------------------------------------------------------------- +%% HTTP Request +%%-------------------------------------------------------------------- + +request(Method, Url, Body) -> + Request = case Body of + [] -> {Url, [auth_header_()]}; + _ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)} + end, + ct:pal("Method: ~p, Request: ~p", [Method, Request]), + case httpc:request(Method, Request, [], [{body_format, binary}]) of + {error, socket_closed_remotely} -> + {error, socket_closed_remotely}; + {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } -> + {ok, Code, Return}; + {ok, {Reason, _, _}} -> + {error, Reason} + end. + +uri() -> uri([]). +uri(Parts) when is_list(Parts) -> + NParts = [E || E <- Parts], + ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). + +get_sources(Result) -> + maps:get(<<"sources">>, jsx:decode(Result), []). + +auth_header_() -> + Username = <<"admin">>, + Password = <<"public">>, + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl similarity index 99% rename from apps/emqx_authz/test/emqx_authz_api_SUITE.erl rename to apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 946b1a30b..55185de78 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -13,7 +13,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authz_api_SUITE). +-module(emqx_authz_api_sources_SUITE). -compile(nowarn_export_all). -compile(export_all). From 12b8297745f672e3090eb73b34356f729b58de61 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 1 Sep 2021 10:35:05 +0800 Subject: [PATCH 218/306] fix(config): emqx_config:fill_defaults/1,2 not working --- apps/emqx/src/emqx_config.erl | 23 ++----------- apps/emqx/test/emqx_config_SUITE.erl | 50 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 apps/emqx/test/emqx_config_SUITE.erl diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index ddedef024..317aba401 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -261,7 +261,7 @@ init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) -> normalize_conf(hocon_schema:richmap_to_map(RawRichConf))). normalize_conf(Conf) -> - maps:with(get_root_names(bin), Conf). + maps:with(get_root_names(), Conf). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). @@ -277,7 +277,7 @@ check_config(SchemaMod, RawConf) -> -spec fill_defaults(raw_config()) -> map(). fill_defaults(RawConf) -> - RootNames = get_root_names(bin), + RootNames = get_root_names(), maps:fold(fun(Key, Conf, Acc) -> SubMap = #{Key => Conf}, WithDefaults = case lists:member(Key, RootNames) of @@ -320,9 +320,6 @@ get_schema_mod(RootName) -> get_root_names() -> maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). -get_root_names(bin) -> - maps:keys(get_schema_mod()). - -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. save_configs(_AppEnvs, Conf, RawConf, OverrideConf) -> %% We may need also support hot config update for the apps that use application envs. @@ -412,14 +409,7 @@ do_deep_put(?RAW_CONF, KeyPath, Map, Value) -> root_names_from_conf(RawConf) -> Keys = maps:keys(RawConf), - StrNames = [str(K) || K <- Keys], - AtomNames = lists:foldl(fun(K, Acc) -> - try [atom(K) | Acc] - catch error:badarg -> Acc - end - end, [], Keys), - PossibleNames = StrNames ++ AtomNames, - [Name || Name <- get_root_names(), lists:member(Name, PossibleNames)]. + [Name || Name <- get_root_names(), lists:member(Name, Keys)]. atom(Bin) when is_binary(Bin) -> binary_to_existing_atom(Bin, latin1); @@ -428,13 +418,6 @@ atom(Str) when is_list(Str) -> atom(Atom) when is_atom(Atom) -> Atom. -str(Bin) when is_binary(Bin) -> - binary_to_list(Bin); -str(Str) when is_list(Str) -> - Str; -str(Atom) when is_atom(Atom) -> - atom_to_list(Atom). - bin(Bin) when is_binary(Bin) -> Bin; bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). diff --git a/apps/emqx/test/emqx_config_SUITE.erl b/apps/emqx/test/emqx_config_SUITE.erl new file mode 100644 index 000000000..50d575c0e --- /dev/null +++ b/apps/emqx/test/emqx_config_SUITE.erl @@ -0,0 +1,50 @@ +%%-------------------------------------------------------------------- +%% 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_config_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). +-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_fill_default_values(_) -> + Conf = #{ + <<"broker">> => #{ + <<"perf">> => #{}, + <<"route_batch_clean">> => false} + }, + ?assertMatch(#{<<"broker">> := + #{<<"enable_session_registry">> := true, + <<"perf">> := + #{<<"route_lock_type">> := key, + <<"trie_compaction">> := true}, + <<"route_batch_clean">> := false, + <<"session_locking_strategy">> := quorum, + <<"shared_dispatch_ack_enabled">> := false, + <<"shared_subscription_strategy">> := round_robin, + <<"sys_heartbeat_interval">> := "30s", + <<"sys_msg_interval">> := "1m"}}, + emqx_config:fill_defaults(Conf)). From 9d2f6503afcd81acd260e9db51378d3b2f4557df Mon Sep 17 00:00:00 2001 From: lafirest Date: Wed, 1 Sep 2021 12:34:02 +0800 Subject: [PATCH 219/306] fix(emqx_retainer): fix function clause error --- apps/emqx_retainer/src/emqx_retainer.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 3fab5958d..8e14dd21d 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -74,9 +74,11 @@ %%-------------------------------------------------------------------- %% Hook API %%-------------------------------------------------------------------- +-spec on_session_subscribed(_, _, emqx_types:subopts(), _) -> any(). on_session_subscribed(_, _, #{share := ShareName}, _) when ShareName =/= undefined -> ok; -on_session_subscribed(_, Topic, #{rh := Rh, is_new := IsNew}, Context) -> +on_session_subscribed(_, Topic, #{rh := Rh} = Opts, Context) -> + IsNew = maps:get(is_new, Opts, true), case Rh =:= 0 orelse (Rh =:= 1 andalso IsNew) of true -> dispatch(Context, Topic); _ -> ok From c4e279bb76f3320359c4c5de485fc65e0e95df22 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:44:34 +0800 Subject: [PATCH 220/306] fix: support https (#5606) * fix: support https --- apps/emqx_dashboard/etc/emqx_dashboard.conf | 41 +++---- apps/emqx_dashboard/src/emqx_dashboard.erl | 111 ++++++++++-------- .../emqx_dashboard/src/emqx_dashboard_app.erl | 2 +- .../src/emqx_dashboard_schema.erl | 5 +- rebar.config | 2 +- 5 files changed, 85 insertions(+), 76 deletions(-) diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf index 31c95a9ee..70b1d1d71 100644 --- a/apps/emqx_dashboard/etc/emqx_dashboard.conf +++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf @@ -11,35 +11,30 @@ emqx_dashboard { token_expired_time = 60m listeners = [ { + protocol = http num_acceptors = 4 max_connections = 512 - protocol = http port = 18083 backlog = 512 - send_timeout = 15s - send_timeout_close = true + send_timeout = 5s inet6 = false ipv6_v6only = false } -## , -## { -## protocol: https -## port: 18084 -## acceptors: 2 -## backlog: 512 -## send_timeout: 15s -## send_timeout_close: true -## 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 -## } + # , + # { + # protocol = https + # port = 18084 + # num_acceptors = 2 + # backlog = 512 + # send_timeout = 5s + # inet6 = false + # ipv6_v6only = false + # certfile = "etc/certs/cert.pem" + # keyfile = "etc/certs/key.pem" + # cacertfile = "etc/certs/cacert.pem" + # verify = verify_peer + # 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"] + # } ] } diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index fb0e25564..603d8009b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -20,9 +20,7 @@ -export([ start_listeners/0 - , stop_listeners/0 - , start_listener/1 - , stop_listener/1]). + , stop_listeners/0]). %% Authorization -export([authorize_appid/1]). @@ -36,15 +34,8 @@ %%-------------------------------------------------------------------- start_listeners() -> - lists:foreach(fun start_listener/1, listeners()). - -stop_listeners() -> - lists:foreach(fun stop_listener/1, listeners()). - -start_listener({Proto, Port, Options}) -> {ok, _} = application:ensure_all_started(minirest), Authorization = {?MODULE, authorize_appid}, - RanchOptions = ranch_opts(Port, Options), GlobalSpec = #{ openapi => "3.0.0", info => #{title => "EMQ X Dashboard API", version => "5.0.0"}, @@ -56,20 +47,33 @@ start_listener({Proto, Port, Options}) -> type => apiKey, name => "authorization", in => header}}}}, - Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, - {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, - {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}], - Minirest = #{ - protocol => Proto, + Dispatch = [ + {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, + {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, + {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}} + ], + BaseMinirest = #{ base_path => ?BASE_PATH, modules => minirest_api:find_api_modules(apps()), authorization => Authorization, security => [#{application => []}], swagger_global_spec => GlobalSpec, - dispatch => Dispatch}, - MinirestOptions = maps:merge(Minirest, RanchOptions), - {ok, _} = minirest:start(listener_name(Proto), MinirestOptions), - ?ULOG("Start ~p listener on ~p successfully.~n", [listener_name(Proto), Port]). + dispatch => Dispatch + }, + [begin + Minirest = maps:put(protocol, Protocol, BaseMinirest), + {ok, _} = minirest:start(Name, RanchOptions, Minirest), + ?ULOG("Start listener ~s on ~p successfully.~n", [Name, Port]) + end || {Name, Protocol, Port, RanchOptions} <- listeners()]. + +stop_listeners() -> + [begin + ok = minirest:stop(Name), + ?ULOG("Stop listener ~s on ~p successfully.~n", [Name, Port]) + end || {Name, _, Port, _} <- listeners()]. + +%%-------------------------------------------------------------------- +%% internal apps() -> [App || {App, _, _} <- application:loaded_applications(), @@ -78,30 +82,48 @@ apps() -> _ -> false end]. -ranch_opts(Port, Options0) -> - 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, _}) -> - ?ULOG("Stop dashboard listener on ~s successfully.~n", [format(Port)]), - minirest:stop(listener_name(Proto)). - listeners() -> - [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} - || Map = #{protocol := Protocol,port := Port} - <- emqx:get_config([emqx_dashboard, listeners], [])]. + [begin + Protocol = maps:get(protocol, ListenerOptions, http), + Port = maps:get(port, ListenerOptions, 18083), + Name = listener_name(Protocol, Port), + RanchOptions = ranch_opts(maps:without([protocol], ListenerOptions)), + {Name, Protocol, Port, RanchOptions} + end || ListenerOptions <- emqx_config:get([emqx_dashboard, listeners], [])]. -listener_name(Proto) -> - list_to_atom(atom_to_list(Proto) ++ ":dashboard"). +ranch_opts(RanchOptions) -> + Keys = [ {ack_timeout, handshake_timeout} + , connection_type + , max_connections + , num_acceptors + , shutdown + , socket], + {S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys), + R#{socket_opts => maps:fold(fun key_only/3, [], S)}. + + +key_take({K, K1}, {All, R}) -> + case maps:get(K, All, undefined) of + undefined -> + {All, R}; + V -> + {maps:remove(K, All), R#{K1 => V}} + end; +key_take(K, {All, R}) -> + case maps:get(K, All, undefined) of + undefined -> + {All, R}; + V -> + {maps:remove(K, All), R#{K => V}} + end. + +key_only(K , true , S) -> [K | S]; +key_only(_K, false, S) -> S; +key_only(K , V , S) -> [{K, V} | S]. + +listener_name(Protocol, Port) -> + Name = "dashboard:" ++ atom_to_list(Protocol) ++ ":" ++ integer_to_list(Port), + list_to_atom(Name). authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of @@ -127,10 +149,3 @@ authorize_appid(Req) -> #{code => <<"UNAUTHORIZED">>, message => <<"POST '/login'">>}} 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_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl index edcc19d8b..4e1b0caec 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl @@ -27,7 +27,7 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_dashboard_sup:start_link(), ok = ekka_rlog:wait_for_shards([?DASHBOARD_SHARD], infinity), - emqx_dashboard:start_listeners(), + _ = emqx_dashboard:start_listeners(), emqx_dashboard_cli:load(), ok = emqx_dashboard_admin:add_default_user(), {ok, Sup}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index e0ff21ada..7dfbc923b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -37,14 +37,13 @@ fields("http") -> , {"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(boolean(), undefined, true)} + , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "5s")} , {"inet6", emqx_schema:t(boolean(), undefined, false)} , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} ]; fields("https") -> - emqx_schema:ssl(#{enable => true}) ++ fields("http"). + proplists:delete("fail_if_no_peer_cert", emqx_schema:ssl(#{})) ++ fields("http"). default_username(type) -> string(); default_username(default) -> "admin"; diff --git a/rebar.config b/rebar.config index c1710bda2..4e622b738 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.0"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.1"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, "0.3.3"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From c7bc2e1a8d86b96993053311ec6ce5e64a193609 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 1 Sep 2021 17:42:57 +0800 Subject: [PATCH 221/306] fix: subscription about api, mqtt5 options param (#5620) --- apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf | 9 ++++----- .../src/emqx_auto_subscribe_api.erl | 2 +- .../src/emqx_auto_subscribe_placeholder.erl | 11 ++++++----- .../src/emqx_auto_subscribe_schema.erl | 11 ++++++++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf index c91b77aa1..f6d041dab 100644 --- a/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf +++ b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf @@ -4,10 +4,12 @@ auto_subscribe { # { # topic = "/c/${clientid}", # qos = 0 - # }, + # rh = 0 + # rap = 0 + # nl = 0 + # } # { # topic = "/u/${username}", - # qos = 1 # }, # { # topic = "/h/${host}", @@ -15,15 +17,12 @@ auto_subscribe { # }, # { # topic = "/p/${port}", - # qos = 0 # }, # { # topic = "/topic/abc", - # qos = 0 # }, # { # topic = "/client/${clientid}/username/${username}/host/${host}/port/${port}", - # qos = 0 # } ] } diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl index 7eeef52ff..97c9674b9 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -31,7 +31,7 @@ api_spec() -> schema() -> emqx_mgmt_util:schema( emqx_mgmt_api_configs:gen_schema( - emqx:get_raw_config([auto_subscribe, topics]))). + emqx:get_raw_config([auto_subscribe, topics])), <<"">>). auto_subscribe_api() -> Metadata = #{ diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl index cbe881bde..70779770d 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl @@ -23,16 +23,17 @@ generate(Topics) when is_list(Topics) -> [generate(Topic) || Topic <- Topics]; -generate(#{qos := Qos, topic := Topic}) when is_binary(Topic) -> - #{qos => Qos, placeholder => generate(Topic, [])}. +generate(T0 = #{topic := Topic}) -> + T = maps:without([topic], T0), + T#{placeholder => generate(Topic, [])}. -spec(to_topic_table(list(), map(), map()) -> list()). -to_topic_table(PlaceHolders, ClientInfo, ConnInfo) -> +to_topic_table(PHs, ClientInfo, ConnInfo) -> [begin Topic0 = to_topic(PlaceHolder, ClientInfo, ConnInfo, []), {Topic, Opts} = emqx_topic:parse(Topic0), - {Topic, Opts#{qos => Qos}} - end || #{qos := Qos, placeholder := PlaceHolder} <- PlaceHolders]. + {Topic, Opts#{qos => Qos, rh => RH, rap => RAP, nl => NL}} + end || #{qos := Qos, rh := RH, rap := RAP, nl := NL, placeholder := PlaceHolder} <- PHs]. %%-------------------------------------------------------------------- %% internal diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl index 73ae262a1..92420e217 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -30,5 +30,14 @@ fields("auto_subscribe") -> fields("topic") -> [ {topic, emqx_schema:t(binary())} - , {qos, emqx_schema:t(integer(), undefined, 0)} + , {qos, t(hoconsc:union([0, 1, 2]), 0)} + , {rh, t(hoconsc:union([0, 1, 2]), 0)} + , {rap, t(hoconsc:union([0, 1]), 0)} + , {nl, t(hoconsc:union([0, 1]), 0)} ]. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +t(Type, Default) -> + hoconsc:t(Type, #{default => Default}). From 7a98289d4a7d311d6988051bf9b1e308248bea70 Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 30 Aug 2021 11:32:48 +0200 Subject: [PATCH 222/306] chore: centos7 add openssl11 dep in rpm spec --- deploy/packages/rpm/emqx.spec | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deploy/packages/rpm/emqx.spec b/deploy/packages/rpm/emqx.spec index 882b7753e..4a4d6d0f3 100644 --- a/deploy/packages/rpm/emqx.spec +++ b/deploy/packages/rpm/emqx.spec @@ -19,6 +19,12 @@ BuildRoot: %{_tmppath}/%{_name}-%{_version}-root Provides: %{_name} AutoReq: 0 +%if 0%{?rhel} == 7 +Requires: openssl11 libatomic +%else +Requires: libatomic +%endif + %description EMQX, a distributed, massively scalable, highly extensible MQTT message broker written in Erlang/OTP. From 8252771306357ecf9b65004998c9e47ac8ae410a Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 1 Sep 2021 15:56:15 +0800 Subject: [PATCH 223/306] feat(authz api): support upload ssl cert file for api --- apps/emqx_authz/src/emqx_authz.erl | 2 +- apps/emqx_authz/src/emqx_authz_api_schema.erl | 26 ++- .../emqx_authz/src/emqx_authz_api_sources.erl | 163 +++++++++--------- .../test/emqx_authz_api_sources_SUITE.erl | 66 +++++-- 4 files changed, 157 insertions(+), 100 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 4a6d7033e..f3b9a4793 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -309,7 +309,7 @@ check_sources(RawSources) -> find_source_by_type(Type) -> find_source_by_type(Type, lookup()). find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1). -find_source_by_type(_, [], _N) -> error(not_found_rule); +find_source_by_type(_, [], _N) -> error(not_found_source); find_source_by_type(Type, [ Source = #{type := T} | Tail], N) -> case Type =:= T of true -> {N, Source}; diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 4c17cd0b6..7d8b583ab 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -26,6 +26,9 @@ definitions() -> type => object, required => [status], properties => #{ + id => #{ + type => string + }, status => #{ type => string, example => <<"healthy">> @@ -41,7 +44,18 @@ definitions() -> oneOf => [ minirest:ref(<<"connector_redis">>) ] }, - ConnectorRedis= #{ + SSL = #{ + type => object, + required => [enable], + properties => #{ + enable => #{type => boolean, example => true}, + cacertfile => #{type => string}, + keyfile => #{type => string}, + certfile => #{type => string}, + verify => #{type => boolean, example => false} + } + }, + ConnectorRedis = #{ type => object, required => [type, enable, config, cmd], properties => #{ @@ -65,7 +79,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } , #{type => object, @@ -80,7 +95,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } , #{type => object, @@ -94,7 +110,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } ], @@ -108,5 +125,6 @@ definitions() -> }, [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} + , #{<<"ssl">> => SSL} , #{<<"connector_redis">> => ConnectorRedis} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 2ad5db1da..a735d5d70 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -19,6 +19,7 @@ -behavior(minirest_api). -include("emqx_authz.hrl"). +-include_lib("emqx/include/logger.hrl"). -define(EXAMPLE_REDIS, #{type=> redis, @@ -32,7 +33,7 @@ maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) ). --define(EXAMPLE_RETURNED_RULES, +-define(EXAMPLE_RETURNED, #{sources => [?EXAMPLE_RETURNED_REDIS ] }). @@ -55,24 +56,6 @@ sources_api() -> Metadata = #{ get => #{ description => "List authorization sources", - parameters => [ - #{ - name => page, - in => query, - schema => #{ - type => integer - }, - required => false - }, - #{ - name => limit, - in => query, - schema => #{ - type => integer - }, - required => false - } - ], responses => #{ <<"200">> => #{ description => <<"OK">>, @@ -90,7 +73,7 @@ sources_api() -> examples => #{ sources => #{ summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULES) + value => jsx:encode(?EXAMPLE_RETURNED) } } } @@ -287,53 +270,38 @@ move_source_api() -> }, {"/authorization/sources/:type/move", Metadata, move_source}. -sources(get, #{query_string := Query}) -> - Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> - NSource = case emqx_resource:health_check(Id) of - ok -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}; - _ -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}} - end, - lists:append(AccIn, [NSource]); - (#{type := _Type, enable := true, annotations := #{id := Id}} = Source, AccIn) -> - NSource = case emqx_resource:health_check(Id) of - ok -> - Source#{annotations => #{status => healthy}}; - _ -> - Source#{annotations => #{status => unhealthy}} - end, - lists:append(AccIn, [NSource]); - (Source, AccIn) -> - lists:append(AccIn, [Source]) +sources(get, _) -> + Sources = lists:foldl(fun (#{type := _Type, enable := true, config := Config, annotations := #{id := Id}} = Source, AccIn) -> + NSource0 = case maps:get(server, Config, undefined) of + undefined -> Source; + Server -> + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + end, + NSource1 = case maps:get(servers, Config, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + end, + NSource2 = case emqx_resource:health_check(Id) of + ok -> + NSource1#{annotations => #{status => healthy}}; + _ -> + NSource1#{annotations => #{status => unhealthy}} + end, + lists:append(AccIn, [NSource2]); + (Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), - case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of - true -> - Page = maps:get(<<"page">>, Query), - Limit = maps:get(<<"limit">>, Query), - Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), - {_, Sources1} = lists:split(Index, Sources), - case binary_to_integer(Limit) < length(Sources1) of - true -> - {Sources2, _} = lists:split(binary_to_integer(Limit), Sources1), - {200, #{sources => Sources2}}; - false -> {200, #{sources => Sources1}} - end; - false -> {200, #{sources => Sources}} - end; -sources(post, #{body := RawConfig}) -> - case emqx_authz:update(head, [RawConfig]) of + {200, #{sources => Sources}}; +sources(post, #{body := Body}) -> + case emqx_authz:update(head, [save_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -sources(put, #{body := RawConfig}) -> - case emqx_authz:update(replace, RawConfig) of +sources(put, #{body := Body}) -> + case emqx_authz:update(replace, save_cert(Body)) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -345,27 +313,28 @@ source(get, #{bindings := #{type := Type}}) -> {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; #{enable := false} = Source -> {200, Source}; #{type := file} = Source -> {200, Source}; - #{config := #{server := Server, - annotations := #{id := Id} - } = Config} = Source -> - case emqx_resource:health_check(Id) of + #{config := Config, annotations := #{id := Id}} = Source -> + NSource0 = case maps:get(server, Config, undefined) of + undefined -> Source; + Server -> + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + end, + NSource1 = case maps:get(servers, Config, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + end, + NSource2 = case emqx_resource:health_check(Id) of ok -> - {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{status => healthy}}}; + NSource1#{annotations => #{status => healthy}}; _ -> - {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{status => unhealthy}}} - end; - #{config := #{annotations := #{id := Id}}} = Source -> - case emqx_resource:health_check(Id) of - ok -> - {200, Source#{annotations => #{status => healthy}}}; - _ -> - {200, Source#{annotations => #{status => unhealthy}}} - end + NSource1#{annotations => #{status => unhealthy}} + end, + {200, NSource2} end; -source(put, #{bindings := #{type := Type}, body := RawConfig}) -> - case emqx_authz:update({replace_once, Type}, RawConfig) of +source(put, #{bindings := #{type := Type}, body := Body}) -> + + case emqx_authz:update({replace_once, Type}, save_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, @@ -391,3 +360,39 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. + +save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Body) -> + CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), + CaCert = case maps:is_key(<<"cacertfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"cacertfile">>, SSL)); + false -> "" + end, + Cert = case maps:is_key(<<"certfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"certfile">>, SSL)); + false -> "" + end, + Key = case maps:is_key(<<"keyfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"keyfile">>, SSL)); + false -> "" + end, + Body#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key} + } + }; +save_cert(Body) -> Body. + +write_file(Filename, Bytes) -> + ok = filelib:ensure_dir(Filename), + case file:write_file(Filename, Bytes) of + ok -> Filename; + {error, Reason} -> + ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), + error(Reason) + end. diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 55185de78..ed3cf18d0 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -48,8 +48,10 @@ -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, + <<"mongo_type">> => <<"sharded">>, + <<"servers">> => [<<"127.0.0.1:27017">>, + <<"192.168.0.1:27017">> + ], <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"ssl">> => #{<<"enable">> => false}}, @@ -59,7 +61,7 @@ -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"server">> => <<"127.0.0.1:3306">>, <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"username">> => <<"xx">>, @@ -71,7 +73,7 @@ -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"server">> => <<"127.0.0.1:5432">>, <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"username">> => <<"xx">>, @@ -83,12 +85,15 @@ -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"servers">> => [<<"127.0.0.1:6379">>, + <<"127.0.0.1:6380">> + ], <<"pool_size">> => 1, <<"database">> => 0, <<"password">> => <<"ee">>, <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"ssl">> => #{<<"enable">> => false} + }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). @@ -144,6 +149,26 @@ set_special_configs(emqx_authz) -> set_special_configs(_App) -> ok. +init_per_testcase(t_api, Config) -> + meck:new(emqx_rule_id, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_rule_id, gen, fun() -> "fake" end), + + meck:new(emqx, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx, get_config, fun([node, data_dir]) -> + % emqx_ct_helpers:deps_path(emqx_authz, "test"); + {data_dir, Data} = lists:keyfind(data_dir, 1, Config), + Data; + (C) -> meck:passthrough([C]) + end), + Config; +init_per_testcase(_, Config) -> Config. + +end_per_testcase(t_api, _Config) -> + meck:unload(emqx_rule_id), + meck:unload(emqx), + ok; +end_per_testcase(_, _Config) -> ok. + %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ @@ -158,13 +183,6 @@ t_api(_) -> {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), - lists:foreach(fun(Page) -> - Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", - Url = uri(["authorization/sources" ++ Query]), - {ok, 200, Result} = request(get, Url, []), - ?assertEqual(10, length(get_sources(Result))) - end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), @@ -176,15 +194,31 @@ t_api(_) -> ], Sources), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), + #{<<"config">> := Config} = ?SOURCE2, + {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), + ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ + <<"enable">> => true, + <<"cacertfile">> => <<"fake cacert file">>, + <<"certfile">> => <<"fake cert file">>, + <<"keyfile">> => <<"fake key file">>, + <<"verify">> => false + }}}), + {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), + ?assertMatch(#{<<"type">> := <<"mongo">>, + <<"config">> := #{<<"ssl">> := #{<<"enable">> := true}} + }, jsx:decode(Result5)), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))), + lists:foreach(fun(#{<<"type">> := Type}) -> {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual([], get_sources(Result5)), + {ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result6)), ok. t_move_source(_) -> From 07dcd9e7052b6901d114348ff6a0964655d61a66 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 09:57:26 +0800 Subject: [PATCH 224/306] feat(authz api): support file type for sources --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 34 ++++++- .../emqx_authz/src/emqx_authz_api_sources.erl | 94 ++++++++++++++++--- apps/emqx_authz/test/emqx_authz_SUITE.erl | 16 +++- .../test/emqx_authz_api_sources_SUITE.erl | 14 ++- 4 files changed, 140 insertions(+), 18 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 7d8b583ab..18a5e2b18 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -41,7 +41,8 @@ definitions() -> ] }, Sources = #{ - oneOf => [ minirest:ref(<<"connector_redis">>) + oneOf => [ minirest:ref(<<"redis">>) + , minirest:ref(<<"file">>) ] }, SSL = #{ @@ -55,7 +56,7 @@ definitions() -> verify => #{type => boolean, example => false} } }, - ConnectorRedis = #{ + Redis = #{ type => object, required => [type, enable, config, cmd], properties => #{ @@ -123,8 +124,35 @@ definitions() -> } } }, + File = #{ + type => object, + required => [type, enable, rules], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + rules => #{ + type => array, + items => #{ + type => string, + example => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">> + } + }, + path => #{ + type => string, + example => <<"/path/to/authorizaiton_rules.conf">> + } + } + }, [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} , #{<<"ssl">> => SSL} - , #{<<"connector_redis">> => ConnectorRedis} + , #{<<"redis">> => Redis} + , #{<<"file">> => File} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index a735d5d70..0ebf5e511 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -23,18 +23,30 @@ -define(EXAMPLE_REDIS, #{type=> redis, + enable => true, config => #{server => <<"127.0.0.1:3306">>, redis_type => single, pool_size => 1, auto_reconnect => true }, cmd => <<"HGETALL mqtt_authz">>}). +-define(EXAMPLE_FILE, + #{type=> file, + enable => true, + rules => [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ]}). + -define(EXAMPLE_RETURNED_REDIS, maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) ). +-define(EXAMPLE_RETURNED_FILE, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_FILE) + ). -define(EXAMPLE_RETURNED, - #{sources => [?EXAMPLE_RETURNED_REDIS + #{sources => [ ?EXAMPLE_RETURNED_REDIS + , ?EXAMPLE_RETURNED_FILE ] }). @@ -91,6 +103,10 @@ sources_api() -> redis => #{ summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -113,7 +129,11 @@ sources_api() -> examples => #{ redis => #{ summary => <<"Redis">>, - value => jsx:encode([?EXAMPLE_REDIS]) + value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -148,9 +168,13 @@ source_api() -> 'application/json' => #{ schema => minirest:ref(<<"returned_sources">>), examples => #{ - sources => #{ - summary => <<"Sources">>, + redis => #{ + summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_RETURNED_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_RETURNED_FILE) } } } @@ -179,6 +203,10 @@ source_api() -> redis => #{ summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -271,7 +299,16 @@ move_source_api() -> {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, _) -> - Sources = lists:foldl(fun (#{type := _Type, enable := true, config := Config, annotations := #{id := Id}} = Source, AccIn) -> + Sources = lists:foldl(fun (#{enable := false} = Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); + (#{type := file, path := Path}, AccIn) -> + {ok, Rules} = file:consult(Path), + lists:append(AccIn, [#{type => file, + enable => true, + rules => [ io_lib:format("~p", [R])|| R <- Rules], + annotations => #{status => healthy} + }]); + (#{type := _Type, config := Config, annotations := #{id := Id}} = Source, AccIn) -> NSource0 = case maps:get(server, Config, undefined) of undefined -> Source; Server -> @@ -293,15 +330,33 @@ sources(get, _) -> lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; -sources(post, #{body := Body}) -> +sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> + Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +sources(post, #{body := Body}) when is_map(Body) -> case emqx_authz:update(head, [save_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -sources(put, #{body := Body}) -> - case emqx_authz:update(replace, save_cert(Body)) of +sources(put, #{body := Body}) when is_list(Body) -> + NBody = [ begin + case Source of + #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> + Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + #{type => file, enable => Enable, path => Filename}; + _ -> save_cert(Source) + end + end || Source <- Body], + case emqx_authz:update(replace, NBody) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -311,8 +366,15 @@ sources(put, #{body := Body}) -> source(get, #{bindings := #{type := Type}}) -> case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{enable := false} = Source -> {200, Source}; - #{type := file} = Source -> {200, Source}; + #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; + #{type := file, path := Path}-> + {ok, Rules} = file:consult(Path), + {200, #{type => file, + enable => true, + rules => Rules, + annotations => #{status => healthy} + } + }; #{config := Config, annotations := #{id := Id}} = Source -> NSource0 = case maps:get(server, Config, undefined) of undefined -> Source; @@ -332,8 +394,16 @@ source(get, #{bindings := #{type := Type}}) -> end, {200, NSource2} end; -source(put, #{bindings := #{type := Type}, body := Body}) -> - +source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> + #{path := Path} = emqx_authz:lookup(file), + write_file(Path, erlang:list_to_bitstring([<> || Rule <- Rules])), + case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Path}) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) -> case emqx_authz:update({replace_once, Type}, save_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index cee83cd30..88a0a9bf3 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -114,6 +114,11 @@ init_per_testcase(_, Config) -> <<"ssl">> => #{<<"enable">> => false}}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/authorization_rules.conf") + }). + %%------------------------------------------------------------------------------ %% Testcases @@ -125,12 +130,14 @@ t_update_source(_) -> {ok, _} = emqx_authz:update(head, [?SOURCE1]), {ok, _} = emqx_authz:update(tail, [?SOURCE4]), {ok, _} = emqx_authz:update(tail, [?SOURCE5]), + {ok, _} = emqx_authz:update(tail, [?SOURCE6]), ?assertMatch([ #{type := http, enable := true} , #{type := mongo, enable := true} , #{type := mysql, enable := true} , #{type := pgsql, enable := true} , #{type := redis, enable := true} + , #{type := file, enable := true} ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}), @@ -138,23 +145,26 @@ t_update_source(_) -> {ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}), {ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}), {ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, file}, ?SOURCE6#{<<"enable">> := false}), ?assertMatch([ #{type := http, enable := false} , #{type := mongo, enable := false} , #{type := mysql, enable := false} , #{type := pgsql, enable := false} , #{type := redis, enable := false} + , #{type := file, enable := false} ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update(replace, []). t_move_source(_) -> - {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), ?assertMatch([ #{type := http} , #{type := mongo} , #{type := mysql} , #{type := pgsql} , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(pgsql, <<"top">>), @@ -163,6 +173,7 @@ t_move_source(_) -> , #{type := mongo} , #{type := mysql} , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(http, <<"bottom">>), @@ -170,6 +181,7 @@ t_move_source(_) -> , #{type := mongo} , #{type := mysql} , #{type := redis} + , #{type := file} , #{type := http} ], emqx_authz:lookup()), @@ -178,6 +190,7 @@ t_move_source(_) -> , #{type := pgsql} , #{type := mongo} , #{type := redis} + , #{type := file} , #{type := http} ], emqx_authz:lookup()), @@ -185,6 +198,7 @@ t_move_source(_) -> ?assertMatch([ #{type := mysql} , #{type := pgsql} , #{type := redis} + , #{type := file} , #{type := http} , #{type := mongo} ], emqx_authz:lookup()), diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index ed3cf18d0..a7e747069 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -96,6 +96,13 @@ }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"rules">> => + [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ] + }). all() -> emqx_ct:all(?MODULE). @@ -119,7 +126,7 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -183,7 +190,7 @@ t_api(_) -> {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), Sources = get_sources(Result3), @@ -191,7 +198,10 @@ t_api(_) -> , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} , #{<<"type">> := <<"pgsql">>} + , #{<<"type">> := <<"redis">>} + , #{<<"type">> := <<"file">>} ], Sources), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), From b8ee977d9d474c6cdf4ae0df3853e4e9e7091950 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 10:44:04 +0800 Subject: [PATCH 225/306] feat(authz api): support read cert file for api --- .../emqx_authz/src/emqx_authz_api_sources.erl | 48 +++++++++++++------ .../test/emqx_authz_api_sources_SUITE.erl | 13 ++++- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 0ebf5e511..06d5aa859 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -325,7 +325,7 @@ sources(get, _) -> _ -> NSource1#{annotations => #{status => unhealthy}} end, - lists:append(AccIn, [NSource2]); + lists:append(AccIn, [read_cert(NSource2)]); (Source, AccIn) -> lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), @@ -340,7 +340,7 @@ sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"ena messgae => atom_to_binary(Reason)}} end; sources(post, #{body := Body}) when is_map(Body) -> - case emqx_authz:update(head, [save_cert(Body)]) of + case emqx_authz:update(head, [write_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -353,7 +353,7 @@ sources(put, #{body := Body}) when is_list(Body) -> Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), #{type => file, enable => Enable, path => Filename}; - _ -> save_cert(Source) + _ -> write_cert(Source) end end || Source <- Body], case emqx_authz:update(replace, NBody) of @@ -392,7 +392,7 @@ source(get, #{bindings := #{type := Type}}) -> _ -> NSource1#{annotations => #{status => unhealthy}} end, - {200, NSource2} + {200, read_cert(NSource2)} end; source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> #{path := Path} = emqx_authz:lookup(file), @@ -404,7 +404,7 @@ source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, < messgae => atom_to_binary(Reason)}} end; source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) -> - case emqx_authz:update({replace_once, Type}, save_cert(Body)) of + case emqx_authz:update({replace_once, Type}, write_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, @@ -431,7 +431,27 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos messgae => atom_to_binary(Reason)}} end. -save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Body) -> +read_cert(#{config := #{ssl := #{enable := true} = SSL} = Config} = Source) -> + CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of + {ok, CaCert0} -> CaCert0; + _ -> "" + end, + Cert = case file:read_file(maps:get(certfile, SSL, "")) of + {ok, Cert0} -> Cert0; + _ -> "" + end, + Key = case file:read_file(maps:get(keyfile, SSL, "")) of + {ok, Key0} -> Key0; + _ -> "" + end, + Source#{config => Config#{ssl => SSL#{cacertfile => CaCert, + certfile => Cert, + keyfile => Key + }} + }; +read_cert(Source) -> Source. + +write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Source) -> CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), CaCert = case maps:is_key(<<"cacertfile">>, SSL) of true -> @@ -439,24 +459,24 @@ save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Conf maps:get(<<"cacertfile">>, SSL)); false -> "" end, - Cert = case maps:is_key(<<"certfile">>, SSL) of + Cert = case maps:is_key(<<"certfile">>, SSL) of true -> write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), maps:get(<<"certfile">>, SSL)); false -> "" end, - Key = case maps:is_key(<<"keyfile">>, SSL) of + Key = case maps:is_key(<<"keyfile">>, SSL) of true -> write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), maps:get(<<"keyfile">>, SSL)); false -> "" end, - Body#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, - <<"certfile">> => Cert, - <<"keyfile">> => Key} - } - }; -save_cert(Body) -> Body. + Source#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key} + } + }; +write_cert(Source) -> Source. write_file(Filename, Bytes) -> ok = filelib:ensure_dir(Filename), diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index a7e747069..bb2bb27e4 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -126,7 +126,7 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -208,6 +208,10 @@ t_api(_) -> ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), #{<<"config">> := Config} = ?SOURCE2, + + dbg:tracer(),dbg:p(all,c), + dbg:tpl(emqx_authz_api_sources, read_cert, cx), + dbg:tpl(emqx_authz_api_sources, write_cert, cx), {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ <<"enable">> => true, @@ -218,7 +222,12 @@ t_api(_) -> }}}), {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), ?assertMatch(#{<<"type">> := <<"mongo">>, - <<"config">> := #{<<"ssl">> := #{<<"enable">> := true}} + <<"config">> := #{<<"ssl">> := #{<<"enable">> := true, + <<"cacertfile">> := <<"fake cacert file">>, + <<"certfile">> := <<"fake cert file">>, + <<"keyfile">> := <<"fake key file">>, + <<"verify">> := false + }} }, jsx:decode(Result5)), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), From 5669ea4034c96ee035ff77b191ab80d0f7e630bc Mon Sep 17 00:00:00 2001 From: Rory Z Date: Thu, 2 Sep 2021 11:13:41 +0800 Subject: [PATCH 226/306] chore(authz api): fix dialyzer error --- .../emqx_authz/src/emqx_authz_api_sources.erl | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 06d5aa859..8bc99188e 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -331,8 +331,9 @@ sources(get, _) -> end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> - Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), - write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of {ok, _} -> {204}; {error, Reason} -> @@ -350,8 +351,9 @@ sources(put, #{body := Body}) when is_list(Body) -> NBody = [ begin case Source of #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> - Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), - write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), #{type => file, enable => Enable, path => Filename}; _ -> write_cert(Source) end @@ -395,9 +397,10 @@ source(get, #{bindings := #{type := Type}}) -> {200, read_cert(NSource2)} end; source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> - #{path := Path} = emqx_authz:lookup(file), - write_file(Path, erlang:list_to_bitstring([<> || Rule <- Rules])), - case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Path}) of + {ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), + case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Filename}) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -455,20 +458,23 @@ write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Con CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), CaCert = case maps:is_key(<<"cacertfile">>, SSL) of true -> - write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), - maps:get(<<"cacertfile">>, SSL)); + {ok, CaCertFile} = write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"cacertfile">>, SSL)), + CaCertFile; false -> "" end, Cert = case maps:is_key(<<"certfile">>, SSL) of true -> - write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), - maps:get(<<"certfile">>, SSL)); + {ok, CertFile} = write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"certfile">>, SSL)), + CertFile; false -> "" end, Key = case maps:is_key(<<"keyfile">>, SSL) of true -> - write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), - maps:get(<<"keyfile">>, SSL)); + {ok, KeyFile} = write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"keyfile">>, SSL)), + KeyFile; false -> "" end, Source#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, @@ -481,7 +487,7 @@ write_cert(Source) -> Source. write_file(Filename, Bytes) -> ok = filelib:ensure_dir(Filename), case file:write_file(Filename, Bytes) of - ok -> Filename; + ok -> {ok, Filename}; {error, Reason} -> ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), error(Reason) From 4e8ac36348070e13a283e5f9b098acddb4ab401c Mon Sep 17 00:00:00 2001 From: Swilder-M Date: Thu, 2 Sep 2021 11:33:24 +0800 Subject: [PATCH 227/306] chore(README): modify slack badge --- 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 b430d4b5f..3e775ad70 100644 --- a/README-CN.md +++ b/README-CN.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow)](https://askemq.com) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ%20中文-FF0000?logo=youtube)](https://www.youtube.com/channel/UCir_r04HIsLjf2qqyZ4A8Cg) diff --git a/README-JP.md b/README-JP.md index 6e1c62f2f..03324a3b1 100644 --- a/README-JP.md +++ b/README-JP.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) diff --git a/README-RU.md b/README-RU.md index 2a06dac71..2eadc92e5 100644 --- a/README-RU.md +++ b/README-RU.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) diff --git a/README.md b/README.md index 1726d426b..207ba601f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) From 2426482ae19d1ca1c496233dba15448015e3f6df Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 30 Aug 2021 22:41:37 +0200 Subject: [PATCH 228/306] ci: install openssl11 as deps. --- .ci/build_packages/tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 87c19621a..240d6214e 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -91,6 +91,8 @@ emqx_test(){ ;; "rpm") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.rpm) + # EMQX OTP requires openssl11 to have TLS1.3 support + yum install -y openssl11 rpm -ivh "${PACKAGE_PATH}/${packagename}" if ! rpm -q emqx | grep -q emqx; then echo "package install error" From 0fd18a2795bd23364b454675d1ffcb5114e0d747 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 14:08:23 +0800 Subject: [PATCH 229/306] chore(emqx_authz): compression configuration items Signed-off-by: zhanghongtong --- apps/emqx_authz/etc/emqx_authz.conf | 76 ++++++------- apps/emqx_authz/src/emqx_authz.erl | 16 ++- .../emqx_authz/src/emqx_authz_api_sources.erl | 47 ++++---- apps/emqx_authz/src/emqx_authz_http.erl | 12 +- apps/emqx_authz/src/emqx_authz_schema.erl | 98 ++++++++--------- apps/emqx_authz/test/emqx_authz_SUITE.erl | 63 +++++------ .../test/emqx_authz_api_sources_SUITE.erl | 103 ++++++++---------- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 11 +- .../test/emqx_authz_mongo_SUITE.erl | 11 +- .../test/emqx_authz_mysql_SUITE.erl | 15 ++- .../test/emqx_authz_pgsql_SUITE.erl | 15 ++- .../test/emqx_authz_redis_SUITE.erl | 13 +-- 12 files changed, 218 insertions(+), 262 deletions(-) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 8eadab38b..99b14f5fe 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -2,66 +2,56 @@ authorization { sources = [ # { # type: http - # config: { - # url: "https://emqx.com" - # headers: { - # Accept: "application/json" - # Content-Type: "application/json" - # } + # url: "https://emqx.com" + # headers: { + # Accept: "application/json" + # Content-Type: "application/json" # } # }, # { # type: mysql - # config: { - # server: "127.0.0.1:3306" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: { - # enable: true - # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" - # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" - # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" - # } + # server: "127.0.0.1:3306" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: { + # enable: true + # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" + # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" # } # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or clientid = '%c'" # }, # { # type: pgsql - # config: { - # server: "127.0.0.1:5432" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: {enable: false} - # } + # server: "127.0.0.1:5432" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: {enable: false} # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" # }, # { # type: redis - # config: { - # server: "127.0.0.1:6379" - # database: 0 - # pool_size: 1 - # password: public - # auto_reconnect: true - # ssl: {enable: false} - # } + # server: "127.0.0.1:6379" + # database: 0 + # pool_size: 1 + # password: public + # auto_reconnect: true + # ssl: {enable: false} # cmd: "HGETALL mqtt_authz:%u" # }, # { # type: mongo - # config: { - # mongo_type: single - # server: "127.0.0.1:27017" - # pool_size: 1 - # database: mqtt - # ssl: {enable: false} - # } + # mongo_type: single + # server: "127.0.0.1:27017" + # pool_size: 1 + # database: mqtt + # ssl: {enable: false} # collection: mqtt_authz # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } # }, diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f3b9a4793..0d116882c 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -224,10 +224,10 @@ init_source(#{enable := true, Source#{annotations => #{rules => Rules}}; init_source(#{enable := true, type := http, - config := #{url := Url} = Config + url := Url } = Source) -> - NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), - case create_resource(Source#{config := NConfig}) of + NSource= maps:put(base_url, maps:remove(query, Url), Source), + case create_resource(NSource) of {error, Reason} -> error({load_config_error, Reason}); Id -> Source#{annotations => #{id => Id}} end; @@ -325,16 +325,14 @@ gen_id(Type) -> iolist_to_binary([io_lib:format("~s_~s",[?APP, Type])]). create_resource(#{type := DB, - config := Config, - annotations := #{id := ResourceID}}) -> - case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of + annotations := #{id := ResourceID}} = Source) -> + case emqx_resource:update(ResourceID, connector_module(DB), Source, []) of {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} end; -create_resource(#{type := DB, - config := Config}) -> +create_resource(#{type := DB} = Source) -> ResourceID = gen_id(DB), - case emqx_resource:create(ResourceID, connector_module(DB), Config) of + case emqx_resource:create(ResourceID, connector_module(DB), Source) of {ok, already_created} -> ResourceID; {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 8bc99188e..b2d33eca5 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -24,11 +24,10 @@ -define(EXAMPLE_REDIS, #{type=> redis, enable => true, - config => #{server => <<"127.0.0.1:3306">>, - redis_type => single, - pool_size => 1, - auto_reconnect => true - }, + server => <<"127.0.0.1:3306">>, + redis_type => single, + pool_size => 1, + auto_reconnect => true, cmd => <<"HGETALL mqtt_authz">>}). -define(EXAMPLE_FILE, #{type=> file, @@ -308,16 +307,16 @@ sources(get, _) -> rules => [ io_lib:format("~p", [R])|| R <- Rules], annotations => #{status => healthy} }]); - (#{type := _Type, config := Config, annotations := #{id := Id}} = Source, AccIn) -> - NSource0 = case maps:get(server, Config, undefined) of + (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> + NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; Server -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)} end, - NSource1 = case maps:get(servers, Config, undefined) of + NSource1 = case maps:get(servers, Source, undefined) of undefined -> NSource0; Servers -> - NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]} end, NSource2 = case emqx_resource:health_check(Id) of ok -> @@ -377,16 +376,16 @@ source(get, #{bindings := #{type := Type}}) -> annotations => #{status => healthy} } }; - #{config := Config, annotations := #{id := Id}} = Source -> - NSource0 = case maps:get(server, Config, undefined) of + #{annotations := #{id := Id}} = Source -> + NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; Server -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)} end, - NSource1 = case maps:get(servers, Config, undefined) of + NSource1 = case maps:get(servers, Source, undefined) of undefined -> NSource0; Servers -> - NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]} end, NSource2 = case emqx_resource:health_check(Id) of ok -> @@ -434,7 +433,7 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos messgae => atom_to_binary(Reason)}} end. -read_cert(#{config := #{ssl := #{enable := true} = SSL} = Config} = Source) -> +read_cert(#{ssl := #{enable := true} = SSL} = Source) -> CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of {ok, CaCert0} -> CaCert0; _ -> "" @@ -447,14 +446,14 @@ read_cert(#{config := #{ssl := #{enable := true} = SSL} = Config} = Source) -> {ok, Key0} -> Key0; _ -> "" end, - Source#{config => Config#{ssl => SSL#{cacertfile => CaCert, - certfile => Cert, - keyfile => Key - }} + Source#{ssl => SSL#{cacertfile => CaCert, + certfile => Cert, + keyfile => Key + } }; read_cert(Source) -> Source. -write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Source) -> +write_cert(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) -> CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), CaCert = case maps:is_key(<<"cacertfile">>, SSL) of true -> @@ -477,9 +476,9 @@ write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Con KeyFile; false -> "" end, - Source#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, - <<"certfile">> => Cert, - <<"keyfile">> => Key} + Source#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key } }; write_cert(Source) -> Source. diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index c95d200e1..93aa634f3 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -35,12 +35,12 @@ description() -> authorize(Client, PubSub, Topic, #{type := http, - config := #{url := #{path := Path} = Url, - headers := Headers, - method := Method, - request_timeout := RequestTimeout} = Config, + url := #{path := Path} = Url, + headers := Headers, + method := Method, + request_timeout := RequestTimeout, annotations := #{id := ResourceID} - }) -> + } = Source) -> Request = case Method of get -> Query = maps:get(query, Url, ""), @@ -49,7 +49,7 @@ authorize(Client, PubSub, Topic, _ -> Body0 = serialize_body( maps:get('Accept', Headers, <<"application/json">>), - maps:get(body, Config, #{}) + maps:get(body, Source, #{}) ), Body1 = replvar(Body0, PubSub, Topic, Client), Path1 = replvar(Path, PubSub, Topic, Client), diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 7fb60bae2..4d8fa3579 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -20,7 +20,20 @@ roots() -> ["authorization"]. fields("authorization") -> - [ {sources, sources()} + [ {sources, #{type => union_array( + [ hoconsc:ref(?MODULE, file) + , hoconsc:ref(?MODULE, http_get) + , hoconsc:ref(?MODULE, http_post) + , hoconsc:ref(?MODULE, mongo_single) + , hoconsc:ref(?MODULE, mongo_rs) + , hoconsc:ref(?MODULE, mongo_sharded) + , hoconsc:ref(?MODULE, mysql) + , hoconsc:ref(?MODULE, pgsql) + , hoconsc:ref(?MODULE, redis_single) + , hoconsc:ref(?MODULE, redis_sentinel) + , hoconsc:ref(?MODULE, redis_cluster) + ])} + } ]; fields(file) -> [ {type, #{type => file}} @@ -34,17 +47,11 @@ fields(file) -> end }} ]; -fields(http) -> +fields(http_get) -> [ {type, #{type => http}} , {enable, #{type => boolean(), default => true}} - , {config, #{type => hoconsc:union([ hoconsc:ref(?MODULE, http_get) - , hoconsc:ref(?MODULE, http_post) - ])} - } - ]; -fields(http_get) -> - [ {url, #{type => url()}} + , {url, #{type => url()}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -68,7 +75,10 @@ fields(http_get) -> , {request_timeout, #{type => timeout(), default => 30000 }} ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); fields(http_post) -> - [ {url, #{type => url()}} + [ {type, #{type => http}} + , {enable, #{type => boolean(), + default => true}} + , {url, #{type => url()}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -97,47 +107,36 @@ fields(http_post) -> } } ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); -fields(mongo) -> - connector_fields(mongo) ++ +fields(mongo_single) -> + connector_fields(mongo, single) ++ + [ {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; +fields(mongo_rs) -> + connector_fields(mongo, rs) ++ + [ {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; +fields(mongo_sharded) -> + connector_fields(mongo, sharded) ++ [ {collection, #{type => atom()}} , {find, #{type => map()}} ]; -fields(redis) -> - connector_fields(redis) ++ - [ {cmd, query()} ]; fields(mysql) -> connector_fields(mysql) ++ [ {sql, query()} ]; fields(pgsql) -> connector_fields(pgsql) ++ [ {sql, query()} ]; -fields(username) -> - [{username, #{type => binary()}}]; -fields(clientid) -> - [{clientid, #{type => binary()}}]; -fields(ipaddress) -> - [{ipaddress, #{type => string()}}]; -fields(andlist) -> - [{'and', #{type => union_array( - [ hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - ]) - } - } - ]; -fields(orlist) -> - [{'or', #{type => union_array( - [ hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - ]) - } - } - ]; -fields(eq_topic) -> - [{eq, #{type => binary()}}]. - +fields(redis_single) -> + connector_fields(redis, single) ++ + [ {cmd, query()} ]; +fields(redis_sentinel) -> + connector_fields(redis, sentinel) ++ + [ {cmd, query()} ]; +fields(redis_cluster) -> + connector_fields(redis, cluster) ++ + [ {cmd, query()} ]. %%-------------------------------------------------------------------- %% Internal functions @@ -146,17 +145,6 @@ fields(eq_topic) -> union_array(Item) when is_list(Item) -> hoconsc:array(hoconsc:union(Item)). -sources() -> - #{type => union_array( - [ hoconsc:ref(?MODULE, file) - , hoconsc:ref(?MODULE, http) - , hoconsc:ref(?MODULE, mysql) - , hoconsc:ref(?MODULE, pgsql) - , hoconsc:ref(?MODULE, redis) - , hoconsc:ref(?MODULE, mongo) - ]) - }. - query() -> #{type => binary(), validator => fun(S) -> @@ -168,6 +156,8 @@ query() -> }. connector_fields(DB) -> + connector_fields(DB, config). +connector_fields(DB, Fields) -> Mod0 = io_lib:format("~s_~s",[emqx_connector, DB]), Mod = try list_to_existing_atom(Mod0) @@ -180,4 +170,4 @@ connector_fields(DB) -> [ {type, #{type => DB}} , {enable, #{type => boolean(), default => true}} - ] ++ Mod:roots(). + ] ++ Mod:fields(Fields). diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 88a0a9bf3..6e6597486 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -62,56 +62,51 @@ init_per_testcase(_, Config) -> -define(SOURCE1, #{<<"type">> => <<"http">>, <<"enable">> => true, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). -define(SOURCE6, #{<<"type">> => <<"file">>, diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index bb2bb27e4..3c054aa7d 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -39,61 +39,55 @@ -define(SOURCE1, #{<<"type">> => <<"http">>, <<"enable">> => true, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, - <<"config">> => #{ - <<"mongo_type">> => <<"sharded">>, - <<"servers">> => [<<"127.0.0.1:27017">>, - <<"192.168.0.1:27017">> - ], - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, + <<"mongo_type">> => <<"sharded">>, + <<"servers">> => [<<"127.0.0.1:27017">>, + <<"192.168.0.1:27017">> + ], + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:3306">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:3306">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:5432">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:5432">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, - <<"config">> => #{ - <<"servers">> => [<<"127.0.0.1:6379">>, - <<"127.0.0.1:6380">> - ], - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, + <<"servers">> => [<<"127.0.0.1:6379">>, + <<"127.0.0.1:6380">> + ], + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). -define(SOURCE6, #{<<"type">> => <<"file">>, @@ -207,27 +201,22 @@ t_api(_) -> {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), - #{<<"config">> := Config} = ?SOURCE2, - - dbg:tracer(),dbg:p(all,c), - dbg:tpl(emqx_authz_api_sources, read_cert, cx), - dbg:tpl(emqx_authz_api_sources, write_cert, cx), {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), - ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ - <<"enable">> => true, - <<"cacertfile">> => <<"fake cacert file">>, - <<"certfile">> => <<"fake cert file">>, - <<"keyfile">> => <<"fake key file">>, - <<"verify">> => false - }}}), + ?SOURCE2#{<<"ssl">> := #{ + <<"enable">> => true, + <<"cacertfile">> => <<"fake cacert file">>, + <<"certfile">> => <<"fake cert file">>, + <<"keyfile">> => <<"fake key file">>, + <<"verify">> => false + }}), {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), ?assertMatch(#{<<"type">> := <<"mongo">>, - <<"config">> := #{<<"ssl">> := #{<<"enable">> := true, - <<"cacertfile">> := <<"fake cacert file">>, - <<"certfile">> := <<"fake cert file">>, - <<"keyfile">> := <<"fake key file">>, - <<"verify">> := false - }} + <<"ssl">> := #{<<"enable">> := true, + <<"cacertfile">> := <<"fake cacert file">>, + <<"certfile">> := <<"fake cert file">>, + <<"keyfile">> := <<"fake key file">>, + <<"verify">> := false + } }, jsx:decode(Result5)), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index fad5e9580..17763d993 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -47,12 +47,11 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"http">>, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000 - }} + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 + } ], {ok, _} = emqx_authz:update(replace, Rules), Config. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index db111ce83..8f4a6f29f 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -47,12 +47,11 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"mongo">>, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }], diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 0675e1caf..1173b0e3e 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -48,14 +48,13 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"mysql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }], {ok, _} = emqx_authz:update(replace, Rules), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 6880ab405..24c2e7b35 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -48,14 +48,13 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"pgsql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }], {ok, _} = emqx_authz:update(replace, Rules), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 09682761d..9949e8b51 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -47,13 +47,12 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"redis">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }], {ok, _} = emqx_authz:update(replace, Rules), From 516f2fd06e988cb370c89530cec7f492aa7a495b Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Thu, 2 Sep 2021 15:30:53 +0800 Subject: [PATCH 230/306] fix: listener api doc (#5627) --- apps/emqx_management/src/emqx_mgmt_api_listeners.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 51487fb2a..ad8ce0f67 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -111,7 +111,7 @@ api_list_listeners_on_node() -> description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"200">> => emqx_mgmt_util:object_schema(resp_schema(), <<"List listeners successfully">>)}}}, + <<"200">> => emqx_mgmt_util:schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. api_get_update_listener_by_id_on_node() -> @@ -124,7 +124,7 @@ api_get_update_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + emqx_mgmt_util:schema(resp_schema(), <<"Get listener successfully">>)}}, put => #{ description => <<"Create or update a listener by a given Id on a specific node">>, parameters => [param_path_node(), param_path_id()], @@ -134,7 +134,7 @@ api_get_update_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + emqx_mgmt_util:schema(resp_schema(), <<"Get listener successfully">>)}}, delete => #{ description => <<"Delete a listener by a given Id to all nodes in the cluster">>, parameters => [param_path_node(), param_path_id()], From b014266fa04e72b8a7b2d38d9216ce2af7b76280 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 14:23:24 +0800 Subject: [PATCH 231/306] chore(connector http): update ssl for http connector --- apps/emqx_connector/src/emqx_connector_http.erl | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 572b2a4e8..159562f33 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -58,9 +58,7 @@ fields(config) -> , {pool_type, fun pool_type/1} , {pool_size, fun pool_size/1} , {enable_pipelining, fun enable_pipelining/1} - , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), - default => #{}}} - ]; + ] ++ emqx_connector_schema_lib:ssl_fields(); fields(ssl_opts) -> [ {cacertfile, fun cacertfile/1} @@ -200,12 +198,11 @@ check_ssl_opts(Conf) -> check_ssl_opts(URLFrom, Conf) -> #{schema := Scheme} = hocon_schema:get_value(URLFrom, Conf), - SSLOpts = hocon_schema:get_value("ssl_opts", Conf), - case {Scheme, maps:size(SSLOpts)} of - {http, 0} -> true; - {http, _} -> false; - {https, 0} -> false; - {https, _} -> true + SSL= hocon_schema:get_value("ssl", Conf), + case {Scheme, maps:get(enable, SSL, false)} of + {http, false} -> true; + {https, true} -> true; + {_, _} -> false end. update_path(BasePath, {Path, Headers}) -> From 3bc92e5845b9fb550b39c6714a94b17f49adbcc8 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Thu, 2 Sep 2021 17:16:13 +0800 Subject: [PATCH 232/306] fix: mgmt listener cli (#5632) * fix: mgmt cli linteners --- apps/emqx/src/emqx_listeners.erl | 24 +++++++++++++ apps/emqx_management/src/emqx_mgmt_cli.erl | 40 ++++++++++++---------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 399bd3d08..8f0141b3a 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -26,6 +26,8 @@ , restart/0 , stop/0 , is_running/1 + , current_conns/2 + , max_conns/2 ]). -export([ start_listener/1 @@ -89,6 +91,28 @@ is_running(quic, _ListenerId, _Conf)-> %% TODO: quic support {error, no_found}. +current_conns(ID, ListenOn) -> + {Type, Name} = parse_listener_id(ID), + current_conns(Type, Name, ListenOn). + +current_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl -> + esockd:get_current_connections({listener_id(Type, Name), ListenOn}); +current_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss -> + proplists:get_value(all_connections, ranch:info(listener_id(Type, Name))); +current_conns(_, _, _) -> + {error, not_support}. + +max_conns(ID, ListenOn) -> + {Type, Name} = parse_listener_id(ID), + max_conns(Type, Name, ListenOn). + +max_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl -> + esockd:get_max_connections({listener_id(Type, Name), ListenOn}); +max_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss -> + proplists:get_value(max_connections, ranch:info(listener_id(Type, Name))); +max_conns(_, _, _) -> + {error, not_support}. + %% @doc Start all listeners. -spec(start() -> ok). start() -> diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 3d4dea31e..c7fb8a7b7 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -412,26 +412,28 @@ trace_off(Who, Name) -> %% @doc Listeners Command listeners([]) -> - lists:foreach(fun({{Protocol, ListenOn}, _Pid}) -> - 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", [Protocol]), + lists:foreach(fun({ID, Conf}) -> + {Host, Port} = maps:get(bind, Conf), + Acceptors = maps:get(acceptors, Conf), + ProxyProtocol = maps:get(proxy_protocol, Conf, undefined), + Running = maps:get(running, Conf), + CurrentConns = case emqx_listeners:current_conns(ID, {Host, Port}) of + {error, _} -> []; + CC -> [{current_conn, CC}] + end, + MaxConn = case emqx_listeners:max_conns(ID, {Host, Port}) of + {error, _} -> []; + MC -> [{max_conns, MC}] + end, + Info = [ + {listen_on, {string, format_listen_on(Port)}}, + {acceptors, Acceptors}, + {proxy_protocol, ProxyProtocol}, + {running, Running} + ] ++ CurrentConns ++ MaxConn, + emqx_ctl:print("~s~n", [ID]), 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, 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", [Protocol]), - lists:foreach(fun indent_print/1, Info) - end, ranch:info()); + end, emqx_listeners:list()); listeners(["stop", ListenerId]) -> case emqx_listeners:stop_listener(list_to_atom(ListenerId)) of From fae12051bbd5d293ad404ffbdf8d5b1c16398c17 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 2 Sep 2021 08:44:32 +0200 Subject: [PATCH 233/306] chore: github issue flow, add need-triage label --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- .github/ISSUE_TEMPLATE/support-needed.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 96d193913..3ec513a37 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -2,7 +2,7 @@ name: Bug Report about: Create a report to help us improve title: '' -labels: Support +labels: "Support, needs-triage" --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 0519e5699..1fb5f401f 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -2,7 +2,7 @@ name: Feature Request about: Suggest an idea for this project title: '' -labels: Feature +labels: "Feature, needs-triage" --- diff --git a/.github/ISSUE_TEMPLATE/support-needed.md b/.github/ISSUE_TEMPLATE/support-needed.md index 18b47bfb5..a19299c42 100644 --- a/.github/ISSUE_TEMPLATE/support-needed.md +++ b/.github/ISSUE_TEMPLATE/support-needed.md @@ -2,7 +2,7 @@ name: Support Needed about: Asking a question about usages, docs or anything you're insterested in title: '' -labels: Support +labels: "Support, needs-triage" --- From c6ed72df3e84a4e6ab8565690c849da4a1ae289e Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 16:44:31 +0800 Subject: [PATCH 234/306] chore(authz api): update swagger json Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 457 ++++++++++++++++-- apps/emqx_authz/src/emqx_authz_schema.erl | 7 +- 2 files changed, 408 insertions(+), 56 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 18a5e2b18..09f145075 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -41,7 +41,15 @@ definitions() -> ] }, Sources = #{ - oneOf => [ minirest:ref(<<"redis">>) + oneOf => [ minirest:ref(<<"http">>) + , minirest:ref(<<"mongo_single">>) + , minirest:ref(<<"mongo_rs">>) + , minirest:ref(<<"mongo_sharded">>) + , minirest:ref(<<"mysql">>) + , minirest:ref(<<"pgsql">>) + , minirest:ref(<<"redis_single">>) + , minirest:ref(<<"redis_sentinel">>) + , minirest:ref(<<"redis_cluster">>) , minirest:ref(<<"file">>) ] }, @@ -56,9 +64,309 @@ definitions() -> verify => #{type => boolean, example => false} } }, - Redis = #{ + HTTP = #{ type => object, - required => [type, enable, config, cmd], + required => [ type + , enable + , method + , headers + , request_timeout + , connect_timeout + , max_retries + , retry_interval + , pool_type + , pool_size + , enable_pipelining + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"http">>], + example => <<"http">> + }, + enable => #{ + type => boolean, + example => true + }, + url => #{ + type => string, + example => <<"https://emqx.com">> + }, + method => #{ + type => string, + enum => [<<"get">>, <<"post">>, <<"put">>], + example => <<"get">> + }, + headers => #{type => object}, + body => #{type => object}, + connect_timeout => #{type => integer}, + max_retries => #{type => integer}, + retry_interval => #{type => integer}, + pool_type => #{ + type => string, + enum => [<<"random">>, <<"hash">>], + example => <<"random">> + }, + pool_size => #{type => integer}, + enable_pipelining => #{type => boolean}, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoSingle= #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , server + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + server => #{type => string, example => <<"127.0.0.1:27017">>}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoRs= #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , servers + , replica_set_name + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"rs">>], + example => <<"rs">>}, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:27017">>}}, + replica_set_name => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoSharded = #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , servers + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"sharded">>], + example => <<"sharded">>}, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:27017">>}}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + Mysql = #{ + type => object, + required => [ type + , enable + , sql + , server + , database + , pool_size + , username + , password + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mysql">>], + example => <<"mysql">> + }, + enable => #{ + type => boolean, + example => true + }, + sql => #{type => string}, + server => #{type => string, + example => <<"127.0.0.1:3306">> + }, + database => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auto_reconnect => #{type => boolean, + example => true + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + Pgsql = #{ + type => object, + required => [ type + , enable + , sql + , server + , database + , pool_size + , username + , password + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"pgsql">>], + example => <<"pgsql">> + }, + enable => #{ + type => boolean, + example => true + }, + sql => #{type => string}, + server => #{type => string, + example => <<"127.0.0.1:5432">> + }, + database => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auto_reconnect => #{type => boolean, + example => true + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisSingle = #{ + type => object, + required => [ type + , enable + , cmd + , server + , redis_type + , pool_size + , auto_reconnect + , ssl + ], properties => #{ type => #{ type => string, @@ -69,59 +377,94 @@ definitions() -> type => boolean, example => true }, - config => #{ - oneOf => [ #{type => object, - required => [server, redis_type, pool_size, auto_reconnect], - properties => #{ - server => #{type => string, example => <<"127.0.0.1:3306">>}, - redis_type => #{type => string, - enum => [<<"single">>], - example => <<"single">>}, - pool_size => #{type => integer}, - auto_reconnect => #{type => boolean, example => true}, - password => #{type => string}, - database => #{type => integer}, - ssl => minirest:ref(<<"ssl">>) - } - } - , #{type => object, - required => [servers, redis_type, sentinel, pool_size, auto_reconnect], - properties => #{ - servers => #{type => array, - items => #{type => string,example => <<"127.0.0.1:3306">>}}, - redis_type => #{type => string, - enum => [<<"sentinel">>], - example => <<"sentinel">>}, - sentinel => #{type => string}, - pool_size => #{type => integer}, - auto_reconnect => #{type => boolean, example => true}, - password => #{type => string}, - database => #{type => integer}, - ssl => minirest:ref(<<"ssl">>) - } - } - , #{type => object, - required => [servers, redis_type, pool_size, auto_reconnect], - properties => #{ - servers => #{type => array, - items => #{type => string, example => <<"127.0.0.1:3306">>}}, - redis_type => #{type => string, - enum => [<<"cluster">>], - example => <<"cluster">>}, - pool_size => #{type => integer}, - auto_reconnect => #{type => boolean, example => true}, - password => #{type => string}, - database => #{type => integer}, - ssl => minirest:ref(<<"ssl">>) - } - } - ], - type => object + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + server => #{type => string, example => <<"127.0.0.1:3306">>}, + redis_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisSentinel= #{ + type => object, + required => [ type + , enable + , cmd + , servers + , redis_type + , sentinel + , pool_size + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true }, cmd => #{ type => string, example => <<"HGETALL mqtt_authz">> - } + }, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"sentinel">>], + example => <<"sentinel">>}, + sentinel => #{type => string}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisCluster= #{ + type => object, + required => [ type + , enable + , cmd + , servers + , redis_type + , pool_size + , auto_reconnect + , ssl], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + servers => #{type => array, + items => #{type => string, example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"cluster">>], + example => <<"cluster">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } }, File = #{ @@ -153,6 +496,14 @@ definitions() -> [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} , #{<<"ssl">> => SSL} - , #{<<"redis">> => Redis} + , #{<<"http">> => HTTP} + , #{<<"mongo_single">> => MongoSingle} + , #{<<"mongo_rs">> => MongoRs} + , #{<<"mongo_sharded">> => MongoSharded} + , #{<<"mysql">> => Mysql} + , #{<<"pgsql">> => Pgsql} + , #{<<"redis_single">> => RedisSingle} + , #{<<"redis_sentinel">> => RedisSentinel} + , #{<<"redis_cluster">> => RedisCluster} , #{<<"file">> => File} ]. diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 4d8fa3579..251e40fe6 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -52,6 +52,7 @@ fields(http_get) -> , {enable, #{type => boolean(), default => true}} , {url, #{type => url()}} + , {method, #{type => get, default => get }} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -71,7 +72,6 @@ fields(http_get) -> end } } - , {method, #{type => get, default => get }} , {request_timeout, #{type => timeout(), default => 30000 }} ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); fields(http_post) -> @@ -79,6 +79,8 @@ fields(http_post) -> , {enable, #{type => boolean(), default => true}} , {url, #{type => url()}} + , {method, #{type => hoconsc:enum([post, put]), + default => get}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -100,8 +102,7 @@ fields(http_post) -> end } } - , {method, #{type => hoconsc:enum([post, put]), - default => get}} + , {request_timeout, #{type => timeout(), default => 30000 }} , {body, #{type => map(), nullable => true } From cd43bb42a7266f7a7b6d293dbe44c1277423a9e4 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 2 Sep 2021 18:37:29 +0200 Subject: [PATCH 235/306] fix(helm-chart): force headless svc ready while pod is not ready fixs: #5254 The dist port behind headless svc should to be accessible during emqx cluster boot. Endpoints of headless SVC is not in 'ready' state that prevents nodes to talk to each other, this issue only happens when K8s host node is restarted and all emqx nodes are deployed on the same host. --- deploy/charts/emqx/templates/service.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 3e9f06b52..54efa6426 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -112,6 +112,7 @@ spec: type: ClusterIP sessionAffinity: None clusterIP: None + publishNotReadyAddresses: true ports: - name: mqtt port: {{ .Values.service.mqtt | default 1883 }} From cfe64d9c6ff873a6cc7919ca141508f9026e4b60 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 3 Sep 2021 09:44:10 +0800 Subject: [PATCH 236/306] fix(gw): not packing udp packages --- .../src/bhvrs/emqx_gateway_conn.erl | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index fa0a830e5..9797a6a4a 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -226,6 +226,9 @@ esockd_send(Data, #state{socket = {udp, _SockPid, Sock}, esockd_send(Data, #state{socket = {esockd_transport, Sock}}) -> esockd_transport:async_send(Sock, Data). +is_datadram_socket({esockd_transport, _}) -> false; +is_datadram_socket({udp, _, _}) -> true. + %%-------------------------------------------------------------------- %% callbacks %%-------------------------------------------------------------------- @@ -678,8 +681,20 @@ with_channel(Fun, Args, State = #state{ %%-------------------------------------------------------------------- %% Handle outgoing packets -handle_outgoing(Packets, State) when is_list(Packets) -> - send(lists:map(serialize_and_inc_stats_fun(State), Packets), State); +handle_outgoing(_Packets = [], _State) -> + ok; +handle_outgoing(Packets, + State = #state{socket = Socket}) when is_list(Packets) -> + case is_datadram_socket(Socket) of + false -> + send( + lists:map(serialize_and_inc_stats_fun(State), Packets), + State); + _ -> + lists:foreach(fun(Packet) -> + handle_outgoing(Packet, State) + end, Packets) + end; handle_outgoing(Packet, State) -> send((serialize_and_inc_stats_fun(State))(Packet), State). From 3f0ef7efa8b037c403433a622186f27135e783c1 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 09:31:10 +0800 Subject: [PATCH 237/306] feat(gw): add api_clients swagger defination --- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 24 +- .../src/emqx_gateway_api_client.erl | 280 +++++++++++++++++- .../src/exproto/emqx_exproto_impl.erl | 8 +- .../src/lwm2m/emqx_lwm2m_impl.erl | 8 +- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 8 +- .../src/stomp/emqx_stomp_impl.erl | 8 +- 7 files changed, 316 insertions(+), 28 deletions(-) diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index da2f2b8e9..b0714f5e9 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -89,11 +89,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -121,10 +121,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index f38624ed9..b9ae0a234 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -348,9 +348,9 @@ gateway_insta(delete, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), case emqx_gateway:unload(Name) of ok -> - {200, ok}; + {200}; {error, not_found} -> - {404, <<"Not Found">>} + return_http_error(404, <<"Gateway not found">>) end; gateway_insta(get, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), @@ -363,7 +363,7 @@ gateway_insta(get, #{bindings := #{name := Name0}}) -> ), {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; undefined -> - {404, <<"Not Found">>} + return_http_error(404, <<"Gateway not found">>) end; gateway_insta(put, #{body := RawConfsIn, bindings := #{name := Name} @@ -371,12 +371,22 @@ gateway_insta(put, #{body := RawConfsIn, %% FIXME: Cluster Consistence ?? case emqx_gateway:update_rawconf(Name, RawConfsIn) of ok -> - {200, <<"Changed">>}; + {200}; {error, not_found} -> - {404, <<"Not Found">>}; + return_http_error(404, <<"Gateway not found">>); {error, Reason} -> - {500, emqx_gateway_utils:stringfy(Reason)} + return_http_error(500, Reason) end. gateway_insta_stats(get, _Req) -> - {401, <<"Implement it later (maybe 5.1)">>}. + return_http_error(401, <<"Implement it later (maybe 5.1)">>). + +return_http_error(Code, Msg) -> + emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }). + +codestr(404) -> 'RESOURCE_NOT_FOUND'; +codestr(401) -> 'NOT_SUPPORTED_NOW'; +codestr(500) -> 'UNKNOW_ERROR'. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_client.erl index 03fb056ad..0a94cd906 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_client.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_client.erl @@ -21,5 +21,283 @@ %% minirest behaviour callbacks -export([api_spec/0]). +-export([ clients/2 + , clients_insta/2 + , subscriptions/2 + ]). + api_spec() -> - {[], []}. + {metadata(apis()), []}. + +apis() -> + [ {"/gateway/:name/clients", clients} + , {"/gateway/:name/clients/:clientid", clients_insta} + , {"/gateway/:name/clients/:clientid/subscriptions", subscriptions} + , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} + ]. + +clients(get, _Req) -> + {200, []}. + +clients_insta(get, _Req) -> + {200, <<"{}">>}; +clients_insta(delete, _Req) -> + {200}. + +subscriptions(get, _Req) -> + {200, []}; +subscriptions(delete, _Req) -> + {200}. + +%%-------------------------------------------------------------------- +%% Swagger defines +%%-------------------------------------------------------------------- + +metadata(APIs) -> + metadata(APIs, []). +metadata([], APIAcc) -> + lists:reverse(APIAcc); +metadata([{Path, Fun}|More], APIAcc) -> + Methods = [get, post, put, delete, patch], + Mds = lists:foldl(fun(M, Acc) -> + try + Acc#{M => swagger(Path, M)} + catch + error : function_clause -> + Acc + end + end, #{}, Methods), + metadata(More, [{Path, Mds, Fun} | APIAcc]). + +swagger("/gateway/:name/clients", get) -> + #{ description => <<"Get the gateway clients">> + , parameters => params_client_query() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_clients_list() + } + }; +swagger("/gateway/:name/clients/:clientid", get) -> + #{ description => <<"Get the gateway client infomation">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_client() + } + }; +swagger("/gateway/:name/clients/:clientid", delete) -> + #{ description => <<"Kick out the gateway client">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions", get) -> + #{ description => <<"Get the gateway client subscriptions">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_subscription_list() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions", post) -> + #{ description => <<"Get the gateway client subscriptions">> + , parameters => params_client_insta() + , requestBody => schema_subscription() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_no_content() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> + #{ description => <<"Unsubscribe the topic for client">> + , parameters => params_client_insta() ++ params_topic_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }. + +params_client_query() -> + params_client_searching_in_qs() + ++ emqx_mgmt_util:page_params() + ++ params_gateway_name_in_path(). + +params_client_insta() -> + params_gateway_name_in_path() + ++ params_clientid_in_path(). + +params_client_searching_in_qs() -> + queries( + [ {node, string} + , {clientid, string} + , {username, string} + , {ip_address, string} + , {conn_state, string} + , {clean_start, boolean} + , {like_clientid, string} + , {like_username, string} + , {gte_created_at, string} + , {lte_created_at, string} + , {gte_connected_at, string} + , {lte_connected_at, string} + ]). + +params_gateway_name_in_path() -> + [#{ name => name + , in => path + , schema => #{type => string} + , required => true + }]. + +params_clientid_in_path() -> + [#{ name => clientid + , in => path + , schema => #{type => string} + , required => true + }]. + +params_topic_name_in_path() -> + [#{ name => topic + , in => path + , schema => #{type => string} + , required => true + }]. + +queries(Ls) -> + lists:map(fun({K, Type}) -> + #{name => K, in => query, + schema => #{type => Type}, + required => false + } + end, Ls). + +%%-------------------------------------------------------------------- +%% Schemas + +schema_not_found() -> + emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). + +schema_no_content() -> + #{description => <<"No Content">>}. + +schema_clients_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_client() + }, + <<"Client lists">> + ). + +schema_client() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => properties_client() + }). + +schema_subscription_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_subscription() + }, + <<"Client subscriptions">> + ). + +schema_subscription() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => properties_subscription() + } + ). + +%%-------------------------------------------------------------------- +%% Object properties def + +properties_client() -> + emqx_mgmt_util:properties( + [ {node, string, + <<"Name of the node to which the client is connected">>} + , {clientid, string, + <<"Client identifier">>} + , {username, string, + <<"Username of client when connecting">>} + , {proto_name, string, + <<"Client protocol name">>} + , {proto_ver, string, + <<"Protocol version used by the client">>} + , {ip_address, string, + <<"Client's IP address">>} + , {is_bridge, boolean, + <<"Indicates whether the client is connectedvia bridge">>} + , {connected_at, string, + <<"Client connection time">>} + , {disconnected_at, string, + <<"Client offline time, This field is only valid and returned " + "when connected is false">>} + , {connected, boolean, + <<"Whether the client is connected">>} + %, {will_msg, string, + % <<"Client will message">>} + %, {zone, string, + % <<"Indicate the configuration group used by the client">>} + , {keepalive, integer, + <<"keepalive time, with the unit of second">>} + , {clean_start, boolean, + <<"Indicate whether the client is using a brand new session">>} + , {expiry_interval, integer, + <<"Session expiration interval, with the unit of second">>} + , {created_at, string, + <<"Session creation time">>} + , {subscriptions_cnt, integer, + <<"Number of subscriptions established by this client">>} + , {subscriptions_max, integer, + <<"v4 api name [max_subscriptions] Maximum number of " + "subscriptions allowed by this client">>} + , {inflight_cnt, integer, + <<"Current length of inflight">>} + , {inflight_max, integer, + <<"v4 api name [max_inflight]. Maximum length of inflight">>} + , {mqueue_len, integer, + <<"Current length of message queue">>} + , {mqueue_max, integer, + <<"v4 api name [max_mqueue]. Maximum length of message queue">>} + , {mqueue_dropped, integer, + <<"Number of messages dropped by the message queue due to " + "exceeding the length">>} + , {awaiting_rel_cnt, integer, + <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>} + , {awaiting_rel_max, integer, + <<"v4 api name [max_awaiting_rel]. Maximum allowed number of " + "awaiting PUBREC packet">>} + , {recv_oct, integer, + <<"Number of bytes received by EMQ X Broker (the same below)">>} + , {recv_cnt, integer, + <<"Number of TCP packets received">>} + , {recv_pkt, integer, + <<"Number of MQTT packets received">>} + , {recv_msg, integer, + <<"Number of PUBLISH packets received">>} + , {send_oct, integer, + <<"Number of bytes sent">>} + , {send_cnt, integer, + <<"Number of TCP packets sent">>} + , {send_pkt, integer, + <<"Number of MQTT packets sent">>} + , {send_msg, integer, + <<"Number of PUBLISH packets sent">>} + , {mailbox_len, integer, + <<"Process mailbox size">>} + , {heap_size, integer, + <<"Process heap size with the unit of byte">>} + , {reductions, integer, + <<"Erlang reduction">>} + ]). + +properties_subscription() -> + emqx_mgmt_util:properties( + [ {topic, string, + <<"Topic Fillter">>} + , {qos, integer, + <<"QoS level">>} + ]). diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 8131f2d0c..3b62ecd20 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -143,11 +143,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -200,10 +200,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index 0a96e98e1..e6720905b 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -90,11 +90,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -133,10 +133,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 039b23924..f510afdf9 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -108,11 +108,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -144,10 +144,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 593b71289..2175b767b 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -93,11 +93,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -129,10 +129,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. From 0a7a14f4cd05f2917f7c86c355ead4b63146945b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 12:13:24 +0800 Subject: [PATCH 238/306] chore(mgmt): callback query function with table name param --- apps/emqx_management/src/emqx_mgmt_api.erl | 31 +-- .../src/emqx_mgmt_api_alarms.erl | 23 +- .../src/emqx_mgmt_api_clients.erl | 244 +++++++++--------- .../src/emqx_mgmt_api_subscriptions.erl | 17 +- 4 files changed, 162 insertions(+), 153 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index e9aaa2725..5a7b020d7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -22,13 +22,13 @@ %% first_next query APIs -export([ params2qs/2 - , node_query/4 - , cluster_query/3 + , node_query/5 + , cluster_query/4 , traverse_table/5 , select_table/5 ]). --export([do_query/5]). +-export([do_query/6]). paginate(Tables, Params, RowFun) -> Qh = query_handle(Tables), @@ -78,14 +78,14 @@ limit(Params) -> %% Node Query %%-------------------------------------------------------------------- -node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> +node_query(Node, Params, Tab, QsSchema, QueryFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), Limit = b2i(limit(Params)), Page = b2i(page(Params)), Start = if Page > 1 -> (Page-1) * Limit; true -> 0 end, - {_, Rows} = do_query(Node, Qs, QueryFun, Start, Limit+1), + {_, Rows} = do_query(Node, Tab, Qs, QueryFun, Start, Limit+1), Meta = #{page => Page, limit => Limit}, NMeta = case CodCnt =:= 0 of true -> Meta#{count => count(Tab)}; @@ -94,10 +94,11 @@ node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> #{meta => NMeta, data => lists:sublist(Rows, Limit)}. %% @private -do_query(Node, Qs, {M,F}, Start, Limit) when Node =:= node() -> - M:F(Qs, Start, Limit); -do_query(Node, Qs, QueryFun, Start, Limit) -> - rpc_call(Node, ?MODULE, do_query, [Node, Qs, QueryFun, Start, Limit], 50000). +do_query(Node, Tab, Qs, {M,F}, Start, Limit) when Node =:= node() -> + M:F(Tab, Qs, Start, Limit); +do_query(Node, Tab, Qs, QueryFun, Start, Limit) -> + rpc_call(Node, ?MODULE, do_query, + [Node, Tab, Qs, QueryFun, Start, Limit], 50000). %% @private rpc_call(Node, M, F, A, T) -> @@ -110,7 +111,7 @@ rpc_call(Node, M, F, A, T) -> %% Cluster Query %%-------------------------------------------------------------------- -cluster_query(Params, {Tab, QsSchema}, QueryFun) -> +cluster_query(Params, Tab, QsSchema, QueryFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), Limit = b2i(limit(Params)), Page = b2i(page(Params)), @@ -118,7 +119,7 @@ cluster_query(Params, {Tab, QsSchema}, QueryFun) -> true -> 0 end, Nodes = ekka_mnesia:running_nodes(), - Rows = do_cluster_query(Nodes, Qs, QueryFun, Start, Limit+1, []), + Rows = do_cluster_query(Nodes, Tab, Qs, QueryFun, Start, Limit+1, []), Meta = #{page => Page, limit => Limit}, NMeta = case CodCnt =:= 0 of true -> Meta#{count => count(Tab, Nodes)}; @@ -127,13 +128,13 @@ cluster_query(Params, {Tab, QsSchema}, QueryFun) -> #{meta => NMeta, data => lists:sublist(Rows, Limit)}. %% @private -do_cluster_query([], _, _, _, _, Acc) -> +do_cluster_query([], _, _, _, _, _, Acc) -> lists:append(lists:reverse(Acc)); -do_cluster_query([Node|Nodes], Qs, QueryFun, Start, Limit, Acc) -> - {NStart, Rows} = do_query(Node, Qs, QueryFun, Start, Limit), +do_cluster_query([Node|Nodes], Tab, Qs, QueryFun, Start, Limit, Acc) -> + {NStart, Rows} = do_query(Node, Tab, Qs, QueryFun, Start, Limit), case Limit - length(Rows) of Rest when Rest > 0 -> - do_cluster_query(Nodes, Qs, QueryFun, NStart, Limit, [Rows|Acc]); + do_cluster_query(Nodes, Tab, Qs, QueryFun, NStart, Limit, [Rows|Acc]); 0 -> lists:append(lists:reverse([Rows|Acc])) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 1adb5fce3..db6484060 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -22,8 +22,10 @@ -export([alarms/2]). --export([ query_activated/3 - , query_deactivated/3]). +%% internal export (for query) +-export([ query/4 + ]). + %% notice: from emqx_alarms -define(ACTIVATED_ALARM, emqx_activated_alarm). -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). @@ -71,14 +73,12 @@ alarms_api() -> %%%============================================================================================== %% parameters trans alarms(get, #{query_string := Qs}) -> - {Table, Function} = + Table = case maps:get(<<"activated">>, Qs, <<"true">>) of - <<"true">> -> - {?ACTIVATED_ALARM, query_activated}; - <<"false">> -> - {?DEACTIVATED_ALARM, query_deactivated} + <<"true">> -> ?ACTIVATED_ALARM; + <<"false">> -> ?DEACTIVATED_ALARM end, - Response = emqx_mgmt_api:cluster_query(Qs, {Table, []}, {?MODULE, Function}), + Response = emqx_mgmt_api:cluster_query(Qs, Table, [], {?MODULE, query}), {200, Response}; alarms(delete, _Params) -> @@ -87,13 +87,8 @@ alarms(delete, _Params) -> %%%============================================================================================== %% internal -query_activated(_, Start, Limit) -> - query(?ACTIVATED_ALARM, Start, Limit). -query_deactivated(_, Start, Limit) -> - query(?DEACTIVATED_ALARM, Start, Limit). - -query(Table, Start, Limit) -> +query(Table, _QsSpec, Start, Limit) -> Ms = [{'$1',[],['$1']}], emqx_mgmt_api:select_table(Table, Ms, Start, Limit, fun format_alarm/1). diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index a4e307114..dd4df58a5 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -35,7 +35,7 @@ , unsubscribe/2 , subscribe_batch/2]). --export([ query/3 +-export([ query/4 , format_channel_info/1]). %% for batch operation @@ -420,14 +420,17 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) -> %% api apply list(Params) -> + {Tab, QuerySchema} = ?CLIENT_QS_SCHEMA, case maps:get(<<"node">>, Params, undefined) of undefined -> - Response = emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun), + Response = emqx_mgmt_api:cluster_query(Params, Tab, + QuerySchema, ?query_fun), {200, Response}; Node1 -> Node = binary_to_atom(Node1, utf8), ParamsWithoutNode = maps:without([<<"node">>], Params), - Response = emqx_mgmt_api:node_query(Node, ParamsWithoutNode, ?CLIENT_QS_SCHEMA, ?query_fun), + Response = emqx_mgmt_api:node_query(Node, ParamsWithoutNode, + Tab, QuerySchema, ?query_fun), {200, Response} end. @@ -492,8 +495,123 @@ subscribe_batch(#{clientid := ClientID, topics := Topics}) -> ArgList = [[ClientID, Topic, Qos]|| #{topic := Topic, qos := Qos} <- Topics], emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList). -%%%============================================================================================== +%%-------------------------------------------------------------------- %% internal function + +do_subscribe(ClientID, Topic0, Qos) -> + {Topic, Opts} = emqx_topic:parse(Topic0), + TopicTable = [{Topic, Opts#{qos => Qos}}], + 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. + +do_unsubscribe(ClientID, Topic) -> + case emqx_mgmt:unsubscribe(ClientID, Topic) of + {error, Reason} -> + {error, Reason}; + Res -> + Res + end. +%%-------------------------------------------------------------------- +%% Query Functions + +query(Tab, {Qs, []}, Start, Limit) -> + Ms = qs2ms(Qs), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, + fun format_channel_info/1); + +query(Tab, {Qs, Fuzzy}, Start, Limit) -> + Ms = qs2ms(Qs), + MatchFun = match_fun(Ms, Fuzzy), + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, + fun format_channel_info/1). + +%%-------------------------------------------------------------------- +%% QueryString to Match Spec + +-spec qs2ms(list()) -> ets:match_spec(). +qs2ms(Qs) -> + {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), + [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. + +qs2ms([], _, {MtchHead, Conds}) -> + {MtchHead, lists:reverse(Conds)}; + +qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), + qs2ms(Rest, N, {NMtchHead, Conds}); +qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> + Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), + NConds = put_conds(Qs, Holder, Conds), + qs2ms(Rest, N+1, {NMtchHead, NConds}). + +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]. + +ms(clientid, X) -> + #{clientinfo => #{clientid => X}}; +ms(username, X) -> + #{clientinfo => #{username => X}}; +ms(zone, X) -> + #{clientinfo => #{zone => X}}; +ms(ip_address, X) -> + #{clientinfo => #{peerhost => X}}; +ms(conn_state, X) -> + #{conn_state => X}; +ms(clean_start, X) -> + #{conninfo => #{clean_start => X}}; +ms(proto_name, X) -> + #{conninfo => #{proto_name => X}}; +ms(proto_ver, X) -> + #{conninfo => #{proto_ver => X}}; +ms(connected_at, X) -> + #{conninfo => #{connected_at => X}}; +ms(created_at, X) -> + #{session => #{created_at => X}}. + +%%-------------------------------------------------------------------- +%% 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). + +%%-------------------------------------------------------------------- +%% format funcs + format_channel_info({_, ClientInfo, ClientStats}) -> Fun = fun @@ -546,116 +664,8 @@ peer_to_binary(Addr) -> list_to_binary(inet:ntoa(Addr)). format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) -> - #{ - access => PubSub, - topic => Topic, - result => AuthzResult, - updated_time => Timestamp - }. - -do_subscribe(ClientID, Topic0, Qos) -> - {Topic, Opts} = emqx_topic:parse(Topic0), - TopicTable = [{Topic, Opts#{qos => Qos}}], - 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. - -do_unsubscribe(ClientID, Topic) -> - case emqx_mgmt:unsubscribe(ClientID, Topic) of - {error, Reason} -> - {error, Reason}; - Res -> - Res - end. -%%%============================================================================================== -%% Query Functions - -query({Qs, []}, Start, Limit) -> - Ms = qs2ms(Qs), - emqx_mgmt_api:select_table(emqx_channel_info, Ms, Start, Limit, fun format_channel_info/1); - -query({Qs, Fuzzy}, Start, Limit) -> - Ms = qs2ms(Qs), - MatchFun = match_fun(Ms, Fuzzy), - emqx_mgmt_api:traverse_table(emqx_channel_info, MatchFun, Start, Limit, fun format_channel_info/1). - -%%%============================================================================================== -%% QueryString to Match Spec --spec qs2ms(list()) -> ets:match_spec(). -qs2ms(Qs) -> - {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), - [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. - -qs2ms([], _, {MtchHead, Conds}) -> - {MtchHead, lists:reverse(Conds)}; - -qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> - NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), - qs2ms(Rest, N, {NMtchHead, Conds}); -qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> - Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), - NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), - NConds = put_conds(Qs, Holder, Conds), - qs2ms(Rest, N+1, {NMtchHead, NConds}). - -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]. - -ms(clientid, X) -> - #{clientinfo => #{clientid => X}}; -ms(username, X) -> - #{clientinfo => #{username => X}}; -ms(zone, X) -> - #{clientinfo => #{zone => X}}; -ms(ip_address, X) -> - #{clientinfo => #{peerhost => X}}; -ms(conn_state, X) -> - #{conn_state => X}; -ms(clean_start, X) -> - #{conninfo => #{clean_start => X}}; -ms(proto_name, X) -> - #{conninfo => #{proto_name => X}}; -ms(proto_ver, X) -> - #{conninfo => #{proto_ver => X}}; -ms(connected_at, X) -> - #{conninfo => #{connected_at => X}}; -ms(created_at, X) -> - #{session => #{created_at => X}}. - -%%%============================================================================================== -%% 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). + #{ access => PubSub, + topic => Topic, + result => AuthzResult, + updated_time => Timestamp + }. diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 058d824ac..5c2475e95 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -29,7 +29,7 @@ -export([subscriptions/2]). --export([ query/3 +-export([ query/4 , format/1 ]). @@ -111,11 +111,14 @@ subscriptions(get, #{query_string := Params}) -> list(Params). list(Params) -> + {Tab, QuerySchema} = ?SUBS_QS_SCHEMA, case maps:get(<<"node">>, Params, undefined) of undefined -> - {200, emqx_mgmt_api:cluster_query(Params, ?SUBS_QS_SCHEMA, ?query_fun)}; + {200, emqx_mgmt_api:cluster_query(Params, Tab, + QuerySchema, ?query_fun)}; Node -> - {200, emqx_mgmt_api:node_query(binary_to_atom(Node, utf8), Params, ?SUBS_QS_SCHEMA, ?query_fun)} + {200, emqx_mgmt_api:node_query(binary_to_atom(Node, utf8), Params, + Tab, QuerySchema, ?query_fun)} end. format(Items) when is_list(Items) -> @@ -145,14 +148,14 @@ format({_Subscriber, Topic, Options}) -> %% Query Function %%-------------------------------------------------------------------- -query({Qs, []}, Start, Limit) -> +query(Tab, {Qs, []}, Start, Limit) -> Ms = qs2ms(Qs), - emqx_mgmt_api:select_table(emqx_suboption, Ms, Start, Limit, fun format/1); + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, fun format/1); -query({Qs, Fuzzy}, Start, Limit) -> +query(Tab, {Qs, Fuzzy}, Start, Limit) -> Ms = qs2ms(Qs), MatchFun = match_fun(Ms, Fuzzy), - emqx_mgmt_api:traverse_table(emqx_suboption, MatchFun, Start, Limit, fun format/1). + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, fun format/1). match_fun(Ms, Fuzzy) -> MsC = ets:match_spec_compile(Ms), From 52b6d620eeeabd1cb9e9c2e1139909cd4dee75d9 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 10:57:11 +0800 Subject: [PATCH 239/306] feat(gw): implement clients list http-api --- .../src/emqx_gateway_api_client.erl | 211 +++++++++++++++++- 1 file changed, 200 insertions(+), 11 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_client.erl index 0a94cd906..d2c3b466c 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_client.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_client.erl @@ -21,11 +21,21 @@ %% minirest behaviour callbacks -export([api_spec/0]). +%% http handlers -export([ clients/2 , clients_insta/2 , subscriptions/2 ]). +%% internal exports (for client query) +-export([ query/4 + , format_channel_info/1 + ]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + api_spec() -> {metadata(apis()), []}. @@ -36,8 +46,49 @@ apis() -> , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} ]. -clients(get, _Req) -> - {200, []}. + +-define(CLIENT_QS_SCHEMA, + [ {<<"node">>, atom} + , {<<"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}). + +clients(get, #{ bindings := #{name := GwName0} + , query_string := Qs + }) -> + GwName = binary_to_existing_atom(GwName0), + TabName = emqx_gateway_cm:tabname(info, GwName), + case maps:get(<<"node">>, Qs, undefined) of + undefined -> + Response = emqx_mgmt_api:cluster_query( + Qs, TabName, + ?CLIENT_QS_SCHEMA, ?query_fun + ), + {200, Response}; + Node1 -> + Node = binary_to_atom(Node1, utf8), + ParamsWithoutNode = maps:without([<<"node">>], Qs), + Response = emqx_mgmt_api:node_query( + Node, ParamsWithoutNode, + TabName, ?CLIENT_QS_SCHEMA, ?query_fun + ), + {200, Response} + end. clients_insta(get, _Req) -> {200, <<"{}">>}; @@ -49,6 +100,145 @@ subscriptions(get, _Req) -> subscriptions(delete, _Req) -> {200}. +%%-------------------------------------------------------------------- +%% query funcs + +query(Tab, {Qs, []}, Start, Limit) -> + Ms = qs2ms(Qs), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, + fun format_channel_info/1); + +query(Tab, {Qs, Fuzzy}, Start, Limit) -> + Ms = qs2ms(Qs), + MatchFun = match_fun(Ms, Fuzzy), + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, + fun format_channel_info/1). + +qs2ms(Qs) -> + {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), + [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. + +qs2ms([], _, {MtchHead, Conds}) -> + {MtchHead, lists:reverse(Conds)}; + +qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), + qs2ms(Rest, N, {NMtchHead, Conds}); +qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> + Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), + NConds = put_conds(Qs, Holder, Conds), + qs2ms(Rest, N+1, {NMtchHead, NConds}). + +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]. + +ms(clientid, X) -> + #{clientinfo => #{clientid => X}}; +ms(username, X) -> + #{clientinfo => #{username => X}}; +ms(zone, X) -> + #{clientinfo => #{zone => X}}; +ms(ip_address, X) -> + #{clientinfo => #{peerhost => X}}; +ms(conn_state, X) -> + #{conn_state => X}; +ms(clean_start, X) -> + #{conninfo => #{clean_start => X}}; +ms(proto_name, X) -> + #{conninfo => #{proto_name => X}}; +ms(proto_ver, X) -> + #{conninfo => #{proto_ver => X}}; +ms(connected_at, X) -> + #{conninfo => #{connected_at => X}}; +ms(created_at, X) -> + #{session => #{created_at => X}}. + +%%-------------------------------------------------------------------- +%% 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). + +%%-------------------------------------------------------------------- +%% format funcs + +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 + , subscriptions + , 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). + +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)). + %%-------------------------------------------------------------------- %% Swagger defines %%-------------------------------------------------------------------- @@ -112,7 +302,7 @@ swagger("/gateway/:name/clients/:clientid/subscriptions", post) -> }; swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> #{ description => <<"Unsubscribe the topic for client">> - , parameters => params_client_insta() ++ params_topic_name_in_path() + , parameters => params_topic_name_in_path() ++ params_client_insta() , responses => #{ <<"404">> => schema_not_found() , <<"204">> => schema_no_content() @@ -120,13 +310,13 @@ swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> }. params_client_query() -> - params_client_searching_in_qs() - ++ emqx_mgmt_util:page_params() - ++ params_gateway_name_in_path(). + params_gateway_name_in_path() + ++ params_client_searching_in_qs() + ++ emqx_mgmt_util:page_params(). params_client_insta() -> - params_gateway_name_in_path() - ++ params_clientid_in_path(). + params_clientid_in_path() + ++ params_gateway_name_in_path(). params_client_searching_in_qs() -> queries( @@ -183,11 +373,10 @@ schema_no_content() -> #{description => <<"No Content">>}. schema_clients_list() -> - emqx_mgmt_util:array_schema( + emqx_mgmt_util:page_schema( #{ type => object , properties => properties_client() - }, - <<"Client lists">> + } ). schema_client() -> From 14b39224d45083d4ddb1b1997e938ca66763354e Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 15:35:53 +0800 Subject: [PATCH 240/306] chore(gw): clients http implement skeleton --- apps/emqx_gateway/src/emqx_gateway.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 21 +- ...lient.erl => emqx_gateway_api_clients.erl} | 212 +++++++++++++----- apps/emqx_gateway/src/emqx_gateway_cli.erl | 3 +- ...gateway_intr.erl => emqx_gateway_http.erl} | 74 +++++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 4 + 6 files changed, 245 insertions(+), 77 deletions(-) rename apps/emqx_gateway/src/{emqx_gateway_api_client.erl => emqx_gateway_api_clients.erl} (69%) rename apps/emqx_gateway/src/{emqx_gateway_intr.erl => emqx_gateway_http.erl} (56%) diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 79ea5d8a4..596b47547 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -25,7 +25,7 @@ , post_config_update/4 ]). -%% APIs +%% Gateway APIs -export([ registered_gateway/0 , load/2 , unload/1 @@ -48,7 +48,7 @@ registered_gateway() -> emqx_gateway_registry:list(). %%-------------------------------------------------------------------- -%% Gateway Instace APIs +%% Gateway APIs -spec list() -> [gateway()]. list() -> @@ -96,7 +96,8 @@ update_rawconf(RawName, RawConfDiff) -> %%-------------------------------------------------------------------- %% Config Handler --spec pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> +-spec pre_config_update(emqx_config:update_request(), + emqx_config:raw_config()) -> {ok, emqx_config:update_request()} | {error, term()}. pre_config_update({RawName, RawConfDiff}, RawConf) -> {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. @@ -117,4 +118,3 @@ post_config_update({RawName, _}, NewConfig, OldConfig, _AppEnvs) -> %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- - diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index b9ae0a234..2ff002fd5 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -20,8 +20,13 @@ -compile(nowarn_unused_function). --import(emqx_mgmt_util, [ schema/1 - ]). +-import(emqx_mgmt_util, + [ schema/1 + ]). + +-import(emqx_gateway_http, + [ return_http_error/2 + ]). %% minirest behaviour callbacks -export([api_spec/0]). @@ -342,7 +347,7 @@ gateway(get, Request) -> undefined -> all; S0 -> binary_to_existing_atom(S0, utf8) end, - {200, emqx_gateway_intr:gateways(Status)}. + {200, emqx_gateway_http:gateways(Status)}. gateway_insta(delete, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), @@ -380,13 +385,3 @@ gateway_insta(put, #{body := RawConfsIn, gateway_insta_stats(get, _Req) -> return_http_error(401, <<"Implement it later (maybe 5.1)">>). - -return_http_error(Code, Msg) -> - emqx_json:encode( - #{code => codestr(Code), - reason => emqx_gateway_utils:stringfy(Msg) - }). - -codestr(404) -> 'RESOURCE_NOT_FOUND'; -codestr(401) -> 'NOT_SUPPORTED_NOW'; -codestr(500) -> 'UNKNOW_ERROR'. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl similarity index 69% rename from apps/emqx_gateway/src/emqx_gateway_api_client.erl rename to apps/emqx_gateway/src/emqx_gateway_api_clients.erl index d2c3b466c..ba61a38fc 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_client.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -13,8 +13,8 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- -%% --module(emqx_gateway_api_client). + +-module(emqx_gateway_api_clients). -behaviour(minirest_api). @@ -32,6 +32,10 @@ , format_channel_info/1 ]). +-import(emqx_gateway_http, + [ return_http_error/2 + ]). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -46,17 +50,14 @@ apis() -> , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} ]. - -define(CLIENT_QS_SCHEMA, [ {<<"node">>, atom} , {<<"clientid">>, binary} , {<<"username">>, binary} - %%, {<<"zone">>, atom} , {<<"ip_address">>, ip} , {<<"conn_state">>, atom} , {<<"clean_start">>, atom} - %%, {<<"proto_name">>, binary} - %%, {<<"proto_ver">>, integer} + , {<<"proto_ver">>, integer} , {<<"like_clientid">>, binary} , {<<"like_username">>, binary} , {<<"gte_created_at">>, timestamp} @@ -90,14 +91,69 @@ clients(get, #{ bindings := #{name := GwName0} {200, Response} end. -clients_insta(get, _Req) -> - {200, <<"{}">>}; -clients_insta(delete, _Req) -> +clients_insta(get, #{ bindings := #{name := GwName0, + clientid := ClientId} + }) -> + GwName = binary_to_existing_atom(GwName0), + TabName = emqx_gateway_cm:tabname(info, GwName), + %% XXX: We need a lookuo function for it instead of a query + #{data := Data} = emqx_mgmt_api:cluster_query( + #{<<"clientid">> => ClientId}, + TabName, ?CLIENT_QS_SCHEMA, ?query_fun + ), + case Data of + [ClientInfo] -> + {200, ClientInfo}; + [] -> + return_http_error(404, <<"Gateway or ClientId not found">>) + end; + +clients_insta(delete, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + emqx_gateway_http:client_kickout(GwName, ClientId), {200}. -subscriptions(get, _Req) -> +subscriptions(get, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + emqx_gateway_http:client_subscriptions(GwName, ClientId), {200, []}; -subscriptions(delete, _Req) -> + +subscriptions(post, #{ bindings := #{name := GwName0, + clientid := ClientId0}, + body := Body + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + + case {maps:get(<<"topic">>, Body, undefined), + maps:get(<<"qos">>, Body, 0)} of + {undefined, _} -> + %% FIXME: more reasonable error code?? + return_http_error(404, <<"Request paramter missed: topic">>); + {Topic, QoS} -> + case emqx_gateway_http:client_subscribe(GwName, ClientId, Topic, QoS) of + {error, Reason} -> + return_http_error(404, Reason); + ok -> + {200} + end + end; + +subscriptions(delete, #{ bindings := #{name := GwName0, + clientid := ClientId0, + topic := Topic0 + } + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + Topic = emqx_mgmt_util:urldecode(Topic0), + _ = emqx_gateway_http:client_unsubscribe(GwName, ClientId, Topic), {200}. %%-------------------------------------------------------------------- @@ -148,8 +204,6 @@ ms(conn_state, X) -> #{conn_state => X}; ms(clean_start, X) -> #{conninfo => #{clean_start => X}}; -ms(proto_name, X) -> - #{conninfo => #{proto_name => X}}; ms(proto_ver, X) -> #{conninfo => #{proto_ver => X}}; ms(connected_at, X) -> @@ -188,49 +242,79 @@ run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) - %%-------------------------------------------------------------------- %% format funcs -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 - , subscriptions - , 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). +format_channel_info({_, Infos, Stats}) -> + ClientInfo = maps:get(clientinfo, Infos, #{}), + ConnInfo = maps:get(conninfo, Infos, #{}), + SessInfo = maps:get(session, Infos, #{}), + FetchX = [ {node, ClientInfo, node()} + , {clientid, ClientInfo} + , {username, ClientInfo} + , {proto_name, ConnInfo} + , {proto_ver, ConnInfo} + , {ip_address, {peername, ConnInfo, fun peer_to_binary/1}} + , {is_bridge, ClientInfo, false} + , {connected_at, + {connected_at, ConnInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {disconnected_at, + {disconnected_at, ConnInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {connected, {conn_state, Infos, fun conn_state_to_connected/1}} + , {keepalive, ClientInfo, 0} + , {clean_start, ConnInfo, true} + , {expiry_interval, ConnInfo, 0} + , {created_at, + {created_at, SessInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {subscriptions_cnt, Stats, 0} + , {subscriptions_max, Stats, infinity} + , {inflight_cnt, Stats, 0} + , {inflight_max, Stats, infinity} + , {mqueue_len, Stats, 0} + , {mqueue_max, Stats, infinity} + , {mqueue_dropped, Stats, 0} + , {awaiting_rel_cnt, Stats, 0} + , {awaiting_rel_max, Stats, infinity} + , {recv_oct, Stats, 0} + , {recv_cnt, Stats, 0} + , {recv_pkt, Stats, 0} + , {recv_msg, Stats, 0} + , {send_oct, Stats, 0} + , {send_cnt, Stats, 0} + , {send_pkt, Stats, 0} + , {send_msg, Stats, 0} + , {mailbox_len, Stats, 0} + , {heap_size, Stats, 0} + , {reductions, Stats, 0} + ], + eval(FetchX). + +eval(Ls) -> + eval(Ls, #{}). +eval([], AccMap) -> + AccMap; +eval([{K, Vx}|More], AccMap) -> + case valuex_get(K, Vx) of + undefined -> eval(More, AccMap#{K => null}); + Value -> eval(More, AccMap#{K => Value}) + end; +eval([{K, Vx, Default}|More], AccMap) -> + case valuex_get(K, Vx) of + undefined -> eval(More, AccMap#{K => Default}); + Value -> eval(More, AccMap#{K => Value}) + end. + +valuex_get(K, Vx) when is_map(Vx); is_list(Vx) -> + key_get(K, Vx); +valuex_get(_K, {InKey, Obj}) when is_map(Obj); is_list(Obj) -> + key_get(InKey, Obj); +valuex_get(_K, {InKey, Obj, MappingFun}) when is_map(Obj); is_list(Obj) -> + case key_get(InKey, Obj) of + undefined -> undefined; + Val -> MappingFun(Val) + end. + +key_get(K, M) when is_map(M) -> + maps:get(K, M, undefined); +key_get(K, L) when is_list(L) -> + proplists:get_value(K, L). peer_to_binary({Addr, Port}) -> AddrBinary = list_to_binary(inet:ntoa(Addr)), @@ -239,6 +323,9 @@ peer_to_binary({Addr, Port}) -> peer_to_binary(Addr) -> list_to_binary(inet:ntoa(Addr)). +conn_state_to_connected(connected) -> true; +conn_state_to_connected(_) -> false. + %%-------------------------------------------------------------------- %% Swagger defines %%-------------------------------------------------------------------- @@ -325,6 +412,7 @@ params_client_searching_in_qs() -> , {username, string} , {ip_address, string} , {conn_state, string} + , {proto_ver, string} , {clean_start, boolean} , {like_clientid, string} , {like_username, string} @@ -426,6 +514,10 @@ properties_client() -> "when connected is false">>} , {connected, boolean, <<"Whether the client is connected">>} + %% FIXME: the will_msg attribute is not a general attribute + %% for every protocol. But it should be returned to frontend if someone + %% want it + %% %, {will_msg, string, % <<"Client will message">>} %, {zone, string, @@ -488,5 +580,11 @@ properties_subscription() -> [ {topic, string, <<"Topic Fillter">>} , {qos, integer, - <<"QoS level">>} + <<"QoS level, enum: 0, 1, 2">>} + , {nl, integer, %% FIXME: why not boolean? + <<"No Local option, enum: 0, 1">>} + , {rap, integer, + <<"Retain as Published option, enum: 0, 1">>} + , {rh, integer, + <<"Retain Handling option, enum: 0, 1, 2">>} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index b446cda92..6ccb444f0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -51,7 +51,7 @@ is_cmd(Fun) -> gateway(["list"]) -> lists:foreach(fun(#{name := Name} = Gateway) -> - %% XXX: More infos: listeners?, connected? + %% TODO: More infos: listeners?, connected? Status = maps:get(status, Gateway, stopped), emqx_ctl:print("Gateway(name=~s, status=~s)~n", [Name, Status]) @@ -106,6 +106,7 @@ gateway(_) -> ]). 'gateway-clients'(["list", Name]) -> + %% FIXME: page me. for example: --limit 100 --page 10 ??? InfoTab = emqx_gateway_cm:tabname(info, Name), case ets:info(InfoTab) of undefined -> diff --git a/apps/emqx_gateway/src/emqx_gateway_intr.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl similarity index 56% rename from apps/emqx_gateway/src/emqx_gateway_intr.erl rename to apps/emqx_gateway/src/emqx_gateway_http.erl index add37e1c5..a36d97dea 100644 --- a/apps/emqx_gateway/src/emqx_gateway_intr.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -15,11 +15,26 @@ %%-------------------------------------------------------------------- %% @doc Gateway Interface Module for HTTP-APIs --module(emqx_gateway_intr). +-module(emqx_gateway_http). +-include("include/emqx_gateway.hrl"). + +%% Mgmt APIs - gateway -export([ gateways/1 ]). +%% Mgmt APIs - clients +-export([ client_lookup/2 + , client_kickout/2 + , client_subscribe/4 + , client_unsubscribe/3 + , client_subscriptions/2 + ]). + +%% Utils for http, swagger, etc. +-export([ return_http_error/2 + ]). + -type gateway_summary() :: #{ name := binary() , status := running | stopped | unloaded @@ -30,7 +45,7 @@ }. %%-------------------------------------------------------------------- -%% APIs +%% Mgmt APIs - gateway %%-------------------------------------------------------------------- -spec gateways(Status :: all | running | stopped | unloaded) @@ -76,3 +91,58 @@ get_listeners_status(GwName, Config) -> %% @private listener_name(GwName, Type, LisName) -> list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + +%%-------------------------------------------------------------------- +%% Mgmt APIs - clients +%%-------------------------------------------------------------------- + +-spec client_lookup(gateway_name(), emqx_type:clientid()) + -> {ok, {emqx_types:infos(), emqx_types:stats()}} + | {error, any()}. +client_lookup(_GwName, _ClientId) -> + %% FIXME: The Gap between `ClientInfo in HTTP-API` and + %% ClientInfo defination + todo. + +-spec client_kickout(gateway_name(), emqx_type:clientid()) + -> {error, any()} + | ok. +client_kickout(GwName, ClientId) -> + emqx_gateway_cm:kick_session(GwName, ClientId). + +-spec client_subscriptions(gateway_name(), emqx_type:clientid()) + -> {error, any()} + | {ok, list()}. %% FIXME: #{<<"t/1">> => + %% #{nl => 0,qos => 0,rap => 0,rh => 0, + %% sub_props => #{}} +client_subscriptions(_GwName, _ClientId) -> + todo. + +-spec client_subscribe(gateway_name(), emqx_type:clientid(), + emqx_type:topic(), emqx_type:qos()) + -> {error, any()} + | ok. +client_subscribe(_GwName, _ClientId, _Topic, _QoS) -> + todo. + +-spec client_unsubscribe(gateway_name(), + emqx_type:clientid(), emqx_type:topic()) + -> {error, any()} + | ok. +client_unsubscribe(_GwName, _ClientId, _Topic) -> + todo. + +%%-------------------------------------------------------------------- +%% Utils +%%-------------------------------------------------------------------- + +-spec return_http_error(integer(), binary()) -> binary(). +return_http_error(Code, Msg) -> + emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }). + +codestr(404) -> 'RESOURCE_NOT_FOUND'; +codestr(401) -> 'NOT_SUPPORTED_NOW'; +codestr(500) -> 'UNKNOW_ERROR'. diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index dc4e38e7d..3300ebf69 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -28,6 +28,7 @@ -export([ apply/2 , format_listenon/1 + , unix_ts_to_rfc3339/1 , unix_ts_to_rfc3339/2 ]). @@ -121,6 +122,9 @@ unix_ts_to_rfc3339(Key, Map) -> emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} end. +unix_ts_to_rfc3339(Ts) -> + emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>). + -spec stringfy(term()) -> binary(). stringfy(T) -> iolist_to_binary(io_lib:format("~0p", [T])). From 1748de5ee3948b947254d4e5a5063aed27382938 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 20:15:21 +0800 Subject: [PATCH 241/306] feat(gw): support the sub/unsub operation --- .../src/emqx_gateway_api_clients.erl | 50 +++++-- apps/emqx_gateway/src/emqx_gateway_http.erl | 125 ++++++++++++++---- .../src/exproto/emqx_exproto_channel.erl | 18 +-- .../src/exproto/emqx_exproto_gsvr.erl | 4 +- .../src/mqttsn/emqx_sn_channel.erl | 18 +-- .../src/stomp/emqx_stomp_channel.erl | 56 ++++++-- 6 files changed, 200 insertions(+), 71 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index ba61a38fc..cfb7f81e8 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -18,6 +18,8 @@ -behaviour(minirest_api). +-include_lib("emqx/include/logger.hrl"). + %% minirest behaviour callbacks -export([api_spec/0]). @@ -92,20 +94,22 @@ clients(get, #{ bindings := #{name := GwName0} end. clients_insta(get, #{ bindings := #{name := GwName0, - clientid := ClientId} + clientid := ClientId0} }) -> GwName = binary_to_existing_atom(GwName0), - TabName = emqx_gateway_cm:tabname(info, GwName), - %% XXX: We need a lookuo function for it instead of a query - #{data := Data} = emqx_mgmt_api:cluster_query( - #{<<"clientid">> => ClientId}, - TabName, ?CLIENT_QS_SCHEMA, ?query_fun - ), - case Data of + ClientId = emqx_mgmt_util:urldecode(ClientId0), + + case emqx_gateway_http:lookup_client(GwName, ClientId, + {?MODULE, format_channel_info}) of [ClientInfo] -> {200, ClientInfo}; + [ClientInfo|_More] -> + ?LOG(warning, "More than one client info was returned on ~s", + [ClientId]), + {200, ClientInfo}; [] -> return_http_error(404, <<"Gateway or ClientId not found">>) + end; clients_insta(delete, #{ bindings := #{name := GwName0, @@ -113,7 +117,7 @@ clients_insta(delete, #{ bindings := #{name := GwName0, }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - emqx_gateway_http:client_kickout(GwName, ClientId), + emqx_gateway_http:kickout_client(GwName, ClientId), {200}. subscriptions(get, #{ bindings := #{name := GwName0, @@ -121,8 +125,7 @@ subscriptions(get, #{ bindings := #{name := GwName0, }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - emqx_gateway_http:client_subscriptions(GwName, ClientId), - {200, []}; + {200, emqx_gateway_http:list_client_subscriptions(GwName, ClientId)}; subscriptions(post, #{ bindings := #{name := GwName0, clientid := ClientId0}, @@ -131,8 +134,7 @@ subscriptions(post, #{ bindings := #{name := GwName0, GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - case {maps:get(<<"topic">>, Body, undefined), - maps:get(<<"qos">>, Body, 0)} of + case {maps:get(<<"topic">>, Body, undefined), subopts(Body)} of {undefined, _} -> %% FIXME: more reasonable error code?? return_http_error(404, <<"Request paramter missed: topic">>); @@ -156,6 +158,23 @@ subscriptions(delete, #{ bindings := #{name := GwName0, _ = emqx_gateway_http:client_unsubscribe(GwName, ClientId, Topic), {200}. +%%-------------------------------------------------------------------- +%% Utils + +subopts(Req) -> + #{ qos => maps:get(<<"qos">>, Req, 0) + , rap => maps:get(<<"rap">>, Req, 0) + , nl => maps:get(<<"nl">>, Req, 0) + , rh => maps:get(<<"rh">>, Req, 0) + , sub_prop => extra_sub_prop(maps:get(<<"sub_prop">>, Req, #{})) + }. + +extra_sub_prop(Props) -> + maps:filter( + fun(_, V) -> V =/= undefined end, + #{subid => maps:get(<<"subid">>, Props, undefined)} + ). + %%-------------------------------------------------------------------- %% query funcs @@ -576,6 +595,10 @@ properties_client() -> ]). properties_subscription() -> + ExtraProps = [ {subid, integer, + <<"Only stomp protocol, an uniquely identity for " + "the subscription. range: 1-65535.">>} + ], emqx_mgmt_util:properties( [ {topic, string, <<"Topic Fillter">>} @@ -587,4 +610,5 @@ properties_subscription() -> <<"Retain as Published option, enum: 0, 1">>} , {rh, integer, <<"Retain Handling option, enum: 0, 1, 2">>} + , {sub_prop, object, ExtraProps} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index a36d97dea..130c31243 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -18,17 +18,20 @@ -module(emqx_gateway_http). -include("include/emqx_gateway.hrl"). +-include_lib("emqx/include/logger.hrl"). %% Mgmt APIs - gateway -export([ gateways/1 ]). %% Mgmt APIs - clients --export([ client_lookup/2 - , client_kickout/2 +-export([ lookup_client/3 + , lookup_client/4 + , kickout_client/2 + , kickout_client/3 + , list_client_subscriptions/2 , client_subscribe/4 , client_unsubscribe/3 - , client_subscriptions/2 ]). %% Utils for http, swagger, etc. @@ -44,6 +47,8 @@ , listeners => [] }. +-define(DEFAULT_CALL_TIMEOUT, 15000). + %%-------------------------------------------------------------------- %% Mgmt APIs - gateway %%-------------------------------------------------------------------- @@ -96,41 +101,104 @@ listener_name(GwName, Type, LisName) -> %% Mgmt APIs - clients %%-------------------------------------------------------------------- --spec client_lookup(gateway_name(), emqx_type:clientid()) - -> {ok, {emqx_types:infos(), emqx_types:stats()}} - | {error, any()}. -client_lookup(_GwName, _ClientId) -> - %% FIXME: The Gap between `ClientInfo in HTTP-API` and - %% ClientInfo defination - todo. +-spec lookup_client(gateway_name(), emqx_type:clientid(), function()) -> list(). +lookup_client(GwName, ClientId, FormatFun) -> + lists:append([lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) + || Node <- ekka_mnesia:running_nodes()]). --spec client_kickout(gateway_name(), emqx_type:clientid()) +lookup_client(Node, GwName, {clientid, ClientId}, {M,F}) when Node =:= node() -> + ChanTab = emqx_gateway_cm:tabname(chan, GwName), + InfoTab = emqx_gateway_cm:tabname(info, GwName), + + lists:append(lists:map( + fun(Key) -> + lists:map(fun M:F/1, ets:lookup(InfoTab, Key)) + end, ets:lookup(ChanTab, ClientId))); + +lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) -> + rpc_call(Node, lookup_client, + [Node, GwName, {clientid, ClientId}, FormatFun]). + +-spec kickout_client(gateway_name(), emqx_type:clientid()) -> {error, any()} | ok. -client_kickout(GwName, ClientId) -> - emqx_gateway_cm:kick_session(GwName, ClientId). +kickout_client(GwName, ClientId) -> + Results = [kickout_client(Node, GwName, ClientId) + || Node <- ekka_mnesia:running_nodes()], + case lists:any(fun(Item) -> Item =:= ok end, Results) of + true -> ok; + false -> lists:last(Results) + end. --spec client_subscriptions(gateway_name(), emqx_type:clientid()) +kickout_client(Node, GwName, ClientId) when Node =:= node() -> + emqx_gateway_cm:kick_session(GwName, ClientId); + +kickout_client(Node, GwName, ClientId) -> + rpc_call(Node, kickout_client, [Node, GwName, ClientId]). + +-spec list_client_subscriptions(gateway_name(), emqx_type:clientid()) -> {error, any()} - | {ok, list()}. %% FIXME: #{<<"t/1">> => - %% #{nl => 0,qos => 0,rap => 0,rh => 0, - %% sub_props => #{}} -client_subscriptions(_GwName, _ClientId) -> - todo. + | {ok, list()}. +list_client_subscriptions(GwName, ClientId) -> + %% Get the subscriptions from session-info + case emqx_gateway_cm:get_chan_info(GwName, ClientId) of + undefined -> + {error, not_found}; + Infos -> + Subs = maps:get(subscriptions, Infos, #{}), + maps:fold(fun(K, V, Acc) -> + [maps:merge( + #{topic => K}, + maps:with([qos, nl, rap, rh], V)) + |Acc] + end, [], Subs) + end. -spec client_subscribe(gateway_name(), emqx_type:clientid(), - emqx_type:topic(), emqx_type:qos()) + emqx_type:topic(), emqx_type:subopts()) -> {error, any()} | ok. -client_subscribe(_GwName, _ClientId, _Topic, _QoS) -> - todo. +client_subscribe(GwName, ClientId, Topic, SubOpts) -> + case emqx_gateway_cm:lookup_channels(GwName, ClientId) of + [] -> {error, not_found}; + [Pid] -> + %% fixed conn module? + emqx_gateway_conn:call( + Pid, {subscribe, Topic, SubOpts}, + ?DEFAULT_CALL_TIMEOUT + ); + Pids -> + ?LOG(warning, "More than one client process ~p was found " + "clientid ~s", [Pids, ClientId]), + _ = [ + emqx_gateway_conn:call( + Pid, {subscribe, Topic, SubOpts}, + ?DEFAULT_CALL_TIMEOUT + ) || Pid <- Pids], + ok + end. -spec client_unsubscribe(gateway_name(), emqx_type:clientid(), emqx_type:topic()) -> {error, any()} | ok. -client_unsubscribe(_GwName, _ClientId, _Topic) -> - todo. +client_unsubscribe(GwName, ClientId, Topic) -> + case emqx_gateway_cm:lookup_channels(GwName, ClientId) of + [] -> {error, not_found}; + [Pid] -> + emqx_gateway_conn:call( + Pid, {unsubscribe, Topic}, + ?DEFAULT_CALL_TIMEOUT); + Pids -> + ?LOG(warning, "More than one client process ~p was found " + "clientid ~s", [Pids, ClientId]), + _ = [ + emqx_gateway_conn:call( + Pid, {unsubscribe, Topic}, + ?DEFAULT_CALL_TIMEOUT + ) || Pid <- Pids], + ok + end. %%-------------------------------------------------------------------- %% Utils @@ -146,3 +214,12 @@ return_http_error(Code, Msg) -> codestr(404) -> 'RESOURCE_NOT_FOUND'; codestr(401) -> 'NOT_SUPPORTED_NOW'; codestr(500) -> 'UNKNOW_ERROR'. + +%%-------------------------------------------------------------------- +%% Internal funcs + +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> {error, Reason}; + Res -> Res + end. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl index b1a1ae027..ace9a7be5 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl @@ -310,7 +310,7 @@ handle_call({start_timer, keepalive, Interval}, NChannel = Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo}, {reply, ok, ensure_keepalive(NChannel)}; -handle_call({subscribe, TopicFilter, Qos}, +handle_call({subscribe_from_client, TopicFilter, Qos}, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -323,11 +323,19 @@ handle_call({subscribe, TopicFilter, Qos}, {reply, ok, NChannel} end; -handle_call({unsubscribe, TopicFilter}, +handle_call({subscribe, Topic, SubOpts}, Channel) -> + {ok, NChannel} = do_subscribe([{Topic, SubOpts}], Channel), + {reply, ok, NChannel}; + +handle_call({unsubscribe_from_client, TopicFilter}, Channel = #channel{conn_state = connected}) -> {ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel), {reply, ok, NChannel}; +handle_call({unsubscribe, Topic}, Channel) -> + {ok, NChannel} = do_unsubscribe([Topic], Channel), + {reply, ok, NChannel}; + handle_call({publish, Topic, Qos, Payload}, Channel = #channel{ ctx = Ctx, @@ -363,12 +371,6 @@ handle_cast(Req, Channel) -> -spec handle_info(any(), channel()) -> {ok, channel()} | {shutdown, Reason :: term(), channel()}. -handle_info({subscribe, TopicFilters}, Channel) -> - do_subscribe(TopicFilters, Channel); - -handle_info({unsubscribe, TopicFilters}, Channel) -> - do_unsubscribe(TopicFilters, Channel); - handle_info({sock_closed, Reason}, Channel = #channel{rqueue = Queue, inflight = Inflight}) -> case queue:len(Queue) =:= 0 diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl index 346f87452..0135aa8e3 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl @@ -96,7 +96,7 @@ publish(Req, Md) -> subscribe(Req = #{conn := Conn, topic := Topic, qos := Qos}, Md) when ?IS_QOS(Qos) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), - {ok, response(call(Conn, {subscribe, Topic, Qos})), Md}; + {ok, response(call(Conn, {subscribe_from_client, Topic, Qos})), Md}; subscribe(Req, Md) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), @@ -107,7 +107,7 @@ subscribe(Req, Md) -> | {error, grpc_cowboy_h:error_response()}. unsubscribe(Req = #{conn := Conn, topic := Topic}, Md) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), - {ok, response(call(Conn, {unsubscribe, Topic})), Md}. + {ok, response(call(Conn, {unsubscribe_from_client, Topic})), Md}. %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 5bba599c8..0707534b7 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -1102,6 +1102,12 @@ message_to_packet(MsgId, Message, | {shutdown, Reason :: term(), Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}. +handle_call({subscribe, _Topic, _Subopts}, Channel) -> + reply({error, not_supported_now}, Channel); + +handle_call({unsubscribe, _Topic}, Channel) -> + reply({error, not_supported_now}, Channel); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); @@ -1150,18 +1156,6 @@ handle_cast(_Req, Channel) -> -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); diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 250f43988..3e7fe8b42 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -626,6 +626,50 @@ handle_out(receipt, ReceiptId, Channel) -> -> {reply, Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). +handle_call({subscribe, Topic, SubOpts}, + Channel = #channel{ + subscriptions = Subs + }) -> + case maps:get(subid, + maps:get(sub_prop, SubOpts, #{}), + undefined) of + undefined -> + reply({error, no_subid}, Channel); + SubId -> + case emqx_misc:pipeline( + [ fun parse_topic_filter/2 + , fun check_subscribed_status/2 + ], {SubId, {Topic, SubOpts}}, Channel) of + {ok, {_, TopicFilter}, NChannel} -> + [MountedTopic] = do_subscribe([TopicFilter], NChannel), + NChannel1 = NChannel#channel{ + subscriptions = + [{SubId, MountedTopic, <<"auto">>}|Subs] + }, + reply(ok, NChannel1); + {error, ErrMsg, NChannel} -> + ?LOG(error, "Failed to subscribe topic ~s, reason: ~s", + [Topic, ErrMsg]), + reply({error, ErrMsg}, NChannel) + end + end; + +handle_call({unsubscribe, Topic}, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo = #{mountpoint := Mountpoint}, + subscriptions = Subs + }) -> + {ParsedTopic, _SubOpts} = emqx_topic:parse(Topic), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), + ok = emqx_broker:unsubscribe(MountedTopic), + _ = run_hooks(Ctx, 'session.unsubscribe', + [ClientInfo, MountedTopic, #{}]), + reply(ok, + Channel#channel{ + subscriptions = lists:keydelete(MountedTopic, 2, Subs)} + ); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), @@ -678,18 +722,6 @@ handle_cast(_Req, Channel) -> -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); From 40d34ccd85310cf6b3cbfcddff9bb29d2a35fce7 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 1 Sep 2021 11:42:46 +0800 Subject: [PATCH 242/306] fix(gw): fix the subscription apis bugs --- .../src/emqx_gateway_api_clients.erl | 20 ++++-- apps/emqx_gateway/src/emqx_gateway_cm.erl | 4 ++ apps/emqx_gateway/src/emqx_gateway_http.erl | 72 +++++++------------ apps/emqx_gateway/src/emqx_gateway_utils.erl | 3 +- .../src/stomp/emqx_stomp_channel.erl | 52 ++++++++------ 5 files changed, 78 insertions(+), 73 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index cfb7f81e8..99876c917 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -120,13 +120,22 @@ clients_insta(delete, #{ bindings := #{name := GwName0, emqx_gateway_http:kickout_client(GwName, ClientId), {200}. +%% FIXME: +%% List the subscription without mountpoint, but has SubOpts, +%% for example, share group ... subscriptions(get, #{ bindings := #{name := GwName0, clientid := ClientId0} }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - {200, emqx_gateway_http:list_client_subscriptions(GwName, ClientId)}; + case emqx_gateway_http:list_client_subscriptions(GwName, ClientId) of + {error, Reason} -> + return_http_error(404, Reason); + {ok, Subs} -> + {200, Subs} + end; +%% Create the subscription without mountpoint subscriptions(post, #{ bindings := #{name := GwName0, clientid := ClientId0}, body := Body @@ -147,6 +156,7 @@ subscriptions(post, #{ bindings := #{name := GwName0, end end; +%% Remove the subscription without mountpoint subscriptions(delete, #{ bindings := #{name := GwName0, clientid := ClientId0, topic := Topic0 @@ -166,10 +176,10 @@ subopts(Req) -> , rap => maps:get(<<"rap">>, Req, 0) , nl => maps:get(<<"nl">>, Req, 0) , rh => maps:get(<<"rh">>, Req, 0) - , sub_prop => extra_sub_prop(maps:get(<<"sub_prop">>, Req, #{})) + , sub_props => extra_sub_props(maps:get(<<"sub_props">>, Req, #{})) }. -extra_sub_prop(Props) -> +extra_sub_props(Props) -> maps:filter( fun(_, V) -> V =/= undefined end, #{subid => maps:get(<<"subid">>, Props, undefined)} @@ -595,7 +605,7 @@ properties_client() -> ]). properties_subscription() -> - ExtraProps = [ {subid, integer, + ExtraProps = [ {subid, string, <<"Only stomp protocol, an uniquely identity for " "the subscription. range: 1-65535.">>} ], @@ -610,5 +620,5 @@ properties_subscription() -> <<"Retain as Published option, enum: 0, 1">>} , {rh, integer, <<"Retain Handling option, enum: 0, 1, 2">>} - , {sub_prop, object, ExtraProps} + , {sub_props, object, ExtraProps} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 7a7ad055d..d8b615fe8 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -48,6 +48,10 @@ , connection_closed/2 ]). +-export([ with_channel/3 + , lookup_channels/2 + ]). + %% Internal funcs for getting tabname by GatewayId -export([cmtabs/1, tabname/2]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index 130c31243..5b7055d25 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -141,63 +141,44 @@ kickout_client(Node, GwName, ClientId) -> | {ok, list()}. list_client_subscriptions(GwName, ClientId) -> %% Get the subscriptions from session-info - case emqx_gateway_cm:get_chan_info(GwName, ClientId) of - undefined -> - {error, not_found}; - Infos -> - Subs = maps:get(subscriptions, Infos, #{}), - maps:fold(fun(K, V, Acc) -> - [maps:merge( - #{topic => K}, - maps:with([qos, nl, rap, rh], V)) - |Acc] - end, [], Subs) - end. + with_channel(GwName, ClientId, + fun(Pid) -> + Subs = emqx_gateway_conn:call( + Pid, + subscriptions, ?DEFAULT_CALL_TIMEOUT), + {ok, lists:map(fun({Topic, SubOpts}) -> + SubOpts#{topic => Topic} + end, Subs)} + end). -spec client_subscribe(gateway_name(), emqx_type:clientid(), emqx_type:topic(), emqx_type:subopts()) -> {error, any()} | ok. client_subscribe(GwName, ClientId, Topic, SubOpts) -> - case emqx_gateway_cm:lookup_channels(GwName, ClientId) of - [] -> {error, not_found}; - [Pid] -> - %% fixed conn module? + with_channel(GwName, ClientId, + fun(Pid) -> emqx_gateway_conn:call( Pid, {subscribe, Topic, SubOpts}, ?DEFAULT_CALL_TIMEOUT - ); - Pids -> - ?LOG(warning, "More than one client process ~p was found " - "clientid ~s", [Pids, ClientId]), - _ = [ - emqx_gateway_conn:call( - Pid, {subscribe, Topic, SubOpts}, - ?DEFAULT_CALL_TIMEOUT - ) || Pid <- Pids], - ok - end. + ) + end). -spec client_unsubscribe(gateway_name(), emqx_type:clientid(), emqx_type:topic()) -> {error, any()} | ok. client_unsubscribe(GwName, ClientId, Topic) -> - case emqx_gateway_cm:lookup_channels(GwName, ClientId) of - [] -> {error, not_found}; - [Pid] -> + with_channel(GwName, ClientId, + fun(Pid) -> emqx_gateway_conn:call( - Pid, {unsubscribe, Topic}, - ?DEFAULT_CALL_TIMEOUT); - Pids -> - ?LOG(warning, "More than one client process ~p was found " - "clientid ~s", [Pids, ClientId]), - _ = [ - emqx_gateway_conn:call( - Pid, {unsubscribe, Topic}, - ?DEFAULT_CALL_TIMEOUT - ) || Pid <- Pids], - ok + Pid, {unsubscribe, Topic}, ?DEFAULT_CALL_TIMEOUT) + end). + +with_channel(GwName, ClientId, Fun) -> + case emqx_gateway_cm:with_channel(GwName, ClientId, Fun) of + undefined -> {error, not_found}; + Res -> Res end. %%-------------------------------------------------------------------- @@ -206,10 +187,11 @@ client_unsubscribe(GwName, ClientId, Topic) -> -spec return_http_error(integer(), binary()) -> binary(). return_http_error(Code, Msg) -> - emqx_json:encode( - #{code => codestr(Code), - reason => emqx_gateway_utils:stringfy(Msg) - }). + {Code, emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }) + }. codestr(404) -> 'RESOURCE_NOT_FOUND'; codestr(401) -> 'NOT_SUPPORTED_NOW'; diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 3300ebf69..2b4e9f0a2 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -209,5 +209,6 @@ default_subopts() -> #{rh => 0, %% Retain Handling rap => 0, %% Retain as Publish nl => 0, %% No Local - qos => 0 %% QoS + qos => 0, %% QoS + is_new => true }. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 3e7fe8b42..a57c8e667 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -393,11 +393,9 @@ handle_in(?PACKET(?CMD_SUBSCRIBE, Headers), [] -> ErrMsg = "Permission denied", handle_out(error, {receipt_id(Headers), ErrMsg}, Channel); - [MountedTopic|_] -> - NChannel1 = NChannel#channel{ - subscriptions = [{SubId, MountedTopic, Ack} - | Subs] - }, + [{MountedTopic, SubOpts}|_] -> + NSubs = [{SubId, MountedTopic, Ack, SubOpts}|Subs], + NChannel1 = NChannel#channel{subscriptions = NSubs}, handle_out(receipt, receipt_id(Headers), NChannel1) end; {error, ErrMsg, NChannel} -> @@ -415,7 +413,7 @@ handle_in(?PACKET(?CMD_UNSUBSCRIBE, Headers), SubId = header(<<"id">>, Headers), {ok, NChannel} = case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack} -> + {SubId, MountedTopic, _Ack, _SubOpts} -> Topic = emqx_mountpoint:unmount(Mountpoint, MountedTopic), %% XXX: eval the return topics? _ = run_hooks(Ctx, 'client.unsubscribe', @@ -539,15 +537,16 @@ trans_pipeline([{Func, Args}|More], Outgoings, Channel) -> %% Subs parse_topic_filter({SubId, Topic}, Channel) -> - TopicFilter = emqx_topic:parse(Topic), - {ok, {SubId, TopicFilter}, Channel}. + {ParsedTopic, SubOpts} = emqx_topic:parse(Topic), + NSubOpts = SubOpts#{sub_props => #{subid => SubId}}, + {ok, {SubId, {ParsedTopic, NSubOpts}}, Channel}. -check_subscribed_status({SubId, TopicFilter}, +check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, #channel{ subscriptions = Subs, clientinfo = #{mountpoint := Mountpoint} }) -> - MountedTopic = emqx_mountpoint:mount(Mountpoint, TopicFilter), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of {SubId, MountedTopic, _Ack} -> ok; @@ -557,11 +556,11 @@ check_subscribed_status({SubId, TopicFilter}, ok end. -check_sub_acl({_SubId, TopicFilter}, +check_sub_acl({_SubId, {ParsedTopic, _SubOpts}}, #channel{ ctx = Ctx, clientinfo = ClientInfo}) -> - case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, TopicFilter) of + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, ParsedTopic) of deny -> {error, "ACL Deny"}; allow -> ok end. @@ -571,17 +570,17 @@ do_subscribe(TopicFilters, Channel) -> do_subscribe([], _Channel, Acc) -> lists:reverse(Acc); -do_subscribe([{TopicFilter, Option}|More], +do_subscribe([{ParsedTopic, SubOpts0}|More], Channel = #channel{ ctx = Ctx, clientinfo = ClientInfo = #{clientid := ClientId, mountpoint := Mountpoint}}, Acc) -> - SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), Option), - MountedTopic = emqx_mountpoint:mount(Mountpoint, TopicFilter), + SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts0), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), _ = emqx_broker:subscribe(MountedTopic, ClientId, SubOpts), run_hooks(Ctx, 'session.subscribed', [ClientInfo, MountedTopic, SubOpts]), - do_subscribe(More, Channel, [MountedTopic|Acc]). + do_subscribe(More, Channel, [{MountedTopic, SubOpts}|Acc]). %%-------------------------------------------------------------------- %% Handle outgoing packet @@ -631,7 +630,7 @@ handle_call({subscribe, Topic, SubOpts}, subscriptions = Subs }) -> case maps:get(subid, - maps:get(sub_prop, SubOpts, #{}), + maps:get(sub_props, SubOpts, #{}), undefined) of undefined -> reply({error, no_subid}, Channel); @@ -641,11 +640,12 @@ handle_call({subscribe, Topic, SubOpts}, , fun check_subscribed_status/2 ], {SubId, {Topic, SubOpts}}, Channel) of {ok, {_, TopicFilter}, NChannel} -> - [MountedTopic] = do_subscribe([TopicFilter], NChannel), - NChannel1 = NChannel#channel{ - subscriptions = - [{SubId, MountedTopic, <<"auto">>}|Subs] - }, + [{MountedTopic, NSubOpts}] = do_subscribe( + [TopicFilter], + NChannel + ), + NSubs = [{SubId, MountedTopic, <<"auto">>, NSubOpts}|Subs], + NChannel1 = NChannel#channel{subscriptions = NSubs}, reply(ok, NChannel1); {error, ErrMsg, NChannel} -> ?LOG(error, "Failed to subscribe topic ~s, reason: ~s", @@ -670,6 +670,14 @@ handle_call({unsubscribe, Topic}, subscriptions = lists:keydelete(MountedTopic, 2, Subs)} ); +%% Reply :: [{emqx_types:topic(), emqx_types:subopts()}] +handle_call(subscriptions, Channel = #channel{subscriptions = Subs}) -> + Reply = lists:map( + fun({_SubId, Topic, _Ack, SubOpts}) -> + {Topic, SubOpts} + end, Subs), + reply(Reply, Channel); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), From fd12a7ac9c828514dd6148e84a9f082747df4af4 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 1 Sep 2021 14:50:02 +0800 Subject: [PATCH 243/306] chore(gw): improve the gateway api swagger codes --- apps/emqx_gateway/src/emqx_gateway_api.erl | 434 ++++++++---------- .../src/emqx_gateway_api_clients.erl | 4 +- .../src/stomp/emqx_stomp_channel.erl | 6 +- apps/emqx_management/src/emqx_mgmt_util.erl | 5 + 4 files changed, 199 insertions(+), 250 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 2ff002fd5..9c9398945 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -18,12 +18,6 @@ -behaviour(minirest_api). --compile(nowarn_unused_function). - --import(emqx_mgmt_util, - [ schema/1 - ]). - -import(emqx_gateway_http, [ return_http_error/2 ]). @@ -37,18 +31,160 @@ , gateway_insta_stats/2 ]). --define(EXAMPLE_GATEWAY_LIST, - [ #{ name => <<"lwm2m">> - , status => <<"running">> - , started_at => <<"2021-08-19T11:45:56.006373+08:00">> - , max_connection => 1024000 - , current_connection => 1000 - , listeners => [ - #{name => <<"lw-udp-1">>, status => <<"activing">>}, - #{name => <<"lw-udp-2">>, status => <<"inactived">>} - ] - } - ]). +%%-------------------------------------------------------------------- +%% minirest behaviour callbacks +%%-------------------------------------------------------------------- + +api_spec() -> + {metadata(apis()), []}. + +apis() -> + [ {"/gateway", gateway} + , {"/gateway/:name", gateway_insta} + , {"/gateway/:name/stats", gateway_insta_stats} + ]. +%%-------------------------------------------------------------------- +%% http handlers + +gateway(get, Request) -> + Params = maps:get(query_string, Request, #{}), + Status = case maps:get(<<"status">>, Params, undefined) of + undefined -> all; + S0 -> binary_to_existing_atom(S0, utf8) + end, + {200, emqx_gateway_http:gateways(Status)}. + +gateway_insta(delete, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), + case emqx_gateway:unload(Name) of + ok -> + {204}; + {error, not_found} -> + return_http_error(404, <<"Gateway not found">>) + end; +gateway_insta(get, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), + case emqx_gateway:lookup(Name) of + #{config := _Config} -> + %% FIXME: Got the parsed config, but we should return rawconfig to + %% frontend + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw([<<"gateway">>]) + ), + {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; + undefined -> + return_http_error(404, <<"Gateway not found">>) + end; +gateway_insta(put, #{body := RawConfsIn, + bindings := #{name := Name} + }) -> + %% FIXME: Cluster Consistence ?? + case emqx_gateway:update_rawconf(Name, RawConfsIn) of + ok -> + {200}; + {error, not_found} -> + return_http_error(404, <<"Gateway not found">>); + {error, Reason} -> + return_http_error(500, Reason) + end. + +gateway_insta_stats(get, _Req) -> + return_http_error(401, <<"Implement it later (maybe 5.1)">>). + + +%%-------------------------------------------------------------------- +%% Swagger defines +%%-------------------------------------------------------------------- + +metadata(APIs) -> + metadata(APIs, []). +metadata([], APIAcc) -> + lists:reverse(APIAcc); +metadata([{Path, Fun}|More], APIAcc) -> + Methods = [get, post, put, delete, patch], + Mds = lists:foldl(fun(M, Acc) -> + try + Acc#{M => swagger(Path, M)} + catch + error : function_clause -> + Acc + end + end, #{}, Methods), + metadata(More, [{Path, Mds, Fun} | APIAcc]). + +swagger("/gateway", get) -> + #{ description => <<"Get gateway list">> + , parameters => params_gateway_status_in_qs() + , responses => + #{ <<"200">> => schema_gateway_overview_list() } + }; +swagger("/gateway/:name", get) -> + #{ description => <<"Get the gateway configurations">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_gateway_conf() + } + }; +swagger("/gateway/:name", delete) -> + #{ description => <<"Delete/Unload the gateway">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }; +swagger("/gateway/:name", put) -> + #{ description => <<"Update the gateway configurations/status">> + , parameters => params_gateway_name_in_path() + , requestBody => schema_gateway_conf() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_no_content() + } + }; +swagger("/gateway/:name/stats", get) -> + #{ description => <<"Get gateway Statistic">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_gateway_stats() + } + }. + +%%-------------------------------------------------------------------- +%% params defines + +params_gateway_name_in_path() -> + [#{ name => name + , in => path + , schema => #{type => string} + , required => true + }]. + +params_gateway_status_in_qs() -> + [#{ name => status + , in => query + , schema => #{type => string} + , required => false + }]. + +%%-------------------------------------------------------------------- +%% schemas + +schema_not_found() -> + emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). + +schema_no_content() -> + #{description => <<"No Content">>}. + +schema_gateway_overview_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_gateway_overview() + }, + <<"Gateway Overview list">> + ). %% XXX: This is whole confs for all type gateways. It is used to fill the %% default configurations and generate the swagger-schema @@ -154,234 +290,42 @@ %% --- END --define(EXAMPLE_GATEWAY_STATS, #{ - max_connection => 10240000, - current_connection => 1000, - messages_in => 100.24, - messages_out => 32.5 - }). +schema_gateway_conf() -> + emqx_mgmt_util:schema( + #{oneOf => + [ emqx_mgmt_api_configs:gen_schema(?STOMP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?MQTTSN_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?COAP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?LWM2M_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?EXPROTO_GATEWAY_CONFS) + ]}). + +schema_gateway_stats() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => + #{ a_key => #{type => string} + }}). %%-------------------------------------------------------------------- -%% minirest behaviour callbacks -%%-------------------------------------------------------------------- +%% properties -api_spec() -> - {apis(), schemas()}. - -apis() -> - [ {"/gateway", metadata(gateway), gateway} - , {"/gateway/:name", metadata(gateway_insta), gateway_insta} - , {"/gateway/:name/stats", metadata(gateway_insta_stats), gateway_insta_stats} - ]. - -metadata(gateway) -> - #{get => #{ - description => <<"Get gateway list">>, - parameters => [ - #{name => status, - in => query, - schema => #{type => string}, - required => false - } +properties_gateway_overview() -> + ListenerProps = + [ {name, string, + <<"Listener Name">>} + , {status, string, + <<"Listener Status">>, [<<"activing">>, <<"inactived">>]} ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"gateway_overrview">>), - examples => #{ - simple => #{ - summary => <<"Gateway List Example">>, - value => emqx_json:encode(?EXAMPLE_GATEWAY_LIST) - } - } - } - } - } - } - }}; - -metadata(gateway_insta) -> - UriNameParamDef = #{name => name, - in => path, - schema => #{type => string}, - required => true - }, - NameNotFoundRespDef = - #{description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - simple => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"The gateway not found">> - } - } - } - } - }}, - #{delete => #{ - description => <<"Delete/Unload the gateway">>, - parameters => [UriNameParamDef], - responses => #{ - <<"404">> => NameNotFoundRespDef, - <<"204">> => #{description => <<"No Content">>} - } - }, - get => #{ - description => <<"Get the gateway configurations">>, - parameters => [UriNameParamDef], - responses => #{ - <<"404">> => NameNotFoundRespDef, - <<"200">> => schema(schema_for_gateway_conf()) - } - }, - put => #{ - description => <<"Update the gateway configurations/status">>, - parameters => [UriNameParamDef], - requestBody => schema(schema_for_gateway_conf()), - responses => #{ - <<"404">> => NameNotFoundRespDef, - <<"200">> => #{description => <<"Changed">>} - } - } - }; - -metadata(gateway_insta_stats) -> - #{get => #{ - description => <<"Get gateway Statistic">>, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"gateway_stats">>), - examples => #{ - simple => #{ - summary => <<"Gateway Statistic">>, - value => emqx_json:encode(?EXAMPLE_GATEWAY_STATS) - } - } - } - } - } - } - }}. - -schemas() -> - [ #{<<"gateway_overrview">> => schema_for_gateway_overrview()} - , #{<<"gateway_stats">> => schema_for_gateway_stats()} - ]. - -schema_for_gateway_overrview() -> - #{type => array, - items => #{ - type => object, - properties => #{ - name => #{ - type => string, - example => <<"lwm2m">> - }, - status => #{ - type => string, - enum => [<<"running">>, <<"stopped">>, <<"unloaded">>], - example => <<"running">> - }, - started_at => #{ - type => string, - example => <<"2021-08-19T11:45:56.006373+08:00">> - }, - max_connection => #{ - type => integer, - example => 1024000 - }, - current_connection => #{ - type => integer, - example => 1000 - }, - listeners => #{ - type => array, - items => #{ - type => object, - properties => #{ - name => #{ - type => string, - example => <<"lw-udp">> - }, - status => #{ - type => string, - enum => [<<"activing">>, <<"inactived">>] - } - } - } - } - } - } - }. - -schema_for_gateway_conf() -> - #{oneOf => - [ emqx_mgmt_api_configs:gen_schema(?STOMP_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?MQTTSN_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?COAP_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?LWM2M_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?EXPROTO_GATEWAY_CONFS) - ]}. - -schema_for_gateway_stats() -> - #{type => object, - properties => #{ - a_key => #{type => string} - }}. - -%%-------------------------------------------------------------------- -%% http handlers - -gateway(get, Request) -> - Params = maps:get(query_string, Request, #{}), - Status = case maps:get(<<"status">>, Params, undefined) of - undefined -> all; - S0 -> binary_to_existing_atom(S0, utf8) - end, - {200, emqx_gateway_http:gateways(Status)}. - -gateway_insta(delete, #{bindings := #{name := Name0}}) -> - Name = binary_to_existing_atom(Name0), - case emqx_gateway:unload(Name) of - ok -> - {200}; - {error, not_found} -> - return_http_error(404, <<"Gateway not found">>) - end; -gateway_insta(get, #{bindings := #{name := Name0}}) -> - Name = binary_to_existing_atom(Name0), - case emqx_gateway:lookup(Name) of - #{config := _Config} -> - %% FIXME: Got the parsed config, but we should return rawconfig to - %% frontend - RawConf = emqx_config:fill_defaults( - emqx_config:get_root_raw([<<"gateway">>]) - ), - {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; - undefined -> - return_http_error(404, <<"Gateway not found">>) - end; -gateway_insta(put, #{body := RawConfsIn, - bindings := #{name := Name} - }) -> - %% FIXME: Cluster Consistence ?? - case emqx_gateway:update_rawconf(Name, RawConfsIn) of - ok -> - {200}; - {error, not_found} -> - return_http_error(404, <<"Gateway not found">>); - {error, Reason} -> - return_http_error(500, Reason) - end. - -gateway_insta_stats(get, _Req) -> - return_http_error(401, <<"Implement it later (maybe 5.1)">>). + emqx_mgmt_util:properties( + [ {name, string, + <<"Gateway Name">>} + , {status, string, + <<"Gateway Status">>, + [<<"running">>, <<"stopped">>, <<"unloaded">>]} + , {started_at, string, + <<>>} + , {max_connection, integer, <<>>} + , {current_connection, integer, <<>>} + , {listeners, {array, object}, ListenerProps} + ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index 99876c917..b463fb468 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -481,7 +481,7 @@ queries(Ls) -> end, Ls). %%-------------------------------------------------------------------- -%% Schemas +%% schemas schema_not_found() -> emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). @@ -518,7 +518,7 @@ schema_subscription() -> ). %%-------------------------------------------------------------------- -%% Object properties def +%% properties defines properties_client() -> emqx_mgmt_util:properties( diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index a57c8e667..1e0c5e2d4 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -548,9 +548,9 @@ check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, }) -> MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack} -> + {SubId, MountedTopic, _Ack, _SubOpts} -> ok; - {SubId, _OtherTopic, _Ack} -> + {SubId, _OtherTopic, _Ack, _SubOpts} -> {error, "Conflict subscribe id"}; false -> ok @@ -795,7 +795,7 @@ handle_deliver(Delivers, Frames0 = lists:foldl(fun({_, _, Message}, Acc) -> Topic0 = emqx_message:topic(Message), case lists:keyfind(Topic0, 2, Subs) of - {Id, Topic, Ack} -> + {Id, Topic, Ack, _SubOpts} -> %% XXX: refactor later metrics_inc('messages.delivered', Channel), NMessage = run_hooks_without_metrics( diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 4c1009610..48bef33ac 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -224,6 +224,11 @@ properties([{Key, Type} | Props], Acc) -> properties([{Key, object, Props1} | Props], Acc) -> properties(Props, maps:put(Key, #{type => object, properties => properties(Props1)}, Acc)); +properties([{Key, {array, object}, Props1} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => array, + items => #{type => object, + properties => properties(Props1) + }}, Acc)); properties([{Key, {array, Type}, Desc} | Props], Acc) -> properties(Props, maps:put(Key, #{type => array, items => #{type => Type}, From dc05cdc58633da5934c9fd8c724b1d5cb9a8ddbe Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 1 Sep 2021 15:21:07 +0800 Subject: [PATCH 244/306] chore(gw): fix dialyzer warnings --- apps/emqx_gateway/src/emqx_gateway_api_clients.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_http.erl | 5 +++-- apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index b463fb468..fcfea7343 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -117,7 +117,7 @@ clients_insta(delete, #{ bindings := #{name := GwName0, }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - emqx_gateway_http:kickout_client(GwName, ClientId), + _ = emqx_gateway_http:kickout_client(GwName, ClientId), {200}. %% FIXME: diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index 5b7055d25..2aa6b4b3d 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -101,7 +101,8 @@ listener_name(GwName, Type, LisName) -> %% Mgmt APIs - clients %%-------------------------------------------------------------------- --spec lookup_client(gateway_name(), emqx_type:clientid(), function()) -> list(). +-spec lookup_client(gateway_name(), + emqx_type:clientid(), {atom(), atom()}) -> list(). lookup_client(GwName, ClientId, FormatFun) -> lists:append([lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) || Node <- ekka_mnesia:running_nodes()]). @@ -185,7 +186,7 @@ with_channel(GwName, ClientId, Fun) -> %% Utils %%-------------------------------------------------------------------- --spec return_http_error(integer(), binary()) -> binary(). +-spec return_http_error(integer(), binary()) -> {integer(), binary()}. return_http_error(Code, Msg) -> {Code, emqx_json:encode( #{code => codestr(Code), diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 0707534b7..e8a763332 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -23,7 +23,6 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/logger.hrl"). - %% API -export([ info/1 , info/2 @@ -1108,6 +1107,9 @@ handle_call({subscribe, _Topic, _Subopts}, Channel) -> handle_call({unsubscribe, _Topic}, Channel) -> reply({error, not_supported_now}, Channel); +handle_call(subscriptions, Channel) -> + reply({error, not_supported_now}, Channel); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); From 956308f0ca7a075a84c490d4483237f1da0f844f Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 2 Sep 2021 11:32:33 +0800 Subject: [PATCH 245/306] feat(gw-mqttsn): support subscribe/unsubscribe operation --- .../src/bhvrs/emqx_gateway_conn.erl | 2 - .../src/mqttsn/emqx_sn_channel.erl | 143 +++++++++++------- .../src/stomp/emqx_stomp_channel.erl | 5 +- 3 files changed, 93 insertions(+), 57 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index fa0a830e5..8f6cf6e97 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -20,7 +20,6 @@ -include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/logger.hrl"). - %% API -export([ start_link/3 , stop/1 @@ -48,7 +47,6 @@ %% Internal callback -export([wakeup_from_hib/2, recvloop/2]). - -record(state, { %% TCP/SSL/UDP/DTLS Wrapped Socket socket :: {esockd_transport, esockd:socket()} | {udp, _, _}, diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index e8a763332..2834c27f6 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -95,9 +95,9 @@ }). -define(DEFAULT_OVERRIDE, - #{ clientid => <<"">> %% Generate clientid by default - , username => <<"${Packet.headers.login}">> - , password => <<"${Packet.headers.passcode}">> + #{ clientid => <<"${ConnInfo.clientid}">> + %, username => <<"${ConnInfo.clientid}">> + %, password => <<"${Packet.headers.passcode}">> }). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]). @@ -189,9 +189,10 @@ stats(#channel{session = Session})-> set_conn_state(ConnState, Channel) -> Channel#channel{conn_state = ConnState}. -enrich_conninfo(?SN_CONNECT_MSG(_Flags, _ProtoId, Duration, _ClientId), +enrich_conninfo(?SN_CONNECT_MSG(_Flags, _ProtoId, Duration, ClientId), Channel = #channel{conninfo = ConnInfo}) -> - NConnInfo = ConnInfo#{ proto_name => <<"MQTT-SN">> + NConnInfo = ConnInfo#{ clientid => ClientId + , proto_name => <<"MQTT-SN">> , proto_ver => <<"1.2">> , clean_start => true , keepalive => Duration @@ -592,9 +593,11 @@ handle_in(SubPkt = ?SN_SUBSCRIBE_MSG(_, MsgId, _), Channel) -> case emqx_misc:pipeline( [ fun preproc_subs_type/2 , fun check_subscribe_authz/2 + , fun run_client_subs_hook/2 , fun do_subscribe/2 ], SubPkt, Channel) of - {ok, {TopicId, GrantedQoS}, NChannel} -> + {ok, {TopicId, _TopicName, SubOpts}, NChannel} -> + GrantedQoS = maps:get(qos, SubOpts), SubAck = ?SN_SUBACK_MSG(#mqtt_sn_flags{qos = GrantedQoS}, TopicId, MsgId, ?SN_RC_ACCEPTED), {ok, outgoing_and_update(SubAck), NChannel}; @@ -610,6 +613,7 @@ handle_in(UnsubPkt = ?SN_UNSUBSCRIBE_MSG(_, MsgId, TopicIdOrName), Channel) -> case emqx_misc:pipeline( [ fun preproc_unsub_type/2 + , fun run_client_unsub_hook/2 , fun do_unsubscribe/2 ], UnsubPkt, Channel) of {ok, _TopicName, NChannel} -> @@ -841,13 +845,10 @@ check_subscribe_authz({_TopicId, TopicName, _QoS}, {error, ?SN_RC_NOT_AUTHORIZE} end. -do_subscribe({TopicId, TopicName, QoS}, - Channel = #channel{ - ctx = Ctx, - session = Session, - clientinfo = ClientInfo - = #{mountpoint := Mountpoint}}) -> - +run_client_subs_hook({TopicId, TopicName, QoS}, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo}) -> {TopicName1, SubOpts0} = emqx_topic:parse(TopicName), TopicFilters = [{TopicName1, SubOpts0#{qos => QoS}}], case run_hooks(Ctx, 'client.subscribe', @@ -855,19 +856,26 @@ do_subscribe({TopicId, TopicName, QoS}, [] -> ?LOG(warning, "Skip to subscribe ~s, " "due to 'client.subscribe' denied!", [TopicName]), - {ok, Channel}; + {error, ?SN_EXCEED_LIMITATION}; [{NTopicName, NSubOpts}|_] -> - NTopicName1 = emqx_mountpoint:mount(Mountpoint, NTopicName), - NSubOpts1 = maps:merge(?DEFAULT_SUBOPTS, NSubOpts), - case emqx_session:subscribe(ClientInfo, NTopicName1, NSubOpts1, Session) of - {ok, NSession} -> - {ok, {TopicId, QoS}, - Channel#channel{session = NSession}}; - {error, ?RC_QUOTA_EXCEEDED} -> - ?LOG(warning, "Cannot subscribe ~s due to ~s.", - [TopicName, emqx_reason_codes:text(?RC_QUOTA_EXCEEDED)]), - {error, ?SN_EXCEED_LIMITATION} - end + {ok, {TopicId, NTopicName, NSubOpts}, Channel} + end. + +do_subscribe({TopicId, TopicName, SubOpts}, + Channel = #channel{ + session = Session, + clientinfo = ClientInfo + = #{mountpoint := Mountpoint}}) -> + NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), + NSubOpts = maps:merge(?DEFAULT_SUBOPTS, SubOpts), + case emqx_session:subscribe(ClientInfo, NTopicName, NSubOpts, Session) of + {ok, NSession} -> + {ok, {TopicId, NTopicName, NSubOpts}, + Channel#channel{session = NSession}}; + {error, ?RC_QUOTA_EXCEEDED} -> + ?LOG(warning, "Cannot subscribe ~s due to ~s.", + [TopicName, emqx_reason_codes:text(?RC_QUOTA_EXCEEDED)]), + {error, ?SN_EXCEED_LIMITATION} end. %%-------------------------------------------------------------------- @@ -899,33 +907,42 @@ preproc_unsub_type(?SN_UNSUBSCRIBE_MSG_TYPE(?SN_SHORT_TOPIC, end, {ok, TopicName, Channel}. -do_unsubscribe(TopicName, - Channel = #channel{ - ctx = Ctx, - session = Session, - clientinfo = ClientInfo - = #{mountpoint := Mountpoint}}) -> +run_client_unsub_hook(TopicName, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo + }) -> TopicFilters = [emqx_topic:parse(TopicName)], case run_hooks(Ctx, 'client.unsubscribe', [ClientInfo, #{}], TopicFilters) of [] -> - %% Skip to unsubscribe - {ok, Channel}; - [{NTopicName, NSubOpts}|_] -> - NTopicName1 = emqx_mountpoint:mount(Mountpoint, NTopicName), - NSubOpts1 = maps:merge( - emqx_gateway_utils:default_subopts(), - NSubOpts - ), - case emqx_session:unsubscribe(ClientInfo, NTopicName1, - NSubOpts1, Session) of - {ok, NSession} -> - {ok, Channel#channel{session = NSession}}; - {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> - {ok, Channel} - end + {ok, [], Channel}; + NTopicFilters -> + {ok, NTopicFilters, Channel} end. +do_unsubscribe(TopicFilters, + Channel = #channel{ + session = Session, + clientinfo = ClientInfo + = #{mountpoint := Mountpoint}}) -> + NChannel = + lists:foldl(fun({TopicName, SubOpts}, ChannAcc) -> + NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), + NSubOpts = maps:merge( + emqx_gateway_utils:default_subopts(), + SubOpts + ), + case emqx_session:unsubscribe(ClientInfo, NTopicName, + NSubOpts, Session) of + {ok, NSession} -> + ChannAcc#channel{session = NSession}; + {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> + ChannAcc + end + end, Channel, TopicFilters), + {ok, TopicFilters, NChannel}. + %%-------------------------------------------------------------------- %% Awake & Asleep @@ -1101,14 +1118,36 @@ message_to_packet(MsgId, Message, | {shutdown, Reason :: term(), Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}. -handle_call({subscribe, _Topic, _Subopts}, Channel) -> - reply({error, not_supported_now}, Channel); +handle_call({subscribe, Topic, SubOpts}, Channel) -> + %% XXX: Only support short_topic_name + SubProps = maps:get(sub_props, SubOpts, #{}), + case maps:get(subtype, SubProps, short_topic_name) of + short_topic_name -> + case byte_size(Topic) of + 2 -> + case do_subscribe({?SN_INVALID_TOPIC_ID, + Topic, SubOpts}, Channel) of + {ok, _, NChannel} -> + reply(ok, NChannel); + {error, ?SN_EXCEED_LIMITATION} -> + reply({error, exceed_limitation}, Channel) + end; + _ -> + reply({error, bad_topic_name}, Channel) + end; + predefined_topic_id -> + reply({error, only_support_short_name_topic}, Channel); + _ -> + reply({error, only_support_short_name_topic}, Channel) + end; -handle_call({unsubscribe, _Topic}, Channel) -> - reply({error, not_supported_now}, Channel); +handle_call({unsubscribe, Topic}, Channel) -> + TopicFilters = [emqx_topic:parse(Topic)], + {ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel), + reply(ok, NChannel); -handle_call(subscriptions, Channel) -> - reply({error, not_supported_now}, Channel); +handle_call(subscriptions, Channel = #channel{session = Session}) -> + reply(maps:to_list(emqx_session:info(subscriptions, Session)), Channel); handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 1e0c5e2d4..9bd2dac1b 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -22,7 +22,6 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). - -import(proplists, [get_value/2, get_value/3]). %% API @@ -548,9 +547,9 @@ check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, }) -> MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack, _SubOpts} -> + {SubId, MountedTopic, _Ack, _} -> ok; - {SubId, _OtherTopic, _Ack, _SubOpts} -> + {SubId, _OtherTopic, _Ack, _} -> {error, "Conflict subscribe id"}; false -> ok From 304874f0ff21a02153a956968fa65b3670b8e575 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 2 Sep 2021 15:02:38 +0800 Subject: [PATCH 246/306] feat(config): load and merge emqx_override.conf at bootup --- apps/emqx/src/emqx_config.erl | 24 +++++++++++------------- apps/emqx/src/emqx_listeners.erl | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 317aba401..d6c257071 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -235,7 +235,7 @@ put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config). %% in the rear of the list overrides prior values. -spec init_load(module(), [string()] | binary() | hocon:config()) -> ok. init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> - ParseOptions = #{format => richmap}, + ParseOptions = #{format => map}, Parser = case is_binary(Conf) of true -> fun hocon:binary/2; false -> fun hocon:files/2 @@ -249,19 +249,14 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> }), error(failed_to_load_hocon_conf) end; -init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) -> - %% check with richmap for line numbers in error reports (future enhancement) - Opts = #{return_plain => true, - nullable => true - }, - %% this call throws exception in case of check failure - {_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaMod, RawRichConf, Opts), +init_load(SchemaMod, RawConf0) when is_map(RawConf0) -> ok = save_schema_mod_and_names(SchemaMod), - ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(normalize_conf(CheckedConf)), - normalize_conf(hocon_schema:richmap_to_map(RawRichConf))). - -normalize_conf(Conf) -> - maps:with(get_root_names(), Conf). + %% override part of the input conf using emqx_override.conf + RawConf = maps:merge(RawConf0, maps:with(maps:keys(RawConf0), read_override_conf())), + %% check and save configs + {_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConf), + ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf), + maps:with(get_root_names(), RawConf)). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). @@ -320,6 +315,9 @@ get_schema_mod(RootName) -> get_root_names() -> maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). +get_atom_root_names() -> + [atom(N) || N <- get_root_names()]. + -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. save_configs(_AppEnvs, Conf, RawConf, OverrideConf) -> %% We may need also support hot config update for the apps that use application envs. diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 8f0141b3a..a91651c6c 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -50,7 +50,7 @@ %% @doc List configured listeners. -spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]). list() -> - [{listener_id(ZoneName, LName), LConf} || {ZoneName, LName, LConf} <- do_list()]. + [{listener_id(Type, LName), LConf} || {Type, LName, LConf} <- do_list()]. do_list() -> Listeners = maps:to_list(emqx:get_config([listeners], #{})), @@ -64,7 +64,7 @@ list(Type, Conf) -> -spec is_running(ListenerId :: atom()) -> boolean() | {error, no_found}. is_running(ListenerId) -> - case lists:filtermap(fun({_Zone, Id, #{running := IsRunning}}) -> + case lists:filtermap(fun({_Type, Id, #{running := IsRunning}}) -> Id =:= ListenerId andalso {true, IsRunning} end, do_list()) of [IsRunning] -> IsRunning; From daca99f0f693eabab44f5678c25349190ee8773b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 2 Sep 2021 20:19:11 +0800 Subject: [PATCH 247/306] feat(config): add option 'persistent => boolean()' to emqx:update_config/3 --- apps/emqx/src/emqx_config.erl | 12 +++++++++-- apps/emqx/src/emqx_config_handler.erl | 18 +++++++++++----- apps/emqx_authz/src/emqx_authz.erl | 30 +++++++++++++++++---------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index d6c257071..ae41ee1a1 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -87,8 +87,14 @@ -type update_request() :: term(). -type update_cmd() :: {update, update_request()} | remove. -type update_opts() :: #{ - %% fill the default values into the rawconf map - rawconf_with_defaults => boolean() + %% rawconf_with_defaults: + %% fill the default values into the `raw_config` field of the return value + %% defaults to `false` + rawconf_with_defaults => boolean(), + %% persistent: + %% save the updated config to the emqx_override.conf file + %% defaults to `true` + persistent => boolean() }. -type update_args() :: {update_cmd(), Opts :: update_opts()}. -type update_stage() :: pre_config_update | post_config_update. @@ -339,6 +345,8 @@ save_to_config_map(Conf, RawConf) -> ?MODULE:put_raw(RawConf). -spec save_to_override_conf(raw_config()) -> ok | {error, term()}. +save_to_override_conf(undefined) -> + ok; save_to_override_conf(RawConf) -> FileName = emqx_override_conf_name(), ok = filelib:ensure_dir(FileName), diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index f16f8a97a..a9020a87a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -134,17 +134,17 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -process_update_request(ConfKeyPath, _Handlers, {remove, _Opts}) -> +process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) -> OldRawConf = emqx_config:get_root_raw(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath), NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf), - OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()), + OverrideConf = remove_from_override_config(BinKeyPath, Opts), {ok, NewRawConf, OverrideConf}; -process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, _Opts}) -> +process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, Opts}) -> OldRawConf = emqx_config:get_root_raw(ConfKeyPath), case do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) of {ok, NewRawConf} -> - OverrideConf = update_override_config(NewRawConf), + OverrideConf = update_override_config(NewRawConf, Opts), {ok, NewRawConf, OverrideConf}; Error -> Error end. @@ -237,7 +237,15 @@ merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) merge_to_old_config(UpdateReq, _RawConf) -> {ok, UpdateReq}. -update_override_config(RawConf) -> +remove_from_override_config(_BinKeyPath, #{persistent := false}) -> + undefined; +remove_from_override_config(BinKeyPath, _Opts) -> + OldConf = emqx_config:read_override_conf(), + emqx_map_lib:deep_remove(BinKeyPath, OldConf). + +update_override_config(_RawConf, #{persistent := false}) -> + undefined; +update_override_config(RawConf, _Opts) -> OldConf = emqx_config:read_override_conf(), maps:merge(OldConf, RawConf). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 0d116882c..f950ed53c 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -30,7 +30,9 @@ , lookup/0 , lookup/1 , move/2 + , move/3 , update/2 + , update/3 , authorize/5 ]). @@ -58,19 +60,25 @@ lookup(Type) -> error:Reason -> {error, Reason} end. -move(Type, #{<<"before">> := Before}) -> - emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}); -move(Type, #{<<"after">> := After}) -> - emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}); -move(Type, Position) -> - emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}). +move(Type, Cmd) -> + move(Type, Cmd, #{}). + +move(Type, #{<<"before">> := Before}, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}, Opts); +move(Type, #{<<"after">> := After}, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}, Opts); +move(Type, Position, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}, Opts). -update({replace_once, Type}, Sources) -> - emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}); -update({delete_once, Type}, Sources) -> - emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}); update(Cmd, Sources) -> - emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}). + update(Cmd, Sources, #{}). + +update({replace_once, Type}, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}, Opts); +update({delete_once, Type}, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}, Opts); +update(Cmd, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}, Opts). pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_source_by_type(Type), From a89bc97ed82f453f522ba0006b4bacaa242038bd Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 2 Sep 2021 20:20:14 +0800 Subject: [PATCH 248/306] fix(config): don't write to override.conf if 'override_conf_file' is not set --- apps/emqx/src/emqx_config.erl | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index ae41ee1a1..bd6e14e8e 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -258,12 +258,15 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> init_load(SchemaMod, RawConf0) when is_map(RawConf0) -> ok = save_schema_mod_and_names(SchemaMod), %% override part of the input conf using emqx_override.conf - RawConf = maps:merge(RawConf0, maps:with(maps:keys(RawConf0), read_override_conf())), + RawConf = merge_with_override_conf(RawConf0), %% check and save configs {_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConf), ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf), maps:with(get_root_names(), RawConf)). +merge_with_override_conf(RawConf) -> + maps:merge(RawConf, maps:with(maps:keys(RawConf), read_override_conf())). + -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). check_config(SchemaMod, RawConf) -> @@ -348,13 +351,16 @@ save_to_config_map(Conf, RawConf) -> save_to_override_conf(undefined) -> ok; save_to_override_conf(RawConf) -> - FileName = emqx_override_conf_name(), - ok = filelib:ensure_dir(FileName), - case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of - ok -> ok; - {error, Reason} -> - logger:error("write to ~s failed, ~p", [FileName, Reason]), - {error, Reason} + case emqx_override_conf_name() of + undefined -> ok; + FileName -> + ok = filelib:ensure_dir(FileName), + case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of + ok -> ok; + {error, Reason} -> + logger:error("write to ~s failed, ~p", [FileName, Reason]), + {error, Reason} + end end. load_hocon_file(FileName, LoadType) -> @@ -366,7 +372,7 @@ load_hocon_file(FileName, LoadType) -> end. emqx_override_conf_name() -> - application:get_env(emqx, override_conf_file, "emqx_override.conf"). + application:get_env(emqx, override_conf_file, undefined). do_get(Type, KeyPath) -> Ref = make_ref(), From f4eae8c0cb1765e5032c42dd669455a15808d0b4 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 3 Sep 2021 08:59:00 +0800 Subject: [PATCH 249/306] fix(retainer): test case failed for expired retained msg --- ..._protocol_v5_SUITE.erl => emqx_retainer_mqtt_v5_SUITE.erl} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/emqx_retainer/test/{mqtt_protocol_v5_SUITE.erl => emqx_retainer_mqtt_v5_SUITE.erl} (99%) diff --git a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl similarity index 99% rename from apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl rename to apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl index cba40de69..0be5df732 100644 --- a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(mqtt_protocol_v5_SUITE). +-module(emqx_retainer_mqtt_v5_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -117,7 +117,7 @@ t_publish_message_expiry_interval(_) -> {ok, _} = emqtt:publish(Client1, <<"topic/B">>, #{'Message-Expiry-Interval' => 1}, <<"retained message">>, [{qos, 2}, {retain, true}]), {ok, _} = emqtt:publish(Client1, <<"topic/C">>, #{'Message-Expiry-Interval' => 10}, <<"retained message">>, [{qos, 1}, {retain, true}]), {ok, _} = emqtt:publish(Client1, <<"topic/D">>, #{'Message-Expiry-Interval' => 10}, <<"retained message">>, [{qos, 2}, {retain, true}]), - timer:sleep(1000), + timer:sleep(1500), {ok, _, [2]} = emqtt:subscribe(Client1, <<"topic/+">>, 2), Msgs = receive_messages(4), ?assertEqual(2, length(Msgs)), %% [MQTT-3.3.2-5] From be0fd6fddd58597935e1aa97280fa2edac4c93f6 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 10:57:54 +0800 Subject: [PATCH 250/306] fix: add sub api doc & test suite (#5634) * fix: add sub api doc & test suite --- .../src/emqx_mgmt_api_subscriptions.erl | 4 ++-- .../test/emqx_mgmt_subscription_api_SUITE.erl | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 5c2475e95..3afd52050 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -96,13 +96,13 @@ parameters() -> #{ name => topic, in => query, - description => <<"Topic">>, + description => <<"Topic, url encoding">>, schema => #{type => string} } #{ name => match_topic, in => query, - description => <<"Match topic string">>, + description => <<"Match topic string, url encoding">>, schema => #{type => string} } | page_params() ]. diff --git a/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl index f2d8c6eb2..b344bcd11 100644 --- a/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl @@ -24,8 +24,10 @@ -define(USERNAME, <<"api_username">>). %% notice: integer topic for sort response --define(TOPIC1, <<"0000">>). --define(TOPIC2, <<"0001">>). +-define(TOPIC1, <<"/t/0000">>). +-define(TOPIC2, <<"/t/0001">>). + +-define(TOPIC_SORT, #{?TOPIC1 => 1, ?TOPIC2 => 2}). all() -> emqx_ct:all(?MODULE). @@ -53,11 +55,24 @@ t_subscription_api(_) -> ?assertEqual(length(Subscriptions), 2), Sort = fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) -> - binary_to_integer(T1) =< binary_to_integer(T2) + maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT) end, [Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions), ?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1), ?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2), ?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID), ?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID), + + QsTopic = "topic=" ++ <<"%2Ft%2F0001">>, + Headers = emqx_mgmt_api_test_util:auth_header_(), + {ok, ResponseTopic1} = emqx_mgmt_api_test_util:request_api(get, Path, QsTopic, Headers), + DataTopic1 = emqx_json:decode(ResponseTopic1, [return_maps]), + Meta1 = maps:get(<<"meta">>, DataTopic1), + ?assertEqual(1, maps:get(<<"page">>, Meta1)), + ?assertEqual(emqx_mgmt:max_row_limit(), maps:get(<<"limit">>, Meta1)), + ?assertEqual(1, maps:get(<<"count">>, Meta1)), + Subscriptions_qs1 = maps:get(<<"data">>, DataTopic1), + ?assertEqual(length(Subscriptions_qs1), 1), + + emqtt:disconnect(Client). From 60b821536059135543f47ce70f121f41a0f445a8 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 3 Sep 2021 11:40:11 +0800 Subject: [PATCH 251/306] feat(config): support wildcard paths for config handlers --- apps/emqx/src/emqx_config_handler.erl | 51 +++++++++++++++++++++------ apps/emqx/src/emqx_map_lib.erl | 8 ++--- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index a9020a87a..f64ffabcb 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -39,6 +39,7 @@ code_change/3]). -define(MOD, {mod}). +-define(WKEY, '?'). -define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL), try [safe_atom(Key) || Key <- PATH] of @@ -80,11 +81,11 @@ update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> -spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok. add_handler(ConfKeyPath, HandlerName) -> - gen_server:call(?MODULE, {add_child, ConfKeyPath, HandlerName}). + gen_server:call(?MODULE, {add_handler, ConfKeyPath, HandlerName}). -spec remove_handler(emqx_config:config_key_path()) -> ok. remove_handler(ConfKeyPath) -> - gen_server:call(?MODULE, {remove_child, ConfKeyPath}). + gen_server:call(?MODULE, {remove_handler, ConfKeyPath}). %%============================================================================ @@ -92,15 +93,18 @@ remove_handler(ConfKeyPath) -> init(_) -> {ok, #{handlers => #{?MOD => ?MODULE}}}. -handle_call({add_child, ConfKeyPath, HandlerName}, _From, - State = #{handlers := Handlers}) -> - {reply, ok, State#{handlers => - emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; +handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) -> + case deep_put_handler(ConfKeyPath, Handlers, HandlerName) of + {ok, NewHandlers} -> + {reply, ok, State#{handlers => NewHandlers}}; + Error -> + {reply, Error, State} + end; -handle_call({remove_child, ConfKeyPath}, _From, +handle_call({remove_handler, ConfKeyPath}, _From, State = #{handlers := Handlers}) -> {reply, ok, State#{handlers => - emqx_map_lib:deep_remove(ConfKeyPath, Handlers)}}; + emqx_map_lib:deep_remove(ConfKeyPath ++ [?MOD], Handlers)}}; handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> @@ -134,6 +138,27 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +deep_put_handler([], _Handlers, Mod) -> + {ok, #{?MOD => Mod}}; +deep_put_handler([?WKEY | KeyPath], Handlers, Mod) -> + deep_put_handler2(?WKEY, KeyPath, Handlers, Mod); +deep_put_handler([Key | KeyPath], Handlers, Mod) -> + case maps:find(?WKEY, Handlers) of + error -> + deep_put_handler2(Key, KeyPath, Handlers, Mod); + {ok, _SubHandlers} -> + {error, {cannot_override_a_wildcard_path, [?WKEY | KeyPath]}} + end. + +deep_put_handler2(Key, KeyPath, Handlers, Mod) -> + SubHandlers = maps:get(Key, Handlers, #{}), + case deep_put_handler(KeyPath, SubHandlers, Mod) of + {ok, SubHandlers1} -> + {ok, Handlers#{Key => SubHandlers1}}; + Error -> + Error + end. + process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) -> OldRawConf = emqx_config:get_root_raw(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath), @@ -153,7 +178,7 @@ do_update_config([], Handlers, OldRawConf, UpdateReq) -> call_pre_config_update(Handlers, OldRawConf, UpdateReq); do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) -> SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf), - SubHandlers = maps:get(ConfKey, Handlers, #{}), + SubHandlers = get_sub_handlers(ConfKey, Handlers), case do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq) of {ok, NewUpdateReq} -> call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}); @@ -184,7 +209,7 @@ do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEn Result) -> SubOldConf = get_sub_config(ConfKey, OldConf), SubNewConf = get_sub_config(ConfKey, NewConf), - SubHandlers = maps:get(ConfKey, Handlers, #{}), + SubHandlers = get_sub_handlers(ConfKey, Handlers), case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, AppEnvs, UpdateArgs, Result) of {ok, Result1} -> @@ -193,6 +218,12 @@ do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEn Error -> Error end. +get_sub_handlers(ConfKey, Handlers) -> + case maps:find(ConfKey, Handlers) of + error -> maps:get(?WKEY, Handlers, #{}); + {ok, SubHandlers} -> SubHandlers + end. + get_sub_config(ConfKey, Conf) when is_map(Conf) -> maps:get(ConfKey, Conf, undefined); get_sub_config(_, _Conf) -> %% the Conf is a primitive diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index d5e851971..6aa6606c0 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -65,13 +65,11 @@ deep_find(_KeyPath, Data) -> {not_found, _KeyPath, Data}. -spec deep_put(config_key_path(), map(), term()) -> map(). -deep_put([], Map, Data) when is_map(Map) -> - Data; -deep_put([], _Map, Data) -> %% not map, replace it +deep_put([], _Map, Data) -> Data; deep_put([Key | KeyPath], Map, Data) -> - SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Data), - Map#{Key => SubMap}. + SubMap = maps:get(Key, Map, #{}), + Map#{Key => deep_put(KeyPath, SubMap, Data)}. -spec deep_remove(config_key_path(), map()) -> map(). deep_remove([], Map) -> From 5f94b8e1ffc32e6ebe3d57e5418428f3027a72ee Mon Sep 17 00:00:00 2001 From: xiangfangyang-tech <62098177+xiangfangyang-tech@users.noreply.github.com> Date: Fri, 3 Sep 2021 15:05:30 +0800 Subject: [PATCH 252/306] chore(CI): add api test workflows for github actions (#5640) --- .github/workflows/run_api_tests.yaml | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/run_api_tests.yaml diff --git a/.github/workflows/run_api_tests.yaml b/.github/workflows/run_api_tests.yaml new file mode 100644 index 000000000..618b9383a --- /dev/null +++ b/.github/workflows/run_api_tests.yaml @@ -0,0 +1,102 @@ +name: API Test Suite + +on: + push: + tags: + - e* + - v* + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + container: "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + steps: + - uses: actions/checkout@v2 + - name: zip emqx-broker + if: endsWith(github.repository, 'emqx') + run: | + make emqx-zip + - name: zip emqx-broker + if: endsWith(github.repository, 'enterprise') + run: | + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + make emqx-ee-zip + - uses: actions/upload-artifact@v2 + with: + name: emqx-broker + path: _packages/**/*.zip + api-test: + needs: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + script_name: + - api_metrics + - api_subscriptions + steps: + - uses: actions/checkout@v2 + with: + repository: emqx/emqx-fvt + path: . + - uses: actions/setup-java@v1 + with: + java-version: '8.0.282' # The JDK version to make available on the path. + java-package: jdk # (jre, jdk, or jdk+fx) - defaults to jdk + architecture: x64 # (x64 or x86) - defaults to x64 + - uses: actions/download-artifact@v2 + with: + name: emqx-broker + path: . + - name: start emqx-broker + env: + EMQX_LISTENERS__WSS__DEFAULT__BIND: "0.0.0.0:8085" + run: | + unzip ./emqx/*.zip + ./emqx/bin/emqx start + - name: install jmeter + timeout-minutes: 10 + env: + JMETER_VERSION: 5.3 + run: | + wget --no-verbose --no-check-certificate -O /tmp/apache-jmeter.tgz https://downloads.apache.org/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz + cd /tmp && tar -xvf apache-jmeter.tgz + echo "jmeter.save.saveservice.output_format=xml" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties + echo "jmeter.save.saveservice.response_data.on_error=true" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties + wget --no-verbose -O /tmp/apache-jmeter-$JMETER_VERSION/lib/ext/mqtt-xmeter-2.0.2-jar-with-dependencies.jar https://raw.githubusercontent.com/xmeter-net/mqtt-jmeter/master/Download/v2.0.2/mqtt-xmeter-2.0.2-jar-with-dependencies.jar + ln -s /tmp/apache-jmeter-$JMETER_VERSION /opt/jmeter + - name: run ${{ matrix.script_name }} + run: | + /opt/jmeter/bin/jmeter.sh \ + -Jjmeter.save.saveservice.output_format=xml -n \ + -t .ci/api-test-suite/${{ matrix.script_name }}.jmx \ + -Demqx_ip="127.0.0.1" \ + -l jmeter_logs/${{ matrix.script_name }}.jtl \ + -j jmeter_logs/logs/${{ matrix.script_name }}.log + - name: check test logs + run: | + if cat jmeter_logs/${{ matrix.script_name }}.jtl | grep -e 'true' > /dev/null 2>&1; then + grep -A 5 -B 3 'true' jmeter_logs/${{ matrix.script_name }}.jtl > jmeter_logs/${{ matrix.script_name }}_err_api.txt + echo "check logs failed" + exit 1 + fi + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: jmeter_logs + path: ./jmeter_logs + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: jmeter_logs + path: emqx/log + delete-package: + runs-on: ubuntu-20.04 + needs: api-test + if: always() + steps: + - uses: geekyeggo/delete-artifact@v1 + with: + name: emqx-broker From cff15dfc44095a9e23b3dfd53009f5031814ddca Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 3 Sep 2021 14:26:32 +0800 Subject: [PATCH 253/306] chore(CI): fix env error for test wrokflows --- .ci/build_packages/tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 240d6214e..5d6422231 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -128,7 +128,7 @@ export EMQX_LOG__FILE_HANDLERS__DEFAULT__LEVEL=debug EOF ## for ARM, due to CI env issue, skip start of quic listener for the moment [[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" < Date: Fri, 3 Sep 2021 13:54:32 +0800 Subject: [PATCH 254/306] feat(authz): check for duplicate source types Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 62 +++++++++++++++---- .../test/emqx_authz_api_sources_SUITE.erl | 25 +++----- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f950ed53c..7fcd80269 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -39,6 +39,7 @@ -export([post_config_update/4, pre_config_update/2]). -define(CONF_KEY_PATH, [authorization, sources]). +-define(SOURCE_TYPES, [file, http, mongo, mysql, pgsql, redis]). -spec(register_metrics() -> ok). register_metrics() -> @@ -47,7 +48,9 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NSources = [init_source(Source) || Source <- emqx:get_config(?CONF_KEY_PATH, [])], + Sources = emqx:get_config(?CONF_KEY_PATH, []), + ok = check_dup_types(Sources), + NSources = [init_source(Source) || Source <- Sources], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1). lookup() -> @@ -83,12 +86,16 @@ update(Cmd, Sources, Opts) -> pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - {ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2}; + NConf = [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2, + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) -> {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - {ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]}; + NConf = lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)], + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) -> {Index1, _} = find_source_by_type(Type), @@ -97,9 +104,11 @@ pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Co Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), - {ok, lists:delete(Conf1, lists:droplast(List1)) - ++ [Conf1] ++ [Conf2] - ++ lists:delete(Conf1, List2)}; + NConf = lists:delete(Conf1, lists:droplast(List1)) + ++ [Conf1] ++ [Conf2] + ++ lists:delete(Conf1, List2), + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) -> {Index1, _} = find_source_by_type(Type), @@ -107,21 +116,31 @@ pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf {Index2, _} = find_source_by_type(After), {List1, List2} = lists:split(Index2, Conf), - {ok, lists:delete(Conf1, List1) - ++ [Conf1] - ++ lists:delete(Conf1, List2)}; + NConf = lists:delete(Conf1, List1) + ++ [Conf1] + ++ lists:delete(Conf1, List2), + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + NConf = Sources ++ Conf, + ok = check_dup_types(NConf), {ok, Sources ++ Conf}; pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + NConf = Conf ++ Sources, + ok = check_dup_types(NConf), {ok, Conf ++ Sources}; pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) -> {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - {ok, lists:droplast(List1) ++ [Source] ++ List2}; + NConf = lists:droplast(List1) ++ [Source] ++ List2, + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) -> {_, Source} = find_source_by_type(Type), - {ok, lists:delete(Source, Conf)}; + NConf = lists:delete(Source, Conf), + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({_, Sources}, _Conf) when is_list(Sources)-> %% overwrite the entire config! {ok, Sources}. @@ -212,6 +231,27 @@ post_config_update(_, NewSources, _OldConf, _AppEnvs) -> %% Initialize source %%-------------------------------------------------------------------- +check_dup_types(Sources) -> + check_dup_types(Sources, ?SOURCE_TYPES). +check_dup_types(_Sources, []) -> ok; +check_dup_types(Sources, [T0 | Tail]) -> + case lists:foldl(fun (#{type := T1}, AccIn) -> + case T0 =:= T1 of + true -> AccIn + 1; + false -> AccIn + end; + (#{<<"type">> := T1}, AccIn) -> + case T0 =:= atom(T1) of + true -> AccIn + 1; + false -> AccIn + end + end, 0, Sources) > 1 of + true -> + ?LOG(error, "The type is duplicated in the Authorization source"), + {error, authz_source_dup}; + false -> check_dup_types(Sources, Tail) + end. + init_source(#{enable := true, type := file, path := Path diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 3c054aa7d..4dc21647a 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -178,16 +178,11 @@ t_api(_) -> {ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []), ?assertEqual([], get_sources(Result1)), - lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1) - end, lists:seq(1, 20)), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), + {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1), + {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual(20, length(get_sources(Result2))), - - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), - - {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), - Sources = get_sources(Result3), + Sources = get_sources(Result2), ?assertMatch([ #{<<"type">> := <<"http">>} , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} @@ -198,8 +193,8 @@ t_api(_) -> ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), - ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), + {ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []), + ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)), {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), ?SOURCE2#{<<"ssl">> := #{ @@ -209,7 +204,7 @@ t_api(_) -> <<"keyfile">> => <<"fake key file">>, <<"verify">> => false }}), - {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), + {ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongo"]), []), ?assertMatch(#{<<"type">> := <<"mongo">>, <<"ssl">> := #{<<"enable">> := true, <<"cacertfile">> := <<"fake cacert file">>, @@ -217,7 +212,7 @@ t_api(_) -> <<"keyfile">> := <<"fake key file">>, <<"verify">> := false } - }, jsx:decode(Result5)), + }, jsx:decode(Result4)), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))), @@ -225,8 +220,8 @@ t_api(_) -> lists:foreach(fun(#{<<"type">> := Type}) -> {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual([], get_sources(Result6)), + {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result5)), ok. t_move_source(_) -> From 07821b9574bb167a4f244a22d8e23c9adef6650e Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:07:34 +0800 Subject: [PATCH 255/306] fix: cli error & routes api doc (#5639) --- apps/emqx_management/src/emqx_mgmt_api_routes.erl | 2 +- apps/emqx_management/src/emqx_mgmt_cli.erl | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 6c74105c0..19f42427e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -64,7 +64,7 @@ route_api() -> name => topic, in => path, required => true, - description => <<"topic">>, + description => <<"Topic string, url encoding">>, schema => #{type => string} }], responses => #{ diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index c7fb8a7b7..abe64d886 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -73,8 +73,9 @@ status(_) -> %% @doc Query broker broker([]) -> - Funs = [sysdescr, version, uptime, datetime], - [emqx_ctl:print("~-10s: ~s~n", [Fun, emqx_sys:Fun()]) || Fun <- Funs]; + Funs = [sysdescr, version, datetime], + [emqx_ctl:print("~-10s: ~s~n", [Fun, emqx_sys:Fun()]) || Fun <- Funs], + emqx_ctl:print("~-10s: ~p~n", [uptime, emqx_sys:uptime()]); broker(["stats"]) -> [emqx_ctl:print("~-30s: ~w~n", [Stat, Val]) || {Stat, Val} <- lists:sort(emqx_stats:getstats())]; @@ -286,7 +287,7 @@ vm(["io"]) -> [emqx_ctl:print("io/~-21s: ~w~n", [Key, proplists:get_value(Key, IoInfo)]) || Key <- [max_fds, active_fds]]; vm(["ports"]) -> - [emqx_ctl:print("ports/~-16s: ~w~n", [Name, erlang:system_info(Key)]) || {Name, Key} <- [{count, port_count}, {limit, port_limit}]]; + [emqx_ctl:print("ports/~-18s: ~w~n", [Name, erlang:system_info(Key)]) || {Name, Key} <- [{count, port_count}, {limit, port_limit}]]; vm(_) -> emqx_ctl:usage([{"vm all", "Show info of Erlang VM"}, From 8f6931e5b06732a5eb24180817d09347598c6a9d Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:07:44 +0800 Subject: [PATCH 256/306] feat: update dashboard ui beta10 (#5644) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 52b0ea209..fabb8a7df 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.9 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.10 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From ec13463f4a5a33970d00f7a2e9e7c79082c91791 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 31 Aug 2021 22:05:31 +0200 Subject: [PATCH 257/306] refactor(schema): prepare for hocon schema doc generation --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_schema.erl | 1023 +++++++++++++---- apps/emqx/src/emqx_zone_schema.erl | 34 + apps/emqx_authn/src/emqx_authn_schema.erl | 5 +- .../emqx_enhanced_authn_scram_mnesia.erl | 5 +- .../src/simple_authn/emqx_authn_http.erl | 9 +- .../src/simple_authn/emqx_authn_jwt.erl | 11 +- .../src/simple_authn/emqx_authn_mnesia.erl | 7 +- .../src/simple_authn/emqx_authn_mongodb.erl | 11 +- .../src/simple_authn/emqx_authn_mysql.erl | 5 +- .../src/simple_authn/emqx_authn_pgsql.erl | 4 +- .../src/simple_authn/emqx_authn_redis.erl | 10 +- apps/emqx_authz/src/emqx_authz_schema.erl | 7 +- .../src/emqx_auto_subscribe_schema.erl | 23 +- .../src/emqx_bridge_mqtt_schema.erl | 55 +- .../src/emqx_connector_mongo.erl | 7 +- .../src/emqx_dashboard_schema.erl | 20 +- .../src/emqx_data_bridge_schema.erl | 2 +- apps/emqx_exhook/src/emqx_exhook_schema.erl | 60 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 229 ++-- apps/emqx_machine/src/emqx_machine_schema.erl | 546 ++++++--- .../src/emqx_management_schema.erl | 5 +- apps/emqx_modules/src/emqx_modules_schema.erl | 37 +- .../src/emqx_prometheus_schema.erl | 13 +- .../src/emqx_retainer_schema.erl | 30 +- .../src/emqx_rule_engine_schema.erl | 11 +- .../emqx_rule_engine/src/emqx_rule_events.erl | 3 +- apps/emqx_statsd/src/emqx_statsd_schema.erl | 7 +- rebar.config | 2 +- 29 files changed, 1537 insertions(+), 646 deletions(-) create mode 100644 apps/emqx/src/emqx_zone_schema.erl diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 271558f6d..bb3a588a9 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -15,7 +15,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {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.14.1"}}} diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 812e52f9b..fe4439aaa 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -49,6 +49,10 @@ -typerefl_from_string({cipher/0, emqx_schema, to_erl_cipher_suite}). -typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). +-export([ validate_heap_size/1 + , parse_user_lookup_fun/1 + ]). + % 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, @@ -65,204 +69,539 @@ cipher/0, comma_separated_atoms/0]). --export([roots/0, fields/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0, fields/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([ssl/1]). +namespace() -> undefined. + roots() -> - ["zones", "mqtt", "flapping_detect", "force_shutdown", "force_gc", - "conn_congestion", "rate_limit", "quota", "listeners", "broker", "plugins", - "stats", "sysmon", "alarm", "authorization"]. + ["zones", + "mqtt", + "flapping_detect", + "force_shutdown", + "force_gc", + "conn_congestion", + "rate_limit", + "quota", + {"listeners", + sc(ref("listeners"), + #{ desc => "MQTT listeners identified by their protocol type and assigned names. " + "The listeners enabled by default are named with 'default'"}) + }, + "broker", + "plugins", + "stats", + "sysmon", + "alarm" + ]. fields("stats") -> - [ {"enable", t(boolean(), undefined, true)} + [ {"enable", + sc(boolean(), + #{ default => true + })} ]; fields("authorization") -> - [ {"no_match", t(union(allow, deny), undefined, allow)} - , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} - , {"cache", ref("authorization_cache")} + [ {"no_match", + sc(union(allow, deny), + #{ default => allow + })} + , {"deny_action", + sc(union(ignore, disconnect), + #{ default => ignore + })} + , {"cache", + sc(ref("authorization_cache"), + #{ + }) + } ]; fields("authorization_cache") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_size", t(range(1, 1048576), undefined, 32)} - , {"ttl", t(duration(), undefined, "1m")} + [ {"enable", + sc(boolean(), + #{ default => true + }) + } + , {"max_size", + sc(range(1, 1048576), + #{ default => 32 + }) + } + , {"ttl", + sc(duration(), + #{ default => "1m" + }) + } ]; fields("mqtt") -> - [ {"idle_timeout", maybe_infinity(duration(), "15s")} - , {"max_packet_size", t(bytesize(), undefined, "1MB")} - , {"max_clientid_len", t(range(23, 65535), undefined, 65535)} - , {"max_topic_levels", t(range(1, 65535), undefined, 65535)} - , {"max_qos_allowed", t(range(0, 2), undefined, 2)} - , {"max_topic_alias", t(range(0, 65535), 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(), undefined, false)} - , {"strict_mode", t(boolean(), undefined, false)} - , {"response_information", t(string(), undefined, "")} - , {"server_keepalive", maybe_disabled(integer())} - , {"keepalive_backoff", t(float(), undefined, 0.75)} - , {"max_subscriptions", maybe_infinity(range(1, inf))} - , {"upgrade_qos", t(boolean(), undefined, false)} - , {"max_inflight", t(range(1, 65535), undefined, 32)} - , {"retry_interval", t(duration(), undefined, "30s")} - , {"max_awaiting_rel", maybe_infinity(integer(), 100)} - , {"await_rel_timeout", t(duration(), undefined, "300s")} - , {"session_expiry_interval", t(duration(), undefined, "2h")} - , {"max_mqueue_len", maybe_infinity(range(0, inf), 1000)} - , {"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)} - , {"peer_cert_as_username", maybe_disabled(union([cn, dn, crt, pem, md5]))} - , {"peer_cert_as_clientid", maybe_disabled(union([cn, dn, crt, pem, md5]))} + [ {"idle_timeout", + sc(hoconsc:union([infinity, duration()]), + #{ default => "15s" + })} + , {"max_packet_size", + sc(bytesize(), + #{ default => "1MB" + })} + , {"max_clientid_len", + sc(range(23, 65535), + #{ default => 65535 + })} + , {"max_topic_levels", + sc(range(1, 65535), + #{ default => 65535 + })} + , {"max_qos_allowed", + sc(range(0, 2), + #{ default => 2 + })} + , {"max_topic_alias", + sc(range(0, 65535), + #{ default => 65535 + })} + , {"retain_available", + sc(boolean(), + #{ default => true + })} + , {"wildcard_subscription", + sc(boolean(), + #{ default => true + })} + , {"shared_subscription", + sc(boolean(), + #{ default => true + })} + , {"ignore_loop_deliver", + sc(boolean(), + #{ default => false + })} + , {"strict_mode", + sc(boolean(), + #{default => false + }) + } + , {"response_information", + sc(string(), + #{default => "" + }) + } + , {"server_keepalive", + sc(hoconsc:union([integer(), disabled]), + #{ default => disabled + }) + } + , {"keepalive_backoff", + sc(float(), + #{default => 0.75 + }) + } + , {"max_subscriptions", + sc(hoconsc:union([range(1, inf), infinity]), + #{ default => infinity + }) + } + , {"upgrade_qos", + sc(boolean(), + #{ default => false + }) + } + , {"max_inflight", + sc(range(1, 65535), + #{ default => 32 + }) + } + , {"retry_interval", + sc(duration(), + #{default => "30s" + }) + } + , {"max_awaiting_rel", + sc(hoconsc:union([integer(), infinity]), + #{ default => 100 + }) + } + , {"await_rel_timeout", + sc(duration(), + #{ default => "300s" + }) + } + , {"session_expiry_interval", + sc(duration(), + #{ default => "2h" + }) + } + , {"max_mqueue_len", + sc(hoconsc:union([range(0, inf), infinity]), + #{ default => 1000 + }) + } + , {"mqueue_priorities", + sc(hoconsc:union([map(), disabled]), + #{ default => disabled + }) + } + , {"mqueue_default_priority", + sc(union(highest, lowest), + #{ default => lowest + }) + } + , {"mqueue_store_qos0", + sc(boolean(), + #{ default => true + }) + } + , {"use_username_as_clientid", + sc(boolean(), + #{ default => false + }) + } + , {"peer_cert_as_username", + sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]), + #{ default => disabled + })} + , {"peer_cert_as_clientid", + sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]), + #{ default => disabled + })} ]; fields("zones") -> - [ {"$name", ref("zone_settings")}]; + [ {"$name", + sc(ref("zone_settings"), + #{ + } + )}]; fields("zone_settings") -> Fields = ["mqtt", "stats", "authorization", "flapping_detect", "force_shutdown", "conn_congestion", "rate_limit", "quota", "force_gc"], - [{F, ref("strip_default:" ++ F)} || F <- Fields]; + [{F, ref(emqx_zone_schema, F)} || F <- Fields]; fields("rate_limit") -> - [ {"max_conn_rate", maybe_infinity(integer(), 1000)} - , {"conn_messages_in", maybe_infinity(comma_separated_list())} - , {"conn_bytes_in", maybe_infinity(comma_separated_list())} + [ {"max_conn_rate", + sc(hoconsc:union([infinity, integer()]), + #{ default => 1000 + }) + } + , {"conn_messages_in", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } + , {"conn_bytes_in", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } ]; fields("quota") -> - [ {"conn_messages_routing", maybe_infinity(comma_separated_list())} - , {"overall_messages_routing", maybe_infinity(comma_separated_list())} + [ {"conn_messages_routing", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } + , {"overall_messages_routing", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } ]; fields("flapping_detect") -> - [ {"enable", t(boolean(), undefined, false)} - , {"max_count", t(integer(), undefined, 15)} - , {"window_time", t(duration(), undefined, "1m")} - , {"ban_time", t(duration(), undefined, "5m")} + [ {"enable", + sc(boolean(), + #{ default => false + })} + , {"max_count", + sc(integer(), + #{ default => 15 + })} + , {"window_time", + sc(duration(), + #{ default => "1m" + })} + , {"ban_time", + sc(duration(), + #{ default => "5m" + })} ]; 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)} + [ {"enable", + sc(boolean(), + #{ default => true})} + , {"max_message_queue_len", + sc(range(0, inf), + #{ default => 1000 + })} + , {"max_heap_size", + sc(wordsize(), + #{ default => "32MB", + validator => fun ?MODULE:validate_heap_size/1 + })} ]; fields("conn_congestion") -> - [ {"enable_alarm", t(boolean(), undefined, false)} - , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} + [ {"enable_alarm", + sc(boolean(), + #{ default => false + })} + , {"min_alarm_sustain_duration", + sc(duration(), + #{ default => "1m" + })} ]; fields("force_gc") -> - [ {"enable", t(boolean(), undefined, true)} - , {"count", t(range(0, inf), undefined, 16000)} - , {"bytes", t(bytesize(), undefined, "16MB")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"count", + sc(range(0, inf), + #{ default => 16000 + })} + , {"bytes", + sc(bytesize(), + #{ default => "16MB" + })} ]; fields("listeners") -> - [ {"tcp", ref("t_tcp_listeners")} - , {"ssl", ref("t_ssl_listeners")} - , {"ws", ref("t_ws_listeners")} - , {"wss", ref("t_wss_listeners")} - , {"quic", ref("t_quic_listeners")} + [ {"tcp", + sc(ref("tcp_listeners"), + #{ desc => "TCP listeners" + }) + } + , {"ssl", + sc(ref("ssl_listeners"), + #{ desc => "SSL listeners" + }) + } + , {"ws", + sc(ref("ws_listeners"), + #{ desc => "HTTP websocket listeners" + }) + } + , {"wss", + sc(ref("wss_listeners"), + #{ desc => "HTTPS websocket listeners" + }) + } + , {"quic", + sc(ref("quic_listeners"), + #{ desc => "QUIC listeners" + }) + } ]; -fields("t_tcp_listeners") -> +fields("tcp_listeners") -> [ {"$name", ref("mqtt_tcp_listener")} ]; -fields("t_ssl_listeners") -> +fields("ssl_listeners") -> [ {"$name", ref("mqtt_ssl_listener")} ]; -fields("t_ws_listeners") -> +fields("ws_listeners") -> [ {"$name", ref("mqtt_ws_listener")} ]; -fields("t_wss_listeners") -> +fields("wss_listeners") -> [ {"$name", ref("mqtt_wss_listener")} ]; -fields("t_quic_listeners") -> +fields("quic_listeners") -> [ {"$name", ref("mqtt_quic_listener")} ]; fields("mqtt_tcp_listener") -> - [ {"tcp", ref("tcp_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{ desc => "TCP listener options" + }) + } ] ++ mqtt_listener(); fields("mqtt_ssl_listener") -> - [ {"tcp", ref("tcp_opts")} - , {"ssl", ref("ssl_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"ssl", + sc(ref("listener_ssl_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_ws_listener") -> - [ {"tcp", ref("tcp_opts")} - , {"websocket", ref("ws_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"websocket", + sc(ref("ws_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_wss_listener") -> - [ {"tcp", ref("tcp_opts")} - , {"ssl", ref("ssl_opts")} - , {"websocket", ref("ws_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"ssl", + sc(ref("listener_ssl_opts"), + #{}) + } + , {"websocket", + sc(ref("ws_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_quic_listener") -> - [ {"enabled", t(boolean(), undefined, true)} - , {"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, "15s")} + [ {"enabled", + sc(boolean(), + #{ default => true + }) + } + , {"certfile", + sc(string(), + #{}) + } + , {"keyfile", + sc(string(), + #{}) + } + , {"ciphers", + sc(comma_separated_list(), + #{ default => "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256," + "TLS_CHACHA20_POLY1305_SHA256" + })} + , {"idle_timeout", + sc(duration(), + #{ default => "15s" + }) + } ] ++ base_listener(); fields("ws_opts") -> - [ {"mqtt_path", t(string(), undefined, "/mqtt")} - , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} - , {"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(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)} - , {"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")} + [ {"mqtt_path", + sc(string(), + #{ default => "/mqtt" + }) + } + , {"mqtt_piggyback", + sc(hoconsc:union([single, multiple]), + #{ default => multiple + }) + } + , {"compress", + sc(boolean(), + #{ default => false + }) + } + , {"idle_timeout", + sc(duration(), + #{ default => "15s" + }) + } + , {"max_frame_size", + sc(hoconsc:union([infinity, integer()]), + #{ default => infinity + }) + } + , {"fail_if_no_subprotocol", + sc(boolean(), + #{ default => true + }) + } + , {"supported_subprotocols", + sc(comma_separated_list(), + #{ default => "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + }) + } + , {"check_origin_enable", + sc(boolean(), + #{ default => false + }) + } + , {"allow_origin_absence", + sc(boolean(), + #{ default => true + }) + } + , {"check_origins", + sc(hoconsc:array(binary()), + #{ default => [] + }) + } + , {"proxy_address_header", + sc(string(), + #{ default => "x-forwarded-for" + }) + } + , {"proxy_port_header", + sc(string(), + #{ default => "x-forwarded-port" + }) + } + , {"deflate_opts", + sc(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(boolean(), undefined, true)} - , {"recbuf", t(bytesize())} - , {"sndbuf", t(bytesize())} - , {"buffer", t(bytesize())} - , {"high_watermark", t(bytesize(), undefined, "1MB")} - , {"nodelay", t(boolean(), undefined, false)} - , {"reuseaddr", t(boolean(), undefined, true)} + [ {"active_n", + sc(integer(), + #{ default => 100 + }) + } + , {"backlog", + sc(integer(), + #{ default => 1024 + }) + } + , {"send_timeout", + sc(duration(), + #{ default => "15s" + }) + } + , {"send_timeout_close", + sc(boolean(), + #{ default => true + }) + } + , {"recbuf", + sc(bytesize(), + #{}) + } + , {"sndbuf", + sc(bytesize(), + #{}) + } + , {"buffer", + sc(bytesize(), + #{}) + } + , {"high_watermark", + sc(bytesize(), + #{ default => "1MB"}) + } + , {"nodelay", + sc(boolean(), + #{ default => false}) + } + , {"reuseaddr", + sc(boolean(), + #{ default => true + }) + } ]; -fields("ssl_opts") -> +fields("listener_ssl_opts") -> ssl(#{handshake_timeout => "15s" , depth => 10 , reuse_sessions => true @@ -271,82 +610,237 @@ fields("ssl_opts") -> }); fields("deflate_opts") -> - [ {"level", t(union([none, default, best_compression, best_speed]))} - , {"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(range(8, 15), undefined, 15)} - , {"client_max_window_bits", t(range(8, 15), undefined, 15)} + [ {"level", + sc(hoconsc:union([none, default, best_compression, best_speed]), + #{}) + } + , {"mem_level", + sc(range(1, 9), + #{ default => 8 + }) + } + , {"strategy", + sc(hoconsc:union([default, filtered, huffman_only, rle]), + #{}) + } + , {"server_context_takeover", + sc(hoconsc:union([takeover, no_takeover]), + #{}) + } + , {"client_context_takeover", + sc(hoconsc:union([takeover, no_takeover]), + #{}) + } + , {"server_max_window_bits", + sc(range(8, 15), + #{ default => 15 + }) + } + , {"client_max_window_bits", + sc(range(8, 15), + #{ default => 15 + }) + } ]; fields("plugins") -> - [ {"expand_plugins_dir", t(string())} + [ {"expand_plugins_dir", + sc(string(), + #{}) + } ]; fields("broker") -> - [ {"sys_msg_interval", maybe_disabled(duration(), "1m")} - , {"sys_heartbeat_interval", maybe_disabled(duration(), "30s")} - , {"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(boolean(), undefined, true)} - , {"perf", ref("perf")} + [ {"sys_msg_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "1m" + }) + } + , {"sys_heartbeat_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "30s" + }) + } + , {"enable_session_registry", + sc(boolean(), + #{ default => true + }) + } + , {"session_locking_strategy", + sc(hoconsc:union([local, leader, quorum, all]), + #{ default => quorum + }) + } + , {"shared_subscription_strategy", + sc(hoconsc:union([random, round_robin]), + #{ default => round_robin + }) + } + , {"shared_dispatch_ack_enabled", + sc(boolean(), + #{ default => false + }) + } + , {"route_batch_clean", + sc(boolean(), + #{ default => true + })} + , {"perf", + sc(ref("broker_perf"), + #{ desc => "Broker performance tuning pamaters" + }) + } ]; -fields("perf") -> - [ {"route_lock_type", t(union([key, tab, global]), undefined, key)} - , {"trie_compaction", t(boolean(), undefined, true)} +fields("broker_perf") -> + [ {"route_lock_type", + sc(hoconsc:union([key, tab, global]), + #{ default => key + })} + , {"trie_compaction", + sc(boolean(), + #{ default => true + })} ]; fields("sysmon") -> - [ {"vm", ref("sysmon_vm")} - , {"os", ref("sysmon_os")} + [ {"vm", + sc(ref("sysmon_vm"), + #{}) + } + , {"os", + sc(ref("sysmon_os"), + #{}) + } ]; fields("sysmon_vm") -> - [ {"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())} - , {"long_schedule", maybe_disabled(duration(), "240ms")} - , {"large_heap", maybe_disabled(bytesize(), "32MB")} - , {"busy_dist_port", t(boolean(), undefined, true)} - , {"busy_port", t(boolean(), undefined, true)} + [ {"process_check_interval", + sc(duration(), + #{ default => "30s" + }) + } + , {"process_high_watermark", + sc(percent(), + #{ default => "80%" + }) + } + , {"process_low_watermark", + sc(percent(), + #{ default => "60%" + }) + } + , {"long_gc", + sc(hoconsc:union([disabled, duration()]), + #{}) + } + , {"long_schedule", + sc(hoconsc:union([disabled, duration()]), + #{ default => "240ms" + }) + } + , {"large_heap", + sc(hoconsc:union([disabled, bytesize()]), + #{default => "32MB"}) + } + , {"busy_dist_port", + sc(boolean(), + #{ default => true + }) + } + , {"busy_port", + sc(boolean(), + #{ default => true + })} ]; fields("sysmon_os") -> - [ {"cpu_check_interval", t(duration(), undefined, "60s")} - , {"cpu_high_watermark", t(percent(), undefined, "80%")} - , {"cpu_low_watermark", t(percent(), undefined, "60%")} - , {"mem_check_interval", maybe_disabled(duration(), "60s")} - , {"sysmem_high_watermark", t(percent(), undefined, "70%")} - , {"procmem_high_watermark", t(percent(), undefined, "5%")} + [ {"cpu_check_interval", + sc(duration(), + #{ default => "60s"}) + } + , {"cpu_high_watermark", + sc(percent(), + #{ default => "80%" + }) + } + , {"cpu_low_watermark", + sc(percent(), + #{ default => "60%" + }) + } + , {"mem_check_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "60s" + })} + , {"sysmem_high_watermark", + sc(percent(), + #{ default => "70%" + }) + } + , {"procmem_high_watermark", + sc(percent(), + #{ default => "5%" + }) + } ]; fields("alarm") -> - [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} - , {"size_limit", t(integer(), undefined, 1000)} - , {"validity_period", t(duration(), undefined, "24h")} - ]; - -fields("strip_default:" ++ Name) -> - strip_default(fields(Name)). + [ {"actions", + sc(hoconsc:array(atom()), + #{ default => [log, publish] + }) + } + , {"size_limit", + sc(integer(), + #{ default => 1000 + }) + } + , {"validity_period", + sc(duration(), + #{ default => "24h" + }) + } + ]. mqtt_listener() -> base_listener() ++ - [ {"access_rules", t(hoconsc:array(string()))} - , {"proxy_protocol", t(boolean(), undefined, false)} - , {"proxy_protocol_timeout", t(duration())} + [ {"access_rules", + sc(hoconsc:array(string()), + #{}) + } + , {"proxy_protocol", + sc(boolean(), + #{ default => false + }) + } + , {"proxy_protocol_timeout", + sc(duration(), + #{}) + } ]. base_listener() -> - [ {"bind", hoconsc:t(union(ip_port(), integer()), #{nullable => false})} - , {"acceptors", t(integer(), undefined, 16)} - , {"max_connections", maybe_infinity(integer(), infinity)} - , {"mountpoint", t(binary(), undefined, <<>>)} - , {"zone", t(atom(), undefined, default)} + [ {"bind", + sc(hoconsc:union([ip_port(), integer()]), + #{ nullable => false + })} + , {"acceptors", + sc(integer(), + #{ default => 16 + })} + , {"max_connections", + sc(hoconsc:union([infinity, integer()]), + #{ default => infinity + })} + , {"mountpoint", + sc(binary(), + #{ default => <<>> + })} + , {"zone", + sc(atom(), + #{ default => 'default' + })} ]. %% utils @@ -372,43 +866,101 @@ conf_get(Key, Conf, Default) -> filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. -%% generate a ssl field. -%% 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(to_atom(Field), Defaults, undefined) end, - [ {"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(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"), - sensitive => true - })} - , {"dhfile", t(string(), undefined, D("dhfile"))} - , {"server_name_indication", t(union(disable, string()), undefined, - D("server_name_indication"))} - , {"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, <<>>})} + [ {"enable", + sc(boolean(), + #{ default => D("enable") + }) + } + , {"cacertfile", + sc(string(), + #{ default => D("cacertfile") + }) + } + , {"certfile", + sc(string(), + #{ default => D("certfile") + }) + } + , {"keyfile", + sc(string(), + #{ default => D("keyfile") + }) + } + , {"verify", + sc(hoconsc:union([verify_peer, verify_none]), + #{ default => D("verify") + }) + } + , {"fail_if_no_peer_cert", + sc(boolean(), + #{ default => D("fail_if_no_peer_cert") + }) + } + , {"secure_renegotiate", + sc(boolean(), + #{ default => D("secure_renegotiate") + }) + } + , {"reuse_sessions", + sc(boolean(), + #{ default => D("reuse_sessions") + }) + } + , {"honor_cipher_order", + sc(boolean(), + #{ default => D("honor_cipher_order") + }) + } + , {"handshake_timeout", + sc(duration(), + #{ default => D("handshake_timeout") + }) + } + , {"depth", + sc(integer(), + #{default => D("depth") + }) + } + , {"password", + sc(string(), + #{ default => D("key_password") + , sensitive => true + }) + } + , {"dhfile", + sc(string(), + #{ default => D("dhfile") + }) + } + , {"server_name_indication", + sc(hoconsc:union([disable, string()]), + #{ default => D("server_name_indication") + }) + } + , {"versions", + sc(typerefl:alias("string", list(atom())), + #{ default => maps:get(versions, Defaults, default_tls_vsns()) + , converter => fun (Vsns) -> [tls_vsn(iolist_to_binary(V)) || V <- Vsns] end + }) + } + , {"ciphers", + sc(hoconsc:array(string()), + #{ default => D("ciphers") + }) + } + , {"user_lookup_fun", + sc(typerefl:alias("string", any()), + #{ default => "emqx_psk:lookup" + , converter => fun ?MODULE:parse_user_lookup_fun/1 + }) + } ]. %% 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'; @@ -451,40 +1003,11 @@ ceiling(X) -> %% types -t(Type) -> hoconsc:t(Type). +sc(Type, Meta) -> hoconsc:mk(Type, Meta). -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). +ref(Field) -> hoconsc:ref(?MODULE, Field). -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). - -t(Type, Mapping, Default, OverrideEnv, Validator) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - , validator => Validator - }). - -ref(Field) -> hoconsc:t(hoconsc:ref(?MODULE, Field)). - -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). +ref(Module, Field) -> hoconsc:ref(Module, Field). to_duration(Str) -> case hocon_postprocess:duration(Str) of @@ -545,22 +1068,26 @@ to_erl_cipher_suite(Str) -> Cipher -> Cipher end. -strip_default(Fields) -> - [do_strip_default(F) || F <- Fields]. - -do_strip_default({Name, #{type := {ref, Ref}}}) -> - {Name, nullable_no_def(ref("strip_default:" ++ Ref))}; -do_strip_default({Name, #{type := {ref, _Mod, Ref}}}) -> - {Name, nullable_no_def(ref("strip_default:" ++ Ref))}; -do_strip_default({Name, Type}) -> - {Name, nullable_no_def(Type)}. - -nullable_no_def(Type) when is_map(Type) -> - Type#{default => undefined, nullable => true}. - to_atom(Atom) when is_atom(Atom) -> Atom; to_atom(Str) when is_list(Str) -> list_to_atom(Str); to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). + +validate_heap_size(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. +parse_user_lookup_fun(StrConf) -> + [ModStr, FunStr] = string:tokens(StrConf, ":"), + Mod = list_to_atom(ModStr), + Fun = list_to_atom(FunStr), + {fun Mod:Fun/3, <<>>}. diff --git a/apps/emqx/src/emqx_zone_schema.erl b/apps/emqx/src/emqx_zone_schema.erl new file mode 100644 index 000000000..013ffb22f --- /dev/null +++ b/apps/emqx/src/emqx_zone_schema.erl @@ -0,0 +1,34 @@ +%%-------------------------------------------------------------------- +%% 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_zone_schema). + +-export([namespace/0, roots/0, fields/1]). + +namespace() -> zone. + +roots() -> []. + +%% zone schemas are clones from the same name from root level +%% only not allowed to have default values. +fields(Name) -> + [{N, no_default(Sc)} || {N, Sc} <- emqx_schema:fields(Name)]. + +%% no default values for zone settings +no_default(Sc) -> + fun(default) -> undefined; + (Other) -> hocon_schema:field_schema(Sc, Other) + end. diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index de0de9fcc..bceedb6bb 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -21,7 +21,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -32,6 +33,8 @@ -export([ authenticators/1 ]). +namespace() -> authn. + roots() -> [ "authentication" ]. fields("authentication") -> 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 index 0ca281aa0..d7902d824 100644 --- 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 @@ -21,7 +21,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -74,6 +75,8 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:scram:builtin_db". + roots() -> [config]. fields(config) -> 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 c5cbc0f02..080b71ab1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 , validations/0 ]). @@ -37,9 +38,11 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:http". + roots() -> - [ {config, {union, [ hoconsc:t(get) - , hoconsc:t(post) + [ {config, {union, [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) ]}} ]. 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 bc26bf70e..1ce10a2cc 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -20,7 +20,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -34,10 +35,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:jwt". + roots() -> - [ {config, {union, [ hoconsc:t('hmac-based') - , hoconsc:t('public-key') - , hoconsc:t('jwks') + [ {config, {union, [ hoconsc:mk('hmac-based') + , hoconsc:mk('public-key') + , hoconsc:mk('jwks') ]}} ]. 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 c525efbf1..efe974145 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -21,7 +21,7 @@ -behaviour(hocon_schema). --export([ roots/0, fields/1 ]). +-export([ namespace/0, roots/0, fields/1 ]). -export([ create/1 , update/2 @@ -79,6 +79,8 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:builtin_db". + roots() -> [config]. fields(config) -> @@ -102,7 +104,8 @@ user_id_type(type) -> user_id_type(); user_id_type(default) -> username; user_id_type(_) -> undefined. -password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(type) -> hoconsc:union([hoconsc:ref(?MODULE, bcrypt), + hoconsc:ref(?MODULE, other_algorithms)]); password_hash_algorithm(default) -> #{<<"name">> => sha256}; password_hash_algorithm(_) -> undefined. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 11411b70f..d272fe05b 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -36,10 +37,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:mongodb". + roots() -> - [ {config, {union, [ hoconsc:t(standalone) - , hoconsc:t('replica-set') - , hoconsc:t('sharded-cluster') + [ {config, {union, [ hoconsc:mk(standalone) + , hoconsc:mk('replica-set') + , hoconsc:mk('sharded-cluster') ]}} ]. 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 3cafdb94e..c94798aa6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -36,6 +37,8 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:mysql". + roots() -> [config]. fields(config) -> 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 5c21d3d6c..6875c5cb9 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -23,7 +23,7 @@ -behaviour(hocon_schema). --export([ roots/0, fields/1 ]). +-export([ namespace/0, roots/0, fields/1 ]). -export([ create/1 , update/2 @@ -35,6 +35,8 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:postgres". + roots() -> [config]. fields(config) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 1b090b007..6c5a81652 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -36,10 +37,11 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:redis". roots() -> - [ {config, {union, [ hoconsc:t(standalone) - , hoconsc:t(cluster) - , hoconsc:t(sentinel) + [ {config, {union, [ hoconsc:mk(standalone) + , hoconsc:mk(cluster) + , hoconsc:mk(sentinel) ]}} ]. diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 251e40fe6..0645990a8 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -13,11 +13,14 @@ -type permission() :: allow | deny. -type url() :: emqx_http_lib:uri_map(). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). -roots() -> ["authorization"]. +namespace() -> authz. + +roots() -> []. fields("authorization") -> [ {sources, #{type => union_array( diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl index 92420e217..5b781455d 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -19,25 +19,30 @@ -include_lib("typerefl/include/types.hrl"). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> "auto_subscribe". + roots() -> ["auto_subscribe"]. fields("auto_subscribe") -> - [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))}]; + [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))} + ]; fields("topic") -> - [ {topic, emqx_schema:t(binary())} - , {qos, t(hoconsc:union([0, 1, 2]), 0)} - , {rh, t(hoconsc:union([0, 1, 2]), 0)} - , {rap, t(hoconsc:union([0, 1]), 0)} - , {nl, t(hoconsc:union([0, 1]), 0)} + [ {topic, sc(binary(), #{})} + , {qos, sc(typerefl:union([0, 1, 2]), #{default => 0})} + , {rh, sc(typerefl:union([0, 1, 2]), #{default => 0})} + , {rap, sc(typerefl:union([0, 1]), #{default => 0})} + , {nl, sc(typerefl:union([0, 1]), #{default => 0})} ]. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -t(Type, Default) -> - hoconsc:t(Type, #{default => Default}). + +sc(Type, Meta) -> + hoconsc:mk(Type, Meta). 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 925bfa403..f370af277 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -20,55 +20,60 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> "bridge_mqtt". + roots() -> [array("bridge_mqtt")]. -array(Name) -> {Name, hoconsc:array(hoconsc:ref(Name))}. +array(Name) -> {Name, hoconsc:array(hoconsc:ref(?MODULE, Name))}. fields("bridge_mqtt") -> - [ {name, emqx_schema:t(string(), undefined, true)} + [ {name, sc(string(), #{default => 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(?MODULE, "queue"))} - , {config, hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), hoconsc:ref(?MODULE, "rpc")])} + , {forward_mountpoint, sc(string(), #{})} + , {reconnect_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} + , {batch_size, sc(integer(), #{default => 100})} + , {queue, sc(hoconsc:ref(?MODULE, "queue"), #{})} + , {config, sc(hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), + hoconsc:ref(?MODULE, "rpc")]), + #{})} ]; fields("mqtt") -> [ {conn_type, fun conn_type/1} - , {address, emqx_schema:t(string(), undefined, "127.0.0.1:1883")} + , {address, sc(string(), #{default => "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)} + , {bridge_mode, sc(boolean(), #{default => true})} + , {clientid, sc(string(), #{})} + , {username, sc(string(), #{})} + , {password, sc(string(), #{})} + , {clean_start, sc(boolean(), #{default => true})} + , {keepalive, sc(integer(), #{default => 300})} + , {subscriptions, sc(hoconsc:array(hoconsc:ref(?MODULE, "subscriptions")), #{})} + , {receive_mountpoint, sc(string(), #{})} + , {retry_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} + , {max_inflight, sc(integer(), #{default => 32})} ]; fields("rpc") -> [ {conn_type, fun conn_type/1} - , {node, emqx_schema:t(atom(), undefined, 'emqx@127.0.0.1')} + , {node, sc(atom(), #{default => 'emqx@127.0.0.1'})} ]; fields("subscriptions") -> [ {topic, #{type => binary(), nullable => false}} - , {qos, emqx_schema:t(integer(), undefined, 1)} + , {qos, sc(integer(), #{default => 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")} + , {replayq_seg_bytes, sc(emqx_schema:bytesize(), #{default => "100MB"})} + , {replayq_offload_mode, sc(boolean(), #{default => false})} + , {replayq_max_total_bytes, sc(emqx_schema:bytesize(), #{default => "1024MB"})} ]. conn_type(type) -> hoconsc:enum([mqtt, rpc]); @@ -85,3 +90,5 @@ start_type(_) -> undefined. forwards(type) -> hoconsc:array(binary()); forwards(default) -> []; forwards(_) -> undefined. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 88dfb2b72..c95679f32 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -82,10 +82,7 @@ mongo_fields() -> , {auth_source, #{type => binary(), nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} - , {topology, #{type => hoconsc:ref(?MODULE, topology), - default => #{}}} - %% TODO: Does the ref type support nullable=ture ? - % nullable => true}} + , {topology, #{type => hoconsc:ref(?MODULE, topology)}} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -178,7 +175,7 @@ do_start(InstId, Opts0, Config = #{mongo_type := Type, ]; false -> [{ssl, false}] end, - Topology= maps:get(topology, Config, #{}), + Topology= maps:get(topology, Config, #{}), Opts = Opts0 ++ [{pool_size, PoolSize}, {options, init_topology_options(maps:to_list(Topology), [])}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 7dfbc923b..3ba3dc803 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -27,19 +27,19 @@ fields("emqx_dashboard") -> hoconsc:ref(?MODULE, "https")]))} , {default_username, fun default_username/1} , {default_password, fun default_password/1} - , {sample_interval, emqx_schema:t(emqx_schema:duration_s(), undefined, "10s")} - , {token_expired_time, emqx_schema:t(emqx_schema:duration(), undefined, "30m")} + , {sample_interval, sc(emqx_schema:duration_s(), #{default => "10s"})} + , {token_expired_time, sc(emqx_schema:duration(), #{default => "30m"})} ]; fields("http") -> [ {"protocol", hoconsc:enum([http, https])} - , {"port", emqx_schema:t(integer(), undefined, 18083)} - , {"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, "5s")} - , {"inet6", emqx_schema:t(boolean(), undefined, false)} - , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} + , {"port", hoconsc:mk(integer(), #{default => 18083})} + , {"num_acceptors", sc(integer(), #{default => 4})} + , {"max_connections", sc(integer(), #{default => 512})} + , {"backlog", sc(integer(), #{default => 1024})} + , {"send_timeout", sc(emqx_schema:duration(), #{default => "5s"})} + , {"inet6", sc(boolean(), #{default => false})} + , {"ipv6_v6only", sc(boolean(), #{dfeault => false})} ]; fields("https") -> @@ -54,3 +54,5 @@ default_password(type) -> string(); default_password(default) -> "public"; default_password(nullable) -> false; default_password(_) -> undefined. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). 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 69f53d6c1..e3c6d8ee9 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -22,5 +22,5 @@ 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())}, + [{name, hoconsc:mk(typerefl:binary())}, {type, #{type => DB}}] ++ Mod:roots(). diff --git a/apps/emqx_exhook/src/emqx_exhook_schema.erl b/apps/emqx_exhook/src/emqx_exhook_schema.erl index 16fd93fa0..64d39eb52 100644 --- a/apps/emqx_exhook/src/emqx_exhook_schema.erl +++ b/apps/emqx_exhook/src/emqx_exhook_schema.erl @@ -32,43 +32,57 @@ -reflect_type([duration/0]). --export([roots/0, fields/1]). +-export([namespace/0, roots/0, fields/1]). --export([t/1, t/3, t/4, ref/1]). +namespace() -> exhook. roots() -> [exhook]. fields(exhook) -> - [ {request_failed_action, t(union([deny, ignore]), undefined, deny)} - , {request_timeout, t(duration(), undefined, "5s")} - , {auto_reconnect, t(union([false, duration()]), undefined, "60s")} - , {servers, t(hoconsc:array(ref(servers)), undefined, [])} + [ {request_failed_action, + sc(union([deny, ignore]), + #{default => deny})} + , {request_timeout, + sc(duration(), + #{default => "5s"})} + , {auto_reconnect, + sc(union([false, duration()]), + #{ default => "60s" + })} + , {servers, + sc(hoconsc:array(ref(servers)), + #{default => []})} ]; fields(servers) -> - [ {name, string()} - , {url, string()} - , {ssl, t(ref(ssl_conf_group))} + [ {name, + sc(string(), + #{})} + , {url, + sc(string(), + #{})} + , {ssl, + sc(ref(ssl_conf), + #{})} ]; -fields(ssl_conf_group) -> - [ {cacertfile, string()} - , {certfile, string()} - , {keyfile, string()} +fields(ssl_conf) -> + [ {cacertfile, + sc(string(), + #{}) + } + , {certfile, + sc(string(), + #{}) + } + , {keyfile, + sc(string(), + #{})} ]. %% 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 - }). +sc(Type, Meta) -> Meta#{type => Type}. ref(Field) -> hoconsc:ref(?MODULE, Field). diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 9ab26e480..da73b85ee 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -43,148 +43,149 @@ , ip_port/0 ]). --export([roots/0 , fields/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0 , fields/1]). + +namespace() -> gateway. roots() -> [gateway]. fields(gateway) -> - [{stomp, t(ref(stomp_structs))}, - {mqttsn, t(ref(mqttsn_structs))}, - {coap, t(ref(coap_structs))}, - {lwm2m, t(ref(lwm2m_structs))}, - {exproto, t(ref(exproto_structs))} + [{stomp, sc(ref(stomp_structs))}, + {mqttsn, sc(ref(mqttsn_structs))}, + {coap, sc(ref(coap_structs))}, + {lwm2m, sc(ref(lwm2m_structs))}, + {exproto, sc(ref(exproto_structs))} ]; fields(stomp_structs) -> - [ {frame, t(ref(stomp_frame))} - , {listeners, t(ref(tcp_listener_group))} + [ {frame, sc(ref(stomp_frame))} + , {listeners, sc(ref(tcp_listener_group))} ] ++ gateway_common_options(); fields(stomp_frame) -> - [ {max_headers, t(integer(), undefined, 10)} - , {max_headers_length, t(integer(), undefined, 1024)} - , {max_body_length, t(integer(), undefined, 8192)} + [ {max_headers, sc(integer(), undefined, 10)} + , {max_headers_length, sc(integer(), undefined, 1024)} + , {max_body_length, sc(integer(), undefined, 8192)} ]; fields(mqttsn_structs) -> - [ {gateway_id, t(integer())} - , {broadcast, t(boolean())} - , {enable_qos3, t(boolean())} + [ {gateway_id, sc(integer())} + , {broadcast, sc(boolean())} + , {enable_qos3, sc(boolean())} , {predefined, hoconsc:array(ref(mqttsn_predefined))} - , {listeners, t(ref(udp_listener_group))} + , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); fields(mqttsn_predefined) -> - [ {id, t(integer())} - , {topic, t(binary())} + [ {id, sc(integer())} + , {topic, sc(binary())} ]; fields(coap_structs) -> - [ {heartbeat, t(duration(), undefined, <<"30s">>)} - , {notify_type, t(union([non, con, qos]), undefined, qos)} - , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {listeners, t(ref(udp_listener_group))} + [ {heartbeat, sc(duration(), undefined, <<"30s">>)} + , {notify_type, sc(union([non, con, qos]), undefined, qos)} + , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {publish_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); fields(lwm2m_structs) -> - [ {xml_dir, t(binary())} - , {lifetime_min, t(duration())} - , {lifetime_max, t(duration())} - , {qmode_time_windonw, t(integer())} - , {auto_observe, t(boolean())} - , {mountpoint, t(string())} - , {update_msg_publish_condition, t(union([always, contains_object_list]))} - , {translators, t(ref(translators))} - , {listeners, t(ref(udp_listener_group))} + [ {xml_dir, sc(binary())} + , {lifetime_min, sc(duration())} + , {lifetime_max, sc(duration())} + , {qmode_time_windonw, sc(integer())} + , {auto_observe, sc(boolean())} + , {mountpoint, sc(string())} + , {update_msg_publish_condition, sc(union([always, contains_object_list]))} + , {translators, sc(ref(translators))} + , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); fields(exproto_structs) -> - [ {server, t(ref(exproto_grpc_server))} - , {handler, t(ref(exproto_grpc_handler))} - , {listeners, t(ref(udp_tcp_listener_group))} + [ {server, sc(ref(exproto_grpc_server))} + , {handler, sc(ref(exproto_grpc_handler))} + , {listeners, sc(ref(udp_tcp_listener_group))} ] ++ gateway_common_options(); fields(exproto_grpc_server) -> - [ {bind, t(union(ip_port(), integer()))} + [ {bind, sc(union(ip_port(), integer()))} %% TODO: ssl options ]; fields(exproto_grpc_handler) -> - [ {address, t(binary())} + [ {address, sc(binary())} %% TODO: ssl ]; fields(clientinfo_override) -> - [ {username, t(binary())} - , {password, t(binary())} - , {clientid, t(binary())} + [ {username, sc(binary())} + , {password, sc(binary())} + , {clientid, sc(binary())} ]; fields(translators) -> - [ {command, t(ref(translator))} - , {response, t(ref(translator))} - , {notify, t(ref(translator))} - , {register, t(ref(translator))} - , {update, t(ref(translator))} + [ {command, sc(ref(translator))} + , {response, sc(ref(translator))} + , {notify, sc(ref(translator))} + , {register, sc(ref(translator))} + , {update, sc(ref(translator))} ]; fields(translator) -> - [ {topic, t(binary())} - , {qos, t(range(0, 2))} + [ {topic, sc(binary())} + , {qos, sc(range(0, 2))} ]; fields(udp_listener_group) -> - [ {udp, t(ref(udp_listener))} - , {dtls, t(ref(dtls_listener))} + [ {udp, sc(ref(udp_listener))} + , {dtls, sc(ref(dtls_listener))} ]; fields(tcp_listener_group) -> - [ {tcp, t(ref(tcp_listener))} - , {ssl, t(ref(ssl_listener))} + [ {tcp, sc(ref(tcp_listener))} + , {ssl, sc(ref(ssl_listener))} ]; fields(udp_tcp_listener_group) -> - [ {udp, t(ref(udp_listener))} - , {dtls, t(ref(dtls_listener))} - , {tcp, t(ref(tcp_listener))} - , {ssl, t(ref(ssl_listener))} + [ {udp, sc(ref(udp_listener))} + , {dtls, sc(ref(dtls_listener))} + , {tcp, sc(ref(tcp_listener))} + , {ssl, sc(ref(ssl_listener))} ]; fields(tcp_listener) -> - [ {"$name", t(ref(tcp_listener_settings))}]; + [ {"$name", sc(ref(tcp_listener_settings))}]; fields(ssl_listener) -> - [ {"$name", t(ref(ssl_listener_settings))}]; + [ {"$name", sc(ref(ssl_listener_settings))}]; fields(udp_listener) -> - [ {"$name", t(ref(udp_listener_settings))}]; + [ {"$name", sc(ref(udp_listener_settings))}]; fields(dtls_listener) -> - [ {"$name", t(ref(dtls_listener_settings))}]; + [ {"$name", sc(ref(dtls_listener_settings))}]; fields(listener_settings) -> - [ {enable, t(boolean(), undefined, true)} - , {bind, 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)} - %, {rate_limit, t(comma_separated_list())} - , {access, t(ref(access))} - , {proxy_protocol, t(boolean())} - , {proxy_protocol_timeout, t(duration())} - , {backlog, t(integer(), undefined, 1024)} - , {send_timeout, t(duration(), undefined, <<"15s">>)} - , {send_timeout_close, t(boolean(), undefined, true)} - , {recbuf, t(bytesize())} - , {sndbuf, t(bytesize())} - , {buffer, t(bytesize())} - , {high_watermark, t(bytesize(), undefined, <<"1MB">>)} - , {tune_buffer, t(boolean())} - , {nodelay, t(boolean())} - , {reuseaddr, t(boolean())} + [ {enable, sc(boolean(), undefined, true)} + , {bind, sc(union(ip_port(), integer()))} + , {acceptors, sc(integer(), undefined, 8)} + , {max_connections, sc(integer(), undefined, 1024)} + , {max_conn_rate, sc(integer())} + , {active_n, sc(integer(), undefined, 100)} + %, {rate_limit, sc(comma_separated_list())} + , {access, sc(ref(access))} + , {proxy_protocol, sc(boolean())} + , {proxy_protocol_timeout, sc(duration())} + , {backlog, sc(integer(), undefined, 1024)} + , {send_timeout, sc(duration(), undefined, <<"15s">>)} + , {send_timeout_close, sc(boolean(), undefined, true)} + , {recbuf, sc(bytesize())} + , {sndbuf, sc(bytesize())} + , {buffer, sc(bytesize())} + , {high_watermark, sc(bytesize(), undefined, <<"1MB">>)} + , {tune_buffer, sc(boolean())} + , {nodelay, sc(boolean())} + , {reuseaddr, sc(boolean())} ]; fields(tcp_listener_settings) -> @@ -242,12 +243,12 @@ authentication() -> ]). gateway_common_options() -> - [ {enable, t(boolean(), undefined, true)} - , {enable_stats, t(boolean(), undefined, true)} - , {idle_timeout, t(duration(), undefined, <<"30s">>)} - , {mountpoint, t(binary())} - , {clientinfo_override, t(ref(clientinfo_override))} - , {authentication, t(authentication(), undefined, undefined)} + [ {enable, sc(boolean(), undefined, true)} + , {enable_stats, sc(boolean(), undefined, true)} + , {idle_timeout, sc(duration(), undefined, <<"30s">>)} + , {mountpoint, sc(binary())} + , {clientinfo_override, sc(ref(clientinfo_override))} + , {authentication, sc(authentication(), undefined, undefined)} ]. %%-------------------------------------------------------------------- @@ -255,16 +256,10 @@ gateway_common_options() -> %% types -t(Type) -> #{type => Type}. +sc(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 - }). +sc(Type, Mapping, Default) -> + hoconsc:mk(Type, #{mapping => Mapping, default => Default}). ref(Field) -> hoconsc:ref(?MODULE, Field). @@ -273,10 +268,10 @@ ref(Field) -> %% 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)} +%% [ {"cacertfile", sc(string(), "emqx.cacertfile", undefined)} +%% , {"certfile", sc(string(), "emqx.certfile", undefined)} +%% , {"keyfile", sc(string(), "emqx.keyfile", undefined)} +%% , {"verify", sc(union(verify_peer, verify_none), "emqx.verify", verify_peer)} %% , {"server_name_indication", "emqx.server_name_indication", undefined)} %% ... ssl(Mapping, Defaults) -> @@ -286,24 +281,24 @@ ssl(Mapping, Defaults) -> _ -> Mapping ++ "." ++ Field end end, D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"enable", t(boolean(), M("enable"), D("enable"))} - , {"cacertfile", t(binary(), M("cacertfile"), D("cacertfile"))} - , {"certfile", t(binary(), M("certfile"), D("certfile"))} - , {"keyfile", t(binary(), 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(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} - , {"reuse_sessions", t(boolean(), M("reuse_sessions"), D("reuse_sessions"))} - , {"honor_cipher_order", t(boolean(), 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(binary(), #{mapping => M("key_password"), - default => D("key_password"), - sensitive => true + [ {"enable", sc(boolean(), M("enable"), D("enable"))} + , {"cacertfile", sc(binary(), M("cacertfile"), D("cacertfile"))} + , {"certfile", sc(binary(), M("certfile"), D("certfile"))} + , {"keyfile", sc(binary(), M("keyfile"), D("keyfile"))} + , {"verify", sc(union(verify_peer, verify_none), M("verify"), D("verify"))} + , {"fail_if_no_peer_cert", sc(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} + , {"secure_renegotiate", sc(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} + , {"reuse_sessions", sc(boolean(), M("reuse_sessions"), D("reuse_sessions"))} + , {"honor_cipher_order", sc(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} + , {"handshake_timeout", sc(duration(), M("handshake_timeout"), D("handshake_timeout"))} + , {"depth", sc(integer(), M("depth"), D("depth"))} + , {"password", hoconsc:mk(binary(), #{ mapping => M("key_password") + , default => D("key_password") + , sensitive => true })} - , {"dhfile", t(binary(), M("dhfile"), D("dhfile"))} - , {"server_name_indication", t(union(disable, binary()), M("server_name_indication"), + , {"dhfile", sc(binary(), M("dhfile"), D("dhfile"))} + , {"server_name_indication", sc(union(disable, binary()), 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"))}]. + , {"tls_versions", sc(comma_separated_list(), M("tls_versions"), D("tls_versions"))} + , {"ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))} + , {"psk_ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))}]. diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index c25ab8139..a3e7e9388 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -23,6 +23,7 @@ -dialyzer(no_fail_call). -include_lib("typerefl/include/types.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). -type log_level() :: debug | info | notice | warning | error | critical | alert | emergency | all. -type file() :: string(). @@ -34,8 +35,7 @@ file/0, cipher/0]). --export([roots/0, fields/1, translations/0, translation/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0, fields/1, translations/0, translation/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). %% Static apps which merge their configs into the merged emqx.conf @@ -58,167 +58,406 @@ , emqx_exhook_schema ]). -%% TODO: add a test case to ensure the list elements are unique +namespace() -> undefined. + roots() -> - ["cluster", "node", "rpc", "log"] - ++ lists:flatmap(fun(Mod) -> Mod:roots() end, ?MERGED_CONFIGS). + ["cluster", "node", "rpc", "log"] ++ lists:flatmap(fun roots/1, ?MERGED_CONFIGS). fields("cluster") -> - [ {"name", t(atom(), "ekka.cluster_name", emqxcl)} - , {"discovery_strategy", t(union([manual, static, mcast, dns, etcd, k8s]), - undefined, manual)} - , {"autoclean", t(emqx_schema:duration(), "ekka.cluster_autoclean", "5m")} - , {"autoheal", t(boolean(), "ekka.cluster_autoheal", true)} - , {"static", ref("static")} - , {"mcast", ref("mcast")} - , {"proto_dist", t(union([inet_tcp, inet6_tcp, inet_tls]), "ekka.proto_dist", inet_tcp)} - , {"dns", ref("dns")} - , {"etcd", ref("etcd")} - , {"k8s", ref("k8s")} - , {"db_backend", t(union([mnesia, rlog]), "ekka.db_backend", mnesia)} - , {"rlog", ref("rlog")} + [ {"name", + sc(atom(), + #{ mapping => "ekka.cluster_name" + , default => emqxcl + })} + , {"discovery_strategy", + sc(union([manual, static, mcast, dns, etcd, k8s]), + #{ default => manual + })} + , {"autoclean", + sc(emqx_schema:duration(), + #{ mapping => "ekka.cluster_autoclean" + , default => "5m" + })} + , {"autoheal", + sc(boolean(), + #{ mapping => "ekka.cluster_autoheal" + , default => true + })} + , {"static", + sc(ref(cluster_static), + #{})} + , {"mcast", + sc(ref(cluster_mcast), + #{})} + , {"proto_dist", + sc(union([inet_tcp, inet6_tcp, inet_tls]), + #{ mapping => "ekka.proto_dist" + , default => inet_tcp + })} + , {"dns", + sc(ref(cluster_dns), + #{})} + , {"etcd", + sc(ref(cluster_etcd), + #{})} + , {"k8s", + sc(ref(cluster_k8s), + #{})} + , {"db_backend", + sc(union([mnesia, rlog]), + #{ mapping => "ekka.db_backend" + , default => mnesia + })} + , {"rlog", + sc(ref("rlog"), + #{})} ]; -fields("static") -> - [ {"seeds", t(hoconsc:array(string()), undefined, [])}]; - -fields("mcast") -> - [ {"addr", t(string(), undefined, "239.192.0.1")} - , {"ports", t(hoconsc:array(integer()), undefined, [4369, 4370])} - , {"iface", t(string(), undefined, "0.0.0.0")} - , {"ttl", t(range(0, 255), undefined, 255)} - , {"loop", t(boolean(), undefined, true)} - , {"sndbuf", t(emqx_schema:bytesize(), undefined, "16KB")} - , {"recbuf", t(emqx_schema:bytesize(), undefined, "16KB")} - , {"buffer", t(emqx_schema:bytesize(), undefined, "32KB")} +fields(cluster_static) -> + [ {"seeds", + sc(hoconsc:array(string()), + #{ default => [] + })} ]; -fields("dns") -> - [ {"name", t(string(), undefined, "localhost")} - , {"app", t(string(), undefined, "emqx")}]; - -fields("etcd") -> - [ {"server", t(emqx_schema:comma_separated_list())} - , {"prefix", t(string(), undefined, "emqxcl")} - , {"node_ttl", t(emqx_schema:duration(), undefined, "1m")} - , {"ssl", ref("etcd_ssl")} +fields(cluster_mcast) -> + [ {"addr", + sc(string(), + #{ default => "239.192.0.1" + })} + , {"ports", + sc(hoconsc:array(integer()), + #{ default => [4369, 4370] + })} + , {"iface", + sc(string(), + #{ default => "0.0.0.0" + })} + , {"ttl", + sc(range(0, 255), + #{ default => 255 + })} + , {"loop", + sc(boolean(), + #{ default => true + })} + , {"sndbuf", + sc(emqx_schema:bytesize(), + #{ default => "16KB" + })} + , {"recbuf", + sc(emqx_schema:bytesize(), + #{ default => "16KB" + })} + , {"buffer", + sc(emqx_schema:bytesize(), + #{ default =>"32KB" + })} ]; -fields("etcd_ssl") -> +fields(cluster_dns) -> + [ {"name", + sc(string(), + #{ default => "localhost" + })} + , {"app", + sc(string(), + #{ default => "emqx" + })} + ]; + +fields(cluster_etcd) -> + [ {"server", + sc(emqx_schema:comma_separated_list(), + #{})} + , {"prefix", + sc(string(), + #{ default => "emqxcl" + })} + , {"node_ttl", + sc(emqx_schema:duration(), + #{ default => "1m" + })} + , {"ssl", + sc(ref(etcd_ssl_opts), + #{})} + ]; + +fields(etcd_ssl_opts) -> emqx_schema:ssl(#{}); -fields("k8s") -> - [ {"apiserver", t(string())} - , {"service_name", t(string(), undefined, "emqx")} - , {"address_type", t(union([ip, dns, hostname]))} - , {"app_name", t(string(), undefined, "emqx")} - , {"namespace", t(string(), undefined, "default")} - , {"suffix", t(string(), undefined, "pod.local")} +fields(cluster_k8s) -> + [ {"apiserver", + sc(string(), + #{})} + , {"service_name", + sc(string(), + #{ default => "emqx" + })} + , {"address_type", + sc(union([ip, dns, hostname]), + #{})} + , {"app_name", + sc(string(), + #{ default => "emqx" + })} + , {"namespace", + sc(string(), + #{ default => "default" + })} + , {"suffix", + sc(string(), + #{default => "pod.local" + })} ]; fields("rlog") -> - [ {"role", t(union([core, replicant]), "ekka.node_role", core)} - , {"core_nodes", t(emqx_schema:comma_separated_atoms(), "ekka.core_nodes", [])} + [ {"role", + sc(union([core, replicant]), + #{ mapping => "ekka.node_role" + , default => core + })} + , {"core_nodes", + sc(emqx_schema:comma_separated_atoms(), + #{ mapping => "ekka.core_nodes" + , default => [] + })} ]; fields("node") -> - [ {"name", hoconsc:t(string(), #{default => "emqx@127.0.0.1", - override_env => "EMQX_NODE_NAME" - })} - , {"cookie", hoconsc:t(string(), #{mapping => "vm_args.-setcookie", - default => "emqxsecretcookie", - sensitive => true, - override_env => "EMQX_NODE_COOKIE" - })} - , {"data_dir", hoconsc:t(string(), #{nullable => false})} - , {"config_files", t(list(string()), "emqx.config_files", undefined)} - , {"global_gc_interval", t(emqx_schema:duration(), undefined, "15m")} - , {"crash_dump_dir", t(file(), "vm_args.-env ERL_CRASH_DUMP", undefined)} - , {"dist_net_ticktime", t(emqx_schema:duration(), "vm_args.-kernel net_ticktime", "2m")} - , {"dist_listen_min", t(range(1024, 65535), "kernel.inet_dist_listen_min", 6369)} - , {"dist_listen_max", t(range(1024, 65535), "kernel.inet_dist_listen_max", 6369)} - , {"backtrace_depth", t(integer(), "emqx_machine.backtrace_depth", 23)} - , {"cluster_call", ref("cluster_call")} + [ {"name", + sc(string(), + #{ default => "emqx@127.0.0.1" + , override_env => "EMQX_NODE_NAME" + })} + , {"cookie", + sc(string(), + #{ mapping => "vm_args.-setcookie", + default => "emqxsecretcookie", + sensitive => true, + override_env => "EMQX_NODE_COOKIE" + })} + , {"data_dir", + sc(string(), + #{ nullable => false + })} + , {"config_files", + sc(list(string()), + #{ mapping => "emqx.config_files" + , default => undefined + })} + , {"global_gc_interval", + sc(emqx_schema:duration(), + #{ default => "15m" + })} + , {"crash_dump_dir", + sc(file(), + #{ mapping => "vm_args.-env ERL_CRASH_DUMP" + })} + , {"dist_net_ticktime", + sc(emqx_schema:duration(), + #{ mapping => "vm_args.-kernel net_ticktime" + , default => "2m" + })} + , {"dist_listen_min", + sc(range(1024, 65535), + #{ mapping => "kernel.inet_dist_listen_min" + , default => 6369 + })} + , {"dist_listen_max", + sc(range(1024, 65535), + #{ mapping => "kernel.inet_dist_listen_max" + , default => 6369 + })} + , {"backtrace_depth", + sc(integer(), + #{ mapping => "emqx_machine.backtrace_depth" + , default => 23 + })} + , {"cluster_call", + sc(ref("cluster_call"), + #{} + )} ]; - fields("cluster_call") -> - [ {"retry_interval", t(emqx_schema:duration(), "emqx_machine.retry_interval", "1s")} - , {"max_history", t(range(1, 500), "emqx_machine.max_history", 100)} - , {"cleanup_interval", t(emqx_schema:duration(), "emqx_machine.cleanup_interval", "5m")} + [ {"retry_interval", + sc(emqx_schema:duration(), + #{ mapping => "emqx_machine.retry_interval" + , default => "1s" + })} + , {"max_history", + sc(range(1, 500), + #{mapping => "emqx_machine.max_history", + default => 100 + })} + , {"cleanup_interval", + sc(emqx_schema:duration(), + #{mapping => "emqx_machine.cleanup_interval", + default => "5m" + })} ]; fields("rpc") -> - [ {"mode", t(union(sync, async), undefined, async)} - , {"async_batch_size", t(integer(), "gen_rpc.max_batch_size", 256)} - , {"port_discovery",t(union(manual, stateless), "gen_rpc.port_discovery", stateless)} - , {"tcp_server_port", t(integer(), "gen_rpc.tcp_server_port", 5369)} - , {"tcp_client_num", t(range(1, 256), undefined, 1)} - , {"connect_timeout", t(emqx_schema:duration(), "gen_rpc.connect_timeout", "5s")} - , {"send_timeout", t(emqx_schema:duration(), "gen_rpc.send_timeout", "5s")} - , {"authentication_timeout", t(emqx_schema:duration(), "gen_rpc.authentication_timeout", "5s")} - , {"call_receive_timeout", t(emqx_schema:duration(), "gen_rpc.call_receive_timeout", "15s")} - , {"socket_keepalive_idle", t(emqx_schema:duration_s(), "gen_rpc.socket_keepalive_idle", "7200s")} - , {"socket_keepalive_interval", t(emqx_schema:duration_s(), "gen_rpc.socket_keepalive_interval", "75s")} - , {"socket_keepalive_count", t(integer(), "gen_rpc.socket_keepalive_count", 9)} - , {"socket_sndbuf", t(emqx_schema:bytesize(), "gen_rpc.socket_sndbuf", "1MB")} - , {"socket_recbuf", t(emqx_schema:bytesize(), "gen_rpc.socket_recbuf", "1MB")} - , {"socket_buffer", t(emqx_schema:bytesize(), "gen_rpc.socket_buffer", "1MB")} + [ {"mode", + sc(union(sync, async), + #{ default => async + })} + , {"async_batch_size", + sc(integer(), + #{ mapping => "gen_rpc.max_batch_size" + , default => 256 + })} + , {"port_discovery", + sc(union(manual, stateless), + #{ mapping => "gen_rpc.port_discovery" + , default => stateless + })} + , {"tcp_server_port", + sc(integer(), + #{ mapping => "gen_rpc.tcp_server_port" + , default => 5369 + })} + , {"tcp_client_num", + sc(range(1, 256), + #{ default => 1 + })} + , {"connect_timeout", + sc(emqx_schema:duration(), + #{ mapping => "gen_rpc.connect_timeout", + default => "5s" + })} + , {"send_timeout", + sc(emqx_schema:duration(), + #{ mapping => "gen_rpc.send_timeout" + , default => "5s" + })} + , {"authentication_timeout", + sc(emqx_schema:duration(), + #{ mapping=> "gen_rpc.authentication_timeout" + , default => "5s" + })} + , {"call_receive_timeout", + sc(emqx_schema:duration(), + #{ mapping => "gen_rpc.call_receive_timeout" + , default => "15s" + })} + , {"socket_keepalive_idle", + sc(emqx_schema:duration_s(), + #{ mapping => "gen_rpc.socket_keepalive_idle" + , default => "7200s" + })} + , {"socket_keepalive_interval", + sc(emqx_schema:duration_s(), + #{ mapping => "gen_rpc.socket_keepalive_interval", + default => "75s" + })} + , {"socket_keepalive_count", + sc(integer(), + #{ mapping => "gen_rpc.socket_keepalive_count" + , default => 9 + })} + , {"socket_sndbuf", + sc(emqx_schema:bytesize(), + #{ mapping => "gen_rpc.socket_sndbuf" + , default => "1MB" + })} + , {"socket_recbuf", + sc(emqx_schema:bytesize(), + #{ mapping => "gen_rpc.socket_recbuf" + , default => "1MB" + })} + , {"socket_buffer", + sc(emqx_schema:bytesize(), + #{ mapping => "gen_rpc.socket_buffer" + , default => "1MB" + })} ]; fields("log") -> [ {"console_handler", ref("console_handler")} - , {"file_handlers", ref("file_handlers")} - , {"error_logger", t(atom(), "kernel.error_logger", silent)} + , {"file_handlers", + sc(ref("file_handlers"), + #{})} + , {"error_logger", + sc(atom(), + #{mapping => "kernel.error_logger", + default => silent})} ]; fields("console_handler") -> - [ {"enable", t(boolean(), undefined, false)} + [ {"enable", + sc(boolean(), + #{ default => false + })} ] ++ log_handler_common_confs(); fields("file_handlers") -> - [ {"$name", ref("log_file_handler")} + [ {"$name", + sc(ref("log_file_handler"), + #{})} ]; fields("log_file_handler") -> - [ {"file", t(file(), undefined, undefined)} - , {"rotation", ref("log_rotation")} - , {"max_size", #{type => union([infinity, emqx_schema:bytesize()]), - default => "10MB"}} + [ {"file", + sc(file(), + #{})} + , {"rotation", + sc(ref("log_rotation"), + #{})} + , {"max_size", + sc(union([infinity, emqx_schema:bytesize()]), + #{ default => "10MB" + })} ] ++ log_handler_common_confs(); fields("log_rotation") -> - [ {"enable", t(boolean(), undefined, true)} - , {"count", t(range(1, 2048), undefined, 10)} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"count", + sc(range(1, 2048), + #{ default => 10 + })} ]; fields("log_overload_kill") -> - [ {"enable", t(boolean(), undefined, true)} - , {"mem_size", t(emqx_schema:bytesize(), undefined, "30MB")} - , {"qlen", t(integer(), undefined, 20000)} - , {"restart_after", t(union(emqx_schema:duration(), infinity), undefined, "5s")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"mem_size", + sc(emqx_schema:bytesize(), + #{ default => "30MB" + })} + , {"qlen", + sc(integer(), + #{ default => 20000 + })} + , {"restart_after", + sc(union(emqx_schema:duration(), infinity), + #{ default => "5s" + })} ]; fields("log_burst_limit") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_count", t(integer(), undefined, 10000)} - , {"window_time", t(emqx_schema:duration(), undefined, "1s")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"max_count", + sc(integer(), + #{ default => 10000 + })} + , {"window_time", + sc(emqx_schema:duration(), + #{default => "1s"})} ]; fields("authorization") -> emqx_schema:fields("authorization") ++ - emqx_authz_schema:fields("authorization"); - -fields(Name) -> - find_field(Name, ?MERGED_CONFIGS). - -find_field(Name, []) -> - error({unknown_config_struct_field, Name}); -find_field(Name, [SchemaModule | Rest]) -> - case lists:member(bin(Name), hocon_schema:root_names(SchemaModule)) of - true -> SchemaModule:fields(Name); - false -> find_field(Name, Rest) - end. + emqx_authz_schema:fields("authorization"). translations() -> ["ekka", "kernel", "emqx"]. @@ -302,20 +541,52 @@ tr_logger(Conf) -> [{handler, default, undefined}] ++ ConsoleHandler ++ FileHandlers. log_handler_common_confs() -> - [ {"level", t(log_level(), undefined, warning)} - , {"time_offset", t(string(), undefined, "system")} - , {"chars_limit", #{type => hoconsc:union([unlimited, range(1, inf)]), - default => unlimited - }} - , {"formatter", t(union([text, json]), undefined, text)} - , {"single_line", t(boolean(), undefined, true)} - , {"sync_mode_qlen", t(integer(), undefined, 100)} - , {"drop_mode_qlen", t(integer(), undefined, 3000)} - , {"flush_qlen", t(integer(), undefined, 8000)} - , {"overload_kill", ref("log_overload_kill")} - , {"burst_limit", ref("log_burst_limit")} - , {"supervisor_reports", t(union([error, progress]), undefined, error)} - , {"max_depth", t(union([unlimited, integer()]), undefined, 100)} + [ {"level", + sc(log_level(), + #{ default => warning + })} + , {"time_offset", + sc(string(), + #{ default => "system" + })} + , {"chars_limit", + sc(hoconsc:union([unlimited, range(1, inf)]), + #{ default => unlimited + })} + , {"formatter", + sc(union([text, json]), + #{ default => text + })} + , {"single_line", + sc(boolean(), + #{ default => true + })} + , {"sync_mode_qlen", + sc(integer(), + #{ default => 100 + })} + , {"drop_mode_qlen", + sc(integer(), + #{ default => 3000 + })} + , {"flush_qlen", + sc(integer(), + #{ default => 8000 + })} + , {"overload_kill", + sc(ref("log_overload_kill"), + #{})} + , {"burst_limit", + sc(ref("log_burst_limit"), + #{})} + , {"supervisor_reports", + sc(union([error, progress]), + #{ default => error + })} + , {"max_depth", + sc(union([unlimited, integer()]), + #{ default => 100 + })} ]. log_handler_conf(Conf) -> @@ -424,18 +695,9 @@ keys(Parent, Conf) -> %% types -t(Type) -> hoconsc:t(Type). +sc(Type, Meta) -> hoconsc:mk(Type, Meta). -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:t(hoconsc:ref(Field)). +ref(Field) -> hoconsc:ref(?MODULE, Field). options(static, Conf) -> [{seeds, [to_atom(S) || S <- conf_get("cluster.static.seeds", Conf, [])]}]; @@ -475,6 +737,6 @@ to_atom(Str) when is_list(Str) -> to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). -bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); -bin(Bin) when is_binary(Bin) -> Bin; -bin(L) when is_list(L) -> iolist_to_binary(L). +roots(Module) -> + lists:map(fun({_BinName, Root}) -> Root end, + maps:to_list(hocon_schema:roots(Module))). diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl index d21f0e106..fce71ad7b 100644 --- a/apps/emqx_management/src/emqx_management_schema.erl +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -19,9 +19,12 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> management. + roots() -> []. fields(_) -> []. diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl index 7a6b72a8a..15f6ab901 100644 --- a/apps/emqx_modules/src/emqx_modules_schema.erl +++ b/apps/emqx_modules/src/emqx_modules_schema.erl @@ -20,9 +20,12 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> modules. + roots() -> ["delayed", "recon", @@ -33,32 +36,34 @@ roots() -> fields(Name) when Name =:= "recon"; Name =:= "telemetry" -> - [ {enable, emqx_schema:t(boolean(), undefined, false)} + [ {enable, hoconsc:mk(boolean(), #{default => false})} ]; fields("delayed") -> - [ {enable, emqx_schema:t(boolean(), undefined, false)} - , {max_delayed_messages, emqx_schema:t(integer())} + [ {enable, hoconsc:mk(boolean(), #{default => false})} + , {max_delayed_messages, sc(integer(), #{})} ]; fields("rewrite") -> [ {action, hoconsc:enum([publish, subscribe])} - , {source_topic, emqx_schema:t(binary())} - , {re, emqx_schema:t(binary())} - , {dest_topic, emqx_schema:t(binary())} + , {source_topic, sc(binary(), #{})} + , {re, sc(binary(), #{})} + , {dest_topic, sc(binary(), #{})} ]; fields("event_message") -> - [ {"$event/client_connected", emqx_schema:t(boolean(), undefined, false)} - , {"$event/client_disconnected", emqx_schema:t(boolean(), undefined, false)} - , {"$event/client_subscribed", emqx_schema:t(boolean(), undefined, false)} - , {"$event/client_unsubscribed", emqx_schema:t(boolean(), undefined, false)} - , {"$event/message_delivered", emqx_schema:t(boolean(), undefined, false)} - , {"$event/message_acked", emqx_schema:t(boolean(), undefined, false)} - , {"$event/message_dropped", emqx_schema:t(boolean(), undefined, false)} + [ {"$event/client_connected", sc(boolean(), #{default => false})} + , {"$event/client_disconnected", sc(boolean(), #{default => false})} + , {"$event/client_subscribed", sc(boolean(), #{default => false})} + , {"$event/client_unsubscribed", sc(boolean(), #{default => false})} + , {"$event/message_delivered", sc(boolean(), #{default => false})} + , {"$event/message_acked", sc(boolean(), #{default => false})} + , {"$event/message_dropped", sc(boolean(), #{default => false})} ]; fields("topic_metrics") -> - [{topic, emqx_schema:t(binary())}]. + [{topic, sc(binary(), #{})}]. -array(Name) -> {Name, hoconsc:array(hoconsc:ref(Name))}. +array(Name) -> {Name, hoconsc:array(hoconsc:ref(?MODULE, Name))}. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index 47630b58d..922de6238 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -19,13 +19,18 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> "prometheus". + roots() -> ["prometheus"]. fields("prometheus") -> - [ {push_gateway_server, emqx_schema:t(string())} - , {interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "15s")} - , {enable, emqx_schema:t(boolean(), undefined, false)} + [ {push_gateway_server, sc(string(), #{})} + , {interval, sc(emqx_schema:duration_ms(), #{default => "15s"})} + , {enable, sc(boolean(), #{default => false})} ]. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl index e2acc7fe7..55cfa2fcc 100644 --- a/apps/emqx_retainer/src/emqx_retainer_schema.erl +++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl @@ -4,40 +4,40 @@ -export([roots/0, fields/1]). --define(TYPE(Type), hoconsc:t(Type)). +-define(TYPE(Type), hoconsc:mk(Type)). roots() -> ["emqx_retainer"]. fields("emqx_retainer") -> - [ {enable, t(boolean(), false)} - , {msg_expiry_interval, t(emqx_schema:duration_ms(), "0s")} - , {msg_clear_interval, t(emqx_schema:duration_ms(), "0s")} + [ {enable, sc(boolean(), false)} + , {msg_expiry_interval, sc(emqx_schema:duration_ms(), "0s")} + , {msg_clear_interval, sc(emqx_schema:duration_ms(), "0s")} , {flow_control, ?TYPE(hoconsc:ref(?MODULE, flow_control))} - , {max_payload_size, t(emqx_schema:bytesize(), "1MB")} + , {max_payload_size, sc(emqx_schema:bytesize(), "1MB")} , {config, config()} ]; fields(mnesia_config) -> [ {type, ?TYPE(hoconsc:union([built_in_database]))} - , {storage_type, t(hoconsc:union([ram, disc, disc_only]), ram)} - , {max_retained_messages, t(integer(), 0, fun is_pos_integer/1)} + , {storage_type, sc(hoconsc:union([ram, disc, disc_only]), ram)} + , {max_retained_messages, sc(integer(), 0, fun is_pos_integer/1)} ]; fields(flow_control) -> - [ {max_read_number, t(integer(), 0, fun is_pos_integer/1)} - , {msg_deliver_quota, t(integer(), 0, fun is_pos_integer/1)} - , {quota_release_interval, t(emqx_schema:duration_ms(), "0ms")} + [ {max_read_number, sc(integer(), 0, fun is_pos_integer/1)} + , {msg_deliver_quota, sc(integer(), 0, fun is_pos_integer/1)} + , {quota_release_interval, sc(emqx_schema:duration_ms(), "0ms")} ]. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -t(Type, Default) -> - hoconsc:t(Type, #{default => Default}). +sc(Type, Default) -> + hoconsc:mk(Type, #{default => Default}). -t(Type, Default, Validator) -> - hoconsc:t(Type, #{default => Default, - validator => Validator}). +sc(Type, Default, Validator) -> + hoconsc:mk(Type, #{default => Default, + validator => Validator}). is_pos_integer(V) -> V >= 0. diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl index a9646ae1e..2614fb8b1 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -20,10 +20,13 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). -roots() -> ["emqx_rule_engine"]. +namespace() -> rule_engine. -fields("emqx_rule_engine") -> - [{ignore_sys_message, emqx_schema:t(boolean(), undefined, true)}]. +roots() -> ["rule_engine"]. + +fields("rule_engine") -> + [{ignore_sys_message, hoconsc:mk(boolean(), #{default => true})}]. diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 8154170b0..a0960df25 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -594,5 +594,6 @@ printable_maps(Headers) -> end, #{}, Headers). ignore_sys_message(#message{flags = Flags}) -> + ConfigRootKey = emqx_rule_engine_schema:namespace(), maps:get(sys, Flags, false) andalso - emqx:get_config([emqx_rule_engine, ignore_sys_message]). + emqx:get_config([ConfigRootKey, ignore_sys_message]). diff --git a/apps/emqx_statsd/src/emqx_statsd_schema.erl b/apps/emqx_statsd/src/emqx_statsd_schema.erl index 906f55a4c..72b245f4a 100644 --- a/apps/emqx_statsd/src/emqx_statsd_schema.erl +++ b/apps/emqx_statsd/src/emqx_statsd_schema.erl @@ -6,15 +6,18 @@ -export([to_ip_port/1]). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). -typerefl_from_string({ip_port/0, emqx_statsd_schema, to_ip_port}). +namespace() -> "statsd". + roots() -> ["statsd"]. fields("statsd") -> - [ {enable, emqx_schema:t(boolean(), undefined, false)} + [ {enable, hoconsc:mk(boolean(), #{default => false})} , {server, fun server/1} , {sample_time_interval, fun duration_ms/1} , {flush_time_interval, fun duration_ms/1} diff --git a/rebar.config b/rebar.config index 4e622b738..690e23bc7 100644 --- a/rebar.config +++ b/rebar.config @@ -60,7 +60,7 @@ , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. From f5bfa4cd43521f2d6854faffa86836702be7e7c9 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 18:32:23 +0800 Subject: [PATCH 258/306] fix: deny creating metrics for empty topics (#5650) * fix: deny null topic create metrics --- apps/emqx_modules/src/emqx_topic_metrics_api.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index e22f5750f..98ae249cb 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -94,7 +94,7 @@ topic_metrics_api() -> responses => #{ <<"200">> => schema(<<"Create topic metrics success">>), <<"409">> => error_schema(<<"Topic metrics max limit">>, [?EXCEED_LIMIT]), - <<"400">> => error_schema(<<"Topic metrics already exist">>, [?BAD_REQUEST]) + <<"400">> => error_schema(<<"Topic metrics already exist or bad topic">>, [?BAD_REQUEST]) } } }, @@ -137,6 +137,8 @@ topic_metrics(put, #{body := #{<<"topic">> := Topic, <<"action">> := <<"reset">> reset(Topic); topic_metrics(put, #{body := #{<<"action">> := <<"reset">>}}) -> reset(); +topic_metrics(post, #{body := #{<<"topic">> := <<>>}}) -> + {400, 'BAD_REQUEST', <<"Topic can not be empty">>}; topic_metrics(post, #{body := #{<<"topic">> := Topic}}) -> register(Topic). From 19aff7bfddfda5859e77438abcb222c363f3abca Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 3 Sep 2021 11:44:43 +0200 Subject: [PATCH 259/306] fix(authz): schema fields used directly. --- apps/emqx/src/emqx_schema.erl | 9 +++++---- apps/emqx_authz/src/emqx_authz.erl | 6 +++--- apps/emqx_authz/src/emqx_authz_schema.erl | 2 ++ apps/emqx_connector/src/emqx_connector_mongo.erl | 3 ++- apps/emqx_machine/src/emqx_machine_schema.erl | 8 +++++++- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index fe4439aaa..0189a468b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -93,7 +93,8 @@ roots() -> "plugins", "stats", "sysmon", - "alarm" + "alarm", + "authorization" ]. fields("stats") -> @@ -113,13 +114,13 @@ fields("authorization") -> #{ default => ignore })} , {"cache", - sc(ref("authorization_cache"), + sc(ref(?MODULE, "cache"), #{ }) } ]; -fields("authorization_cache") -> +fields("cache") -> [ {"enable", sc(boolean(), #{ default => true @@ -276,7 +277,7 @@ fields("zones") -> )}]; fields("zone_settings") -> - Fields = ["mqtt", "stats", "authorization", "flapping_detect", "force_shutdown", + Fields = ["mqtt", "stats", "flapping_detect", "force_shutdown", "conn_congestion", "rate_limit", "quota", "force_gc"], [{F, ref(emqx_zone_schema, F)} || F <- Fields]; diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 7fcd80269..af77390d5 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -350,9 +350,9 @@ do_authorize(Client, PubSub, Topic, %%-------------------------------------------------------------------- check_sources(RawSources) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), + Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}}, + Conf = #{<<"sources">> => RawSources}, + #{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true}), Sources. find_source_by_type(Type) -> find_source_by_type(Type, lookup()). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 0645990a8..b90d522e8 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -20,6 +20,8 @@ namespace() -> authz. +%% @doc authorization schema is not exported +%% but directly used by emqx_schema roots() -> []. fields("authorization") -> diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index c95679f32..0b769748a 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -82,7 +82,8 @@ mongo_fields() -> , {auth_source, #{type => binary(), nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} - , {topology, #{type => hoconsc:ref(?MODULE, topology)}} + , {topology, #{type => hoconsc:ref(?MODULE, topology), + nullable => true}} ] ++ emqx_connector_schema_lib:ssl_fields(). diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index a3e7e9388..657594ae8 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -61,7 +61,13 @@ namespace() -> undefined. roots() -> - ["cluster", "node", "rpc", "log"] ++ lists:flatmap(fun roots/1, ?MERGED_CONFIGS). + %% This is a temp workaround to define part of authorization config + %% in emqx_schema and part of it in emqx_authz_schema but then + %% merged here in this module + %% The proper fix should be to make connection (channel, session) state + %% extendable by e.g. allow hooks be stateful. + ["cluster", "node", "rpc", "log", "authorization"] ++ + lists:keydelete("authorization", 1, lists:flatmap(fun roots/1, ?MERGED_CONFIGS)). fields("cluster") -> [ {"name", From c42c1e698a88fddc6377b549664a3dc1edfa5dea Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 3 Sep 2021 14:56:00 +0800 Subject: [PATCH 260/306] chore(gw): add From param for _channel:handle_call/3 --- .../src/bhvrs/emqx_gateway_channel.erl | 8 +++- .../src/bhvrs/emqx_gateway_conn.erl | 14 +++++-- .../src/coap/emqx_coap_channel.erl | 4 +- .../src/exproto/emqx_exproto_channel.erl | 31 +++++++-------- .../src/lwm2m/emqx_lwm2m_channel.erl | 4 +- .../src/mqttsn/emqx_sn_channel.erl | 33 ++++++++-------- .../src/stomp/emqx_stomp_channel.erl | 38 +++++++++---------- 7 files changed, 72 insertions(+), 60 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl index 06efe4fd0..c4fd114e4 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl @@ -44,6 +44,8 @@ -type conn_state() :: idle | connecting | connected | disconnected | atom(). +-type gen_server_from() :: {pid(), Tag :: term()}. + -type reply() :: {outgoing, emqx_gateway_frame:packet()} | {outgoing, [emqx_gateway_frame:packet()]} | {event, conn_state() | updated} @@ -71,11 +73,13 @@ | {shutdown, Reason :: any(), channel()}. %% @doc Handle the custom gen_server:call/2 for its connection process --callback handle_call(Req :: any(), channel()) +-callback handle_call(Req :: any(), From :: gen_server_from(), channel()) -> {reply, Reply :: any(), channel()} %% Reply to caller and trigger an event(s) | {reply, Reply :: any(), - EventOrEvents:: tuple() | list(tuple()), channel()} + EventOrEvents :: tuple() | list(tuple()), channel()} + | {noreply, channel()} + | {noreply, EventOrEvents :: tuple() | list(tuple()), channel()} | {shutdown, Reason :: any(), Reply :: any(), channel()} %% Shutdown the process, reply to caller and write a packet to client | {shutdown, Reason :: any(), Reply :: any(), diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index 528f8dfd8..51bcbd358 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -394,6 +394,10 @@ append_msg(Q, Msg) -> handle_msg({'$gen_call', From, Req}, State) -> case handle_call(From, Req, State) of + {noreply, NState} -> + {ok, NState}; + {noreply, Msgs, NState} -> + {ok, next_msgs(Msgs), NState}; {reply, Reply, NState} -> gen_server:reply(From, Reply), {ok, NState}; @@ -545,10 +549,14 @@ handle_call(_From, info, State) -> handle_call(_From, stats, State) -> {reply, stats(State), State}; -handle_call(_From, Req, State = #state{ +handle_call(From, Req, State = #state{ chann_mod = ChannMod, channel = Channel}) -> - case ChannMod:handle_call(Req, Channel) of + case ChannMod:handle_call(Req, From, Channel) of + {noreply, NChannel} -> + {noreply, State#state{channel = NChannel}}; + {noreply, Msgs, NChannel} -> + {noreply, Msgs, State#state{channel = NChannel}}; {reply, Reply, NChannel} -> {reply, Reply, State#state{channel = NChannel}}; {reply, Reply, Msgs, NChannel} -> @@ -559,8 +567,6 @@ handle_call(_From, Req, State = #state{ NState = State#state{channel = NChannel}, ok = handle_outgoing(Packet, NState), shutdown(Reason, Reply, NState) - - end. %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 24f06549b..6e554b9ef 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -34,7 +34,7 @@ , terminate/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -165,7 +165,7 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl index ace9a7be5..3de231958 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl @@ -31,7 +31,7 @@ , handle_in/2 , handle_deliver/2 , handle_timeout/3 - , handle_call/2 + , handle_call/3 , handle_cast/2 , handle_info/2 , terminate/2 @@ -243,23 +243,24 @@ handle_timeout(_TRef, Msg, Channel) -> ?WARN("Unexpected timeout: ~p", [Msg]), {ok, Channel}. --spec handle_call(any(), channel()) +-spec handle_call(Req :: any(), From :: any(), channel()) -> {reply, Reply :: term(), channel()} | {reply, Reply :: term(), replies(), channel()} | {shutdown, Reason :: term(), Reply :: term(), channel()}. -handle_call({send, Data}, Channel) -> +handle_call({send, Data}, _From, Channel) -> {reply, ok, [{outgoing, Data}], Channel}; -handle_call(close, Channel = #channel{conn_state = connected}) -> +handle_call(close, _From, Channel = #channel{conn_state = connected}) -> {reply, ok, [{event, disconnected}, {close, normal}], Channel}; -handle_call(close, Channel) -> +handle_call(close, _From, Channel) -> {reply, ok, [{close, normal}], Channel}; -handle_call({auth, ClientInfo, _Password}, Channel = #channel{conn_state = connected}) -> +handle_call({auth, ClientInfo, _Password}, _From, + Channel = #channel{conn_state = connected}) -> ?LOG(warning, "Duplicated authorized command, dropped ~p", [ClientInfo]), {reply, {error, ?RESP_PERMISSION_DENY, <<"Duplicated authenticate command">>}, Channel}; -handle_call({auth, ClientInfo0, Password}, +handle_call({auth, ClientInfo0, Password}, _From, Channel = #channel{ ctx = Ctx, conninfo = ConnInfo, @@ -300,7 +301,7 @@ handle_call({auth, ClientInfo0, Password}, {reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel} end; -handle_call({start_timer, keepalive, Interval}, +handle_call({start_timer, keepalive, Interval}, _From, Channel = #channel{ conninfo = ConnInfo, clientinfo = ClientInfo @@ -310,7 +311,7 @@ handle_call({start_timer, keepalive, Interval}, NChannel = Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo}, {reply, ok, ensure_keepalive(NChannel)}; -handle_call({subscribe_from_client, TopicFilter, Qos}, +handle_call({subscribe_from_client, TopicFilter, Qos}, _From, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -323,20 +324,20 @@ handle_call({subscribe_from_client, TopicFilter, Qos}, {reply, ok, NChannel} end; -handle_call({subscribe, Topic, SubOpts}, Channel) -> +handle_call({subscribe, Topic, SubOpts}, _From, Channel) -> {ok, NChannel} = do_subscribe([{Topic, SubOpts}], Channel), {reply, ok, NChannel}; -handle_call({unsubscribe_from_client, TopicFilter}, +handle_call({unsubscribe_from_client, TopicFilter}, _From, Channel = #channel{conn_state = connected}) -> {ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel), {reply, ok, NChannel}; -handle_call({unsubscribe, Topic}, Channel) -> +handle_call({unsubscribe, Topic}, _From, Channel) -> {ok, NChannel} = do_unsubscribe([Topic], Channel), {reply, ok, NChannel}; -handle_call({publish, Topic, Qos, Payload}, +handle_call({publish, Topic, Qos, Payload}, _From, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -353,10 +354,10 @@ handle_call({publish, Topic, Qos, Payload}, {reply, ok, Channel} end; -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> {shutdown, kicked, ok, Channel}; -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(warning, "Unexpected call: ~p", [Req]), {reply, {error, unexpected_call}, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl index 80078407b..b67032313 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -35,7 +35,7 @@ , terminate/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -152,7 +152,7 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 2834c27f6..d9576c253 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -38,7 +38,7 @@ , set_conn_state/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -1113,12 +1113,13 @@ message_to_packet(MsgId, Message, %% 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({subscribe, Topic, SubOpts}, Channel) -> +-spec handle_call(Req :: term(), From :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {reply, Reply :: term(), replies(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), + emqx_types:packet(), channel()}. +handle_call({subscribe, Topic, SubOpts}, _From, Channel) -> %% XXX: Only support short_topic_name SubProps = maps:get(sub_props, SubOpts, #{}), case maps:get(subtype, SubProps, short_topic_name) of @@ -1141,26 +1142,26 @@ handle_call({subscribe, Topic, SubOpts}, Channel) -> reply({error, only_support_short_name_topic}, Channel) end; -handle_call({unsubscribe, Topic}, Channel) -> +handle_call({unsubscribe, Topic}, _From, Channel) -> TopicFilters = [emqx_topic:parse(Topic)], {ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel), reply(ok, NChannel); -handle_call(subscriptions, Channel = #channel{session = Session}) -> +handle_call(subscriptions, _From, Channel = #channel{session = Session}) -> reply(maps:to_list(emqx_session:info(subscriptions, Session)), Channel); -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); -handle_call(discard, Channel) -> +handle_call(discard, _From, Channel) -> shutdown_and_reply(discarded, ok, Channel); %% XXX: No Session Takeover -%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) -> % reply(Session, Channel#channel{takeover = true}); % -%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +%handle_call({takeover, 'end'}, _From, Channel = #channel{session = Session, % pendings = Pendings}) -> % ok = emqx_session:takeover(Session), % %% TODO: Should not drain deliver here (side effect) @@ -1168,16 +1169,16 @@ handle_call(discard, Channel) -> % AllPendings = lists:append(Delivers, Pendings), % shutdown_and_reply(takeovered, AllPendings, Channel); -%handle_call(list_authz_cache, Channel) -> +%handle_call(list_authz_cache, _From, Channel) -> % {reply, emqx_authz_cache:list_authz_cache(), Channel}; %% XXX: No Quota Now -% handle_call({quota, Policy}, Channel) -> +% handle_call({quota, Policy}, _From, Channel) -> % Zone = info(zone, Channel), % Quota = emqx_limiter:init(Zone, Policy), % reply(ok, Channel#channel{quota = Quota}); -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(ignored, Channel). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 9bd2dac1b..673535ebd 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -39,7 +39,7 @@ , set_conn_state/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -586,10 +586,10 @@ do_subscribe([{ParsedTopic, SubOpts0}|More], %%-------------------------------------------------------------------- -spec(handle_out(atom(), term(), channel()) - -> {ok, channel()} - | {ok, replies(), channel()} - | {shutdown, Reason :: term(), channel()} - | {shutdown, Reason :: term(), replies(), 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), @@ -620,11 +620,12 @@ handle_out(receipt, ReceiptId, Channel) -> %% Handle call %%-------------------------------------------------------------------- --spec(handle_call(Req :: term(), channel()) - -> {reply, Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). -handle_call({subscribe, Topic, SubOpts}, +-spec(handle_call(Req :: term(), From :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {reply, Reply :: term(), replies(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). +handle_call({subscribe, Topic, SubOpts}, _From, Channel = #channel{ subscriptions = Subs }) -> @@ -653,7 +654,7 @@ handle_call({subscribe, Topic, SubOpts}, end end; -handle_call({unsubscribe, Topic}, +handle_call({unsubscribe, Topic}, _From, Channel = #channel{ ctx = Ctx, clientinfo = ClientInfo = #{mountpoint := Mountpoint}, @@ -670,27 +671,27 @@ handle_call({unsubscribe, Topic}, ); %% Reply :: [{emqx_types:topic(), emqx_types:subopts()}] -handle_call(subscriptions, Channel = #channel{subscriptions = Subs}) -> +handle_call(subscriptions, _From, Channel = #channel{subscriptions = Subs}) -> Reply = lists:map( fun({_SubId, Topic, _Ack, SubOpts}) -> {Topic, SubOpts} end, Subs), reply(Reply, Channel); -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), shutdown_and_reply(kicked, ok, Frame, NChannel); -handle_call(discard, Channel) -> +handle_call(discard, _From, Channel) -> Frame = error_frame(undefined, <<"Discarded">>), shutdown_and_reply(discarded, ok, Frame, Channel); %% XXX: No Session Takeover -%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) -> % reply(Session, Channel#channel{takeover = true}); % -%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +%handle_call({takeover, 'end'}, _From, Channel = #channel{session = Session, % pendings = Pendings}) -> % ok = emqx_session:takeover(Session), % %% TODO: Should not drain deliver here (side effect) @@ -698,7 +699,7 @@ handle_call(discard, Channel) -> % AllPendings = lists:append(Delivers, Pendings), % shutdown_and_reply(takeovered, AllPendings, Channel); -handle_call(list_authz_cache, Channel) -> +handle_call(list_authz_cache, _From, Channel) -> %% This won't work {reply, emqx_authz_cache:list_authz_cache(), Channel}; @@ -708,11 +709,10 @@ handle_call(list_authz_cache, Channel) -> % Quota = emqx_limiter:init(Zone, Policy), % reply(ok, Channel#channel{quota = Quota}); -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(ignored, Channel). - %%-------------------------------------------------------------------- %% Handle cast %%-------------------------------------------------------------------- From f514f0c89b4e32b05d15d1d78eb07757dd48d9b2 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:36:56 +0800 Subject: [PATCH 261/306] feat: minirest support swagger UI new version (#5658) --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 690e23bc7..b1616bebc 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.1"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.2"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, "0.3.3"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From b1023d973344a62a391f03c97fd6c7cf86e625a0 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:32:29 +0800 Subject: [PATCH 262/306] fix: clients ip address params trans (#5657) * fix: clients ip address params trans --- apps/emqx_management/src/emqx_mgmt_api.erl | 7 +++++++ apps/emqx_management/src/emqx_mgmt_api_clients.erl | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 5a7b020d7..8cf2fa1cb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -289,6 +289,7 @@ to_type_(V, atom) -> to_atom(V); to_type_(V, integer) -> to_integer(V); to_type_(V, timestamp) -> to_timestamp(V); to_type_(V, ip) -> aton(V); +to_type_(V, ip_port) -> to_ip_port(V); to_type_(V, _) -> V. to_atom(A) when is_atom(A) -> @@ -309,6 +310,12 @@ to_timestamp(B) when is_binary(B) -> aton(B) when is_binary(B) -> list_to_tuple([binary_to_integer(T) || T <- re:split(B, "[.]")]). +to_ip_port(IPAddress) -> + [IP0, Port0] = string:tokens(binary_to_list(IPAddress), ":"), + {ok, IP} = inet:parse_address(IP0), + Port = list_to_integer(Port0), + {IP, Port}. + %%-------------------------------------------------------------------- %% EUnits %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index dd4df58a5..4b558eaae 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -45,7 +45,7 @@ [ {<<"node">>, atom} , {<<"username">>, binary} , {<<"zone">>, atom} - , {<<"ip_address">>, ip} + , {<<"ip_address">>, ip_port} , {<<"conn_state">>, atom} , {<<"clean_start">>, atom} , {<<"proto_name">>, binary} @@ -566,10 +566,10 @@ ms(username, X) -> #{clientinfo => #{username => X}}; ms(zone, X) -> #{clientinfo => #{zone => X}}; -ms(ip_address, X) -> - #{clientinfo => #{peerhost => X}}; ms(conn_state, X) -> #{conn_state => X}; +ms(ip_address, X) -> + #{conninfo => #{peername => X}}; ms(clean_start, X) -> #{conninfo => #{clean_start => X}}; ms(proto_name, X) -> From e998770f2e1b0f2a2c1b40968f38ad4970d94f02 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Mon, 6 Sep 2021 18:46:08 +0800 Subject: [PATCH 263/306] refactor(authn): refactor to support global and listener authentication --- apps/emqx/include/emqx.hrl | 16 + apps/emqx/src/emqx_authentication.erl | 731 +++++++++ apps/emqx/src/emqx_broker_sup.erl | 10 +- apps/emqx/src/emqx_config_handler.erl | 2 + apps/emqx/src/emqx_listeners.erl | 4 + apps/emqx/src/emqx_metrics.erl | 2 - apps/emqx/src/emqx_schema.erl | 7 +- apps/emqx_authn/etc/emqx_authn.conf | 43 +- apps/emqx_authn/include/emqx_authn.hrl | 19 +- apps/emqx_authn/rebar.config | 4 +- apps/emqx_authn/src/emqx_authn.erl | 637 -------- apps/emqx_authn/src/emqx_authn_api.erl | 1405 ++++++++++------- apps/emqx_authn/src/emqx_authn_app.erl | 51 +- apps/emqx_authn/src/emqx_authn_schema.erl | 49 +- apps/emqx_authn/src/emqx_authn_sup.erl | 8 +- .../emqx_enhanced_authn_scram_mnesia.erl | 20 +- .../src/simple_authn/emqx_authn_http.erl | 63 +- .../src/simple_authn/emqx_authn_jwt.erl | 17 +- .../src/simple_authn/emqx_authn_mnesia.erl | 21 +- .../src/simple_authn/emqx_authn_mongodb.erl | 19 +- .../src/simple_authn/emqx_authn_mysql.erl | 17 +- .../src/simple_authn/emqx_authn_pgsql.erl | 22 +- .../src/simple_authn/emqx_authn_redis.erl | 20 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 98 -- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 234 +-- .../test/emqx_authn_mnesia_SUITE.erl | 238 +-- .../src/emqx_connector_mongo.erl | 11 +- .../src/emqx_connector_redis.erl | 14 +- apps/emqx_gateway/etc/emqx_gateway.conf | 20 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 40 +- apps/emqx_machine/src/emqx_machine_schema.erl | 1 - apps/emqx_retainer/src/emqx_retainer.erl | 2 - rebar.config | 1 + 33 files changed, 2050 insertions(+), 1796 deletions(-) create mode 100644 apps/emqx/src/emqx_authentication.erl diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 633527b57..63ab13256 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -134,3 +134,19 @@ }). -endif. + +%%-------------------------------------------------------------------- +%% Authentication +%%-------------------------------------------------------------------- + +-record(authenticator, + { id :: binary() + , provider :: module() + , enable :: boolean() + , state :: map() + }). + +-record(chain, + { name :: binary() + , authenticators :: [#authenticator{}] + }). \ No newline at end of file diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl new file mode 100644 index 000000000..2b561d298 --- /dev/null +++ b/apps/emqx/src/emqx_authentication.erl @@ -0,0 +1,731 @@ +%%-------------------------------------------------------------------- +%% 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). + +-behaviour(gen_server). +-behaviour(hocon_schema). +-behaviour(emqx_config_handler). + +-include("emqx.hrl"). +-include("logger.hrl"). + +-export([ roots/0 + , fields/1 + ]). + +-export([ pre_config_update/2 + , post_config_update/4 + ]). + +-export([ authenticate/2 + ]). + +-export([ initialize_authentication/2 ]). + +-export([ start_link/0 + , stop/0 + ]). + +-export([ add_provider/2 + , remove_provider/1 + , create_chain/1 + , delete_chain/1 + , lookup_chain/1 + , list_chains/0 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/3 + , lookup_authenticator/2 + , list_authenticators/1 + , move_authenticator/3 + ]). + +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 + ]). + +-export([ generate_id/1 ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-define(CHAINS_TAB, emqx_authn_chains). + +-define(VER_1, <<"1">>). +-define(VER_2, <<"2">>). + +-type config() :: #{atom() => term()}. +-type state() :: #{atom() => term()}. +-type extra() :: #{superuser := boolean(), + atom() => term()}. +-type user_info() :: #{user_id := binary(), + atom() => term()}. + +-callback refs() -> [{ref, Module, Name}] when Module::module(), Name::atom(). + +-callback create(Config) + -> {ok, State} + | {error, term()} + when Config::config(), State::state(). + +-callback update(Config, State) + -> {ok, NewState} + | {error, term()} + when Config::config(), State::state(), NewState::state(). + +-callback authenticate(Credential, State) + -> ignore + | {ok, Extra} + | {ok, Extra, AuthData} + | {continue, AuthCache} + | {continue, AuthData, AuthCache} + | {error, term()} + when Credential::map(), State::state(), Extra::extra(), AuthData::binary(), AuthCache::map(). + +-callback destroy(State) + -> ok + when State::state(). + +-callback import_users(Filename, State) + -> ok + | {error, term()} + when Filename::binary(), State::state(). + +-callback add_user(UserInfo, State) + -> {ok, User} + | {error, term()} + when UserInfo::user_info(), State::state(), User::user_info(). + +-callback delete_user(UserID, State) + -> ok + | {error, term()} + when UserID::binary(), State::state(). + +-callback update_user(UserID, UserInfo, State) + -> {ok, User} + | {error, term()} + when UserID::binary, UserInfo::map(), State::state(), User::user_info(). + +-callback list_users(State) + -> {ok, Users} + when State::state(), Users::[user_info()]. + +-optional_callbacks([ import_users/2 + , add_user/2 + , delete_user/2 + , update_user/3 + , list_users/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +roots() -> [{authentication, fun authentication/1}]. + +fields(_) -> []. + +authentication(type) -> + {ok, Refs} = get_refs(), + hoconsc:union([hoconsc:array(hoconsc:union(Refs)) | Refs]); +authentication(default) -> []; +authentication(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% Callbacks of config handler +%%------------------------------------------------------------------------------ + +pre_config_update(UpdateReq, OldConfig) -> + case do_pre_config_update(UpdateReq, to_list(OldConfig)) of + {error, Reason} -> {error, Reason}; + {ok, NewConfig} -> {ok, may_to_map(NewConfig)} + end. + +do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) -> + {ok, OldConfig ++ [Config]}; +do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) -> + NewConfig = lists:filter(fun(OldConfig0) -> + AuthenticatorID =/= generate_id(OldConfig0) + end, OldConfig), + {ok, NewConfig}; +do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) -> + NewConfig = lists:map(fun(OldConfig0) -> + case AuthenticatorID =:= generate_id(OldConfig0) of + true -> maps:merge(OldConfig0, Config); + false -> OldConfig0 + end + end, OldConfig), + {ok, NewConfig}; +do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) -> + case split_by_id(AuthenticatorID, OldConfig) of + {error, Reason} -> {error, Reason}; + {ok, Part1, [Found | Part2]} -> + case Position of + <<"top">> -> + {ok, [Found | Part1] ++ Part2}; + <<"bottom">> -> + {ok, Part1 ++ Part2 ++ [Found]}; + <<"before:", Before/binary>> -> + case split_by_id(Before, Part1 ++ Part2) of + {error, Reason} -> + {error, Reason}; + {ok, NPart1, [NFound | NPart2]} -> + {ok, NPart1 ++ [Found, NFound | NPart2]} + end; + _ -> + {error, {invalid_parameter, position}} + end + end. + +post_config_update(UpdateReq, NewConfig, OldConfig, AppEnvs) -> + do_post_config_update(UpdateReq, check_config(to_list(NewConfig)), OldConfig, AppEnvs). + +do_post_config_update({create_authenticator, ChainName, Config}, _NewConfig, _OldConfig, _AppEnvs) -> + NConfig = check_config(Config), + _ = create_chain(ChainName), + create_authenticator(ChainName, NConfig); + +do_post_config_update({delete_authenticator, ChainName, AuthenticatorID}, _NewConfig, _OldConfig, _AppEnvs) -> + delete_authenticator(ChainName, AuthenticatorID); + +do_post_config_update({update_authenticator, ChainName, AuthenticatorID, _Config}, NewConfig, _OldConfig, _AppEnvs) -> + [Config] = lists:filter(fun(NewConfig0) -> + AuthenticatorID =:= generate_id(NewConfig0) + end, NewConfig), + NConfig = check_config(Config), + update_authenticator(ChainName, AuthenticatorID, NConfig); + +do_post_config_update({move_authenticator, ChainName, AuthenticatorID, Position}, _NewConfig, _OldConfig, _AppEnvs) -> + NPosition = case Position of + <<"top">> -> top; + <<"bottom">> -> bottom; + <<"before:", Before/binary>> -> + {before, Before} + end, + move_authenticator(ChainName, AuthenticatorID, NPosition). + +check_config(Config) -> + #{authentication := CheckedConfig} = hocon_schema:check_plain(emqx_authentication, + #{<<"authentication">> => Config}, #{nullable => true, atom_key => true}), + CheckedConfig. + +%%------------------------------------------------------------------------------ +%% Authenticate +%%------------------------------------------------------------------------------ + +authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) -> + case ets:lookup(?CHAINS_TAB, Listener) of + [#chain{authenticators = Authenticators}] when Authenticators =/= [] -> + do_authenticate(Authenticators, Credential); + _ -> + case ets:lookup(?CHAINS_TAB, global_chain(Protocol)) of + [#chain{authenticators = Authenticators}] when Authenticators =/= [] -> + do_authenticate(Authenticators, Credential); + _ -> + ignore + end + end. + +do_authenticate([], _) -> + {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, Extra} + %% {ok, Extra, AuthData} + %% {continue, AuthCache} + %% {continue, AuthData, AuthCache} + %% {error, Reason} + {stop, Result} + end. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +initialize_authentication(_, []) -> + ok; +initialize_authentication(ChainName, AuthenticatorsConfig) -> + _ = create_chain(ChainName), + CheckedConfig = check_config(to_list(AuthenticatorsConfig)), + lists:foreach(fun(AuthenticatorConfig) -> + case create_authenticator(ChainName, AuthenticatorConfig) of + {ok, _} -> + ok; + {error, Reason} -> + ?LOG(error, "Failed to create authenticator '~s': ~p", [generate_id(AuthenticatorConfig), Reason]) + end + end, CheckedConfig). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:stop(?MODULE). + +get_refs() -> + gen_server:call(?MODULE, get_refs). + +add_provider(AuthNType, Provider) -> + gen_server:call(?MODULE, {add_provider, AuthNType, Provider}). + +remove_provider(AuthNType) -> + gen_server:call(?MODULE, {remove_provider, AuthNType}). + +create_chain(Name) -> + gen_server:call(?MODULE, {create_chain, Name}). + +delete_chain(Name) -> + gen_server:call(?MODULE, {delete_chain, Name}). + +lookup_chain(Name) -> + gen_server:call(?MODULE, {lookup_chain, Name}). + +list_chains() -> + Chains = ets:tab2list(?CHAINS_TAB), + {ok, [serialize_chain(Chain) || Chain <- Chains]}. + +create_authenticator(ChainName, Config) -> + gen_server:call(?MODULE, {create_authenticator, ChainName, Config}). + +delete_authenticator(ChainName, AuthenticatorID) -> + gen_server:call(?MODULE, {delete_authenticator, ChainName, AuthenticatorID}). + +update_authenticator(ChainName, AuthenticatorID, Config) -> + gen_server:call(?MODULE, {update_authenticator, ChainName, AuthenticatorID, Config}). + +lookup_authenticator(ChainName, AuthenticatorID) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [#chain{authenticators = Authenticators}] -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + Authenticator -> + {ok, serialize_authenticator(Authenticator)} + end + end. + +list_authenticators(ChainName) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [#chain{authenticators = Authenticators}] -> + {ok, serialize_authenticators(Authenticators)} + end. + +move_authenticator(ChainName, AuthenticatorID, Position) -> + gen_server:call(?MODULE, {move_authenticator, ChainName, AuthenticatorID, Position}). + +import_users(ChainName, AuthenticatorID, Filename) -> + gen_server:call(?MODULE, {import_users, ChainName, AuthenticatorID, Filename}). + +add_user(ChainName, AuthenticatorID, UserInfo) -> + gen_server:call(?MODULE, {add_user, ChainName, AuthenticatorID, UserInfo}). + +delete_user(ChainName, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {delete_user, ChainName, AuthenticatorID, UserID}). + +update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) -> + gen_server:call(?MODULE, {update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}). + +lookup_user(ChainName, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {lookup_user, ChainName, AuthenticatorID, UserID}). + +%% TODO: Support pagination +list_users(ChainName, AuthenticatorID) -> + gen_server:call(?MODULE, {list_users, ChainName, AuthenticatorID}). + +generate_id(#{mechanism := Mechanism0, backend := Backend0}) -> + Mechanism = atom_to_binary(Mechanism0), + Backend = atom_to_binary(Backend0), + <>; +generate_id(#{mechanism := Mechanism}) -> + atom_to_binary(Mechanism); +generate_id(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}) -> + <>; +generate_id(#{<<"mechanism">> := Mechanism}) -> + Mechanism. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init(_Opts) -> + _ = ets:new(?CHAINS_TAB, [ named_table, set, public + , {keypos, #chain.name} + , {read_concurrency, true}]), + ok = emqx_config_handler:add_handler([authentication], ?MODULE), + ok = emqx_config_handler:add_handler([listeners, '?', '?', authentication], ?MODULE), + {ok, #{hooked => false, providers => #{}}}. + +handle_call({add_provider, AuthNType, Provider}, _From, #{providers := Providers} = State) -> + reply(ok, State#{providers := Providers#{AuthNType => Provider}}); + +handle_call({remove_provider, AuthNType}, _From, #{providers := Providers} = State) -> + reply(ok, State#{providers := maps:remove(AuthNType, Providers)}); + +handle_call(get_refs, _From, #{providers := Providers} = State) -> + Refs = lists:foldl(fun({_, Provider}, Acc) -> + Acc ++ Provider:refs() + end, [], maps:to_list(Providers)), + reply({ok, Refs}, State); + +handle_call({create_chain, Name}, _From, State) -> + case ets:member(?CHAINS_TAB, Name) of + true -> + reply({error, {already_exists, {chain, Name}}}, State); + false -> + Chain = #chain{name = Name, + authenticators = []}, + true = ets:insert(?CHAINS_TAB, Chain), + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({delete_chain, Name}, _From, State) -> + case ets:lookup(?CHAINS_TAB, Name) of + [] -> + reply({error, {not_found, {chain, Name}}}, State); + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || Authenticator <- Authenticators], + true = ets:delete(?CHAINS_TAB, Name), + reply(ok, may_unhook(State)) + end; + +handle_call({lookup_chain, Name}, _From, State) -> + case ets:lookup(?CHAINS_TAB, Name) of + [] -> + reply({error, {not_found, {chain, Name}}}, State); + [Chain] -> + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Providers} = State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + AuthenticatorID = generate_id(Config), + case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of + true -> + {error, {already_exists, {authenticator, AuthenticatorID}}}; + false -> + case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [Authenticator], + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, may_hook(State)); + +handle_call({delete_authenticator, ChainName, AuthenticatorID}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keytake(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, Authenticator, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + ok + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, may_unhook(State)); + +handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + #authenticator{provider = Provider, + state = #{version := Version} = ST} = Authenticator -> + case AuthenticatorID =:= generate_id(Config) of + true -> + Unique = <>, + case Provider:update(Config#{'_unique' => Unique}, ST) of + {ok, NewST} -> + NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end; + false -> + {error, mechanism_or_backend_change_is_not_alloed} + end + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, State); + +handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case do_move_authenticator(AuthenticatorID, Authenticators, Position) of + {ok, NAuthenticators} -> + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + ok; + {error, Reason} -> + {error, Reason} + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, State); + +handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]), + reply(Reply, State); + +handle_call({add_user, ChainName, AuthenticatorID, UserInfo}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, add_user, [UserInfo]), + reply(Reply, State); + +handle_call({delete_user, ChainName, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, delete_user, [UserID]), + reply(Reply, State); + +handle_call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, update_user, [UserID, NewUserInfo]), + reply(Reply, State); + +handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]), + reply(Reply, State); + +handle_call({list_users, ChainName, AuthenticatorID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, list_users, []), + reply(Reply, State); + +handle_call(Req, _From, State) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Req, State) -> + ?LOG(error, "Unexpected case: ~p", [Req]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + emqx_config_handler:remove_handler([authentication]), + emqx_config_handler:remove_handler([listeners, '?', '?', authentication]), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +reply(Reply, State) -> + {reply, Reply, State}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +split_by_id(ID, AuthenticatorsConfig) -> + case lists:foldl( + fun(C, {P1, P2, F0}) -> + F = case ID =:= generate_id(C) of + true -> true; + false -> F0 + end, + case F of + false -> {[C | P1], P2, F}; + true -> {P1, [C | P2], F} + end + end, {[], [], false}, AuthenticatorsConfig) of + {_, _, false} -> + {error, {not_found, {authenticator, ID}}}; + {Part1, Part2, true} -> + {ok, lists:reverse(Part1), lists:reverse(Part2)} + end. + +global_chain(mqtt) -> + <<"mqtt:global">>; +global_chain('mqtt-sn') -> + <<"mqtt-sn:global">>; +global_chain(coap) -> + <<"coap:global">>; +global_chain(lwm2m) -> + <<"lwm2m:global">>; +global_chain(stomp) -> + <<"stomp:global">>; +global_chain(_) -> + <<"unknown:global">>. + +may_hook(#{hooked := false} = State) -> + case lists:any(fun(#chain{authenticators = []}) -> false; + (_) -> true + end, ets:tab2list(?CHAINS_TAB)) of + true -> + _ = emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}), + State#{hooked => true}; + false -> + State + end; +may_hook(State) -> + State. + +may_unhook(#{hooked := true} = State) -> + case lists:all(fun(#chain{authenticators = []}) -> true; + (_) -> false + end, ets:tab2list(?CHAINS_TAB)) of + true -> + _ = emqx:unhook('client.authenticate', {emqx_authentication, authenticate, []}), + State#{hooked => false}; + false -> + State + end; +may_unhook(State) -> + State. + +do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config, Providers) -> + case maps:get(authn_type(Config), Providers, undefined) of + undefined -> + {error, no_available_provider}; + Provider -> + Unique = <>, + case Provider:create(Config#{'_unique' => Unique}) of + {ok, State} -> + Authenticator = #authenticator{id = AuthenticatorID, + provider = Provider, + enable = Enable, + state = switch_version(State)}, + {ok, Authenticator}; + {error, Reason} -> + {error, Reason} + end + end. + +do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> + _ = Provider:destroy(State), + ok. + +replace_authenticator(ID, Authenticator, Authenticators) -> + lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator). + +do_move_authenticator(ID, Authenticators, Position) -> + case lists:keytake(ID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, ID}}}; + {value, Authenticator, NAuthenticators} -> + case Position of + top -> + {ok, [Authenticator | NAuthenticators]}; + bottom -> + {ok, NAuthenticators ++ [Authenticator]}; + {before, ID0} -> + insert(Authenticator, NAuthenticators, ID0, []) + end + end. + +insert(_, [], ID, _) -> + {error, {not_found, {authenticator, ID}}}; +insert(Authenticator, [#authenticator{id = ID} | _] = Authenticators, ID, Acc) -> + {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]}; +insert(Authenticator, [Authenticator0 | More], ID, Acc) -> + insert(Authenticator, More, ID, [Authenticator0 | Acc]). + +update_chain(ChainName, UpdateFun) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [Chain] -> + UpdateFun(Chain) + end. + +call_authenticator(ChainName, AuthenticatorID, Func, Args) -> + UpdateFun = + fun(#chain{authenticators = Authenticators}) -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + #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, + update_chain(ChainName, UpdateFun). + +serialize_chain(#chain{name = Name, + authenticators = Authenticators}) -> + #{ name => Name + , authenticators => serialize_authenticators(Authenticators) + }. + +serialize_authenticators(Authenticators) -> + [serialize_authenticator(Authenticator) || Authenticator <- Authenticators]. + +serialize_authenticator(#authenticator{id = ID, + provider = Provider, + enable = Enable, + state = State}) -> + #{ id => ID + , provider => Provider + , enable => Enable + , state => State + }. + +switch_version(State = #{version := ?VER_1}) -> + State#{version := ?VER_2}; +switch_version(State = #{version := ?VER_2}) -> + State#{version := ?VER_1}; +switch_version(State) -> + State#{version => ?VER_1}. + +authn_type(#{mechanism := Mechanism, backend := Backend}) -> + {Mechanism, Backend}; +authn_type(#{mechanism := Mechanism}) -> + Mechanism. + +may_to_map([L]) -> + L; +may_to_map(L) -> + L. + +to_list(undefined) -> + []; +to_list(M) when M =:= #{} -> + []; +to_list(M) when is_map(M) -> + [M]; +to_list(L) when is_list(L) -> + L. diff --git a/apps/emqx/src/emqx_broker_sup.erl b/apps/emqx/src/emqx_broker_sup.erl index 69df72408..a479e9ff1 100644 --- a/apps/emqx/src/emqx_broker_sup.erl +++ b/apps/emqx/src/emqx_broker_sup.erl @@ -43,6 +43,14 @@ init([]) -> type => worker, modules => [emqx_shared_sub]}, + %% Authentication + AuthN = #{id => authn, + start => {emqx_authentication, start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => [emqx_authentication]}, + %% Broker helper Helper = #{id => helper, start => {emqx_broker_helper, start_link, []}, @@ -51,5 +59,5 @@ init([]) -> type => worker, modules => [emqx_broker_helper]}, - {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}. + {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, AuthN, Helper]}}. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index f64ffabcb..d92f1d35a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -138,6 +138,8 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +deep_put_handler([], Handlers, Mod) when is_map(Handlers) -> + {ok, Handlers#{?MOD => Mod}}; deep_put_handler([], _Handlers, Mod) -> {ok, #{?MOD => Mod}}; deep_put_handler([?WKEY | KeyPath], Handlers, Mod) -> diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index a91651c6c..7b1d6b0dd 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -252,11 +252,15 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> {ok, {skipped, quic_app_missing}} end. +delete_authentication(Type, ListenerName, _Conf) -> + emqx_authentication:delete_chain(atom_to_binary(listener_id(Type, ListenerName))). + %% Update the listeners at runtime post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) -> #{added := Added, removed := Removed, changed := Updated} = diff_listeners(NewListeners, OldListeners), perform_listener_changes(fun stop_listener/3, Removed), + perform_listener_changes(fun delete_authentication/3, Removed), perform_listener_changes(fun start_listener/3, Added), perform_listener_changes(fun restart_listener/3, Updated). diff --git a/apps/emqx/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl index 736bb05b0..282b8b5f3 100644 --- a/apps/emqx/src/emqx_metrics.erl +++ b/apps/emqx/src/emqx_metrics.erl @@ -22,8 +22,6 @@ -include("logger.hrl"). -include("types.hrl"). -include("emqx_mqtt.hrl"). --include("emqx.hrl"). - -export([ start_link/0 , stop/0 diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 0189a468b..01989c5a1 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -94,7 +94,8 @@ roots() -> "stats", "sysmon", "alarm", - "authorization" + "authorization", + {"authentication", sc(hoconsc:lazy(hoconsc:array(map())), #{})} ]. fields("stats") -> @@ -819,6 +820,10 @@ mqtt_listener() -> sc(duration(), #{}) } + , {"authentication", + sc(hoconsc:lazy(hoconsc:array(map())), + #{}) + } ]. base_listener() -> diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 59f4aa9ee..d1d3d16f8 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,37 +1,6 @@ -authentication { - enable = false - authenticators = [ - # { - # name: "authenticator1" - # mechanism: password-based - # server_type: built-in-database - # user_id_type: clientid - # }, - # { - # name: "authenticator2" - # mechanism: password-based - # server_type: mongodb - # server: "127.0.0.1:27017" - # database: mqtt - # collection: users - # selector: { - # username: "${mqtt-username}" - # } - # password_hash_field: password_hash - # salt_field: salt - # password_hash_algorithm: sha256 - # salt_position: prefix - # }, - # { - # name: "authenticator 3" - # mechanism: password-based - # server_type: redis - # server: "127.0.0.1:6379" - # password: "public" - # database: 0 - # query: "HMGET ${mqtt-username} password_hash salt" - # password_hash_algorithm: sha256 - # salt_position: prefix - # } - ] -} +# authentication: { +# mechanism: password-based +# backend: 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 c5a392fd0..bdf93204a 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -15,24 +15,11 @@ %%-------------------------------------------------------------------- -define(APP, emqx_authn). --define(CHAIN, <<"mqtt">>). --define(VER_1, <<"1">>). --define(VER_2, <<"2">>). +-define(AUTHN, emqx_authentication). + +-define(GLOBAL, <<"mqtt:global">>). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}"). --record(authenticator, - { id :: binary() - , name :: binary() - , provider :: module() - , state :: map() - }). - --record(chain, - { id :: binary() - , authenticators :: [{binary(), binary(), #authenticator{}}] - , created_at :: integer() - }). - -define(AUTH_SHARD, emqx_authn_shard). diff --git a/apps/emqx_authn/rebar.config b/apps/emqx_authn/rebar.config index 32b5a43e0..73696b033 100644 --- a/apps/emqx_authn/rebar.config +++ b/apps/emqx_authn/rebar.config @@ -1,6 +1,4 @@ -{deps, [ - {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} -]}. +{deps, []}. {edoc_opts, [{preprocess, true}]}. {erl_opts, [warn_unused_vars, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 1034682e5..3ab05e6b0 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -15,640 +15,3 @@ %%-------------------------------------------------------------------- -module(emqx_authn). - --behaviour(gen_server). - --behaviour(emqx_config_handler). - --include("emqx_authn.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ pre_config_update/2 - , post_config_update/4 - , update_config/2 - ]). - --export([ enable/0 - , disable/0 - , is_enabled/0 - ]). - --export([authenticate/2]). - --export([ start_link/0 - , stop/0 - ]). - --export([ create_chain/1 - , delete_chain/1 - , lookup_chain/1 - , list_chains/0 - , create_authenticator/2 - , delete_authenticator/2 - , update_authenticator/3 - , update_or_create_authenticator/3 - , lookup_authenticator/2 - , list_authenticators/1 - , move_authenticator/3 - ]). - --export([ import_users/3 - , add_user/3 - , delete_user/3 - , update_user/4 - , lookup_user/3 - , list_users/2 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(CHAIN_TAB, emqx_authn_chain). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -pre_config_update({enable, Enable}, _OldConfig) -> - {ok, Enable}; -pre_config_update({create_authenticator, Config}, OldConfig) -> - {ok, OldConfig ++ [Config]}; -pre_config_update({delete_authenticator, ID}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name}} -> - NewConfig = lists:filter(fun(#{<<"name">> := N}) -> - N =/= Name - end, OldConfig), - {ok, NewConfig} - end; -pre_config_update({update_authenticator, ID, Config}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name}} -> - NewConfig = lists:map(fun(#{<<"name">> := N} = C) -> - case N =:= Name of - true -> Config; - false -> C - end - end, OldConfig), - {ok, NewConfig} - end; -pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, _Reason} -> OldConfig ++ [Config]; - {ok, #{name := Name}} -> - NewConfig = lists:map(fun(#{<<"name">> := N} = C) -> - case N =:= Name of - true -> Config; - false -> C - end - end, OldConfig), - {ok, NewConfig} - end; -pre_config_update({move_authenticator, ID, Position}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name}} -> - {ok, Found, Part1, Part2} = split_by_name(Name, OldConfig), - case Position of - <<"top">> -> - {ok, [Found | Part1] ++ Part2}; - <<"bottom">> -> - {ok, Part1 ++ Part2 ++ [Found]}; - Before -> - case binary:split(Before, <<":">>, [global]) of - [<<"before">>, ID0] -> - case lookup_authenticator(?CHAIN, ID0) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name1}} -> - {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2), - {ok, NPart1 ++ [Found, NFound | NPart2]} - end; - _ -> - {error, {invalid_parameter, position}} - end - end - end. - -post_config_update({enable, true}, _NewConfig, _OldConfig, _AppEnvs) -> - emqx_authn:enable(); -post_config_update({enable, false}, _NewConfig, _OldConfig, _AppEnvs) -> - emqx_authn:disable(); -post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> - case lists:filter( - fun(#{name := N}) -> - N =:= Name - end, NewConfig) of - [Config] -> - create_authenticator(?CHAIN, Config); - [_Config | _] -> - {error, name_has_be_used} - end; -post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig, _AppEnvs) -> - case delete_authenticator(?CHAIN, ID) of - ok -> ok; - {error, Reason} -> throw(Reason) - end; -post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> - case lists:filter( - fun(#{name := N}) -> - N =:= Name - end, NewConfig) of - [Config] -> - update_authenticator(?CHAIN, ID, Config); - [_Config | _] -> - {error, name_has_be_used} - end; -post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> - case lists:filter( - fun(#{name := N}) -> - N =:= Name - end, NewConfig) of - [Config] -> - update_or_create_authenticator(?CHAIN, ID, Config); - [_Config | _] -> - {error, name_has_be_used} - end; -post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig, _AppEnvs) -> - NPosition = case Position of - <<"top">> -> top; - <<"bottom">> -> bottom; - Before -> - case binary:split(Before, <<":">>, [global]) of - [<<"before">>, ID0] -> - {before, ID0}; - _ -> - {error, {invalid_parameter, position}} - end - end, - move_authenticator(?CHAIN, ID, NPosition). - -update_config(Path, ConfigRequest) -> - emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). - -enable() -> - case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of - ok -> ok; - {error, already_exists} -> ok - end. - -disable() -> - emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), - ok. - -is_enabled() -> - Callbacks = emqx_hooks:lookup('client.authenticate'), - lists:any(fun({callback, {?MODULE, authenticate, []}, _, _}) -> - true; - (_) -> - false - end, Callbacks). - -authenticate(Credential, _AuthResult) -> - case ets:lookup(?CHAIN_TAB, ?CHAIN) of - [#chain{authenticators = Authenticators}] -> - do_authenticate(Authenticators, Credential); - [] -> - {stop, {error, not_authorized}} - end. - -do_authenticate([], _) -> - {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, Extra} - %% {ok, Extra, AuthData} - %% {ok, MetaData} - %% {continue, AuthCache} - %% {continue, AuthData, AuthCache} - %% {error, Reason} - {stop, Result} - end. - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -stop() -> - gen_server:stop(?MODULE). - -create_chain(#{id := ID}) -> - gen_server:call(?MODULE, {create_chain, ID}). - -delete_chain(ID) -> - gen_server:call(?MODULE, {delete_chain, ID}). - -lookup_chain(ID) -> - gen_server:call(?MODULE, {lookup_chain, ID}). - -list_chains() -> - Chains = ets:tab2list(?CHAIN_TAB), - {ok, [serialize_chain(Chain) || Chain <- Chains]}. - -create_authenticator(ChainID, Config) -> - gen_server:call(?MODULE, {create_authenticator, ChainID, Config}). - -delete_authenticator(ChainID, AuthenticatorID) -> - gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}). - -update_authenticator(ChainID, AuthenticatorID, Config) -> - gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}). - -update_or_create_authenticator(ChainID, AuthenticatorID, Config) -> - gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}). - -lookup_authenticator(ChainID, AuthenticatorID) -> - case ets:lookup(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - case lists:keyfind(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {_, _, Authenticator} -> - {ok, serialize_authenticator(Authenticator)} - end - end. - -list_authenticators(ChainID) -> - case ets:lookup(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - {ok, serialize_authenticators(Authenticators)} - end. - -move_authenticator(ChainID, AuthenticatorID, Position) -> - gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}). - -import_users(ChainID, AuthenticatorID, Filename) -> - gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}). - -add_user(ChainID, AuthenticatorID, UserInfo) -> - gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}). - -delete_user(ChainID, AuthenticatorID, UserID) -> - gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}). - -update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> - gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}). - -lookup_user(ChainID, AuthenticatorID, UserID) -> - gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}). - -%% TODO: Support pagination -list_users(ChainID, AuthenticatorID) -> - gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init(_Opts) -> - _ = ets:new(?CHAIN_TAB, [ named_table, set, public - , {keypos, #chain.id} - , {read_concurrency, true}]), - {ok, #{}}. - -handle_call({create_chain, ID}, _From, State) -> - case ets:member(?CHAIN_TAB, ID) of - true -> - reply({error, {already_exists, {chain, ID}}}, State); - false -> - Chain = #chain{id = ID, - authenticators = [], - created_at = erlang:system_time(millisecond)}, - true = ets:insert(?CHAIN_TAB, Chain), - reply({ok, serialize_chain(Chain)}, State) - end; - -handle_call({delete_chain, ID}, _From, State) -> - case ets:lookup(?CHAIN_TAB, ID) of - [] -> - reply({error, {not_found, {chain, ID}}}, State); - [#chain{authenticators = Authenticators}] -> - _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators], - true = ets:delete(?CHAIN_TAB, ID), - reply(ok, State) - end; - -handle_call({lookup_chain, ID}, _From, State) -> - case ets:lookup(?CHAIN_TAB, ID) of - [] -> - reply({error, {not_found, {chain, ID}}}, State); - [Chain] -> - reply({ok, serialize_chain(Chain)}, State) - end; - -handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case lists:keymember(Name, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - AlreadyExist = fun(ID) -> - lists:keymember(ID, 1, Authenticators) - end, - AuthenticatorID = gen_id(AlreadyExist), - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}], - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end, - Reply = update_chain(ChainID, UpdateFun), - reply(Reply, State); - -handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {value, {_, _, Authenticator}, NAuthenticators} -> - _ = do_delete_authenticator(Authenticator), - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - ok - end - end, - Reply = update_chain(ChainID, UpdateFun), - reply(Reply, State); - -handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> - Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false), - reply(Reply, State); - -handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> - Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true), - reply(Reply, State); - -handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case do_move_authenticator(AuthenticatorID, Authenticators, Position) of - {ok, NAuthenticators} -> - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - ok; - {error, Reason} -> - {error, Reason} - end - end, - Reply = update_chain(ChainID, UpdateFun), - reply(Reply, State); - -handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]), - reply(Reply, State); - -handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]), - reply(Reply, State); - -handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]), - reply(Reply, State); - -handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]), - reply(Reply, State); - -handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]), - reply(Reply, State); - -handle_call({list_users, ChainID, AuthenticatorID}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []), - reply(Reply, State); - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Req, State) -> - ?LOG(error, "Unexpected case: ~p", [Req]), - {noreply, State}. - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -reply(Reply, State) -> - {reply, Reply, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) -> - emqx_authn_mnesia; -authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) -> - emqx_authn_mysql; -authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) -> - emqx_authn_pgsql; -authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) -> - emqx_authn_mongodb; -authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) -> - emqx_authn_redis; -authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) -> - emqx_authn_http; -authenticator_provider(#{mechanism := jwt}) -> - emqx_authn_jwt; -authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) -> - emqx_enhanced_authn_scram_mnesia. - -gen_id(AlreadyExist) -> - ID = list_to_binary(emqx_rule_id:gen()), - case AlreadyExist(ID) of - true -> gen_id(AlreadyExist); - false -> ID - end. - -switch_version(State = #{version := ?VER_1}) -> - State#{version := ?VER_2}; -switch_version(State = #{version := ?VER_2}) -> - State#{version := ?VER_1}; -switch_version(State) -> - State#{version => ?VER_1}. - -split_by_name(Name, Config) -> - {Part1, Part2, true} = lists:foldl( - fun(#{<<"name">> := N} = C, {P1, P2, F0}) -> - F = case N =:= Name of - true -> true; - false -> F0 - end, - case F of - false -> {[C | P1], P2, F}; - true -> {P1, [C | P2], F} - end - end, {[], [], false}, Config), - [Found | NPart2] = lists:reverse(Part2), - {ok, Found, lists:reverse(Part1), NPart2}. - -do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> - Provider = authenticator_provider(Config), - Unique = <>, - case Provider:create(Config#{'_unique' => Unique}) of - {ok, State} -> - Authenticator = #authenticator{id = AuthenticatorID, - name = Name, - provider = Provider, - state = switch_version(State)}, - {ok, Authenticator}; - {error, Reason} -> - {error, Reason} - end. - -do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> - _ = Provider:destroy(State), - ok. - -update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - case CreateWhenNotFound of - true -> - case lists:keymember(NewName, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end; - false -> - {error, {not_found, {authenticator, AuthenticatorID}}} - end; - {value, - {_, _, #authenticator{provider = Provider, - state = #{version := Version} = State} = Authenticator}, - Others} -> - case lists:keymember(NewName, 2, Others) of - true -> - {error, name_has_be_used}; - false -> - case (NewProvider = authenticator_provider(Config)) =:= Provider of - true -> - Unique = <>, - case Provider:update(Config#{'_unique' => Unique}, State) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end; - false -> - Unique = <>, - case NewProvider:create(Config#{'_unique' => Unique}) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - provider = NewProvider, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), - _ = Provider:destroy(State), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end - end - end, - update_chain(ChainID, UpdateFun). - -replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> - lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). - -do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {value, Authenticator, NAuthenticators} -> - do_move_authenticator(Authenticator, NAuthenticators, Position) - end; - -do_move_authenticator(Authenticator, Authenticators, top) -> - {ok, [Authenticator | Authenticators]}; -do_move_authenticator(Authenticator, Authenticators, bottom) -> - {ok, Authenticators ++ [Authenticator]}; -do_move_authenticator(Authenticator, Authenticators, {before, ID}) -> - insert(Authenticator, Authenticators, ID, []). - -insert(_, [], ID, _) -> - {error, {not_found, {authenticator, ID}}}; -insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) -> - {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]}; -insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) -> - insert(Authenticator, More, ID, [Authenticator0 | Acc]). - -update_chain(ChainID, UpdateFun) -> - case ets:lookup(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end. - -call_authenticator(ChainID, AuthenticatorID, Func, Args) -> - UpdateFun = - fun(#chain{authenticators = Authenticators}) -> - case lists:keyfind(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {_, _, #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, - update_chain(ChainID, UpdateFun). - -serialize_chain(#chain{id = ID, - authenticators = Authenticators, - created_at = CreatedAt}) -> - #{id => ID, - authenticators => serialize_authenticators(Authenticators), - created_at => CreatedAt}. - -serialize_authenticators(Authenticators) -> - [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators]. - -serialize_authenticator(#authenticator{id = ID, - name = Name, - provider = Provider, - state = State}) -> - #{id => ID, name => Name, provider => Provider, state => State}. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 5f2b96b57..3303f88ef 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -22,37 +22,36 @@ -export([ api_spec/0 , authentication/2 - , authenticators/2 - , authenticators2/2 + , authentication2/2 + , authentication3/2 + , authentication4/2 , move/2 + , move2/2 , import_users/2 , users/2 , users2/2 ]). --define(EXAMPLE_1, #{name => <<"example 1">>, - mechanism => <<"password-based">>, - server_type => <<"built-in-database">>, - user_id_type => <<"username">>, +-define(EXAMPLE_1, #{mechanism => <<"password-based">>, + backend => <<"built-in-database">>, + query => <<"SELECT password_hash from built-in-database WHERE username = ${username}">>, password_hash_algorithm => #{ name => <<"sha256">> }}). --define(EXAMPLE_2, #{name => <<"example 2">>, - mechanism => <<"password-based">>, - server_type => <<"http-server">>, +-define(EXAMPLE_2, #{mechanism => <<"password-based">>, + backend => <<"http-server">>, method => <<"post">>, url => <<"http://localhost:80/login">>, headers => #{ <<"content-type">> => <<"application/json">> }, - form_data => #{ + body => #{ <<"username">> => <<"${mqtt-username}">>, <<"password">> => <<"${mqtt-password}">> }}). --define(EXAMPLE_3, #{name => <<"example 3">>, - mechanism => <<"jwt">>, +-define(EXAMPLE_3, #{mechanism => <<"jwt">>, use_jwks => false, algorithm => <<"hmac-based">>, secret => <<"mysecret">>, @@ -61,9 +60,8 @@ <<"username">> => <<"${mqtt-username}">> }}). --define(EXAMPLE_4, #{name => <<"example 4">>, - mechanism => <<"password-based">>, - server_type => <<"mongodb">>, +-define(EXAMPLE_4, #{mechanism => <<"password-based">>, + backend => <<"mongodb">>, server => <<"127.0.0.1:27017">>, database => example, collection => users, @@ -76,9 +74,8 @@ salt_position => <<"prefix">> }). --define(EXAMPLE_5, #{name => <<"example 5">>, - mechanism => <<"password-based">>, - server_type => <<"redis">>, +-define(EXAMPLE_5, #{mechanism => <<"password-based">>, + backend => <<"redis">>, server => <<"127.0.0.1:6379">>, database => 0, query => <<"HMGET ${mqtt-username} password_hash salt">>, @@ -86,10 +83,53 @@ salt_position => <<"prefix">> }). +-define(INSTANCE_EXAMPLE_1, maps:merge(?EXAMPLE_1, #{id => <<"password-based:built-in-database">>, + enable => true})). + +-define(INSTANCE_EXAMPLE_2, maps:merge(?EXAMPLE_2, #{id => <<"password-based:http-server">>, + connect_timeout => 5000, + enable_pipelining => true, + headers => #{ + <<"accept">> => <<"application/json">>, + <<"cache-control">> => <<"no-cache">>, + <<"connection">> => <<"keepalive">>, + <<"content-type">> => <<"application/json">>, + <<"keep-alive">> => <<"timeout=5">> + }, + max_retries => 5, + pool_size => 8, + request_timeout => 5000, + retry_interval => 1000, + enable => true})). + +-define(INSTANCE_EXAMPLE_3, maps:merge(?EXAMPLE_3, #{id => <<"jwt">>, + enable => true})). + +-define(INSTANCE_EXAMPLE_4, maps:merge(?EXAMPLE_4, #{id => <<"password-based:mongodb">>, + mongo_type => <<"single">>, + pool_size => 8, + ssl => #{ + enable => false + }, + topology => #{ + max_overflow => 8, + pool_size => 8 + }, + enable => true})). + +-define(INSTANCE_EXAMPLE_5, maps:merge(?EXAMPLE_5, #{id => <<"password-based:redis">>, + auto_reconnect => true, + redis_type => single, + pool_size => 8, + ssl => #{ + enable => false + }, + enable => true})). + -define(ERR_RESPONSE(Desc), #{description => Desc, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"error">>), + schema => minirest:ref(<<"Error">>), examples => #{ example1 => #{ summary => <<"Not Found">>, @@ -107,9 +147,11 @@ api_spec() -> {[ authentication_api() - , authenticators_api() - , authenticators_api2() + , authentication_api2() , move_api() + , authentication_api3() + , authentication_api4() + , move_api2() , import_users_api() , users_api() , users2_api() @@ -117,350 +159,473 @@ api_spec() -> authentication_api() -> Metadata = #{ - post => #{ - description => "Enable or disbale authentication", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [enable], - properties => #{ - enable => #{ - type => boolean, - example => true - } - } - } - } - } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>) - } - }, - get => #{ - description => "Get status of authentication", - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - properties => #{ - enabled => #{ - type => boolean, - example => true - } - } - } - } - } - } - } - } + post => create_authenticator_api_spec(), + get => list_authenticators_api_spec() }, {"/authentication", Metadata, authentication}. -authenticators_api() -> +authentication_api2() -> Metadata = #{ - post => #{ - description => "Create authenticator", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"authenticator">>), - examples => #{ - default => #{ - summary => <<"Default">>, - value => emqx_json:encode(?EXAMPLE_1) - }, - http => #{ - summary => <<"Authentication provided by HTTP Server">>, - value => emqx_json:encode(?EXAMPLE_2) - }, - jwt => #{ - summary => <<"JWT Authentication">>, - value => emqx_json:encode(?EXAMPLE_3) - }, - mongodb => #{ - summary => <<"Authentication with MongoDB">>, - value => emqx_json:encode(?EXAMPLE_4) - }, - redis => #{ - summary => <<"Authentication with Redis">>, - value => emqx_json:encode(?EXAMPLE_5) - } - } - } - } - }, - responses => #{ - <<"201">> => #{ - description => <<"Created">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - %% TODO: return full content - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) - } - }, - get => #{ - description => "List authenticators", - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => minirest:ref(<<"returned_authenticator">>) - }, - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode([ maps:put(id, <<"example 1">>, ?EXAMPLE_1) - , maps:put(id, <<"example 2">>, ?EXAMPLE_2) - , maps:put(id, <<"example 3">>, ?EXAMPLE_3) - , maps:put(id, <<"example 4">>, ?EXAMPLE_4) - , maps:put(id, <<"example 5">>, ?EXAMPLE_5) - ]) - } - } - } - } - } - } - } + get => list_authenticator_api_spec(), + put => update_authenticator_api_spec(), + delete => delete_authenticator_api_spec() }, - {"/authentication/authenticators", Metadata, authenticators}. + {"/authentication/:id", Metadata, authentication2}. -authenticators_api2() -> +authentication_api3() -> Metadata = #{ - get => #{ - description => "Get authenicator by id", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 5">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - put => #{ - description => "Update authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] - }, - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(?EXAMPLE_1) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(?EXAMPLE_2) - } - } - } - } - }, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 5">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), - <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) - } - }, - delete => #{ - description => "Delete authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } + post => create_authenticator_api_spec2(), + get => list_authenticators_api_spec2() }, - {"/authentication/authenticators/:id", Metadata, authenticators2}. + {"/listeners/:listener_id/authentication", Metadata, authentication3}. + +authentication_api4() -> + Metadata = #{ + get => list_authenticator_api_spec2(), + put => update_authenticator_api_spec2(), + delete => delete_authenticator_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id", Metadata, authentication4}. move_api() -> Metadata = #{ - post => #{ - description => "Move authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true + post => move_authenticator_api_spec() + }, + {"/authentication/:id/move", Metadata, move}. + +move_api2() -> + Metadata = #{ + post => move_authenticator_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/move", Metadata, move2}. + +create_authenticator_api_spec() -> + #{ + description => "Create a authenticator for global authentication", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorConfig">>), + examples => #{ + default => #{ + summary => <<"Default">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + http => #{ + summary => <<"Authentication provided by HTTP Server">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + jwt => #{ + summary => <<"JWT Authentication">>, + value => emqx_json:encode(?EXAMPLE_3) + }, + mongodb => #{ + summary => <<"Authentication with MongoDB">>, + value => emqx_json:encode(?EXAMPLE_4) + }, + redis => #{ + summary => <<"Authentication with Redis">>, + value => emqx_json:encode(?EXAMPLE_5) + } + } } - ], - requestBody => #{ + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, content => #{ 'application/json' => #{ - schema => #{ - oneOf => [ - #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => string, - enum => [<<"top">>, <<"bottom">>], - example => <<"top">> - } - } - }, - #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => string, - description => <<"before:">>, - example => <<"before:67e4c9d3">> - } - } - } - ] + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } } } } }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }. + +create_authenticator_api_spec2() -> + Spec = create_authenticator_api_spec(), + Spec#{ + description => "Create a authenticator for listener", + parameters => [ + #{ + name => listener_id, + in => path, + schema => #{ + type => string }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + required => true } + ] + }. + +list_authenticators_api_spec() -> + #{ + description => "List authenticators for global authentication", + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => minirest:ref(<<"AuthenticatorInstance">>) + }, + examples => #{ + example => #{ + summary => <<"Example">>, + value => emqx_json:encode([ ?INSTANCE_EXAMPLE_1 + , ?INSTANCE_EXAMPLE_2 + , ?INSTANCE_EXAMPLE_3 + , ?INSTANCE_EXAMPLE_4 + , ?INSTANCE_EXAMPLE_5 + ])}}}}}}}. + +list_authenticators_api_spec2() -> + Spec = list_authenticators_api_spec(), + Spec#{ + description => "List authenticators for listener", + parameters => [ + #{ + name => listener_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ] + }. + +list_authenticator_api_spec() -> + #{ + description => "Get authenticator by id", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/authenticators/:id/move", Metadata, move}. + }. + +list_authenticator_api_spec2() -> + Spec = list_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. + +update_authenticator_api_spec() -> + #{ + description => "Update authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorConfig">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?EXAMPLE_5) + } + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }. + +update_authenticator_api_spec2() -> + Spec = update_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. + +delete_authenticator_api_spec() -> + #{ + description => "Delete authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +delete_authenticator_api_spec2() -> + Spec = delete_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. + +move_authenticator_api_spec() -> + #{ + description => "Move authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + oneOf => [ + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + enum => [<<"top">>, <<"bottom">>], + example => <<"top">> + } + } + }, + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + description => <<"before:">>, + example => <<"before:password-based:mysql">> + } + } + } + ] + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +move_authenticator_api_spec2() -> + Spec = move_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. import_users_api() -> Metadata = #{ @@ -470,6 +635,7 @@ import_users_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -500,7 +666,7 @@ import_users_api() -> } } }, - {"/authentication/authenticators/:id/import-users", Metadata, import_users}. + {"/authentication/:id/import_users", Metadata, import_users}. users_api() -> Metadata = #{ @@ -510,6 +676,7 @@ users_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -567,6 +734,7 @@ users_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -599,7 +767,7 @@ users_api() -> } } }, - {"/authentication/authenticators/:id/users", Metadata, users}. + {"/authentication/:id/users", Metadata, users}. users2_api() -> Metadata = #{ @@ -609,6 +777,7 @@ users2_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -672,6 +841,7 @@ users2_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -717,6 +887,7 @@ users2_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -739,17 +910,36 @@ users2_api() -> } } }, - {"/authentication/authenticators/:id/users/:user_id", Metadata, users2}. + {"/authentication/:id/users/:user_id", Metadata, users2}. definitions() -> - AuthenticatorDef = #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] + AuthenticatorConfigDef = #{ + allOf => [ + #{ + type => object, + properties => #{ + enable => #{ + type => boolean, + default => true, + example => true + } + } + }, + #{ + oneOf => [ minirest:ref(<<"PasswordBasedBuiltInDatabase">>) + , minirest:ref(<<"PasswordBasedMySQL">>) + , minirest:ref(<<"PasswordBasedPostgreSQL">>) + , minirest:ref(<<"PasswordBasedMongoDB">>) + , minirest:ref(<<"PasswordBasedRedis">>) + , minirest:ref(<<"PasswordBasedHTTPServer">>) + , minirest:ref(<<"JWT">>) + , minirest:ref(<<"SCRAMBuiltInDatabase">>) + ] + } + ] }, - ReturnedAuthenticatorDef = #{ + AuthenticatorInstanceDef = #{ allOf => [ #{ type => object, @@ -758,148 +948,49 @@ definitions() -> type => string } } - }, - #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] } - ] - }, - - PasswordBasedDef = #{ - allOf => [ - #{ - type => object, - required => [name, mechanism], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"password-based">>], - example => <<"password-based">> - } - } - }, - #{ - oneOf => [ minirest:ref(<<"password_based_built_in_database">>) - , minirest:ref(<<"password_based_mysql">>) - , minirest:ref(<<"password_based_pgsql">>) - , minirest:ref(<<"password_based_mongodb">>) - , minirest:ref(<<"password_based_redis">>) - , minirest:ref(<<"password_based_http_server">>) - ] - } - ] - }, - - JWTDef = #{ - type => object, - required => [name, mechanism], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"jwt">>], - example => <<"jwt">> - }, - use_jwks => #{ - type => boolean, - default => false, - example => false - }, - algorithm => #{ - type => string, - enum => [<<"hmac-based">>, <<"public-key">>], - default => <<"hmac-based">>, - example => <<"hmac-based">> - }, - secret => #{ - type => string - }, - secret_base64_encoded => #{ - type => boolean, - default => false - }, - certificate => #{ - type => string - }, - verify_claims => #{ - type => object, - additionalProperties => #{ - type => string - } - }, - ssl => minirest:ref(<<"ssl">>) - } - }, - - SCRAMDef = #{ - type => object, - required => [name, mechanism, server_type], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"scram">>], - example => <<"scram">> - }, - server_type => #{ - type => string, - enum => [<<"built-in-database">>], - default => <<"built-in-database">> - }, - algorithm => #{ - type => string, - enum => [<<"sha256">>, <<"sha512">>], - default => <<"sha256">> - }, - iteration_count => #{ - type => integer, - default => 4096 - } - } + ] ++ maps:get(allOf, AuthenticatorConfigDef) }, PasswordBasedBuiltInDatabaseDef = #{ type => object, - required => [server_type], + required => [mechanism, backend], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"built-in-database">>], example => <<"built-in-database">> }, - user_id_type => #{ + query => #{ type => string, - enum => [<<"username">>, <<"clientid">>], - default => <<"username">>, - example => <<"username">> + default => <<"SELECT password_hash from built-in-database WHERE username = ${username}">>, + example => <<"SELECT password_hash from built-in-database WHERE username = ${username}">> }, - password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>) + password_hash_algorithm => minirest:ref(<<"PasswordHashAlgorithm">>) } }, PasswordBasedMySQLDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , database , username , password , query], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"mysql">>], example => <<"mysql">> @@ -925,7 +1016,7 @@ definitions() -> type => boolean, default => true }, - ssl => minirest:ref(<<"ssl">>), + ssl => minirest:ref(<<"SSL">>), password_hash_algorithm => #{ type => string, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], @@ -948,19 +1039,25 @@ definitions() -> } }, - PasswordBasedPgSQLDef = #{ + PasswordBasedPostgreSQLDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , database , username , password , query], properties => #{ - server_type => #{ + mechanism => #{ type => string, - enum => [<<"pgsql">>], - example => <<"pgsql">> + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ + type => string, + enum => [<<"postgresql">>], + example => <<"postgresql">> }, server => #{ type => string, @@ -1002,7 +1099,8 @@ definitions() -> PasswordBasedMongoDBDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , servers , replica_set_name @@ -1014,10 +1112,15 @@ definitions() -> , password_hash_field ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"mongodb">>], - example => [<<"mongodb">>] + example => <<"mongodb">> }, server => #{ description => <<"Mutually exclusive with the 'servers' field, only valid in standalone mode">>, @@ -1087,7 +1190,8 @@ definitions() -> PasswordBasedRedisDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , servers , password @@ -1095,10 +1199,15 @@ definitions() -> , query ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"redis">>], - example => [<<"redis">>] + example => <<"redis">> }, server => #{ description => <<"Mutually exclusive with the 'servers' field, only valid in standalone mode">>, @@ -1153,12 +1262,18 @@ definitions() -> PasswordBasedHTTPServerDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , url - , form_data + , body ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"http-server">>], example => <<"http-server">> @@ -1178,8 +1293,8 @@ definitions() -> type => string } }, - form_data => #{ - type => string + body => #{ + type => object }, connect_timeout => #{ type => integer, @@ -1208,6 +1323,72 @@ definitions() -> } }, + JWTDef = #{ + type => object, + required => [mechanism], + properties => #{ + mechanism => #{ + type => string, + enum => [<<"jwt">>], + example => <<"jwt">> + }, + use_jwks => #{ + type => boolean, + default => false, + example => false + }, + algorithm => #{ + type => string, + enum => [<<"hmac-based">>, <<"public-key">>], + default => <<"hmac-based">>, + example => <<"hmac-based">> + }, + secret => #{ + type => string + }, + secret_base64_encoded => #{ + type => boolean, + default => false + }, + certificate => #{ + type => string + }, + verify_claims => #{ + type => object, + additionalProperties => #{ + type => string + } + }, + ssl => minirest:ref(<<"SSL">>) + } + }, + + SCRAMBuiltInDatabaseDef = #{ + type => object, + required => [mechanism, backend], + properties => #{ + mechanism => #{ + type => string, + enum => [<<"scram">>], + example => <<"scram">> + }, + backend => #{ + type => string, + enum => [<<"built-in-database">>], + example => <<"built-in-database">> + }, + algorithm => #{ + type => string, + enum => [<<"sha256">>, <<"sha512">>], + default => <<"sha256">> + }, + iteration_count => #{ + type => integer, + default => 4096 + } + } + }, + PasswordHashAlgorithmDef = #{ type => object, required => [name], @@ -1273,93 +1454,92 @@ definitions() -> } }, - [ #{<<"authenticator">> => AuthenticatorDef} - , #{<<"returned_authenticator">> => ReturnedAuthenticatorDef} - , #{<<"password_based">> => PasswordBasedDef} - , #{<<"jwt">> => JWTDef} - , #{<<"scram">> => SCRAMDef} - , #{<<"password_based_built_in_database">> => PasswordBasedBuiltInDatabaseDef} - , #{<<"password_based_mysql">> => PasswordBasedMySQLDef} - , #{<<"password_based_pgsql">> => PasswordBasedPgSQLDef} - , #{<<"password_based_mongodb">> => PasswordBasedMongoDBDef} - , #{<<"password_based_redis">> => PasswordBasedRedisDef} - , #{<<"password_based_http_server">> => PasswordBasedHTTPServerDef} - , #{<<"password_hash_algorithm">> => PasswordHashAlgorithmDef} - , #{<<"ssl">> => SSLDef} - , #{<<"error">> => ErrorDef} + [ #{<<"AuthenticatorConfig">> => AuthenticatorConfigDef} + , #{<<"AuthenticatorInstance">> => AuthenticatorInstanceDef} + , #{<<"PasswordBasedBuiltInDatabase">> => PasswordBasedBuiltInDatabaseDef} + , #{<<"PasswordBasedMySQL">> => PasswordBasedMySQLDef} + , #{<<"PasswordBasedPostgreSQL">> => PasswordBasedPostgreSQLDef} + , #{<<"PasswordBasedMongoDB">> => PasswordBasedMongoDBDef} + , #{<<"PasswordBasedRedis">> => PasswordBasedRedisDef} + , #{<<"PasswordBasedHTTPServer">> => PasswordBasedHTTPServerDef} + , #{<<"JWT">> => JWTDef} + , #{<<"SCRAMBuiltInDatabase">> => SCRAMBuiltInDatabaseDef} + , #{<<"PasswordHashAlgorithm">> => PasswordHashAlgorithmDef} + , #{<<"SSL">> => SSLDef} + , #{<<"Error">> => ErrorDef} ]. authentication(post, #{body := Config}) -> - case Config of - #{<<"enable">> := Enable} -> - {ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}), - {204}; - _ -> - serialize_error({missing_parameter, enable}) - end; + create_authenticator([authentication], ?GLOBAL, Config); + authentication(get, _Params) -> - Enabled = emqx_authn:is_enabled(), - {200, #{enabled => Enabled}}. + list_authenticators([authentication]). -authenticators(post, #{body := Config}) -> - case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of - {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, - raw_config := RawConfig}} -> - [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], - {200, RawConfig1#{id => ID}}; - {error, {_, _, Reason}} -> - serialize_error(Reason) - end; -authenticators(get, _Params) -> - RawConfig = get_raw_config([authentication, authenticators]), - {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), - NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) -> - Config#{id => ID} - end, RawConfig, Authenticators), - {200, NAuthenticators}. +authentication2(get, #{bindings := #{id := AuthenticatorID}}) -> + list_authenticator([authentication], AuthenticatorID); -authenticators2(get, #{bindings := #{id := AuthenticatorID}}) -> - case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of - {ok, #{id := ID, name := Name}} -> - RawConfig = get_raw_config([authentication, authenticators]), - [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], - {200, RawConfig1#{id => ID}}; +authentication2(put, #{bindings := #{id := AuthenticatorID}, body := Config}) -> + update_authenticator([authentication], ?GLOBAL, AuthenticatorID, Config); + +authentication2(delete, #{bindings := #{id := AuthenticatorID}}) -> + delete_authenticator([authentication], ?GLOBAL, AuthenticatorID). + +authentication3(post, #{bindings := #{listener_id := ListenerID}, body := Config}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + create_authenticator([listeners, Type, Name, authentication], ListenerID, Config); {error, Reason} -> serialize_error(Reason) end; -authenticators2(put, #{bindings := #{id := AuthenticatorID}, body := Config}) -> - case emqx_authn:update_config([authentication, authenticators], - {update_or_create_authenticator, AuthenticatorID, Config}) of - {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, - raw_config := RawConfig}} -> - [RawConfig0] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], - {200, RawConfig0#{id => ID}}; - {error, {_, _, Reason}} -> - serialize_error(Reason) - end; -authenticators2(delete, #{bindings := #{id := AuthenticatorID}}) -> - case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of - {ok, _} -> - {204}; - {error, {_, _, Reason}} -> +authentication3(get, #{bindings := #{listener_id := ListenerID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + list_authenticators([listeners, Type, Name, authentication]); + {error, Reason} -> serialize_error(Reason) end. -move(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> - case Body of - #{<<"position">> := Position} -> - case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of - {ok, _} -> {204}; - {error, {_, _, Reason}} -> serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, position}) +authentication4(get, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + list_authenticator([listeners, Type, Name, authentication], AuthenticatorID); + {error, Reason} -> + serialize_error(Reason) + end; +authentication4(put, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := Config}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + update_authenticator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID, Config); + {error, Reason} -> + serialize_error(Reason) + end; +authentication4(delete, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + delete_authenticator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID); + {error, Reason} -> + serialize_error(Reason) end. +move(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"position">> := Position}}) -> + move_authenitcator([authentication], ?GLOBAL, AuthenticatorID, Position); +move(post, #{bindings := #{id := _}, body := _}) -> + serialize_error({missing_parameter, position}). + +move2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"position">> := Position}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + move_authenitcator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID, Position); + {error, Reason} -> + serialize_error(Reason) + end; +move2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> + serialize_error({missing_parameter, position}). + import_users(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> case Body of #{<<"filename">> := Filename} -> - case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of + case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of ok -> {204}; {error, Reason} -> serialize_error(Reason) end; @@ -1371,9 +1551,9 @@ users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> case UserInfo of #{ <<"user_id">> := UserID, <<"password">> := Password} -> Superuser = maps:get(<<"superuser">>, UserInfo, false), - case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID - , password => Password - , superuser => Superuser}) of + case ?AUTHN:add_user(?GLOBAL, AuthenticatorID, #{ user_id => UserID + , password => Password + , superuser => Superuser}) of {ok, User} -> {201, User}; {error, Reason} -> @@ -1385,7 +1565,7 @@ users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> serialize_error({missing_parameter, user_id}) end; users(get, #{bindings := #{id := AuthenticatorID}}) -> - case emqx_authn:list_users(?CHAIN, AuthenticatorID) of + case ?AUTHN:list_users(?GLOBAL, AuthenticatorID) of {ok, Users} -> {200, Users}; {error, Reason} -> @@ -1400,7 +1580,7 @@ users2(patch, #{bindings := #{id := AuthenticatorID, true -> serialize_error({missing_parameter, password}); false -> - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of + case ?AUTHN:update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo) of {ok, User} -> {200, User}; {error, Reason} -> @@ -1408,28 +1588,110 @@ users2(patch, #{bindings := #{id := AuthenticatorID, end end; users2(get, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of + case ?AUTHN:lookup_user(?GLOBAL, AuthenticatorID, UserID) of {ok, User} -> {200, User}; {error, Reason} -> serialize_error(Reason) end; users2(delete, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of + case ?AUTHN:delete_user(?GLOBAL, AuthenticatorID, UserID) of ok -> {204}; {error, Reason} -> serialize_error(Reason) end. -get_raw_config(ConfKeyPath) -> - %% TODO: call emqx_config:get_raw(ConfKeyPath) directly +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +find_listener(ListenerID) -> + {Type, Name} = emqx_listeners:parse_listener_id(ListenerID), + case emqx_config:find([listeners, Type, Name]) of + {not_found, _, _} -> + {error, {not_found, {listener, ListenerID}}}; + {ok, _} -> + {ok, {Type, Name}} + end. + +create_authenticator(ConfKeyPath, ChainName, Config) -> + case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of + {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, + raw_config := AuthenticatorsConfig}} -> + {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig), + {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +list_authenticators(ConfKeyPath) -> + AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath), + NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), AuthenticatorConfig) + || AuthenticatorConfig <- AuthenticatorsConfig], + {200, NAuthenticators}. + +list_authenticator(ConfKeyPath, AuthenticatorID) -> + AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath), + case find_config(AuthenticatorID, AuthenticatorsConfig) of + {ok, AuthenticatorConfig} -> + {200, AuthenticatorConfig#{id => AuthenticatorID}}; + {error, Reason} -> + serialize_error(Reason) + end. + +update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) -> + case update_config(ConfKeyPath, + {update_authenticator, ChainName, AuthenticatorID, Config}) of + {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, + raw_config := AuthenticatorsConfig}} -> + {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig), + {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) -> + case update_config(ConfKeyPath, {delete_authenticator, ChainName, AuthenticatorID}) of + {ok, _} -> + {204}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> + case update_config(ConfKeyPath, {move_authenticator, ChainName, AuthenticatorID, Position}) of + {ok, _} -> + {204}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +update_config(Path, ConfigRequest) -> + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). + +get_raw_config_with_defaults(ConfKeyPath) -> NConfKeyPath = [atom_to_binary(Key, utf8) || Key <- ConfKeyPath], - emqx_map_lib:deep_get(NConfKeyPath, emqx_config:fill_defaults(emqx_config:get_raw([]))). + RawConfig = emqx_map_lib:deep_get(NConfKeyPath, emqx_config:get_raw([]), []), + to_list(fill_defaults(RawConfig)). + +find_config(AuthenticatorID, AuthenticatorsConfig) -> + case [AC || AC <- to_list(AuthenticatorsConfig), AuthenticatorID =:= ?AUTHN:generate_id(AC)] of + [] -> {error, {not_found, {authenticator, AuthenticatorID}}}; + [AuthenticatorConfig] -> {ok, AuthenticatorConfig} + end. + +fill_defaults(Config) -> + #{<<"authentication">> := CheckedConfig} = hocon_schema:check_plain( + ?AUTHN, #{<<"authentication">> => Config}, #{nullable => true, no_conversion => true}), + CheckedConfig. serialize_error({not_found, {authenticator, ID}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}}; +serialize_error({not_found, {listener, ID}}) -> + {404, #{code => <<"NOT_FOUND">>, + message => list_to_binary(io_lib:format("Listener '~s' does not exist", [ID]))}}; serialize_error(name_has_be_used) -> {409, #{code => <<"ALREADY_EXISTS">>, message => <<"Name has be used">>}}; @@ -1446,3 +1708,8 @@ serialize_error({invalid_parameter, Name}) -> serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. + +to_list(M) when is_map(M) -> + [M]; +to_list(L) when is_list(L) -> + L. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index b7f409bc9..58470289a 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -17,7 +17,6 @@ -module(emqx_authn_app). -include("emqx_authn.hrl"). --include_lib("emqx/include/logger.hrl"). -behaviour(application). @@ -26,33 +25,45 @@ , stop/1 ]). +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + start(_StartType, _StartArgs) -> ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), {ok, Sup} = emqx_authn_sup:start_link(), - emqx_config_handler:add_handler([authentication, authenticators], emqx_authn), - initialize(), + ok = add_providers(), + ok = initialize(), {ok, Sup}. stop(_State) -> + ok = remove_providers(), ok. +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +add_providers() -> + _ = [?AUTHN:add_provider(AuthNType, Provider) || {AuthNType, Provider} <- providers()], ok. + +remove_providers() -> + _ = [?AUTHN:remove_provider(AuthNType) || {AuthNType, _} <- providers()], ok. + initialize() -> - AuthNConfig = emqx:get_config([authentication], #{enable => false, - authenticators => []}), - initialize(AuthNConfig). - -initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) -> - {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), - initialize_authenticators(AuthenticatorsConfig), - Enable =:= true andalso emqx_authn:enable(), + ?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])), + lists:foreach(fun({ListenerID, ListenerConfig}) -> + ?AUTHN:initialize_authentication(atom_to_binary(ListenerID), maps:get(authentication, ListenerConfig, [])) + end, emqx_listeners:list()), ok. -initialize_authenticators([]) -> - ok; -initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) -> - case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of - {ok, _} -> - initialize_authenticators(More); - {error, Reason} -> - ?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason]) - end. +providers() -> + [ {{'password-based', 'built-in-database'}, emqx_authn_mnesia} + , {{'password-based', mysql}, emqx_authn_mysql} + , {{'password-based', posgresql}, emqx_authn_pgsql} + , {{'password-based', mongodb}, emqx_authn_mongodb} + , {{'password-based', redis}, emqx_authn_redis} + , {{'password-based', 'http-server'}, emqx_authn_http} + , {jwt, emqx_authn_jwt} + , {{scram, 'built-in-database'}, emqx_enhanced_authn_scram_mnesia} + ]. \ 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 bceedb6bb..23e412088 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -16,56 +16,15 @@ -module(emqx_authn_schema). --include("emqx_authn.hrl"). -include_lib("typerefl/include/types.hrl"). --behaviour(hocon_schema). - --export([ namespace/0 - , roots/0 - , fields/1 +-export([ common_fields/0 ]). --export([ authenticator_name/1 - ]). - -%% Export it for emqx_gateway_schema module --export([ authenticators/1 - ]). - -namespace() -> authn. - -roots() -> [ "authentication" ]. - -fields("authentication") -> - [ {enable, fun enable/1} - , {authenticators, fun authenticators/1} +common_fields() -> + [ {enable, fun enable/1} ]. -authenticator_name(type) -> binary(); -authenticator_name(nullable) -> false; -authenticator_name(_) -> undefined. - enable(type) -> boolean(); -enable(default) -> false; +enable(default) -> true; enable(_) -> undefined. - -authenticators(type) -> - hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config) - , hoconsc:ref(emqx_authn_mysql, config) - , hoconsc:ref(emqx_authn_pgsql, config) - , hoconsc:ref(emqx_authn_mongodb, standalone) - , hoconsc:ref(emqx_authn_mongodb, 'replica-set') - , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') - , hoconsc:ref(emqx_authn_redis, standalone) - , hoconsc:ref(emqx_authn_redis, cluster) - , hoconsc:ref(emqx_authn_redis, sentinel) - , hoconsc:ref(emqx_authn_http, get) - , hoconsc:ref(emqx_authn_http, post) - , hoconsc:ref(emqx_authn_jwt, 'hmac-based') - , hoconsc:ref(emqx_authn_jwt, 'public-key') - , hoconsc:ref(emqx_authn_jwt, 'jwks') - , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) - ]}); -authenticators(default) -> []; -authenticators(_) -> undefined. diff --git a/apps/emqx_authn/src/emqx_authn_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl index 56fcf299a..dd672a7c7 100644 --- a/apps/emqx_authn/src/emqx_authn_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -26,11 +26,5 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - ChildSpecs = [ - #{id => emqx_authn, - start => {emqx_authn, start_link, []}, - restart => permanent, - type => worker, - modules => [emqx_authn]} - ], + ChildSpecs = [], {ok, {{one_for_one, 10, 10}, ChildSpecs}}. 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 index d7902d824..aa21c0484 100644 --- 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 @@ -20,13 +20,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -75,21 +77,16 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:scram:builtin_db". +namespace() -> "authn:scram:builtin-db". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, [scram]}} - , {server_type, fun server_type/1} + [ {mechanism, {enum, [scram]}} + , {backend, {enum, ['built-in-database']}} , {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. + ] ++ emqx_authn_schema:common_fields(). algorithm(type) -> hoconsc:enum([sha256, sha512]); algorithm(default) -> sha256; @@ -103,6 +100,9 @@ iteration_count(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ algorithm := Algorithm , iteration_count := IterationCount , '_unique' := Unique 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 080b71ab1..1bec0d903 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -21,6 +21,7 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 @@ -28,7 +29,8 @@ , validations/0 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -38,7 +40,7 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:http". +namespace() -> "authn:password-based:http-server". roots() -> [ {config, {union, [ hoconsc:ref(?MODULE, get) @@ -59,15 +61,15 @@ fields(post) -> ] ++ common_fields(). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, ['http-server']}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['http-server']}} , {url, fun url/1} - , {form_data, fun form_data/1} + , {body, fun body/1} , {request_timeout, fun request_timeout/1} - ] ++ maps:to_list(maps:without([ base_url - , pool_type], - maps:from_list(emqx_connector_http:fields(config)))). + ] ++ emqx_authn_schema:common_fields() + ++ maps:to_list(maps:without([ base_url + , pool_type], + maps:from_list(emqx_connector_http:fields(config)))). validations() -> [ {check_ssl_opts, fun check_ssl_opts/1} @@ -95,11 +97,10 @@ headers_no_content_type(converter) -> headers_no_content_type(default) -> default_headers_no_content_type(); headers_no_content_type(_) -> undefined. -%% TODO: Using map() -form_data(type) -> map(); -form_data(nullable) -> false; -form_data(validate) -> [fun check_form_data/1]; -form_data(_) -> undefined. +body(type) -> map(); +body(nullable) -> false; +body(validate) -> [fun check_body/1]; +body(_) -> undefined. request_timeout(type) -> non_neg_integer(); request_timeout(default) -> 5000; @@ -109,10 +110,15 @@ request_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) + ]. + create(#{ method := Method , url := URL , headers := Headers - , form_data := FormData + , body := Body , request_timeout := RequestTimeout , '_unique' := Unique } = Config) -> @@ -121,8 +127,8 @@ create(#{ method := Method State = #{ method => Method , path => Path , base_query => cow_qs:parse_qs(list_to_binary(Query)) - , headers => normalize_headers(Headers) - , form_data => maps:to_list(FormData) + , headers => maps:to_list(Headers) + , body => maps:to_list(Body) , request_timeout => RequestTimeout , '_unique' => Unique }, @@ -189,10 +195,10 @@ check_url(URL) -> {error, _} -> false end. -check_form_data(FormData) -> +check_body(Body) -> lists:any(fun({_, V}) -> not is_binary(V) - end, maps:to_list(FormData)). + end, maps:to_list(Body)). default_headers() -> maps:put(<<"content-type">>, @@ -232,23 +238,20 @@ parse_url(URL) -> URIMap end. -normalize_headers(Headers) -> - [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)]. - generate_request(Credential, #{method := Method, path := Path, base_query := BaseQuery, headers := Headers, - form_data := FormData0}) -> - FormData = replace_placeholders(FormData0, Credential), + body := Body0}) -> + Body = replace_placeholders(Body0, Credential), case Method of get -> - NPath = append_query(Path, BaseQuery ++ FormData), + NPath = append_query(Path, BaseQuery ++ Body), {NPath, Headers}; post -> NPath = append_query(Path, BaseQuery), ContentType = proplists:get_value(<<"content-type">>, Headers), - Body = serialize_body(ContentType, FormData), + Body = serialize_body(ContentType, Body), {NPath, Headers, Body} end. @@ -279,10 +282,10 @@ qs([], Acc) -> 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). +serialize_body(<<"application/json">>, Body) -> + emqx_json:encode(Body); +serialize_body(<<"application/x-www-form-urlencoded">>, Body) -> + qs(Body). safely_parse_body(ContentType, Body) -> try parse_body(ContentType, Body) of 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 1ce10a2cc..e55b58795 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -19,13 +19,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -81,12 +83,11 @@ fields(ssl_disable) -> [ {enable, #{type => false}} ]. common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, [jwt]}} + [ {mechanism, {enum, [jwt]}} , {verify_claims, fun verify_claims/1} - ]. + ] ++ emqx_authn_schema:common_fields(). -secret(type) -> string(); +secret(type) -> binary(); secret(_) -> undefined. secret_base64_encoded(type) -> boolean(); @@ -133,6 +134,12 @@ verify_claims(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, 'hmac-based') + , hoconsc:ref(?MODULE, 'public-key') + , hoconsc:ref(?MODULE, 'jwks') + ]. + create(#{verify_claims := VerifyClaims} = Config) -> create2(Config#{verify_claims => handle_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 efe974145..f41edab8b 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -20,10 +20,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ namespace/0, roots/0, fields/1 ]). +-export([ namespace/0 + , roots/0 + , fields/1 + ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -79,17 +84,16 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:builtin_db". +namespace() -> "authn:password-based:builtin-db". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, ['built-in-database']}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['built-in-database']}} , {user_id_type, fun user_id_type/1} , {password_hash_algorithm, fun password_hash_algorithm/1} - ]; + ] ++ emqx_authn_schema:common_fields(); fields(bcrypt) -> [ {name, {enum, [bcrypt]}} @@ -117,6 +121,9 @@ salt_rounds(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ user_id_type := Type , password_hash_algorithm := #{name := bcrypt, salt_rounds := SaltRounds} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index d272fe05b..f35be985a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -21,13 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,7 +39,7 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:mongodb". +namespace() -> "authn:password-based:mongodb". roots() -> [ {config, {union, [ hoconsc:mk(standalone) @@ -56,16 +58,15 @@ fields('sharded-cluster') -> common_fields() ++ emqx_connector_mongo:fields(sharded). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [mongodb]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [mongodb]}} , {collection, fun collection/1} , {selector, fun selector/1} , {password_hash_field, fun password_hash_field/1} , {salt_field, fun salt_field/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} - ]. + ] ++ emqx_authn_schema:common_fields(). collection(type) -> binary(); collection(nullable) -> false; @@ -95,6 +96,12 @@ salt_position(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, standalone) + , hoconsc:ref(?MODULE, 'replica-set') + , hoconsc:ref(?MODULE, 'sharded-cluster') + ]. + create(#{ selector := Selector , '_unique' := Unique } = Config) -> 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 c94798aa6..67ccbf7ae 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -21,13 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,19 +39,19 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:mysql". +namespace() -> "authn:password-based:mysql". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [mysql]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [mysql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} , {query, fun query/1} , {query_timeout, fun query_timeout/1} - ] ++ emqx_connector_schema_lib:relational_db_fields() + ] ++ emqx_authn_schema:common_fields() + ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; @@ -72,6 +74,9 @@ query_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ password_hash_algorithm := Algorithm , salt_position := SaltPosition , query := Query0 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 6875c5cb9..7676f338d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -22,10 +22,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ namespace/0, roots/0, fields/1 ]). +-export([ namespace/0 + , roots/0 + , fields/1 + ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -35,18 +40,18 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:postgres". +namespace() -> "authn:password-based:postgresql". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [pgsql]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [postgresql]}} , {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_authn_schema:common_fields() + ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; @@ -61,6 +66,9 @@ query(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ query := Query0 , password_hash_algorithm := Algorithm , salt_position := SaltPosition diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 6c5a81652..18840fdea 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -21,13 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,7 +39,8 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:redis". +namespace() -> "authn:password-based:redis". + roots() -> [ {config, {union, [ hoconsc:mk(standalone) , hoconsc:mk(cluster) @@ -55,13 +58,12 @@ fields(sentinel) -> common_fields() ++ emqx_connector_redis:fields(sentinel). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [redis]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [redis]}} , {query, fun query/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} - ]. + ] ++ emqx_authn_schema:common_fields(). query(type) -> string(); query(nullable) -> false; @@ -79,6 +81,12 @@ salt_position(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, standalone) + , hoconsc:ref(?MODULE, cluster) + , hoconsc:ref(?MODULE, sentinel) + ]. + create(#{ query := Query , '_unique' := Unique } = Config) -> diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index eb7f0291a..31bac76a3 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -15,101 +15,3 @@ %%-------------------------------------------------------------------- -module(emqx_authn_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --include("emqx_authn.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]), - Config. - -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. - -t_chain(_) -> - ?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)), - - ChainID = <<"mychain">>, - 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)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), - ok. - -t_authenticator(_) -> - AuthenticatorName1 = <<"myauthenticator1">>, - AuthenticatorConfig1 = #{name => AuthenticatorName1, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), - ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)), - ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - - AuthenticatorConfig2 = #{name => AuthenticatorName1, - mechanism => jwt, - use_jwks => false, - algorithm => 'hmac-based', - secret => <<"abcdef">>, - secret_base64_encoded => false, - verify_claims => []}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), - - ID2 = <<"random">>, - ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), - ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), - - AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2}, - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), - ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)), - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), - - ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), - ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), - ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, bottom)), - ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})), - - ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})), - - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), - ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), - ok. - -t_authenticate(_) -> - ClientInfo = #{zone => default, - listener => {tcp, default}, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), - ?assertEqual(false, emqx_authn:is_enabled()), - emqx_authn:enable(), - ?assertEqual(true, emqx_authn:is_enabled()), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)). diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index ddb2bb209..5e06211a7 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -16,143 +16,143 @@ -module(emqx_authn_jwt_SUITE). --compile(export_all). --compile(nowarn_export_all). +% -compile(export_all). +% -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). +% -include_lib("common_test/include/ct.hrl"). +% -include_lib("eunit/include/eunit.hrl"). --include("emqx_authn.hrl"). +% -include("emqx_authn.hrl"). --define(AUTH, emqx_authn). +% -define(AUTH, emqx_authn). -all() -> - emqx_ct:all(?MODULE). +% all() -> +% emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), - Config. +% init_per_suite(Config) -> +% emqx_ct_helpers:start_apps([emqx_authn]), +% Config. -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. +% end_per_suite(_) -> +% emqx_ct_helpers:stop_apps([emqx_authn]), +% ok. -t_jwt_authenticator(_) -> - AuthenticatorName = <<"myauthenticator">>, - Config = #{name => AuthenticatorName, - mechanism => jwt, - use_jwks => false, - algorithm => 'hmac-based', - secret => <<"abcdef">>, - secret_base64_encoded => false, - verify_claims => []}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), +% t_jwt_authenticator(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% Config = #{name => AuthenticatorName, +% mechanism => jwt, +% use_jwks => false, +% algorithm => 'hmac-based', +% secret => <<"abcdef">>, +% secret_base64_encoded => false, +% verify_claims => []}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), - Payload = #{<<"username">> => <<"myuser">>}, - JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), - ClientInfo = #{username => <<"myuser">>, - password => JWS}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% Payload = #{<<"username">> => <<"myuser">>}, +% JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), +% ClientInfo = #{username => <<"myuser">>, +% password => JWS}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, - JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), - ClientInfo1 = #{username => <<"myuser">>, - password => JWS1}, - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, +% JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), +% ClientInfo1 = #{username => <<"myuser">>, +% password => JWS1}, +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), - ClientInfo2 = ClientInfo#{password => BadJWS}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), +% ClientInfo2 = ClientInfo#{password => BadJWS}, +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), - %% secret_base64_encoded - Config2 = Config#{secret => base64:encode(<<"abcdef">>), - secret_base64_encoded => true}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% %% secret_base64_encoded +% Config2 = Config#{secret => base64:encode(<<"abcdef">>), +% secret_base64_encoded => true}, +% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), +% Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, +% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?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({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% %% Expiration +% Payload3 = #{ <<"username">> => <<"myuser">> +% , <<"exp">> => erlang:system_time(second) - 60}, +% JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), +% ClientInfo3 = ClientInfo#{password => JWS3}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), - Payload4 = #{ <<"username">> => <<"myuser">> - , <<"exp">> => erlang:system_time(second) + 60}, - JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), - ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% Payload4 = #{ <<"username">> => <<"myuser">> +% , <<"exp">> => erlang:system_time(second) + 60}, +% JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), +% ClientInfo4 = ClientInfo#{password => JWS4}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - %% Issued At - Payload5 = #{ <<"username">> => <<"myuser">> - , <<"iat">> => erlang:system_time(second) - 60}, - JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), - ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), +% %% Issued At +% Payload5 = #{ <<"username">> => <<"myuser">> +% , <<"iat">> => erlang:system_time(second) - 60}, +% JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), +% ClientInfo5 = ClientInfo#{password => JWS5}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), - Payload6 = #{ <<"username">> => <<"myuser">> - , <<"iat">> => erlang:system_time(second) + 60}, - JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), - ClientInfo6 = ClientInfo#{password => JWS6}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)), +% Payload6 = #{ <<"username">> => <<"myuser">> +% , <<"iat">> => erlang:system_time(second) + 60}, +% JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), +% ClientInfo6 = ClientInfo#{password => JWS6}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)), - %% Not Before - Payload7 = #{ <<"username">> => <<"myuser">> - , <<"nbf">> => erlang:system_time(second) - 60}, - JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), - ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), +% %% Not Before +% Payload7 = #{ <<"username">> => <<"myuser">> +% , <<"nbf">> => erlang:system_time(second) - 60}, +% JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), +% ClientInfo7 = ClientInfo#{password => JWS7}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), - Payload8 = #{ <<"username">> => <<"myuser">> - , <<"nbf">> => erlang:system_time(second) + 60}, - JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), - ClientInfo8 = ClientInfo#{password => JWS8}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)), +% Payload8 = #{ <<"username">> => <<"myuser">> +% , <<"nbf">> => erlang:system_time(second) + 60}, +% JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), +% ClientInfo8 = ClientInfo#{password => JWS8}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. -t_jwt_authenticator2(_) -> - 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 = #{name => AuthenticatorName, - mechanism => jwt, - use_jwks => false, - algorithm => 'public-key', - certificate => PublicKey, - verify_claims => []}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), +% t_jwt_authenticator2(_) -> +% 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 = #{name => AuthenticatorName, +% mechanism => jwt, +% use_jwks => false, +% algorithm => 'public-key', +% certificate => PublicKey, +% verify_claims => []}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), - Payload = #{<<"username">> => <<"myuser">>}, - JWS = generate_jws('public-key', Payload, PrivateKey), - ClientInfo = #{username => <<"myuser">>, - password => JWS}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), +% Payload = #{<<"username">> => <<"myuser">>}, +% JWS = generate_jws('public-key', Payload, PrivateKey), +% ClientInfo = #{username => <<"myuser">>, +% password => JWS}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% 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. +% 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 index d6425a89c..acfe71809 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -16,149 +16,149 @@ -module(emqx_authn_mnesia_SUITE). --compile(export_all). --compile(nowarn_export_all). +% -compile(export_all). +% -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). +% -include_lib("common_test/include/ct.hrl"). +% -include_lib("eunit/include/eunit.hrl"). --include("emqx_authn.hrl"). +% -include("emqx_authn.hrl"). --define(AUTH, emqx_authn). +% -define(AUTH, emqx_authn). -all() -> - emqx_ct:all(?MODULE). +% all() -> +% emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), - Config. +% init_per_suite(Config) -> +% emqx_ct_helpers:start_apps([emqx_authn]), +% Config. -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. +% end_per_suite(_) -> +% emqx_ct_helpers:stop_apps([emqx_authn]), +% ok. -t_mnesia_authenticator(_) -> - AuthenticatorName = <<"myauthenticator">>, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% t_mnesia_authenticator(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% AuthenticatorConfig = #{name => AuthenticatorName, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - UserInfo = #{user_id => <<"myuser">>, - password => <<"mypass">>}, - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% UserInfo = #{user_id => <<"myuser">>, +% password => <<"mypass">>}, +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ClientInfo = #{zone => external, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - ?AUTH:enable(), - ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), +% ClientInfo = #{zone => external, +% username => <<"myuser">>, +% password => <<"mypass">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?AUTH:enable(), +% ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), - ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), +% ClientInfo2 = ClientInfo#{username => <<"baduser">>}, +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), - ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), - ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), +% ClientInfo3 = ClientInfo#{password => <<"badpass">>}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), - UserInfo2 = UserInfo#{password => <<"mypass2">>}, - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), - ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% UserInfo2 = UserInfo#{password => <<"mypass2">>}, +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), +% ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ok. +% {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), +% ok. -t_import(_) -> - AuthenticatorName = <<"myauthenticator">>, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% t_import(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% AuthenticatorConfig = #{name => AuthenticatorName, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - Dir = code:lib_dir(emqx_authn, test), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), +% Dir = code:lib_dir(emqx_authn, test), +% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), +% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), +% ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), +% ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), - ClientInfo1 = #{username => <<"myuser1">>, - password => <<"mypassword1">>}, - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ClientInfo1 = #{username => <<"myuser1">>, +% password => <<"mypassword1">>}, +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, - password => <<"mypassword2">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, +% password => <<"mypassword2">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, - password => <<"mypassword3">>}, - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, +% password => <<"mypassword3">>}, +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. -t_multi_mnesia_authenticator(_) -> - AuthenticatorName1 = <<"myauthenticator1">>, - AuthenticatorConfig1 = #{name => AuthenticatorName1, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig2 = #{name => AuthenticatorName2, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => clientid, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), +% t_multi_mnesia_authenticator(_) -> +% AuthenticatorName1 = <<"myauthenticator1">>, +% AuthenticatorConfig1 = #{name => AuthenticatorName1, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% AuthenticatorName2 = <<"myauthenticator2">>, +% AuthenticatorConfig2 = #{name => AuthenticatorName2, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => clientid, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), +% {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, - ?AUTH:add_user(?CHAIN, ID1, - #{user_id => <<"myuser">>, - password => <<"mypass1">>})), - ?assertMatch({ok, #{user_id := <<"myclient">>}}, - ?AUTH:add_user(?CHAIN, ID2, - #{user_id => <<"myclient">>, - password => <<"mypass2">>})), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, +% ?AUTH:add_user(?CHAIN, ID1, +% #{user_id => <<"myuser">>, +% password => <<"mypass1">>})), +% ?assertMatch({ok, #{user_id := <<"myclient">>}}, +% ?AUTH:add_user(?CHAIN, ID2, +% #{user_id => <<"myclient">>, +% password => <<"mypass2">>})), - ClientInfo1 = #{username => <<"myuser">>, - clientid => <<"myclient">>, - password => <<"mypass1">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), +% ClientInfo1 = #{username => <<"myuser">>, +% clientid => <<"myclient">>, +% password => <<"mypass1">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), +% ok. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 0b769748a..906b57fb3 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -242,15 +242,8 @@ init_worker_options([_ | R], Acc) -> init_worker_options(R, Acc); init_worker_options([], Acc) -> Acc. -host_port(HostPort) -> - case string:split(HostPort, ":") of - [Host, Port] -> - {ok, Host1} = inet:parse_address(Host), - [{host, Host1}, {port, list_to_integer(Port)}]; - [Host] -> - {ok, Host1} = inet:parse_address(Host), - [{host, Host1}] - end. +host_port({Host, Port}) -> + [{host, Host}, {port, Port}]. server(type) -> server(); server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 4fe26381e..44b036f39 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -19,9 +19,13 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --type server() :: emqx_schema:ip_port(). +-type server() :: tuple(). + -reflect_type([server/0]). --typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}). + +-typerefl_from_string({server/0, ?MODULE, to_server}). + +-export([to_server/1]). -export([roots/0, fields/1]). @@ -168,3 +172,9 @@ redis_fields() -> default => 0}} , {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} ]. + +to_server(Server) -> + case string:tokens(Server, ":") of + [Host, Port] -> {ok, {Host, list_to_integer(Port)}}; + _ -> {error, Server} + end. \ No newline at end of file diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 5134246cd..b3b568271 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -29,12 +29,13 @@ gateway.stomp { password = "${Packet.headers.passcode}" } - authentication { - name = "authenticator1" - mechanism = password-based - server_type = built-in-database - user_id_type = clientid - } + authentication: [ + # { + # name = "authenticator1" + # type = "password-based:built-in-database" + # user_id_type = clientid + # } + ] listeners.tcp.default { bind = 61613 @@ -63,13 +64,6 @@ gateway.coap { subscribe_qos = qos0 publish_qos = qos1 - authentication { - name = "authenticator1" - mechanism = password-based - server_type = built-in-database - user_id_type = clientid - } - listeners.udp.default { bind = 5683 } diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index da73b85ee..45e338a36 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -222,25 +222,25 @@ fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). -authentication() -> - hoconsc:union( - [ undefined - , hoconsc:ref(emqx_authn_mnesia, config) - , hoconsc:ref(emqx_authn_mysql, config) - , hoconsc:ref(emqx_authn_pgsql, config) - , hoconsc:ref(emqx_authn_mongodb, standalone) - , hoconsc:ref(emqx_authn_mongodb, 'replica-set') - , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') - , hoconsc:ref(emqx_authn_redis, standalone) - , hoconsc:ref(emqx_authn_redis, cluster) - , hoconsc:ref(emqx_authn_redis, sentinel) - , hoconsc:ref(emqx_authn_http, get) - , hoconsc:ref(emqx_authn_http, post) - , hoconsc:ref(emqx_authn_jwt, 'hmac-based') - , hoconsc:ref(emqx_authn_jwt, 'public-key') - , hoconsc:ref(emqx_authn_jwt, 'jwks') - , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) - ]). +% authentication() -> +% hoconsc:union( +% [ undefined +% , hoconsc:ref(emqx_authn_mnesia, config) +% , hoconsc:ref(emqx_authn_mysql, config) +% , hoconsc:ref(emqx_authn_pgsql, config) +% , hoconsc:ref(emqx_authn_mongodb, standalone) +% , hoconsc:ref(emqx_authn_mongodb, 'replica-set') +% , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') +% , hoconsc:ref(emqx_authn_redis, standalone) +% , hoconsc:ref(emqx_authn_redis, cluster) +% , hoconsc:ref(emqx_authn_redis, sentinel) +% , hoconsc:ref(emqx_authn_http, get) +% , hoconsc:ref(emqx_authn_http, post) +% , hoconsc:ref(emqx_authn_jwt, 'hmac-based') +% , hoconsc:ref(emqx_authn_jwt, 'public-key') +% , hoconsc:ref(emqx_authn_jwt, 'jwks') +% , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) +% ]). gateway_common_options() -> [ {enable, sc(boolean(), undefined, true)} @@ -248,7 +248,7 @@ gateway_common_options() -> , {idle_timeout, sc(duration(), undefined, <<"30s">>)} , {mountpoint, sc(binary())} , {clientinfo_override, sc(ref(clientinfo_override))} - , {authentication, sc(authentication(), undefined, undefined)} + , {authentication, sc(hoconsc:lazy(map()))} ]. %%-------------------------------------------------------------------- diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 657594ae8..4894bda98 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -46,7 +46,6 @@ , emqx_data_bridge_schema , emqx_retainer_schema , emqx_statsd_schema - , emqx_authn_schema , emqx_authz_schema , emqx_auto_subscribe_schema , emqx_bridge_mqtt_schema diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 8e14dd21d..3df52b7a7 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -19,10 +19,8 @@ -behaviour(gen_server). -include("emqx_retainer.hrl"). --include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). - -export([start_link/0]). -export([ on_session_subscribed/4 diff --git a/rebar.config b/rebar.config index b1616bebc..1abeef9a6 100644 --- a/rebar.config +++ b/rebar.config @@ -63,6 +63,7 @@ , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} + , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} ]}. {xref_ignores, From 455baa54656934ff06500133316d44a194a9edaa Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 6 Sep 2021 17:59:14 +0800 Subject: [PATCH 264/306] feat: dashboard UI version, beat 11 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fabb8a7df..e646f5b7a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.10 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.11 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From 0dd09b06f18f09a3e302e3f5773e952b2a5a9e14 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 3 Sep 2021 09:40:06 +0200 Subject: [PATCH 265/306] build: dep openssl11 is limited to amd64 centos7 --- deploy/packages/rpm/emqx.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/packages/rpm/emqx.spec b/deploy/packages/rpm/emqx.spec index 4a4d6d0f3..44e02ea45 100644 --- a/deploy/packages/rpm/emqx.spec +++ b/deploy/packages/rpm/emqx.spec @@ -19,7 +19,7 @@ BuildRoot: %{_tmppath}/%{_name}-%{_version}-root Provides: %{_name} AutoReq: 0 -%if 0%{?rhel} == 7 +%if "%{_arch} %{?rhel}" == "amd64 7" Requires: openssl11 libatomic %else Requires: libatomic From 6e7d3d05e4ca4ba877d8a4aa40619c2bbe83b331 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 3 Sep 2021 09:50:25 +0200 Subject: [PATCH 266/306] ci: install openssl11 for centos7 amd64 only --- .ci/build_packages/tests.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 5d6422231..53ab9ac57 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -91,8 +91,12 @@ emqx_test(){ ;; "rpm") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.rpm) - # EMQX OTP requires openssl11 to have TLS1.3 support - yum install -y openssl11 + + if [[ "${ARCH}" == "amd64" && $(rpm -E '%{rhel}') == 7 ]] ; + then + # EMQX OTP requires openssl11 to have TLS1.3 support + yum install -y openssl11; + fi rpm -ivh "${PACKAGE_PATH}/${packagename}" if ! rpm -q emqx | grep -q emqx; then echo "package install error" From ba9ad47cc188dd5d42bb472aaa4ec86483ebeb31 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 3 Sep 2021 10:40:38 +0800 Subject: [PATCH 267/306] fix(gw-exproto): fix grpc remote call --- .../src/exproto/emqx_exproto_gsvr.erl | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl index 0135aa8e3..04f3dea1c 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl @@ -22,9 +22,10 @@ -include("src/exproto/include/emqx_exproto.hrl"). -include_lib("emqx/include/logger.hrl"). - -define(IS_QOS(X), (X =:= 0 orelse X =:= 1 orelse X =:= 2)). +-define(DEFAULT_CALL_TIMEOUT, 5000). + %% gRPC server callbacks -export([ send/2 , close/2 @@ -117,18 +118,22 @@ to_pid(ConnStr) -> binary_to_term(base64:decode(ConnStr)). call(ConnStr, Req) -> - case catch to_pid(ConnStr) of - {'EXIT', {badarg, _}} -> - {error, ?RESP_PARAMS_TYPE_ERROR, - <<"The conn type error">>}; - Pid when is_pid(Pid) -> - case erlang:is_process_alive(Pid) of - true -> - emqx_gateway_conn:call(Pid, Req); - false -> - {error, ?RESP_CONN_PROCESS_NOT_ALIVE, - <<"Connection process is not alive">>} - end + try + Pid = to_pid(ConnStr), + emqx_gateway_conn:call(Pid, Req, ?DEFAULT_CALL_TIMEOUT) + catch + exit : badarg -> + {error, ?RESP_PARAMS_TYPE_ERROR, <<"The conn type error">>}; + exit : noproc -> + {error, ?RESP_CONN_PROCESS_NOT_ALIVE, + <<"Connection process is not alive">>}; + exit : timeout -> + {error, ?RESP_UNKNOWN, <<"Connection is not answered">>}; + Class : Reason : Stk-> + ?LOG(error, "Call ~p crashed: {~0p, ~0p}, " + "stacktrace: ~0p", + [Class, Reason, Stk]), + {error, ?RESP_UNKNOWN, <<"Unkwown crashs">>} end. %%-------------------------------------------------------------------- From caee51f92a6a5a803e9d37a45e11f683c31e9d3e Mon Sep 17 00:00:00 2001 From: lafirest Date: Mon, 6 Sep 2021 10:37:22 +0800 Subject: [PATCH 268/306] fix(emqx_lwm2m): fix some error and incomplete function --- apps/emqx_gateway/etc/emqx_gateway.conf | 15 +- apps/emqx_gateway/src/coap/README.md | 17 +- .../src/coap/emqx_coap_channel.erl | 16 +- .../src/coap/emqx_coap_medium.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 1 + .../src/lwm2m/emqx_lwm2m_channel.erl | 92 +++--- .../emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl | 28 +- .../src/lwm2m/emqx_lwm2m_session.erl | 263 ++++++------------ apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 17 +- 9 files changed, 210 insertions(+), 247 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 5134246cd..7a608cf15 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -60,6 +60,9 @@ gateway.coap { heartbeat = 30s notify_type = qos + + ## if true, you need to establish a connection before use + connection_required = false subscribe_qos = qos0 publish_qos = qos1 @@ -134,7 +137,7 @@ gateway.lwm2m { enable_stats = true ## When publishing or subscribing, prefix all topics with a mountpoint string. - mountpoint = "lwm2m" + mountpoint = "lwm2m/%u" xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" @@ -149,27 +152,27 @@ gateway.lwm2m { translators { command { - topic = "dn/#" + topic = "/dn/#" qos = 0 } response { - topic = "up/resp" + topic = "/up/resp" qos = 0 } notify { - topic = "up/notify" + topic = "/up/notify" qos = 0 } register { - topic = "up/resp" + topic = "/up/resp" qos = 0 } update { - topic = "up/resp" + topic = "/up/resp" qos = 0 } } diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index 88f657537..54f0fde84 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -414,21 +414,30 @@ Server will return token **X** in payload 2. Update Connection ``` -coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" +coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&token=X" ``` 3. Publish ``` -coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +coap-client -m post -e "Hellow" "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" ``` if you want to publish with auth, you must first establish a connection, and then post publish request on the same socket, so libcoap client can't simulation publish with a token +``` +coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&token=X" +``` + 4. Subscribe ``` coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" ``` +**Or** +``` +coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&token=X" +``` 5. Close Connection ``` -coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" -``` \ No newline at end of file +coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&token=X +``` + diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 6e554b9ef..11aca8cc8 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -55,6 +55,8 @@ %% Timer timers :: #{atom() => disable | undefined | reference()}, + connection_required :: boolean(), + conn_state :: idle | connected, token :: binary() | undefined @@ -63,6 +65,8 @@ -type channel() :: #channel{}. -define(TOKEN_MAXIMUM, 4294967295). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). +-define(DEF_IDLE_TIME, timer:seconds(30)). +-define(GET_IDLE_TIME(Cfg), maps:get(idle_timeout, Cfg, ?DEF_IDLE_TIME)). -import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- @@ -110,13 +114,14 @@ init(ConnInfo = #{peername := {PeerHost, _}, } ), - Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), + Heartbeat = ?GET_IDLE_TIME(Config), #channel{ ctx = Ctx , conninfo = ConnInfo , clientinfo = ClientInfo , timers = #{} , session = emqx_coap_session:new() , keepalive = emqx_keepalive:init(Heartbeat) + , connection_required = maps:get(connection_required, Config, false) , conn_state = idle }. @@ -216,13 +221,12 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> ensure_keepalive_timer(Channel) -> ensure_keepalive_timer(fun ensure_timer/4, Channel). -ensure_keepalive_timer(Fun, Channel) -> - Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), +ensure_keepalive_timer(Fun, #channel{keepalive = KeepAlive} = Channel) -> + Heartbeat = emqx_keepalive:info(interval, KeepAlive), Fun(keepalive, Heartbeat, keepalive, Channel). -check_auth_state(Msg, Channel) -> - Enable = emqx:get_config([gateway, coap, enable_stats]), - check_token(Enable, Msg, Channel). +check_auth_state(Msg, #channel{connection_required = Required} = Channel) -> + check_token(Required, Msg, Channel). check_token(true, Msg, diff --git a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl index ae5763179..8dafc7bbb 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl @@ -29,13 +29,14 @@ , reply/2, reply/3, reply/4]). %%-type result() :: map() | empty. --define(DEFINE_DEF(Name), Name(Msg) -> Name(Msg, #{})). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- empty() -> #{}. -?DEFINE_DEF(reset). +reset(Msg) -> + reset(Msg, #{}). reset(Msg, Result) -> out(emqx_coap_message:reset(Msg), Result). @@ -49,7 +50,8 @@ out(Msg, #{out := Outs} = Result) -> out(Msg, Result) -> Result#{out => [Msg]}. -?DEFINE_DEF(proto_out). +proto_out(Proto) -> + proto_out(Proto, #{}). proto_out(Proto, Resut) -> Resut#{proto => Proto}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index da73b85ee..4e0142a47 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -83,6 +83,7 @@ fields(mqttsn_predefined) -> fields(coap_structs) -> [ {heartbeat, sc(duration(), undefined, <<"30s">>)} + , {connection_required, sc(boolean(), undefined, false)} , {notify_type, sc(union([non, con, qos]), undefined, qos)} , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl index b67032313..d0647897b 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -24,8 +24,7 @@ -export([ info/1 , info/2 , stats/1 - , validator/2 - , validator/4 + , with_context/2 , do_takeover/3]). -export([ init/2 @@ -53,11 +52,10 @@ %% Timer timers :: #{atom() => disable | undefined | reference()}, - validator :: function() + with_context :: function() }). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). - -import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- @@ -109,16 +107,13 @@ init(ConnInfo = #{peername := {PeerHost, _}, , clientinfo = ClientInfo , timers = #{} , session = emqx_lwm2m_session:new() - , validator = validator(Ctx, ClientInfo) + , with_context = with_context(Ctx, ClientInfo) }. -validator(_Type, _Topic, _Ctx, _ClientInfo) -> - allow. - %emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). -validator(Ctx, ClientInfo) -> +with_context(Ctx, ClientInfo) -> fun(Type, Topic) -> - validator(Type, Topic, Ctx, ClientInfo) + with_context(Type, Topic, Ctx, ClientInfo) end. %%-------------------------------------------------------------------- @@ -137,7 +132,10 @@ handle_deliver(Delivers, Channel) -> %%-------------------------------------------------------------------- %% Handle timeout %%-------------------------------------------------------------------- -handle_timeout(_, lifetime, Channel) -> +handle_timeout(_, lifetime, #channel{ctx = Ctx, + clientinfo = ClientInfo, + conninfo = ConnInfo} = Channel) -> + ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, timeout, ConnInfo]), {shutdown, timeout, Channel}; handle_timeout(_, {transport, _} = Msg, Channel) -> @@ -166,6 +164,10 @@ handle_cast(Req, Channel) -> %%-------------------------------------------------------------------- %% Handle Info %%-------------------------------------------------------------------- +handle_info({subscribe, _AutoSubs}, Channel) -> + %% not need handle this message + {ok, Channel}; + handle_info(Info, Channel) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, Channel}. @@ -173,8 +175,12 @@ handle_info(Info, Channel) -> %%-------------------------------------------------------------------- %% Terminate %%-------------------------------------------------------------------- -terminate(_Reason, #channel{session = Session}) -> - emqx_lwm2m_session:on_close(Session). +terminate(Reason, #channel{ctx = Ctx, + clientinfo = ClientInfo, + session = Session}) -> + MountedTopic = emqx_lwm2m_session:on_close(Session), + _ = run_hooks(Ctx, 'session.unsubscribe', [ClientInfo, MountedTopic, #{}]), + run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]). %%-------------------------------------------------------------------- %% Internal functions @@ -220,12 +226,12 @@ do_connect(Req, Result, Channel, Iter) -> Req, Channel) of {ok, _Input, #channel{session = Session, - validator = Validator} = NChannel} -> + with_context = WithContext} = NChannel} -> case emqx_lwm2m_session:info(reg_info, Session) of undefined -> process_connect(ensure_connected(NChannel), Req, Result, Iter); _ -> - NewResult = emqx_lwm2m_session:reregister(Req, Validator, Session), + NewResult = emqx_lwm2m_session:reregister(Req, WithContext, Session), iter(Iter, maps:merge(Result, NewResult), NChannel) end; {error, ReasonCode, NChannel} -> @@ -251,7 +257,7 @@ check_lwm2m_version(#coap_message{options = Opts}, end, if IsValid -> NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) - , proto_name => <<"lwm2m">> + , proto_name => <<"LwM2M">> , proto_ver => Ver }, {ok, Channel#channel{conninfo = NConnInfo}}; @@ -274,7 +280,7 @@ enrich_clientinfo(#coap_message{options = Options} = Msg, Query = maps:get(uri_query, Options, #{}), case Query of #{<<"ep">> := Epn} -> - UserName = maps:get(<<"imei">>, Query, undefined), + UserName = maps:get(<<"imei">>, Query, Epn), Password = maps:get(<<"password">>, Query, undefined), ClientId = maps:get(<<"device_id">>, Query, Epn), ClientInfo = @@ -298,7 +304,7 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of {ok, NClientInfo} -> {ok, Channel#channel{clientinfo = NClientInfo, - validator = validator(Ctx, ClientInfo)}}; + with_context = with_context(Ctx, ClientInfo)}}; {error, Reason} -> ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", [ClientId, Username, Reason]), @@ -308,14 +314,13 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> {ok, ClientInfo}; 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}}. ensure_connected(Channel = #channel{ctx = Ctx, conninfo = ConnInfo, clientinfo = ClientInfo}) -> + _ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]), ok = run_hooks(Ctx, 'client.connected', [ClientInfo, ConnInfo]), Channel. @@ -323,7 +328,7 @@ process_connect(Channel = #channel{ctx = Ctx, session = Session, conninfo = ConnInfo, clientinfo = ClientInfo, - validator = Validator}, + with_context = WithContext}, Msg, Result, Iter) -> %% inherit the old session SessFun = fun(_,_) -> #{} end, @@ -336,7 +341,8 @@ process_connect(Channel = #channel{ctx = Ctx, emqx_lwm2m_session ) of {ok, _} -> - NewResult = emqx_lwm2m_session:init(Msg, Validator, Session), + Mountpoint = maps:get(mountpoint, ClientInfo, <<>>), + NewResult = emqx_lwm2m_session:init(Msg, Mountpoint, WithContext, Session), iter(Iter, maps:merge(Result, NewResult), Channel); {error, Reason} -> ?LOG(error, "Failed to open session du to ~p", [Reason]), @@ -358,13 +364,34 @@ gets([H | T], Map) -> gets([], Val) -> Val. +with_context(publish, [Topic, Msg], Ctx, ClientInfo) -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of + allow -> + emqx:publish(Msg); + _ -> + ?LOG(error, "topic:~p not allow to publish ", [Topic]) + end; + +with_context(subscribe, [Topic, Opts], Ctx, #{username := UserName} = ClientInfo) -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of + allow -> + run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, UserName]), + ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, UserName]), + emqx:subscribe(Topic, UserName, Opts); + _ -> + ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) + end; + +with_context(metrics, Name, Ctx, _ClientInfo) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). + %%-------------------------------------------------------------------- %% Call Chain %%-------------------------------------------------------------------- call_session(Fun, Msg, #channel{session = Session, - validator = Validator} = Channel) -> + with_context = WithContext} = Channel) -> iter([ session, fun process_session/4 , proto, fun process_protocol/4 , return, fun process_return/4 @@ -373,7 +400,7 @@ call_session(Fun, , out, fun process_out/4 , fun process_nothing/3 ], - emqx_lwm2m_session:Fun(Msg, Validator, Session), + emqx_lwm2m_session:Fun(Msg, WithContext, Session), Channel). process_session(Session, Result, Channel, Iter) -> @@ -384,8 +411,8 @@ process_protocol({request, Msg}, Result, Channel, Iter) -> handle_request_protocol(Method, Msg, Result, Channel, Iter); process_protocol(Msg, Result, - #channel{validator = Validator, session = Session} = Channel, Iter) -> - ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, Validator, Session), + #channel{with_context = WithContext, session = Session} = Channel, Iter) -> + ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, WithContext, Session), iter(Iter, maps:merge(Result, ProtoResult), Channel). handle_request_protocol(post, #coap_message{options = Opts} = Msg, @@ -415,10 +442,10 @@ handle_request_protocol(delete, #coap_message{options = Opts} = Msg, end. do_update(Location, Msg, Result, - #channel{session = Session, validator = Validator} = Channel, Iter) -> + #channel{session = Session, with_context = WithContext} = Channel, Iter) -> case check_location(Location, Channel) of true -> - NewResult = emqx_lwm2m_session:update(Msg, Validator, Session), + NewResult = emqx_lwm2m_session:update(Msg, WithContext, Session), iter(Iter, maps:merge(Result, NewResult), Channel); _ -> iter(Iter, reply({error, not_found}, Msg, Result), Channel) @@ -438,13 +465,8 @@ process_out(Outs, Result, Channel, _) -> Reply -> [Reply | Outs2] end, - %% emqx_gateway_conn bug, work around - case Outs3 of - [] -> - {ok, Channel}; - _ -> - {ok, {outgoing, Outs3}, Channel} - end. + + {ok, {outgoing, Outs3}, Channel}. process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> Session2 = emqx_lwm2m_session:set_reply(Reply, Session), diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl index 925ca1d94..7c0cc95cd 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -30,20 +30,20 @@ -define(STANDARD, 1). -%-type msg_type() :: <<"create">> -% | <<"delete">> -% | <<"read">> -% | <<"write">> -% | <<"execute">> -% | <<"discover">> -% | <<"write-attr">> -% | <<"observe">> -% | <<"cancel-observe">>. -% - %-type cmd() :: #{ <<"msgType">> := msg_type() - % , <<"data">> := maps() - % %% more keys? - % }. +%%-type msg_type() :: <<"create">> +%% | <<"delete">> +%% | <<"read">> +%% | <<"write">> +%% | <<"execute">> +%% | <<"discover">> +%% | <<"write-attr">> +%% | <<"observe">> +%% | <<"cancel-observe">>. +%% +%%-type cmd() :: #{ <<"msgType">> := msg_type() +%% , <<"data">> := maps() +%% %%%% more keys? +%% }. %%-------------------------------------------------------------------- %% APIs diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl index 700302bdc..a1d03e04f 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -22,7 +22,7 @@ -include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). %% API --export([new/0, init/3, update/3, reregister/3, on_close/1]). +-export([new/0, init/4, update/3, reregister/3, on_close/1]). -export([ info/1 , info/2 @@ -47,9 +47,10 @@ , wait_ack :: request_context() | undefined , endpoint_name :: binary() | undefined , location_path :: list(binary()) | undefined - , headers :: map() | undefined , reg_info :: map() | undefined , lifetime :: non_neg_integer() | undefined + , is_cache_mode :: boolean() + , mountpoint :: binary() , last_active_at :: non_neg_integer() }). @@ -61,7 +62,7 @@ <<"7">>, <<"9">>, <<"15">>]). %% uplink and downlink topic configuration --define(lwm2m_up_dm_topic, {<<"v1/up/dm">>, 0}). +-define(lwm2m_up_dm_topic, {<<"/v1/up/dm">>, 0}). %% steal from emqx_session -define(INFO_KEYS, [subscriptions, @@ -95,41 +96,44 @@ new() -> #session{ coap = emqx_coap_tm:new() , queue = queue:new() , last_active_at = ?NOW + , is_cache_mode = false + , mountpoint = <<>> , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. --spec init(emqx_coap_message(), function(), session()) -> map(). -init(#coap_message{options = Opts, payload = Payload} = Msg, Validator, Session) -> +-spec init(emqx_coap_message(), binary(), function(), session()) -> map(). +init(#coap_message{options = Opts, + payload = Payload} = Msg, MountPoint, WithContext, Session) -> Query = maps:get(uri_query, Opts), RegInfo = append_object_list(Query, Payload), - Headers = get_headers(RegInfo), LifeTime = get_lifetime(RegInfo), Epn = maps:get(<<"ep">>, Query), Location = [?PREFIX, Epn], - Result = return(register_init(Validator, - Session#session{headers = Headers, - endpoint_name = Epn, - location_path = Location, - reg_info = RegInfo, - lifetime = LifeTime, - queue = queue:new()})), + NewSession = Session#session{endpoint_name = Epn, + location_path = Location, + reg_info = RegInfo, + lifetime = LifeTime, + mountpoint = MountPoint, + is_cache_mode = is_psm(RegInfo) orelse is_qmode(RegInfo), + queue = queue:new()}, + Result = return(register_init(WithContext, NewSession)), Reply = emqx_coap_message:piggyback({ok, created}, Msg), Reply2 = emqx_coap_message:set(location_path, Location, Reply), reply(Reply2, Result#{lifetime => true}). -reregister(Msg, Validator, Session) -> - update(Msg, Validator, <<"register">>, Session). +reregister(Msg, WithContext, Session) -> + update(Msg, WithContext, <<"register">>, Session). -update(Msg, Validator, Session) -> - update(Msg, Validator, <<"update">>, Session). +update(Msg, WithContext, Session) -> + update(Msg, WithContext, <<"update">>, Session). --spec on_close(session()) -> ok. -on_close(#session{endpoint_name = Epn}) -> +-spec on_close(session()) -> binary(). +on_close(Session) -> #{topic := Topic} = downlink_topic(), - MountedTopic = mount(Topic, mountpoint(Epn)), + MountedTopic = mount(Topic, Session), emqx:unsubscribe(MountedTopic), - ok. + MountedTopic. %%-------------------------------------------------------------------- %% Info, Stats @@ -194,15 +198,15 @@ stats(Session) -> info(?STATS_KEYS, Session). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -handle_coap_in(Msg, _Validator, Session) -> +handle_coap_in(Msg, _WithContext, Session) -> call_coap(case emqx_coap_message:is_request(Msg) of true -> handle_request; _ -> handle_response end, Msg, Session#session{last_active_at = ?NOW}). -handle_deliver(Delivers, _Validator, Session) -> - return(deliver(Delivers, Session)). +handle_deliver(Delivers, WithContext, Session) -> + return(deliver(Delivers, WithContext, Session)). timeout({transport, Msg}, _, Session) -> call_coap(timeout, Msg, Session). @@ -214,17 +218,17 @@ set_reply(Msg, #session{coap = Coap} = Session) -> %%-------------------------------------------------------------------- %% Protocol Stack %%-------------------------------------------------------------------- -handle_protocol_in({response, CtxMsg}, Validator, Session) -> - return(handle_coap_response(CtxMsg, Validator, Session)); +handle_protocol_in({response, CtxMsg}, WithContext, Session) -> + return(handle_coap_response(CtxMsg, WithContext, Session)); -handle_protocol_in({ack, CtxMsg}, Validator, Session) -> - return(handle_ack(CtxMsg, Validator, Session)); +handle_protocol_in({ack, CtxMsg}, WithContext, Session) -> + return(handle_ack(CtxMsg, WithContext, Session)); -handle_protocol_in({ack_failure, CtxMsg}, Validator, Session) -> - return(handle_ack_failure(CtxMsg, Validator, Session)); +handle_protocol_in({ack_failure, CtxMsg}, WithContext, Session) -> + return(handle_ack_failure(CtxMsg, WithContext, Session)); -handle_protocol_in({reset, CtxMsg}, Validator, Session) -> - return(handle_ack_reset(CtxMsg, Validator, Session)). +handle_protocol_in({reset, CtxMsg}, WithContext, Session) -> + return(handle_ack_reset(CtxMsg, WithContext, Session)). %%-------------------------------------------------------------------- %% Register @@ -302,50 +306,6 @@ delink(Str) -> Ltrim = binary_util:ltrim(Str, $<), binary_util:rtrim(Ltrim, $>). -get_headers(RegInfo) -> - lists:foldl(fun(K, Acc) -> - get_header(K, RegInfo, Acc) - end, - extract_module_params(RegInfo), - [<<"apn">>, <<"im">>, <<"ct">>, <<"mv">>, <<"mt">>]). - -get_header(Key, RegInfo, Headers) -> - case maps:get(Key, RegInfo, undefined) of - undefined -> - Headers; - Val -> - AtomKey = erlang:binary_to_atom(Key), - Headers#{AtomKey => Val} - end. - -extract_module_params(RegInfo) -> - Keys = [<<"module">>, <<"sv">>, <<"chip">>, <<"imsi">>, <<"iccid">>], - case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Keys) of - true -> #{module_params => undefined}; - false -> - Extras = [<<"rsrp">>, <<"sinr">>, <<"txpower">>, <<"cellid">>], - case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Extras) of - true -> - #{module_params => - #{module => maps:get(<<"module">>, RegInfo), - softversion => maps:get(<<"sv">>, RegInfo), - chiptype => maps:get(<<"chip">>, RegInfo), - imsi => maps:get(<<"imsi">>, RegInfo), - iccid => maps:get(<<"iccid">>, RegInfo)}}; - false -> - #{module_params => - #{module => maps:get(<<"module">>, RegInfo), - softversion => maps:get(<<"sv">>, RegInfo), - chiptype => maps:get(<<"chip">>, RegInfo), - imsi => maps:get(<<"imsi">>, RegInfo), - iccid => maps:get(<<"iccid">>, RegInfo), - rsrp => maps:get(<<"rsrp">>, RegInfo), - sinr => maps:get(<<"sinr">>, RegInfo), - txpower => maps:get(<<"txpower">>, RegInfo), - cellid => maps:get(<<"cellid">>, RegInfo)}} - end - end. - get_lifetime(#{<<"lt">> := LT}) -> case LT of 0 -> emqx:get_config([gateway, lwm2m, lifetime_max]); @@ -362,7 +322,7 @@ get_lifetime(_, OldRegInfo) -> -spec update(emqx_coap_message(), function(), binary(), session()) -> map(). update(#coap_message{options = Opts, payload = Payload} = Msg, - Validator, + WithContext, CmdType, #session{reg_info = OldRegInfo} = Session) -> Query = maps:get(uri_query, Opts), @@ -370,58 +330,51 @@ update(#coap_message{options = Opts, payload = Payload} = Msg, UpdateRegInfo = maps:merge(OldRegInfo, RegInfo), LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo), - Session2 = proto_subscribe(Validator, - Session#session{reg_info = UpdateRegInfo, - lifetime = LifeTime}), + NewSession = Session#session{reg_info = UpdateRegInfo, + is_cache_mode = + is_psm(UpdateRegInfo) orelse is_qmode(UpdateRegInfo), + lifetime = LifeTime}, + + Session2 = proto_subscribe(WithContext, NewSession), Session3 = send_dl_msg(Session2), RegPayload = #{<<"data">> => UpdateRegInfo}, - Session4 = send_to_mqtt(#{}, CmdType, RegPayload, Validator, Session3), + Session4 = send_to_mqtt(#{}, CmdType, RegPayload, WithContext, Session3), Result = return(Session4), Reply = emqx_coap_message:piggyback({ok, changed}, Msg), reply(Reply, Result#{lifetime => true}). -register_init(Validator, #session{reg_info = RegInfo, - endpoint_name = Epn} = Session) -> - +register_init(WithContext, #session{reg_info = RegInfo} = Session) -> Session2 = send_auto_observe(RegInfo, Session), %% - subscribe to the downlink_topic and wait for commands #{topic := Topic, qos := Qos} = downlink_topic(), - MountedTopic = mount(Topic, mountpoint(Epn)), - Session3 = subscribe(MountedTopic, Qos, Validator, Session2), + MountedTopic = mount(Topic, Session), + Session3 = subscribe(MountedTopic, Qos, WithContext, Session2), Session4 = send_dl_msg(Session3), %% - report the registration info RegPayload = #{<<"data">> => RegInfo}, - send_to_mqtt(#{}, <<"register">>, RegPayload, Validator, Session4). + send_to_mqtt(#{}, <<"register">>, RegPayload, WithContext, Session4). %%-------------------------------------------------------------------- %% Subscribe %%-------------------------------------------------------------------- -proto_subscribe(Validator, #session{endpoint_name = Epn, wait_ack = WaitAck} = Session) -> +proto_subscribe(WithContext, #session{wait_ack = WaitAck} = Session) -> #{topic := Topic, qos := Qos} = downlink_topic(), - MountedTopic = mount(Topic, mountpoint(Epn)), + MountedTopic = mount(Topic, Session), Session2 = case WaitAck of undefined -> Session; Ctx -> MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, <<"coap_timeout">>), - send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, Validator, Session) + send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, WithContext, Session) end, - subscribe(MountedTopic, Qos, Validator, Session2). + subscribe(MountedTopic, Qos, WithContext, Session2). -subscribe(Topic, Qos, Validator, - #session{headers = Headers, endpoint_name = EndpointName} = Session) -> - case Validator(subscribe, Topic) of - allow -> - ClientId = maps:get(device_id, Headers, undefined), - Opts = get_sub_opts(Qos), - ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, EndpointName]), - emqx:subscribe(Topic, ClientId, Opts); - _ -> - ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) - end, +subscribe(Topic, Qos, WithContext, Session) -> + Opts = get_sub_opts(Qos), + WithContext(subscribe, [Topic, Opts]), Session. send_auto_observe(RegInfo, Session) -> @@ -486,7 +439,7 @@ handle_coap_response({Ctx = #{<<"msgType">> := EventType}, type = CoapMsgType, payload = CoapMsgPayload, options = CoapMsgOpts}}, - Validator, + WithContext, Session) -> MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), @@ -495,46 +448,43 @@ handle_coap_response({Ctx = #{<<"msgType">> := EventType}, {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is a notification for status update during NB firmware upgrade. %% need to reply to DM http callbacks - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, Validator, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session); {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is actually a notification, correct the msgType - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, Validator, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session); _ -> - send_to_mqtt(Ctx, EventType, MqttPayload, Validator, Session) + send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session) end, send_dl_msg(Ctx, Session2). %%-------------------------------------------------------------------- %% Ack %%-------------------------------------------------------------------- -handle_ack({Ctx, _}, Validator, Session) -> +handle_ack({Ctx, _}, WithContext, Session) -> Session2 = send_dl_msg(Ctx, Session), MqttPayload = emqx_lwm2m_cmd:empty_ack_to_mqtt(Ctx), - send_to_mqtt(Ctx, <<"ack">>, MqttPayload, Validator, Session2). + send_to_mqtt(Ctx, <<"ack">>, MqttPayload, WithContext, Session2). %%-------------------------------------------------------------------- %% Ack Failure(Timeout/Reset) %%-------------------------------------------------------------------- -handle_ack_failure({Ctx, _}, Validator, Session) -> - handle_ack_failure(Ctx, <<"coap_timeout">>, Validator, Session). +handle_ack_failure({Ctx, _}, WithContext, Session) -> + handle_ack_failure(Ctx, <<"coap_timeout">>, WithContext, Session). -handle_ack_reset({Ctx, _}, Validator, Session) -> - handle_ack_failure(Ctx, <<"coap_reset">>, Validator, Session). +handle_ack_reset({Ctx, _}, WithContext, Session) -> + handle_ack_failure(Ctx, <<"coap_reset">>, WithContext, Session). -handle_ack_failure(Ctx, MsgType, Validator, Session) -> +handle_ack_failure(Ctx, MsgType, WithContext, Session) -> Session2 = may_send_dl_msg(coap_timeout, Ctx, Session), MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, MsgType), - send_to_mqtt(Ctx, MsgType, MqttPayload, Validator, Session2). + send_to_mqtt(Ctx, MsgType, MqttPayload, WithContext, Session2). %%-------------------------------------------------------------------- %% Send To CoAP %%-------------------------------------------------------------------- -may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers, - reg_info = RegInfo, - wait_ack = WaitAck} = Session) -> - Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), - case is_cache_mode(Lwm2mMode, RegInfo, Session) of +may_send_dl_msg(coap_timeout, Ctx, #session{wait_ack = WaitAck} = Session) -> + case is_cache_mode(Session) of false -> send_dl_msg(Ctx, Session); true -> case WaitAck of @@ -545,14 +495,11 @@ may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers, end end. -is_cache_mode(Lwm2mMode, RegInfo, #session{last_active_at = LastActiveAt}) -> - case Lwm2mMode =:= psm orelse is_psm(RegInfo) orelse is_qmode(RegInfo) of - true -> - QModeTimeWind = emqx:get_config([gateway, lwm2m, qmode_time_window]), - Now = ?NOW, - (Now - LastActiveAt) >= QModeTimeWind; - false -> false - end. +is_cache_mode(#session{is_cache_mode = IsCacheMode, + last_active_at = LastActiveAt}) -> + IsCacheMode andalso + ((?NOW - LastActiveAt) >= + emqx:get_config([gateway, lwm2m, qmode_time_window])). is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>; APN =:= <<"psmA.eDRX0.ctnb">>; @@ -611,54 +558,27 @@ send_msg_not_waiting_ack(Ctx, Req, Session) -> %%-------------------------------------------------------------------- %% Send To MQTT %%-------------------------------------------------------------------- -send_to_mqtt(Ref, EventType, Payload, Validator, Session = #session{headers = Headers}) -> +send_to_mqtt(Ref, EventType, Payload, WithContext, Session) -> #{topic := Topic, qos := Qos} = uplink_topic(EventType), - NHeaders = extract_ext_flags(Headers), Mheaders = maps:get(mheaders, Ref, #{}), - NHeaders1 = maps:merge(NHeaders, Mheaders), - proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session). send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos}, - Validator, #session{headers = Headers} = Session) -> + WithContext, Session) -> Mheaders = maps:get(mheaders, Ctx, #{}), - NHeaders = extract_ext_flags(Headers), - NHeaders1 = maps:merge(NHeaders, Mheaders), - proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session). -proto_publish(Topic, Payload, Qos, Headers, Validator, +proto_publish(Topic, Payload, Qos, Headers, WithContext, #session{endpoint_name = Epn} = Session) -> - MountedTopic = mount(Topic, mountpoint(Epn)), - _ = case Validator(publish, MountedTopic) of - allow -> - Msg = emqx_message:make(Epn, Qos, MountedTopic, - emqx_json:encode(Payload), #{}, Headers), - emqx:publish(Msg); - _ -> - ?LOG(error, "topic:~p not allow to publish ", [MountedTopic]) - end, + MountedTopic = mount(Topic, Session), + Msg = emqx_message:make(Epn, Qos, MountedTopic, + emqx_json:encode(Payload), #{}, Headers), + WithContext(publish, [MountedTopic, Msg]), Session. -mountpoint(Epn) -> - Prefix = emqx:get_config([gateway, lwm2m, mountpoint]), - <>. - -mount(Topic, MountPoint) when is_binary(Topic), is_binary(MountPoint) -> +mount(Topic, #session{mountpoint = MountPoint}) when is_binary(Topic) -> <>. -extract_ext_flags(Headers) -> - Header0 = #{is_tr => maps:get(is_tr, Headers, true)}, - check(Header0, Headers, [sota_type, appId, nbgwFlag]). - -check(Params, _Headers, []) -> Params; -check(Params, Headers, [Key | Rest]) -> - case maps:get(Key, Headers, null) of - V when V == undefined; V == null -> - check(Params, Headers, Rest); - Value -> - Params1 = Params#{Key => Value}, - check(Params1, Headers, Rest) - end. - downlink_topic() -> emqx:get_config([gateway, lwm2m, translators, command]). @@ -678,29 +598,30 @@ uplink_topic(_) -> %% Deliver %%-------------------------------------------------------------------- -deliver(Delivers, #session{headers = Headers, reg_info = RegInfo} = Session) -> - Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), - IsCacheMode = is_cache_mode(Lwm2mMode, RegInfo, Session), +deliver(Delivers, WithContext, #session{reg_info = RegInfo} = Session) -> + IsCacheMode = is_cache_mode(Session), AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), lists:foldl(fun({deliver, _, MQTT}, Acc) -> deliver_to_coap(AlternatePath, - MQTT#message.payload, MQTT, IsCacheMode, Acc) + MQTT#message.payload, MQTT, IsCacheMode, WithContext, Acc) end, Session, Delivers). -deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, Session) when is_binary(JsonData)-> +deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, WithContext, Session) when is_binary(JsonData)-> try TermData = emqx_json:decode(JsonData, [return_maps]), - deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) + deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) catch ExClass:Error:ST -> ?LOG(error, "deliver_to_coap - Invalid JSON: ~0p, Exception: ~0p, stacktrace: ~0p", [JsonData, {ExClass, Error}, ST]), + WithContext(metrics, 'delivery.dropped'), Session end; -deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) when is_map(TermData) -> +deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) when is_map(TermData) -> + WithContext(metrics, 'messages.delivered'), {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), ExpiryTime = get_expiry_time(MQTT), maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session). diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index e355e05cf..27b8f82a1 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -35,14 +35,14 @@ gateway.lwm2m { lifetime_max = 86400s qmode_time_windonw = 22 auto_observe = false - mountpoint = \"lwm2m\" + mountpoint = \"lwm2m/%u\" update_msg_publish_condition = contains_object_list translators { - command = {topic = \"dn/#\", qos = 0} - response = {topic = \"up/resp\", qos = 0} - notify = {topic = \"up/notify\", qos = 0} - register = {topic = \"up/resp\", qos = 0} - update = {topic = \"up/resp\", qos = 0} + command = {topic = \"/dn/#\", qos = 0} + response = {topic = \"/up/resp\", qos = 0} + notify = {topic = \"/up/notify\", qos = 0} + register = {topic = \"/up/resp\", qos = 0} + update = {topic = \"/up/resp\", qos = 0} } listeners.udp.default { bind = 5783 @@ -66,7 +66,7 @@ all() -> , {group, test_grp_6_observe} %% {group, test_grp_8_object_19} - %% {group, test_grp_9_psm_queue_mode} + , {group, test_grp_9_psm_queue_mode} ]. suite() -> [{timetrap, {seconds, 90}}]. @@ -1750,7 +1750,7 @@ server_cache_mode(Config, RegOption) -> verify_read_response_1(0, UdpSock), %% server inters into PSM mode - timer:sleep(2), + timer:sleep(2500), %% verify server caches downlink commands send_read_command_1(1, UdpSock), @@ -1797,6 +1797,7 @@ verify_read_response_1(CmdId, UdpSock) -> ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"read">>, <<"data">> => #{ + <<"reqPath">> => <<"/3/0/0">>, <<"code">> => <<"2.05">>, <<"codeMsg">> => <<"content">>, <<"content">> => [#{ From 627de1d58c2adbb3bb906a7dfb6e41daf723bf28 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 7 Sep 2021 10:29:45 +0800 Subject: [PATCH 269/306] fix(test): fix test case --- apps/emqx/test/emqx_authentication_SUITE.erl | 238 ++++++++++++++++++ .../src/simple_authn/emqx_authn_http.erl | 4 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 5 + apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 8 +- .../test/emqx_authn_mnesia_SUITE.erl | 8 +- .../src/emqx_connector_schema_lib.erl | 4 +- apps/emqx_gateway/src/emqx_gateway.app.src | 2 +- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 7 +- .../test/emqx_gateway_registry_SUITE.erl | 4 +- apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 4 +- .../test/emqx_sn_protocol_SUITE.erl | 4 +- apps/emqx_gateway/test/emqx_stomp_SUITE.erl | 4 +- 12 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 apps/emqx/test/emqx_authentication_SUITE.erl diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl new file mode 100644 index 000000000..a940adc88 --- /dev/null +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -0,0 +1,238 @@ +%%-------------------------------------------------------------------- +%% 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). + +-behaviour(hocon_schema). +-behaviour(emqx_authentication). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-export([ fields/1 ]). + +-export([ refs/0 + , create/1 + , update/2 + , authenticate/2 + , destroy/1 + ]). + +-define(AUTHN, emqx_authentication). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +fields(type1) -> + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['built-in-database']}} + , {enable, fun enable/1} + ]; + +fields(type2) -> + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['mysql']}} + , {enable, fun enable/1} + ]. + +enable(type) -> boolean(); +enable(default) -> true; +enable(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% Callbacks +%%------------------------------------------------------------------------------ + +refs() -> + [ hoconsc:ref(?MODULE, type1) + , hoconsc:ref(?MODULE, type2) + ]. + +create(_Config) -> + {ok, #{mark => 1}}. + +update(_Config, _State) -> + {ok, #{mark => 2}}. + +authenticate(#{username := <<"good">>}, _State) -> + {ok, #{superuser => true}}; +authenticate(#{username := _}, _State) -> + {error, bad_username_or_password}. + +destroy(_State) -> + ok. + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([]), + ok. + +t_chain(_) -> + % CRUD of authentication chain + ChainName = <<"test">>, + ?assertMatch({ok, []}, ?AUTHN:list_chains()), + ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:create_chain(ChainName)), + ?assertEqual({error, {already_exists, {chain, ChainName}}}, ?AUTHN:create_chain(ChainName)), + ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:lookup_chain(ChainName)), + ?assertMatch({ok, [#{name := ChainName}]}, ?AUTHN:list_chains()), + ?assertEqual(ok, ?AUTHN:delete_chain(ChainName)), + ?assertMatch({error, {not_found, {chain, ChainName}}}, ?AUTHN:lookup_chain(ChainName)), + ok. + +t_authenticator(_) -> + ChainName = <<"test">>, + AuthenticatorConfig1 = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + + % Create an authenticator when the authentication chain does not exist + ?assertEqual({error, {not_found, {chain, ChainName}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?AUTHN:create_chain(ChainName), + % Create an authenticator when the provider does not exist + ?assertEqual({error, no_available_provider}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + + AuthNType1 = {'password-based', 'built-in-database'}, + ?AUTHN:add_provider(AuthNType1, ?MODULE), + ID1 = <<"password-based:built-in-database">>, + + % CRUD of authencaticator + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID1}}, ?AUTHN:lookup_authenticator(ChainName, ID1)), + ?assertMatch({ok, [#{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual({error, {already_exists, {authenticator, ID1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)), + ?assertEqual(ok, ?AUTHN:delete_authenticator(ChainName, ID1)), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)), + ?assertMatch({ok, []}, ?AUTHN:list_authenticators(ChainName)), + + % Multiple authenticators exist at the same time + AuthNType2 = {'password-based', mysql}, + ?AUTHN:add_provider(AuthNType2, ?MODULE), + ID2 = <<"password-based:mysql">>, + AuthenticatorConfig2 = #{mechanism => 'password-based', + backend => mysql, + enable => true}, + ?assertMatch({ok, #{id := ID1}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID2}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig2)), + + % Move authenticator + ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, top)), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, bottom)), + ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, {before, ID1})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + + ?AUTHN:delete_chain(ChainName), + ?AUTHN:remove_provider(AuthNType1), + ?AUTHN:remove_provider(AuthNType2), + ok. + +t_authenticate(_) -> + ListenerID = <<"tcp:default">>, + ClientInfo = #{zone => default, + listener => ListenerID, + protocol => mqtt, + username => <<"good">>, + password => <<"any">>}, + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), + + AuthNType = {'password-based', 'built-in-database'}, + ?AUTHN:add_provider(AuthNType, ?MODULE), + + AuthenticatorConfig = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + ?AUTHN:create_chain(ListenerID), + ?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)), + ?assertEqual({ok, #{superuser => true}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})), + + ?AUTHN:delete_chain(ListenerID), + ?AUTHN:remove_provider(AuthNType), + ok. + +t_update_config(_) -> + emqx_config_handler:add_handler([authentication], emqx_authentication), + + AuthNType1 = {'password-based', 'built-in-database'}, + AuthNType2 = {'password-based', mysql}, + ?AUTHN:add_provider(AuthNType1, ?MODULE), + ?AUTHN:add_provider(AuthNType2, ?MODULE), + + Global = <<"mqtt:global">>, + AuthenticatorConfig1 = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + AuthenticatorConfig2 = #{mechanism => 'password-based', + backend => mysql, + enable => true}, + ID1 = <<"password-based:built-in-database">>, + ID2 = <<"password-based:mysql">>, + + ?assertMatch({ok, []}, ?AUTHN:list_chains()), + ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig1})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})), + ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)), + + ?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, #{}})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, <<"top">>})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(Global)), + + ?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ListenerID = <<"tcp:default">>, + ConfKeyPath = [listeners, tcp, default, authentication], + ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})), + ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, #{}})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, <<"top">>})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ListenerID)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {delete_authenticator, ListenerID, ID1})), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?AUTHN:delete_chain(Global), + ?AUTHN:remove_provider(AuthNType1), + ?AUTHN:remove_provider(AuthNType2), + ok. + +update_config(Path, ConfigRequest) -> + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). 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 1bec0d903..19417218d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -251,8 +251,8 @@ generate_request(Credential, #{method := Method, post -> NPath = append_query(Path, BaseQuery), ContentType = proplists:get_value(<<"content-type">>, Headers), - Body = serialize_body(ContentType, Body), - {NPath, Headers, Body} + NBody = serialize_body(ContentType, Body), + {NPath, Headers, NBody} end. replace_placeholders(KVs, Credential) -> diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 31bac76a3..74ec397cc 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -15,3 +15,8 @@ %%-------------------------------------------------------------------- -module(emqx_authn_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +all() -> emqx_ct:all(?MODULE). \ No newline at end of file diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 5e06211a7..9d8b1d9fc 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -16,8 +16,8 @@ -module(emqx_authn_jwt_SUITE). -% -compile(export_all). -% -compile(nowarn_export_all). +-compile(export_all). +-compile(nowarn_export_all). % -include_lib("common_test/include/ct.hrl"). % -include_lib("eunit/include/eunit.hrl"). @@ -26,8 +26,8 @@ % -define(AUTH, emqx_authn). -% all() -> -% emqx_ct:all(?MODULE). +all() -> + emqx_ct:all(?MODULE). % init_per_suite(Config) -> % emqx_ct_helpers:start_apps([emqx_authn]), diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index acfe71809..4bc6961dd 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -16,8 +16,8 @@ -module(emqx_authn_mnesia_SUITE). -% -compile(export_all). -% -compile(nowarn_export_all). +-compile(export_all). +-compile(nowarn_export_all). % -include_lib("common_test/include/ct.hrl"). % -include_lib("eunit/include/eunit.hrl"). @@ -26,8 +26,8 @@ % -define(AUTH, emqx_authn). -% all() -> -% emqx_ct:all(?MODULE). +all() -> + emqx_ct:all(?MODULE). % init_per_suite(Config) -> % emqx_ct_helpers:start_apps([emqx_authn]), diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 5f9472cca..a6d33ffca 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -142,7 +142,9 @@ to_ip_port(Str) -> _ -> {error, Str} end. -ip_port_to_string({Ip, Port}) -> +ip_port_to_string({Ip, Port}) when is_list(Ip) -> + iolist_to_binary([Ip, ":", integer_to_list(Port)]); +ip_port_to_string({Ip, Port}) when is_tuple(Ip) -> iolist_to_binary([inet:ntoa(Ip), ":", integer_to_list(Port)]). to_servers(Str) -> diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index e25b767cc..2fc329711 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {emqx_gateway_app, []}}, - {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx, emqx_authn]}, + {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx]}, {env, []}, {modules, []}, {licenses, ["Apache 2.0"]}, diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 4902aacf5..4ab91da5d 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -55,20 +55,19 @@ metrics() -> init_per_group(GrpName, Cfg) -> put(grpname, GrpName), Svrs = emqx_exproto_echo_svr:start(), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway], fun set_special_cfg/1), + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_cfg/1), emqx_logger:set_log_level(debug), [{servers, Svrs}, {listener_type, GrpName} | Cfg]. end_per_group(_, Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]), + emqx_ct_helpers:stop_apps([emqx_gateway]), emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)). set_special_cfg(emqx_gateway) -> LisType = get(grpname), emqx_config:put( [gateway, exproto], - #{authentication => #{enable => false}, - server => #{bind => 9100}, + #{server => #{bind => 9100}, handler => #{address => "http://127.0.0.1:9001"}, listeners => listener_confs(LisType) }); diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl index da03b17c5..56776957f 100644 --- a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -35,11 +35,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Cfg) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_gateway]), Cfg. end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:stop_apps([emqx_gateway]), ok. %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index e355e05cf..5df13f8d6 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -148,12 +148,12 @@ groups() -> ]. init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), + emqx_ct_helpers:start_apps([]), Config. end_per_suite(Config) -> timer:sleep(300), - emqx_ct_helpers:stop_apps([emqx_authn]), + emqx_ct_helpers:stop_apps([]), Config. init_per_testcase(_AllTestCase, Config) -> diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 2fbd031ff..e4b3d0095 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -83,11 +83,11 @@ all() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_gateway]), Config. end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]). + emqx_ct_helpers:stop_apps([emqx_gateway]). %%-------------------------------------------------------------------- %% Test cases diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index 75f6dadc3..9c3f1090f 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -43,11 +43,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Cfg) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_gateway]), Cfg. end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]), + emqx_ct_helpers:stop_apps([emqx_gateway]), ok. %%-------------------------------------------------------------------- From 1699a8dc63046cf56b9775636c6efb10fc53f3e5 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 6 Sep 2021 09:39:27 +0800 Subject: [PATCH 270/306] chore(authz): rename authorization_rules.conf to acl.conf --- apps/emqx_authz/etc/{authorization_rules.conf => acl.conf} | 0 apps/emqx_authz/etc/emqx_authz.conf | 2 +- apps/emqx_authz/src/emqx_authz_api_sources.erl | 4 ++-- apps/emqx_authz/test/emqx_authz_SUITE.erl | 2 +- apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl | 2 +- rebar.config.erl | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename apps/emqx_authz/etc/{authorization_rules.conf => acl.conf} (100%) diff --git a/apps/emqx_authz/etc/authorization_rules.conf b/apps/emqx_authz/etc/acl.conf similarity index 100% rename from apps/emqx_authz/etc/authorization_rules.conf rename to apps/emqx_authz/etc/acl.conf diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 99b14f5fe..ed4ad573c 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -57,7 +57,7 @@ authorization { # }, { type: file - path: "{{ platform_etc_dir }}/authorization_rules.conf" + path: "{{ platform_etc_dir }}/acl.conf" } ] } diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index b2d33eca5..4be631560 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -330,7 +330,7 @@ sources(get, _) -> end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> - {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), erlang:list_to_bitstring([<> || Rule <- Rules]) ), case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of @@ -350,7 +350,7 @@ sources(put, #{body := Body}) when is_list(Body) -> NBody = [ begin case Source of #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> - {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), erlang:list_to_bitstring([<> || Rule <- Rules]) ), #{type => file, enable => Enable, path => Filename}; diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 6e6597486..f2cb01d05 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -111,7 +111,7 @@ init_per_testcase(_, Config) -> }). -define(SOURCE6, #{<<"type">> => <<"file">>, <<"enable">> => true, - <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/authorization_rules.conf") + <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/acl.conf") }). diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 4dc21647a..8c37189c9 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -190,7 +190,7 @@ t_api(_) -> , #{<<"type">> := <<"redis">>} , #{<<"type">> := <<"file">>} ], Sources), - ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), {ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []), diff --git a/rebar.config.erl b/rebar.config.erl index aac6e0085..084f068e4 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -340,7 +340,7 @@ relx_overlay(ReleaseType) -> , {copy, "bin/emqx_ctl", "bin/emqx_ctl-{{release_version}}"} %% for relup , {copy, "bin/install_upgrade.escript", "bin/install_upgrade.escript-{{release_version}}"} %% for relup , {copy, "apps/emqx_gateway/src/lwm2m/lwm2m_xml", "etc/lwm2m_xml"} - , {copy, "apps/emqx_authz/etc/authorization_rules.conf", "etc/authorization_rules.conf"} + , {copy, "apps/emqx_authz/etc/acl.conf", "etc/acl.conf"} , {template, "bin/emqx.cmd", "bin/emqx.cmd"} , {template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"} , {copy, "bin/nodetool", "bin/nodetool"} From c68edb390581c78372f03f0c99b662ed115ed5be Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 7 Sep 2021 17:29:05 +0800 Subject: [PATCH 271/306] chore(authn): update apis of user --- apps/emqx_authn/src/emqx_authn_api.erl | 877 ++++++++++++++++--------- 1 file changed, 569 insertions(+), 308 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 3303f88ef..afac57f99 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -28,8 +28,11 @@ , move/2 , move2/2 , import_users/2 + , import_users2/2 , users/2 , users2/2 + , users3/2 + , users4/2 ]). -define(EXAMPLE_1, #{mechanism => <<"password-based">>, @@ -153,8 +156,11 @@ api_spec() -> , authentication_api4() , move_api2() , import_users_api() + , import_users_api2() , users_api() , users2_api() + , users3_api() + , users4_api() ], definitions()}. authentication_api() -> @@ -166,7 +172,7 @@ authentication_api() -> authentication_api2() -> Metadata = #{ - get => list_authenticator_api_spec(), + get => find_authenticator_api_spec(), put => update_authenticator_api_spec(), delete => delete_authenticator_api_spec() }, @@ -181,7 +187,7 @@ authentication_api3() -> authentication_api4() -> Metadata = #{ - get => list_authenticator_api_spec2(), + get => find_authenticator_api_spec2(), put => update_authenticator_api_spec2(), delete => delete_authenticator_api_spec2() }, @@ -199,6 +205,48 @@ move_api2() -> }, {"/listeners/:listener_id/authentication/:id/move", Metadata, move2}. +import_users_api() -> + Metadata = #{ + post => import_users_api_spec() + }, + {"/authentication/:id/import_users", Metadata, import_users}. + +import_users_api2() -> + Metadata = #{ + post => import_users_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/import_users", Metadata, import_users2}. + +users_api() -> + Metadata = #{ + post => create_user_api_spec(), + get => list_users_api_spec() + }, + {"/authentication/:id/users", Metadata, users}. + +users2_api() -> + Metadata = #{ + put => update_user_api_spec(), + get => find_user_api_spec(), + delete => delete_user_api_spec() + }, + {"/authentication/:id/users/:user_id", Metadata, users2}. + +users3_api() -> + Metadata = #{ + post => create_user_api_spec2(), + get => list_users_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/users", Metadata, users3}. + +users4_api() -> + Metadata = #{ + put => update_user_api_spec2(), + get => find_user_api_spec2(), + delete => delete_user_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/users/:user_id", Metadata, users4}. + create_authenticator_api_spec() -> #{ description => "Create a authenticator for global authentication", @@ -321,14 +369,14 @@ list_authenticators_api_spec2() -> ] }. -list_authenticator_api_spec() -> +find_authenticator_api_spec() -> #{ description => "Get authenticator by id", parameters => [ #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -370,14 +418,14 @@ list_authenticator_api_spec() -> } }. -list_authenticator_api_spec2() -> - Spec = list_authenticator_api_spec(), +find_authenticator_api_spec2() -> + Spec = find_authenticator_api_spec(), Spec#{ parameters => [ #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -386,7 +434,7 @@ list_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -402,7 +450,7 @@ update_authenticator_api_spec() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -482,7 +530,7 @@ update_authenticator_api_spec2() -> #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -491,7 +539,7 @@ update_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -507,7 +555,7 @@ delete_authenticator_api_spec() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -529,7 +577,7 @@ delete_authenticator_api_spec2() -> #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -538,7 +586,7 @@ delete_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -554,7 +602,7 @@ move_authenticator_api_spec() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -609,7 +657,7 @@ move_authenticator_api_spec2() -> #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -618,7 +666,7 @@ move_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -627,90 +675,176 @@ move_authenticator_api_spec2() -> ] }. -import_users_api() -> - Metadata = #{ - post => #{ - description => "Import users from json/csv file", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", +import_users_api_spec() -> + #{ + description => "Import users from json/csv file", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [filename], - properties => #{ - filename => #{ - type => string - } + type => object, + required => [filename], + properties => #{ + filename => #{ + type => string } } } } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/:id/import_users", Metadata, import_users}. + }. -users_api() -> - Metadata = #{ - post => #{ - description => "Add user", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", +import_users_api_spec2() -> + Spec = import_users_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +create_user_api_spec() -> + #{ + description => "Add user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ schema => #{ - type => string - }, - required => true + type => object, + required => [user_id, password], + properties => #{ + user_id => #{ + type => string + }, + password => #{ + type => string + }, + superuser => #{ + type => boolean, + default => false + } + } + } } - ], - requestBody => #{ + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, content => #{ 'application/json' => #{ schema => #{ type => object, - required => [user_id, password], properties => #{ user_id => #{ type => string }, - password => #{ - type => string - }, superuser => #{ - type => boolean, - default => false + type => boolean } } } } } }, - responses => #{ - <<"201">> => #{ - description => <<"Created">>, - content => #{ - 'application/json' => #{ - schema => #{ + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +create_user_api_spec2() -> + Spec = create_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +list_users_api_spec() -> + #{ + description => "List users", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ type => object, properties => #{ user_id => #{ @@ -723,194 +857,286 @@ users_api() -> } } } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - get => #{ - description => "List users", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - }, - superuser => #{ - type => boolean - } - } - } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +list_users_api_spec2() -> + Spec = list_users_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +update_user_api_spec() -> + #{ + description => "Update user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + properties => #{ + password => #{ + type => string + }, + superuser => #{ + type => boolean } } } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } - }, - {"/authentication/:id/users", Metadata, users}. - -users2_api() -> - Metadata = #{ - patch => #{ - description => "Update user", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true } - ], - requestBody => #{ + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, content => #{ 'application/json' => #{ schema => #{ - type => object, - properties => #{ - password => #{ - type => string - }, - superuser => #{ - type => boolean + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + superuser => #{ + type => boolean + } } } } } } }, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - }, - superuser => #{ - type => boolean - } - } - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - get => #{ - description => "Get user info", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - }, - superuser => #{ - type => boolean - } - } - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - delete => #{ - description => "Delete user", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/:id/users/:user_id", Metadata, users2}. + }. + +update_user_api_spec2() -> + Spec = update_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +find_user_api_spec() -> + #{ + description => "Get user info", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + superuser => #{ + type => boolean + } + } + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +find_user_api_spec2() -> + Spec = find_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +delete_user_api_spec() -> + #{ + description => "Delete user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +delete_user_api_spec2() -> + Spec = delete_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + definitions() -> AuthenticatorConfigDef = #{ @@ -1536,71 +1762,54 @@ move2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, b move2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> serialize_error({missing_parameter, position}). -import_users(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> - case Body of - #{<<"filename">> := Filename} -> - case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of - ok -> {204}; - {error, Reason} -> serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, filename}) - end. +import_users(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) -> + case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; +import_users(post, #{bindings := #{id := _}, body := _}) -> + serialize_error({missing_parameter, filename}). + +import_users2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) -> + case ?AUTHN:import_users(ListenerID, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; +import_users2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> + serialize_error({missing_parameter, filename}). users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> - case UserInfo of - #{ <<"user_id">> := UserID, <<"password">> := Password} -> - Superuser = maps:get(<<"superuser">>, UserInfo, false), - case ?AUTHN:add_user(?GLOBAL, AuthenticatorID, #{ user_id => UserID - , password => Password - , superuser => Superuser}) of - {ok, User} -> - {201, User}; - {error, Reason} -> - serialize_error(Reason) - end; - #{<<"user_id">> := _} -> - serialize_error({missing_parameter, password}); - _ -> - serialize_error({missing_parameter, user_id}) - end; + add_user(?GLOBAL, AuthenticatorID, UserInfo); users(get, #{bindings := #{id := AuthenticatorID}}) -> - case ?AUTHN:list_users(?GLOBAL, AuthenticatorID) of - {ok, Users} -> - {200, Users}; - {error, Reason} -> - serialize_error(Reason) - end. + list_users(?GLOBAL, AuthenticatorID). -users2(patch, #{bindings := #{id := AuthenticatorID, - user_id := UserID}, - body := UserInfo}) -> - NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo), - case NUserInfo =:= #{} of - true -> - serialize_error({missing_parameter, password}); - false -> - case ?AUTHN:update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end - end; +users2(put, #{bindings := #{id := AuthenticatorID, + user_id := UserID}, body := UserInfo}) -> + update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo); users2(get, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case ?AUTHN:lookup_user(?GLOBAL, AuthenticatorID, UserID) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end; + find_user(?GLOBAL, AuthenticatorID, UserID); users2(delete, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case ?AUTHN:delete_user(?GLOBAL, AuthenticatorID, UserID) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) - end. + delete_user(?GLOBAL, AuthenticatorID, UserID). + +users3(post, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID}, body := UserInfo}) -> + add_user(ListenerID, AuthenticatorID, UserInfo); +users3(get, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID}}) -> + list_users(ListenerID, AuthenticatorID). + +users4(put, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}, body := UserInfo}) -> + update_user(ListenerID, AuthenticatorID, UserID, UserInfo); +users4(get, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}}) -> + find_user(ListenerID, AuthenticatorID, UserID); +users4(delete, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}}) -> + delete_user(ListenerID, AuthenticatorID, UserID). %%------------------------------------------------------------------------------ %% Internal functions @@ -1667,6 +1876,58 @@ move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> serialize_error(Reason) end. +add_user(ChainName, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> + Superuser = maps:get(<<"superuser">>, UserInfo, false), + case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID + , password => Password + , superuser => Superuser}) of + {ok, User} -> + {201, User}; + {error, Reason} -> + serialize_error(Reason) + end; +add_user(_, _, #{<<"user_id">> := _}) -> + serialize_error({missing_parameter, password}); +add_user(_, _, _) -> + serialize_error({missing_parameter, user_id}). + +update_user(ChainName, AuthenticatorID, UserID, UserInfo) -> + case maps:with([<<"password">>, <<"superuser">>], UserInfo) =:= #{} of + true -> + serialize_error({missing_parameter, password}); + false -> + case ?AUTHN:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end + end. + +find_user(ChainName, AuthenticatorID, UserID) -> + case ?AUTHN:lookup_user(ChainName, AuthenticatorID, UserID) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end. + +delete_user(ChainName, AuthenticatorID, UserID) -> + case ?AUTHN:delete_user(ChainName, AuthenticatorID, UserID) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end. + +list_users(ChainName, AuthenticatorID) -> + case ?AUTHN:list_users(ChainName, AuthenticatorID) of + {ok, Users} -> + {200, Users}; + {error, Reason} -> + serialize_error(Reason) + end. + update_config(Path, ConfigRequest) -> emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). From 8b2488e099f9794e2fb913649bee084b27fac08d Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 7 Sep 2021 18:31:40 +0800 Subject: [PATCH 272/306] fix: routes api add params (#5675) * fix: routes api add topic query params * fix: routes api add node query params --- .../src/emqx_mgmt_api_routes.erl | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 19f42427e..870f66892 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -26,8 +26,12 @@ -export([ routes/2 , route/2]). +-export([query/4]). + -define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND'). +-define(ROUTES_QS_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]). + -import(emqx_mgmt_util, [ object_schema/2 , object_array_schema/2 , error_schema/2 @@ -48,7 +52,7 @@ routes_api() -> Metadata = #{ get => #{ description => <<"EMQ X routes">>, - parameters => page_params(), + parameters => [topic_param(query) , node_param()] ++ page_params(), responses => #{ <<"200">> => object_array_schema(properties(), <<"List route info">>) } @@ -60,13 +64,7 @@ route_api() -> Metadata = #{ get => #{ description => <<"EMQ X routes">>, - parameters => [#{ - name => topic, - in => path, - required => true, - description => <<"Topic string, url encoding">>, - schema => #{type => string} - }], + parameters => [topic_param(path)], responses => #{ <<"200">> => object_schema(properties(), <<"Route info">>), @@ -80,15 +78,15 @@ route_api() -> %%%============================================================================================== %% parameters trans routes(get, #{query_string := Qs}) -> - list(Qs). + list(generate_topic(Qs)). route(get, #{bindings := Bindings}) -> - lookup(Bindings). + lookup(generate_topic(Bindings)). %%%============================================================================================== %% api apply list(Params) -> - Response = emqx_mgmt_api:paginate(emqx_route, Params, fun format/1), + Response = emqx_mgmt_api:node_query(node(), Params, emqx_route, ?ROUTES_QS_SCHEMA, {?MODULE, query}), {200, Response}. lookup(#{topic := Topic}) -> @@ -101,7 +99,41 @@ lookup(#{topic := Topic}) -> %%%============================================================================================== %% internal +generate_topic(Params = #{<<"topic">> := Topic}) -> + Params#{<<"topic">> => uri_string:percent_decode(Topic)}; +generate_topic(Params = #{topic := Topic}) -> + Params#{topic => uri_string:percent_decode(Topic)}; +generate_topic(Params) -> Params. + +query(Tab, {Qs, _}, Start, Limit) -> + Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, fun format/1). + +qs2ms([], Res) -> Res; +qs2ms([{topic,'=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) -> + qs2ms(Qs, [{{route, T, N}, [], ['$_']}]); +qs2ms([{node,'=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) -> + qs2ms(Qs, [{{route, T, N}, [], ['$_']}]). + format(#route{topic = Topic, dest = {_, Node}}) -> #{topic => Topic, node => Node}; format(#route{topic = Topic, dest = Node}) -> #{topic => Topic, node => Node}. + +topic_param(In) -> + #{ + name => topic, + in => In, + required => In == path, + description => <<"Topic string, url encoding">>, + schema => #{type => string} + }. + +node_param()-> + #{ + name => node, + in => query, + required => false, + description => <<"Node">>, + schema => #{type => string} + }. From 89f48f89ebe18e24ddfebbc7cf375b9c32151f07 Mon Sep 17 00:00:00 2001 From: lafirest Date: Fri, 3 Sep 2021 13:55:05 +0800 Subject: [PATCH 273/306] feat(emqx_coap): add emqx_coap_api 1. add a request api for emqx_coap 2. fix some emqx_coap logic error --- apps/emqx_gateway/etc/emqx_gateway.conf | 1 - apps/emqx_gateway/src/coap/emqx_coap_api.erl | 145 ++++++++++++ .../src/coap/emqx_coap_channel.erl | 69 ++++-- .../src/coap/emqx_coap_session.erl | 10 +- apps/emqx_gateway/src/coap/emqx_coap_tm.erl | 12 +- .../src/coap/emqx_coap_transport.erl | 11 +- .../coap/handler/emqx_coap_pubsub_handler.erl | 28 ++- apps/emqx_gateway/src/emqx_gateway_app.erl | 2 +- .../emqx_gateway/src/emqx_gateway_metrics.erl | 2 +- .../emqx_gateway/test/emqx_coap_api_SUITE.erl | 224 ++++++++++++++++++ .../test/emqx_mgmt_api_test_util.erl | 27 ++- 11 files changed, 478 insertions(+), 53 deletions(-) create mode 100644 apps/emqx_gateway/src/coap/emqx_coap_api.erl create mode 100644 apps/emqx_gateway/test/emqx_coap_api_SUITE.erl diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 6fdadcc3b..5212d319f 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -59,7 +59,6 @@ gateway.coap { ## When publishing or subscribing, prefix all topics with a mountpoint string. mountpoint = "" - heartbeat = 30s notify_type = qos ## if true, you need to establish a connection before use diff --git a/apps/emqx_gateway/src/coap/emqx_coap_api.erl b/apps/emqx_gateway/src/coap/emqx_coap_api.erl new file mode 100644 index 000000000..428e99ac5 --- /dev/null +++ b/apps/emqx_gateway/src/coap/emqx_coap_api.erl @@ -0,0 +1,145 @@ +%%-------------------------------------------------------------------- +%% 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_coap_api). + +-behaviour(minirest_api). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). + +%% API +-export([api_spec/0]). + +-export([request/2]). + +-define(PREFIX, "/gateway/coap/:clientid"). +-define(DEF_WAIT_TIME, 10). + +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , error_schema/2 + , properties/1]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +api_spec() -> + {[request_api()], []}. + +request_api() -> + Metadata = #{post => request_method_meta()}, + {?PREFIX ++ "/request", Metadata, request}. + +request(post, #{body := Body, bindings := Bindings}) -> + ClientId = maps:get(clientid, Bindings, undefined), + + Method = maps:get(<<"method">>, Body, <<"get">>), + CT = maps:get(<<"content_type">>, Body, <<"text/plain">>), + Token = maps:get(<<"token">>, Body, <<>>), + Payload = maps:get(<<"payload">>, Body, <<>>), + WaitTime = maps:get(<<"timeout">>, Body, ?DEF_WAIT_TIME), + + Payload2 = parse_payload(CT, Payload), + ReqType = erlang:binary_to_atom(Method), + + Msg = emqx_coap_message:request(con, + ReqType, Payload2, #{content_format => CT}), + + Msg2 = Msg#coap_message{token = Token}, + + case call_client(ClientId, Msg2, timer:seconds(WaitTime)) of + timeout -> + {504}; + not_found -> + {404}; + Response -> + {200, format_to_response(CT, Response)} + end. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +request_parameters() -> + [#{name => clientid, + in => path, + schema => #{type => string}, + required => true}]. + +request_properties() -> + properties([ {token, string, "message token, can be empty"} + , {method, string, "request method type", ["get", "put", "post", "delete"]} + , {timeout, integer, "timespan for response"} + , {content_type, string, "payload type", + [<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]} + , {payload, string, "payload"}]). + +coap_message_properties() -> + properties([ {id, integer, "message id"} + , {token, string, "message token, can be empty"} + , {method, string, "response code"} + , {payload, string, "payload"}]). + +request_method_meta() -> + #{description => <<"lookup matching messages">>, + parameters => request_parameters(), + 'requestBody' => object_schema(request_properties(), + <<"request payload, binary must encode by base64">>), + responses => #{ + <<"200">> => object_schema(coap_message_properties()), + <<"404">> => schema(<<"NotFound">>), + <<"504">> => schema(<<"Timeout">>) + }}. + + +format_to_response(ContentType, #coap_message{id = Id, + token = Token, + method = Method, + payload = Payload}) -> + #{id => Id, + token => Token, + method => format_to_binary(Method), + payload => format_payload(ContentType, Payload)}. + +format_to_binary(Obj) -> + erlang:list_to_binary(io_lib:format("~p", [Obj])). + +format_payload(<<"application/octet-stream">>, Payload) -> + base64:encode(Payload); + +format_payload(_, Payload) -> + Payload. + +parse_payload(<<"application/octet-stream">>, Body) -> + base64:decode(Body); + +parse_payload(_, Body) -> + Body. + +call_client(ClientId, Msg, Timeout) -> + case emqx_gateway_cm_registry:lookup_channels(coap, ClientId) of + [Channel | _] -> + RequestId = emqx_coap_channel:send_request(Channel, Msg), + case gen_server:wait_response(RequestId, Timeout) of + {reply, Reply} -> + Reply; + _ -> + timeout + end; + _ -> + not_found + end. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 11aca8cc8..112efdc44 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -25,7 +25,10 @@ -export([ info/1 , info/2 , stats/1 - , validator/4]). + , validator/4 + , metrics_inc/2 + , run_hooks/3 + , send_request/2]). -export([ init/2 , handle_in/2 @@ -57,7 +60,7 @@ connection_required :: boolean(), - conn_state :: idle | connected, + conn_state :: idle | connected | disconnected, token :: binary() | undefined }). @@ -99,7 +102,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, sockname := {_, SockPort}}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), - Mountpoint = maps:get(mountpoint, Config, undefined), + Mountpoint = maps:get(mountpoint, Config, <<>>), ClientInfo = set_peercert_infos( Peercert, #{ zone => default @@ -128,6 +131,10 @@ init(ConnInfo = #{peername := {PeerHost, _}, validator(Type, Topic, Ctx, ClientInfo) -> emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). +-spec send_request(pid(), emqx_coap_message()) -> any(). +send_request(Channel, Request) -> + gen_server:send_request(Channel, {?FUNCTION_NAME, Request}). + %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- @@ -143,8 +150,9 @@ handle_in(Msg, ChannleT) -> %%-------------------------------------------------------------------- %% Handle Delivers from broker to client %%-------------------------------------------------------------------- -handle_deliver(Delivers, Channel) -> - call_session(deliver, Delivers, Channel). +handle_deliver(Delivers, #channel{session = Session, + ctx = Ctx} = Channel) -> + handle_result(emqx_coap_session:deliver(Delivers, Ctx, Session), Channel). %%-------------------------------------------------------------------- %% Handle timeout @@ -155,7 +163,7 @@ handle_timeout(_, {keepalive, NewVal}, #channel{keepalive = KeepAlive} = Channel Channel2 = ensure_keepalive_timer(fun make_timer/4, Channel), {ok, Channel2#channel{keepalive = NewKeepAlive}}; {error, timeout} -> - {shutdown, timeout, Channel} + {shutdown, timeout, ensure_disconnected(keepalive_timeout, Channel)} end; handle_timeout(_, {transport, Msg}, Channel) -> @@ -170,6 +178,10 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- +handle_call({send_request, Msg}, From, Channel) -> + Result = call_session(handle_out, {{send_request, From}, Msg}, Channel), + erlang:setelement(1, Result, noreply); + handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. @@ -184,6 +196,9 @@ handle_cast(Req, Channel) -> %%-------------------------------------------------------------------- %% Handle Info %%-------------------------------------------------------------------- +handle_info({subscribe, _}, Channel) -> + {ok, Channel}; + handle_info(Info, Channel) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, Channel}. @@ -191,8 +206,10 @@ handle_info(Info, Channel) -> %%-------------------------------------------------------------------- %% Terminate %%-------------------------------------------------------------------- -terminate(_Reason, _Channel) -> - ok. +terminate(Reason, #channel{clientinfo = ClientInfo, + ctx = Ctx, + session = Session}) -> + run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]). %%-------------------------------------------------------------------- %% Internal functions @@ -242,17 +259,17 @@ check_token(true, try_takeover(CState, DesireId, Msg, Channel); _ -> Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), - {ok, {outgoing, Reply}, Msg} + {ok, {outgoing, Reply}, Channel} end; check_token(false, Msg, Channel) -> case emqx_coap_message:get_option(uri_query, Msg) of #{<<"clientid">> := _} -> Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), - {ok, {outgoing, Reply}, Msg}; + {ok, {outgoing, Reply}, Channel}; #{<<"token">> := _} -> Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), - {ok, {outgoing, Reply}, Msg}; + {ok, {outgoing, Reply}, Channel}; _ -> call_session(handle_request, Msg, Channel) end. @@ -322,11 +339,9 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, {error, Reason} end. -fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> +fix_mountpoint(_Packet, #{mountpoint := <<>>} = ClientInfo) -> {ok, ClientInfo}; 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}}. @@ -338,6 +353,7 @@ ensure_connected(Channel = #channel{ctx = Ctx, , proto_ver => <<"1">> }, ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), + _ = run_hooks(Ctx, 'client.connack', [NConnInfo, connection_accepted, []]), Channel#channel{conninfo = NConnInfo}. process_connect(#channel{ctx = Ctx, @@ -374,19 +390,32 @@ run_hooks(Ctx, Name, Args, Acc) -> emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_hooks:run_fold(Name, Args, Acc). +metrics_inc(Name, Ctx) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). + +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}. + %%-------------------------------------------------------------------- %% Call Chain %%-------------------------------------------------------------------- -call_session(Fun, - Msg, - #channel{session = Session} = Channel) -> +call_session(Fun, Msg, #channel{session = Session} = Channel) -> + Result = emqx_coap_session:Fun(Msg, Session), + handle_result(Result, Channel). + +handle_result(Result, Channel) -> iter([ session, fun process_session/4 , proto, fun process_protocol/4 , reply, fun process_reply/4 , out, fun process_out/4 , fun process_nothing/3 ], - emqx_coap_session:Fun(Msg, Session), + Result, Channel). call_handler(request, Msg, Result, @@ -406,6 +435,10 @@ call_handler(request, Msg, Result, maps:merge(Result, HandlerResult), Channel); +call_handler(response, {{send_request, From}, Response}, Result, Channel, Iter) -> + gen_server:reply(From, Response), + iter(Iter, Result, Channel); + call_handler(_, _, Result, Channel, Iter) -> iter(Iter, Result, Channel). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index b7e6c53f4..0fbc47cf8 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -33,7 +33,7 @@ , handle_response/2 , handle_out/2 , set_reply/2 - , deliver/2 + , deliver/3 , timeout/2]). -export_type([session/0]). @@ -66,6 +66,7 @@ ]). -import(emqx_coap_medium, [iter/3]). +-import(emqx_coap_channel, [metrics_inc/2]). %%%------------------------------------------------------------------- %%% API @@ -147,13 +148,16 @@ set_reply(Msg, #session{transport_manager = TM} = Session) -> TM2 = emqx_coap_tm:set_reply(Msg, TM), Session#session{transport_manager = TM2}. -deliver(Delivers, #session{observe_manager = OM, - transport_manager = TM} = Session) -> +deliver(Delivers, Ctx, #session{observe_manager = OM, + transport_manager = TM} = Session) -> Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) -> case emqx_coap_observe_res:res_changed(Topic, OMAcc) of undefined -> + metrics_inc('delivery.dropped', Ctx), + metrics_inc('delivery.dropped.no_subid', Ctx), Acc; {Token, SeqId, OM2} -> + metrics_inc('messages.delivered', Ctx), Msg = mqtt_to_coap(Message, Token, SeqId), #{out := Out, tm := TM2} = emqx_coap_tm:handle_out(Msg, TMAcc), {Out ++ OutAcc, OM2, TM2} diff --git a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl index bdc061b1d..b5e4deb7f 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl @@ -108,6 +108,9 @@ handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM) end. %% send to a client, msg can be request/piggyback/separate/notify +handle_out({Ctx, Msg}, TM) -> + handle_out(Msg, Ctx, TM); + handle_out(Msg, TM) -> handle_out(Msg, undefined, TM). @@ -119,8 +122,8 @@ handle_out(#coap_message{token = Token} = MsgT, Ctx, TM) -> %% TODO why find by token ? case find_machine_by_keys([Id, TokenId], TM2) of undefined -> - {Machine, TM3} = new_out_machine(Id, Msg, TM), - process_event(out, {Ctx, Msg}, TM3, Machine); + {Machine, TM3} = new_out_machine(Id, Ctx, Msg, TM2), + process_event(out, Msg, TM3, Machine); _ -> %% ignore repeat send empty() @@ -293,9 +296,10 @@ new_in_machine(MachineId, #{seq_id := SeqId} = Manager) -> SeqId => Machine, MachineId => SeqId}}. --spec new_out_machine(state_machine_key(), emqx_coap_message(), manager()) -> +-spec new_out_machine(state_machine_key(), any(), emqx_coap_message(), manager()) -> {state_machine(), manager()}. new_out_machine(MachineId, + Ctx, #coap_message{type = Type, token = Token, options = Opts}, #{seq_id := SeqId} = Manager) -> Observe = maps:get(observe, Opts, undefined), @@ -305,7 +309,7 @@ new_out_machine(MachineId, , observe = Observe , state = idle , timers = #{} - , transport = emqx_coap_transport:new()}, + , transport = emqx_coap_transport:new(Ctx)}, Manager2 = Manager#{seq_id := SeqId + 1, SeqId => Machine, diff --git a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl index eb7ce9bd4..2e858a2e1 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl @@ -20,7 +20,7 @@ -type transport() :: #transport{}. --export([ new/0, idle/3, maybe_reset/3, set_cache/2 +-export([ new/0, new/1, idle/3, maybe_reset/3, set_cache/2 , maybe_resend_4request/3, wait_ack/3, until_stop/3 , observe/3, maybe_resend_4response/3]). @@ -33,9 +33,13 @@ -spec new() -> transport(). new() -> + new(undefined). + +new(ReqCtx) -> #transport{cache = undefined, retry_interval = 0, - retry_count = 0}. + retry_count = 0, + req_context = ReqCtx}. idle(in, #coap_message{type = non, method = Method} = Msg, @@ -62,9 +66,6 @@ idle(in, timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}) end; -idle(out, {Ctx, Msg}, Transport) -> - idle(out, Msg, Transport#transport{req_context = Ctx}); - idle(out, #coap_message{type = non} = Msg, _) -> out(Msg, #{next => maybe_reset, timeouts => [{stop_timeout, ?NON_LIFETIME}]}); diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl index ca734993a..85cf32c6d 100644 --- a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl @@ -24,11 +24,14 @@ -import(emqx_coap_message, [response/2, response/3]). -import(emqx_coap_medium, [reply/2, reply/3]). +-import(emqx_coap_channel, [run_hooks/3]). -define(UNSUB(Topic, Msg), #{subscribe => {Topic, Msg}}). -define(SUB(Topic, Token, Msg), #{subscribe => {{Topic, Token}, Msg}}). -define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}). +%% TODO maybe can merge this code into emqx_coap_session, simplify the call chain + handle_request(Path, #coap_message{method = Method} = Msg, Ctx, CInfo) -> case check_topic(Path) of {ok, Topic} -> @@ -42,7 +45,7 @@ handle_method(get, Topic, Msg, Ctx, CInfo) -> 0 -> subscribe(Msg, Topic, Ctx, CInfo); 1 -> - unsubscribe(Msg, Topic, CInfo); + unsubscribe(Msg, Topic, Ctx, CInfo); _ -> reply({error, bad_request}, <<"invalid observe value">>, Msg) end; @@ -51,8 +54,9 @@ handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) - case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of allow -> #{clientid := ClientId} = CInfo, + MountTopic = mount(CInfo, Topic), QOS = get_publish_qos(Msg), - MQTTMsg = emqx_message:make(ClientId, QOS, Topic, Payload), + MQTTMsg = emqx_message:make(ClientId, QOS, MountTopic, Payload), MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), _ = emqx_broker:publish(MQTTMsg2), reply({ok, changed}, Msg); @@ -139,15 +143,19 @@ subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) -> allow -> #{clientid := ClientId} = CInfo, SubOpts = get_sub_opts(Msg), - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', - [CInfo, Topic, SubOpts]), - ?SUB(Topic, Token, Msg); + MountTopic = mount(CInfo, Topic), + emqx_broker:subscribe(MountTopic, ClientId, SubOpts), + run_hooks(Ctx, 'session.subscribed', [CInfo, Topic, SubOpts]), + ?SUB(MountTopic, Token, Msg); _ -> reply({error, unauthorized}, Msg) end. -unsubscribe(Msg, Topic, CInfo) -> - emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), - ?UNSUB(Topic, Msg). +unsubscribe(Msg, Topic, Ctx, CInfo) -> + MountTopic = mount(CInfo, Topic), + emqx_broker:unsubscribe(MountTopic), + run_hooks(Ctx, 'session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), + ?UNSUB(MountTopic, Msg). + +mount(#{mountpoint := Mountpoint}, Topic) -> + <>. diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index 1ecd9cf26..d90942220 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -83,4 +83,4 @@ load_gateway_by_default([{Type, Confs}|More]) -> load_gateway_by_default(More). confs() -> - maps:to_list(emqx:get_config([gateway], [])). + maps:to_list(emqx:get_config([gateway], #{})). diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl index 458017118..77b97a6a1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_metrics.erl +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -18,7 +18,7 @@ -behaviour(gen_server). --include("include/emqx_gateway.hrl"). +-include_lib("emqx_gateway/include/emqx_gateway.hrl"). %% APIs diff --git a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl new file mode 100644 index 000000000..74b0cadc8 --- /dev/null +++ b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl @@ -0,0 +1,224 @@ +%%-------------------------------------------------------------------- +%% 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_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +gateway.coap { + idle_timeout = 30s + enable_stats = false + mountpoint = \"\" + notify_type = qos + connection_required = true + subscribe_qos = qos1 + publish_qos = qos1 + authentication = undefined + + listeners.udp.default { + bind = 5683 + } + } + ">>). + +-define(HOST, "127.0.0.1"). +-define(PORT, 5683). +-define(CONN_URI, "coap://127.0.0.1/mqtt/connection?clientid=client1&username=admin&password=public"). + +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + emqx_mgmt_api_test_util:init_suite([emqx_gateway]), + Config. + +end_per_suite(Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_gateway]), + Config. + +set_special_configs(emqx_gatewway) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT); + +set_special_configs(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Cases +%%-------------------------------------------------------------------- +t_send_request_api(_) -> + ClientId = start_client(), + timer:sleep(200), + Path = emqx_mgmt_api_test_util:api_path(["gateway/coap/client1/request"]), + Token = <<"atoken">>, + Payload = <<"simple echo this">>, + Req = #{token => Token, + payload => Payload, + timeout => 10, + content_type => <<"text/plain">>, + method => <<"get">>}, + Auth = emqx_mgmt_api_test_util:auth_header_(), + {ok, Response} = emqx_mgmt_api_test_util:request_api(post, + Path, + "method=get", + Auth, + Req + ), + #{<<"token">> := RToken, <<"payload">> := RPayload} = + emqx_json:decode(Response, [return_maps]), + ?assertEqual(Token, RToken), + ?assertEqual(Payload, RPayload), + erlang:exit(ClientId, kill), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal Functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +start_client() -> + spawn(fun coap_client/0). + +coap_client() -> + {ok, CSock} = gen_udp:open(0, [binary, {active, false}]), + test_send_coap_request(CSock, post, <<>>, [], 1), + Response = test_recv_coap_response(CSock), + ?assertEqual({ok, created}, Response#coap_message.method), + echo_loop(CSock). + +echo_loop(CSock) -> + #coap_message{payload = Payload} = Req = test_recv_coap_request(CSock), + test_send_coap_response(CSock, ?HOST, ?PORT, {ok, content}, Payload, Req), + echo_loop(CSock). + +test_send_coap_request(UdpSock, Method, Content, Options, MsgId) -> + is_list(Options) orelse error("Options must be a list"), + case resolve_uri(?CONN_URI) of + {coap, {IpAddr, Port}, Path, Query} -> + Request0 = emqx_coap_message:request(con, Method, Content, + [{uri_path, Path}, + {uri_query, Query} | Options]), + Request = Request0#coap_message{id = MsgId}, + ?LOGT("send_coap_request Request=~p", [Request]), + RequestBinary = emqx_coap_frame:serialize_pkt(Request, undefined), + ?LOGT("test udp socket send to ~p:~p, data=~p", [IpAddr, Port, RequestBinary]), + ok = gen_udp:send(UdpSock, IpAddr, Port, RequestBinary); + {SchemeDiff, ChIdDiff, _, _} -> + error(lists:flatten(io_lib:format("scheme ~s or ChId ~s does not match with socket", [SchemeDiff, ChIdDiff]))) + end. + +test_recv_coap_response(UdpSock) -> + {ok, {Address, Port, Packet}} = gen_udp:recv(UdpSock, 0, 2000), + {ok, Response, _, _} = emqx_coap_frame:parse(Packet, undefined), + ?LOGT("test udp receive from ~p:~p, data1=~p, Response=~p", [Address, Port, Packet, Response]), + #coap_message{type = ack, method = Method, id=Id, token = Token, options = Options, payload = Payload} = Response, + ?LOGT("receive coap response Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Response. + +test_recv_coap_request(UdpSock) -> + case gen_udp:recv(UdpSock, 0) of + {ok, {_Address, _Port, Packet}} -> + {ok, Request, _, _} = emqx_coap_frame:parse(Packet, undefined), + #coap_message{type = con, method = Method, id=Id, token = Token, payload = Payload, options = Options} = Request, + ?LOGT("receive coap request Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Request; + {error, Reason} -> + ?LOGT("test_recv_coap_request failed, Reason=~p", [Reason]), + timeout_test_recv_coap_request + end. + +test_send_coap_response(UdpSock, Host, Port, Code, Content, Request) -> + is_list(Host) orelse error("Host is not a string"), + {ok, IpAddr} = inet:getaddr(Host, inet), + Response = emqx_coap_message:piggyback(Code, Content, Request), + ?LOGT("test_send_coap_response Response=~p", [Response]), + Binary = emqx_coap_frame:serialize_pkt(Response, undefined), + ok = gen_udp:send(UdpSock, IpAddr, Port, Binary). + +resolve_uri(Uri) -> + {ok, #{scheme := Scheme, + host := Host, + port := PortNo, + path := Path} = URIMap} = emqx_http_lib:uri_parse(Uri), + Query = maps:get(query, URIMap, undefined), + {ok, PeerIP} = inet:getaddr(Host, inet), + {Scheme, {PeerIP, PortNo}, split_path(Path), split_query(Query)}. + +split_path([]) -> []; +split_path([$/]) -> []; +split_path([$/ | Path]) -> split_segments(Path, $/, []). + +split_query(undefined) -> #{}; +split_query(Path) -> + split_segments(Path, $&, []). + +split_segments(Path, Char, Acc) -> + case string:rchr(Path, Char) of + 0 -> + [make_segment(Path) | Acc]; + N when N > 0 -> + split_segments(string:substr(Path, 1, N-1), Char, + [make_segment(string:substr(Path, N+1)) | Acc]) + end. + +make_segment(Seg) -> + list_to_binary(emqx_http_lib:uri_decode(Seg)). + + +get_coap_path(Options) -> + get_path(Options, <<>>). + +get_coap_query(Options) -> + proplists:get_value(uri_query, Options, []). + +get_coap_observe(Options) -> + get_observe(Options). + + +get_path([], Acc) -> + %?LOGT("get_path Acc=~p", [Acc]), + Acc; +get_path([{uri_path, Path1}|T], Acc) -> + %?LOGT("Path=~p, Acc=~p", [Path1, Acc]), + get_path(T, join_path(Path1, Acc)); +get_path([{_, _}|T], Acc) -> + get_path(T, Acc). + +get_observe([]) -> + undefined; +get_observe([{observe, V}|_T]) -> + V; +get_observe([{_, _}|T]) -> + get_observe(T). + +join_path([], Acc) -> Acc; +join_path([<<"/">>|T], Acc) -> + join_path(T, Acc); +join_path([H|T], Acc) -> + join_path(T, <>). + +sprintf(Format, Args) -> + lists:flatten(io_lib:format(Format, Args)). diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index fa924a71c..34fa731c7 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -21,23 +21,30 @@ -define(BASE_PATH, "/api/v5"). init_suite() -> + init_suite([]). + +init_suite(Apps) -> ekka_mnesia:start(), application:load(emqx_management), - emqx_ct_helpers:start_apps([emqx_dashboard], fun set_special_configs/1). + emqx_ct_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1). + end_suite() -> + end_suite([]). + +end_suite(Apps) -> application:unload(emqx_management), - emqx_ct_helpers:stop_apps([emqx_dashboard]). + emqx_ct_helpers:stop_apps(Apps ++ [emqx_dashboard]). set_special_configs(emqx_dashboard) -> Config = #{ - default_username => <<"admin">>, - default_password => <<"public">>, - listeners => [#{ - protocol => http, - port => 18083 - }] - }, + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, emqx_config:put([emqx_dashboard], Config), ok; set_special_configs(_App) -> @@ -53,7 +60,7 @@ request_api(Method, Url, QueryParams, Auth) -> request_api(Method, Url, QueryParams, Auth, []). request_api(Method, Url, QueryParams, Auth, []) - when (Method =:= options) orelse + when (Method =:= options) orelse (Method =:= get) orelse (Method =:= put) orelse (Method =:= head) orelse From c54847b6e9033bf422d273e819cd58a64f757617 Mon Sep 17 00:00:00 2001 From: Jim Moen Date: Wed, 1 Sep 2021 14:40:10 +0800 Subject: [PATCH 274/306] refactor(mqtt_frame): Avoid duplicate variable when frame assembling. --- apps/emqx/src/emqx_frame.erl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 082801bad..79a740bed 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -100,14 +100,10 @@ parse(<>, StrictMode andalso validate_header(Type, Dup, QoS, Retain), Header = #mqtt_packet_header{type = Type, dup = bool(Dup), - qos = QoS, + qos = fixqos(Type, QoS), retain = bool(Retain) }, - Header1 = case fixqos(Type, QoS) of - QoS -> Header; - FixedQoS -> Header#mqtt_packet_header{qos = FixedQoS} - end, - parse_remaining_len(Rest, Header1, Options); + parse_remaining_len(Rest, Header, Options); parse(Bin, {{len, #{hdr := Header, len := {Multiplier, Length}} From be38bcc5ccccc2a7a421506c6c4de33c8f1c9531 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 09:46:47 +0800 Subject: [PATCH 275/306] chore(authn): adapt listener id type --- apps/emqx/include/emqx.hrl | 2 +- apps/emqx/src/emqx_authentication.erl | 20 +++++++----- apps/emqx/src/emqx_channel.erl | 8 ++--- apps/emqx/test/emqx_authentication_SUITE.erl | 10 +++--- apps/emqx_authn/include/emqx_authn.hrl | 2 +- apps/emqx_authn/src/emqx_authn_api.erl | 34 ++++++++++++++------ apps/emqx_authn/src/emqx_authn_app.erl | 2 +- 7 files changed, 48 insertions(+), 30 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 63ab13256..550e650a2 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -147,6 +147,6 @@ }). -record(chain, - { name :: binary() + { name :: atom() , authenticators :: [#authenticator{}] }). \ No newline at end of file diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 2b561d298..8dcca50eb 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -473,7 +473,7 @@ handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, S state = #{version := Version} = ST} = Authenticator -> case AuthenticatorID =:= generate_id(Config) of true -> - Unique = <>, + Unique = unique(ChainName, AuthenticatorID, Version), case Provider:update(Config#{'_unique' => Unique}, ST) of {ok, NewST} -> NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)}, @@ -575,17 +575,17 @@ split_by_id(ID, AuthenticatorsConfig) -> end. global_chain(mqtt) -> - <<"mqtt:global">>; + 'mqtt:global'; global_chain('mqtt-sn') -> - <<"mqtt-sn:global">>; + 'mqtt-sn:global'; global_chain(coap) -> - <<"coap:global">>; + 'coap:global'; global_chain(lwm2m) -> - <<"lwm2m:global">>; + 'lwm2m:global'; global_chain(stomp) -> - <<"stomp:global">>; + 'stomp:global'; global_chain(_) -> - <<"unknown:global">>. + 'unknown:global'. may_hook(#{hooked := false} = State) -> case lists:any(fun(#chain{authenticators = []}) -> false; @@ -618,7 +618,7 @@ do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config undefined -> {error, no_available_provider}; Provider -> - Unique = <>, + Unique = unique(ChainName, AuthenticatorID, ?VER_1), case Provider:create(Config#{'_unique' => Unique}) of {ok, State} -> Authenticator = #authenticator{id = AuthenticatorID, @@ -704,6 +704,10 @@ serialize_authenticator(#authenticator{id = ID, , state => State }. +unique(ChainName, AuthenticatorID, Version) -> + NChainName = atom_to_binary(ChainName), + <>. + switch_version(State = #{version := ?VER_1}) -> State#{version := ?VER_2}; switch_version(State = #{version := ?VER_2}) -> diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index e25a9c8d6..5e978669d 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -214,7 +214,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, ClientInfo = set_peercert_infos( Peercert, #{zone => Zone, - listener => Listener, + listener => emqx_listeners:listener_id(Type, Listener), protocol => Protocol, peerhost => PeerHost, sockport => SockPort, @@ -223,7 +223,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, mountpoint => MountPoint, is_bridge => false, is_superuser => false - }, Zone, Listener), + }, Zone), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), #channel{conninfo = NConnInfo, clientinfo = NClientInfo, @@ -244,12 +244,12 @@ quota_policy(RawPolicy) -> erlang:trunc(hocon_postprocess:duration(StrWind) / 1000)}} || {Name, [StrCount, StrWind]} <- maps:to_list(RawPolicy)]. -set_peercert_infos(NoSSL, ClientInfo, _, _) +set_peercert_infos(NoSSL, ClientInfo, _) when NoSSL =:= nossl; NoSSL =:= undefined -> ClientInfo#{username => undefined}; -set_peercert_infos(Peercert, ClientInfo, Zone, _Listener) -> +set_peercert_infos(Peercert, ClientInfo, Zone) -> {DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)}, PeercetAs = fun(Key) -> diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index a940adc88..0b610d2e5 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -94,7 +94,7 @@ end_per_suite(_) -> t_chain(_) -> % CRUD of authentication chain - ChainName = <<"test">>, + ChainName = 'test', ?assertMatch({ok, []}, ?AUTHN:list_chains()), ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:create_chain(ChainName)), ?assertEqual({error, {already_exists, {chain, ChainName}}}, ?AUTHN:create_chain(ChainName)), @@ -105,7 +105,7 @@ t_chain(_) -> ok. t_authenticator(_) -> - ChainName = <<"test">>, + ChainName = 'test', AuthenticatorConfig1 = #{mechanism => 'password-based', backend => 'built-in-database', enable => true}, @@ -155,7 +155,7 @@ t_authenticator(_) -> ok. t_authenticate(_) -> - ListenerID = <<"tcp:default">>, + ListenerID = 'tcp:default', ClientInfo = #{zone => default, listener => ListenerID, protocol => mqtt, @@ -186,7 +186,7 @@ t_update_config(_) -> ?AUTHN:add_provider(AuthNType1, ?MODULE), ?AUTHN:add_provider(AuthNType2, ?MODULE), - Global = <<"mqtt:global">>, + Global = 'mqtt:global', AuthenticatorConfig1 = #{mechanism => 'password-based', backend => 'built-in-database', enable => true}, @@ -212,7 +212,7 @@ t_update_config(_) -> ?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})), ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), - ListenerID = <<"tcp:default">>, + ListenerID = 'tcp:default', ConfKeyPath = [listeners, tcp, default, authentication], ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})), ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index bdf93204a..5eef08012 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -18,7 +18,7 @@ -define(AUTHN, emqx_authentication). --define(GLOBAL, <<"mqtt:global">>). +-define(GLOBAL, 'mqtt:global'). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}"). diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index afac57f99..7c3bcbd63 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1824,7 +1824,8 @@ find_listener(ListenerID) -> {ok, {Type, Name}} end. -create_authenticator(ConfKeyPath, ChainName, Config) -> +create_authenticator(ConfKeyPath, ChainName0, Config) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, raw_config := AuthenticatorsConfig}} -> @@ -1849,7 +1850,8 @@ list_authenticator(ConfKeyPath, AuthenticatorID) -> serialize_error(Reason) end. -update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) -> +update_authenticator(ConfKeyPath, ChainName0, AuthenticatorID, Config) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {update_authenticator, ChainName, AuthenticatorID, Config}) of {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, @@ -1860,7 +1862,8 @@ update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) -> serialize_error(Reason) end. -delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) -> +delete_authenticator(ConfKeyPath, ChainName0, AuthenticatorID) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {delete_authenticator, ChainName, AuthenticatorID}) of {ok, _} -> {204}; @@ -1868,7 +1871,8 @@ delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) -> serialize_error(Reason) end. -move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> +move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {move_authenticator, ChainName, AuthenticatorID, Position}) of {ok, _} -> {204}; @@ -1876,7 +1880,8 @@ move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> serialize_error(Reason) end. -add_user(ChainName, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> +add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> + ChainName = to_atom(ChainName0), Superuser = maps:get(<<"superuser">>, UserInfo, false), case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID , password => Password @@ -1891,7 +1896,8 @@ add_user(_, _, #{<<"user_id">> := _}) -> add_user(_, _, _) -> serialize_error({missing_parameter, user_id}). -update_user(ChainName, AuthenticatorID, UserID, UserInfo) -> +update_user(ChainName0, AuthenticatorID, UserID, UserInfo) -> + ChainName = to_atom(ChainName0), case maps:with([<<"password">>, <<"superuser">>], UserInfo) =:= #{} of true -> serialize_error({missing_parameter, password}); @@ -1904,7 +1910,8 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo) -> end end. -find_user(ChainName, AuthenticatorID, UserID) -> +find_user(ChainName0, AuthenticatorID, UserID) -> + ChainName = to_atom(ChainName0), case ?AUTHN:lookup_user(ChainName, AuthenticatorID, UserID) of {ok, User} -> {200, User}; @@ -1912,7 +1919,8 @@ find_user(ChainName, AuthenticatorID, UserID) -> serialize_error(Reason) end. -delete_user(ChainName, AuthenticatorID, UserID) -> +delete_user(ChainName0, AuthenticatorID, UserID) -> + ChainName = to_atom(ChainName0), case ?AUTHN:delete_user(ChainName, AuthenticatorID, UserID) of ok -> {204}; @@ -1920,7 +1928,8 @@ delete_user(ChainName, AuthenticatorID, UserID) -> serialize_error(Reason) end. -list_users(ChainName, AuthenticatorID) -> +list_users(ChainName0, AuthenticatorID) -> + ChainName = to_atom(ChainName0), case ?AUTHN:list_users(ChainName, AuthenticatorID) of {ok, Users} -> {200, Users}; @@ -1973,4 +1982,9 @@ serialize_error(Reason) -> to_list(M) when is_map(M) -> [M]; to_list(L) when is_list(L) -> - L. \ No newline at end of file + L. + +to_atom(B) when is_binary(B) -> + binary_to_atom(B); +to_atom(A) when is_atom(A) -> + A. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 58470289a..016decdd2 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -53,7 +53,7 @@ remove_providers() -> initialize() -> ?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])), lists:foreach(fun({ListenerID, ListenerConfig}) -> - ?AUTHN:initialize_authentication(atom_to_binary(ListenerID), maps:get(authentication, ListenerConfig, [])) + ?AUTHN:initialize_authentication(ListenerID, maps:get(authentication, ListenerConfig, [])) end, emqx_listeners:list()), ok. From 8531e9ce11b720ec85a3c7c62c74d9d695da3746 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 09:53:39 +0800 Subject: [PATCH 276/306] chore(authn): rename superuser to is_superuser --- apps/emqx/src/emqx_access_control.erl | 4 +-- apps/emqx/src/emqx_authentication.erl | 2 +- apps/emqx/src/emqx_channel.erl | 6 ++-- apps/emqx/test/emqx_authentication_SUITE.erl | 6 ++-- apps/emqx/test/emqx_channel_SUITE.erl | 2 +- apps/emqx_authn/data/user-credentials.csv | 2 +- apps/emqx_authn/data/user-credentials.json | 4 +-- apps/emqx_authn/src/emqx_authn_api.erl | 18 +++++----- .../emqx_enhanced_authn_scram_mnesia.erl | 24 ++++++------- .../src/simple_authn/emqx_authn_http.erl | 6 ++-- .../src/simple_authn/emqx_authn_jwt.erl | 2 +- .../src/simple_authn/emqx_authn_mnesia.erl | 34 +++++++++---------- .../src/simple_authn/emqx_authn_mongodb.erl | 6 ++-- .../src/simple_authn/emqx_authn_mysql.erl | 2 +- .../src/simple_authn/emqx_authn_pgsql.erl | 2 +- .../emqx_authn/test/data/user-credentials.csv | 2 +- .../test/data/user-credentials.json | 4 +-- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 18 +++++----- .../test/emqx_authn_mnesia_SUITE.erl | 20 +++++------ 19 files changed, 82 insertions(+), 82 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 7d5b009ba..914651535 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -29,9 +29,9 @@ -spec(authenticate(emqx_types:clientinfo()) -> {ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). authenticate(Credential) -> - case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of + case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of ok -> - {ok, #{superuser => false}}; + {ok, #{is_superuser => false}}; Other -> Other end. diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 8dcca50eb..8cc8cf2df 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -80,7 +80,7 @@ -type config() :: #{atom() => term()}. -type state() :: #{atom() => term()}. --type extra() :: #{superuser := boolean(), +-type extra() :: #{is_superuser := boolean(), atom() => term()}. -type user_info() :: #{user_id := binary(), atom() => term()}. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5e978669d..26342d8aa 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1303,11 +1303,11 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = case emqx_access_control:authenticate(Credential) of {ok, Result} -> {ok, Properties, - Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, auth_cache = #{}}}; {ok, Result, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, - Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, auth_cache = #{}}}; {continue, AuthCache} -> {continue, Properties, Channel#channel{auth_cache = AuthCache}}; @@ -1320,7 +1320,7 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - {ok, #{superuser := Superuser}} -> + {ok, #{is_superuser := Superuser}} -> {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index 0b610d2e5..aa4d55fee 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -73,7 +73,7 @@ update(_Config, _State) -> {ok, #{mark => 2}}. authenticate(#{username := <<"good">>}, _State) -> - {ok, #{superuser => true}}; + {ok, #{is_superuser => true}}; authenticate(#{username := _}, _State) -> {error, bad_username_or_password}. @@ -161,7 +161,7 @@ t_authenticate(_) -> protocol => mqtt, username => <<"good">>, password => <<"any">>}, - ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)), AuthNType = {'password-based', 'built-in-database'}, ?AUTHN:add_provider(AuthNType, ?MODULE), @@ -171,7 +171,7 @@ t_authenticate(_) -> enable => true}, ?AUTHN:create_chain(ListenerID), ?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)), - ?assertEqual({ok, #{superuser => true}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{is_superuser => true}}, emqx_access_control:authenticate(ClientInfo)), ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})), ?AUTHN:delete_chain(ListenerID), diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 031f89612..775b40ee8 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -144,7 +144,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, #{superuser => false}} end), + fun(_) -> {ok, #{is_superuser => false}} end), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), diff --git a/apps/emqx_authn/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv index 0548308b7..cbadaefbc 100644 --- a/apps/emqx_authn/data/user-credentials.csv +++ b/apps/emqx_authn/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt,superuser +user_id,password_hash,salt,is_superuser myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json index e54501233..94375df22 100644 --- a/apps/emqx_authn/data/user-credentials.json +++ b/apps/emqx_authn/data/user-credentials.json @@ -3,12 +3,12 @@ "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", "salt": "e378187547bf2d6f0545a3f441aa4d8a", - "superuser": true + "is_superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", - "superuser": false + "is_superuser": false } ] diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 7c3bcbd63..c306e102b 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -765,7 +765,7 @@ create_user_api_spec() -> password => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean, default => false } @@ -785,7 +785,7 @@ create_user_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -850,7 +850,7 @@ list_users_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -920,7 +920,7 @@ update_user_api_spec() -> password => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -941,7 +941,7 @@ update_user_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -1025,7 +1025,7 @@ find_user_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -1882,10 +1882,10 @@ move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> ChainName = to_atom(ChainName0), - Superuser = maps:get(<<"superuser">>, UserInfo, false), + Superuser = maps:get(<<"is_superuser">>, UserInfo, false), case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID , password => Password - , superuser => Superuser}) of + , is_superuser => Superuser}) of {ok, User} -> {201, User}; {error, Reason} -> @@ -1898,7 +1898,7 @@ add_user(_, _, _) -> update_user(ChainName0, AuthenticatorID, UserID, UserInfo) -> ChainName = to_atom(ChainName0), - case maps:with([<<"password">>, <<"superuser">>], UserInfo) =:= #{} of + case maps:with([<<"password">>, <<"is_superuser">>], UserInfo) =:= #{} of true -> serialize_error({missing_parameter, password}); false -> 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 index aa21c0484..4aac21bb2 100644 --- 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 @@ -53,7 +53,7 @@ , stored_key , server_key , salt - , superuser + , is_superuser }). %%------------------------------------------------------------------------------ @@ -147,9 +147,9 @@ add_user(#{user_id := UserID, fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - Superuser = maps:get(superuser, UserInfo, false), + Superuser = maps:get(is_superuser, UserInfo, false), add_user(UserID, Password, Superuser, State), - {ok, #{user_id => UserID, superuser => Superuser}}; + {ok, #{user_id => UserID, is_superuser => Superuser}}; [_] -> {error, already_exist} end @@ -173,8 +173,8 @@ update_user(UserID, User, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [#user_info{superuser = Superuser} = UserInfo] -> - UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)}, + [#user_info{is_superuser = Superuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, Superuser)}, UserInfo2 = case maps:get(password, User, undefined) of undefined -> UserInfo1; @@ -229,13 +229,13 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} end. -check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) -> +check_client_final_message(Bin, #{is_superuser := Superuser} = Cache, #{algorithm := Alg}) -> case esasl_scram:check_client_final_message( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, #{superuser => Superuser}, ServerFinalMessage}; + {ok, #{is_superuser => Superuser}, ServerFinalMessage}; {error, _Reason} -> {error, not_authorized} end. @@ -246,7 +246,7 @@ add_user(UserID, Password, Superuser, State) -> stored_key = StoredKey, server_key = ServerKey, salt = Salt, - superuser = Superuser}, + is_superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). retrieve(UserID, #{user_group := UserGroup}) -> @@ -254,11 +254,11 @@ retrieve(UserID, #{user_group := UserGroup}) -> [#user_info{stored_key = StoredKey, server_key = ServerKey, salt = Salt, - superuser = Superuser}] -> + is_superuser = Superuser}] -> {ok, #{stored_key => StoredKey, server_key => ServerKey, salt => Salt, - superuser => Superuser}}; + is_superuser => Superuser}}; [] -> {error, not_found} end. @@ -273,5 +273,5 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. -serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> - #{user_id => UserID, superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> + #{user_id => UserID, is_superuser => Superuser}. 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 19417218d..5495b139a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -161,16 +161,16 @@ authenticate(Credential, #{'_unique' := Unique, try Request = generate_request(Credential, State), case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of - {ok, 204, _Headers} -> {ok, #{superuser => false}}; + {ok, 204, _Headers} -> {ok, #{is_superuser => false}}; {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, #{superuser => maps:get(<<"superuser">>, NBody, false), + {ok, #{is_superuser => maps:get(<<"is_superuser">>, NBody, false), user_property => NBody}}; {error, _Reason} -> - {ok, #{superuser => false}} + {ok, #{is_superuser => false}} end; {error, _Reason} -> ignore 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 e55b58795..774d75157 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -249,7 +249,7 @@ verify(JWS, [JWK | More], VerifyClaims) -> Claims = emqx_json:decode(Payload, [return_maps]), case verify_claims(Claims, VerifyClaims) of ok -> - {ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}}; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Claims, false)}}; {error, Reason} -> {error, Reason} end; 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 f41edab8b..563a255f0 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -51,7 +51,7 @@ { user_id :: {user_group(), user_id()} , password_hash :: binary() , salt :: binary() - , superuser :: boolean() + , is_superuser :: boolean() }). -reflect_type([ user_id_type/0 ]). @@ -158,13 +158,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = Superuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> {ok, #{superuser => Superuser}}; + true -> {ok, #{is_superuser => Superuser}}; false -> {error, bad_username_or_password} end end. @@ -197,9 +197,9 @@ add_user(#{user_id := UserID, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {PasswordHash, Salt} = hash(Password, State), - Superuser = maps:get(superuser, UserInfo, false), + Superuser = maps:get(is_superuser, UserInfo, false), insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), - {ok, #{user_id => UserID, superuser => Superuser}}; + {ok, #{user_id => UserID, is_superuser => Superuser}}; [_] -> {error, already_exist} end @@ -225,8 +225,8 @@ update_user(UserID, UserInfo, {error, not_found}; [#user_info{ password_hash = PasswordHash , salt = Salt - , superuser = Superuser}] -> - NSuperuser = maps:get(superuser, UserInfo, Superuser), + , is_superuser = Superuser}] -> + NSuperuser = maps:get(is_superuser, UserInfo, Superuser), {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of undefined -> {PasswordHash, Salt}; @@ -234,7 +234,7 @@ update_user(UserID, UserInfo, hash(Password, State) end, insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser), - {ok, #{user_id => UserID, superuser => NSuperuser}} + {ok, #{user_id => UserID, is_superuser => NSuperuser}} end end). @@ -290,7 +290,7 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> Salt = maps:get(<<"salt">>, UserInfo, <<>>), - Superuser = maps:get(<<"superuser">>, UserInfo, false), + Superuser = maps:get(<<"is_superuser">>, UserInfo, false), insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> @@ -305,7 +305,7 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = UserInfo} -> Salt = maps:get(salt, UserInfo, <<>>), - Superuser = maps:get(superuser, UserInfo, false), + Superuser = maps:get(is_superuser, UserInfo, false), insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), import(UserGroup, File, Seq); {error, Reason} -> @@ -341,10 +341,10 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc) get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash}); get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) -> get_user_info_by_seq(More1, More2, Acc#{salt => Salt}); -get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) -> - get_user_info_by_seq(More1, More2, Acc#{superuser => true}); -get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) -> - get_user_info_by_seq(More1, More2, Acc#{superuser => false}); +get_user_info_by_seq([<<"true">> | More1], [<<"is_superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{is_superuser => true}); +get_user_info_by_seq([<<"false">> | More1], [<<"is_superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{is_superuser => false}); get_user_info_by_seq(_, _, _) -> {error, bad_format}. @@ -372,7 +372,7 @@ insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) -> UserInfo = #user_info{user_id = {UserGroup, UserID}, password_hash = PasswordHash, salt = Salt, - superuser = Superuser}, + is_superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -400,5 +400,5 @@ to_binary(B) when is_binary(B) -> to_binary(L) when is_list(L) -> iolist_to_binary(L). -serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> - #{user_id => UserID, superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> + #{user_id => UserID, is_superuser => Superuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index f35be985a..9c2ec935c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -149,7 +149,7 @@ authenticate(#{password := Password} = Credential, Doc -> case check_password(Password, Doc, State) of ok -> - {ok, #{superuser => superuser(Doc, State)}}; + {ok, #{is_superuser => is_superuser(Doc, State)}}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), {error, bad_username_or_password}; @@ -230,9 +230,9 @@ check_password(Password, end end. -superuser(Doc, #{superuser_field := SuperuserField}) -> +is_superuser(Doc, #{superuser_field := SuperuserField}) -> maps:get(SuperuserField, Doc, false); -superuser(_, _) -> +is_superuser(_, _) -> false. hash(Algorithm, Password, Salt, prefix) -> 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 67ccbf7ae..991bb6aee 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -123,7 +123,7 @@ authenticate(#{password := Password} = Credential, Selected = maps:from_list(lists:zip(Columns, Rows)), case check_password(Password, Selected, State) of ok -> - {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}}; {error, Reason} -> {error, Reason} 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 index 7676f338d..c497074de 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -113,7 +113,7 @@ authenticate(#{password := Password} = Credential, Selected = maps:from_list(lists:zip(NColumns, Rows)), case check_password(Password, Selected, State) of ok -> - {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}}; {error, Reason} -> {error, Reason} end; diff --git a/apps/emqx_authn/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv index 0548308b7..cbadaefbc 100644 --- a/apps/emqx_authn/test/data/user-credentials.csv +++ b/apps/emqx_authn/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt,superuser +user_id,password_hash,salt,is_superuser myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json index e54501233..94375df22 100644 --- a/apps/emqx_authn/test/data/user-credentials.json +++ b/apps/emqx_authn/test/data/user-credentials.json @@ -3,12 +3,12 @@ "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", "salt": "e378187547bf2d6f0545a3f441aa4d8a", - "superuser": true + "is_superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", - "superuser": false + "is_superuser": false } ] diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 9d8b1d9fc..16c04771d 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -52,13 +52,13 @@ all() -> % JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), % ClientInfo = #{username => <<"myuser">>, % password => JWS}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), -% Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, +% Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true}, % JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), % ClientInfo1 = #{username => <<"myuser">>, % password => JWS1}, -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), % BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), % ClientInfo2 = ClientInfo#{password => BadJWS}, @@ -68,11 +68,11 @@ all() -> % Config2 = Config#{secret => base64:encode(<<"abcdef">>), % secret_base64_encoded => true}, % ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, % ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), % %% Expiration @@ -86,14 +86,14 @@ all() -> % , <<"exp">> => erlang:system_time(second) + 60}, % JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), % ClientInfo4 = ClientInfo#{password => JWS4}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), % %% Issued At % Payload5 = #{ <<"username">> => <<"myuser">> % , <<"iat">> => erlang:system_time(second) - 60}, % JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), % ClientInfo5 = ClientInfo#{password => JWS5}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), % Payload6 = #{ <<"username">> => <<"myuser">> % , <<"iat">> => erlang:system_time(second) + 60}, @@ -106,7 +106,7 @@ all() -> % , <<"nbf">> => erlang:system_time(second) - 60}, % JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), % ClientInfo7 = ClientInfo#{password => JWS7}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), % Payload8 = #{ <<"username">> => <<"myuser">> % , <<"nbf">> => erlang:system_time(second) + 60}, @@ -134,7 +134,7 @@ all() -> % JWS = generate_jws('public-key', Payload, PrivateKey), % ClientInfo = #{username => <<"myuser">>, % password => JWS}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 4bc6961dd..959cf0323 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -56,9 +56,9 @@ all() -> % ClientInfo = #{zone => external, % username => <<"myuser">>, % password => <<"mypass">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % ?AUTH:enable(), -% ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), +% ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)), % ClientInfo2 = ClientInfo#{username => <<"baduser">>}, % ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), @@ -71,10 +71,10 @@ all() -> % UserInfo2 = UserInfo#{password => <<"mypass2">>}, % ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), % ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), -% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{is_superuser => true})), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), % ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), % ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), @@ -107,15 +107,15 @@ all() -> % ClientInfo1 = #{username => <<"myuser1">>, % password => <<"mypassword1">>}, -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), % ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, % password => <<"mypassword2">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), % ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, % password => <<"mypassword3">>}, -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), % ok. @@ -152,12 +152,12 @@ all() -> % ClientInfo1 = #{username => <<"myuser">>, % clientid => <<"myclient">>, % password => <<"mypass1">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), % ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), % ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), % ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), From ce851e5b0f8da27edebdb48c179a53fb018ce618 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 10:32:54 +0800 Subject: [PATCH 277/306] chore(authn): miss redis --- apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 18840fdea..949aeeaea 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -135,7 +135,7 @@ authenticate(#{password := Password} = Credential, Selected = merge(Fields, Values), case check_password(Password, Selected, State) of ok -> - {ok, #{superuser => maps:get("superuser", Selected, false)}}; + {ok, #{is_superuser => maps:get("is_superuser", Selected, false)}}; {error, Reason} -> {error, Reason} end; @@ -180,7 +180,7 @@ check_fields(["password_hash" | More], false) -> check_fields(More, true); check_fields(["salt" | More], HasPassHash) -> check_fields(More, HasPassHash); -check_fields(["superuser" | More], HasPassHash) -> +check_fields(["is_superuser" | More], HasPassHash) -> check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). From b5ded1ece0ebbe90719810bff78c1022dafab5eb Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 10:46:18 +0800 Subject: [PATCH 278/306] chore(authn): add the serialization of more errors --- apps/emqx_authn/src/emqx_authn_api.erl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c306e102b..a4d322712 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1962,9 +1962,11 @@ serialize_error({not_found, {authenticator, ID}}) -> serialize_error({not_found, {listener, ID}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Listener '~s' does not exist", [ID]))}}; -serialize_error(name_has_be_used) -> +serialize_error({already_exists, {authenticator, ID}}) -> {409, #{code => <<"ALREADY_EXISTS">>, - message => <<"Name has be used">>}}; + message => list_to_binary( + io_lib:format("Authenticator '~s' already exist", [ID]) + )}}; serialize_error({missing_parameter, Name}) -> {400, #{code => <<"MISSING_PARAMETER">>, message => list_to_binary( @@ -1977,7 +1979,7 @@ serialize_error({invalid_parameter, Name}) -> )}}; serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, - message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. + message => list_to_binary(io_lib:format("~p", [Reason]))}}. to_list(M) when is_map(M) -> [M]; From 29cad91a471526af5540bd42c5763acb28a2519a Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 10:58:00 +0800 Subject: [PATCH 279/306] fix(authn): fix superuser in mongodb authn --- apps/emqx/src/emqx_channel.erl | 4 +- apps/emqx_authn/src/emqx_authn_api.erl | 9 ++++- .../emqx_enhanced_authn_scram_mnesia.erl | 40 +++++++++---------- .../src/simple_authn/emqx_authn_mnesia.erl | 30 +++++++------- .../src/simple_authn/emqx_authn_mongodb.erl | 10 ++++- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 26342d8aa..0b1ff7e25 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1320,8 +1320,8 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - {ok, #{is_superuser := Superuser}} -> - {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; + {ok, #{is_superuser := IsSuperuser}} -> + {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index a4d322712..5ba1419f0 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -73,6 +73,7 @@ }, password_hash_field => <<"password_hash">>, salt_field => <<"salt">>, + is_superuser_field => <<"is_superuser">>, password_hash_algorithm => <<"sha256">>, salt_position => <<"prefix">> }). @@ -1398,6 +1399,10 @@ definitions() -> type => string, example => <<"salt">> }, + is_superuser_field => #{ + type => string, + example => <<"is_superuser">> + }, password_hash_algorithm => #{ type => string, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], @@ -1882,10 +1887,10 @@ move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> ChainName = to_atom(ChainName0), - Superuser = maps:get(<<"is_superuser">>, UserInfo, false), + IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID , password => Password - , is_superuser => Superuser}) of + , is_superuser => IsSuperuser}) of {ok, User} -> {201, User}; {error, Reason} -> 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 index 4aac21bb2..e0f37a50d 100644 --- 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 @@ -147,9 +147,9 @@ add_user(#{user_id := UserID, fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - Superuser = maps:get(is_superuser, UserInfo, false), - add_user(UserID, Password, Superuser, State), - {ok, #{user_id => UserID, is_superuser => Superuser}}; + IsSuperuser = maps:get(is_superuser, UserInfo, false), + add_user(UserID, Password, IsSuperuser, State), + {ok, #{user_id => UserID, is_superuser => IsSuperuser}}; [_] -> {error, already_exist} end @@ -173,8 +173,8 @@ update_user(UserID, User, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [#user_info{is_superuser = Superuser} = UserInfo] -> - UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, Superuser)}, + [#user_info{is_superuser = IsSuperuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, IsSuperuser)}, UserInfo2 = case maps:get(password, User, undefined) of undefined -> UserInfo1; @@ -229,36 +229,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} end. -check_client_final_message(Bin, #{is_superuser := Superuser} = Cache, #{algorithm := Alg}) -> +check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) -> case esasl_scram:check_client_final_message( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, #{is_superuser => Superuser}, ServerFinalMessage}; + {ok, #{is_superuser => IsSuperuser}, ServerFinalMessage}; {error, _Reason} -> {error, not_authorized} end. -add_user(UserID, Password, Superuser, State) -> +add_user(UserID, Password, IsSuperuser, State) -> {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), - UserInfo = #user_info{user_id = UserID, - stored_key = StoredKey, - server_key = ServerKey, - salt = Salt, - is_superuser = Superuser}, + UserInfo = #user_info{user_id = UserID, + stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + is_superuser = IsSuperuser}, mnesia:write(?TAB, UserInfo, write). retrieve(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#user_info{stored_key = StoredKey, - server_key = ServerKey, - salt = Salt, - is_superuser = Superuser}] -> + [#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + is_superuser = IsSuperuser}] -> {ok, #{stored_key => StoredKey, server_key => ServerKey, salt => Salt, - is_superuser => Superuser}}; + is_superuser => IsSuperuser}}; [] -> {error, not_found} end. @@ -273,5 +273,5 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. -serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> - #{user_id => UserID, is_superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> + #{user_id => UserID, is_superuser => IsSuperuser}. 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 563a255f0..b69d613f8 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -158,13 +158,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = Superuser}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = IsSuperuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> {ok, #{is_superuser => Superuser}}; + true -> {ok, #{is_superuser => IsSuperuser}}; false -> {error, bad_username_or_password} end end. @@ -197,9 +197,9 @@ add_user(#{user_id := UserID, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {PasswordHash, Salt} = hash(Password, State), - Superuser = maps:get(is_superuser, UserInfo, false), - insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), - {ok, #{user_id => UserID, is_superuser => Superuser}}; + IsSuperuser = maps:get(is_superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), + {ok, #{user_id => UserID, is_superuser => IsSuperuser}}; [_] -> {error, already_exist} end @@ -225,8 +225,8 @@ update_user(UserID, UserInfo, {error, not_found}; [#user_info{ password_hash = PasswordHash , salt = Salt - , is_superuser = Superuser}] -> - NSuperuser = maps:get(is_superuser, UserInfo, Superuser), + , is_superuser = IsSuperuser}] -> + NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser), {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of undefined -> {PasswordHash, Salt}; @@ -290,8 +290,8 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> Salt = maps:get(<<"salt">>, UserInfo, <<>>), - Superuser = maps:get(<<"is_superuser">>, UserInfo, false), - insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> {error, bad_format}. @@ -305,8 +305,8 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = UserInfo} -> Salt = maps:get(salt, UserInfo, <<>>), - Superuser = maps:get(is_superuser, UserInfo, false), - insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + IsSuperuser = maps:get(is_superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), import(UserGroup, File, Seq); {error, Reason} -> {error, Reason} @@ -368,11 +368,11 @@ hash(Password, #{password_hash_algorithm := Algorithm} = State) -> PasswordHash = hash(Algorithm, Password, Salt), {PasswordHash, Salt}. -insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) -> +insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) -> UserInfo = #user_info{user_id = {UserGroup, UserID}, password_hash = PasswordHash, salt = Salt, - is_superuser = Superuser}, + is_superuser = IsSuperuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -400,5 +400,5 @@ to_binary(B) when is_binary(B) -> to_binary(L) when is_list(L) -> iolist_to_binary(L). -serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> - #{user_id => UserID, is_superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> + #{user_id => UserID, is_superuser => IsSuperuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 9c2ec935c..9d77c673c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -64,6 +64,7 @@ common_fields() -> , {selector, fun selector/1} , {password_hash_field, fun password_hash_field/1} , {salt_field, fun salt_field/1} + , {is_superuser_field, fun is_superuser_field/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} ] ++ emqx_authn_schema:common_fields(). @@ -84,6 +85,10 @@ salt_field(type) -> binary(); salt_field(nullable) -> true; salt_field(_) -> undefined. +is_superuser_field(type) -> binary(); +is_superuser_field(nullable) -> true; +is_superuser_field(_) -> undefined. + password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; password_hash_algorithm(default) -> sha256; password_hash_algorithm(_) -> undefined. @@ -109,6 +114,7 @@ create(#{ selector := Selector State = maps:with([ collection , password_hash_field , salt_field + , is_superuser_field , password_hash_algorithm , salt_position , '_unique'], Config), @@ -230,8 +236,8 @@ check_password(Password, end end. -is_superuser(Doc, #{superuser_field := SuperuserField}) -> - maps:get(SuperuserField, Doc, false); +is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) -> + maps:get(IsSuperuserField, Doc, false); is_superuser(_, _) -> false. From 287d315ed506a3addd4b1b560e1ad770307b2151 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 11:24:59 +0800 Subject: [PATCH 280/306] fix(listener): updating authentication no longer causes the listener to restart --- apps/emqx/src/emqx_listeners.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 7b1d6b0dd..06d900ed5 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -280,7 +280,7 @@ flatten_listeners(Conf0) -> || {Type, Conf} <- maps:to_list(Conf0)])). do_flatten_listeners(Type, Conf0) -> - [{listener_id(Type, Name), Conf} || {Name, Conf} <- maps:to_list(Conf0)]. + [{listener_id(Type, Name), maps:remove(authentication, Conf)} || {Name, Conf} <- maps:to_list(Conf0)]. esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), From bcebe1de2470b6813645d11d582628ad4e796833 Mon Sep 17 00:00:00 2001 From: Jim Moen Date: Tue, 7 Sep 2021 16:54:05 +0800 Subject: [PATCH 281/306] refactor(emqx_cm_sup): Internal functions to create workers. --- apps/emqx/src/emqx_cm_sup.erl | 61 +++++++++++--------------- apps/emqx/test/emqx_client_SUITE.erl | 4 +- apps/emqx/test/emqx_flapping_SUITE.erl | 6 +-- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/apps/emqx/src/emqx_cm_sup.erl b/apps/emqx/src/emqx_cm_sup.erl index f332a0868..cddd8aa5e 100644 --- a/apps/emqx/src/emqx_cm_sup.erl +++ b/apps/emqx/src/emqx_cm_sup.erl @@ -22,49 +22,38 @@ -export([init/1]). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). +%%-------------------------------------------------------------------- +%% Supervisor callbacks +%%-------------------------------------------------------------------- + init([]) -> - Banned = #{id => banned, - start => {emqx_banned, start_link, []}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [emqx_banned]}, - Flapping = #{id => flapping, - start => {emqx_flapping, start_link, []}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [emqx_flapping]}, - %% Channel locker - Locker = #{id => locker, - start => {emqx_cm_locker, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm_locker] - }, - %% Channel registry - Registry = #{id => registry, - start => {emqx_cm_registry, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm_registry] - }, - %% Channel Manager - Manager = #{id => manager, - start => {emqx_cm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm] - }, SupFlags = #{strategy => one_for_one, intensity => 100, period => 10 }, + Banned = child_spec(emqx_banned, 1000, worker), + Flapping = child_spec(emqx_flapping, 1000, worker), + Locker = child_spec(emqx_cm_locker, 5000, worker), + Registry = child_spec(emqx_cm_registry, 5000, worker), + Manager = child_spec(emqx_cm, 5000, worker), {ok, {SupFlags, [Banned, Flapping, Locker, Registry, Manager]}}. +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +child_spec(Mod, Shutdown, Type) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => Shutdown, + type => Type, + modules => [Mod] + }. diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 117a0f5b9..0a3a050ac 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -114,8 +114,8 @@ t_cm(_) -> emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000). t_cm_registry(_) -> - Info = supervisor:which_children(emqx_cm_sup), - {_, Pid, _, _} = lists:keyfind(registry, 1, Info), + Children = supervisor:which_children(emqx_cm_sup), + {_, Pid, _, _} = lists:keyfind(emqx_cm_registry, 1, Children), ignored = gen_server:call(Pid, <<"Unexpected call">>), gen_server:cast(Pid, <<"Unexpected cast">>), Pid ! <<"Unexpected info">>. diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index 5ac6b9cdf..a8e783c49 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -55,8 +55,8 @@ t_detect_check(_) -> true = emqx_banned:check(ClientInfo), timer:sleep(3000), false = emqx_banned:check(ClientInfo), - Childrens = supervisor:which_children(emqx_cm_sup), - {flapping, Pid, _, _} = lists:keyfind(flapping, 1, Childrens), + Children = supervisor:which_children(emqx_cm_sup), + {emqx_flapping, Pid, _, _} = lists:keyfind(emqx_flapping, 1, Children), gen_server:call(Pid, unexpected_msg), gen_server:cast(Pid, unexpected_msg), Pid ! test, @@ -72,4 +72,4 @@ t_expired_detecting(_) -> (_) -> false end, ets:tab2list(emqx_flapping))), timer:sleep(200), ?assertEqual(true, lists:all(fun({flapping, <<"client008">>, _, _, _}) -> false; - (_) -> true end, ets:tab2list(emqx_flapping))). \ No newline at end of file + (_) -> true end, ets:tab2list(emqx_flapping))). From 4e5d781d21b5178c07d628817d77bab9b3d641b6 Mon Sep 17 00:00:00 2001 From: Jim Moen Date: Tue, 7 Sep 2021 18:54:02 +0800 Subject: [PATCH 282/306] fix: Words spelling fix. --- apps/emqx/etc/emqx.conf | 8 ++++---- apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl | 2 +- apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 7c404ff2d..6834d2a6e 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -64,7 +64,7 @@ listeners.tcp.default { 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. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.tcp..proxy_protocol_timeout ## ValueType: Duration @@ -163,7 +163,7 @@ listeners.ssl.default { 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. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.ssl..proxy_protocol_timeout ## ValueType: Duration @@ -345,7 +345,7 @@ listeners.ws.default { 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. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.ws..proxy_protocol_timeout ## ValueType: Duration @@ -448,7 +448,7 @@ listeners.wss.default { 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. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.wss..proxy_protocol_timeout ## ValueType: Duration diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index 51bcbd358..543b2e169 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -835,7 +835,7 @@ inc_incoming_stats(Ctx, FrameMod, Packet) -> ok end, Name = list_to_atom( - lists:concat(["packets.", FrameMod:type(Packet), ".recevied"])), + lists:concat(["packets.", FrameMod:type(Packet), ".received"])), emqx_gateway_ctx:metrics_inc(Ctx, Name). inc_outgoing_stats(Ctx, FrameMod, Packet) -> diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index e4b3d0095..23fb691d9 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -994,7 +994,7 @@ t_will_case06(_) -> receive {deliver, WillTopic, #message{payload = WillMsg}} -> ok; - Msg -> ct:print("recevived --- unex: ~p", [Msg]) + Msg -> ct:print("received --- unex: ~p", [Msg]) after 1000 -> ct:fail(wait_willmsg_timeout) end, From e2d9d9bfcb46b781eda2851b487a5349a5d96a7a Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:53:52 +0800 Subject: [PATCH 283/306] fix: banned api rfc time & login hidden error type (#5681) --- apps/emqx/src/emqx_banned.erl | 37 +++++++++++++++++++ .../emqx_dashboard/src/emqx_dashboard_api.erl | 8 ++-- .../src/emqx_mgmt_api_banned.erl | 30 ++------------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index 715548d41..608734363 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -37,6 +37,7 @@ , delete/1 , info/1 , format/1 + , parse/1 ]). %% gen_server callbacks @@ -107,6 +108,33 @@ format(#banned{who = Who0, until => to_rfc3339(Until) }. +parse(Params) -> + Who = pares_who(Params), + By = maps:get(<<"by">>, Params, <<"mgmt_api">>), + Reason = maps:get(<<"reason">>, Params, <<"">>), + At = pares_time(maps:get(<<"at">>, Params, undefined), erlang:system_time(second)), + Until = pares_time(maps:get(<<"until">>, Params, undefined), At + 5 * 60), + #banned{ + who = Who, + by = By, + reason = Reason, + at = At, + until = Until + }. + +pares_who(#{as := As, who := Who}) -> + pares_who(#{<<"as">> => As, <<"who">> => Who}); +pares_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> + {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), + {peerhost, Peerhost}; +pares_who(#{<<"as">> := As, <<"who">> := Who}) -> + {binary_to_atom(As, utf8), Who}. + +pares_time(undefined, Default) -> + Default; +pares_time(Rfc3339, _Default) -> + to_timestamp(Rfc3339). + maybe_format_host({peerhost, Host}) -> AddrBinary = list_to_binary(inet:ntoa(Host)), {peerhost, AddrBinary}; @@ -116,6 +144,11 @@ maybe_format_host({As, Who}) -> to_rfc3339(Timestamp) -> list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])). +to_timestamp(Rfc3339) when is_binary(Rfc3339) -> + to_timestamp(binary_to_list(Rfc3339)); +to_timestamp(Rfc3339) -> + calendar:rfc3339_to_system_time(Rfc3339, [{unit, second}]). + -spec(create(emqx_types:banned() | map()) -> ok). create(#{who := Who, by := By, @@ -130,12 +163,16 @@ create(#{who := Who, create(Banned) when is_record(Banned, banned) -> ekka_mnesia:dirty_write(?BANNED_TAB, Banned). +look_up(Who) when is_map(Who) -> + look_up(pares_who(Who)); look_up(Who) -> mnesia:dirty_read(?BANNED_TAB, Who). -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). +delete(Who) when is_map(Who)-> + delete(pares_who(Who)); delete(Who) -> ekka_mnesia:dirty_delete(?BANNED_TAB, Who). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 88ae85d9d..4761432fb 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -49,6 +49,8 @@ -define(EMPTY(V), (V == undefined orelse V == <<>>)). +-define(ERROR_USERNAME_OR_PWD, 'ERROR_USERNAME_OR_PWD'). + api_spec() -> {[ login_api() , logout_api() @@ -164,8 +166,8 @@ login(post, #{body := Params}) -> {ok, Token} -> Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), {200, #{token => Token, version => Version, license => #{edition => ?RELEASE}}}; - {error, Code} -> - {401, #{code => Code, message => <<"Auth filed">>}} + {error, _} -> + {401, #{code => ?ERROR_USERNAME_OR_PWD, message => <<"Auth filed">>}} end. logout(_, #{body := Params}) -> @@ -233,7 +235,7 @@ parameters() -> unauthorized_request() -> object_schema( properties([{message, string}, - {code, string, <<"Resp Code">>, ['PASSWORD_ERROR','USERNAME_ERROR']} + {code, string, <<"Resp Code">>, [?ERROR_USERNAME_OR_PWD]} ]), <<"Unauthorized">> ). diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl index 18abbd7e1..42addb4c7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl @@ -101,44 +101,20 @@ banned(get, #{query_string := Params}) -> Response = emqx_mgmt_api:paginate(?TAB, Params, fun format/1), {200, Response}; banned(post, #{body := Body}) -> - Banned = trans_param(Body), - _ = emqx_banned:create(Banned), + _ = emqx_banned:create(emqx_banned:parse(Body)), {200}. delete_banned(delete, #{bindings := Params}) -> - Who = trans_who(Params), - case emqx_banned:look_up(Who) of + case emqx_banned:look_up(Params) of [] -> As0 = maps:get(as, Params), Who0 = maps:get(who, Params), Message = list_to_binary(io_lib:format("~p: ~p not found", [As0, Who0])), {404, #{code => 'RESOURCE_NOT_FOUND', message => Message}}; _ -> - ok = emqx_banned:delete(Who), + ok = emqx_banned:delete(Params), {200} end. -trans_param(Params) -> - Who = trans_who(Params), - By = maps:get(<<"by">>, Params, <<"mgmt_api">>), - Reason = maps:get(<<"reason">>, Params, <<"">>), - At = maps:get(<<"at">>, Params, erlang:system_time(second)), - Until = maps:get(<<"until">>, Params, At + 5 * 60), - #banned{ - who = Who, - by = By, - reason = Reason, - at = At, - until = Until - }. - -trans_who(#{as := As, who := Who}) -> - trans_who(#{<<"as">> => As, <<"who">> => Who}); -trans_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> - {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), - {peerhost, Peerhost}; -trans_who(#{<<"as">> := As, <<"who">> := Who}) -> - {binary_to_atom(As, utf8), Who}. - format(Banned) -> emqx_banned:format(Banned). From f87a41a54ffdb643628042091ab3fab8e738036c Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:58:04 +0800 Subject: [PATCH 284/306] fix: api support basic auth (#5687) --- apps/emqx_dashboard/src/emqx_dashboard.erl | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 603d8009b..d109dd445 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -127,6 +127,16 @@ listener_name(Protocol, Port) -> authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of + {basic, Username, Password} -> + case emqx_dashboard_admin:check(Username, Password) of + ok -> + ok; + {error, _} -> + {401, #{<<"WWW-Authenticate">> => + <<"Basic Realm=\"minirest-server\"">>}, + #{code => <<"ERROR_USERNAME_OR_PWD">>, + message => <<"Check your username and password">>}} + end; {bearer, Token} -> case emqx_dashboard_admin:verify_token(Token) of ok -> @@ -135,8 +145,7 @@ authorize_appid(Req) -> {401, #{<<"WWW-Authenticate">> => <<"Bearer Realm=\"minirest-server\"">>}, #{code => <<"TOKEN_TIME_OUT">>, - message => <<"POST '/login', get your new token">>} - }; + message => <<"POST '/login', get your new token">>}}; {error, not_found} -> {401, #{<<"WWW-Authenticate">> => <<"Bearer Realm=\"minirest-server\"">>}, @@ -145,7 +154,7 @@ authorize_appid(Req) -> end; _ -> {401, #{<<"WWW-Authenticate">> => - <<"Bearer Realm=\"minirest-server\"">>}, - #{code => <<"UNAUTHORIZED">>, - message => <<"POST '/login'">>}} + <<"Basic Realm=\"minirest-server\"">>}, + #{code => <<"ERROR_USERNAME_OR_PWD">>, + message => <<"Check your username and password">>}} end. From 5602cfc22341b28cf4be221dff5549e99d6c24dc Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Wed, 8 Sep 2021 21:04:56 +0200 Subject: [PATCH 285/306] chore(build): ensure no gpb in release --- build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build b/build index 727c3a1a2..27626387e 100755 --- a/build +++ b/build @@ -65,6 +65,10 @@ log() { make_rel() { # shellcheck disable=SC1010 ./rebar3 as "$PROFILE" do release,tar + if [ "$(find "_build/$PROFILE/rel/emqx/lib/" -name 'gpb-*' -type d)" != "" ]; then + echo "gpb should not be included in the release" + exit 1 + fi } ## unzip previous version .zip files to _build/$PROFILE/rel/emqx/releases before making relup From 3be6374f91e8f2bf8cb655e2b8023c5e538c0a35 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 9 Sep 2021 10:53:59 +0800 Subject: [PATCH 286/306] fix(auth mnesia api): fix get file type error --- apps/emqx_authz/src/emqx_authz_api_sources.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 4be631560..df62a418a 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -304,7 +304,7 @@ sources(get, _) -> {ok, Rules} = file:consult(Path), lists:append(AccIn, [#{type => file, enable => true, - rules => [ io_lib:format("~p", [R])|| R <- Rules], + rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], annotations => #{status => healthy} }]); (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> @@ -372,7 +372,7 @@ source(get, #{bindings := #{type := Type}}) -> {ok, Rules} = file:consult(Path), {200, #{type => file, enable => true, - rules => Rules, + rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], annotations => #{status => healthy} } }; From 0813a81517ac73831eff100957e75dd47eaf5711 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 9 Sep 2021 11:51:46 +0800 Subject: [PATCH 287/306] fix(auth mnesia api): fix put file type error --- apps/emqx_authz/src/emqx_authz.erl | 1 + apps/emqx_authz/src/emqx_authz_api_sources.erl | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index af77390d5..e0e584806 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -198,6 +198,7 @@ post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources {Index, OldSource} = find_source_by_type(Type, OldInitedSources), case maps:get(type, OldSource, undefined) of undefined -> ok; + file -> ok; _ -> #{annotations := #{id := Id}} = OldSource, ok = emqx_resource:remove(Id) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index df62a418a..00d0a5b7a 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -304,7 +304,7 @@ sources(get, _) -> {ok, Rules} = file:consult(Path), lists:append(AccIn, [#{type => file, enable => true, - rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], + rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} }]); (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> @@ -372,7 +372,7 @@ source(get, #{bindings := #{type := Type}}) -> {ok, Rules} = file:consult(Path), {200, #{type => file, enable => true, - rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], + rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} } }; @@ -395,7 +395,7 @@ source(get, #{bindings := #{type := Type}}) -> end, {200, read_cert(NSource2)} end; -source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> +source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> {ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), erlang:list_to_bitstring([<> || Rule <- Rules]) ), From 718dd80b481e69ffb1638daf66f2051a41d67213 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 9 Sep 2021 14:19:37 +0800 Subject: [PATCH 288/306] fix(auth mnesia api): fix write file error --- apps/emqx_authz/src/emqx_authz_api_sources.erl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 00d0a5b7a..209bbc01f 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -298,15 +298,15 @@ move_source_api() -> {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, _) -> - Sources = lists:foldl(fun (#{enable := false} = Source, AccIn) -> - lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); - (#{type := file, path := Path}, AccIn) -> + Sources = lists:foldl(fun (#{type := file, enable := Enable, path := Path}, AccIn) -> {ok, Rules} = file:consult(Path), lists:append(AccIn, [#{type => file, - enable => true, + enable => Enable, rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} }]); + (#{enable := false} = Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; @@ -367,15 +367,15 @@ sources(put, #{body := Body}) when is_list(Body) -> source(get, #{bindings := #{type := Type}}) -> case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; - #{type := file, path := Path}-> + #{type := file, enable := Enable, path := Path}-> {ok, Rules} = file:consult(Path), {200, #{type => file, - enable => true, + enable => Enable, rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} } }; + #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; #{annotations := #{id := Id}} = Source -> NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; @@ -486,7 +486,7 @@ write_cert(Source) -> Source. write_file(Filename, Bytes) -> ok = filelib:ensure_dir(Filename), case file:write_file(Filename, Bytes) of - ok -> {ok, Filename}; + ok -> {ok, iolist_to_binary(Filename)}; {error, Reason} -> ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), error(Reason) From 60914697b2be6983e9b6b83abb58fc661ac56190 Mon Sep 17 00:00:00 2001 From: lafirest Date: Wed, 8 Sep 2021 17:59:53 +0800 Subject: [PATCH 289/306] refactor(emqx_lwm2m): refactor lwm2m api use new rest framework --- apps/emqx_gateway/src/coap/emqx_coap_api.erl | 8 +- .../emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl | 232 ++++++------- .../src/lwm2m/emqx_lwm2m_channel.erl | 10 +- .../emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl | 4 +- .../src/lwm2m/emqx_lwm2m_session.erl | 56 +++- .../emqx_gateway/test/emqx_coap_api_SUITE.erl | 24 -- .../test/emqx_lwm2m_api_SUITE.erl | 317 ++++++++++++++++++ 7 files changed, 485 insertions(+), 166 deletions(-) create mode 100644 apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl diff --git a/apps/emqx_gateway/src/coap/emqx_coap_api.erl b/apps/emqx_gateway/src/coap/emqx_coap_api.erl index 428e99ac5..4d0e8aff8 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_api.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_api.erl @@ -64,9 +64,9 @@ request(post, #{body := Body, bindings := Bindings}) -> case call_client(ClientId, Msg2, timer:seconds(WaitTime)) of timeout -> - {504}; + {504, #{code => 'CLIENT_NOT_RESPONSE'}}; not_found -> - {404}; + {404, #{code => 'CLIENT_NOT_FOUND'}}; Response -> {200, format_to_response(CT, Response)} end. @@ -101,8 +101,8 @@ request_method_meta() -> <<"request payload, binary must encode by base64">>), responses => #{ <<"200">> => object_schema(coap_message_properties()), - <<"404">> => schema(<<"NotFound">>), - <<"504">> => schema(<<"Timeout">>) + <<"404">> => error_schema("client not found error", ['CLIENT_NOT_FOUND']), + <<"504">> => error_schema("timeout", ['CLIENT_NOT_RESPONSE']) }}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl index 03c3a6bc2..98c9fabe8 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl @@ -16,143 +16,119 @@ -module(emqx_lwm2m_api). --rest_api(#{name => list, - method => 'GET', - path => "/lwm2m_channels/", - func => list, - descr => "A list of all lwm2m channel" - }). +-behaviour(minirest_api). --rest_api(#{name => list, - method => 'GET', - path => "/nodes/:atom:node/lwm2m_channels/", - func => list, - descr => "A list of lwm2m channel of a node" - }). +-export([api_spec/0]). --rest_api(#{name => lookup_cmd, - method => 'GET', - path => "/lookup_cmd/:bin:ep/", - func => lookup_cmd, - descr => "Send a lwm2m downlink command" - }). +-export([lookup_cmd/2]). --rest_api(#{name => lookup_cmd, - method => 'GET', - path => "/nodes/:atom:node/lookup_cmd/:bin:ep/", - func => lookup_cmd, - descr => "Send a lwm2m downlink command of a node" - }). +-define(PREFIX, "/gateway/lwm2m/:clientid"). --export([ list/2 - , lookup_cmd/2 - ]). +-import(emqx_mgmt_util, [ object_schema/1 + , error_schema/2 + , properties/1]). -list(#{node := Node }, Params) -> - case Node = node() of - true -> list(#{}, Params); - _ -> rpc_call(Node, list, [#{}, Params]) - end; +api_spec() -> + {[lookup_cmd_api()], []}. -list(#{}, _Params) -> - %% Channels = emqx_lwm2m_cm:all_channels(), - Channels = [], - return({ok, format(Channels)}). +lookup_cmd_paramters() -> + [ make_paramter(clientid, path, true, "string") + , make_paramter(path, query, true, "string") + , make_paramter(action, query, true, "string")]. -lookup_cmd(#{ep := Ep, node := Node}, Params) -> - case Node = node() of - true -> lookup_cmd(#{ep => Ep}, Params); - _ -> rpc_call(Node, lookup_cmd, [#{ep => Ep}, Params]) - end; +lookup_cmd_properties() -> + properties([ {clientid, string} + , {path, string} + , {action, string} + , {code, string} + , {codeMsg, string} + , {content, {array, object}, lookup_cmd_content_props()}]). -lookup_cmd(#{ep := _Ep}, Params) -> - _MsgType = proplists:get_value(<<"msgType">>, Params), - _Path0 = proplists:get_value(<<"path">>, Params), - %% case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of - %% [] -> return({ok, []}); - %% [{_, undefined} | _] -> return({ok, []}); - %% [{{IMEI, Path, MsgType}, undefined}] -> - %% return({ok, [{imei, IMEI}, - %% {'msgType', IMEI}, - %% {'code', <<"6.01">>}, - %% {'codeMsg', <<"reply_not_received">>}, - %% {'path', Path}]}); - %% [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> - %% Payload1 = format_cmd_content(Content, MsgType), - %% return({ok, [{imei, IMEI}, - %% {'msgType', IMEI}, - %% {'code', Code}, - %% {'codeMsg', CodeMsg}, - %% {'path', Path}] ++ Payload1}) - %% end. - return({ok, []}). +lookup_cmd_content_props() -> + [ {operations, string, <<"Resource Operations">>} + , {dataType, string, <<"Resource Type">>} + , {path, string, <<"Resource Path">>} + , {name, string, <<"Resource Name">>}]. -rpc_call(Node, Fun, Args) -> - case rpc:call(Node, ?MODULE, Fun, Args) of - {badrpc, Reason} -> {error, Reason}; - Res -> Res +lookup_cmd_api() -> + Metadata = #{get => + #{description => <<"look up resource">>, + parameters => lookup_cmd_paramters(), + responses => + #{<<"200">> => object_schema(lookup_cmd_properties()), + <<"404">> => error_schema("client not found error", ['CLIENT_NOT_FOUND']) + } + }}, + {?PREFIX ++ "/lookup_cmd", Metadata, lookup_cmd}. + + +lookup_cmd(get, #{bindings := Bindings, query_string := QS}) -> + ClientId = maps:get(clientid, Bindings), + case emqx_gateway_cm_registry:lookup_channels(lwm2m, ClientId) of + [Channel | _] -> + #{<<"path">> := Path, + <<"action">> := Action} = QS, + {ok, Result} = emqx_lwm2m_channel:lookup_cmd(Channel, Path, Action), + lookup_cmd_return(Result, ClientId, Action, Path); + _ -> + {404, #{code => 'CLIENT_NOT_FOUND'}} end. -format(Channels) -> - lists:map(fun({IMEI, #{lifetime := LifeTime, - peername := Peername, - version := Version, - reg_info := RegInfo}}) -> - ObjectList = lists:map(fun(Path) -> - [ObjId | _] = path_list(Path), - case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of - {error, _} -> - {Path, Path}; - ObjDefinition -> - ObjectName = emqx_lwm2m_xml_object:get_object_name(ObjDefinition), - {Path, list_to_binary(ObjectName)} - end - end, maps:get(<<"objectList">>, RegInfo)), - {IpAddr, Port} = Peername, - [{imei, IMEI}, - {lifetime, LifeTime}, - {ip_address, iolist_to_binary(ntoa(IpAddr))}, - {port, Port}, - {version, Version}, - {'objectList', ObjectList}] - end, Channels). +lookup_cmd_return(undefined, ClientId, Action, Path) -> + {200, + #{clientid => ClientId, + action => Action, + code => <<"6.01">>, + codeMsg => <<"reply_not_received">>, + path => Path}}; -%% format_cmd_content(undefined, _MsgType) -> []; -%% format_cmd_content(_Content, <<"discover">>) -> -%% %% [H | Content1] = Content, -%% %% {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), -%% %% [ObjId | _]= path_list(HObjId), -%% %% ObjectList = case Content1 of -%% %% [Content2 | _] -> -%% %% {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), -%% %% ObjL; -%% %% [] -> [] -%% %% end, -%% %% R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of -%% %% {error, _} -> -%% %% lists:map(fun(Object) -> {Object, Object} end, ObjectList); -%% %% ObjDefinition -> -%% %% lists:map(fun(Object) -> -%% %% [_, _, ResId| _] = path_list(Object), -%% %% Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of -%% %% "E" -> [{operations, list_to_binary("E")}]; -%% %% Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, -%% %% {operations, list_to_binary(Oper)}] -%% %% end, -%% %% [{path, Object}, -%% %% {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} -%% %% ] ++ Operations -%% %% end, ObjectList) -%% %% end, -%% %% [{content, R}]; -%% []; -%% format_cmd_content(Content, _) -> -%% [{content, Content}]. +lookup_cmd_return({Code, CodeMsg, Content}, ClientId, Action, Path) -> + {200, + format_cmd_content(Content, + Action, + #{clientid => ClientId, + action => Action, + code => Code, + codeMsg => CodeMsg, + path => Path})}. -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); -ntoa(IP) -> - inet_parse:ntoa(IP). +format_cmd_content(undefined, _MsgType, Result) -> + Result; + +format_cmd_content(Content, <<"discover">>, Result) -> + [H | Content1] = Content, + {_, [HObjId]} = emqx_lwm2m_session:parse_object_list(H), + [ObjId | _]= path_list(HObjId), + ObjectList = case Content1 of + [Content2 | _] -> + {_, ObjL} = emqx_lwm2m_session:parse_object_list(Content2), + ObjL; + [] -> [] + end, + + R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of + {error, _} -> + lists:map(fun(Object) -> #{Object => Object} end, ObjectList); + ObjDefinition -> + lists:map( + fun(Object) -> + [_, _, RawResId| _] = path_list(Object), + ResId = binary_to_integer(RawResId), + Operations = case emqx_lwm2m_xml_object:get_resource_operations(ResId, ObjDefinition) of + "E" -> + #{operations => list_to_binary("E")}; + Oper -> + #{'dataType' => list_to_binary(emqx_lwm2m_xml_object:get_resource_type(ResId, ObjDefinition)), + operations => list_to_binary(Oper)} + end, + Operations#{path => Object, + name => list_to_binary(emqx_lwm2m_xml_object:get_resource_name(ResId, ObjDefinition))} + end, ObjectList) + end, + Result#{content => R}; + +format_cmd_content(Content, _, Result) -> + Result#{content => Content}. path_list(Path) -> case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of @@ -162,6 +138,8 @@ path_list(Path) -> [ObjId] -> [ObjId] end. -return(_) -> -%% TODO: V5 API - ok. +make_paramter(Name, In, IsRequired, Type) -> + #{name => Name, + in => In, + required => IsRequired, + schema => #{type => Type}}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl index d0647897b..6ad78742f 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -25,7 +25,8 @@ , info/2 , stats/1 , with_context/2 - , do_takeover/3]). + , do_takeover/3 + , lookup_cmd/3]). -export([ init/2 , handle_in/2 @@ -116,6 +117,9 @@ with_context(Ctx, ClientInfo) -> with_context(Type, Topic, Ctx, ClientInfo) end. +lookup_cmd(Channel, Path, Action) -> + gen_server:call(Channel, {?FUNCTION_NAME, Path, Action}). + %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- @@ -150,6 +154,10 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- +handle_call({lookup_cmd, Path, Type}, _From, #channel{session = Session} = Channel) -> + Result = emqx_lwm2m_session:find_cmd_record(Path, Type, Session), + {reply, {ok, Result}, Channel}; + handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl index 7c0cc95cd..e17a83195 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -341,8 +341,8 @@ extract_path(Ref = #{}) -> drop_query( case Ref of #{<<"data">> := Data} -> - case maps:get(<<"path">>, Data, nil) of - nil -> maps:get(<<"basePath">>, Data, undefined); + case maps:get(<<"path">>, Data, undefined) of + undefined -> maps:get(<<"basePath">>, Data, undefined); Path -> Path end; #{<<"path">> := Path} -> diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl index a1d03e04f..ab27dfbca 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -22,7 +22,8 @@ -include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). %% API --export([new/0, init/4, update/3, reregister/3, on_close/1]). +-export([ new/0, init/4, update/3, parse_object_list/1 + , reregister/3, on_close/1, find_cmd_record/3]). -export([ info/1 , info/2 @@ -42,6 +43,15 @@ -type timestamp() :: non_neg_integer(). -type queued_request() :: {timestamp(), request_context(), emqx_coap_message()}. +-type cmd_path() :: binary(). +-type cmd_type() :: binary(). +-type cmd_record_key() :: {cmd_path(), cmd_type()}. +-type cmd_code() :: binary(). +-type cmd_code_msg() :: binary(). +-type cmd_code_content() :: list(map()). +-type cmd_result() :: undefined | {cmd_code(), cmd_code_msg(), cmd_code_content()}. +-type cmd_record() :: #{cmd_record_key() => cmd_result()}. + -record(session, { coap :: emqx_coap_tm:manager() , queue :: queue:queue(queued_request()) , wait_ack :: request_context() | undefined @@ -52,6 +62,7 @@ , is_cache_mode :: boolean() , mountpoint :: binary() , last_active_at :: non_neg_integer() + , cmd_record :: cmd_record() }). -type session() :: #session{}. @@ -61,6 +72,8 @@ -define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"9">>, <<"15">>]). +-define(CMD_KEY(Path, Type), {Path, Type}). + %% uplink and downlink topic configuration -define(lwm2m_up_dm_topic, {<<"/v1/up/dm">>, 0}). @@ -98,6 +111,7 @@ new() -> , last_active_at = ?NOW , is_cache_mode = false , mountpoint = <<>> + , cmd_record = #{} , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. -spec init(emqx_coap_message(), binary(), function(), session()) -> map(). @@ -135,6 +149,10 @@ on_close(Session) -> emqx:unsubscribe(MountedTopic), MountedTopic. +-spec find_cmd_record(cmd_path(), cmd_type(), session()) -> cmd_result(). +find_cmd_record(Path, Type, #session{cmd_record = Record}) -> + maps:get(?CMD_KEY(Path, Type), Record, undefined). + %%-------------------------------------------------------------------- %% Info, Stats %%-------------------------------------------------------------------- @@ -271,7 +289,7 @@ parse_object_list(FullObjLinkList) -> (<>) when Prefix =:= AlterPath -> trim(Link); (Link) -> Link - end, ObjLinkList), + end, ObjLinkList), {AlterPath, WithOutPrefix} end. @@ -443,19 +461,20 @@ handle_coap_response({Ctx = #{<<"msgType">> := EventType}, Session) -> MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), - Session2 = + Session2 = record_response(EventType, MqttPayload, Session), + Session3 = case {ReqPath, MqttPayload, EventType, CoapMsgType} of {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is a notification for status update during NB firmware upgrade. %% need to reply to DM http callbacks - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session2); {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is actually a notification, correct the msgType - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session2); _ -> - send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session) + send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session2) end, - send_dl_msg(Ctx, Session2). + send_dl_msg(Ctx, Session3). %%-------------------------------------------------------------------- %% Ack @@ -624,7 +643,8 @@ deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) WithContext(metrics, 'messages.delivered'), {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), ExpiryTime = get_expiry_time(MQTT), - maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session). + Session2 = record_request(Ctx, Session), + maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session2). maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, #session{wait_ack = WaitAck, @@ -692,3 +712,23 @@ do_out([{Ctx, Out} | T], TM, Msgs) -> do_out(_, TM, Msgs) -> {ok, TM, Msgs}. + + +%%-------------------------------------------------------------------- +%% CMD Record +%%-------------------------------------------------------------------- +-spec record_request(request_context(), session()) -> session(). +record_request(#{<<"msgType">> := Type} = Context, Session) -> + Path = emqx_lwm2m_cmd:extract_path(Context), + record_cmd(Path, Type, undefined, Session). + +record_response(EventType, #{<<"data">> := Data}, Session) -> + ReqPath = maps:get(<<"reqPath">>, Data, undefined), + Code = maps:get(<<"code">>, Data, undefined), + CodeMsg = maps:get(<<"codeMsg">>, Data, undefined), + Content = maps:get(<<"content">>, Data, undefined), + record_cmd(ReqPath, EventType, {Code, CodeMsg, Content}, Session). + +record_cmd(Path, Type, Result, #session{cmd_record = Record} = Session) -> + Record2 = Record#{?CMD_KEY(Path, Type) => Result}, + Session#session{cmd_record = Record2}. diff --git a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl index 74b0cadc8..83521f5cd 100644 --- a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl @@ -62,12 +62,6 @@ end_per_suite(Config) -> emqx_mgmt_api_test_util:end_suite([emqx_gateway]), Config. -set_special_configs(emqx_gatewway) -> - ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT); - -set_special_configs(_) -> - ok. - %%-------------------------------------------------------------------- %% Cases %%-------------------------------------------------------------------- @@ -187,17 +181,6 @@ split_segments(Path, Char, Acc) -> make_segment(Seg) -> list_to_binary(emqx_http_lib:uri_decode(Seg)). - -get_coap_path(Options) -> - get_path(Options, <<>>). - -get_coap_query(Options) -> - proplists:get_value(uri_query, Options, []). - -get_coap_observe(Options) -> - get_observe(Options). - - get_path([], Acc) -> %?LOGT("get_path Acc=~p", [Acc]), Acc; @@ -207,13 +190,6 @@ get_path([{uri_path, Path1}|T], Acc) -> get_path([{_, _}|T], Acc) -> get_path(T, Acc). -get_observe([]) -> - undefined; -get_observe([{observe, V}|_T]) -> - V; -get_observe([{_, _}|T]) -> - get_observe(T). - join_path([], Acc) -> Acc; join_path([<<"/">>|T], Acc) -> join_path(T, Acc); diff --git a/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl new file mode 100644 index 000000000..cb2ccf3f8 --- /dev/null +++ b/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl @@ -0,0 +1,317 @@ +%%-------------------------------------------------------------------- +%% 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_lwm2m_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-define(PORT, 5783). + +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("lwm2m_coap/include/coap.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +gateway.lwm2m { + xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + mountpoint = \"lwm2m/%u\" + update_msg_publish_condition = contains_object_list + translators { + command = {topic = \"/dn/#\", qos = 0} + response = {topic = \"/up/resp\", qos = 0} + notify = {topic = \"/up/notify\", qos = 0} + register = {topic = \"/up/resp\", qos = 0} + update = {topic = \"/up/resp\", qos = 0} + } + listeners.udp.default { + bind = 5783 + } +} +">>). + +-define(assertExists(Map, Key), + ?assertNotEqual(maps:get(Key, Map, undefined), undefined)). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + emqx_mgmt_api_test_util:init_suite([emqx_gateway]), + Config. + +end_per_suite(Config) -> + timer:sleep(300), + emqx_mgmt_api_test_util:end_suite([emqx_gateway]), + Config. + +init_per_testcase(_AllTestCase, Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + {ok, _} = application:ensure_all_started(emqx_gateway), + {ok, ClientUdpSock} = gen_udp:open(0, [binary, {active, false}]), + + {ok, C} = emqtt:start_link([{host, "localhost"},{port, 1883},{clientid, <<"c1">>}]), + {ok, _} = emqtt:connect(C), + timer:sleep(100), + + [{sock, ClientUdpSock}, {emqx_c, C} | Config]. + +end_per_testcase(_AllTestCase, Config) -> + timer:sleep(300), + gen_udp:close(?config(sock, Config)), + emqtt:disconnect(?config(emqx_c, Config)), + ok = application:stop(emqx_gateway). + +%%-------------------------------------------------------------------- +%% Cases +%%-------------------------------------------------------------------- +t_lookup_cmd_read(Config) -> + UdpSock = ?config(sock, Config), + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + %% step 1, device register ... + test_send_coap_request( UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{content_format = <<"text/plain">>, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + [], + MsgId1), + #coap_message{method = Method1} = test_recv_coap_response(UdpSock), + ?assertEqual({ok,created}, Method1), + test_recv_mqtt_response(RespTopic), + + %% step2, send a READ command to device + CmdId = 206, + CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, + Command = #{ + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, + CommandJson = emqx_json:encode(Command), + ?LOGT("CommandJson=~p", [CommandJson]), + test_mqtt_broker:publish(CommandTopic, CommandJson, 0), + timer:sleep(50), + + no_received_request(Epn, <<"/3/0/0">>, <<"read">>), + + Request2 = test_recv_coap_request(UdpSock), + ?LOGT("LwM2M client got ~p", [Request2]), + timer:sleep(50), + + test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, content}, #coap_content{content_format = <<"text/plain">>, payload = <<"EMQ">>}, Request2, true), + timer:sleep(100), + + normal_received_request(Epn, <<"/3/0/0">>, <<"read">>). + +t_lookup_cmd_discover(Config) -> + %% step 1, device register ... + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + UdpSock = ?config(sock, Config), + ObjectList = <<", , , , ">>, + RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + + std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), + + %% step2, send a WRITE command to device + CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, + CmdId = 307, + Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"discover">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/7">> + } }, + CommandJson = emqx_json:encode(Command), + test_mqtt_broker:publish(CommandTopic, CommandJson, 0), + + no_received_request(Epn, <<"/3/0/7">>, <<"discover">>), + + timer:sleep(50), + Request2 = test_recv_coap_request(UdpSock), + timer:sleep(50), + + PayloadDiscover = <<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2,">>, + test_send_coap_response(UdpSock, + "127.0.0.1", + ?PORT, + {ok, content}, + #coap_content{content_format = <<"application/link-format">>, payload = PayloadDiscover}, + Request2, + true), + timer:sleep(100), + discover_received_request(Epn, <<"/3/0/7">>, <<"discover">>). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal Functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +send_request(ClientId, Path, Action) -> + ApiPath = emqx_mgmt_api_test_util:api_path(["gateway/lwm2m", ClientId, "lookup_cmd"]), + Auth = emqx_mgmt_api_test_util:auth_header_(), + Query = io_lib:format("path=~s&action=~s", [Path, Action]), + {ok, Response} = emqx_mgmt_api_test_util:request_api(get, ApiPath, Query, Auth), + ?LOGT("rest api response:~s~n", [Response]), + Response. + +no_received_request(ClientId, Path, Action) -> + Response = send_request(ClientId, Path, Action), + NotReceived = #{<<"clientid">> => list_to_binary(ClientId), + <<"action">> => Action, + <<"code">> => <<"6.01">>, + <<"codeMsg">> => <<"reply_not_received">>, + <<"path">> => Path}, + ?assertEqual(NotReceived, emqx_json:decode(Response, [return_maps])). +normal_received_request(ClientId, Path, Action) -> + Response = send_request(ClientId, Path, Action), + RCont = emqx_json:decode(Response, [return_maps]), + ?assertEqual(list_to_binary(ClientId), maps:get(<<"clientid">>, RCont, undefined)), + ?assertEqual(Path, maps:get(<<"path">>, RCont, undefined)), + ?assertEqual(Action, maps:get(<<"action">>, RCont, undefined)), + ?assertExists(RCont, <<"code">>), + ?assertExists(RCont, <<"codeMsg">>), + ?assertExists(RCont, <<"content">>), + RCont. + +discover_received_request(ClientId, Path, Action) -> + RCont = normal_received_request(ClientId, Path, Action), + [Res | _] = maps:get(<<"content">>, RCont), + ?assertExists(Res, <<"path">>), + ?assertExists(Res, <<"name">>), + ?assertExists(Res, <<"operations">>). + +test_recv_mqtt_response(RespTopic) -> + receive + {publish, #{topic := RespTopic, payload := RM}} -> + ?LOGT("test_recv_mqtt_response Response=~p", [RM]), + RM + after 1000 -> timeout_test_recv_mqtt_response + end. + +test_send_coap_request(UdpSock, Method, Uri, Content, Options, MsgId) -> + is_record(Content, coap_content) orelse error("Content must be a #coap_content!"), + is_list(Options) orelse error("Options must be a list"), + case resolve_uri(Uri) of + {coap, {IpAddr, Port}, Path, Query} -> + Request0 = lwm2m_coap_message:request(con, Method, Content, [{uri_path, Path}, {uri_query, Query} | Options]), + Request = Request0#coap_message{id = MsgId}, + ?LOGT("send_coap_request Request=~p", [Request]), + RequestBinary = lwm2m_coap_message_parser:encode(Request), + ?LOGT("test udp socket send to ~p:~p, data=~p", [IpAddr, Port, RequestBinary]), + ok = gen_udp:send(UdpSock, IpAddr, Port, RequestBinary); + {SchemeDiff, ChIdDiff, _, _} -> + error(lists:flatten(io_lib:format("scheme ~s or ChId ~s does not match with socket", [SchemeDiff, ChIdDiff]))) + end. + +test_recv_coap_response(UdpSock) -> + {ok, {Address, Port, Packet}} = gen_udp:recv(UdpSock, 0, 2000), + Response = lwm2m_coap_message_parser:decode(Packet), + ?LOGT("test udp receive from ~p:~p, data1=~p, Response=~p", [Address, Port, Packet, Response]), + #coap_message{type = ack, method = Method, id=Id, token = Token, options = Options, payload = Payload} = Response, + ?LOGT("receive coap response Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Response. + +test_recv_coap_request(UdpSock) -> + case gen_udp:recv(UdpSock, 0, 2000) of + {ok, {_Address, _Port, Packet}} -> + Request = lwm2m_coap_message_parser:decode(Packet), + #coap_message{type = con, method = Method, id=Id, token = Token, payload = Payload, options = Options} = Request, + ?LOGT("receive coap request Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Request; + {error, Reason} -> + ?LOGT("test_recv_coap_request failed, Reason=~p", [Reason]), + timeout_test_recv_coap_request + end. + +test_send_coap_response(UdpSock, Host, Port, Code, Content, Request, Ack) -> + is_record(Content, coap_content) orelse error("Content must be a #coap_content!"), + is_list(Host) orelse error("Host is not a string"), + + {ok, IpAddr} = inet:getaddr(Host, inet), + Response = lwm2m_coap_message:response(Code, Content, Request), + Response2 = case Ack of + true -> Response#coap_message{type = ack}; + false -> Response + end, + ?LOGT("test_send_coap_response Response=~p", [Response2]), + ok = gen_udp:send(UdpSock, IpAddr, Port, lwm2m_coap_message_parser:encode(Response2)). + +std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic) -> + test_send_coap_request( UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{content_format = <<"text/plain">>, payload = ObjectList}, + [], + MsgId1), + #coap_message{method = {ok,created}} = test_recv_coap_response(UdpSock), + test_recv_mqtt_response(RespTopic), + timer:sleep(100). + +resolve_uri(Uri) -> + {ok, #{scheme := Scheme, + host := Host, + port := PortNo, + path := Path} = URIMap} = emqx_http_lib:uri_parse(Uri), + Query = maps:get(query, URIMap, ""), + {ok, PeerIP} = inet:getaddr(Host, inet), + {Scheme, {PeerIP, PortNo}, split_path(Path), split_query(Query)}. + +split_path([]) -> []; +split_path([$/]) -> []; +split_path([$/ | Path]) -> split_segments(Path, $/, []). + +split_query([]) -> []; +split_query(Path) -> split_segments(Path, $&, []). + +split_segments(Path, Char, Acc) -> + case string:rchr(Path, Char) of + 0 -> + [make_segment(Path) | Acc]; + N when N > 0 -> + split_segments(string:substr(Path, 1, N-1), Char, + [make_segment(string:substr(Path, N+1)) | Acc]) + end. + +make_segment(Seg) -> + list_to_binary(emqx_http_lib:uri_decode(Seg)). + +join_path([], Acc) -> Acc; +join_path([<<"/">>|T], Acc) -> + join_path(T, Acc); +join_path([H|T], Acc) -> + join_path(T, <>). + +sprintf(Format, Args) -> + lists:flatten(io_lib:format(Format, Args)). From e94e09075c2069f1cea6dea6cdf8afa808be123b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 7 Sep 2021 16:17:58 +0800 Subject: [PATCH 290/306] refactor(gw): keep listeners conf tree compliance with core --- apps/emqx_gateway/etc/emqx_gateway.conf | 50 +++++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 166 +++++++++--------- apps/emqx_gateway/src/emqx_gateway_utils.erl | 37 +++- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 67 +++---- 4 files changed, 198 insertions(+), 122 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 5212d319f..01fb9f316 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -42,7 +42,32 @@ gateway.stomp { acceptors = 16 max_connections = 1024000 max_conn_rate = 1000 - active_n = 100 + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.active_n = 100 + tcp.backlog = 1024 + tcp.buffer = 4KB + } + + listeners.ssl.default { + bind = 61614 + acceptors = 16 + max_connections = 1024000 + max_conn_rate = 1000 + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.active_n = 100 + tcp.backlog = 1024 + tcp.buffer = 4KB + + ## SSL options + ## See ${example_common_ssl_options} for more information + 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" } } @@ -68,6 +93,29 @@ gateway.coap { listeners.udp.default { bind = 5683 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + } + listeners.dtls.default { + bind = 5684 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + + ## DTLS Options + ## See #{example_common_dtls_options} for more information + dtls.versions = ["dtlsv1"] } } diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index c3e241389..8c3663358 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -63,9 +63,9 @@ fields(stomp_structs) -> ] ++ gateway_common_options(); fields(stomp_frame) -> - [ {max_headers, sc(integer(), undefined, 10)} - , {max_headers_length, sc(integer(), undefined, 1024)} - , {max_body_length, sc(integer(), undefined, 8192)} + [ {max_headers, sc(integer(), 10)} + , {max_headers_length, sc(integer(), 1024)} + , {max_body_length, sc(integer(), 8192)} ]; fields(mqttsn_structs) -> @@ -82,11 +82,11 @@ fields(mqttsn_predefined) -> ]; fields(coap_structs) -> - [ {heartbeat, sc(duration(), undefined, <<"30s">>)} - , {connection_required, sc(boolean(), undefined, false)} - , {notify_type, sc(union([non, con, qos]), undefined, qos)} - , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {publish_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} + [ {heartbeat, sc(duration(), <<"30s">>)} + , {connection_required, sc(boolean(), false)} + , {notify_type, sc(union([non, con, qos]), qos)} + , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), coap)} + , {publish_qos, sc(union([qos0, qos1, qos2, coap]), coap)} , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); @@ -166,58 +166,53 @@ fields(udp_listener) -> fields(dtls_listener) -> [ {"$name", sc(ref(dtls_listener_settings))}]; -fields(listener_settings) -> - [ {enable, sc(boolean(), undefined, true)} - , {bind, sc(union(ip_port(), integer()))} - , {acceptors, sc(integer(), undefined, 8)} - , {max_connections, sc(integer(), undefined, 1024)} - , {max_conn_rate, sc(integer())} - , {active_n, sc(integer(), undefined, 100)} - %, {rate_limit, sc(comma_separated_list())} - , {access, sc(ref(access))} - , {proxy_protocol, sc(boolean())} - , {proxy_protocol_timeout, sc(duration())} - , {backlog, sc(integer(), undefined, 1024)} - , {send_timeout, sc(duration(), undefined, <<"15s">>)} - , {send_timeout_close, sc(boolean(), undefined, true)} - , {recbuf, sc(bytesize())} - , {sndbuf, sc(bytesize())} - , {buffer, sc(bytesize())} - , {high_watermark, sc(bytesize(), undefined, <<"1MB">>)} - , {tune_buffer, sc(boolean())} - , {nodelay, sc(boolean())} - , {reuseaddr, sc(boolean())} - ]; - fields(tcp_listener_settings) -> [ %% some special confs for tcp listener - ] ++ fields(listener_settings); + ] ++ tcp_opts() + ++ proxy_protocol_opts() + ++ common_listener_opts(); fields(ssl_listener_settings) -> [ %% some special confs for ssl listener - ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ] ++ tcp_opts() + ++ ssl_opts() + ++ proxy_protocol_opts() + ++ common_listener_opts(); fields(udp_listener_settings) -> [ %% some special confs for udp listener - ] ++ fields(listener_settings); + ] ++ udp_opts() + ++ common_listener_opts(); fields(dtls_listener_settings) -> [ %% some special confs for dtls listener - ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ] ++ udp_opts() + ++ dtls_opts() + ++ common_listener_opts(); -fields(access) -> - [ {"$id", #{type => binary(), - nullable => true}}]; +fields(udp_opts) -> + [ {active_n, sc(integer(), 100)} + , {recbuf, sc(bytesize())} + , {sndbuf, sc(bytesize())} + , {buffer, sc(bytesize())} + , {reuseaddr, sc(boolean(), true)} + ]; + +fields(dtls_listener_ssl_opts) -> + Base = emqx_schema:fields("listener_ssl_opts"), + %% XXX: ciphers ??? + DtlsVers = hoconsc:mk( + typerefl:alias("string", list(atom())), + #{ default => default_dtls_vsns(), + converter => fun (Vsns) -> + [dtls_vsn(iolist_to_binary(V)) || V <- Vsns] + end + }), + lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}); fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), @@ -244,14 +239,47 @@ fields(ExtraField) -> % ]). gateway_common_options() -> - [ {enable, sc(boolean(), undefined, true)} - , {enable_stats, sc(boolean(), undefined, true)} - , {idle_timeout, sc(duration(), undefined, <<"30s">>)} + [ {enable, sc(boolean(), true)} + , {enable_stats, sc(boolean(), true)} + , {idle_timeout, sc(duration(), <<"30s">>)} , {mountpoint, sc(binary())} , {clientinfo_override, sc(ref(clientinfo_override))} , {authentication, sc(hoconsc:lazy(map()))} ]. +common_listener_opts() -> + [ {enable, sc(boolean(), true)} + , {bind, sc(union(ip_port(), integer()))} + , {acceptors, sc(integer(), 16)} + , {max_connections, sc(integer(), 1024)} + , {max_conn_rate, sc(integer())} + %, {rate_limit, sc(comma_separated_list())} + , {access_rules, sc(hoconsc:array(string()), [])} + ]. + +tcp_opts() -> + [{tcp, sc(ref(emqx_schema, "tcp_opts"), #{})}]. + +udp_opts() -> + [{udp, sc(ref(udp_opts), #{})}]. + +ssl_opts() -> + [{ssl, sc(ref(emqx_schema, "listener_ssl_opts"), #{})}]. + +dtls_opts() -> + [{dtls, sc(ref(dtls_listener_ssl_opts), #{})}]. + +proxy_protocol_opts() -> + [ {proxy_protocol, sc(boolean())} + , {proxy_protocol_timeout, sc(duration())} + ]. + +default_dtls_vsns() -> + [<<"dtlsv1.2">>, <<"dtlsv1">>]. + +dtls_vsn(<<"dtlsv1.2">>) -> 'dtlsv1.2'; +dtls_vsn(<<"dtlsv1">>) -> 'dtlsv1'. + %%-------------------------------------------------------------------- %% Helpers @@ -259,47 +287,11 @@ gateway_common_options() -> sc(Type) -> #{type => Type}. -sc(Type, Mapping, Default) -> - hoconsc:mk(Type, #{mapping => Mapping, default => Default}). +sc(Type, Default) -> + hoconsc:mk(Type, #{default => Default}). ref(Field) -> hoconsc:ref(?MODULE, Field). -%% utils - -%% generate a ssl field. -%% ssl("emqx", #{"verify" => verify_peer}) will return -%% [ {"cacertfile", sc(string(), "emqx.cacertfile", undefined)} -%% , {"certfile", sc(string(), "emqx.certfile", undefined)} -%% , {"keyfile", sc(string(), "emqx.keyfile", undefined)} -%% , {"verify", sc(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", sc(boolean(), M("enable"), D("enable"))} - , {"cacertfile", sc(binary(), M("cacertfile"), D("cacertfile"))} - , {"certfile", sc(binary(), M("certfile"), D("certfile"))} - , {"keyfile", sc(binary(), M("keyfile"), D("keyfile"))} - , {"verify", sc(union(verify_peer, verify_none), M("verify"), D("verify"))} - , {"fail_if_no_peer_cert", sc(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", sc(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} - , {"reuse_sessions", sc(boolean(), M("reuse_sessions"), D("reuse_sessions"))} - , {"honor_cipher_order", sc(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} - , {"handshake_timeout", sc(duration(), M("handshake_timeout"), D("handshake_timeout"))} - , {"depth", sc(integer(), M("depth"), D("depth"))} - , {"password", hoconsc:mk(binary(), #{ mapping => M("key_password") - , default => D("key_password") - , sensitive => true - })} - , {"dhfile", sc(binary(), M("dhfile"), D("dhfile"))} - , {"server_name_indication", sc(union(disable, binary()), M("server_name_indication"), - D("server_name_indication"))} - , {"tls_versions", sc(comma_separated_list(), M("tls_versions"), D("tls_versions"))} - , {"ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))} - , {"psk_ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))}]. +ref(Mod, Field) -> + hoconsc:ref(Mod, Field). diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 2b4e9f0a2..27e64bed3 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -143,16 +143,47 @@ normalize_config(RawConf) -> Listeners = maps:fold(fun(Name, Confs, AccIn2) -> ListenOn = maps:get(bind, Confs), - SocketOpts = esockd:parse_opt(maps:to_list(Confs)), + SocketOpts = esockd_opts(Type, Confs), RemainCfgs = maps:without( - [bind] ++ proplists:get_keys(SocketOpts), - Confs), + [bind, tcp, ssl, udp, dtls] + ++ proplists:get_keys(SocketOpts), Confs), Cfg = maps:merge(Cfg0, RemainCfgs), [{Type, Name, ListenOn, SocketOpts, Cfg}|AccIn2] end, [], Liss), [Listeners|AccIn1] end, [], LisMap)). +esockd_opts(Type, Opts0) -> + Opts1 = maps:with([acceptors, max_connections, max_conn_rate, + proxy_protocol, proxy_protocol_timeout], Opts0), + Opts2 = Opts1#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))}, + maps:to_list(case Type of + tcp -> Opts2#{tcp_options => sock_opts(tcp, Opts0)}; + ssl -> Opts2#{tcp_options => sock_opts(tcp, Opts0), + ssl_options => ssl_opts(ssl, Opts0)}; + udp -> Opts2#{udp_options => sock_opts(udp, Opts0)}; + dtls -> Opts2#{udp_options => sock_opts(udp, Opts0), + dtls_options => ssl_opts(dtls, Opts0)} + end). + +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]. + +ssl_opts(Name, Opts) -> + maps:to_list( + emqx_tls_lib:drop_tls13_for_old_otp( + maps:without([enable], + maps:get(Name, Opts, #{})))). + +sock_opts(Name, Opts) -> + maps:to_list( + maps:without([active_n], + maps:get(Name, Opts, #{}))). + %%-------------------------------------------------------------------- %% Envs diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 4ab91da5d..b91cd03b9 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -76,7 +76,7 @@ set_special_cfg(_App) -> listener_confs(Type) -> Default = #{bind => 7993, acceptors => 8}, - #{Type => #{'1' => maps:merge(Default, maps:from_list(socketopts(Type)))}}. + #{Type => #{'default' => maps:merge(Default, socketopts(Type))}}. %%-------------------------------------------------------------------- %% Tests cases @@ -360,11 +360,11 @@ open(udp) -> {ok, Sock} = gen_udp:open(0, ?TCPOPTS), {udp, Sock}; open(ssl) -> - SslOpts = client_ssl_opts(), + SslOpts = maps:to_list(client_ssl_opts()), {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?TCPOPTS ++ SslOpts), {ssl, SslSock}; open(dtls) -> - SslOpts = client_ssl_opts(), + SslOpts = maps:to_list(client_ssl_opts()), {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?DTLSOPTS ++ SslOpts), {dtls, SslSock}. @@ -400,51 +400,56 @@ close({dtls, Sock}) -> %% Server-Opts socketopts(tcp) -> - [{tcp_options, tcp_opts()}]; + #{tcp => tcp_opts()}; socketopts(ssl) -> - [{tcp_options, tcp_opts()}, - {ssl_options, ssl_opts()}]; + #{tcp => tcp_opts(), + ssl => ssl_opts()}; socketopts(udp) -> - [{udp_options, udp_opts()}]; + #{udp => udp_opts()}; socketopts(dtls) -> - [{udp_options, udp_opts()}, - {dtls_options, dtls_opts()}]. + #{udp => udp_opts(), + dtls => dtls_opts()}. tcp_opts() -> - [{send_timeout, 15000}, - {send_timeout_close, true}, - {backlog, 100}, - {nodelay, true} | udp_opts()]. + maps:merge( + udp_opts(), + #{send_timeout => 15000, + send_timeout_close => true, + backlog => 100, + nodelay => true} + ). udp_opts() -> - [{recbuf, 1024}, - {sndbuf, 1024}, - {buffer, 1024}, - {reuseaddr, true}]. + #{recbuf => 1024, + sndbuf => 1024, + buffer => 1024, + reuseaddr => true}. ssl_opts() -> Certs = certs("key.pem", "cert.pem", "cacert.pem"), - [{versions, emqx_tls_lib:default_versions()}, - {ciphers, emqx_tls_lib:default_ciphers()}, - {verify, verify_peer}, - {fail_if_no_peer_cert, true}, - {secure_renegotiate, false}, - {reuse_sessions, true}, - {honor_cipher_order, true}]++Certs. + maps:merge( + Certs, + #{versions => emqx_tls_lib:default_versions(), + ciphers => emqx_tls_lib:default_ciphers(), + verify => verify_peer, + fail_if_no_peer_cert => true, + secure_renegotiate => false, + reuse_sessions => true, + honor_cipher_order => true} + ). dtls_opts() -> - Opts = ssl_opts(), - lists:keyreplace(versions, 1, Opts, {versions, ['dtlsv1.2', 'dtlsv1']}). + maps:merge(ssl_opts(), #{versions => ['dtlsv1.2', 'dtlsv1']}). %%-------------------------------------------------------------------- %% Client-Opts client_ssl_opts() -> - certs( "client-key.pem", "client-cert.pem", "cacert.pem" ). + certs("client-key.pem", "client-cert.pem", "cacert.pem"). -certs( Key, Cert, CACert ) -> +certs(Key, Cert, CACert) -> CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"), - [ { keyfile, filename:join([ CertsPath, Key ]) }, - { certfile, filename:join([ CertsPath, Cert ]) }, - { cacertfile, filename:join([ CertsPath, CACert ]) } ]. + #{keyfile => filename:join([ CertsPath, Key ]), + certfile => filename:join([ CertsPath, Cert ]), + cacertfile => filename:join([ CertsPath, CACert])}. From 0453702ce5bd5f88e306925ba0c4461cbc405d10 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 8 Sep 2021 18:59:59 +0800 Subject: [PATCH 291/306] refactor(gw): improve http-api return structure --- apps/emqx_gateway/etc/emqx_gateway.conf | 17 ++++ apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 7 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 94 ++++++++++++------- apps/emqx_gateway/src/emqx_gateway_http.erl | 86 ++++++++++++++--- apps/emqx_gateway/src/emqx_gateway_schema.erl | 35 ++++++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 35 +++++++ .../src/exproto/emqx_exproto_impl.erl | 7 +- .../src/lwm2m/emqx_lwm2m_impl.erl | 8 +- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 7 +- .../src/stomp/emqx_stomp_impl.erl | 7 +- 10 files changed, 228 insertions(+), 75 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 01fb9f316..9f558b761 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -43,6 +43,10 @@ gateway.stomp { max_connections = 1024000 max_conn_rate = 1000 + access_rules = [ + "allow all" + ] + ## TCP options ## See ${example_common_tcp_options} for more information tcp.active_n = 100 @@ -68,6 +72,16 @@ gateway.stomp { ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + #ssl.verify = verify_none + #ssl.fail_if_no_peer_cert = false + #ssl.server_name_indication = disable + #ssl.secure_renegotiate = false + #ssl.reuse_sessions = false + #ssl.honor_cipher_order = false + #ssl.handshake_timeout = 15s + #ssl.depth = 10 + #ssl.password = foo + #ssl.dhfile = path-to-your-file } } @@ -116,6 +130,9 @@ gateway.coap { ## DTLS Options ## See #{example_common_dtls_options} for more information dtls.versions = ["dtlsv1"] + dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" } } diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index b0714f5e9..69015788a 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -99,7 +99,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_coap_frame, @@ -114,9 +114,6 @@ do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), @@ -130,5 +127,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 9c9398945..f264339a4 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -66,18 +66,21 @@ gateway_insta(get, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), case emqx_gateway:lookup(Name) of #{config := _Config} -> - %% FIXME: Got the parsed config, but we should return rawconfig to - %% frontend - RawConf = emqx_config:fill_defaults( - emqx_config:get_root_raw([<<"gateway">>]) - ), - {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; + GwCfs = filled_raw_confs([<<"gateway">>, Name0]), + NGwCfs = GwCfs#{<<"listeners">> => + emqx_gateway_http:mapping_listener_m2l( + Name0, maps:get(<<"listeners">>, GwCfs, #{}) + ) + }, + {200, NGwCfs}; undefined -> return_http_error(404, <<"Gateway not found">>) end; -gateway_insta(put, #{body := RawConfsIn, +gateway_insta(put, #{body := RawConfsIn0, bindings := #{name := Name} }) -> + RawConfsIn = maps:without([<<"authentication">>, + <<"listeners">>], RawConfsIn0), %% FIXME: Cluster Consistence ?? case emqx_gateway:update_rawconf(Name, RawConfsIn) of ok -> @@ -91,6 +94,12 @@ gateway_insta(put, #{body := RawConfsIn, gateway_insta_stats(get, _Req) -> return_http_error(401, <<"Implement it later (maybe 5.1)">>). +filled_raw_confs(Path) -> + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw(Path) + ), + Confs = emqx_map_lib:deep_get(Path, RawConf), + emqx_map_lib:jsonable_map(Confs). %%-------------------------------------------------------------------- %% Swagger defines @@ -199,8 +208,13 @@ schema_gateway_overview_list() -> <<"enable">> => true, <<"enable_stats">> => true,<<"heartbeat">> => <<"30s">>, <<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"udp">> => #{<<"default">> => #{<<"bind">> => 5683}}}, + <<"listeners">> => [ + #{<<"id">> => <<"coap:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"acceptors">> => 8,<<"bind">> => 5683, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}], <<"mountpoint">> => <<>>,<<"notify_type">> => <<"qos">>, <<"publish_qos">> => <<"qos1">>, <<"subscribe_qos">> => <<"qos0">>} @@ -212,12 +226,13 @@ schema_gateway_overview_list() -> <<"handler">> => #{<<"address">> => <<"http://127.0.0.1:9001">>}, <<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"tcp">> => - #{<<"default">> => - #{<<"acceptors">> => 8,<<"bind">> => 7993, - <<"max_conn_rate">> => 1000, - <<"max_connections">> => 10240}}}, + <<"listeners">> => [ + #{<<"id">> => <<"exproto:tcp:default">>, + <<"type">> => <<"tcp">>, + <<"running">> => true, + <<"acceptors">> => 8,<<"bind">> => 7993, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}], <<"mountpoint">> => <<>>, <<"server">> => #{<<"bind">> => 9100}} ). @@ -229,8 +244,11 @@ schema_gateway_overview_list() -> <<"idle_timeout">> => <<"30s">>, <<"lifetime_max">> => <<"86400s">>, <<"lifetime_min">> => <<"1s">>, - <<"listeners">> => - #{<<"udp">> => #{<<"default">> => #{<<"bind">> => 5783}}}, + <<"listeners">> => [ + #{<<"id">> => <<"lwm2m:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"bind">> => 5783}], <<"mountpoint">> => <<"lwm2m/%e/">>, <<"qmode_time_windonw">> => 22, <<"translators">> => @@ -251,11 +269,12 @@ schema_gateway_overview_list() -> <<"enable">> => true, <<"enable_qos3">> => true,<<"enable_stats">> => true, <<"gateway_id">> => 1,<<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"udp">> => - #{<<"default">> => - #{<<"bind">> => 1884,<<"max_conn_rate">> => 1000, - <<"max_connections">> => 10240000}}}, + <<"listeners">> => [ + #{<<"id">> => <<"mqttsn:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"bind">> => 1884,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240000}], <<"mountpoint">> => <<>>, <<"predefined">> => [#{<<"id">> => 1, @@ -279,12 +298,13 @@ schema_gateway_overview_list() -> #{<<"max_body_length">> => 8192,<<"max_headers">> => 10, <<"max_headers_length">> => 1024}, <<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"tcp">> => - #{<<"default">> => - #{<<"acceptors">> => 16,<<"active_n">> => 100, - <<"bind">> => 61613,<<"max_conn_rate">> => 1000, - <<"max_connections">> => 1024000}}}, + <<"listeners">> => [ + #{<<"id">> => <<"stomp:tcp:default">>, + <<"type">> => <<"tcp">>, + <<"running">> => true, + <<"acceptors">> => 16,<<"active_n">> => 100, + <<"bind">> => 61613,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 1024000}], <<"mountpoint">> => <<>>} ). @@ -312,10 +332,12 @@ schema_gateway_stats() -> properties_gateway_overview() -> ListenerProps = - [ {name, string, - <<"Listener Name">>} - , {status, string, - <<"Listener Status">>, [<<"activing">>, <<"inactived">>]} + [ {id, string, + <<"Listener ID">>} + , {running, boolean, + <<"Listener Running status">>} + , {type, string, + <<"Listener Type">>, [<<"tcp">>, <<"ssl">>, <<"udp">>, <<"dtls">>]} ], emqx_mgmt_util:properties( [ {name, string, @@ -323,9 +345,13 @@ properties_gateway_overview() -> , {status, string, <<"Gateway Status">>, [<<"running">>, <<"stopped">>, <<"unloaded">>]} + , {created_at, string, + <<>>} , {started_at, string, <<>>} - , {max_connection, integer, <<>>} - , {current_connection, integer, <<>>} + , {stopped_at, string, + <<>>} + , {max_connections, integer, <<>>} + , {current_connections, integer, <<>>} , {listeners, {array, object}, ListenerProps} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index 2aa6b4b3d..f233a6151 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -24,6 +24,12 @@ -export([ gateways/1 ]). +%% Mgmt APIs - listeners +-export([ listeners/1 + , listener/2 + , mapping_listener_m2l/2 + ]). + %% Mgmt APIs - clients -export([ lookup_client/3 , lookup_client/4 @@ -42,8 +48,8 @@ #{ name := binary() , status := running | stopped | unloaded , started_at => binary() - , max_connection => integer() - , current_connect => integer() + , max_connections => integer() + , current_connections => integer() , listeners => [] }. @@ -68,8 +74,10 @@ gateways(Status) -> created_at, started_at, stopped_at], GwInfo0), - GwInfo1#{listeners => get_listeners_status(GwName, Config)} - + GwInfo1#{ + max_connections => max_connections_count(Config), + current_connections => current_connections_count(GwName), + listeners => get_listeners_status(GwName, Config)} end end, emqx_gateway_registry:list()), case Status of @@ -78,24 +86,78 @@ gateways(Status) -> [Gw || Gw = #{status := S} <- Gateways, S == Status] end. +%% @private +max_connections_count(Config) -> + Listeners = emqx_gateway_utils:normalize_config(Config), + lists:foldl(fun({_, _, _, SocketOpts, _}, Acc) -> + Acc + proplists:get_value(max_connections, SocketOpts, 0) + end, 0, Listeners). + +%% @private +current_connections_count(GwName) -> + try + InfoTab = emqx_gateway_cm:tabname(info, GwName), + ets:info(InfoTab, size) + catch _ : _ -> + 0 + end. + %% @private get_listeners_status(GwName, Config) -> Listeners = emqx_gateway_utils:normalize_config(Config), lists:map(fun({Type, LisName, ListenOn, _, _}) -> - Name0 = listener_name(GwName, Type, LisName), + Name0 = emqx_gateway_utils:listener_id(GwName, Type, LisName), Name = {Name0, ListenOn}, + LisO = #{id => Name0, type => Type}, case catch esockd:listener(Name) of _Pid when is_pid(_Pid) -> - #{Name0 => <<"activing">>}; + LisO#{running => true}; _ -> - #{Name0 => <<"inactived">>} - + LisO#{running => false} end end, Listeners). -%% @private -listener_name(GwName, Type, LisName) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). +%%-------------------------------------------------------------------- +%% Mgmt APIs - listeners +%%-------------------------------------------------------------------- + +listeners(GwName) when is_atom (GwName) -> + listeners(atom_to_binary(GwName)); +listeners(GwName) -> + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw([<<"gateway">>])), + Listeners = emqx_map_lib:jsonable_map( + emqx_map_lib:deep_get( + [<<"gateway">>, GwName, <<"listeners">>], RawConf)), + mapping_listener_m2l(GwName, Listeners). + +listener(_GwName, _ListenerId) -> + ok. + +mapping_listener_m2l(GwName, Listeners0) -> + Listeners = maps:to_list(Listeners0), + lists:append([listener(GwName, Type, maps:to_list(Conf)) + || {Type, Conf} <- Listeners]). + +listener(GwName, Type, Conf) -> + [begin + ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LName), + Running = is_running(ListenerId, LConf), + LConf#{ + id => ListenerId, + type => Type, + running => Running + } + end || {LName, LConf} <- Conf, is_map(LConf)]. + +is_running(ListenerId, #{<<"bind">> := ListenOn0}) -> + ListenOn = emqx_gateway_utils:parse_listenon(ListenOn0), + try esockd:listener({ListenerId, ListenOn}) of + Pid when is_pid(Pid)-> + true + catch _:_ -> + false + end. %%-------------------------------------------------------------------- %% Mgmt APIs - clients @@ -145,7 +207,7 @@ list_client_subscriptions(GwName, ClientId) -> with_channel(GwName, ClientId, fun(Pid) -> Subs = emqx_gateway_conn:call( - Pid, + Pid, subscriptions, ?DEFAULT_CALL_TIMEOUT), {ok, lists:map(fun({Topic, SubOpts}) -> SubOpts#{topic => Topic} diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 8c3663358..41840c7f7 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -204,7 +204,6 @@ fields(udp_opts) -> fields(dtls_listener_ssl_opts) -> Base = emqx_schema:fields("listener_ssl_opts"), - %% XXX: ciphers ??? DtlsVers = hoconsc:mk( typerefl:alias("string", list(atom())), #{ default => default_dtls_vsns(), @@ -212,12 +211,41 @@ fields(dtls_listener_ssl_opts) -> [dtls_vsn(iolist_to_binary(V)) || V <- Vsns] end }), - lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}); + Ciphers = sc(hoconsc:array(string()), default_ciphers()), + lists:keydelete( + "handshake_timeout", 1, + lists:keyreplace( + "ciphers", 1, + lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}), + {"ciphers", Ciphers} + ) + ); fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). +default_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_ciphers(). + +psk_ciphers() -> + ["PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", + "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA" + ]. + % authentication() -> % hoconsc:union( % [ undefined @@ -242,7 +270,7 @@ gateway_common_options() -> [ {enable, sc(boolean(), true)} , {enable_stats, sc(boolean(), true)} , {idle_timeout, sc(duration(), <<"30s">>)} - , {mountpoint, sc(binary())} + , {mountpoint, sc(binary(), undefined)} , {clientinfo_override, sc(ref(clientinfo_override))} , {authentication, sc(hoconsc:lazy(map()))} ]. @@ -254,6 +282,7 @@ common_listener_opts() -> , {max_connections, sc(integer(), 1024)} , {max_conn_rate, sc(integer())} %, {rate_limit, sc(comma_separated_list())} + , {mountpoint, sc(binary(), undefined)} , {access_rules, sc(hoconsc:array(string()), [])} ]. diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 27e64bed3..4f19db23b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -28,8 +28,11 @@ -export([ apply/2 , format_listenon/1 + , parse_listenon/1 , unix_ts_to_rfc3339/1 , unix_ts_to_rfc3339/2 + , listener_id/3 + , parse_listener_id/1 ]). -export([ stringfy/1 @@ -112,6 +115,38 @@ format_listenon({Addr, Port}) when is_list(Addr) -> format_listenon({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). +parse_listenon(Port) when is_integer(Port) -> + Port; +parse_listenon(Str) when is_binary(Str) -> + parse_listenon(binary_to_list(Str)); +parse_listenon(Str) when is_list(Str) -> + case emqx_schema:to_ip_port(Str) of + {ok, R} -> R; + {error, _} -> + error({invalid_listenon_name, Str}) + end. + +listener_id(GwName, Type, LisName) -> + binary_to_atom( + <<(bin(GwName))/binary, ":", + (bin(Type))/binary, ":", + (bin(LisName))/binary + >>). + +parse_listener_id(Id) -> + try + [GwName, Type, Name] = binary:split(bin(Id), <<":">>, [global]), + {binary_to_existing_atom(GwName), binary_to_existing_atom(Type), + binary_to_atom(Name)} + catch + _ : _ -> error({invalid_listener_id, Id}) + end. + +bin(A) when is_atom(A) -> + atom_to_binary(A); +bin(L) when is_list(L); is_binary(L) -> + iolist_to_binary(L). + unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) -> lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys); unix_ts_to_rfc3339(Key, Map) -> diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 3b62ecd20..a9c0ee5fd 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -153,7 +153,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_exproto_frame, @@ -172,9 +172,6 @@ do_start_listener(udp, Name, ListenOn, Opts, MFA) -> do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> Default = emqx_gateway_utils:default_tcp_options(), @@ -209,5 +206,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index e6720905b..5e1e9a70d 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -100,7 +100,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, udp), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx , frame_mod => emqx_coap_frame , chann_mod => emqx_lwm2m_channel @@ -119,16 +119,12 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> esockd:open_udp(Name, ListenOn, SocketOpts, MFA); do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). - stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), @@ -142,5 +138,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index f510afdf9..8e64a41d1 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -118,7 +118,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_sn_frame, @@ -127,9 +127,6 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), case lists:keytake(udp_options, 1, Options) of @@ -153,5 +150,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 2175b767b..96c7b7a3b 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -103,7 +103,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_stomp_frame, @@ -112,9 +112,6 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> esockd:open(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), case lists:keytake(tcp_options, 1, Options) of @@ -138,5 +135,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). From 020e04e5cf4747d46023370278c5168aa16a2c49 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 9 Sep 2021 09:57:48 +0800 Subject: [PATCH 292/306] chore(gw): improve the default confs --- apps/emqx_gateway/etc/emqx_gateway.conf | 22 ++++++++++++++++++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 3 +-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 9f558b761..2ce48bf75 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -129,7 +129,7 @@ gateway.coap { ## DTLS Options ## See #{example_common_dtls_options} for more information - dtls.versions = ["dtlsv1"] + dtls.versions = ["dtlsv1.2", "dtlsv1"] dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" @@ -182,6 +182,26 @@ gateway.mqttsn { max_connections = 10240000 max_conn_rate = 1000 } + + listeners.dtls.default { + bind = 1885 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + + ## DTLS Options + ## See #{example_common_dtls_options} for more information + dtls.versions = ["dtlsv1.2", "dtlsv1"] + dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + } + } gateway.lwm2m { diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 41840c7f7..7fb945ba0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -96,7 +96,6 @@ fields(lwm2m_structs) -> , {lifetime_max, sc(duration())} , {qmode_time_windonw, sc(integer())} , {auto_observe, sc(boolean())} - , {mountpoint, sc(string())} , {update_msg_publish_condition, sc(union([always, contains_object_list]))} , {translators, sc(ref(translators))} , {listeners, sc(ref(udp_listener_group))} @@ -270,7 +269,7 @@ gateway_common_options() -> [ {enable, sc(boolean(), true)} , {enable_stats, sc(boolean(), true)} , {idle_timeout, sc(duration(), <<"30s">>)} - , {mountpoint, sc(binary(), undefined)} + , {mountpoint, sc(binary(), <<>>)} , {clientinfo_override, sc(ref(clientinfo_override))} , {authentication, sc(hoconsc:lazy(map()))} ]. From 5da085baccf9f7aa3f425963030b691e102bbfb6 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 9 Sep 2021 09:58:05 +0800 Subject: [PATCH 293/306] chore(gw): improve the listener started banner --- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 10 +++++----- apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl | 10 +++++----- apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl | 8 ++++---- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 8 ++++---- apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index 69015788a..055eab759 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -89,11 +89,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -118,10 +118,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", - [GwName, Type, LisName, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index a9c0ee5fd..3e142f3dc 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -143,11 +143,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", - [GwName, Type, LisName, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -197,10 +197,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index 5e1e9a70d..649a14643 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -90,11 +90,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -129,10 +129,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 8e64a41d1..a79173cff 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -108,11 +108,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -141,10 +141,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gatewat ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 96c7b7a3b..9599ef6e3 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -93,11 +93,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -126,10 +126,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. From 9a09bf796420e50c703fe13413eaabe44bca362d Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 10 Sep 2021 09:20:16 +0800 Subject: [PATCH 294/306] fix: logout api delete token (#5686) --- apps/emqx_dashboard/src/emqx_dashboard_admin.erl | 11 ++++++++--- apps/emqx_dashboard/src/emqx_dashboard_api.erl | 12 ++++++++---- apps/emqx_dashboard/src/emqx_dashboard_token.erl | 6 ++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 8a1306e94..b477bd779 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -40,7 +40,7 @@ -export([ sign_token/2 , verify_token/1 - , destroy_token_by_username/1 + , destroy_token_by_username/2 ]). -export([add_default_user/0]). @@ -177,8 +177,13 @@ sign_token(Username, Password) -> verify_token(Token) -> emqx_dashboard_token:verify(Token). -destroy_token_by_username(Username) -> - emqx_dashboard_token:destroy_by_username(Username). +destroy_token_by_username(Username, Token) -> + case emqx_dashboard_token:lookup(Token) of + {ok, #mqtt_admin_jwt{username = Username}} -> + emqx_dashboard_token:destroy(Token); + _ -> + {error, not_found} + end. %%-------------------------------------------------------------------- %% Internal functions diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 4761432fb..68c737488 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -170,10 +170,14 @@ login(post, #{body := Params}) -> {401, #{code => ?ERROR_USERNAME_OR_PWD, message => <<"Auth filed">>}} end. -logout(_, #{body := Params}) -> - Username = maps:get(<<"username">>, Params), - emqx_dashboard_admin:destroy_token_by_username(Username), - {200}. +logout(_, #{body := #{<<"username">> := Username}, + headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}}) -> + case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of + ok -> + 200; + _R -> + {401, 'BAD_TOKEN_OR_USERNAME', <<"Ensure your token & username">>} + end. users(get, _Request) -> {200, [row(User) || User <- emqx_dashboard_admin:all_users()]}; diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index 9086b4c2e..2acf00f13 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -22,6 +22,7 @@ -export([ sign/2 , verify/1 + , lookup/1 , destroy/1 , destroy_by_username/1 ]). @@ -121,14 +122,15 @@ do_verify(Token)-> do_destroy(Token) -> Fun = fun mnesia:delete/1, - ekka_mnesia:transaction(?DASHBOARD_SHARD, Fun, [{?TAB, Token}]). + {atomic, ok} = ekka_mnesia:transaction(?DASHBOARD_SHARD, Fun, [{?TAB, Token}]), + ok. do_destroy_by_username(Username) -> gen_server:cast(?MODULE, {destroy, Username}). %%-------------------------------------------------------------------- %% jwt internal util function - +-spec(lookup(Token :: binary()) -> {ok, #mqtt_admin_jwt{}} | {error, not_found}). lookup(Token) -> case mnesia:dirty_read(?TAB, Token) of [JWT] -> {ok, JWT}; From bfb2df37ce7cf7f03b955817432fe4e206ef480a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 6 Sep 2021 17:32:46 +0800 Subject: [PATCH 295/306] refactor(bridge): rename emqx_data_bridge to emqx_bridge --- .../.gitignore | 0 .../README.md | 2 +- .../etc/emqx_bridge.conf} | 49 ++++++++++++++++--- .../rebar.config | 2 +- .../src/emqx_bridge.app.src} | 4 +- .../src/emqx_bridge.erl} | 12 +++-- .../src/emqx_bridge_api.erl} | 28 +++++------ .../src/emqx_bridge_app.erl} | 8 +-- .../src/emqx_bridge_monitor.erl} | 14 ++++-- apps/emqx_bridge/src/emqx_bridge_schema.erl | 17 +++++++ .../src/emqx_bridge_sup.erl} | 8 +-- apps/emqx_machine/src/emqx_machine.erl | 2 +- apps/emqx_machine/src/emqx_machine_schema.erl | 2 +- apps/emqx_resource/README.md | 2 +- apps/emqx_resource/examples/demo.md | 6 +-- rebar.config.erl | 2 +- 16 files changed, 107 insertions(+), 51 deletions(-) rename apps/{emqx_data_bridge => emqx_bridge}/.gitignore (100%) rename apps/{emqx_data_bridge => emqx_bridge}/README.md (95%) rename apps/{emqx_data_bridge/etc/emqx_data_bridge.conf => emqx_bridge/etc/emqx_bridge.conf} (78%) rename apps/{emqx_data_bridge => emqx_bridge}/rebar.config (73%) rename apps/{emqx_data_bridge/src/emqx_data_bridge.app.src => emqx_bridge/src/emqx_bridge.app.src} (75%) rename apps/{emqx_data_bridge/src/emqx_data_bridge.erl => emqx_bridge/src/emqx_bridge.erl} (86%) rename apps/{emqx_data_bridge/src/emqx_data_bridge_api.erl => emqx_bridge/src/emqx_bridge_api.erl} (83%) rename apps/{emqx_data_bridge/src/emqx_data_bridge_app.erl => emqx_bridge/src/emqx_bridge_app.erl} (87%) rename apps/{emqx_data_bridge/src/emqx_data_bridge_monitor.erl => emqx_bridge/src/emqx_bridge_monitor.erl} (84%) create mode 100644 apps/emqx_bridge/src/emqx_bridge_schema.erl rename apps/{emqx_data_bridge/src/emqx_data_bridge_sup.erl => emqx_bridge/src/emqx_bridge_sup.erl} (86%) diff --git a/apps/emqx_data_bridge/.gitignore b/apps/emqx_bridge/.gitignore similarity index 100% rename from apps/emqx_data_bridge/.gitignore rename to apps/emqx_bridge/.gitignore diff --git a/apps/emqx_data_bridge/README.md b/apps/emqx_bridge/README.md similarity index 95% rename from apps/emqx_data_bridge/README.md rename to apps/emqx_bridge/README.md index 8f76f17a5..0f274eea1 100644 --- a/apps/emqx_data_bridge/README.md +++ b/apps/emqx_bridge/README.md @@ -1,4 +1,4 @@ -# emqx_data_bridge +# emqx_bridge EMQ X Data Bridge is an application that managing the resources (see emqx_resource) used by emqx rule engine. diff --git a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf b/apps/emqx_bridge/etc/emqx_bridge.conf similarity index 78% rename from apps/emqx_data_bridge/etc/emqx_data_bridge.conf rename to apps/emqx_bridge/etc/emqx_bridge.conf index 99a49dba3..663ae6586 100644 --- a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf +++ b/apps/emqx_bridge/etc/emqx_bridge.conf @@ -1,10 +1,46 @@ ##-------------------------------------------------------------------- -## EMQ X Bridge Plugin +## EMQ X Bridge ##-------------------------------------------------------------------- -emqx_data_bridge { - bridges:[ - # {name: "mysql_bridge_1" +bridges.mqtt.my_mqtt_bridge { + server = "127.0.0.1:1883" + proto_ver = "v4" + clientid = "client1" + username = "username1" + password = "" + clean_start = true + keepalive = 300 + retry_interval = "30s" + max_inflight = 32 + reconnect_interval = "30s" + bridge_mode = true + replayq { + dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" + seg_bytes = "100MB" + offload = false + max_total_bytes = "1GB" + } + ssl { + enable = false + keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" + certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" + cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + } + in [{ + from_remote_topic = "msg/#" + to_local_topic = "from_aws/${topic}" + payload_template = "${message}" + qos = 1 + }] + out [{ + from_local_topic = "msg/#" + to_remote_topic = "from_emqx/${topic}" + payload_template = "${message}" + }] +} + + +# {name: "mysql_bridge_1" # type: mysql # config: { # server: "192.168.0.172:3306" @@ -123,7 +159,4 @@ emqx_data_bridge { # pool_size: 1 # ssl: false # } - # } - - ] -} + # } \ No newline at end of file diff --git a/apps/emqx_data_bridge/rebar.config b/apps/emqx_bridge/rebar.config similarity index 73% rename from apps/emqx_data_bridge/rebar.config rename to apps/emqx_bridge/rebar.config index cf4cfcf1b..3fd6b41e0 100644 --- a/apps/emqx_data_bridge/rebar.config +++ b/apps/emqx_bridge/rebar.config @@ -3,5 +3,5 @@ {shell, [ % {config, "config/sys.config"}, - {apps, [emqx_data_bridge]} + {apps, [emqx_bridge]} ]}. diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src similarity index 75% rename from apps/emqx_data_bridge/src/emqx_data_bridge.app.src rename to apps/emqx_bridge/src/emqx_bridge.app.src index 84486da19..42fc245f5 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,8 +1,8 @@ -{application, emqx_data_bridge, +{application, emqx_bridge, [{description, "An OTP application"}, {vsn, "0.1.0"}, {registered, []}, - {mod, {emqx_data_bridge_app, []}}, + {mod, {emqx_bridge_app, []}}, {applications, [kernel, stdlib, diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl similarity index 86% rename from apps/emqx_data_bridge/src/emqx_data_bridge.erl rename to apps/emqx_bridge/src/emqx_bridge.erl index 52cea80fb..4e05f8e96 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge). +-module(emqx_bridge). -export([ load_bridges/0 , resource_type/1 @@ -27,15 +27,17 @@ ]). load_bridges() -> - Bridges = emqx:get_config([emqx_data_bridge, bridges], []), - emqx_data_bridge_monitor:ensure_all_started(Bridges). + Bridges = emqx:get_config([bridges], #{}), + emqx_bridge_monitor:ensure_all_started(Bridges). +resource_type(mqtt) -> emqx_connector_mqtt; resource_type(mysql) -> emqx_connector_mysql; resource_type(pgsql) -> emqx_connector_pgsql; resource_type(mongo) -> emqx_connector_mongo; resource_type(redis) -> emqx_connector_redis; resource_type(ldap) -> emqx_connector_ldap. +bridge_type(emqx_connector_mqtt) -> mqtt; bridge_type(emqx_connector_mysql) -> mysql; bridge_type(emqx_connector_pgsql) -> pgsql; bridge_type(emqx_connector_mongo) -> mongo; @@ -49,7 +51,7 @@ resource_id_to_name(<<"bridge:", BridgeName/binary>> = _ResourceId) -> BridgeName. list_bridges() -> - emqx_resource_api:list_instances(fun emqx_data_bridge:is_bridge/1). + emqx_resource_api:list_instances(fun emqx_bridge:is_bridge/1). is_bridge(#{id := <<"bridge:", _/binary>>}) -> true; @@ -57,7 +59,7 @@ is_bridge(_Data) -> false. config_key_path() -> - [emqx_data_bridge, bridges]. + [emqx_bridge, bridges]. update_config(ConfigReq) -> emqx:update_config(config_key_path(), ConfigReq). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl similarity index 83% rename from apps/emqx_data_bridge/src/emqx_data_bridge_api.erl rename to apps/emqx_bridge/src/emqx_bridge_api.erl index 6fe75e4ce..c10875e55 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_api). +-module(emqx_bridge_api). -rest_api(#{ name => list_data_bridges , method => 'GET' @@ -61,10 +61,10 @@ list_bridges(_Binding, _Params) -> {200, #{code => 0, data => [format_api_reply(Data) || - Data <- emqx_data_bridge:list_bridges()]}}. + Data <- emqx_bridge:list_bridges()]}}. get_bridge(#{name := Name}, _Params) -> - case emqx_resource:get_instance(emqx_data_bridge:name_to_resource_id(Name)) of + case emqx_resource:get_instance(emqx_bridge:name_to_resource_id(Name)) of {ok, Data} -> {200, #{code => 0, data => format_api_reply(emqx_resource_api:format_data(Data))}}; {error, not_found} -> @@ -75,8 +75,8 @@ create_bridge(#{name := Name}, Params) -> Config = proplists:get_value(<<"config">>, Params), BridgeType = proplists:get_value(<<"type">>, Params), case emqx_resource:check_and_create( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of {ok, already_created} -> {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}}; {ok, Data} -> @@ -91,8 +91,8 @@ update_bridge(#{name := Name}, Params) -> Config = proplists:get_value(<<"config">>, Params), BridgeType = proplists:get_value(<<"type">>, Params), case emqx_resource:check_and_update( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of {ok, Data} -> update_config_and_reply(Name, BridgeType, Config, Data); {error, not_found} -> @@ -104,26 +104,26 @@ update_bridge(#{name := Name}, Params) -> end. delete_bridge(#{name := Name}, _Params) -> - case emqx_resource:remove(emqx_data_bridge:name_to_resource_id(Name)) of + case emqx_resource:remove(emqx_bridge:name_to_resource_id(Name)) of ok -> delete_config_and_reply(Name); {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} end. format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := Status}) -> - #{type => emqx_data_bridge:bridge_type(Type), - name => emqx_data_bridge:resource_id_to_name(Id), + #{type => emqx_bridge:bridge_type(Type), + name => emqx_bridge:resource_id_to_name(Id), config => Conf, status => Status}. % format_conf(#{resource_type := Type, id := Id, config := Conf}) -> -% #{type => Type, name => emqx_data_bridge:resource_id_to_name(Id), +% #{type => Type, name => emqx_bridge:resource_id_to_name(Id), % config => Conf}. % get_all_configs() -> -% [format_conf(Data) || Data <- emqx_data_bridge:list_bridges()]. +% [format_conf(Data) || Data <- emqx_bridge:list_bridges()]. update_config_and_reply(Name, BridgeType, Config, Data) -> - case emqx_data_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of + case emqx_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of {ok, _} -> {200, #{code => 0, data => format_api_reply( emqx_resource_api:format_data(Data))}}; @@ -132,7 +132,7 @@ update_config_and_reply(Name, BridgeType, Config, Data) -> end. delete_config_and_reply(Name) -> - case emqx_data_bridge:update_config({delete, Name}) of + case emqx_bridge:update_config({delete, Name}) of {ok, _} -> {200, #{code => 0, data => #{}}}; {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl b/apps/emqx_bridge/src/emqx_bridge_app.erl similarity index 87% rename from apps/emqx_data_bridge/src/emqx_data_bridge_app.erl rename to apps/emqx_bridge/src/emqx_bridge_app.erl index 859952480..cfefe118f 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl +++ b/apps/emqx_bridge/src/emqx_bridge_app.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_app). +-module(emqx_bridge_app). -behaviour(application). @@ -22,9 +22,9 @@ -export([start/2, stop/1, pre_config_update/2]). start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_data_bridge_sup:start_link(), - ok = emqx_data_bridge:load_bridges(), - emqx_config_handler:add_handler(emqx_data_bridge:config_key_path(), ?MODULE), + {ok, Sup} = emqx_bridge_sup:start_link(), + ok = emqx_bridge:load_bridges(), + emqx_config_handler:add_handler(emqx_bridge:config_key_path(), ?MODULE), {ok, Sup}. stop(_State) -> diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl b/apps/emqx_bridge/src/emqx_bridge_monitor.erl similarity index 84% rename from apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl rename to apps/emqx_bridge/src/emqx_bridge_monitor.erl index 4917833ec..4b3695615 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl +++ b/apps/emqx_bridge/src/emqx_bridge_monitor.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- %% This process monitors all the data bridges, and try to restart a bridge %% when one of it stopped. --module(emqx_data_bridge_monitor). +-module(emqx_bridge_monitor). -behaviour(gen_server). @@ -65,14 +65,18 @@ code_change(_OldVsn, State, _Extra) -> %%============================================================================ load_bridges(Configs) -> - lists:foreach(fun load_bridge/1, Configs). + lists:foreach(fun(Type, NamedConf) -> + lists:foreach(fun(Name, Conf) -> + load_bridge(Name, Type, Conf) + end, maps:to_list(NamedConf)) + end, maps:to_list(Configs)). %% TODO: move this monitor into emqx_resource %% emqx_resource:check_and_create_local(ResourceId, ResourceType, Config, #{keep_retry => true}). -load_bridge(#{name := Name, type := Type, config := Config}) -> +load_bridge(Name, Type, Config) -> case emqx_resource:create_local( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(Type), Config) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(Type), Config) of {ok, already_created} -> ok; {ok, _} -> ok; {error, Reason} -> diff --git a/apps/emqx_bridge/src/emqx_bridge_schema.erl b/apps/emqx_bridge/src/emqx_bridge_schema.erl new file mode 100644 index 000000000..f651ce189 --- /dev/null +++ b/apps/emqx_bridge/src/emqx_bridge_schema.erl @@ -0,0 +1,17 @@ +-module(emqx_bridge_schema). + +-export([roots/0, fields/1]). + +%%====================================================================================== +%% Hocon Schema Definitions + +roots() -> ["bridges"]. + +fields("bridges") -> + [{mqtt, hoconsc:ref("mqtt")}]; + +fields("mqtt") -> + [{"?name"}, hoconsc:ref("mqtt_briage")]; + +fields("mqtt_briage") -> + emqx_connector_mqtt:fields("config"). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl b/apps/emqx_bridge/src/emqx_bridge_sup.erl similarity index 86% rename from apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl rename to apps/emqx_bridge/src/emqx_bridge_sup.erl index a699a72a0..fd12b1a99 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl +++ b/apps/emqx_bridge/src/emqx_bridge_sup.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_sup). +-module(emqx_bridge_sup). -behaviour(supervisor). @@ -31,11 +31,11 @@ init([]) -> intensity => 10, period => 10}, ChildSpecs = [ - #{id => emqx_data_bridge_monitor, - start => {emqx_data_bridge_monitor, start_link, []}, + #{id => emqx_bridge_monitor, + start => {emqx_bridge_monitor, start_link, []}, restart => permanent, type => worker, - modules => [emqx_data_bridge_monitor]} + modules => [emqx_bridge_monitor]} ], {ok, {SupFlags, ChildSpecs}}. diff --git a/apps/emqx_machine/src/emqx_machine.erl b/apps/emqx_machine/src/emqx_machine.erl index 3e5772b4a..97125d79f 100644 --- a/apps/emqx_machine/src/emqx_machine.erl +++ b/apps/emqx_machine/src/emqx_machine.erl @@ -140,7 +140,7 @@ reboot_apps() -> , emqx_statsd , emqx_resource , emqx_rule_engine - , emqx_data_bridge + , emqx_bridge , emqx_bridge_mqtt , emqx_plugin_libs , emqx_management diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 4894bda98..1f5d639cd 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -43,7 +43,7 @@ %% by nodetool to generate app.)Xr}=W0 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

    ()tPcaXYm%S-P)Pz0toF&0B;|4K0pXVlYp{fsTT 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 `~M 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"]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-bridge-mqtt"} - ]} - ]}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl deleted file mode 100644 index a145009c9..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl +++ /dev/null @@ -1,31 +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_mqtt_app). - --behaviour(application). - --export([start/2, stop/1]). - -start(_StartType, _StartArgs) -> - emqx_ctl:register_command(bridges, {emqx_bridge_mqtt_cli, cli}, []), - emqx_bridge_worker:register_metrics(), - emqx_bridge_mqtt_sup:start_link(). - -stop(_State) -> - emqx_ctl:unregister_command(bridges), - ok. - diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl deleted file mode 100644 index a76ea3a8c..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.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_bridge_mqtt_cli). - --include("emqx_bridge_mqtt.hrl"). - --import(lists, [foreach/2]). - --export([cli/1]). - -cli(["list"]) -> - foreach(fun({Name, State0}) -> - State = case State0 of - connected -> <<"Running">>; - _ -> <<"Stopped">> - end, - emqx_ctl:print("name: ~s status: ~s~n", [Name, State]) - end, emqx_bridge_mqtt_sup:bridges()); - -cli(["start", Name]) -> - emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_started(Name) of - ok -> <<"Start bridge successfully">>; - connected -> <<"Bridge already started">>; - _ -> <<"Start bridge failed">> - catch - _Error:_Reason -> - <<"Start bridge failed">> - end]); - -cli(["stop", Name]) -> - emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_stopped(Name) of - ok -> <<"Stop bridge successfully">>; - _ -> <<"Stop bridge failed">> - catch - _Error:_Reason -> - <<"Stop bridge failed">> - end]); - -cli(["forwards", Name]) -> - foreach(fun(Topic) -> - emqx_ctl:print("topic: ~s~n", [Topic]) - end, emqx_bridge_worker:get_forwards(Name)); - -cli(["add-forward", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_forward_present(Name, iolist_to_binary(Topic)), - emqx_ctl:print("Add-forward topic successfully.~n"); - -cli(["del-forward", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_forward_absent(Name, iolist_to_binary(Topic)), - emqx_ctl:print("Del-forward topic successfully.~n"); - -cli(["subscriptions", Name]) -> - foreach(fun({Topic, Qos}) -> - emqx_ctl:print("topic: ~s, qos: ~p~n", [Topic, Qos]) - end, emqx_bridge_worker:get_subscriptions(Name)); - -cli(["add-subscription", Name, Topic, Qos]) -> - case emqx_bridge_worker:ensure_subscription_present(Name, Topic, list_to_integer(Qos)) of - ok -> emqx_ctl:print("Add-subscription topic successfully.~n"); - {error, Reason} -> emqx_ctl:print("Add-subscription failed reason: ~p.~n", [Reason]) - end; - -cli(["del-subscription", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_subscription_absent(Name, Topic), - emqx_ctl:print("Del-subscription topic successfully.~n"); - -cli(_) -> - emqx_ctl:usage([{"bridges list", "List bridges"}, - {"bridges start ", "Start a bridge"}, - {"bridges stop ", "Stop a bridge"}, - {"bridges forwards ", "Show a bridge forward topic"}, - {"bridges add-forward ", "Add bridge forward topic"}, - {"bridges del-forward ", "Delete bridge forward topic"}, - {"bridges subscriptions ", "Show a bridge subscriptions topic"}, - {"bridges add-subscription ", "Add bridge subscriptions topic"}, - {"bridges del-subscription ", "Delete bridge subscriptions topic"}]). - - diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl deleted file mode 100644 index f370af277..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ /dev/null @@ -1,94 +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_mqtt_schema). - --include_lib("typerefl/include/types.hrl"). - --behaviour(hocon_schema). - --export([ namespace/0 - , roots/0 - , fields/1]). - -namespace() -> "bridge_mqtt". - -roots() -> [array("bridge_mqtt")]. - -array(Name) -> {Name, hoconsc:array(hoconsc:ref(?MODULE, Name))}. - -fields("bridge_mqtt") -> - [ {name, sc(string(), #{default => true})} - , {start_type, fun start_type/1} - , {forwards, fun forwards/1} - , {forward_mountpoint, sc(string(), #{})} - , {reconnect_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} - , {batch_size, sc(integer(), #{default => 100})} - , {queue, sc(hoconsc:ref(?MODULE, "queue"), #{})} - , {config, sc(hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), - hoconsc:ref(?MODULE, "rpc")]), - #{})} - ]; - -fields("mqtt") -> - [ {conn_type, fun conn_type/1} - , {address, sc(string(), #{default => "127.0.0.1:1883"})} - , {proto_ver, fun proto_ver/1} - , {bridge_mode, sc(boolean(), #{default => true})} - , {clientid, sc(string(), #{})} - , {username, sc(string(), #{})} - , {password, sc(string(), #{})} - , {clean_start, sc(boolean(), #{default => true})} - , {keepalive, sc(integer(), #{default => 300})} - , {subscriptions, sc(hoconsc:array(hoconsc:ref(?MODULE, "subscriptions")), #{})} - , {receive_mountpoint, sc(string(), #{})} - , {retry_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} - , {max_inflight, sc(integer(), #{default => 32})} - ]; - -fields("rpc") -> - [ {conn_type, fun conn_type/1} - , {node, sc(atom(), #{default => 'emqx@127.0.0.1'})} - ]; - -fields("subscriptions") -> - [ {topic, #{type => binary(), nullable => false}} - , {qos, sc(integer(), #{default => 1})} - ]; - -fields("queue") -> - [ {replayq_dir, hoconsc:union([boolean(), string()])} - , {replayq_seg_bytes, sc(emqx_schema:bytesize(), #{default => "100MB"})} - , {replayq_offload_mode, sc(boolean(), #{default => false})} - , {replayq_max_total_bytes, sc(emqx_schema:bytesize(), #{default => "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. - -sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl deleted file mode 100644 index c75592edb..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl +++ /dev/null @@ -1,69 +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_mqtt_sup). --behaviour(supervisor). - --include("emqx_bridge_mqtt.hrl"). --include_lib("emqx/include/logger.hrl"). - - -%% APIs --export([ start_link/0 - ]). - --export([ create_bridge/1 - , drop_bridge/1 - , bridges/0 - ]). - -%% supervisor callbacks --export([init/1]). - --define(WORKER_SUP, emqx_bridge_worker_sup). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - SupFlag = #{strategy => one_for_one, - intensity => 100, - period => 10}, - {ok, {SupFlag, []}}. - -bridge_spec(Config) -> - #{id => maps:get(name, Config), - start => {emqx_bridge_worker, start_link, [Config]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_bridge_worker]}. - --spec(bridges() -> [{node(), map()}]). -bridges() -> - [{Name, emqx_bridge_worker:status(Name)} - || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. - -create_bridge(Config) -> - supervisor:start_child(?MODULE, bridge_spec(Config)). - -drop_bridge(Name) -> - case supervisor:terminate_child(?MODULE, Name) of - ok -> - supervisor:delete_child(?MODULE, Name); - {error, Error} -> - {error, Error} - end. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl deleted file mode 100644 index 33511cc03..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl +++ /dev/null @@ -1,95 +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 This module implements EMQX Bridge transport layer based on gen_rpc. - --module(emqx_bridge_rpc). - --export([ start/1 - , send/2 - , stop/1 - ]). - -%% Internal exports --export([ handle_send/1 - , heartbeat/2 - ]). - --type ack_ref() :: emqx_bridge_worker:ack_ref(). --type batch() :: emqx_bridge_worker:batch(). --define(HEARTBEAT_INTERVAL, timer:seconds(1)). - --define(RPC, emqx_rpc). - -start(#{node := RemoteNode}) -> - case poke(RemoteNode) of - ok -> - Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), RemoteNode]), - {ok, #{client_pid => Pid, remote_node => RemoteNode}}; - Error -> - Error - end. - -stop(#{client_pid := Pid}) when is_pid(Pid) -> - Ref = erlang:monitor(process, Pid), - unlink(Pid), - Pid ! stop, - receive - {'DOWN', Ref, process, Pid, _Reason} -> - ok - after - 1000 -> - exit(Pid, kill) - end, - ok. - -%% @doc Callback for `emqx_bridge_connect' behaviour --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}, - {ok, Ref}; - {badrpc, Reason} -> {error, Reason} - end. - -%% @doc Handle send on receiver side. --spec handle_send(batch()) -> ok. -handle_send(Batch) -> - lists:foreach(fun(Msg) -> emqx_broker:publish(Msg) end, Batch). - -%% @hidden Heartbeat loop -heartbeat(Parent, RemoteNode) -> - Interval = ?HEARTBEAT_INTERVAL, - receive - stop -> exit(normal) - after - Interval -> - case poke(RemoteNode) of - ok -> - ?MODULE:heartbeat(Parent, RemoteNode); - {error, Reason} -> - Parent ! {disconnected, self(), Reason}, - exit(normal) - end - end. - -poke(RemoteNode) -> - case ?RPC:call(RemoteNode, erlang, node, []) of - RemoteNode -> ok; - {badrpc, Reason} -> {error, Reason} - 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 deleted file mode 100644 index cbd80ba3d..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl +++ /dev/null @@ -1,42 +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_rpc_tests). --include_lib("eunit/include/eunit.hrl"). - -send_and_ack_test() -> - %% delegate from emqx_rpc to rpc for unit test - meck:new(emqx_rpc, [passthrough, no_history]), - meck:expect(emqx_rpc, call, 4, - fun(Node, Module, Fun, Args) -> - rpc:call(Node, Module, Fun, Args) - end), - meck:expect(emqx_rpc, cast, 4, - fun(Node, Module, Fun, Args) -> - rpc:cast(Node, Module, Fun, Args) - end), - meck:new(emqx_bridge_worker, [passthrough, no_history]), - try - {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 - end, - ok = emqx_bridge_rpc:stop( #{client_pid => Pid}) - after - meck:unload(emqx_rpc) - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl deleted file mode 100644 index 4c2fde6dd..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl +++ /dev/null @@ -1,38 +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_bridge_stub_conn). - --export([ start/1 - , send/2 - , stop/1 - ]). - --type ack_ref() :: emqx_bridge_worker:ack_ref(). --type batch() :: emqx_bridge_worker:batch(). - -start(#{client_pid := Pid} = Cfg) -> - Pid ! {self(), ?MODULE, ready}, - {ok, Cfg}. - -stop(_) -> ok. - -%% @doc Callback for `emqx_bridge_connect' behaviour --spec send(_, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{client_pid := Pid}, Batch) -> - Ref = make_ref(), - Pid ! {stub_message, self(), Ref, Batch}, - {ok, Ref}. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl deleted file mode 100644 index f3f5d5ceb..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl +++ /dev/null @@ -1,372 +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_worker_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("snabbkaffe/include/snabbkaffe.hrl"). - --define(wait(For, Timeout), emqx_ct_helpers:wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). - --define(SNK_WAIT(WHAT), ?assertMatch({ok, _}, ?block_until(#{?snk_kind := WHAT}, 2000, 1000))). - -receive_messages(Count) -> - receive_messages(Count, []). - -receive_messages(0, Msgs) -> - Msgs; -receive_messages(Count, Msgs) -> - receive - {publish, Msg} -> - receive_messages(Count-1, [Msg|Msgs]); - _Other -> - receive_messages(Count, Msgs) - after 1000 -> - Msgs - end. - -all() -> - lists:filtermap( - fun({FunName, _Arity}) -> - case atom_to_list(FunName) of - "t_" ++ _ -> {true, FunName}; - _ -> false - end - end, - ?MODULE:module_info(exports)). - -init_per_suite(Config) -> - case node() of - nonode@nohost -> net_kernel:start(['emqx@127.0.0.1', longnames]); - _ -> ok - end, - 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]). - -init_per_testcase(_TestCase, Config) -> - ok = snabbkaffe:start_trace(), - Config. - -end_per_testcase(_TestCase, _Config) -> - ok = snabbkaffe:stop(). - -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) -> - 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) -> - SendToTopic = <<"t_mqtt/one">>, - SendToTopic2 = <<"t_mqtt/two">>, - SendToTopic3 = <<"t_mqtt/three">>, - Mountpoint = <<"forwarded/${node}/">>, - 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:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Pid). - -t_stub_normal(Config) when is_list(Config) -> - 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_mqtt_sup:create_bridge(Cfg), - receive - {Pid, emqx_bridge_stub_conn, ready} -> ok - after - 5000 -> - error(timeout) - end, - {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 = emqx_bridge_worker:stop(Pid). - -t_stub_overflow(_Config) -> - Topic = <<"t_stub_overflow/one">>, - MaxInflight = 20, - 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) -> - Topic = <<"t_stub_random_order/a">>, - MaxInflight = 10, - 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">>, - {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) -> - Topic = <<"to_stub_retry_inflight/a">>, - MaxInflight = 10, - 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">>, - 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, []). - -stub_receive(0, Acc) -> lists:reverse(Acc); -stub_receive(N, Acc) -> - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - stub_receive(N - 1, [{WorkerPid, BatchRef} | Acc]) - after - 5000 -> - lists:reverse(Acc) - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl deleted file mode 100644 index ffa2e9ee5..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl +++ /dev/null @@ -1,135 +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_worker_tests). - --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - --define(BRIDGE_NAME, test). --define(BRIDGE_REG_NAME, emqx_bridge_worker_test). --define(WAIT(PATTERN, TIMEOUT), - receive - PATTERN -> - ok - after - TIMEOUT -> - error(timeout) - end). - -%% stub callbacks --export([start/1, send/2, stop/1]). - -start(#{connect_result := Result, test_pid := Pid, test_ref := Ref}) -> - case is_pid(Pid) of - true -> Pid ! {connection_start_attempt, Ref}; - false -> ok - end, - Result. - -send(SendFun, Batch) when is_function(SendFun, 2) -> - SendFun(Batch). - -stop(_Pid) -> ok. - -%% bridge worker should retry connecting remote node indefinitely -% reconnect_test() -> -% emqx_metrics:start_link(), -% emqx_bridge_worker:register_metrics(), -% Ref = make_ref(), -% Config = make_config(Ref, self(), {error, test}), -% {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), -% %% assert name registered -% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), -% ?WAIT({connection_start_attempt, Ref}, 1000), -% %% expect same message again -% ?WAIT({connection_start_attempt, Ref}, 1000), -% ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME), -% emqx_metrics:stop(), -% ok. - -%% connect first, disconnect, then connect again -disturbance_test() -> - emqx_metrics:start_link(), - 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(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(Pid). - -% % %% buffer should continue taking in messages when disconnected -% buffer_when_disconnected_test_() -> -% {timeout, 10000, fun test_buffer_when_disconnected/0}. - -% test_buffer_when_disconnected() -> -% Ref = make_ref(), -% Nums = lists:seq(1, 100), -% Sender = spawn_link(fun() -> receive {bridge, Pid} -> sender_loop(Pid, Nums, _Interval = 5) end end), -% SenderMref = monitor(process, Sender), -% Receiver = spawn_link(fun() -> receive {bridge, Pid} -> receiver_loop(Pid, Nums, _Interval = 1) end end), -% ReceiverMref = monitor(process, Receiver), -% SendFun = fun(Batch) -> -% BatchRef = make_ref(), -% Receiver ! {batch, BatchRef, Batch}, -% {ok, BatchRef} -% end, -% Config0 = make_config(Ref, false, {ok, #{client_pid => undefined}}), -% Config = Config0#{reconnect_delay_ms => 100}, -% emqx_metrics:start_link(), -% emqx_bridge_worker:register_metrics(), -% {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), -% Sender ! {bridge, Pid}, -% Receiver ! {bridge, Pid}, -% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), -% Pid ! {disconnected, Ref, test}, -% ?WAIT({'DOWN', SenderMref, process, Sender, normal}, 5000), -% ?WAIT({'DOWN', ReceiverMref, process, Receiver, normal}, 1000), -% ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME), -% emqx_metrics:stop(). - -manual_start_stop_test() -> - emqx_metrics:start_link(), - 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(Config#{name => BridgeName}), - %% call ensure_started again should yeld the same result - 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) -> - #{ - start_type => auto, - reconnect_interval => 50, - config => #{ - test_pid => TestPid, - test_ref => Ref, - conn_type => ?MODULE, - connect_result => Result - } - }. diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index cbeff37eb..fd2329cbd 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -17,7 +17,8 @@ %% By accident, We have always been using the upstream fork due to %% eredis_cluster's dependency getting resolved earlier. %% Here we pin 1.5.2 to avoid surprises in the future. - {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}} + {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}, + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} ]}. {shell, [ diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 5e1ca2ca8..f4481dc2c 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -13,7 +13,8 @@ epgsql, mysql, mongodb, - emqx + emqx, + emqtt ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_connector/src/emqx_connector_app.erl b/apps/emqx_connector/src/emqx_connector_app.erl index 64e6b8109..4de078076 100644 --- a/apps/emqx_connector/src/emqx_connector_app.erl +++ b/apps/emqx_connector/src/emqx_connector_app.erl @@ -21,6 +21,7 @@ -export([start/2, stop/1]). start(_StartType, _StartArgs) -> + emqx_connector_mqtt_worker:register_metrics(), emqx_connector_sup:start_link(). stop(_State) -> diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 1f2ee0b12..bbd347ae9 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -18,6 +18,16 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). +-behaviour(supervisor). + +%% API and callbacks for supervisor +-export([ start_link/0 + , init/1 + , create_bridge/1 + , drop_bridge/1 + , bridges/0 + ]). + %% callbacks of behaviour emqx_resource -export([ on_start/2 , on_stop/2 @@ -36,55 +46,42 @@ roots() -> [{config, #{type => hoconsc:ref(?MODULE, "config")}}]. fields("config") -> - [ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})} - , {reconnect_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} - , {proto_ver, fun proto_ver/1} - , {bridge_mode, hoconsc:mk(boolean(), #{default => true})} - , {clientid_prefix, hoconsc:mk(string(), #{default => ""})} - , {username, hoconsc:mk(string())} - , {password, hoconsc:mk(string())} - , {clean_start, hoconsc:mk(boolean(), #{default => true})} - , {keepalive, hoconsc:mk(integer(), #{default => 300})} - , {retry_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} - , {max_inflight, hoconsc:mk(integer(), #{default => 32})} - , {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))} - , {in, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "in")), #{default => []})} - , {out, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "out")), #{default => []})} - ] ++ emqx_connector_schema_lib:ssl_fields(); + emqx_connector_mqtt_schema:fields("config"). -fields("in") -> - [ {subscribe_remote_topic, #{type => binary(), nullable => false}} - , {local_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} - , {subscribe_qos, hoconsc:mk(qos(), #{default => 1})} - ] ++ common_inout_confs(); +%% =================================================================== +%% supervisor APIs +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). -fields("out") -> - [ {subscribe_local_topic, #{type => binary(), nullable => false}} - , {remote_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} - ] ++ common_inout_confs(); +init([]) -> + SupFlag = #{strategy => one_for_one, + intensity => 100, + period => 10}, + {ok, {SupFlag, []}}. -fields("replayq") -> - [ {dir, hoconsc:union([boolean(), string()])} - , {seg_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "100MB"})} - , {offload, hoconsc:mk(boolean(), #{default => false})} - , {max_total_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "1024MB"})} - ]. +bridge_spec(Config) -> + #{id => maps:get(name, Config), + start => {emqx_connector_mqtt_worker, start_link, [Config]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_connector_mqtt_worker]}. -common_inout_confs() -> - [{id, #{type => binary(), nullable => false}}] ++ publish_confs(). +-spec(bridges() -> [{node(), map()}]). +bridges() -> + [{Name, emqx_connector_mqtt_worker:status(Name)} + || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. -publish_confs() -> - [ {qos, hoconsc:mk(qos(), #{default => <<"${qos}">>})} - , {retain, hoconsc:mk(hoconsc:union([boolean(), binary()]), #{default => <<"${retain}">>})} - , {payload, hoconsc:mk(binary(), #{default => <<"${payload}">>})} - ]. +create_bridge(Config) -> + supervisor:start_child(?MODULE, bridge_spec(Config)). -qos() -> - hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2), binary()]). - -proto_ver(type) -> hoconsc:enum([v3, v4, v5]); -proto_ver(default) -> v4; -proto_ver(_) -> undefined. +drop_bridge(Name) -> + case supervisor:terminate_child(?MODULE, Name) of + ok -> + supervisor:delete_child(?MODULE, Name); + {error, Error} -> + {error, Error} + end. %% =================================================================== on_start(InstId, Conf) -> @@ -105,7 +102,7 @@ on_start(InstId, Conf) -> on_stop(InstId, #{}) -> logger:info("stopping mqtt connector: ~p", [InstId]), - case emqx_bridge_mqtt_sup:drop_bridge(InstId) of + case ?MODULE:drop_bridge(InstId) of ok -> ok; {error, not_found} -> ok; {error, Reason} -> @@ -124,7 +121,7 @@ on_query(InstId, {publish_to_remote, Msg}, _AfterQuery, _State) -> logger:debug("publish to remote node, connector: ~p, msg: ~p", [InstId, Msg]). on_health_check(_InstId, #{sub_bridges := NameList} = State) -> - Results = [{Name, emqx_bridge_worker:ping(Name)} || Name <- NameList], + Results = [{Name, emqx_connector_mqtt_worker:ping(Name)} || Name <- NameList], case lists:all(fun({_, pong}) -> true; ({_, _}) -> false end, Results) of true -> {ok, State}; false -> {error, {some_sub_bridge_down, Results}, State} @@ -155,7 +152,7 @@ create_channel(#{subscribe_local_topic := _, id := BridgeId} = OutConf, NamePref subscriptions => undefined, forwards => OutConf}). create_sub_bridge(#{name := Name} = Conf) -> - case emqx_bridge_mqtt_sup:create_bridge(Conf) of + case ?MODULE:create_bridge(Conf) of {ok, _Pid} -> start_sub_bridge(Name); {error, {already_started, _Pid}} -> @@ -165,7 +162,7 @@ create_sub_bridge(#{name := Name} = Conf) -> end. start_sub_bridge(Name) -> - case emqx_bridge_worker:ensure_started(Name) of + case emqx_connector_mqtt_worker:ensure_started(Name) of ok -> {ok, Name}; {error, Reason} -> {error, Reason} end. diff --git a/apps/emqx_connector/src/emqx_connector_sup.erl b/apps/emqx_connector/src/emqx_connector_sup.erl index 603b9a8ad..a24a97b8f 100644 --- a/apps/emqx_connector/src/emqx_connector_sup.erl +++ b/apps/emqx_connector/src/emqx_connector_sup.erl @@ -28,9 +28,19 @@ start_link() -> init([]) -> SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], + intensity => 5, + period => 20}, + ChildSpecs = [ + child_spec(emqx_connector_mqtt) + ], {ok, {SupFlags, ChildSpecs}}. +child_spec(Mod) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 3000, + type => supervisor, + modules => [Mod]}. + %% internal functions diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl similarity index 97% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index 2609e8bea..c8b7ff77b 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -16,7 +16,7 @@ %% @doc This module implements EMQX Bridge transport layer on top of MQTT protocol --module(emqx_bridge_mqtt). +-module(emqx_connector_mqtt_mod). -export([ start/1 , send/2 @@ -51,7 +51,7 @@ start(Config) -> {Host, Port} = maps:get(server, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), Subscriptions = maps:get(subscriptions, Config), - Vars = emqx_bridge_msg:make_pub_vars(Mountpoint, Subscriptions), + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), Handlers = make_hdlr(Parent, Vars), Config1 = Config#{ msg_handler => Handlers, @@ -161,7 +161,7 @@ handle_publish(Msg, undefined) -> ?LOG(error, "cannot publish to local broker as 'bridge.mqtt..in' not configured, msg: ~p", [Msg]); handle_publish(Msg, Vars) -> ?LOG(debug, "publish to local broker, msg: ~p, vars: ~p", [Msg, Vars]), - emqx_broker:publish(emqx_bridge_msg:to_broker_msg(Msg, Vars)). + emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars)). handle_disconnected(Reason, Parent) -> Parent ! {disconnected, self(), Reason}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl similarity index 92% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index e78844ed4..425fa06f1 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_msg). +-module(emqx_connector_mqtt_msg). -export([ to_binary/1 , from_binary/1 @@ -28,7 +28,6 @@ -include_lib("emqx/include/emqx.hrl"). --include_lib("emqx_bridge_mqtt/include/emqx_bridge_mqtt.hrl"). -include_lib("emqtt/include/emqtt.hrl"). @@ -56,13 +55,13 @@ make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(emqx_bridge_rpc | emqx_bridge_worker, msg(), variables()) +-spec to_remote_msg(emqx_bridge_rpc | emqx_connector_mqtt_mod, msg(), variables()) -> exp_msg(). -to_remote_msg(emqx_bridge_mqtt, #message{flags = Flags0} = Msg, Vars) -> +to_remote_msg(emqx_connector_mqtt_mod, #message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), MapMsg = maps:put(retain, Retain0, emqx_message:to_map(Msg)), - to_remote_msg(emqx_bridge_mqtt, MapMsg, Vars); -to_remote_msg(emqx_bridge_mqtt, MapMsg, #{topic := TopicToken, payload := PayloadToken, + to_remote_msg(emqx_connector_mqtt_mod, MapMsg, Vars); +to_remote_msg(emqx_connector_mqtt_mod, MapMsg, #{topic := TopicToken, payload := PayloadToken, qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) -> Topic = replace_vars_in_str(TopicToken, MapMsg), Payload = replace_vars_in_str(PayloadToken, MapMsg), diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl new file mode 100644 index 000000000..ed7fd4408 --- /dev/null +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -0,0 +1,78 @@ +%%-------------------------------------------------------------------- +%% 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_mqtt_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ roots/0 + , fields/1]). + +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, "config")}}]. + +fields("config") -> + [ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})} + , {reconnect_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} + , {proto_ver, fun proto_ver/1} + , {bridge_mode, hoconsc:mk(boolean(), #{default => true})} + , {clientid_prefix, hoconsc:mk(string(), #{default => ""})} + , {username, hoconsc:mk(string())} + , {password, hoconsc:mk(string())} + , {clean_start, hoconsc:mk(boolean(), #{default => true})} + , {keepalive, hoconsc:mk(integer(), #{default => 300})} + , {retry_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} + , {max_inflight, hoconsc:mk(integer(), #{default => 32})} + , {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))} + , {in, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "in")), #{default => []})} + , {out, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "out")), #{default => []})} + ] ++ emqx_connector_schema_lib:ssl_fields(); + +fields("in") -> + [ {subscribe_remote_topic, #{type => binary(), nullable => false}} + , {local_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} + , {subscribe_qos, hoconsc:mk(qos(), #{default => 1})} + ] ++ common_inout_confs(); + +fields("out") -> + [ {subscribe_local_topic, #{type => binary(), nullable => false}} + , {remote_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} + ] ++ common_inout_confs(); + +fields("replayq") -> + [ {dir, hoconsc:union([boolean(), string()])} + , {seg_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "100MB"})} + , {offload, hoconsc:mk(boolean(), #{default => false})} + , {max_total_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "1024MB"})} + ]. + +common_inout_confs() -> + [{id, #{type => binary(), nullable => false}}] ++ publish_confs(). + +publish_confs() -> + [ {qos, hoconsc:mk(qos(), #{default => <<"${qos}">>})} + , {retain, hoconsc:mk(hoconsc:union([boolean(), binary()]), #{default => <<"${retain}">>})} + , {payload, hoconsc:mk(binary(), #{default => <<"${payload}">>})} + ]. + +qos() -> + hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2), binary()]). + +proto_ver(type) -> hoconsc:enum([v3, v4, v5]); +proto_ver(default) -> v4; +proto_ver(_) -> undefined. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl similarity index 97% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index ac861d08f..83f7ce746 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -19,7 +19,7 @@ %% to remote MQTT node/cluster via `connection' transport layer. %% In case `REMOTE' is also an EMQX node, `connection' is recommended to be %% the `gen_rpc' based implementation `emqx_bridge_rpc'. Otherwise `connection' -%% has to be `emqx_bridge_mqtt'. +%% has to be `emqx_connector_mqtt_mod'. %% %% ``` %% +------+ +--------+ @@ -59,7 +59,7 @@ %% NOTES: %% * Local messages are all normalised to QoS-1 when exporting to remote --module(emqx_bridge_worker). +-module(emqx_connector_mqtt_worker). -behaviour(gen_statem). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -106,7 +106,7 @@ -type id() :: atom() | string() | pid(). -type qos() :: emqx_mqtt_types:qos(). -type config() :: map(). --type batch() :: [emqx_bridge_msg:exp_msg()]. +-type batch() :: [emqx_connector_mqtt_msg:exp_msg()]. -type ack_ref() :: term(). -type topic() :: emqx_topic:topic(). @@ -222,7 +222,7 @@ open_replayq(Name, QCfg) -> false -> #{dir => filename:join([Dir, node(), Name]), seg_bytes => SegBytes, max_total_size => MaxTotalSize} end, - replayq:open(QueueConfig#{sizer => fun emqx_bridge_msg:estimate_size/1, + replayq:open(QueueConfig#{sizer => fun emqx_connector_mqtt_msg:estimate_size/1, marshaller => fun ?MODULE:msg_marshaller/1}). pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) -> @@ -412,10 +412,10 @@ do_send(#{inflight := Inflight, mountpoint := Mountpoint, connect_opts := #{forwards := Forwards}, if_record_metrics := IfRecordMetrics} = State, QAckRef, [_ | _] = Batch) -> - Vars = emqx_bridge_msg:make_pub_vars(Mountpoint, Forwards), + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), ExportMsg = fun(Message) -> bridges_metrics_inc(IfRecordMetrics, 'bridge.mqtt.message_sent'), - emqx_bridge_msg:to_remote_msg(Module, Message, Vars) + emqx_connector_mqtt_msg:to_remote_msg(Module, Message, Vars) end, ?LOG(debug, "publish to remote broker, msg: ~p, vars: ~p", [Batch, Vars]), case Module:send(Connection, [ExportMsg(M) || M <- Batch]) of @@ -501,8 +501,8 @@ disconnect(State) -> State. %% Called only when replayq needs to dump it to disk. -msg_marshaller(Bin) when is_binary(Bin) -> emqx_bridge_msg:from_binary(Bin); -msg_marshaller(Msg) -> emqx_bridge_msg:to_binary(Msg). +msg_marshaller(Bin) when is_binary(Bin) -> emqx_connector_mqtt_msg:from_binary(Bin); +msg_marshaller(Msg) -> emqx_connector_mqtt_msg:to_binary(Msg). format_mountpoint(undefined) -> undefined; @@ -541,7 +541,7 @@ is_sensitive(_) -> false. conn_type(rpc) -> emqx_bridge_rpc; conn_type(mqtt) -> - emqx_bridge_mqtt; + emqx_connector_mqtt_mod; conn_type(Mod) when is_atom(Mod) -> Mod. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl similarity index 89% rename from apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl rename to apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl index 5babe0ed9..7943f5a77 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl +++ b/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl @@ -37,11 +37,11 @@ send_and_ack_test() -> try Max = 1, Batch = lists:seq(1, Max), - {ok, Conn} = emqx_bridge_mqtt:start(#{address => "127.0.0.1:1883"}), + {ok, Conn} = emqx_connector_mqtt_mod:start(#{address => "127.0.0.1:1883"}), % %% return last packet id as batch reference - {ok, _AckRef} = emqx_bridge_mqtt:send(Conn, Batch), + {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), - ok = emqx_bridge_mqtt:stop(Conn) + ok = emqx_connector_mqtt_mod:stop(Conn) after meck:unload(emqtt) end. diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 1f5d639cd..614609875 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -48,7 +48,6 @@ , emqx_statsd_schema , emqx_authz_schema , emqx_auto_subscribe_schema - , emqx_bridge_mqtt_schema , emqx_modules_schema , emqx_dashboard_schema , emqx_gateway_schema 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 fc5f89091..ce1192579 100644 --- a/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl +++ b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl @@ -411,7 +411,7 @@ test_resource_status(PoolName) -> IsConnected = fun(Worker) -> case ecpool_worker:client(Worker) of {ok, Bridge} -> - try emqx_bridge_worker:status(Bridge) of + try emqx_connector_mqtt_worker:status(Bridge) of connected -> true; _ -> false catch _Error:_Reason -> @@ -524,7 +524,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(Options2#{name => name(Pool, Id)}). + emqx_connector_mqtt_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/rebar.config.erl b/rebar.config.erl index 65b469c1c..3f4d86f37 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -272,7 +272,6 @@ relx_apps(ReleaseType) -> , emqx_bridge , emqx_rule_engine , emqx_rule_actions - , emqx_bridge_mqtt , emqx_modules , emqx_management , emqx_dashboard From 1ecec5ef3a32b3c25182ae4e2a2fb6208c28379b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 9 Sep 2021 19:23:06 +0800 Subject: [PATCH 304/306] refactor(bridges): move some test cases from old emqx_bridge_mqtt app --- .../src/emqx_connector_mqtt.erl | 1 - .../src/mqtt/emqx_connector_mqtt_mod.erl | 2 +- .../src/mqtt/emqx_connector_mqtt_msg.erl | 12 +- .../src/mqtt/emqx_connector_mqtt_worker.erl | 46 ++---- ...ests.erl => emqx_connector_mqtt_tests.erl} | 4 +- .../test/emqx_connector_mqtt_worker_tests.erl | 149 ++++++++++++++++++ ...TE.erl => emqx_plugin_libs_rule_SUITE.erl} | 2 +- 7 files changed, 173 insertions(+), 43 deletions(-) rename apps/emqx_connector/test/{emqx_connetor_mqtt_tests.erl => emqx_connector_mqtt_tests.erl} (93%) create mode 100644 apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl rename apps/emqx_plugin_libs/test/{emqx_rule_libs_rule_SUITE.erl => emqx_plugin_libs_rule_SUITE.erl} (99%) diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index bbd347ae9..6631fd23a 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -182,7 +182,6 @@ basic_config(#{ replayq := ReplayQ, ssl := #{enable := EnableSsl} = Ssl}) -> #{ - conn_type => mqtt, replayq => ReplayQ, %% connection opts server => Server, diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index c8b7ff77b..3de7feac4 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -50,7 +50,7 @@ start(Config) -> Parent = self(), {Host, Port} = maps:get(server, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), - Subscriptions = maps:get(subscriptions, Config), + Subscriptions = maps:get(subscriptions, Config, undefined), Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), Handlers = make_hdlr(Parent, Vars), Config1 = Config#{ diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index 425fa06f1..7f8435fd1 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -19,7 +19,7 @@ -export([ to_binary/1 , from_binary/1 , make_pub_vars/2 - , to_remote_msg/3 + , to_remote_msg/2 , to_broker_msg/2 , estimate_size/1 ]). @@ -55,13 +55,13 @@ make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(emqx_bridge_rpc | emqx_connector_mqtt_mod, msg(), variables()) +-spec to_remote_msg(msg(), variables()) -> exp_msg(). -to_remote_msg(emqx_connector_mqtt_mod, #message{flags = Flags0} = Msg, Vars) -> +to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), MapMsg = maps:put(retain, Retain0, emqx_message:to_map(Msg)), - to_remote_msg(emqx_connector_mqtt_mod, MapMsg, Vars); -to_remote_msg(emqx_connector_mqtt_mod, MapMsg, #{topic := TopicToken, payload := PayloadToken, + to_remote_msg(MapMsg, Vars); +to_remote_msg(MapMsg, #{topic := TopicToken, payload := PayloadToken, qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) -> Topic = replace_vars_in_str(TopicToken, MapMsg), Payload = replace_vars_in_str(PayloadToken, MapMsg), @@ -72,7 +72,7 @@ to_remote_msg(emqx_connector_mqtt_mod, MapMsg, #{topic := TopicToken, payload := topic = topic(Mountpoint, Topic), props = #{}, payload = Payload}; -to_remote_msg(_Module, #message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) -> +to_remote_msg(#message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) -> Msg#message{topic = topic(Mountpoint, Topic)}. %% published from remote node over a MQTT connection diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 83f7ce746..6ced719df 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -185,12 +185,10 @@ callback_mode() -> [state_functions]. init(#{name := Name} = ConnectOpts) -> ?LOG(info, "starting bridge worker for ~p", [Name]), erlang:process_flag(trap_exit, true), - ConnectModule = conn_type(maps:get(conn_type, ConnectOpts)), Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})), State = init_state(ConnectOpts), self() ! idle, {ok, idle, State#{ - connect_module => ConnectModule, connect_opts => pre_process_opts(ConnectOpts), replayq => Queue }}. @@ -311,15 +309,13 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> {keep_state_and_data, [{reply, From, StateName}]}; -common(_StateName, {call, From}, ping, #{connection := Conn, - connect_module := ConnectModule} =_State) -> - Reply = ConnectModule:ping(Conn), +common(_StateName, {call, From}, ping, #{connection := Conn} =_State) -> + Reply = emqx_connector_mqtt_mod:ping(Conn), {keep_state_and_data, [{reply, From, Reply}]}; 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), +common(_StateName, {call, From}, ensure_stopped, #{connection := Conn} = State) -> + Reply = emqx_connector_mqtt_mod:stop(Conn), {next_state, idle, State#{connection => undefined}, [{reply, From, Reply}]}; common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := Forwards}}) -> {keep_state_and_data, [{reply, From, Forwards}]}; @@ -341,22 +337,21 @@ common(StateName, Type, Content, #{name := Name} = State) -> [Name, Type, StateName, Content]), {keep_state, State}. -do_connect(#{connect_module := ConnectModule, - connect_opts := ConnectOpts = #{forwards := Forwards}, +do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards}, inflight := Inflight, name := Name} = State) -> case Forwards of undefined -> ok; #{subscribe_local_topic := Topic} -> subscribe_local_topic(Topic, Name) end, - case ConnectModule:start(ConnectOpts) of + case emqx_connector_mqtt_mod:start(ConnectOpts) of {ok, Conn} -> ?tp(info, connected, #{name => Name, inflight => length(Inflight)}), {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]), + ?LOG(error, "Failed to connect \n" + "config=~p\nreason:~p", [ConnectOpts1, Reason]), {error, Reason, State} end. @@ -385,16 +380,13 @@ pop_and_send(#{inflight := Inflight, max_inflight := Max} = State) -> pop_and_send_loop(State, 0) -> ?tp(debug, inflight_full, #{}), {ok, State}; -pop_and_send_loop(#{replayq := Q, connect_module := Module} = State, N) -> +pop_and_send_loop(#{replayq := Q} = State, N) -> case replayq:is_empty(Q) of true -> ?tp(debug, replayq_drained, #{}), {ok, State}; false -> - BatchSize = case Module of - emqx_bridge_rpc -> maps:get(batch_size, State); - _ -> 1 - end, + BatchSize = 1, Opts = #{count_limit => BatchSize, bytes_limit => 999999999}, {Q1, QAckRef, Batch} = replayq:pop(Q, Opts), case do_send(State#{replayq := Q1}, QAckRef, Batch) of @@ -407,7 +399,6 @@ pop_and_send_loop(#{replayq := Q, connect_module := Module} = State, N) -> do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Batch) -> ?LOG(error, "cannot forward messages to remote broker as 'bridge.mqtt..in' not configured, msg: ~p", [Batch]); do_send(#{inflight := Inflight, - connect_module := Module, connection := Connection, mountpoint := Mountpoint, connect_opts := #{forwards := Forwards}, @@ -415,10 +406,10 @@ do_send(#{inflight := Inflight, Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), ExportMsg = fun(Message) -> bridges_metrics_inc(IfRecordMetrics, 'bridge.mqtt.message_sent'), - emqx_connector_mqtt_msg:to_remote_msg(Module, Message, Vars) + emqx_connector_mqtt_msg:to_remote_msg(Message, Vars) end, ?LOG(debug, "publish to remote broker, msg: ~p, vars: ~p", [Batch, Vars]), - case Module:send(Connection, [ExportMsg(M) || M <- Batch]) of + case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(M) || M <- Batch]) of {ok, Refs} -> {ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef, send_ack_ref => map_set(Refs), @@ -492,10 +483,8 @@ do_subscribe(RawTopic, Name) -> {Topic, SubOpts} = emqx_topic:parse(TopicFilter, #{qos => ?QOS_2}), emqx_broker:subscribe(Topic, Name, SubOpts). -disconnect(#{connection := Conn, - connect_module := Module - } = State) when Conn =/= undefined -> - Module:stop(Conn), +disconnect(#{connection := Conn} = State) when Conn =/= undefined -> + emqx_connector_mqtt_mod:stop(Conn), State#{connection => undefined}; disconnect(State) -> State. @@ -538,13 +527,6 @@ obfuscate(Map) -> is_sensitive(password) -> true; is_sensitive(_) -> false. -conn_type(rpc) -> - emqx_bridge_rpc; -conn_type(mqtt) -> - emqx_connector_mqtt_mod; -conn_type(Mod) when is_atom(Mod) -> - Mod. - str(A) when is_atom(A) -> atom_to_list(A); str(B) when is_binary(B) -> diff --git a/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl similarity index 93% rename from apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl rename to apps/emqx_connector/test/emqx_connector_mqtt_tests.erl index 7943f5a77..0f4d651c9 100644 --- a/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl +++ b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_mqtt_tests). +-module(emqx_connector_mqtt_tests). -include_lib("eunit/include/eunit.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -37,7 +37,7 @@ send_and_ack_test() -> try Max = 1, Batch = lists:seq(1, Max), - {ok, Conn} = emqx_connector_mqtt_mod:start(#{address => "127.0.0.1:1883"}), + {ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127,0,0,1}, 1883}}), % %% return last packet id as batch reference {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), diff --git a/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl new file mode 100644 index 000000000..090106cef --- /dev/null +++ b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl @@ -0,0 +1,149 @@ +%%-------------------------------------------------------------------- +%% 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_mqtt_worker_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). + +-define(BRIDGE_NAME, test). +-define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test). +-define(WAIT(PATTERN, TIMEOUT), + receive + PATTERN -> + ok + after + TIMEOUT -> + error(timeout) + end). + +-export([start/1, send/2, stop/1]). + +start(#{connect_result := Result, test_pid := Pid, test_ref := Ref}) -> + case is_pid(Pid) of + true -> Pid ! {connection_start_attempt, Ref}; + false -> ok + end, + Result. + +send(SendFun, Batch) when is_function(SendFun, 2) -> + SendFun(Batch). + +stop(_Pid) -> ok. + +%% bridge worker should retry connecting remote node indefinitely +% reconnect_test() -> +% emqx_metrics:start_link(), +% emqx_connector_mqtt_worker:register_metrics(), +% Ref = make_ref(), +% Config = make_config(Ref, self(), {error, test}), +% {ok, Pid} = emqx_connector_mqtt_worker:start_link(?BRIDGE_NAME, Config), +% %% assert name registered +% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), +% ?WAIT({connection_start_attempt, Ref}, 1000), +% %% expect same message again +% ?WAIT({connection_start_attempt, Ref}, 1000), +% ok = emqx_connector_mqtt_worker:stop(?BRIDGE_REG_NAME), +% emqx_metrics:stop(), +% ok. + +%% connect first, disconnect, then connect again +disturbance_test() -> + meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), + meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), + meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), + meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), + try + emqx_metrics:start_link(), + emqx_connector_mqtt_worker:register_metrics(), + Ref = make_ref(), + TestPid = self(), + Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => bridge_disturbance}), + ?assertEqual(Pid, whereis(bridge_disturbance)), + ?WAIT({connection_start_attempt, Ref}, 1000), + Pid ! {disconnected, TestPid, test}, + ?WAIT({connection_start_attempt, Ref}, 1000), + emqx_metrics:stop(), + ok = emqx_connector_mqtt_worker:stop(Pid) + after + meck:unload(emqx_connector_mqtt_mod) + end. + +% % %% buffer should continue taking in messages when disconnected +% buffer_when_disconnected_test_() -> +% {timeout, 10000, fun test_buffer_when_disconnected/0}. + +% test_buffer_when_disconnected() -> +% Ref = make_ref(), +% Nums = lists:seq(1, 100), +% Sender = spawn_link(fun() -> receive {bridge, Pid} -> sender_loop(Pid, Nums, _Interval = 5) end end), +% SenderMref = monitor(process, Sender), +% Receiver = spawn_link(fun() -> receive {bridge, Pid} -> receiver_loop(Pid, Nums, _Interval = 1) end end), +% ReceiverMref = monitor(process, Receiver), +% SendFun = fun(Batch) -> +% BatchRef = make_ref(), +% Receiver ! {batch, BatchRef, Batch}, +% {ok, BatchRef} +% end, +% Config0 = make_config(Ref, false, {ok, #{client_pid => undefined}}), +% Config = Config0#{reconnect_delay_ms => 100}, +% emqx_metrics:start_link(), +% emqx_connector_mqtt_worker:register_metrics(), +% {ok, Pid} = emqx_connector_mqtt_worker:start_link(?BRIDGE_NAME, Config), +% Sender ! {bridge, Pid}, +% Receiver ! {bridge, Pid}, +% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), +% Pid ! {disconnected, Ref, test}, +% ?WAIT({'DOWN', SenderMref, process, Sender, normal}, 5000), +% ?WAIT({'DOWN', ReceiverMref, process, Receiver, normal}, 1000), +% ok = emqx_connector_mqtt_worker:stop(?BRIDGE_REG_NAME), +% emqx_metrics:stop(). + +manual_start_stop_test() -> + meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), + meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), + meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), + meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), + try + emqx_metrics:start_link(), + emqx_connector_mqtt_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_connector_mqtt_worker:start_link(Config#{name => BridgeName}), + %% call ensure_started again should yeld the same result + ok = emqx_connector_mqtt_worker:ensure_started(BridgeName), + emqx_connector_mqtt_worker:ensure_stopped(BridgeName), + emqx_metrics:stop(), + ok = emqx_connector_mqtt_worker:stop(Pid) + after + meck:unload(emqx_connector_mqtt_mod) + end. + +make_config(Ref, TestPid, Result) -> + #{ + start_type => auto, + subscriptions => undefined, + forwards => undefined, + reconnect_interval => 50, + test_pid => TestPid, + test_ref => Ref, + connect_result => Result + }. diff --git a/apps/emqx_plugin_libs/test/emqx_rule_libs_rule_SUITE.erl b/apps/emqx_plugin_libs/test/emqx_plugin_libs_rule_SUITE.erl similarity index 99% rename from apps/emqx_plugin_libs/test/emqx_rule_libs_rule_SUITE.erl rename to apps/emqx_plugin_libs/test/emqx_plugin_libs_rule_SUITE.erl index e4c358695..56733147f 100644 --- a/apps/emqx_plugin_libs/test/emqx_rule_libs_rule_SUITE.erl +++ b/apps/emqx_plugin_libs/test/emqx_plugin_libs_rule_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_rule_utils_SUITE). +-module(emqx_plugin_libs_rule_SUITE). -compile(export_all). -compile(nowarn_export_all). From 135c005467d7fca6675e316c7aa18394256ff067 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 10 Sep 2021 09:21:06 +0800 Subject: [PATCH 305/306] fix(bridges): do not start any bridge by default --- apps/emqx_bridge/etc/emqx_bridge.conf | 207 ++++-------------- .../src/mqtt/emqx_connector_mqtt_msg.erl | 2 +- .../src/emqx_rule_runtime.erl | 1 - 3 files changed, 44 insertions(+), 166 deletions(-) diff --git a/apps/emqx_bridge/etc/emqx_bridge.conf b/apps/emqx_bridge/etc/emqx_bridge.conf index 2844af3bf..08873228d 100644 --- a/apps/emqx_bridge/etc/emqx_bridge.conf +++ b/apps/emqx_bridge/etc/emqx_bridge.conf @@ -2,169 +2,48 @@ ## EMQ X Bridge ##-------------------------------------------------------------------- -bridges.mqtt.my_mqtt_bridge { - server = "127.0.0.1:1883" - proto_ver = "v4" - ## the clientid will be the concatenation of `clientid_prefix` and ids in `in` and `out`. - clientid_prefix = "bridge_client:" - username = "username1" - password = "" - clean_start = true - keepalive = 300 - retry_interval = "30s" - max_inflight = 32 - reconnect_interval = "30s" - bridge_mode = true - replayq { - dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" - seg_bytes = "100MB" - offload = false - max_total_bytes = "1GB" - } - ssl { - enable = false - keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" - certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" - cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - } - ## we will create one MQTT connection for each element of the `in` - in: [{ - id = "pull_msgs_from_aws" - subscribe_remote_topic = "aws/#" - subscribe_qos = 1 - local_topic = "from_aws/${topic}" - payload = "${payload}" - qos = "${qos}" - retain = "${retain}" - }] - ## we will create one MQTT connection for each element of the `out` - out: [{ - id = "push_msgs_to_aws" - subscribe_local_topic = "emqx/#" - remote_topic = "from_emqx/${topic}" - payload = "${payload}" - qos = 1 - retain = false - }] -} - -# {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 -# } +#bridges.mqtt.my_mqtt_bridge { +# server = "127.0.0.1:1883" +# proto_ver = "v4" +# ## the clientid will be the concatenation of `clientid_prefix` and ids in `in` and `out`. +# clientid_prefix = "bridge_client:" +# username = "username1" +# password = "" +# clean_start = true +# keepalive = 300 +# retry_interval = "30s" +# max_inflight = 32 +# reconnect_interval = "30s" +# bridge_mode = true +# replayq { +# dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" +# seg_bytes = "100MB" +# offload = false +# max_total_bytes = "1GB" # } -# , {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 -# } +# ssl { +# enable = false +# keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" +# certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" +# cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" # } -# , {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 -# } -# } +# ## we will create one MQTT connection for each element of the `in` +# in: [{ +# id = "pull_msgs_from_aws" +# subscribe_remote_topic = "aws/#" +# subscribe_qos = 1 +# local_topic = "from_aws/${topic}" +# payload = "${payload}" +# qos = "${qos}" +# retain = "${retain}" +# }] +# ## we will create one MQTT connection for each element of the `out` +# out: [{ +# id = "push_msgs_to_aws" +# subscribe_local_topic = "emqx/#" +# remote_topic = "from_emqx/${topic}" +# payload = "${payload}" +# qos = 1 +# retain = false +# }] +#} diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index 7f8435fd1..5f076ed9e 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -55,7 +55,7 @@ make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(msg(), variables()) +-spec to_remote_msg(msg() | map(), variables()) -> exp_msg(). to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), diff --git a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl index e8dcc8a58..f9e210ab3 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl @@ -222,7 +222,6 @@ do_compare('<=', L, R) -> L =< R; do_compare('>=', L, R) -> L >= R; do_compare('<>', L, R) -> L /= R; do_compare('!=', L, R) -> L /= R; -do_compare('~=', T, F) -> emqx_topic:match(T, F); do_compare('=~', T, F) -> emqx_topic:match(T, F). number(Bin) -> From 07069898b12a0364228c4b77bf1e08b94232680e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 10 Sep 2021 09:22:00 +0800 Subject: [PATCH 306/306] fix(README): update docs for running ct on single app --- 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 fa068fc29..80e926199 100644 --- a/README-CN.md +++ b/README-CN.md @@ -90,7 +90,7 @@ make eunit ct ### 执行部分应用的 common tests ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### 静态分析(Dialyzer) diff --git a/README-JP.md b/README-JP.md index 7e276e9eb..57c9a1809 100644 --- a/README-JP.md +++ b/README-JP.md @@ -84,7 +84,7 @@ make eunit ct ### common test の一部を実行する ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/README-RU.md b/README-RU.md index 093b49782..e02f47aa4 100644 --- a/README-RU.md +++ b/README-RU.md @@ -93,7 +93,7 @@ make eunit ct Пример: ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/README.md b/README.md index 7f445b226..f60ed3cd9 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ make eunit ct Examples ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### Dialyzer

a912#5(-*?bdR2Qu%$GFVH%@Yzp-`P0-bI1<5sR%)sTGknL@5%&o=Z;@B;VBi zG(+}Q;1r}oh>7DbIObWThjcB^_=xJ(l{#l!(XZPwCF65zw;NPNjB7)RrbJ%<9CYMm z<$+y?{dBE} zcO3Wx@4T~~_g+!^p(-~{;or#oJSuFJlUSfOC?OLb$1D)nO%O zcOH9s$?MMyX;?dT)EKrXaV;t^_Q0~f@_z72b5-6y7)R`oi>O5=)?Ko`<$b3ecLQi~ zSL1$L`_15Z%}w6ElN=|*M#Fxdu3Is5uY+SViZAD-5N*3NMX>Ee`Tc;!m?S4>*xU zN3FB`8wogF~N+f((rJwFx6szj@WKF)@qVHV2 z9U_n|9^azx*clXTSkf=cS{;@QLcb5#SS%-Cs3!m-O`p!2lD(N#cWFuFWd-Uf_7kNvoJ@Po7buy!?=;4wIRe@DA=7HCj$)x7W6-<2Tl zrO7zp@D{V%Beuyqr*ewTv;^9huWS!3T8DPw%i!3N!w$C)_eNI3KZtLfV}Cd*!*!MU z9?SeD7g>xhmn{|!pSo6=8IX??_mCDUYrh-8Tj%??p5x`{jv_zBbh*lvFHY6mR>)0u zqNIuG{Xe0;;s(mFyDp_Iuc%MUDA$EACkmW*%CbFP?;Cz_a%}Bg<%wo@UHjZv=)EKz z+k%QA=*iVUjhL~?LvY}RKL&xRb;>qui&poF5A?{y$HRv&Vf{hNE|saP2y>#g+{p43 zmRu?mU)sRO2I6K;>S448-SFV}CI0MH|E>V=Ml#T}>FXNmwpD&Vael$X@qH zWoJ}~_*DE;I&mX)(Voy6m-TKxlPOKn0hdOLtaW7`Jxh8-Z>`iPaZ@Uzx-r=mmp%9y8$!vTx6_Xbv0h6=&Jl+qkCcO+~44l&d3jP0wpcU_K#`nS6? zW-6oZ+Q}H60Y6q%1h7tG4@}n)Hc*6S$J3=c@n!(e&NXQrZ|=CNN8W2$z!wGU2jSNT zteH1Cb)(%3zrE$phRPWj+uYb01!rVS0f+W!Ti+#1T3w@>j_pC3w3s-0FeD__#u9jzYt@g2nQxmM6DPbv#+|U4M=3 zmS@-U??mY0^0mfy^r~`@6W$JVAOShaInq@0S7q@FeI-Nlt+LgSowl}H!=lREeiMb; z?_W``81nx*;f60|$rZ@FY4TRJGPh}MA|R$TlzaShQI#fZOojG)5NRStmb13GV2(u@ zY{4KHl3v+e|0>)|1>x?odRK8Oc6XVf25eTMrPW0V55i6w$zV zwacY)RPyu#{TXA+9m$CoM_-h5)j%R1>0fn|X@$Z)zS{)AoP=bZLs!gKjlVnPjvkI% zH>wKOUj6e@_woGYj-A2Tiyndb9*nH<^|Du`LY|jre0HXgc=g|{M;b+U*XIB;*vwUC z89T_L6&jT}sW7Zx+KMP3Ogjv%5b{26>=2E*K(LHALL|1ZdRy&`h#Fjc6scjoGb(45 zAl7bXf5Nh#OMEdl1DHyAoR$wGQk zk(YjLCqGx>o+rNJ8?xVpczAPV(cQ)K2PFM$iUK$H>lvSyXGJLWy%;;w-h z2o|%ou`}>{xt6O2~cxvl~UUa1~5b=|8I;+=<;cYRtj z?N9^1Wi>Tu{g*3m+fw|f-X>+Yd0dOvsA-cuf>f2cHg6|)fIhC-Q*rUJq#h%7lw$fs z9TMD67{jK;Hw)tI`AZuhhTHjUx=*JzIr7Sh3#=MrLizi%=+M?qOBKpkVDq@o@ZOB6 zLe5wx1BPsxB!SfjS_}O>x#^zuC*}Us{969*v3TOGy4~xQnhHoj*%W>BZp7GBe8@hSK=N3|ADI_5vygUb>}r{V=ja5BY9>EN ztU%a@TnWs!)t$p$?fG%%Ya0*xSxBQk!wN3^T{6q%MOdbam|mfM`|ph7Xb%O{$Yr}A z8ii7~ZMUD#<*$Q*<_ay}_SC>Vlp*!HvL>B@vc75Fb^OI*HrJY{aE)JL#)lK9#&cZB zQOX^zKsy&n%>CvMrkj=wo1=JzWWsx$88DfqDo?^0`^Z817A~Hb5G&X*JrTRTiR-yK zIP_H3&^Pok4OUWD5Zdk)akVa82j)4OnqV}V!LzngayjWf=?-jBSA->~Km^pyk|*!n z4WVGflYV$k{o)9Z^At<)!G0-Cw7KA)YFp+JF4{%u<8UfK3uDiZx~IS$ACSRke#-dh z6nD94JbMH6SqqB}>OA(VUOm5)M6LZ_e~DPBJXFwI!&Zds5CXj(xomc6ARXdk0-zOB zUDykyG7J=%EcptOf(3Rdd!wI-mDF(7ZwOwCOz+E8bytAqV`7IoSN)nzZ&gn{V>EiJ z%EC#*_F z^dPW3`R}3YFE|V$NbFnR4>^(Q5dHVL?h3)#QVPU2xlsC7aE<{$7|CwZ+cpRn^kMlh zPTi)4Y!%$gr8grBJK9BS06oXWow}W6nfx7C}E$`Jo`M)N4mk?uYuV zDffO)+j{WaIB}#7@?AtHR4O`#!z%9Y5b8!2PI7bFFgMl&{_}ap%O^3tEz4&O!r%DU zJqZ__i@Pk8iJCX`?R8SEwjrpGx}Th=6{ePK_s~<#-3BW zHH0DK^Vv(eRpP8&=?^hkWoCvXF&)`LCjGd*R-~>n;1=<9u*aFexCDodwzp5Ydgp7n z`VQI<1avz|)dExcT#k_p;zuLS6td<<^LPYr8amz0Q*`>G6XQ_{b^vUS;^&Xuy!u~? z12=oS_oI{ELlp#C@?Dl0qUBmoiL&b#8{dDnE0L6VF8g}HyuFO)S_EBnqGJ@8vyqMN z32N|)zH-q~>*pKHX~V3RKp10RVm_tqDzZ0ju6BEWfZNxsKz11cE8jc#1jm+qt&YHo zAR_2#xiQlbeo0ueJYEgXev95cwM%6ipH1HTaS$dxU z?bmlp*2?4AoXr(CYi)t;x7mr6;FAo>_z(k9QijUj{K#Z8J&cYkdYRo~mySR!U#{RZ zbsF*Nsir3OmVrv8aG#~^_IK7L54{|glZ3GT25w&@ngmj0IkPfDe6Ey9iLHZzLo+^k zXb_qRlqR(esx`G}v^PyEBSpcME>TX!uy~;8pf|Td`$At=|CDl}6H3P#srTkbq1t>AzXglc zOnfVung}+tN4Hb^xM7vR-hs4Ou|Yrv?s#XOH zEwqATbrP%JsywLw!Yj_;^!$CkJrqZ@cs4%;D(mmBBlUD&3l-6Gj3Df|vw|sXl}-9# zo~D#GRa*nR{q5(b>X43g>^E7;Zo|bMT+=#V$;y40_b0-cwU-x}I@Uk;F5Bko@3VfN zY?N>i(>XTV#W!pKAvb&vnE}aoLwZtm;Ef>e9$PA4$wm%a=_YAsZF`vN{St7uf3(0L zNp|tcE3rnfP+09iDpjm)-Fi$c@XgLS_4y{Ext>qM`Sh;!y*I-;S`eHV;IOq7bvvM+ zal#UcS?dJPTQ@xTDvpDsz!ljoDvd|I$X5fH@#^KGfFPFQ~X z`q{vhR~)&qSnMA+(LwvHt+Kjr(E{1V7KExmQJCT#&F2J%n%K%8c9dMUrJKQI(`}+! z{D@;~T~kKDtfiZ5mJvk1uVekm+NzPZ4k&S?$zjig58E?1^zBs4t^L;QSQjMK^D=o0ZPRCRF~TNZS=5xF9?JklVV^(s z1iKCvox_NxE8h9M)f@+Gonjabx787?%zETRlv z7L+tOt~_ITVI0!fruil*4r*=}yqHM;+s*8v`0ZpT{@PvDx7?6L!gh}Ai^+b{T6?1{ zt}e*-u2!G#Or}nQ?JX*|e0{f=cGtR~=HuQTL_zh86TZ9QuHBk!_6*6x*QJ$t?78q^~d$bA9+U)5%QT?*-mvX}D4wn?FHA9Bw`+{-BI=@gP5Tfnb+@=u)ZeDUqLHCMz=DcLcpme`*s) zWWcP4OFvC4O%bQLW=y612jlReN1>|;$j~h8{r2^U2JxAgGgk@o&-@s_oo1c7iH z|K5XHG;d}o%n2O}=EevZk2|WK{A8OXSWsL;j0zXX<`Co*>ND&DwFGo5v@Du)$Fdf_ zSKsS7EpItd!7w;(<(qcWUBYwb(nSVJvydtn`p_mCrFIrU`F%oK-!h89tZ|0hLlKch zIIBN9^N`i1Q^?R`)D_6f1;xvKT(-QUBR?ow70s%XEVQd@n;eP3AI_51A?BcLz{&rz zgw}ezE|3j7x%3q{IQWhPDBT#cjbblmmh!p+n^Ypqw(Poub}}Mb;uPyGhNO&JmolgcPr=XV<`u{KHo6CcsO<{WVJTKby|CkT@{tJS& z^8$0P+ipqAE=qqjZSkH6CG>IrFfEA1z7KCcR^a1YTo98QMSlj7wfFhvmk=-0XmxdxQpZR8m%*kn@iSfz((6_MVu>JXG}rojd+ zOHOUJ{~Pr$j6cLIB}|{SJYh+zGt}$0_;h*Sc4Kb0{T-57ao1eGiWru5F?S^Rxm~Ka zI-5<6YKgZpP;P5@k`6+%js!n-2zkzON+k?*0O-o3SGL?b6epiy=kX8zoJe#C>b$u0 zcFDx4_SPj>N@6pXy1hz0Eu^K)8{(Iw;W7ETOtl3`^n?>npq8s=-x=3!YW&$sivKhl zDdCrs2w(7cOs8v}I?Wuq_+7i~)?5DO&xWV4Fzz$@ktnqY1XbVAtLV_>GMhp4)>LPd_`_ zH+H8b$_KxHMYEDXGIOP>J`;^2xOPP~rO#9;U8HoI-FNO-DW}Vc6hinFjxp4rEdD7d zeVOs8q>_%E+8p`8CYS%TF^ZHrY{h_iRJ*(2kjtN4AEhJLV7s^iQlhYzW`NqRS<{W+EB^{}O{_$$d)JM9?`Zjx8EueUx zKF$7dge}ndRtP2Mhg7z!JNzhe2(mbAfR1fKWWL6e=rkaVojpT*$`j(KfJQf%+sIkRk&`?S=I@U31iSQOxPrcrsR>XX>8fCQ@n`C&x^9CVk zt~pg8doA7GnFdy%GgKz;Z$FE$6`75 zNhbE4+d6acaoavLyFwDVAcIoq6f?37UPU^XB6|%8f7}{g<7Nm;)1`@gwX)RRO(7*y z3c5Jo^YLVP`#@E%I8rbCke;(3d8^~cNe!DXIDfaHh9~^lYlJ17o{J_@EG^1-I@_^p zRfzNWq0R$FD+z%gnxIaWHR=5X+`y(Hcxi;|WDEAy3DY-}X1Nvj-XsQx=j<^z^^7f% zz5Bi9UwjQ7Lhw#>9B>L-g_9F?nJ~m;^U_;|vGiwylla*Lvy-O4co+Zey!Wi?C2)?K)vdkR)nd6{ zw&!nB={-DiKEWNkuF@Rp0r%4~gQM*SU?|WZzPFbc90smbzN?tjf_)drb%1q&Wq?O|873DT*oL8>CD9e_8p}7t>~L+ zu931=6Q_GVCcPWAM2gilHWdEZTRjnpvvv9XtjGvLZdbDt6v1CA!o==(^|s!s%9w>0 z=_*?W%-;&^SCe63(^g(qTCV!d7SdFViA|^1ICa|MiuO<7JAp(4BC9%``1MK7&QP+h z@^X_p{#j5-kQOWH39OpkvPQJ+4J#>vC&{tUYCazg=a4aSUR>Y%IHJE{cXtG-VyDQW z6ZlSHZJO{FR&{iR$V%C=c&Xn->k-oz#(wd3e0_wWU0;>>V9) zL(vT_36-w|^UnI?!=tJHGl)95DKJ*Z&) zVhuR;=s42pmA_uOmBH5O(Xj^WHl8C{8EwLq1s4hs>DJ&M zEZFRF>PFekJS765KNH|X3J{!x=jBgups76F>J2s5D3>3Y$C0`>F%^HbABef|?4#HJm;tOMwF0uG# zy1|^1lNjcgD=qC~3JXhHRaXQ0O@+A`*yB6(8Q5sdOIMd(yD|4PRZ*+3sQhFAkV5oL-N3 z7M{C&Tre*dyH!Tv@sL>P4Q#CwLY9+~&xhGz}&n5~xr0aow*&j{kfLF{3)PjyeP{U65ROOG+J7 zP4xawV+5 zOa==%YRFy0%R}%BgG9k9G#&?cgryJ}RGbxANUCmqXXBqSs#jYa|DPb+bIIsYx%&pK^<5^Ha2FE7RW7Jx3~TGFxqiL7^Ty)b#mS==B8bN9gx6hVI{d0)oB-J7@cA^gfoYZ`f6yGZfgFBw3!_)@TY&3&_J*WNNNmd=sC@?!GJOHMUQ<)rZ?(>q+Au4`kzRfHvMuuKsys)M( zIlhFxz&%)7Q$-3ioS&6Da<6RMWFq&h%nPIXTj+h-y+r zTw$!E>lGr|62mvAw=Q4d+Yk+sU1Td05@`tg4qsCAiy!hx^r&}1>KxpZ~a(B1Z= zZ;eO1XrN|oN~_Bma1&Q+z3UGW#OQ@9%`_kVJZ*0(^%!CvxGXW(fYvy$DXvX}J1$RD zMvY?re)b|1)G9_3SLRm=ycis=2pU0tS$-{!ph0*=;pJPs=Qq=+A$Bz?V%w9islwf zLH^`}y1^E-=z!b~dLy@hxm1j_4_XU;LbEOb*_M|+!8s(Oq+C(@20AyMgFci?WUZt& zM(Agi(#+zO18-n=HzVX^gz~*xic0`m*b%Yc#!z;}&}rt=^yQ}752lTteLFTIn)mO0 zXK62p2XWZhOr81@KX)5lJD12tb>)t$w|4CQruADV4K+XxAlj>XcUm7|?F{^|5d?qG zPO&>!sr3ztUxZTGBl)vYV^djH?c*93B?2ufO5lzn8`_pt&bmEcEFi%%iNgjRhAf+B z&*T^jnv~WtU|gaSeiYGAZbo4Ry*kRmw}N=J3|iKaLx&=4ddeDLl!cw#(kfc=uAu`6 znB(>U{>D2zP#Vggt?GI6D-Pec$nw%dsN)eBLn9-18Q59e-8K(dJy9D^^jp*T-E{av&IDW;j&)%O_C0H?qf}z28RO!kaqwMK?*y)XWSV2U z(|!80-lWAG`ZZqZy4bD&MQ?q<)KLO~UKY$oQ7%N(CVGtr%aJEuDRn$@zCbvlrfdJ` z0Jth33~C>WUGeGH8+9Gu?^D(vTlfjQj-{d|<@n!(ilTd(x8c4KaFcn|IRguSMb^pf zf=m+?_bEAR0!U;qbCu0_u^HoIv;C$%ggnwB=+Qkqzh9Of{Ey9D!LuZ50Ar9cu*oTNVr_^eIw^=1Z&-M6yzw&!P^PH(VlIEBI;*Q&q}HV1fKk-g)y@Y0_teZl$v+*ee?65Bn-@qY6i zLF5LA0rXp1d()d3FiQYV)8bJgk8|163RyoGx11P%@(w-}H>r(rH#>AS&)~UJ(n1^% zI1*EIn@z3sMv=cWYwDW+dPTQ21aib*RKvonnie4I8!6kP!5Fe)s2>O#^FvNuIsukG zgXgJ&_SQ%*fTI(>Dz-4l=3j?W3BeD#GhiAM=$j{mXSaaaCTp|Cy$ZP@dqbxurdN`B zt~di8F!{z(yRX!HmetJ1{{ZJ^-p1d`5o;p+*$TZj%4`eTuA}XWxkFE8K*b(kPJ_Hy=&EvS%vvYJmDa&t!RiK{ zf3FN)vNq*^*J}e%BU)_2yie~LpF}}r7f;&|L5EByyDC^p5rPlZ8M^?aNPt<2Vd96_ zj~H5GDP=8ceZD12;6T%JZ@tTh+qLiFY_B@{5O3j+n_k=6sq=jiO-zy>4E7IM=$W&f z-sA}k>h*}k9%p5hN_xca+fqT&g6OZbuAj&KNNF?%{DtjYA}B|W=Dlb8<2LHAJ{*=yX3-mzRe-b*C==glvo@w1{HWRa zGitb2>YMK?@M9iH928;+fh(8uEe~@D|en5EXL6Tfy@!%nP;Xp=w2V$`Asz<+hrAoxIK7@vNxh(*xHutryfw*6LJ;In#q zN^9n74_4G*QP5IwJsWq^KJ0FKxyG-E$mN7zg&=JjYAg)h-Tm!#9;t_h zG{}X$!&LBqp#?HJ`tNTLNWhohFT?kKoX*1F8s2Z!?v7M2INad9KB5tKe}%(b0a^ZG z{D%WsC!V=h{Gm;71RF9ZUo>chDHLwf!W&&lf9~c~D5hqcId88RdXICMrDUa(DLRYl z`c^(#5H|Z4I?KktalY@b1rG2a%_}ay_Jq>C?H_>`70(A~T`RQl(YK}cAQ=Q_DX%_c z%$_t~B>wp6FX3nYHZH%_(Za{;`tbzsUiNT(Z>W%_jcXwg>P7`gR`lbZ z`S%Vh-qd$fK=mC5D9+Kh+7S!lrb!P{r%8$(#t%VDYCW+9yN3*pK(qOLuET)+N%y_O z`(xd|$JaG9d-tKU%XVq8?DU<;-)DaT*?IC3KZDJBx`-WvT zsZfHmm9^Db9G$N*LcwOh7sevct3ww{7v2xMN{=s3fLtX>zp(;e16bVkf_tep+i_7o zo^D(uE?8A!K8SBUMuQ$H8F_Z}?HQH{H~!^}B7>>mL7mSs`)HLj4| zu?myOfL2+wdCRmMAxpf?X+ljzw$UKN#I081pt(Q8b|q>0^HcW3(z-6J?_2nnXDztC zQAblo7gu342xhGY8Ieu52+np1tt>ZPtABwzgGw;wF*C?o5DPQB`{`MRd~xwz?<;NN z^DIJD6cET(lN0gDj6F$#^j*irwUl8GO`Z&$=a31XH2wF|k9?Q)kcr;yG~XFN5`Eku zR_54H0?G$q|(DXFd&i>^G z&R)|9J?oeAHc}aj0hR9@=|GVegvkrHB(`n*y|0gpcRWUPzPSXh;GjfZNs+WkJm0q3 zt$Wo!bhlA8L8Yb-N@dhXCg$YYq(KeiWI<`=q0a-NiM47$tYcbLJ^i9>t^fSNxLiZ9 zUK!{Hmz*%h3@xqu#cpNGMc}&BWMZ9 z^|9GVaOQz(i^=lugdev1i$g2pm+!j_iQpTF$8}p+FD~L^yjw%|ghaA4W z#(q$=cag)$YV_y$cF`2;0D$v+-ZorJ^%*h*lEJ@S2lPDWrw(nff_r2cm#Fj(Ly^3i zp3emWk7d>bfp^TGh<~YCD9YpUV|H^^j7$TH?^SyM*pS!MlUvy)W}G(B<3nQJ3xfb1 zldSW`?8i(cws`t4sq~X)EqIP^Md~qQ)(_@2WA^Rp(5OzOQ#(7DsR!?yWP;7-HF<0} zVR4RKRb0JH8k$}I0NIN^5IUhDBA-Q3y-DP}++IuQ6|otIq51o$Bc~_2PZo4x1tXFc zi}K!60ApkxjrpNJ-f}F>e@1*!2qB^PvSK&C!C)P#fza;Af8OrkFDcpKC#lJg$8V!!7GtwXN8H8- zFS%aR$Sco9bfCxP_17J8J0BJnx+{Nw!(iiiPxP>;ZeUl1lJp-q`@u$fk=B^cX!dos zc9p3wt+xAmg*nF>)4wD;G~B6VP76-Hf0%NGezn%Ors4!L;X`(?Y#!C+h=zlBdX+DZ zFJH(1dcc`Cjzu)+qZFdClwv8+$@?O%RSaLIH`8Y%77?SH`1lDRiinAJ_nP;dQM9Mr z=Zdb`FAQ}4=DaqUT;7cs%16&XTS}nUyy7U<{b)z%=?%Aw)4Z@MomfJsp^B+oLx&sL zeLw!{kX#(S$ic-rEBzKkX@GG@9vwCkKMbIBDulgtrP`kSsTNC5leT!8D{Z}hR7%9Z zfqtAYRXm3jyRG4_|1@7&OV_FR{?5qn@Z!6=Kh1px=PQ!;6?Iu|*j46I3~CED^LY}w z?C<_vlWW0*E=*A4FJzTwNbxJ^eVySN%`4Uy50LuPFK>&QNdAE87;7DgamNJ%5NIRk zh&twxcfoN)Z(i-l@eEl0HBn?&KaqM8|MgDuk{v2s=i{rk;GM%bK_(}rL#S|AaWKI5 zLo?sWtZ($8uy8t?LC{W>M}>I5S};g;seP?`XL}=^7x!R(lLfc-!(yHru_z43Sze#j zcFKEk&O(~UNQ1BBCELibw&#oo7!c79rg2?|-t}B7YLVTa+u}IA`H}p=2a&mOXD|2N zOi`f|n%@&2AIFKuD_Ew5v$d9 z1)ap1>t@z@6s>4X_fH;gQmp%WOGN&i(KbHEcQYKmLffE&oqgR+Zlx;Rc~^2FPoybw zwhB~Z;&~C2dJngb7(MTPGv$X$2DP}}+@2oS*7%U84nED?>@kIM*XR2k)^qxP3XoIC zP5)JQ2mKF|0gtLh48}*YpOjt}Teu zJP2EH(845>vss7wEUc%TSk7m+;@xH&dX9IduBcnbByd7-$FcG zK15xmaCnSVr4>3*{JdqIa!x_J=VijJ2)3d_pLhxq^XG9F2&G?=)_Qeg%7wUr&#v=t zv8>Os1!0u4OmxusgE;9%i(0Viyuqsd(O1FArEAft8GyeR~Kb?|W%tbqB^mYjVJS}S?$bq^S#Zz!7 z9?44vx{$`!|4?FuOI$M!hX+Yn3Dm~_X4JCi6rcgg&DHTa3#oJ|EwE|d7plL0AaA+$ zmKDqn*8a0nQTku+P=ahRB3UB;c`#s69sduD_rfCm5HVF)HelZJze&IkAdQZIq`Qiy zSAqz)(JZ%do%1xH^OW~IHXVZcC4EWvXg$By8SV&JwvN93k_;F(K>KvYZGNr4%|c=` z0*t}a3=Vt$&wH^;$|3UJ^RP26uDac4NdPo1nu)9c!rxncfg4hb1=YYy4|DSK~0yO($-;g%nTgI=wUIN~9|I)F){?B`w$lGbo)&0;0 zA$+H+XTnIDMs@rz-YAOtjdYH=0bo5o#wnX4S=7pp^#j`UfvGB>O@A_e`!DUkh>>=J$eAy>OM1H?&Tq+n40&vf95Z(mz!fbhr^fm*-0w@5YR(Att21PXyO-Y z!{{R_Zgf{N%G98;eJ>xL6qpG88Rx~Pasuh~x5}zHFcO~;kko%K(VWn$2Q;$%jc!5v z=k}%-f``9;H&qEp=(dbK&U0Er#rMO*B>rq5hMxlH@0;^IU_^+R&yvvwod0!_S~ME1 zONYHo{`B-fJRDrHIZI`}f8__HB7gl&+kTZz1`e*F_=n@-fBqPJ7_-@A&wg(OkXnDk5;!z8a_RL%v3I_2y8wZS&Y#5C$N$pVz1Y4( zXu1O*=2*of%Z1<6xlH`RU9WAM6Hz|K{1Tllt#Va^g#`4_X(9taDFLjx*Z46IACO^r z)RFT@w+Hx}D985V;A9}jAi61%{qY|ihU2M?&tKbTmJ+WAoMdEjQOk7W^z#!`S-rb| zR_80-XMy?0ACxa5y}$qo+D1sYrFFW|CTIXnfa6GQPfT~Om!mu^i$m0T+!lC@bBAZ= zv;Pq@khiVkVF8v>G?f+10xkdkfFPWt9k9^&~ z$}YdoGW@FyO#AbkrPhB$!hvw}ZrK01B*`A)Nz)oa_RdWh?fEK-j~^DopM?D{&)t(? literal 0 HcmV?d00001 diff --git a/apps/emqx_gateway/src/coap/doc/shared_state.png b/apps/emqx_gateway/src/coap/doc/shared_state.png new file mode 100644 index 0000000000000000000000000000000000000000..2a7df229fc3c6e283d73f80b060667ca5f7041bf GIT binary patch literal 31330 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ⅅl2LhO9VkScW_%Fl%zyIZ#OLC))tmp@n81`MWpd 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