Merge branch 'master' into EMQX-788
This commit is contained in:
commit
ed34783dd7
|
@ -1,7 +1,7 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
{deps,
|
||||
[
|
||||
{minirest, {git, "https://github.com/emqx/minirest.git", {tag, "0.3.5"}}}
|
||||
{minirest, {git, "https://github.com/emqx/minirest.git", {tag, "0.3.6"}}}
|
||||
]}.
|
||||
|
||||
{shell, [
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
!sed -i '/emqx_telemetry/d' data/loaded_plugins
|
||||
|
||||
!./bin/emqx start
|
||||
?EMQ X (.*) is started successfully!
|
||||
?EMQ X .* is started successfully!
|
||||
?SH-PROMPT
|
||||
|
||||
!./bin/emqx_ctl cluster join emqx@127.0.0.1
|
||||
|
@ -99,6 +99,10 @@
|
|||
"""
|
||||
?SH-PROMPT
|
||||
|
||||
!./bin/emqx_ctl plugins list | grep emqx_management
|
||||
?Plugin\(emqx_management.*active=true\)
|
||||
?SH-PROMPT
|
||||
|
||||
[shell emqx2]
|
||||
!echo "" > log/emqx.log.1
|
||||
?SH-PROMPT
|
||||
|
@ -120,6 +124,10 @@
|
|||
"""
|
||||
?SH-PROMPT
|
||||
|
||||
!./bin/emqx_ctl plugins list | grep emqx_management
|
||||
?Plugin\(emqx_management.*active=true\)
|
||||
?SH-PROMPT
|
||||
|
||||
[shell bench]
|
||||
???publish complete
|
||||
??SH-PROMPT:
|
||||
|
|
|
@ -83,6 +83,7 @@ jobs:
|
|||
- name: build
|
||||
env:
|
||||
PYTHON: python
|
||||
DIAGNOSTIC: 1
|
||||
run: |
|
||||
$env:PATH = "${{ steps.install_erlang.outputs.erlpath }}\bin;$env:PATH"
|
||||
|
||||
|
@ -168,9 +169,11 @@ jobs:
|
|||
- name: build
|
||||
run: |
|
||||
. $HOME/.kerl/${{ matrix.erl_otp }}/activate
|
||||
make -C source ensure-rebar3
|
||||
sudo cp source/rebar3 /usr/local/bin/rebar3
|
||||
make -C source ${{ matrix.profile }}-zip
|
||||
cd source
|
||||
make ensure-rebar3
|
||||
sudo cp rebar3 /usr/local/bin/rebar3
|
||||
rm -rf _build/${{ matrix.profile }}/lib
|
||||
make ${{ matrix.profile }}-zip
|
||||
- name: test
|
||||
run: |
|
||||
cd source
|
||||
|
|
|
@ -38,6 +38,11 @@ jobs:
|
|||
run: make ${EMQX_NAME}-zip
|
||||
- name: build deb/rpm packages
|
||||
run: make ${EMQX_NAME}-pkg
|
||||
- uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: rebar3.crashdump
|
||||
path: ./rebar3.crashdump
|
||||
- name: pakcages test
|
||||
run: |
|
||||
export CODE_PATH=$GITHUB_WORKSPACE
|
||||
|
@ -94,6 +99,11 @@ jobs:
|
|||
make ensure-rebar3
|
||||
sudo cp rebar3 /usr/local/bin/rebar3
|
||||
make ${EMQX_NAME}-zip
|
||||
- uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: rebar3.crashdump
|
||||
path: ./rebar3.crashdump
|
||||
- name: test
|
||||
run: |
|
||||
pkg_name=$(basename _packages/${EMQX_NAME}/emqx-*.zip)
|
||||
|
|
|
@ -355,8 +355,8 @@ rpc.port_discovery = stateless
|
|||
## Number of outgoing RPC connections.
|
||||
##
|
||||
## Value: Interger [0-256]
|
||||
## Defaults to NumberOfCPUSchedulers / 2 when set to 0
|
||||
#rpc.tcp_client_num = 0
|
||||
## Default = 1
|
||||
#rpc.tcp_client_num = 1
|
||||
|
||||
## RCP Client connect timeout.
|
||||
##
|
||||
|
|
|
@ -30,11 +30,13 @@
|
|||
%% MQTT Protocol Version and Names
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(MQTT_SN_PROTO_V1, 1).
|
||||
-define(MQTT_PROTO_V3, 3).
|
||||
-define(MQTT_PROTO_V4, 4).
|
||||
-define(MQTT_PROTO_V5, 5).
|
||||
|
||||
-define(PROTOCOL_NAMES, [
|
||||
{?MQTT_SN_PROTO_V1, <<"MQTT-SN">>}, %% XXX:Compatible with emqx-sn plug-in
|
||||
{?MQTT_PROTO_V3, <<"MQIsdp">>},
|
||||
{?MQTT_PROTO_V4, <<"MQTT">>},
|
||||
{?MQTT_PROTO_V5, <<"MQTT">>}]).
|
||||
|
|
|
@ -1,12 +1,31 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{VSN,
|
||||
[{"4.3.2",
|
||||
[{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
|
||||
[
|
||||
{"4.3.4",
|
||||
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_app,brutal_purge,soft_purge,[]}
|
||||
]},
|
||||
{"4.3.3",
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_app,brutal_purge,soft_purge,[]}
|
||||
]},
|
||||
{"4.3.2",
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_app,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]}]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_cm,brutal_purge,soft_purge,[]}
|
||||
]},
|
||||
{"4.3.1",
|
||||
[{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
|
||||
|
@ -18,7 +37,9 @@
|
|||
{load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
|
||||
{"4.3.0",
|
||||
[{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]},
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_congestion,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
|
||||
|
@ -34,13 +55,32 @@
|
|||
{apply,{emqx_metrics,upgrade_retained_delayed_counter_type,[]}},
|
||||
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
|
||||
{<<".*">>,[]}],
|
||||
[{"4.3.2",
|
||||
[{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
|
||||
[
|
||||
{"4.3.4",
|
||||
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_app,brutal_purge,soft_purge,[]}
|
||||
]},
|
||||
{"4.3.3",
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_app,brutal_purge,soft_purge,[]}
|
||||
]},
|
||||
{"4.3.2",
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_channel,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_app,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]}]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_cm,brutal_purge,soft_purge,[]}
|
||||
]},
|
||||
{"4.3.1",
|
||||
[{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
|
||||
|
@ -52,7 +92,9 @@
|
|||
{load_module,emqx_logger_textfmt,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]}]},
|
||||
{"4.3.0",
|
||||
[{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]},
|
||||
[{load_module,emqx_packet,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_logger_jsonfmt,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_connection,brutal_purge,soft_purge,[]},
|
||||
{load_module,emqx_congestion,brutal_purge,soft_purge,[]},
|
||||
|
|
|
@ -294,6 +294,9 @@ do_discard_session(ClientId, Pid) ->
|
|||
_ : {noproc, _} -> % emqx_connection: gen_server:call
|
||||
?tp(debug, "session_already_gone", #{pid => Pid}),
|
||||
ok;
|
||||
_ : {'EXIT', {noproc, _}} -> % rpc_call/3
|
||||
?tp(debug, "session_already_gone", #{pid => Pid}),
|
||||
ok;
|
||||
_ : {{shutdown, _}, _} ->
|
||||
?tp(debug, "session_already_shutdown", #{pid => Pid}),
|
||||
ok;
|
||||
|
|
|
@ -336,9 +336,13 @@ handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = P
|
|||
#emqx_shared_subscription{subpid = SubPid} = NewRecord,
|
||||
{noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})};
|
||||
|
||||
handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) ->
|
||||
#emqx_shared_subscription{subpid = SubPid} = OldRecord,
|
||||
{noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})};
|
||||
%% The subscriber may have subscribed multiple topics, so we need to keep monitoring the PID until
|
||||
%% it `unsubscribed` the last topic.
|
||||
%% The trick is we don't demonitor the subscriber here, and (after a long time) it will eventually
|
||||
%% be disconnected.
|
||||
% handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) ->
|
||||
% #emqx_shared_subscription{subpid = SubPid} = OldRecord,
|
||||
% {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})};
|
||||
|
||||
handle_info({mnesia_table_event, _Event}, State) ->
|
||||
{noreply, State};
|
||||
|
@ -348,8 +352,7 @@ handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMo
|
|||
cleanup_down(SubPid),
|
||||
{noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
|
|
|
@ -403,7 +403,10 @@ websocket_close(Reason, State) ->
|
|||
|
||||
terminate(Reason, _Req, #state{channel = Channel}) ->
|
||||
?LOG(debug, "Terminated due to ~p", [Reason]),
|
||||
emqx_channel:terminate(Reason, Channel).
|
||||
emqx_channel:terminate(Reason, Channel);
|
||||
|
||||
terminate(_Reason, _Req, _UnExpectedState) ->
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle call
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{VSN,
|
||||
[ {"4.3.0",
|
||||
%% load all plugins
|
||||
%% NOTE: this depends on the fact that emqx_dashboard is always
|
||||
%% the last application gets upgraded
|
||||
[ {apply, {emqx_plugins, load, []}}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[ {"4.3.0",
|
||||
[ {apply, {emqx_plugins, load, []}}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_exhook,
|
||||
[{description, "EMQ X Extension for Hook"},
|
||||
{vsn, "4.3.1"},
|
||||
{vsn, "4.3.2"},
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{mod, {emqx_exhook_app, []}},
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.1", [
|
||||
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_exhook_pb, brutal_purge, soft_purge, []}
|
||||
{load_module, emqx_exhook_pb, brutal_purge, soft_purge, []},
|
||||
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{"4.3.1", [
|
||||
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_exhook_pb, brutal_purge, soft_purge, []}
|
||||
{load_module, emqx_exhook_pb, brutal_purge, soft_purge, []},
|
||||
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
]
|
||||
|
|
|
@ -122,7 +122,7 @@ channel_opts(Opts) ->
|
|||
Scheme = proplists:get_value(scheme, Opts),
|
||||
Host = proplists:get_value(host, Opts),
|
||||
Port = proplists:get_value(port, Opts),
|
||||
SvrAddr = lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])),
|
||||
SvrAddr = format_http_uri(Scheme, Host, Port),
|
||||
ClientOpts = case Scheme of
|
||||
https ->
|
||||
SslOpts = lists:keydelete(ssl, 1, proplists:get_value(ssl_options, Opts, [])),
|
||||
|
@ -133,6 +133,13 @@ channel_opts(Opts) ->
|
|||
end,
|
||||
{SvrAddr, ClientOpts}.
|
||||
|
||||
format_http_uri(Scheme, Host0, Port) ->
|
||||
Host = case is_tuple(Host0) of
|
||||
true -> inet:ntoa(Host0);
|
||||
_ -> Host0
|
||||
end,
|
||||
lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])).
|
||||
|
||||
-spec unload(server()) -> ok.
|
||||
unload(#server{name = Name, hookspec = HookSpecs}) ->
|
||||
_ = do_deinit(Name),
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
deps/
|
||||
ebin/
|
||||
_rel/
|
||||
.erlang.mk/
|
||||
*.d
|
||||
data/
|
||||
*.iml
|
||||
.idea/
|
||||
logs/
|
||||
*.beam
|
||||
.DS_Store
|
||||
erlang.mk
|
||||
_build/
|
||||
rebar.lock
|
||||
rebar3.crashdump
|
||||
bbmustache/
|
||||
*.conf.rendered
|
||||
.rebar3
|
||||
*.swp
|
|
@ -1,338 +0,0 @@
|
|||
|
||||
# emqx-lua-hook
|
||||
|
||||
This plugin makes it possible to write hooks in lua scripts.
|
||||
|
||||
Lua virtual machine is implemented by [luerl](https://github.com/rvirding/luerl) which supports Lua 5.2. Following features may not work properly:
|
||||
* label and goto
|
||||
* tail-call optimisation in return
|
||||
* only limited standard libraries
|
||||
* proper handling of `__metatable`
|
||||
|
||||
For the supported functions, please refer to luerl's [project page](https://github.com/rvirding/luerl).
|
||||
|
||||
Lua scripts are stored in 'data/scripts' directory, and will be loaded automatically. If a script is changed during runtime, it should be reloaded to take effect.
|
||||
|
||||
Each lua script could export several functions binding with emqx hooks, triggered by message publish, topic subscribe, client connect, etc. Different lua scripts may export same type function, binding with a same event. But their order being triggered is not guaranteed.
|
||||
|
||||
To start this plugin, run following command:
|
||||
|
||||
```shell
|
||||
bin/emqx_ctl plugins load emqx_lua_hook
|
||||
```
|
||||
|
||||
|
||||
## NOTE
|
||||
|
||||
* Since lua VM is run on erlang VM, its performance is poor. Please do NOT write long or complicated lua scripts which may degrade entire system.
|
||||
* It's hard to debug lua script in emqx environment. Recommended to unit test your lua script in your host first. If everything is OK, deploy it to emqx 'data/scripts' directory.
|
||||
* Global variable will lost its value for each call. Do NOT use global variable in lua scripts.
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
Suppose your emqx is installed in /emqx, and the lua script directory should be /emqx/data/scripts.
|
||||
|
||||
Make a new file called "test.lua" and put following code into this file:
|
||||
|
||||
```lua
|
||||
function on_message_publish(clientid, username, topic, payload, qos, retain)
|
||||
return topic, "hello", qos, retain
|
||||
end
|
||||
|
||||
function register_hook()
|
||||
return "on_message_publish"
|
||||
end
|
||||
```
|
||||
|
||||
Execute following command to start emq-lua-hook and load scripts in 'data/scripts' directory.
|
||||
|
||||
```
|
||||
/emqx/bin/emqx_ctl plugins load emqx_lua_hook
|
||||
```
|
||||
|
||||
Now let's take a look at what will happend.
|
||||
|
||||
- Start a mqtt client, such as mqtt.fx.
|
||||
- Subscribe a topic="a/b".
|
||||
- Send a message, topic="a/b", payload="123"
|
||||
- Subscriber will get a message with topic="a/b" and payload="hello". test.lua modifies the payload.
|
||||
|
||||
If there are "test1.lua", "test2.lua" and "test3.lua" in /emqx/data/scripts, all these files will be loaded once emq-lua-hook get started.
|
||||
|
||||
If test2.lua has been changed, restart emq-lua-hook to reload all scripts, or execute following command to reload test2.lua only:
|
||||
|
||||
```
|
||||
/emqx/bin/emqx_ctl luahook reload test2.lua
|
||||
```
|
||||
|
||||
|
||||
# Hook API
|
||||
|
||||
You can find all example codes in the `examples.lua` file.
|
||||
|
||||
## on_client_connected
|
||||
|
||||
```lua
|
||||
function on_client_connected(clientId, userName, returncode)
|
||||
return 0
|
||||
end
|
||||
```
|
||||
This API is called after a mqtt client has establish a connection with broker.
|
||||
|
||||
### Input
|
||||
* clientid : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* returncode : a string, has following values
|
||||
- success : Connection accepted
|
||||
- Others is failed reason
|
||||
|
||||
### Output
|
||||
Needless
|
||||
|
||||
## on_client_disconnected
|
||||
|
||||
```lua
|
||||
function on_client_disconnected(clientId, username, error)
|
||||
return
|
||||
end
|
||||
```
|
||||
This API is called after a mqtt client has disconnected.
|
||||
|
||||
### Input
|
||||
* clientId : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* error : a string, denote the disconnection reason.
|
||||
|
||||
### Output
|
||||
Needless
|
||||
|
||||
## on_client_subscribe
|
||||
|
||||
```lua
|
||||
function on_client_subscribe(clientId, username, topic)
|
||||
-- do your job here
|
||||
if some_condition then
|
||||
return new_topic
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
```
|
||||
This API is called before mqtt engine process client's subscribe command. It is possible to change topic or cancel it.
|
||||
|
||||
### Input
|
||||
* clientid : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* topic : a string, mqtt message's topic
|
||||
|
||||
### Output
|
||||
* new_topic : a string, change mqtt message's topic
|
||||
* false : cancel subscription
|
||||
|
||||
|
||||
## on_client_unsubscribe
|
||||
|
||||
```lua
|
||||
function on_client_unsubscribe(clientId, username, topic)
|
||||
-- do your job here
|
||||
if some_condition then
|
||||
return new_topic
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
```
|
||||
This API is called before mqtt engine process client's unsubscribe command. It is possible to change topic or cancel it.
|
||||
|
||||
### Input
|
||||
* clientid : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* topic : a string, mqtt message's topic
|
||||
|
||||
### Output
|
||||
* new_topic : a string, change mqtt message's topic
|
||||
* false : cancel unsubscription
|
||||
|
||||
|
||||
## on_session_subscribed
|
||||
|
||||
```lua
|
||||
function on_session_subscribed(ClientId, Username, Topic)
|
||||
return
|
||||
end
|
||||
```
|
||||
This API is called after a subscription has been done.
|
||||
|
||||
### Input
|
||||
* clientid : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* topic : a string, mqtt's topic filter.
|
||||
|
||||
### Output
|
||||
Needless
|
||||
|
||||
|
||||
## on_session_unsubscribed
|
||||
|
||||
```lua
|
||||
function on_session_unsubscribed(clientid, username, topic)
|
||||
return
|
||||
end
|
||||
```
|
||||
This API is called after a unsubscription has been done.
|
||||
|
||||
### Input
|
||||
* clientid : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* topic : a string, mqtt's topic filter.
|
||||
|
||||
### Output
|
||||
Needless
|
||||
|
||||
## on_message_delivered
|
||||
|
||||
```lua
|
||||
function on_message_delivered(clientid, username, topic, payload, qos, retain)
|
||||
-- do your job here
|
||||
return topic, payload, qos, retain
|
||||
end
|
||||
```
|
||||
This API is called after a message has been pushed to mqtt clients.
|
||||
|
||||
### Input
|
||||
* clientId : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* topic : a string, mqtt message's topic
|
||||
* payload : a string, mqtt message's payload
|
||||
* qos : a number, mqtt message's QOS (0, 1, 2)
|
||||
* retain : a boolean, mqtt message's retain flag
|
||||
|
||||
### Output
|
||||
Needless
|
||||
|
||||
## on_message_acked
|
||||
|
||||
```lua
|
||||
function on_message_acked(clientId, username, topic, payload, qos, retain)
|
||||
return
|
||||
end
|
||||
```
|
||||
This API is called after a message has been acknowledged.
|
||||
|
||||
### Input
|
||||
* clientId : a string, mqtt client id.
|
||||
* username : a string mqtt username
|
||||
* topic : a string, mqtt message's topic
|
||||
* payload : a string, mqtt message's payload
|
||||
* qos : a number, mqtt message's QOS (0, 1, 2)
|
||||
* retain : a boolean, mqtt message's retain flag
|
||||
|
||||
### Output
|
||||
Needless
|
||||
|
||||
## on_message_publish
|
||||
|
||||
```lua
|
||||
function on_message_publish(clientid, username, topic, payload, qos, retain)
|
||||
-- do your job here
|
||||
if some_condition then
|
||||
return new_topic, new_payload, new_qos, new_retain
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
```
|
||||
This API is called before publishing message into mqtt engine. It's possible to change message or cancel publish in this API.
|
||||
|
||||
### Input
|
||||
* clientid : a string, mqtt client id of publisher.
|
||||
* username : a string, mqtt username of publisher
|
||||
* topic : a string, mqtt message's topic
|
||||
* payload : a string, mqtt message's payload
|
||||
* qos : a number, mqtt message's QOS (0, 1, 2)
|
||||
* retain : a boolean, mqtt message's retain flag
|
||||
|
||||
### Output
|
||||
* new_topic : a string, change mqtt message's topic
|
||||
* new_payload : a string, change mqtt message's payload
|
||||
* new_qos : a number, change mqtt message's QOS
|
||||
* new_retain : a boolean, change mqtt message's retain flag
|
||||
* false : cancel publishing this mqtt message
|
||||
|
||||
## register_hook
|
||||
|
||||
```lua
|
||||
function register_hook()
|
||||
return "hook_name"
|
||||
end
|
||||
|
||||
-- Or register multiple callbacks
|
||||
|
||||
function register_hook()
|
||||
return "hook_name1", "hook_name2", ... , "hook_nameX"
|
||||
end
|
||||
```
|
||||
|
||||
This API exports hook(s) implemented in its lua script.
|
||||
|
||||
### Output
|
||||
* hook_name must be a string, which is equal to the hook API(s) implemented. Possible values:
|
||||
- "on_client_connected"
|
||||
- "on_client_disconnected"
|
||||
- "on_client_subscribe"
|
||||
- "on_client_unsubscribe"
|
||||
- "on_session_subscribed"
|
||||
- "on_session_unsubscribed"
|
||||
- "on_message_delivered"
|
||||
- "on_message_acked"
|
||||
- "on_message_publish"
|
||||
|
||||
# management command
|
||||
|
||||
## load
|
||||
|
||||
```shell
|
||||
emqx_ctl luahook load script_name
|
||||
```
|
||||
This command will load lua file "script_name" in 'data/scripts' directory, into emqx hook.
|
||||
|
||||
## unload
|
||||
|
||||
```shell
|
||||
emqx_ctl luahook unload script_name
|
||||
```
|
||||
This command will unload lua file "script_name" out of emqx hook.
|
||||
|
||||
## reload
|
||||
|
||||
```shell
|
||||
emqx_ctl luahook reload script_name
|
||||
```
|
||||
This command will reload lua file "script_name" in 'data/scripts'. It is useful if a lua script has been modified and apply it immediately.
|
||||
|
||||
## enable
|
||||
|
||||
```shell
|
||||
emqx_ctl luahook enable script_name
|
||||
```
|
||||
This command will rename lua file "script_name.x" to "script_name", and load it immediately.
|
||||
|
||||
## disable
|
||||
|
||||
```shell
|
||||
emqx_ctl luahook disable script_name
|
||||
```
|
||||
This command will unload this script, and rename lua file "script_name" to "script_name.x", which will not be loaded during next boot.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
##--------------------------------------------------------------------
|
||||
## EMQ X Lua Hook
|
||||
##--------------------------------------------------------------------
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
--
|
||||
-- Given all funcation names needed register to system
|
||||
--
|
||||
function register_hook()
|
||||
return "on_client_connected",
|
||||
"on_client_disconnected",
|
||||
"on_client_subscribe",
|
||||
"on_client_unsubscribe",
|
||||
"on_session_subscribed",
|
||||
"on_session_unsubscribed",
|
||||
"on_message_delivered",
|
||||
"on_message_acked",
|
||||
"on_message_publish"
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Callback Functions
|
||||
|
||||
function on_client_connected(clientid, username, returncode)
|
||||
print("Lua: on_client_connected - " .. clientid)
|
||||
-- do your job here
|
||||
return
|
||||
end
|
||||
|
||||
function on_client_disconnected(clientid, username, reason)
|
||||
print("Lua: on_client_disconnected - " .. clientid)
|
||||
-- do your job here
|
||||
return
|
||||
end
|
||||
|
||||
function on_client_subscribe(clientid, username, topic)
|
||||
print("Lua: on_client_subscribe - " .. clientid)
|
||||
-- do your job here
|
||||
return topic
|
||||
end
|
||||
|
||||
function on_client_unsubscribe(clientid, username, topic)
|
||||
print("Lua: on_client_unsubscribe - " .. clientid)
|
||||
-- do your job here
|
||||
return topic
|
||||
end
|
||||
|
||||
function on_session_subscribed(clientid, username, topic)
|
||||
print("Lua: on_session_subscribed - " .. clientid)
|
||||
-- do your job here
|
||||
return
|
||||
end
|
||||
|
||||
function on_session_unsubscribed(clientid, username, topic)
|
||||
print("Lua: on_session_unsubscribed - " .. clientid)
|
||||
-- do your job here
|
||||
return
|
||||
end
|
||||
|
||||
function on_message_delivered(clientid, username, topic, payload, qos, retain)
|
||||
print("Lua: on_message_delivered - " .. clientid)
|
||||
-- do your job here
|
||||
return topic, payload, qos, retain
|
||||
end
|
||||
|
||||
function on_message_acked(clientid, username, topic, payload, qos, retain)
|
||||
print("Lua: on_message_acked- " .. clientid)
|
||||
-- do your job here
|
||||
return
|
||||
end
|
||||
|
||||
function on_message_publish(clientid, username, topic, payload, qos, retain)
|
||||
print("Lua: on_message_publish - " .. clientid)
|
||||
-- do your job here
|
||||
return topic, payload, qos, retain
|
||||
end
|
|
@ -1,18 +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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(LOG(Level, Format, Args), emqx_logger:Level("Lua Hook: " ++ Format, Args)).
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
{deps,
|
||||
[{luerl, {git, "https://github.com/emqx/luerl", {tag, "v0.3.1"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
compressed,
|
||||
{parse_transform}
|
||||
]}.
|
||||
{overrides, [{add, [{erl_opts, [compressed]}]}]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions]}.
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_lua_hook,
|
||||
[{description, "EMQ X Lua Hooks"},
|
||||
{vsn, "4.3.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{applications, [kernel,stdlib]},
|
||||
{mod, {emqx_lua_hook_app,[]}},
|
||||
{env,[]},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://github.com/emqx/emqx-lua-hook"}
|
||||
]}
|
||||
]}.
|
|
@ -1,199 +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_lua_hook).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("emqx_lua_hook.hrl").
|
||||
-include_lib("luerl/include/luerl.hrl").
|
||||
|
||||
-export([ start_link/0
|
||||
, stop/0
|
||||
]).
|
||||
|
||||
-export([ load_scripts/0
|
||||
, unload_scripts/0
|
||||
, load_script/1
|
||||
, unload_script/1
|
||||
]).
|
||||
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-export([lua_dir/0]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {loaded_scripts = []}).
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, {}, []).
|
||||
|
||||
stop() ->
|
||||
gen_server:call(?SERVER, stop).
|
||||
|
||||
load_scripts() ->
|
||||
gen_server:call(?SERVER, load_scripts).
|
||||
|
||||
unload_scripts() ->
|
||||
gen_server:call(?SERVER, unload_scrips).
|
||||
|
||||
load_script(ScriptName) ->
|
||||
gen_server:call(?SERVER, {load_script, ScriptName}).
|
||||
|
||||
unload_script(ScriptName) ->
|
||||
gen_server:call(?SERVER, {unload_script, ScriptName}).
|
||||
|
||||
lua_dir() ->
|
||||
filename:join([emqx:get_env(data_dir, "data"), "scripts"]).
|
||||
|
||||
%%-----------------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%-----------------------------------------------------------------------------
|
||||
|
||||
init({}) ->
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call(stop, _From, State) ->
|
||||
{stop, normal, ok, State};
|
||||
|
||||
handle_call(load_scripts, _From, State) ->
|
||||
{reply, ok, State#state{loaded_scripts = do_loadall()}, hibernate};
|
||||
|
||||
handle_call(unload_scrips, _From, State=#state{loaded_scripts = Scripts}) ->
|
||||
do_unloadall(Scripts),
|
||||
{reply, ok, State#state{loaded_scripts = []}, hibernate};
|
||||
|
||||
handle_call({load_script, ScriptName}, _From, State=#state{loaded_scripts = Scripts}) ->
|
||||
{Ret, NewScripts} = case do_load(ScriptName) of
|
||||
error -> {error, Scripts};
|
||||
{ScriptName, LuaState} ->
|
||||
case lists:member({ScriptName, LuaState}, Scripts) of
|
||||
true -> {ok, Scripts};
|
||||
false -> {ok, lists:append([{ScriptName, LuaState}], Scripts)}
|
||||
end
|
||||
end,
|
||||
{reply, Ret, State#state{loaded_scripts = NewScripts}, hibernate};
|
||||
|
||||
handle_call({unload_script, ScriptName}, _From, State=#state{loaded_scripts = Scripts}) ->
|
||||
case proplists:get_all_values(ScriptName, Scripts) of
|
||||
[] ->
|
||||
{reply, ok, State, hibernate};
|
||||
LuaStates ->
|
||||
lists:foreach(fun(LuaState) ->
|
||||
% Unload first! If this gen_server has been crashed, loaded_scripts will be empty
|
||||
do_unload({ScriptName, LuaState})
|
||||
end, LuaStates),
|
||||
NewScripts = proplists:delete(ScriptName, Scripts),
|
||||
{reply, ok, State#state{loaded_scripts = NewScripts}, hibernate}
|
||||
end;
|
||||
|
||||
handle_call(Request, From, State) ->
|
||||
?LOG(error, "Unknown Request=~p from ~p", [Request, From]),
|
||||
{reply, ignored, State, hibernate}.
|
||||
|
||||
handle_cast(Msg, State) ->
|
||||
?LOG(error, "unexpected cast: ~p", [Msg]),
|
||||
{noreply, State, hibernate}.
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?LOG(error, "unexpected info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, #state{loaded_scripts = Scripts}) ->
|
||||
do_unloadall(Scripts),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% Internal Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
do_loadall() ->
|
||||
FileList = filelib:wildcard(filename:join([lua_dir(), "*.lua"])),
|
||||
List = [do_load(X) || X <- FileList],
|
||||
[X || X <- List, is_tuple(X)].
|
||||
|
||||
do_load(FileName) ->
|
||||
case catch luerl:dofile(FileName) of
|
||||
{'EXIT', St00} ->
|
||||
?LOG(error, "Failed to load lua script ~p due to error ~p", [FileName, St00]),
|
||||
error;
|
||||
{_Ret, St0=#luerl{}} ->
|
||||
case catch luerl:call_function([register_hook], [], St0) of
|
||||
{'EXIT', St1} ->
|
||||
?LOG(error, "Failed to execute register_hook function in lua script ~p, which has syntax error, St1=~p", [FileName, St1]),
|
||||
error;
|
||||
{Ret1, St1} ->
|
||||
?LOG(debug, "Register lua script ~p", [FileName]),
|
||||
_ = do_register_hooks(Ret1, FileName, St1),
|
||||
{FileName, St1};
|
||||
Other ->
|
||||
?LOG(error, "Failed to load lua script ~p, register_hook() raise exception ~p", [FileName, Other]),
|
||||
error
|
||||
end;
|
||||
Exception ->
|
||||
?LOG(error, "Failed to load lua script ~p with error ~p", [FileName, Exception]),
|
||||
error
|
||||
end.
|
||||
|
||||
do_register(<<"on_message_publish">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_message_publish(ScriptName, St);
|
||||
do_register(<<"on_message_delivered">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_message_delivered(ScriptName, St);
|
||||
do_register(<<"on_message_acked">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_message_acked(ScriptName, St);
|
||||
do_register(<<"on_client_connected">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_client_connected(ScriptName, St);
|
||||
do_register(<<"on_client_subscribe">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_client_subscribe(ScriptName, St);
|
||||
do_register(<<"on_client_unsubscribe">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_client_unsubscribe(ScriptName, St);
|
||||
do_register(<<"on_client_disconnected">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_client_disconnected(ScriptName, St);
|
||||
do_register(<<"on_session_subscribed">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_session_subscribed(ScriptName, St);
|
||||
do_register(<<"on_client_authenticate">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_client_authenticate(ScriptName, St);
|
||||
do_register(<<"on_client_check_acl">>, ScriptName, St) ->
|
||||
emqx_lua_script:register_on_client_check_acl(ScriptName, St);
|
||||
do_register(Hook, ScriptName, _St) ->
|
||||
?LOG(error, "Discard unknown hook ~p ScriptName=~p", [Hook, ScriptName]).
|
||||
|
||||
do_register_hooks([], _ScriptName, _St) ->
|
||||
ok;
|
||||
do_register_hooks([H|T], ScriptName, St) ->
|
||||
_ = do_register(H, ScriptName, St),
|
||||
do_register_hooks(T, ScriptName, St);
|
||||
do_register_hooks(Hook = <<$o, $n, _Rest/binary>>, ScriptName, St) ->
|
||||
do_register(Hook, ScriptName, St);
|
||||
do_register_hooks(Hook, ScriptName, _St) ->
|
||||
?LOG(error, "Discard unknown hook type ~p from ~p", [Hook, ScriptName]).
|
||||
|
||||
do_unloadall(Scripts) ->
|
||||
lists:foreach(fun do_unload/1, Scripts).
|
||||
|
||||
do_unload(Script) ->
|
||||
emqx_lua_script:unregister_hooks(Script),
|
||||
ok.
|
|
@ -1,40 +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_lua_hook_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
, prep_stop/1
|
||||
]).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
{ok, Sup} = emqx_lua_hook_sup:start_link(),
|
||||
emqx_lua_hook:load_scripts(),
|
||||
emqx_lua_hook_cli:load(),
|
||||
{ok, Sup}.
|
||||
|
||||
prep_stop(State) ->
|
||||
emqx_lua_hook:unload_scripts(),
|
||||
emqx_lua_hook_cli:unload(),
|
||||
State.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
|
@ -1,88 +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_lua_hook_cli).
|
||||
|
||||
-export([ load/0
|
||||
, cmd/1
|
||||
, unload/0
|
||||
]).
|
||||
|
||||
-include("emqx_lua_hook.hrl").
|
||||
-include_lib("luerl/include/luerl.hrl").
|
||||
|
||||
-define(PRINT(Format, Args), io:format(Format, Args)).
|
||||
-define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~s~n", [Cmd, Descr])).
|
||||
-define(USAGE(CmdList), [?PRINT_CMD(Cmd, Descr) || {Cmd, Descr} <- CmdList]).
|
||||
|
||||
load() ->
|
||||
emqx_ctl:register_command(luahook, {?MODULE, cmd}, []).
|
||||
|
||||
unload() ->
|
||||
emqx_ctl:unregister_command(luahook).
|
||||
|
||||
cmd(["load", Script]) ->
|
||||
case emqx_lua_hook:load_script(fullname(Script)) of
|
||||
ok -> emqx_ctl:print("Load ~p successfully~n", [Script]);
|
||||
error -> emqx_ctl:print("Load ~p error~n", [Script])
|
||||
end;
|
||||
|
||||
cmd(["reload", Script]) ->
|
||||
FullName = fullname(Script),
|
||||
emqx_lua_hook:unload_script(FullName),
|
||||
case emqx_lua_hook:load_script(FullName) of
|
||||
ok -> emqx_ctl:print("Reload ~p successfully~n", [Script]);
|
||||
error -> emqx_ctl:print("Reload ~p error~n", [Script])
|
||||
end;
|
||||
|
||||
cmd(["unload", Script]) ->
|
||||
emqx_lua_hook:unload_script(fullname(Script)),
|
||||
emqx_ctl:print("Unload ~p successfully~n", [Script]);
|
||||
|
||||
cmd(["enable", Script]) ->
|
||||
FullName = fullname(Script),
|
||||
case file:rename(fullnamedisable(Script), FullName) of
|
||||
ok -> case emqx_lua_hook:load_script(FullName) of
|
||||
ok ->
|
||||
emqx_ctl:print("Enable ~p successfully~n", [Script]);
|
||||
error ->
|
||||
emqx_ctl:print("Fail to enable ~p~n", [Script])
|
||||
end;
|
||||
{error, Reason} ->
|
||||
emqx_ctl:print("Fail to enable ~p due to ~p~n", [Script, Reason])
|
||||
end;
|
||||
|
||||
cmd(["disable", Script]) ->
|
||||
FullName = fullname(Script),
|
||||
emqx_lua_hook:unload_script(FullName),
|
||||
case file:rename(FullName, fullnamedisable(Script)) of
|
||||
ok ->
|
||||
emqx_ctl:print("Disable ~p successfully~n", [Script]);
|
||||
{error, Reason} ->
|
||||
emqx_ctl:print("Fail to disable ~p due to ~p~n", [Script, Reason])
|
||||
end;
|
||||
|
||||
cmd(_) ->
|
||||
emqx_ctl:usage([{"luahook load <Script>", "load lua script into hook"},
|
||||
{"luahook unload <Script>", "unload lua script from hook"},
|
||||
{"luahook reload <Script>", "reload lua script into hook"},
|
||||
{"luahook enable <Script>", "enable lua script and load it into hook"},
|
||||
{"luahook disable <Script>", "unload lua script out of hook and disable it"}]).
|
||||
|
||||
fullname(Script) ->
|
||||
filename:join([emqx_lua_hook:lua_dir(), Script]).
|
||||
fullnamedisable(Script) ->
|
||||
fullname(Script)++".x".
|
|
@ -1,342 +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_lua_script).
|
||||
|
||||
-include("emqx_lua_hook.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-export([ register_on_message_publish/2
|
||||
, register_on_client_connected/2
|
||||
, register_on_client_disconnected/2
|
||||
, register_on_client_subscribe/2
|
||||
, register_on_client_unsubscribe/2
|
||||
, register_on_message_acked/2
|
||||
, register_on_message_delivered/2
|
||||
, register_on_session_subscribed/2
|
||||
, register_on_session_unsubscribed/2
|
||||
, register_on_client_authenticate/2
|
||||
, register_on_client_check_acl/2
|
||||
, unregister_hooks/1
|
||||
]).
|
||||
|
||||
-export([ on_client_connected/4
|
||||
, on_client_disconnected/5
|
||||
, on_client_authenticate/4
|
||||
, on_client_check_acl/6
|
||||
, on_client_subscribe/5
|
||||
, on_client_unsubscribe/5
|
||||
, on_session_subscribed/5
|
||||
, on_session_unsubscribed/5
|
||||
, on_message_publish/3
|
||||
, on_message_delivered/4
|
||||
, on_message_acked/4
|
||||
]).
|
||||
|
||||
-define(EMPTY_USERNAME, <<"">>).
|
||||
|
||||
-define(HOOK_ADD(A, B), emqx:hook(A, B)).
|
||||
-define(HOOK_DEL(A, B), emqx:unhook(A, B)).
|
||||
|
||||
register_on_client_connected(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('client.connected', {?MODULE, on_client_connected, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_client_disconnected(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('client.disconnected', {?MODULE, on_client_disconnected, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_client_authenticate(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('client.authenticate', {?MODULE, on_client_authenticate, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_client_check_acl(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('client.check_acl', {?MODULE, on_client_check_acl, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_client_subscribe(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('client.subscribe', {?MODULE, on_client_subscribe, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_client_unsubscribe(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('client.unsubscribe', {?MODULE, on_client_unsubscribe, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_session_subscribed(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('session.subscribed', {?MODULE, on_session_subscribed, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_session_unsubscribed(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('session.unsubscribed', {?MODULE, on_session_unsubscribed, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_message_publish(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('message.publish', {?MODULE, on_message_publish, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_message_delivered(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('message.delivered', {?MODULE, on_message_delivered, [ScriptName, LuaState]}).
|
||||
|
||||
register_on_message_acked(ScriptName, LuaState) ->
|
||||
?HOOK_ADD('message.acked', {?MODULE, on_message_acked, [ScriptName, LuaState]}).
|
||||
|
||||
unregister_hooks({ScriptName, LuaState}) ->
|
||||
?HOOK_DEL('client.connected', {?MODULE, on_client_connected, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('client.disconnected', {?MODULE, on_client_disconnected, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('client.authenticate', {?MODULE, on_client_authenticate, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('client.check_acl', {?MODULE, on_client_check_acl, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('client.subscribe', {?MODULE, on_client_subscribe, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('client.unsubscribe', {?MODULE, on_client_unsubscribe, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('session.subscribed', {?MODULE, on_session_subscribed, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('session.unsubscribed', {?MODULE, on_session_unsubscribed, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('message.publish', {?MODULE, on_message_publish, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('message.delivered', {?MODULE, on_message_delivered, [ScriptName, LuaState]}),
|
||||
?HOOK_DEL('message.acked', {?MODULE, on_message_acked, [ScriptName, LuaState]}).
|
||||
|
||||
on_client_connected(ClientInfo = #{clientid := ClientId, username := Username},
|
||||
ConnInfo, _ScriptName, LuaState) ->
|
||||
?LOG(debug, "Client(~s) connected, ClientInfo:~n~p~n, ConnInfo:~n~p~n",
|
||||
[ClientId, ClientInfo, ConnInfo]),
|
||||
case catch luerl:call_function([on_client_connected], [ClientId, Username], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_client_connected(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{_Result, _St} ->
|
||||
ok;
|
||||
Other ->
|
||||
?LOG(error, "Lua function on_client_connected() caught exception, ~p", [Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_client_disconnected(ClientInfo = #{clientid := ClientId, username := Username},
|
||||
ReasonCode, ConnInfo, _ScriptName, LuaState) ->
|
||||
?LOG(debug, "Client(~s) disconnected due to ~p, ClientInfo:~n~p~n, ConnInfo:~n~p~n",
|
||||
[ClientId, ReasonCode, ClientInfo, ConnInfo]),
|
||||
case catch luerl:call_function([on_client_disconnected], [ClientId, Username, ReasonCode], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_client_disconnected(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{_Result, _St} ->
|
||||
ok;
|
||||
Other ->
|
||||
?LOG(error, "Lua function on_client_disconnected() caught exception, ~p", [Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_client_authenticate(#{clientid := ClientId,
|
||||
username := Username,
|
||||
peerhost := Peerhost,
|
||||
password := Password}, Result, _ScriptName, LuaState) ->
|
||||
case catch luerl:call_function([on_client_authenticate],
|
||||
[ClientId, Username, inet:ntoa(Peerhost), Password], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_client_authenticate(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{[<<"ignore">>], _St} ->
|
||||
ok;
|
||||
{[<<"ok">>], _St} ->
|
||||
{stop, Result#{auth_result => success}};
|
||||
Other ->
|
||||
?LOG(error, "Lua function on_client_authenticate() caught exception, ~p", [Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_client_check_acl(#{clientid := ClientId,
|
||||
username := Username,
|
||||
peerhost := Peerhost,
|
||||
password := Password}, Topic, PubSub, _Result, _ScriptName, LuaState) ->
|
||||
case catch luerl:call_function([on_client_check_acl], [ClientId, Username, inet:ntoa(Peerhost), Password, Topic, PubSub], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_client_check_acl(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{[<<"ignore">>],_St} ->
|
||||
ok;
|
||||
{[<<"allow">>], _St} ->
|
||||
{stop, allow};
|
||||
{[<<"deny">>], _St} ->
|
||||
{stop, deny};
|
||||
Other ->
|
||||
?LOG(error, "Lua function on_client_check_acl() caught exception, ~p", [Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_client_subscribe(#{clientid := ClientId, username := Username}, _Properties, TopicFilters, _ScriptName, LuaState) ->
|
||||
NewTopicFilters =
|
||||
lists:foldr(fun(TopicFilter, Acc) ->
|
||||
case on_client_subscribe_single(ClientId, Username, TopicFilter, LuaState) of
|
||||
false -> Acc;
|
||||
NewTopicFilter -> [NewTopicFilter | Acc]
|
||||
end
|
||||
end, [], TopicFilters),
|
||||
case NewTopicFilters of
|
||||
[] -> stop;
|
||||
_ -> {ok, NewTopicFilters}
|
||||
end.
|
||||
|
||||
on_client_subscribe_single(_ClientId, _Username, TopicFilter = {<<$$, _Rest/binary>>, _SubOpts}, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
TopicFilter;
|
||||
on_client_subscribe_single(ClientId, Username, TopicFilter = {Topic, SubOpts}, LuaState) ->
|
||||
?LOG(debug, "hook client(~s/~s) will subscribe: ~p~n", [ClientId, Username, Topic]),
|
||||
case catch luerl:call_function([on_client_subscribe], [ClientId, Username, Topic], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_client_subscribe(), which has syntax error, St=~p", [St]),
|
||||
TopicFilter;
|
||||
{[false], _St} ->
|
||||
false; % cancel this topic's subscription
|
||||
{[NewTopic], _St} ->
|
||||
?LOG(debug, "LUA function on_client_subscribe() return ~p", [NewTopic]),
|
||||
{NewTopic, SubOpts}; % modify topic
|
||||
Other ->
|
||||
?LOG(error, "Lua function on_client_subscribe() caught exception, ~p", [Other]),
|
||||
TopicFilter
|
||||
end.
|
||||
|
||||
on_client_unsubscribe(#{clientid := ClientId, username := Username}, _Properties, TopicFilters, _ScriptName, LuaState) ->
|
||||
NewTopicFilters =
|
||||
lists:foldr(fun(TopicFilter, Acc) ->
|
||||
case on_client_unsubscribe_single(ClientId, Username, TopicFilter, LuaState) of
|
||||
false -> Acc;
|
||||
NewTopicFilter -> [NewTopicFilter | Acc]
|
||||
end
|
||||
end, [], TopicFilters),
|
||||
case NewTopicFilters of
|
||||
[] -> stop;
|
||||
_ -> {ok, NewTopicFilters}
|
||||
end.
|
||||
|
||||
on_client_unsubscribe_single(_ClientId, _Username, TopicFilter = {<<$$, _Rest/binary>>, _SubOpts}, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
TopicFilter;
|
||||
on_client_unsubscribe_single(ClientId, Username, TopicFilter = {Topic, SubOpts}, LuaState) ->
|
||||
?LOG(debug, "hook client(~s/~s) unsubscribe ~p~n", [ClientId, Username, Topic]),
|
||||
case catch luerl:call_function([on_client_unsubscribe], [ClientId, Username, Topic], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_client_unsubscribe(), which has syntax error, St=~p", [St]),
|
||||
TopicFilter;
|
||||
{[false], _St} ->
|
||||
false; % cancel this topic's unsubscription
|
||||
{[NewTopic], _} ->
|
||||
?LOG(debug, "Lua function on_client_unsubscribe() return ~p", [NewTopic]),
|
||||
{NewTopic, SubOpts}; % modify topic
|
||||
Other ->
|
||||
?LOG(error, "Topic=~p, lua function on_client_unsubscribe() caught exception, ~p", [Topic, Other]),
|
||||
TopicFilter
|
||||
end.
|
||||
|
||||
on_session_subscribed(#{}, <<$$, _Rest/binary>>, _SubOpts, _ScriptName, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
ok;
|
||||
on_session_subscribed(#{clientid := ClientId, username := Username},
|
||||
Topic, SubOpts, _ScriptName, LuaState) ->
|
||||
?LOG(debug, "Session(~s/s) subscribed ~s with subopts: ~p~n", [ClientId, Username, Topic, SubOpts]),
|
||||
case catch luerl:call_function([on_session_subscribed], [ClientId, Username, Topic], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_session_subscribed(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{_Result, _St} ->
|
||||
ok;
|
||||
Other ->
|
||||
?LOG(error, "Topic=~p, lua function on_session_subscribed() caught exception, ~p", [Topic, Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_session_unsubscribed(#{}, <<$$, _Rest/binary>>, _SubOpts, _ScriptName, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
ok;
|
||||
on_session_unsubscribed(#{clientid := ClientId, username := Username},
|
||||
Topic, _SubOpts, _ScriptName, LuaState) ->
|
||||
?LOG(debug, "Session(~s/~s) unsubscribed ~s~n", [ClientId, Username, Topic]),
|
||||
case catch luerl:call_function([on_session_unsubscribed], [ClientId, Username, Topic], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_session_unsubscribed(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{_Result, _St} ->
|
||||
ok;
|
||||
Other ->
|
||||
?LOG(error, "Topic=~p, lua function on_session_unsubscribed() caught exception, ~p", [Topic, Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_message_publish(Message = #message{topic = <<$$, _Rest/binary>>}, _ScriptName, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
{ok, Message};
|
||||
on_message_publish(Message = #message{from = ClientId,
|
||||
qos = QoS,
|
||||
flags = Flags = #{retain := Retain},
|
||||
topic = Topic,
|
||||
payload = Payload,
|
||||
headers = Headers},
|
||||
_ScriptName, LuaState) ->
|
||||
Username = maps:get(username, Headers, ?EMPTY_USERNAME),
|
||||
?LOG(debug, "Publish ~s~n", [emqx_message:format(Message)]),
|
||||
case catch luerl:call_function([on_message_publish], [ClientId, Username, Topic, Payload, QoS, Retain], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_message_publish(), which has syntax error, St=~p", [St]),
|
||||
{ok, Message};
|
||||
{[false], _St} ->
|
||||
{stop, Message};
|
||||
{[NewTopic, NewPayload, NewQos, NewRetain], _St} ->
|
||||
?LOG(debug, "Lua function on_message_publish() return ~p", [{NewTopic, NewPayload, NewQos, NewRetain}]),
|
||||
{ok, Message#message{topic = NewTopic, payload = NewPayload,
|
||||
qos = round(NewQos), flags = Flags#{retain => to_retain(NewRetain)}}};
|
||||
Other ->
|
||||
?LOG(error, "Topic=~p, lua function on_message_publish caught exception, ~p", [Topic, Other]),
|
||||
{ok, Message}
|
||||
end.
|
||||
|
||||
on_message_delivered(#{}, #message{topic = <<$$, _Rest/binary>>}, _ScriptName, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
ok;
|
||||
on_message_delivered(#{clientid := ClientId, username := Username},
|
||||
Message = #message{topic = Topic, payload = Payload, qos = QoS, flags = Flags = #{retain := Retain}},
|
||||
_ScriptName, LuaState) ->
|
||||
?LOG(debug, "Message delivered to client(~s): ~s~n",
|
||||
[ClientId, emqx_message:format(Message)]),
|
||||
case catch luerl:call_function([on_message_delivered], [ClientId, Username, Topic, Payload, QoS, Retain], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_message_delivered(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{[false], _St} ->
|
||||
ok;
|
||||
{[NewTopic, NewPayload, NewQos, NewRetain], _St} ->
|
||||
{ok, Message#message{topic = NewTopic, payload = NewPayload,
|
||||
qos = round(NewQos), flags = Flags#{retain => to_retain(NewRetain)}}};
|
||||
Other ->
|
||||
?LOG(error, "Topic=~p, lua function on_message_delivered() caught exception, ~p", [Topic, Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
on_message_acked(#{}, #message{topic = <<$$, _Rest/binary>>}, _ScriptName, _LuaState) ->
|
||||
%% ignore topics starting with $
|
||||
ok;
|
||||
on_message_acked(#{clientid := ClientId, username := Username},
|
||||
Message = #message{topic = Topic, payload = Payload, qos = QoS, flags = #{retain := Retain}}, _ScriptName, LuaState) ->
|
||||
?LOG(debug, "Message acked by client(~s): ~s~n",
|
||||
[ClientId, emqx_message:format(Message)]),
|
||||
case catch luerl:call_function([on_message_acked], [ClientId, Username, Topic, Payload, QoS, Retain], LuaState) of
|
||||
{'EXIT', St} ->
|
||||
?LOG(error, "Failed to execute function on_message_acked(), which has syntax error, St=~p", [St]),
|
||||
ok;
|
||||
{_Result, _St} ->
|
||||
ok;
|
||||
Other ->
|
||||
?LOG(error, "Topic=~p, lua function on_message_acked() caught exception, ~p", [Topic, Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
to_retain(0) -> false;
|
||||
to_retain(1) -> true;
|
||||
to_retain("true") -> true;
|
||||
to_retain("false") -> false;
|
||||
to_retain(<<"true">>) -> true;
|
||||
to_retain(<<"false">>) -> false;
|
||||
to_retain(true) -> true;
|
||||
to_retain(false) -> false;
|
||||
to_retain(Num) when is_float(Num) ->
|
||||
case round(Num) of 0 -> false; _ -> true end.
|
|
@ -1,693 +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_lua_hook_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
all() ->
|
||||
[case01, case02, case03, case04,
|
||||
case11, case12, case13,
|
||||
case21, case22,
|
||||
case31, case32,
|
||||
case41, case42, case43,
|
||||
case51, case52, case53,
|
||||
case61, case62,
|
||||
case71, case72, case73,
|
||||
case81, case82, case83,
|
||||
case101,
|
||||
case110, case111, case112, case113, case114, case115,
|
||||
case201, case202, case203, case204, case205,
|
||||
case301, case302
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_lua_hook], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(Config) ->
|
||||
emqx_ct_helpers:stop_apps([emqx_lua_hook]),
|
||||
Config.
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, modules, []);
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
init_per_testcase(_, Config) ->
|
||||
ok = filelib:ensure_dir(filename:join([emqx_lua_hook:lua_dir(), "a"])),
|
||||
emqx_lua_hook:start_link(),
|
||||
Config.
|
||||
|
||||
end_per_testcase(_, _Config) ->
|
||||
emqx_lua_hook:stop(),
|
||||
AllScripts = filelib:wildcard(filename:join([emqx_lua_hook:lua_dir(), "*"])),
|
||||
[file:delete(Filename) || Filename <- AllScripts].
|
||||
|
||||
case01(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(ClientId, Username, topic, payload, qos, retain)"
|
||||
"\n return topic, \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{from = <<"myclient">>, qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{username => <<"tester">>}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{payload = <<"hello">>}, Ret).
|
||||
|
||||
case02(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return false" % return false to stop hook
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{from = <<"myclient">>, qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{username => <<"tester">>}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg, Ret).
|
||||
|
||||
case03(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{from = <<"myclient">>, qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{username => <<"tester">>}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg, Ret).
|
||||
|
||||
case04(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n if clientid == \"broker\" then"
|
||||
"\n return topic, \"hello broker\", qos, retain"
|
||||
"\n else"
|
||||
"\n return false" % return false to stop hook
|
||||
"\n end"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{from = <<"broker">>, qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{username => <<"tester">>}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{payload = <<"hello broker">>}, Ret).
|
||||
|
||||
case11(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_delivered(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return false"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_delivered\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.delivered', [#{clientid => <<"myclient">>, username => <<"myuser">>}], Msg),
|
||||
?assertEqual(Msg, Ret),
|
||||
ok = file:delete(ScriptName).
|
||||
|
||||
case12(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_delivered(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return topic, \"hello broker\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_delivered\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.delivered', [#{clientid => <<"myclient">>, username => <<"myuser">>}], Msg),
|
||||
?assertEqual(Msg#message{payload = <<"hello broker">>}, Ret).
|
||||
|
||||
case13(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_delivered(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_delivered\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.delivered', [#{clientid => <<"myclient">>, username => <<"myuser">>}], Msg),
|
||||
?assertEqual(Msg, Ret).
|
||||
|
||||
case21(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_acked(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return true"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_acked\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run('message.acked', [#{clientid => <<"myclient">>, username => <<"myuser">>}, Msg]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case22(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_acked(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_acked\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run('message.acked', [#{clientid => <<"myclient">>, username => <<"myuser">>}, Msg]),
|
||||
?assertEqual(ok, Ret),
|
||||
ok = file:delete(ScriptName).
|
||||
|
||||
case31(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_connected(clientid, username)"
|
||||
"\n return 0"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_connected\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
?assertEqual(ok,
|
||||
emqx_hooks:run('client.connected',
|
||||
[#{clientid => <<"myclient">>, username => <<"tester">>}, #{}])).
|
||||
|
||||
case32(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_connected(clientid, username)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_connected\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
?assertEqual(ok,
|
||||
emqx_hooks:run('client.connected',
|
||||
[#{clientid => <<"myclient">>, username => <<"tester">>}, #{}])).
|
||||
|
||||
case41(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_subscribe(clientid, username, topic)"
|
||||
"\n if topic == \"a/b/c\" then"
|
||||
"\n topic = \"a1/b1/c1\";"
|
||||
"\n end"
|
||||
"\n return topic"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_subscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret = emqx_hooks:run_fold('client.subscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual([{<<"a1/b1/c1">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}], Ret).
|
||||
|
||||
case42(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_subscribe(clientid, username, topic)"
|
||||
"\n return false" % return false to stop hook
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_subscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret = emqx_hooks:run_fold('client.subscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual(TopicTable, Ret).
|
||||
|
||||
case43(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_subscribe(clientid, username, topic)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_subscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret = emqx_hooks:run_fold('client.subscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual(TopicTable, Ret).
|
||||
|
||||
case51(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_unsubscribe(clientid, username, topic)"
|
||||
"\n if topic == \"a/b/c\" then"
|
||||
"\n topic = \"a1/b1/c1\";"
|
||||
"\n end"
|
||||
"\n return topic"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_unsubscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret = emqx_hooks:run_fold('client.unsubscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual([{<<"a1/b1/c1">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}], Ret).
|
||||
|
||||
case52(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_unsubscribe(clientid, username, topic)"
|
||||
"\n return false" % return false to stop hook
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_unsubscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret = emqx_hooks:run_fold('client.unsubscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual(TopicTable, Ret).
|
||||
|
||||
case53(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_unsubscribe(clientid, username, topic)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_unsubscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret = emqx_hooks:run_fold('client.unsubscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual(TopicTable, Ret).
|
||||
|
||||
case61(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_disconnected(clientid, username, reasoncode)"
|
||||
"\n return 0"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_disconnected\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
?assertEqual(ok,
|
||||
emqx_hooks:run('client.disconnected',
|
||||
[#{clientid => <<"myclient">>, username => <<"tester">>}, 0])).
|
||||
|
||||
case62(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_disconnected(clientid, username, reasoncode)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_disconnected\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
?assertEqual(ok,
|
||||
emqx_hooks:run('client.disconnected',
|
||||
[#{clientid => <<"myclient">>, username => <<"tester">>}, 0])).
|
||||
|
||||
case71(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_subscribed(clientid, username, topic)"
|
||||
"\n return 0"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_session_subscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.subscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case72(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_subscribed(clientid, username, topic)"
|
||||
"\n return false" % return false to stop hook
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_session_subscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.subscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case73(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_subscribed(clientid, username, topic)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_session_subscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.subscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case81(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_unsubscribed(clientid, username, topic)"
|
||||
"\n return 0"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_session_unsubscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.unsubscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case82(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_unsubscribed(clientid, username, topic)"
|
||||
"\n return false" % return false to stop hook
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_session_unsubscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.unsubscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case83(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_unsubscribed(clientid, username, topic)"
|
||||
"\n return 9/0" % this code has fatal error
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_session_unsubscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.unsubscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case101(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
ScriptName2 = filename:join([emqx_lua_hook:lua_dir(), "mn.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return topic, \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code),
|
||||
|
||||
Code2 = "function on_client_subscribe(clientid, username, topic)"
|
||||
"\n if topic == \"a/b/c\" then"
|
||||
"\n topic = \"a1/b1/c1\";"
|
||||
"\n end"
|
||||
"\n return topic"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_subscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName2, Code2), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}}),
|
||||
?assertEqual(#message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"hello">>, headers = #{}}, Ret),
|
||||
|
||||
TopicTable = [{<<"a/b/c">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret2 = emqx_hooks:run_fold('client.subscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual([{<<"a1/b1/c1">>, [qos, 1]}, {<<"d/+/e">>, [{qos, 2}]}], Ret2).
|
||||
|
||||
case110(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return \"changed/topic\", \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{topic = <<"changed/topic">>, payload = <<"hello">>}, Ret).
|
||||
|
||||
case111(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = " function on_message_publish(topic, payload, qos, retain)"
|
||||
"\n return \"changed/topic\", \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
emqx_ctl:run_command(["luahook", "unload", ScriptName]),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg, Ret).
|
||||
|
||||
case112(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = " function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return \"changed/topic\", \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
emqx_ctl:run_command(["luahook", "unload", "abc.lua"]),
|
||||
timer:sleep(100),
|
||||
emqx_ctl:run_command(["luahook", "load", "abc.lua"]),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{topic = <<"changed/topic">>, payload = <<"hello">>}, Ret).
|
||||
|
||||
case113(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
ScriptDisabled = ScriptName ++ ".x",
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return \"changed/topic\", \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code),
|
||||
file:delete(ScriptDisabled),
|
||||
emqx_ctl:run_command(["luahook", "disable", "abc.lua"]), % this command will rename "abc.lua" to "abc.lua.x"
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg, Ret),
|
||||
true = filelib:is_file(ScriptDisabled).
|
||||
|
||||
case114(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua.x"]), % disabled script
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return \"changed/topic\", \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
emqx_ctl:run_command(["luahook", "enable", "abc.lua"]),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{topic = <<"changed/topic">>, payload = <<"hello">>}, Ret).
|
||||
|
||||
case115(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return \"changed/topic\", \"hello\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"function on_client_subscribe(ClientId, Username, Topic)"
|
||||
"\n return \"play/football\""
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\", \"on_client_subscribe\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
emqx_ctl:run_command(["luahook", "reload", "abc.lua"]),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{topic = <<"changed/topic">>, payload = <<"hello">>}, Ret),
|
||||
|
||||
TopicTable = [{<<"d/+/e">>, [{qos, 2}]}],
|
||||
Ret2 = emqx_hooks:run_fold('client.subscribe',[#{clientid => <<"myclient">>, username => <<"myuser">>}, #{}], TopicTable),
|
||||
?assertEqual([{<<"play/football">>, [{qos, 2}]}], Ret2).
|
||||
|
||||
case201(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_session_subscribed(clientid, username, topic)"
|
||||
"\n return 0"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction on_session_subscribed1()" % register_hook() is missing
|
||||
"\n return \"on_session_subscribed\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.subscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case202(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function abc(clientid, username, topic)"
|
||||
"\n return 0"
|
||||
"\nend"
|
||||
"\n"
|
||||
"\n9/0", % error code
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.subscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case203(_Config) ->
|
||||
file:del_dir(emqx_lua_hook:lua_dir()), % if this dir is not exist, what will happen?
|
||||
emqx_lua_hook:load_scripts(),
|
||||
|
||||
Topic = <<"a/b/c">>,
|
||||
Ret = emqx_hooks:run('session.subscribed',[#{clientid => <<"myclient">>, username => <<"myuser">>}, Topic, #{first => false}]),
|
||||
?assertEqual(ok, Ret).
|
||||
|
||||
case204(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return topic, payload .. \"_Z\", qos, retain"
|
||||
"\nend"
|
||||
"\n"
|
||||
"function on_client_subscribe(ClientId, Username, Topic)"
|
||||
"\n return \"play/football\""
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\", \"on_client_subscribe\", \"on_message_publish\"" % if 2 on_message_publish() are registered, what will happend?
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg#message{payload = <<"123_Z">>}, Ret).
|
||||
|
||||
case205(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_message_publish(clientid, username, topic, payload, qos, retain)"
|
||||
"\n return topic, \"hello\", qos, retain"
|
||||
"\nend_with_error" %% syntax error
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_message_publish\", \"on_client_subscribe\", \"on_message_publish\"" % if 2 on_message_publish() are registered, what will happend?
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
Msg = #message{qos = 2, flags = #{retain => true}, topic = <<"a/b/c">>, payload = <<"123">>, headers = #{}},
|
||||
Ret = emqx_hooks:run_fold('message.publish',[], Msg),
|
||||
?assertEqual(Msg, Ret).
|
||||
|
||||
case301(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_authenticate(clientid, username, peerhost, password)"
|
||||
"\n return \"ok\""
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_authenticate\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
|
||||
ClientInfo = #{clientid => undefined,
|
||||
username => <<"test">>,
|
||||
peerhost => {127, 0, 0, 1},
|
||||
password => <<"mqtt">>
|
||||
},
|
||||
Result = #{auth_result => success, anonymous => true},
|
||||
?assertEqual(Result#{auth_result => success},
|
||||
emqx_hooks:run_fold('client.authenticate', [ClientInfo], Result)).
|
||||
|
||||
case302(_Config) ->
|
||||
ScriptName = filename:join([emqx_lua_hook:lua_dir(), "abc.lua"]),
|
||||
Code = "function on_client_check_acl(clientid, username, peerhost, password, topic, pubsub)"
|
||||
"\n return \"allow\""
|
||||
"\nend"
|
||||
"\n"
|
||||
"\nfunction register_hook()"
|
||||
"\n return \"on_client_check_acl\""
|
||||
"\nend",
|
||||
ok = file:write_file(ScriptName, Code), ok = emqx_lua_hook:load_scripts(),
|
||||
ClientInfo = #{clientid => undefined,
|
||||
username => <<"test">>,
|
||||
peerhost => {127, 0, 0, 1},
|
||||
password => <<"mqtt">>
|
||||
},
|
||||
?assertEqual(allow, emqx_hooks:run_fold('client.check_acl',
|
||||
[ClientInfo, publish, <<"mytopic">>], deny)).
|
|
@ -1,6 +1,6 @@
|
|||
{application,emqx_lwm2m,
|
||||
[{description,"EMQ X LwM2M Gateway"},
|
||||
{vsn, "4.3.1"}, % strict semver, bump manually!
|
||||
{vsn, "4.3.2"}, % strict semver, bump manually!
|
||||
{modules,[]},
|
||||
{registered,[emqx_lwm2m_sup]},
|
||||
{applications,[kernel,stdlib,lwm2m_coap]},
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_lwm2m_protocol, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
{<<"4.3.[0-1]">>, [
|
||||
{restart_application, emqx_lwm2m}
|
||||
]}
|
||||
],
|
||||
[
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_lwm2m_protocol, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
{<<"4.3.[0-1]">>, [
|
||||
{restart_application, emqx_lwm2m}
|
||||
]}
|
||||
]
|
||||
}.
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_api).
|
||||
|
||||
-import(minirest, [return/1]).
|
||||
|
||||
-rest_api(#{name => list,
|
||||
method => 'GET',
|
||||
path => "/lwm2m_channels/",
|
||||
func => list,
|
||||
descr => "A list of all lwm2m channel"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list,
|
||||
method => 'GET',
|
||||
path => "/nodes/:atom:node/lwm2m_channels/",
|
||||
func => list,
|
||||
descr => "A list of lwm2m channel of a node"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_cmd,
|
||||
method => 'GET',
|
||||
path => "/lookup_cmd/:bin:ep/",
|
||||
func => lookup_cmd,
|
||||
descr => "Send a lwm2m downlink command"
|
||||
}).
|
||||
|
||||
-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"
|
||||
}).
|
||||
|
||||
-export([ list/2
|
||||
, lookup_cmd/2
|
||||
]).
|
||||
|
||||
list(#{node := Node }, Params) ->
|
||||
case Node = node() of
|
||||
true -> list(#{}, Params);
|
||||
_ -> rpc_call(Node, list, [#{}, Params])
|
||||
end;
|
||||
|
||||
list(#{}, _Params) ->
|
||||
Channels = emqx_lwm2m_cm:all_channels(),
|
||||
return({ok, format(Channels)}).
|
||||
|
||||
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(#{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.
|
||||
|
||||
rpc_call(Node, Fun, Args) ->
|
||||
case rpc:call(Node, ?MODULE, Fun, Args) of
|
||||
{badrpc, Reason} -> {error, Reason};
|
||||
Res -> Res
|
||||
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).
|
||||
|
||||
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});
|
||||
ntoa(IP) ->
|
||||
inet_parse:ntoa(IP).
|
||||
|
||||
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.
|
|
@ -0,0 +1,153 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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).
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%% 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.
|
||||
|
@ -14,22 +15,27 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lua_hook_sup).
|
||||
-module(emqx_lwm2m_cm_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init(_Args) ->
|
||||
{ok, {{one_for_one, 10, 3600},
|
||||
[#{id => lua_hook,
|
||||
start => {emqx_lua_hook, start_link, []},
|
||||
init([]) ->
|
||||
CM = #{id => emqx_lwm2m_cm,
|
||||
start => {emqx_lwm2m_cm, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [emqx_lua_hook]}]}}.
|
||||
modules => [emqx_lwm2m_cm]},
|
||||
SupFlags = #{strategy => one_for_one,
|
||||
intensity => 100,
|
||||
period => 10
|
||||
},
|
||||
{ok, {SupFlags, [CM]}}.
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
-export([ mqtt2coap/2
|
||||
, coap2mqtt/4
|
||||
, ack2mqtt/1
|
||||
, extract_path/1
|
||||
]).
|
||||
|
||||
-export([path_list/1]).
|
||||
|
|
|
@ -48,11 +48,11 @@
|
|||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-RESOURCE: " ++ Format, Args)).
|
||||
|
||||
-dialyzer([{nowarn_function, [coap_discover/2]}]).
|
||||
% we use {'absolute', string(), [{atom(), binary()}]} as coap_uri()
|
||||
% 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", []}].
|
||||
[{absolute, [<<"mqtt">>], []}].
|
||||
|
||||
coap_get(ChId, [?PREFIX], Query, Content, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p GET Query=~p, Content=~p", [self(),ChId, Query, Content]),
|
||||
|
|
|
@ -197,7 +197,10 @@ value_ex(K, Value) when K =:= <<"Integer">>; K =:= <<"Float">>; K =:= <<"Time">>
|
|||
value_ex(K, Value) when K =:= <<"String">> ->
|
||||
Value;
|
||||
value_ex(K, Value) when K =:= <<"Opaque">> ->
|
||||
Value;
|
||||
%% XXX: force to decode it with base64
|
||||
%% This may not be a good implementation, but it is
|
||||
%% consistent with the treatment of Opaque in value/3
|
||||
base64:decode(Value);
|
||||
value_ex(K, <<"true">>) when K =:= <<"Boolean">> -> <<1>>;
|
||||
value_ex(K, <<"false">>) when K =:= <<"Boolean">> -> <<0>>;
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">>
|
|||
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} ->
|
||||
|
@ -120,10 +121,8 @@ post_init(Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName,
|
|||
_ = 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}) ->
|
||||
|
||||
update_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer, register_info = RegInfo,
|
||||
coap_pid = CoapPid, endpoint_name = Epn}) ->
|
||||
UpdatedRegInfo = maps:merge(RegInfo, NewRegInfo),
|
||||
|
||||
_ = case proplists:get_value(update_msg_publish_condition,
|
||||
|
@ -134,6 +133,7 @@ update_reg_info(NewRegInfo, Lwm2mState = #lwm2m_state{
|
|||
%% - 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
|
||||
|
@ -151,7 +151,8 @@ update_reg_info(NewRegInfo, Lwm2mState = #lwm2m_state{
|
|||
register_info = UpdatedRegInfo}.
|
||||
|
||||
replace_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer,
|
||||
coap_pid = CoapPid}) ->
|
||||
coap_pid = CoapPid,
|
||||
endpoint_name = EndpointName}) ->
|
||||
_ = send_to_broker(<<"register">>, #{<<"data">> => NewRegInfo}, Lwm2mState),
|
||||
|
||||
%% - flush cached donwlink commands
|
||||
|
@ -161,7 +162,7 @@ replace_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer,
|
|||
UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer(
|
||||
maps:get(<<"lt">>, NewRegInfo), LifeTimer),
|
||||
|
||||
_ = send_auto_observe(CoapPid, NewRegInfo),
|
||||
_ = send_auto_observe(CoapPid, NewRegInfo, EndpointName),
|
||||
|
||||
?LOG(debug, "Replace RegInfo to: ~p", [NewRegInfo]),
|
||||
Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer,
|
||||
|
@ -174,15 +175,20 @@ send_ul_data(EventType, Payload, Lwm2mState=#lwm2m_state{coap_pid = CoapPid}) ->
|
|||
Lwm2mState.
|
||||
|
||||
auto_observe(Lwm2mState = #lwm2m_state{register_info = RegInfo,
|
||||
coap_pid = CoapPid}) ->
|
||||
_ = send_auto_observe(CoapPid, 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}) ->
|
||||
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),
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, IsCacheMode, EndpointName),
|
||||
Lwm2mState.
|
||||
|
||||
get_info(Lwm2mState = #lwm2m_state{endpoint_name = EndpointName, peername = {PeerHost, _},
|
||||
|
@ -238,20 +244,21 @@ time_now() -> erlang:system_time(millisecond).
|
|||
%% Deliver downlink message to coap
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
deliver_to_coap(AlternatePath, JsonData, CoapPid, CacheMode) when is_binary(JsonData)->
|
||||
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)
|
||||
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) when is_map(TermData) ->
|
||||
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);
|
||||
|
@ -266,7 +273,12 @@ deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode) when is_map(TermDat
|
|||
send_to_broker(EventType, Payload = #{}, Lwm2mState) ->
|
||||
do_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).
|
||||
|
@ -281,7 +293,7 @@ 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) ->
|
||||
send_auto_observe(CoapPid, RegInfo, EndpointName) ->
|
||||
%% - auto observe the objects
|
||||
case proplists:get_value(auto_observe, lwm2m_coap_responder:options(), false) of
|
||||
false ->
|
||||
|
@ -292,25 +304,37 @@ send_auto_observe(CoapPid, RegInfo) ->
|
|||
maps:get(<<"objectList">>, RegInfo, [])
|
||||
),
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
auto_observe(AlternatePath, Objectlists, CoapPid)
|
||||
auto_observe(AlternatePath, Objectlists, CoapPid, EndpointName)
|
||||
end.
|
||||
|
||||
auto_observe(AlternatePath, ObjectList, CoapPid) ->
|
||||
auto_observe(AlternatePath, ObjectList, CoapPid, EndpointName) ->
|
||||
?LOG(info, "Auto Observe on: ~p", [ObjectList]),
|
||||
erlang:spawn(fun() ->
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid)
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName)
|
||||
end).
|
||||
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid) ->
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName) ->
|
||||
lists:foreach(fun(ObjectPath) ->
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100)
|
||||
[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) ->
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid),
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, Interval, EndpointName) ->
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName),
|
||||
timer:sleep(Interval).
|
||||
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid) ->
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName) ->
|
||||
Payload = #{
|
||||
<<"msgType">> => <<"observe">>,
|
||||
<<"data">> => #{
|
||||
|
@ -318,7 +342,7 @@ observe_object(AlternatePath, ObjectPath, CoapPid) ->
|
|||
}
|
||||
},
|
||||
?LOG(info, "Observe ObjectPath: ~p", [ObjectPath]),
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, false).
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, false, EndpointName).
|
||||
|
||||
do_deliver_to_coap_slowly(CoapPid, CoapRequestList, Interval) ->
|
||||
erlang:spawn(fun() ->
|
||||
|
|
|
@ -29,4 +29,11 @@ start_link() ->
|
|||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init(_Args) ->
|
||||
{ok, { {one_for_all, 10, 3600}, [?CHILD(emqx_lwm2m_xml_object_db)] }}.
|
||||
CmSup = #{id => emqx_lwm2m_cm_sup,
|
||||
start => {emqx_lwm2m_cm_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => infinity,
|
||||
type => supervisor,
|
||||
modules => [emqx_lwm2m_cm_sup]
|
||||
},
|
||||
{ok, { {one_for_all, 10, 3600}, [?CHILD(emqx_lwm2m_xml_object_db), CmSup] }}.
|
||||
|
|
|
@ -21,9 +21,11 @@
|
|||
|
||||
-export([ get_obj_def/2
|
||||
, get_object_id/1
|
||||
, get_object_name/1
|
||||
, get_object_and_resource_id/2
|
||||
, get_resource_type/2
|
||||
, get_resource_name/2
|
||||
, get_resource_operations/2
|
||||
]).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
|
@ -42,6 +44,10 @@ get_object_id(ObjDefinition) ->
|
|||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
ObjectId.
|
||||
|
||||
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),
|
||||
|
@ -60,3 +66,8 @@ get_resource_name(ResourceIdInt, ObjDefinition) ->
|
|||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=Name}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Name/text()", ObjDefinition),
|
||||
Name.
|
||||
|
||||
get_resource_operations(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=Operations}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Operations/text()", ObjDefinition),
|
||||
Operations.
|
||||
|
|
|
@ -58,7 +58,7 @@ find_objectid(ObjectId) ->
|
|||
false -> ObjectId
|
||||
end,
|
||||
case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of
|
||||
[] -> error(no_xml_definition);
|
||||
[] -> {error, no_xml_definition};
|
||||
[{ObjectId, Xml}] -> Xml
|
||||
end.
|
||||
|
||||
|
@ -121,8 +121,10 @@ load(BaseDir) ->
|
|||
true -> BaseDir++"*.xml";
|
||||
false -> BaseDir++"/*.xml"
|
||||
end,
|
||||
AllXmlFiles = filelib:wildcard(Wild),
|
||||
load_loop(AllXmlFiles).
|
||||
case filelib:wildcard(Wild) of
|
||||
[] -> error(no_xml_files_found, BaseDir);
|
||||
AllXmlFiles -> load_loop(AllXmlFiles)
|
||||
end.
|
||||
|
||||
load_loop([]) ->
|
||||
ok;
|
||||
|
|
|
@ -40,6 +40,7 @@ all() ->
|
|||
, {group, test_grp_4_discover}
|
||||
, {group, test_grp_5_write_attr}
|
||||
, {group, test_grp_6_observe}
|
||||
, {group, test_grp_8_object_19}
|
||||
].
|
||||
|
||||
suite() -> [{timetrap, {seconds, 90}}].
|
||||
|
@ -98,9 +99,9 @@ groups() ->
|
|||
]},
|
||||
{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
|
||||
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,
|
||||
|
@ -1655,6 +1656,7 @@ case80_specail_object_19_1_0_write(Config) ->
|
|||
<<"value">> => base64:encode(<<12345:32>>)
|
||||
}
|
||||
},
|
||||
|
||||
CommandJson = emqx_json:encode(Command),
|
||||
test_mqtt_broker:publish(CommandTopic, CommandJson, 0),
|
||||
timer:sleep(50),
|
||||
|
@ -1663,7 +1665,7 @@ case80_specail_object_19_1_0_write(Config) ->
|
|||
Path2 = get_coap_path(Options2),
|
||||
?assertEqual(put, Method2),
|
||||
?assertEqual(<<"/19/1/0">>, Path2),
|
||||
?assertEqual(<<12345:32>>, Payload2),
|
||||
?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),
|
||||
|
@ -1672,6 +1674,7 @@ case80_specail_object_19_1_0_write(Config) ->
|
|||
ReadResult = emqx_json:encode(#{
|
||||
<<"requestID">> => CmdId, <<"cacheID">> => CmdId,
|
||||
<<"data">> => #{
|
||||
<<"reqPath">> => <<"/19/1/0">>,
|
||||
<<"code">> => <<"2.04">>,
|
||||
<<"codeMsg">> => <<"changed">>
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_rule_engine,
|
||||
[{description, "EMQ X Rule Engine"},
|
||||
{vsn, "4.3.2"}, % strict semver, bump manually!
|
||||
{vsn, "4.3.3"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_rule_engine_sup, emqx_rule_registry]},
|
||||
{applications, [kernel,stdlib,rulesql,getopt]},
|
||||
|
|
|
@ -1,21 +1,37 @@
|
|||
%% -*-: erlang -*-
|
||||
{"4.3.2",
|
||||
{"4.3.3",
|
||||
[ {"4.3.0",
|
||||
[ {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []},
|
||||
{load_module, emqx_rule_engine, brutal_purge, soft_purge, []}
|
||||
[ {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}
|
||||
, {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}
|
||||
, {load_module, emqx_rule_registry, brutal_purge, soft_purge, []}
|
||||
, {apply, {emqx_stats, cancel_update, [rule_registery_stats]}}
|
||||
]},
|
||||
{"4.3.1",
|
||||
[ {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}
|
||||
, {load_module, emqx_rule_registry, brutal_purge, soft_purge, []}
|
||||
, {apply, {emqx_stats, cancel_update, [rule_registery_stats]}}
|
||||
]},
|
||||
{"4.3.2",
|
||||
[ {load_module, emqx_rule_registry, brutal_purge, soft_purge, []}
|
||||
, {apply, {emqx_stats, cancel_update, [rule_registery_stats]}}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{"4.3.0",
|
||||
[ {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []},
|
||||
{load_module, emqx_rule_engine, brutal_purge, soft_purge, []}
|
||||
[ {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}
|
||||
, {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}
|
||||
, {load_module, emqx_rule_registry, brutal_purge, soft_purge, []}
|
||||
, {apply, {emqx_stats, cancel_update, [rule_registery_stats]}}
|
||||
]},
|
||||
{"4.3.1",
|
||||
[ {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}
|
||||
, {load_module, emqx_rule_registry, brutal_purge, soft_purge, []}
|
||||
, {apply, {emqx_stats, cancel_update, [rule_registery_stats]}}
|
||||
]},
|
||||
{"4.3.2",
|
||||
[ {load_module, emqx_rule_registry, brutal_purge, soft_purge, []}
|
||||
, {apply, {emqx_stats, cancel_update, [rule_registery_stats]}}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
]
|
||||
|
|
|
@ -93,13 +93,6 @@
|
|||
|
||||
-define(REGISTRY, ?MODULE).
|
||||
|
||||
%% Statistics
|
||||
-define(STATS,
|
||||
[ {?RULE_TAB, 'rules.count', 'rules.max'}
|
||||
, {?ACTION_TAB, 'actions.count', 'actions.max'}
|
||||
, {?RES_TAB, 'resources.count', 'resources.max'}
|
||||
]).
|
||||
|
||||
-define(T_CALL, 10000).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -392,8 +385,11 @@ find_rules_depends_on_resource(ResId) ->
|
|||
end, [], get_rules()).
|
||||
|
||||
search_action_despends_on_resource(ResId, Actions) ->
|
||||
lists:search(fun(#action_instance{args = #{<<"$resource">> := ResId0}}) ->
|
||||
ResId0 =:= ResId
|
||||
lists:search(fun
|
||||
(#action_instance{args = #{<<"$resource">> := ResId0}}) ->
|
||||
ResId0 =:= ResId;
|
||||
(_) ->
|
||||
false
|
||||
end, Actions).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -439,8 +435,6 @@ delete_resource_type(Type) ->
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
%% Enable stats timer
|
||||
ok = emqx_stats:update_interval(rule_registery_stats, fun update_stats/0),
|
||||
_TableId = ets:new(?KV_TAB, [named_table, set, public, {write_concurrency, true},
|
||||
{read_concurrency, true}]),
|
||||
{ok, #{}}.
|
||||
|
@ -466,7 +460,7 @@ handle_info(Info, State) ->
|
|||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
emqx_stats:cancel_update(rule_registery_stats).
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
@ -475,13 +469,6 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%% Private functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
update_stats() ->
|
||||
lists:foreach(
|
||||
fun({Tab, Stat, MaxStat}) ->
|
||||
Size = mnesia:table_info(Tab, size),
|
||||
emqx_stats:setstat(Stat, MaxStat, Size)
|
||||
end, ?STATS).
|
||||
|
||||
get_all_records(Tab) ->
|
||||
%mnesia:dirty_match_object(Tab, mnesia:table_info(Tab, wild_pattern)).
|
||||
ets:tab2list(Tab).
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
data/
|
||||
*.swp
|
||||
*.swo
|
||||
.erlang.mk/
|
||||
emqx_sasl.d
|
||||
erlang.mk
|
||||
rebar3.crashdump
|
||||
_build
|
||||
cover/
|
||||
ct.coverdata
|
||||
eunit.coverdata
|
||||
logs/
|
||||
rebar.lock
|
||||
test/ct.cover.spec
|
||||
etc/emqx_sasl.conf.rendered
|
||||
.rebar3/
|
|
@ -1,2 +0,0 @@
|
|||
# emqx-sasl
|
||||
Simple Authentication and Security Layer
|
|
@ -1,19 +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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(APP, emqx_sasl).
|
||||
|
||||
-define(SCRAM_AUTH_TAB, scram_auth).
|
|
@ -1,19 +0,0 @@
|
|||
{deps,
|
||||
[{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warnings_as_errors,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
{parse_transform}]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions]}.
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_sasl,
|
||||
[{description, "EMQ X SASL"},
|
||||
{vsn, "4.3.1"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_sasl_sup]},
|
||||
{applications, [kernel,stdlib,pbkdf2]},
|
||||
{mod, {emqx_sasl_app,[]}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://github.com/emqx/emqx-sasl"}
|
||||
]}
|
||||
]}.
|
|
@ -1,13 +0,0 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.0", [
|
||||
{restart_application, emqx_sasl}
|
||||
]}
|
||||
],
|
||||
[
|
||||
{"4.3.0", [
|
||||
{restart_application, emqx_sasl}
|
||||
]}
|
||||
]
|
||||
}.
|
|
@ -1,56 +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_sasl).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([ load/0
|
||||
, unload/0
|
||||
, init/0
|
||||
, check/3
|
||||
, supported/0
|
||||
]).
|
||||
|
||||
load() ->
|
||||
emqx:hook('client.enhanced_authenticate', {?MODULE, check, []}).
|
||||
|
||||
unload() ->
|
||||
emqx:unhook('client.enhanced_authenticate', {?MODULE, check}).
|
||||
|
||||
init() ->
|
||||
emqx_sasl_scram:init().
|
||||
|
||||
check(Method, Data, Cache) ->
|
||||
try
|
||||
case Method of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
case emqx_sasl_scram:check(Data, Cache) of
|
||||
{ok, NData, NCache} -> {ok, {ok, NData, NCache}};
|
||||
{continue, NData, NCache} -> {ok, {continue, NData, NCache}};
|
||||
Re -> {stop, Re}
|
||||
end;
|
||||
_ ->
|
||||
{error, unsupported_mechanism}
|
||||
end
|
||||
catch
|
||||
_Class:_Reason:Stack ->
|
||||
?LOG(error, "[emqx_sasl] authentication failed: ~0p", [Stack]),
|
||||
{error, authentication_failed}
|
||||
end.
|
||||
|
||||
supported() ->
|
||||
[<<"SCRAM-SHA-1">>].
|
|
@ -1,227 +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_sasl_api).
|
||||
|
||||
-include("emqx_sasl.hrl").
|
||||
|
||||
-import(minirest, [ return/0
|
||||
, return/1
|
||||
]).
|
||||
|
||||
-rest_api(#{name => add,
|
||||
method => 'POST',
|
||||
path => "/sasl",
|
||||
func => add,
|
||||
descr => "Add authentication information"}).
|
||||
|
||||
-rest_api(#{name => delete,
|
||||
method => 'DELETE',
|
||||
path => "/sasl",
|
||||
func => delete,
|
||||
descr => "Delete authentication information"}).
|
||||
|
||||
-rest_api(#{name => update,
|
||||
method => 'PUT',
|
||||
path => "/sasl",
|
||||
func => update,
|
||||
descr => "Update authentication information"}).
|
||||
|
||||
-rest_api(#{name => get,
|
||||
method => 'GET',
|
||||
path => "/sasl",
|
||||
func => get,
|
||||
descr => "Get authentication information"}).
|
||||
|
||||
-export([ add/2
|
||||
, delete/2
|
||||
, update/2
|
||||
, get/2
|
||||
]).
|
||||
|
||||
add(_Bindings, Params) ->
|
||||
case pipeline([fun ensure_required_add_params/1,
|
||||
fun validate_params/1,
|
||||
fun do_add/1], Params) of
|
||||
ok ->
|
||||
return();
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end.
|
||||
|
||||
delete(_Bindings, Params) ->
|
||||
case pipeline([fun ensure_required_delete_params/1,
|
||||
fun validate_params/1,
|
||||
fun do_delete/1], Params) of
|
||||
ok ->
|
||||
return();
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end.
|
||||
|
||||
update(_Bindings, Params) ->
|
||||
case pipeline([fun ensure_required_add_params/1,
|
||||
fun validate_params/1,
|
||||
fun do_update/1], Params) of
|
||||
ok ->
|
||||
return();
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end.
|
||||
|
||||
get(Bindings, Params) when is_list(Params) ->
|
||||
get(Bindings, maps:from_list(Params));
|
||||
|
||||
get(_Bindings, #{<<"mechanism">> := Mechanism0,
|
||||
<<"username">> := Username0}) ->
|
||||
Mechanism = urldecode(Mechanism0),
|
||||
Username = urldecode(Username0),
|
||||
case Mechanism of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
case emqx_sasl_scram:lookup(Username) of
|
||||
{ok, AuthInfo = #{salt := Salt}} ->
|
||||
return({ok, AuthInfo#{salt => base64:decode(Salt)}});
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end;
|
||||
_ ->
|
||||
return({error, unsupported_mechanism})
|
||||
end;
|
||||
get(_Bindings, #{<<"mechanism">> := Mechanism}) ->
|
||||
case urldecode(Mechanism) of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
Data = #{Mechanism => mnesia:dirty_all_keys(?SCRAM_AUTH_TAB)},
|
||||
return({ok, Data});
|
||||
_ ->
|
||||
return({error, <<"Unsupported mechanism">>})
|
||||
end;
|
||||
|
||||
get(_Bindings, _Params) ->
|
||||
Data = lists:foldl(fun(Mechanism, Acc) ->
|
||||
case Mechanism of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
[#{Mechanism => mnesia:dirty_all_keys(?SCRAM_AUTH_TAB)} | Acc]
|
||||
end
|
||||
end, [], emqx_sasl:supported()),
|
||||
return({ok, Data}).
|
||||
|
||||
ensure_required_add_params(Params) when is_list(Params) ->
|
||||
case proplists:get_value(<<"mechanism">>, Params) of
|
||||
undefined ->
|
||||
{missing, missing_required_param};
|
||||
Mechaism ->
|
||||
ensure_required_add_params(Mechaism, Params)
|
||||
end.
|
||||
|
||||
ensure_required_add_params(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Required = [<<"username">>, <<"password">>, <<"salt">>],
|
||||
case erlang:map_size(maps:with(Required, maps:from_list(Params))) =:= erlang:length(Required) of
|
||||
true -> ok;
|
||||
false -> {missing, missing_required_param}
|
||||
end;
|
||||
ensure_required_add_params(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
ensure_required_delete_params(Params) when is_list(Params) ->
|
||||
case proplists:get_value(<<"mechanism">>, Params) of
|
||||
undefined ->
|
||||
{missing, missing_required_param};
|
||||
Mechaism ->
|
||||
ensure_required_delete_params(Mechaism, Params)
|
||||
end.
|
||||
|
||||
ensure_required_delete_params(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Required = [<<"username">>],
|
||||
case erlang:map_size(maps:with(Required, maps:from_list(Params))) =:= erlang:length(Required) of
|
||||
true -> ok;
|
||||
false -> {missing, missing_required_param}
|
||||
end;
|
||||
ensure_required_delete_params(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
validate_params(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
validate_params(Mechaism, Params).
|
||||
|
||||
validate_params(<<"SCRAM-SHA-1">>, []) ->
|
||||
ok;
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"username">>, Username} | More]) when is_binary(Username) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"username">>, _} | _]) ->
|
||||
{error, invalid_username};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"password">>, Password} | More]) when is_binary(Password) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"password">>, _} | _]) ->
|
||||
{error, invalid_password};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"salt">>, Salt} | More]) when is_binary(Salt) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"salt">>, _} | _]) ->
|
||||
{error, invalid_salt};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"iteration_count">>, IterationCount} | More]) when is_integer(IterationCount) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"iteration_count">>, _} | _]) ->
|
||||
{error, invalid_iteration_count};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [_ | More]) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More).
|
||||
|
||||
do_add(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
do_add(Mechaism, Params).
|
||||
|
||||
do_add(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Username = proplists:get_value(<<"username">>, Params),
|
||||
Password = proplists:get_value(<<"password">>, Params),
|
||||
Salt = proplists:get_value(<<"salt">>, Params),
|
||||
IterationCount = proplists:get_value(<<"iteration_count">>, Params, 4096),
|
||||
emqx_sasl_scram:add(Username, Password, Salt, IterationCount);
|
||||
do_add(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
do_delete(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
do_delete(Mechaism, Params).
|
||||
|
||||
do_delete(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Username = proplists:get_value(<<"username">>, Params),
|
||||
emqx_sasl_scram:delete(Username);
|
||||
do_delete(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
do_update(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
do_update(Mechaism, Params).
|
||||
|
||||
do_update(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Username = proplists:get_value(<<"username">>, Params),
|
||||
Password = proplists:get_value(<<"password">>, Params),
|
||||
Salt = proplists:get_value(<<"salt">>, Params),
|
||||
IterationCount = proplists:get_value(<<"iteration_count">>, Params, 4096),
|
||||
emqx_sasl_scram:update(Username, Password, Salt, IterationCount);
|
||||
do_update(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
pipeline([], _) ->
|
||||
ok;
|
||||
pipeline([Fun | More], Params) ->
|
||||
case Fun(Params) of
|
||||
ok ->
|
||||
pipeline(More, Params);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
urldecode(S) ->
|
||||
emqx_http_lib:uri_decode(S).
|
|
@ -1,46 +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_sasl_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
ok = emqx_sasl:init(),
|
||||
_ = emqx_sasl:load(),
|
||||
emqx_sasl_cli:load(),
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
stop(_State) ->
|
||||
emqx_sasl_cli:unload(),
|
||||
emqx_sasl:unload().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Dummy supervisor
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, { {one_for_all, 1, 10}, []} }.
|
|
@ -1,82 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_sasl_cli).
|
||||
|
||||
-include("emqx_sasl.hrl").
|
||||
|
||||
%% APIs
|
||||
-export([ load/0
|
||||
, unload/0
|
||||
, cli/1
|
||||
]).
|
||||
|
||||
load() ->
|
||||
emqx_ctl:register_command(sasl, {?MODULE, cli}, []).
|
||||
|
||||
unload() ->
|
||||
emqx_ctl:unregister_command(sasl).
|
||||
|
||||
cli(["scram", "add", Username, Password, Salt]) ->
|
||||
cli(["scram", "add", Username, Password, Salt, "4096"]);
|
||||
cli(["scram", "add", Username, Password, Salt, IterationCount]) ->
|
||||
case emqx_sasl_scram:add(list_to_binary(Username),
|
||||
list_to_binary(Password),
|
||||
list_to_binary(Salt),
|
||||
list_to_integer(IterationCount)) of
|
||||
ok ->
|
||||
emqx_ctl:print("Authentication information added successfully~n");
|
||||
{error, already_existed} ->
|
||||
emqx_ctl:print("Authentication information already exists~n")
|
||||
end;
|
||||
|
||||
cli(["scram", "delete", Username0]) ->
|
||||
Username = list_to_binary(Username0),
|
||||
ok = emqx_sasl_scram:delete(Username),
|
||||
emqx_ctl:print("Authentication information deleted successfully~n");
|
||||
|
||||
cli(["scram", "update", Username, Password, Salt]) ->
|
||||
cli(["scram", "update", Username, Password, Salt, "4096"]);
|
||||
cli(["scram", "update", Username, Password, Salt, IterationCount]) ->
|
||||
case emqx_sasl_scram:update(list_to_binary(Username),
|
||||
list_to_binary(Password),
|
||||
list_to_binary(Salt),
|
||||
list_to_integer(IterationCount)) of
|
||||
ok ->
|
||||
emqx_ctl:print("Authentication information updated successfully~n");
|
||||
{error, not_found} ->
|
||||
emqx_ctl:print("Authentication information not found~n")
|
||||
end;
|
||||
|
||||
cli(["scram", "lookup", Username0]) ->
|
||||
Username = list_to_binary(Username0),
|
||||
case emqx_sasl_scram:lookup(Username) of
|
||||
{ok, #{username := Username,
|
||||
stored_key := StoredKey,
|
||||
server_key := ServerKey,
|
||||
salt := Salt,
|
||||
iteration_count := IterationCount}} ->
|
||||
emqx_ctl:print("Username: ~s, Stored Key: ~s, Server Key: ~s, Salt: ~s, Iteration Count: ~p~n",
|
||||
[Username, StoredKey, ServerKey, base64:decode(Salt), IterationCount]);
|
||||
{error, not_found} ->
|
||||
emqx_ctl:print("Authentication information not found~n")
|
||||
end;
|
||||
|
||||
cli(_) ->
|
||||
emqx_ctl:usage([{"sasl scram add <Username> <Password> <Salt> [<IterationCount>]", "Add SCRAM-SHA-1 authentication information"},
|
||||
{"sasl scram delete <Username>", "Delete SCRAM-SHA-1 authentication information"},
|
||||
{"sasl scram update <Username> <Password> <Salt> [<IterationCount>]", "Update SCRAM-SHA-1 authentication information"},
|
||||
{"sasl scram lookup <Username>", "Check if SCRAM-SHA-1 authentication information exists"}]).
|
|
@ -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_sasl_scram).
|
||||
|
||||
-include("emqx_sasl.hrl").
|
||||
|
||||
-export([ init/0
|
||||
, add/3
|
||||
, add/4
|
||||
, update/3
|
||||
, update/4
|
||||
, delete/1
|
||||
, lookup/1
|
||||
, check/2
|
||||
, make_client_first/1]).
|
||||
|
||||
-record(?SCRAM_AUTH_TAB, {
|
||||
username,
|
||||
stored_key,
|
||||
server_key,
|
||||
salt,
|
||||
iteration_count :: integer()
|
||||
}).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
init() ->
|
||||
ok = ekka_mnesia:create_table(?SCRAM_AUTH_TAB, [
|
||||
{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, ?SCRAM_AUTH_TAB)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||
ok = ekka_mnesia:copy_table(?SCRAM_AUTH_TAB, disc_copies).
|
||||
|
||||
add(Username, Password, Salt) ->
|
||||
add(Username, Password, Salt, 4096).
|
||||
|
||||
add(Username, Password, Salt, IterationCount) ->
|
||||
case lookup(Username) of
|
||||
{error, not_found} ->
|
||||
do_add(Username, Password, Salt, IterationCount);
|
||||
_ ->
|
||||
{error, already_existed}
|
||||
end.
|
||||
|
||||
update(Username, Password, Salt) ->
|
||||
update(Username, Password, Salt, 4096).
|
||||
|
||||
update(Username, Password, Salt, IterationCount) ->
|
||||
case lookup(Username) of
|
||||
{error, not_found} ->
|
||||
{error, not_found};
|
||||
_ ->
|
||||
do_add(Username, Password, Salt, IterationCount)
|
||||
end.
|
||||
|
||||
delete(Username) ->
|
||||
ret(mnesia:transaction(fun mnesia:delete/3, [?SCRAM_AUTH_TAB, Username, write])).
|
||||
|
||||
lookup(Username) ->
|
||||
case mnesia:dirty_read(?SCRAM_AUTH_TAB, Username) of
|
||||
[#scram_auth{username = Username,
|
||||
stored_key = StoredKey,
|
||||
server_key = ServerKey,
|
||||
salt = Salt,
|
||||
iteration_count = IterationCount}] ->
|
||||
{ok, #{username => Username,
|
||||
stored_key => StoredKey,
|
||||
server_key => ServerKey,
|
||||
salt => Salt,
|
||||
iteration_count => IterationCount}};
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
do_add(Username, Password, Salt, IterationCount) ->
|
||||
SaltedPassword = pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ClientKey = client_key(SaltedPassword),
|
||||
ServerKey = server_key(SaltedPassword),
|
||||
StoredKey = crypto:hash(sha, ClientKey),
|
||||
AuthInfo = #scram_auth{username = Username,
|
||||
stored_key = base64:encode(StoredKey),
|
||||
server_key = base64:encode(ServerKey),
|
||||
salt = base64:encode(Salt),
|
||||
iteration_count = IterationCount},
|
||||
ret(mnesia:transaction(fun mnesia:write/3, [?SCRAM_AUTH_TAB, AuthInfo, write])).
|
||||
|
||||
ret({atomic, ok}) -> ok;
|
||||
ret({aborted, Error}) -> {error, Error}.
|
||||
|
||||
check(Data, Cache) when map_size(Cache) =:= 0 ->
|
||||
check_client_first(Data);
|
||||
check(Data, Cache) ->
|
||||
case maps:get(next_step, Cache, undefined) of
|
||||
undefined -> check_server_first(Data, Cache);
|
||||
check_client_final -> check_client_final(Data, Cache);
|
||||
check_server_final -> check_server_final(Data, Cache)
|
||||
end.
|
||||
|
||||
check_client_first(ClientFirst) ->
|
||||
ClientFirstWithoutHeader = without_header(ClientFirst),
|
||||
Attributes = parse(ClientFirstWithoutHeader),
|
||||
Username = proplists:get_value(username, Attributes),
|
||||
ClientNonce = proplists:get_value(nonce, Attributes),
|
||||
case lookup(Username) of
|
||||
{error, not_found} ->
|
||||
{error, not_found};
|
||||
{ok, #{stored_key := StoredKey0,
|
||||
server_key := ServerKey0,
|
||||
salt := Salt0,
|
||||
iteration_count := IterationCount}} ->
|
||||
StoredKey = base64:decode(StoredKey0),
|
||||
ServerKey = base64:decode(ServerKey0),
|
||||
Salt = base64:decode(Salt0),
|
||||
ServerNonce = nonce(),
|
||||
Nonce = list_to_binary(binary_to_list(ClientNonce) ++ binary_to_list(ServerNonce)),
|
||||
ServerFirst = make_server_first(Nonce, Salt, IterationCount),
|
||||
{continue, ServerFirst, #{next_step => check_client_final,
|
||||
client_first_without_header => ClientFirstWithoutHeader,
|
||||
server_first => ServerFirst,
|
||||
stored_key => StoredKey,
|
||||
server_key => ServerKey,
|
||||
nonce => Nonce}}
|
||||
end.
|
||||
|
||||
check_client_final(ClientFinal, #{client_first_without_header := ClientFirstWithoutHeader,
|
||||
server_first := ServerFirst,
|
||||
server_key := ServerKey,
|
||||
stored_key := StoredKey,
|
||||
nonce := OldNonce}) ->
|
||||
ClientFinalWithoutProof = without_proof(ClientFinal),
|
||||
Attributes = parse(ClientFinal),
|
||||
ClientProof = base64:decode(proplists:get_value(proof, Attributes)),
|
||||
NewNonce = proplists:get_value(nonce, Attributes),
|
||||
Auth0 = io_lib:format("~s,~s,~s", [ClientFirstWithoutHeader, ServerFirst, ClientFinalWithoutProof]),
|
||||
Auth = iolist_to_binary(Auth0),
|
||||
ClientSignature = hmac(StoredKey, Auth),
|
||||
ClientKey = crypto:exor(ClientProof, ClientSignature),
|
||||
case NewNonce =:= OldNonce andalso crypto:hash(sha, ClientKey) =:= StoredKey of
|
||||
true ->
|
||||
ServerSignature = hmac(ServerKey, Auth),
|
||||
ServerFinal = make_server_final(ServerSignature),
|
||||
{ok, ServerFinal, #{}};
|
||||
false ->
|
||||
{error, invalid_client_final}
|
||||
end.
|
||||
|
||||
check_server_first(ServerFirst, #{password := Password,
|
||||
client_first := ClientFirst}) ->
|
||||
Attributes = parse(ServerFirst),
|
||||
Nonce = proplists:get_value(nonce, Attributes),
|
||||
ClientFirstWithoutHeader = without_header(ClientFirst),
|
||||
ClientFinalWithoutProof = serialize([{channel_binding, <<"biws">>}, {nonce, Nonce}]),
|
||||
Auth = list_to_binary(io_lib:format("~s,~s,~s", [ClientFirstWithoutHeader, ServerFirst, ClientFinalWithoutProof])),
|
||||
Salt = base64:decode(proplists:get_value(salt, Attributes)),
|
||||
IterationCount = binary_to_integer(proplists:get_value(iteration_count, Attributes)),
|
||||
SaltedPassword = pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ClientKey = client_key(SaltedPassword),
|
||||
StoredKey = crypto:hash(sha, ClientKey),
|
||||
ClientSignature = hmac(StoredKey, Auth),
|
||||
ClientProof = base64:encode(crypto:exor(ClientKey, ClientSignature)),
|
||||
ClientFinal = serialize([{channel_binding, <<"biws">>},
|
||||
{nonce, Nonce},
|
||||
{proof, ClientProof}]),
|
||||
{continue, ClientFinal, #{next_step => check_server_final,
|
||||
password => Password,
|
||||
client_first => ClientFirst,
|
||||
server_first => ServerFirst}}.
|
||||
|
||||
check_server_final(ServerFinal, #{password := Password,
|
||||
client_first := ClientFirst,
|
||||
server_first := ServerFirst}) ->
|
||||
NewAttributes = parse(ServerFinal),
|
||||
Attributes = parse(ServerFirst),
|
||||
Nonce = proplists:get_value(nonce, Attributes),
|
||||
ClientFirstWithoutHeader = without_header(ClientFirst),
|
||||
ClientFinalWithoutProof = serialize([{channel_binding, <<"biws">>}, {nonce, Nonce}]),
|
||||
Auth = list_to_binary(io_lib:format("~s,~s,~s", [ClientFirstWithoutHeader, ServerFirst, ClientFinalWithoutProof])),
|
||||
Salt = base64:decode(proplists:get_value(salt, Attributes)),
|
||||
IterationCount = binary_to_integer(proplists:get_value(iteration_count, Attributes)),
|
||||
SaltedPassword = pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ServerKey = server_key(SaltedPassword),
|
||||
ServerSignature = hmac(ServerKey, Auth),
|
||||
case base64:encode(ServerSignature) =:= proplists:get_value(verifier, NewAttributes) of
|
||||
true ->
|
||||
{ok, <<>>, #{}};
|
||||
false ->
|
||||
{stop, invalid_server_final}
|
||||
end.
|
||||
|
||||
make_client_first(Username) ->
|
||||
list_to_binary("n,," ++ binary_to_list(serialize([{username, Username}, {nonce, nonce()}]))).
|
||||
|
||||
make_server_first(Nonce, Salt, IterationCount) ->
|
||||
serialize([{nonce, Nonce}, {salt, base64:encode(Salt)}, {iteration_count, IterationCount}]).
|
||||
|
||||
make_server_final(ServerSignature) ->
|
||||
serialize([{verifier, base64:encode(ServerSignature)}]).
|
||||
|
||||
nonce() ->
|
||||
base64:encode([$a + rand:uniform(26) || _ <- lists:seq(1, 10)]).
|
||||
|
||||
pbkdf2_sha_1(Password, Salt, IterationCount) ->
|
||||
{ok, Bin} = pbkdf2:pbkdf2(sha, Password, Salt, IterationCount),
|
||||
pbkdf2:to_hex(Bin).
|
||||
|
||||
-if(?OTP_RELEASE >= 23).
|
||||
hmac(Key, Data) ->
|
||||
HMAC = crypto:mac_init(hmac, sha, Key),
|
||||
HMAC1 = crypto:mac_update(HMAC, Data),
|
||||
crypto:mac_final(HMAC1).
|
||||
-else.
|
||||
hmac(Key, Data) ->
|
||||
HMAC = crypto:hmac_init(sha, Key),
|
||||
HMAC1 = crypto:hmac_update(HMAC, Data),
|
||||
crypto:hmac_final(HMAC1).
|
||||
-endif.
|
||||
|
||||
client_key(SaltedPassword) ->
|
||||
hmac(<<"Client Key">>, SaltedPassword).
|
||||
|
||||
server_key(SaltedPassword) ->
|
||||
hmac(<<"Server Key">>, SaltedPassword).
|
||||
|
||||
without_header(<<"n,,", ClientFirstWithoutHeader/binary>>) ->
|
||||
ClientFirstWithoutHeader;
|
||||
without_header(<<GS2CbindFlag:1/binary, _/binary>>) ->
|
||||
error({unsupported_gs2_cbind_flag, binary_to_atom(GS2CbindFlag, utf8)}).
|
||||
|
||||
without_proof(ClientFinal) ->
|
||||
[ClientFinalWithoutProof | _] = binary:split(ClientFinal, <<",p=">>, [global, trim_all]),
|
||||
ClientFinalWithoutProof.
|
||||
|
||||
parse(Message) ->
|
||||
Attributes = binary:split(Message, <<$,>>, [global, trim_all]),
|
||||
lists:foldl(fun(<<Key:1/binary, "=", Value/binary>>, Acc) ->
|
||||
[{to_long(Key), Value} | Acc]
|
||||
end, [], Attributes).
|
||||
|
||||
serialize(Attributes) ->
|
||||
iolist_to_binary(
|
||||
lists:foldl(fun({Key, Value}, []) ->
|
||||
[to_short(Key), "=", to_list(Value)];
|
||||
({Key, Value}, Acc) ->
|
||||
Acc ++ [",", to_short(Key), "=", to_list(Value)]
|
||||
end, [], Attributes)).
|
||||
|
||||
to_long(<<"a">>) ->
|
||||
authzid;
|
||||
to_long(<<"c">>) ->
|
||||
channel_binding;
|
||||
to_long(<<"n">>) ->
|
||||
username;
|
||||
to_long(<<"p">>) ->
|
||||
proof;
|
||||
to_long(<<"r">>) ->
|
||||
nonce;
|
||||
to_long(<<"s">>) ->
|
||||
salt;
|
||||
to_long(<<"v">>) ->
|
||||
verifier;
|
||||
to_long(<<"i">>) ->
|
||||
iteration_count;
|
||||
to_long(_) ->
|
||||
error(test).
|
||||
|
||||
to_short(authzid) ->
|
||||
"a";
|
||||
to_short(channel_binding) ->
|
||||
"c";
|
||||
to_short(username) ->
|
||||
"n";
|
||||
to_short(proof) ->
|
||||
"p";
|
||||
to_short(nonce) ->
|
||||
"r";
|
||||
to_short(salt) ->
|
||||
"s";
|
||||
to_short(verifier) ->
|
||||
"v";
|
||||
to_short(iteration_count) ->
|
||||
"i";
|
||||
to_short(_) ->
|
||||
error(test).
|
||||
|
||||
to_list(V) when is_binary(V) ->
|
||||
binary_to_list(V);
|
||||
to_list(V) when is_list(V) ->
|
||||
V;
|
||||
to_list(V) when is_integer(V) ->
|
||||
integer_to_list(V);
|
||||
to_list(_) ->
|
||||
error(bad_type).
|
||||
|
|
@ -1,140 +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_sasl_scram_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_sasl]),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_ct_helpers:stop_apps([]).
|
||||
|
||||
all() -> emqx_ct:all(?MODULE).
|
||||
|
||||
t_crud(_) ->
|
||||
Username = <<"test">>,
|
||||
Password = <<"public">>,
|
||||
Salt = <<"emqx">>,
|
||||
IterationCount = 4096,
|
||||
EncodedSalt = base64:encode(Salt),
|
||||
SaltedPassword = emqx_sasl_scram:pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ClientKey = emqx_sasl_scram:client_key(SaltedPassword),
|
||||
ServerKey = base64:encode(emqx_sasl_scram:server_key(SaltedPassword)),
|
||||
StoredKey = base64:encode(crypto:hash(sha, ClientKey)),
|
||||
|
||||
{error, not_found} = emqx_sasl_scram:lookup(Username),
|
||||
ok = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
{error, already_existed} = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
|
||||
{ok, #{username := Username,
|
||||
stored_key := StoredKey,
|
||||
server_key := ServerKey,
|
||||
salt := EncodedSalt,
|
||||
iteration_count := IterationCount}} = emqx_sasl_scram:lookup(Username),
|
||||
|
||||
NewSalt = <<"new salt">>,
|
||||
NewEncodedSalt = base64:encode(NewSalt),
|
||||
emqx_sasl_scram:update(Username, Password, NewSalt),
|
||||
{ok, #{username := Username,
|
||||
salt := NewEncodedSalt}} = emqx_sasl_scram:lookup(Username),
|
||||
emqx_sasl_scram:delete(Username),
|
||||
{error, not_found} = emqx_sasl_scram:lookup(Username).
|
||||
|
||||
t_scram(_) ->
|
||||
AuthMethod = <<"SCRAM-SHA-1">>,
|
||||
[AuthMethod] = emqx_sasl:supported(),
|
||||
|
||||
Username = <<"test">>,
|
||||
Password = <<"public">>,
|
||||
Salt = <<"emqx">>,
|
||||
ok = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
ClientFirst = emqx_sasl_scram:make_client_first(Username),
|
||||
|
||||
{ok, {continue, ServerFirst, Cache}} = emqx_sasl:check(AuthMethod, ClientFirst, #{}),
|
||||
|
||||
{ok, {continue, ClientFinal, ClientCache}} = emqx_sasl:check(AuthMethod, ServerFirst, #{password => Password, client_first => ClientFirst}),
|
||||
|
||||
{ok, {ok, ServerFinal, #{}}} = emqx_sasl:check(AuthMethod, ClientFinal, Cache),
|
||||
|
||||
{ok, _} = emqx_sasl:check(AuthMethod, ServerFinal, ClientCache).
|
||||
|
||||
%t_proto(_) ->
|
||||
% process_flag(trap_exit, true),
|
||||
%
|
||||
% Username = <<"username">>,
|
||||
% Password = <<"password">>,
|
||||
% Salt = <<"emqx">>,
|
||||
% AuthMethod = <<"SCRAM-SHA-1">>,
|
||||
%
|
||||
% {ok, Client0} = emqtt:start_link([{clean_start, true},
|
||||
% {proto_ver, v5},
|
||||
% {enhanced_auth, #{method => AuthMethod,
|
||||
% params => #{username => Username,
|
||||
% password => Password,
|
||||
% salt => Salt}}},
|
||||
% {connect_timeout, 6000}]),
|
||||
% {error,{not_authorized,#{}}} = emqtt:connect(Client0),
|
||||
%
|
||||
% ok = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
% {ok, Client1} = emqtt:start_link([{clean_start, true},
|
||||
% {proto_ver, v5},
|
||||
% {enhanced_auth, #{method => AuthMethod,
|
||||
% params => #{username => Username,
|
||||
% password => Password,
|
||||
% salt => Salt}}},
|
||||
% {connect_timeout, 6000}]),
|
||||
% {ok, _} = emqtt:connect(Client1),
|
||||
%
|
||||
% timer:sleep(200),
|
||||
% ok = emqtt:reauthentication(Client1, #{params => #{username => Username,
|
||||
% password => Password,
|
||||
% salt => Salt}}),
|
||||
%
|
||||
% timer:sleep(200),
|
||||
% ErrorFun = fun (_State) -> {ok, <<>>, #{}} end,
|
||||
% ok = emqtt:reauthentication(Client1, #{params => #{},function => ErrorFun}),
|
||||
% receive
|
||||
% {disconnected,ReasonCode2,#{}} ->
|
||||
% ?assertEqual(ReasonCode2, 135)
|
||||
% after 500 ->
|
||||
% error("emqx re-authentication failed")
|
||||
% end,
|
||||
%
|
||||
% {ok, Client2} = emqtt:start_link([{clean_start, true},
|
||||
% {proto_ver, v5},
|
||||
% {enhanced_auth, #{method => AuthMethod,
|
||||
% params => #{},
|
||||
% function =>fun (_State) -> {ok, <<>>, #{}} end}},
|
||||
% {connect_timeout, 6000}]),
|
||||
% {error,{not_authorized,#{}}} = emqtt:connect(Client2),
|
||||
%
|
||||
% receive_msg(),
|
||||
% process_flag(trap_exit, false).
|
||||
|
||||
receive_msg() ->
|
||||
receive
|
||||
{'EXIT', Msg} ->
|
||||
ct:print("==========+~p~n", [Msg]),
|
||||
receive_msg()
|
||||
after 200 -> ok
|
||||
end.
|
|
@ -1,11 +1,17 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.2", [
|
||||
{load_module, emqx_sn_gateway, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<"4.3.[0-1]">>, [
|
||||
{restart_application, emqx_sn}
|
||||
]}
|
||||
],
|
||||
[
|
||||
{"4.3.2", [
|
||||
{load_module, emqx_sn_gateway, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<"4.3.[0-1]">>, [
|
||||
{restart_application, emqx_sn}
|
||||
]}
|
||||
|
|
|
@ -250,8 +250,9 @@ wait_for_will_topic(cast, {incoming, ?SN_ADVERTISE_MSG(_GwId, _Radius)}, _State)
|
|||
% ignore
|
||||
keep_state_and_data;
|
||||
|
||||
wait_for_will_topic(cast, {incoming, ?SN_CONNECT_MSG(Flags, _ProtoId, Duration, ClientId)}, State) ->
|
||||
do_2nd_connect(Flags, Duration, ClientId, State);
|
||||
wait_for_will_topic(cast, {incoming, ?SN_CONNECT_MSG(_Flags, _ProtoId, _Duration, _ClientId)}, _State) ->
|
||||
?LOG(warning, "Receive connect packet in wait_for_will_topic state", []),
|
||||
keep_state_and_data;
|
||||
|
||||
wait_for_will_topic(cast, {outgoing, Packet}, State) ->
|
||||
{keep_state, handle_outgoing(Packet, State)};
|
||||
|
@ -275,9 +276,9 @@ wait_for_will_msg(cast, {incoming, ?SN_ADVERTISE_MSG(_GwId, _Radius)}, _State) -
|
|||
% ignore
|
||||
keep_state_and_data;
|
||||
|
||||
%% XXX: ?? Why we will handling the 2nd CONNECT packet ??
|
||||
wait_for_will_msg(cast, {incoming, ?SN_CONNECT_MSG(Flags, _ProtoId, Duration, ClientId)}, State) ->
|
||||
do_2nd_connect(Flags, Duration, ClientId, State);
|
||||
wait_for_will_msg(cast, {incoming, ?SN_CONNECT_MSG(_Flags, _ProtoId, _Duration, _ClientId)}, _State) ->
|
||||
?LOG(warning, "Receive connect packet in wait_for_will_msg state", []),
|
||||
keep_state_and_data;
|
||||
|
||||
wait_for_will_msg(cast, {outgoing, Packet}, State) ->
|
||||
{keep_state, handle_outgoing(Packet, State)};
|
||||
|
@ -365,8 +366,9 @@ connected(cast, {incoming, ?SN_ADVERTISE_MSG(_GwId, _Radius)}, State) ->
|
|||
% ignore
|
||||
{keep_state, State};
|
||||
|
||||
connected(cast, {incoming, ?SN_CONNECT_MSG(Flags, _ProtoId, Duration, ClientId)}, State) ->
|
||||
do_2nd_connect(Flags, Duration, ClientId, State);
|
||||
connected(cast, {incoming, ?SN_CONNECT_MSG(_Flags, _ProtoId, _Duration, _ClientId)}, _State) ->
|
||||
?LOG(warning, "Receive connect packet in wait_for_will_topic state", []),
|
||||
keep_state_and_data;
|
||||
|
||||
connected(cast, {outgoing, Packet}, State) ->
|
||||
{keep_state, handle_outgoing(Packet, State)};
|
||||
|
@ -826,8 +828,10 @@ do_connect(ClientId, CleanStart, WillFlag, Duration, State) ->
|
|||
clean_start = CleanStart,
|
||||
username = State#state.username,
|
||||
password = State#state.password,
|
||||
proto_name = <<"MQTT-SN">>,
|
||||
keepalive = Duration,
|
||||
properties = OnlyOneInflight
|
||||
properties = OnlyOneInflight,
|
||||
proto_ver = 1
|
||||
},
|
||||
case WillFlag of
|
||||
true -> State0 = send_message(?SN_WILLTOPICREQ_MSG(), State),
|
||||
|
@ -843,26 +847,6 @@ do_connect(ClientId, CleanStart, WillFlag, Duration, State) ->
|
|||
handle_incoming(?CONNECT_PACKET(ConnPkt), NState)
|
||||
end.
|
||||
|
||||
do_2nd_connect(Flags, Duration, ClientId, State = #state{sockname = Sockname,
|
||||
peername = Peername,
|
||||
channel = Channel}) ->
|
||||
emqx_logger:set_metadata_clientid(ClientId),
|
||||
#mqtt_sn_flags{will = Will, clean_start = CleanStart} = Flags,
|
||||
NChannel = case CleanStart of
|
||||
true ->
|
||||
emqx_channel:terminate(normal, Channel),
|
||||
emqx_sn_registry:unregister_topic(ClientId),
|
||||
emqx_channel:init(#{socktype => udp,
|
||||
sockname => Sockname,
|
||||
peername => Peername,
|
||||
peercert => ?NO_PEERCERT,
|
||||
conn_mod => ?MODULE
|
||||
}, ?DEFAULT_CHAN_OPTIONS);
|
||||
false -> Channel
|
||||
end,
|
||||
NState = State#state{channel = NChannel},
|
||||
do_connect(ClientId, CleanStart, Will, Duration, NState).
|
||||
|
||||
handle_subscribe(?SN_NORMAL_TOPIC, TopicName, QoS, MsgId,
|
||||
State=#state{channel = Channel}) ->
|
||||
ClientId = emqx_channel:info(clientid, Channel),
|
||||
|
|
|
@ -98,19 +98,6 @@ t_connect(_) ->
|
|||
?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)),
|
||||
gen_udp:close(Socket).
|
||||
|
||||
t_do_2nd_connect(_) ->
|
||||
{ok, Socket} = gen_udp:open(0, [binary]),
|
||||
ClientId = ?CLIENTID,
|
||||
send_connect_msg(Socket, ClientId),
|
||||
?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)),
|
||||
timer:sleep(100),
|
||||
send_connect_msg(Socket, <<"client_id_other">>),
|
||||
?assertEqual(<<3, ?SN_CONNACK, 0>>, receive_response(Socket)),
|
||||
|
||||
send_disconnect_msg(Socket, undefined),
|
||||
?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)),
|
||||
gen_udp:close(Socket).
|
||||
|
||||
t_subscribe(_) ->
|
||||
Dup = 0,
|
||||
QoS = 0,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_web_hook,
|
||||
[{description, "EMQ X WebHook Plugin"},
|
||||
{vsn, "4.3.1"}, % strict semver, bump manually!
|
||||
{vsn, "4.3.2"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_web_hook_sup]},
|
||||
{applications, [kernel,stdlib,ehttpc]},
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_web_hook_actions, brutal_purge, soft_purge, []}
|
||||
{<<"4.3.[0-1]">>, [
|
||||
{restart_application, emqx_web_hook},
|
||||
{apply,{emqx_rule_engine,refresh_resource,[web_hook]}}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_web_hook_actions, brutal_purge, soft_purge, []}
|
||||
{<<"4.3.[0-1]">>, [
|
||||
{restart_application, emqx_web_hook},
|
||||
{apply,{emqx_rule_engine,refresh_resource,[web_hook]}}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
]
|
||||
|
|
|
@ -292,7 +292,7 @@ parse_action_params(Params = #{<<"url">> := URL}) ->
|
|||
Headers = headers(maps:get(<<"headers">>, Params, undefined)),
|
||||
NHeaders = ensure_content_type_header(Headers, Method),
|
||||
#{method => Method,
|
||||
path => path(filename:join(CommonPath, maps:get(<<"path">>, Params, <<>>))),
|
||||
path => merge_path(CommonPath, maps:get(<<"path">>, Params, <<>>)),
|
||||
headers => NHeaders,
|
||||
body => maps:get(<<"body">>, Params, <<>>),
|
||||
request_timeout => cuttlefish_duration:parse(str(maps:get(<<"request_timeout">>, Params, <<"5s">>))),
|
||||
|
@ -306,8 +306,16 @@ ensure_content_type_header(Headers, Method) when Method =:= post orelse Method =
|
|||
ensure_content_type_header(Headers, _Method) ->
|
||||
lists:keydelete("content-type", 1, Headers).
|
||||
|
||||
path(<<>>) -> <<"/">>;
|
||||
path(Path) -> Path.
|
||||
merge_path(CommonPath, <<>>) ->
|
||||
CommonPath;
|
||||
merge_path(CommonPath, Path0) ->
|
||||
case emqx_http_lib:uri_parse(Path0) of
|
||||
{ok, #{path := Path1, 'query' := Query}} ->
|
||||
Path2 = filename:join(CommonPath, Path1),
|
||||
<<Path2/binary, "?", Query/binary>>;
|
||||
{ok, #{path := Path1}} ->
|
||||
filename:join(CommonPath, Path1)
|
||||
end.
|
||||
|
||||
method(GET) when GET == <<"GET">>; GET == <<"get">> -> get;
|
||||
method(POST) when POST == <<"POST">>; POST == <<"post">> -> post;
|
||||
|
|
|
@ -42,10 +42,9 @@ stop(_State) ->
|
|||
translate_env() ->
|
||||
{ok, URL} = application:get_env(?APP, url),
|
||||
{ok, #{host := Host,
|
||||
path := Path0,
|
||||
port := Port,
|
||||
scheme := Scheme}} = emqx_http_lib:uri_parse(URL),
|
||||
Path = path(Path0),
|
||||
scheme := Scheme} = URIMap} = emqx_http_lib:uri_parse(URL),
|
||||
Path = path(URIMap),
|
||||
PoolSize = application:get_env(?APP, pool_size, 32),
|
||||
MoreOpts = case Scheme of
|
||||
http ->
|
||||
|
@ -89,9 +88,13 @@ translate_env() ->
|
|||
NHeaders = set_content_type(Headers),
|
||||
application:set_env(?APP, headers, NHeaders).
|
||||
|
||||
path("") ->
|
||||
path(#{path := "", 'query' := Query}) ->
|
||||
"?" ++ Query;
|
||||
path(#{path := Path, 'query' := Query}) ->
|
||||
Path ++ "?" ++ Query;
|
||||
path(#{path := ""}) ->
|
||||
"/";
|
||||
path(Path) ->
|
||||
path(#{path := Path}) ->
|
||||
Path.
|
||||
|
||||
set_content_type(Headers) ->
|
||||
|
|
|
@ -26,7 +26,9 @@ COPY . /emqx
|
|||
ARG PKG_VSN
|
||||
ARG EMQX_NAME=emqx
|
||||
|
||||
RUN cd /emqx && make $EMQX_NAME
|
||||
RUN cd /emqx \
|
||||
&& rm -rf _build/$EMQX_NAME/lib \
|
||||
&& make $EMQX_NAME
|
||||
|
||||
FROM $RUN_FROM
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ These environment variables will ignore for configuration file.
|
|||
|
||||
The list is incomplete and may changed with [etc/emqx.conf](https://github.com/emqx/emqx/blob/master/etc/emqx.conf) and plugin configuration files. But the mapping rule is similar.
|
||||
|
||||
If set ``EMQX_NAME`` and ``EMQX_HOST``, and unset ``EMQX_NODE__NAME``, ``EMQX_NODE__NAME=$EMQX_NAME@$EMQX_HOST``.
|
||||
If set ``EMQX_NAME`` and ``EMQX_HOST``, and unset ``EMQX_NODE_NAME``, ``EMQX_NODE_NAME=$EMQX_NAME@$EMQX_HOST``.
|
||||
|
||||
For example, set mqtt tcp port to 1883
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -42,7 +42,7 @@
|
|||
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.2"}}}
|
||||
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
|
||||
, {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} % TODO: delete when all apps moved to hocon
|
||||
, {minirest, {git, "https://github.com/emqx/minirest", {tag, "0.3.5"}}}
|
||||
, {minirest, {git, "https://github.com/emqx/minirest", {tag, "0.3.6"}}}
|
||||
, {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}}
|
||||
, {replayq, {git, "https://github.com/emqx/replayq", {tag, "0.3.2"}}}
|
||||
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}}
|
||||
|
|
|
@ -127,23 +127,30 @@ prod_compile_opts() ->
|
|||
prod_overrides() ->
|
||||
[{add, [ {erl_opts, [deterministic]}]}].
|
||||
|
||||
relup_deps(Profile) ->
|
||||
{post_hooks, [{"(linux|darwin|solaris|freebsd|netbsd|openbsd)", compile, "scripts/inject-deps.escript " ++ atom_to_list(Profile)}]}.
|
||||
|
||||
profiles() ->
|
||||
Vsn = get_vsn(),
|
||||
[ {'emqx', [ {erl_opts, prod_compile_opts()}
|
||||
, {relx, relx(Vsn, cloud, bin)}
|
||||
, {overrides, prod_overrides()}
|
||||
, relup_deps('emqx')
|
||||
]}
|
||||
, {'emqx-pkg', [ {erl_opts, prod_compile_opts()}
|
||||
, {relx, relx(Vsn, cloud, pkg)}
|
||||
, {overrides, prod_overrides()}
|
||||
, relup_deps('emqx-pkg')
|
||||
]}
|
||||
, {'emqx-edge', [ {erl_opts, prod_compile_opts()}
|
||||
, {relx, relx(Vsn, edge, bin)}
|
||||
, {overrides, prod_overrides()}
|
||||
, relup_deps('emqx-edge')
|
||||
]}
|
||||
, {'emqx-edge-pkg', [ {erl_opts, prod_compile_opts()}
|
||||
, {relx, relx(Vsn, edge, pkg)}
|
||||
, {overrides, prod_overrides()}
|
||||
, relup_deps('emqx-edge-pkg')
|
||||
]}
|
||||
, {check, [ {erl_opts, common_compile_opts()}
|
||||
]}
|
||||
|
@ -256,8 +263,7 @@ relx_apps(ReleaseType) ->
|
|||
++ [{N, load} || N <- relx_plugin_apps(ReleaseType)].
|
||||
|
||||
relx_apps_per_rel(cloud) ->
|
||||
[ luerl
|
||||
, xmerl
|
||||
[ xmerl
|
||||
| [{observer, load} || is_app(observer)]
|
||||
];
|
||||
relx_apps_per_rel(edge) ->
|
||||
|
@ -281,7 +287,6 @@ relx_plugin_apps(ReleaseType) ->
|
|||
, emqx_authentication
|
||||
, emqx_web_hook
|
||||
, emqx_rule_engine
|
||||
, emqx_sasl
|
||||
, emqx_statsd
|
||||
]
|
||||
++ relx_plugin_apps_per_rel(ReleaseType)
|
||||
|
@ -290,7 +295,6 @@ relx_plugin_apps(ReleaseType) ->
|
|||
|
||||
relx_plugin_apps_per_rel(cloud) ->
|
||||
[ emqx_lwm2m
|
||||
, emqx_lua_hook
|
||||
, emqx_exhook
|
||||
, emqx_exproto
|
||||
, emqx_prometheus
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env escript
|
||||
|
||||
%% This script injects implicit relup dependencies for emqx applications.
|
||||
%%
|
||||
%% By 'implicit', it means that it is not feasible to define application
|
||||
%% dependencies in .app.src files.
|
||||
%%
|
||||
%% For instance, during upgrade/downgrade, emqx_dashboard usually requires
|
||||
%% a restart after (but not before) all plugins are upgraded (and maybe
|
||||
%% restarted), however, the dependencies are not resolvable at build time
|
||||
%% when relup is generated.
|
||||
%%
|
||||
%% This script is to be executed after compile, with the profile given as the
|
||||
%% first argument. For each dependency, it modifies the .app file to
|
||||
%% have the `relup_deps` list extended to application attributes.
|
||||
%%
|
||||
%% The `relup_deps` application attribute is then picked up by (EMQ's fork of)
|
||||
%% `relx` when top-sorting apps to generate relup instructions
|
||||
|
||||
-mode(compile).
|
||||
|
||||
usage() ->
|
||||
"Usage: " ++ escript:script_name() ++ " emqx|emqx-edge".
|
||||
|
||||
-type app() :: atom().
|
||||
-type deps_overlay() :: {re, string()} | app().
|
||||
|
||||
%% deps/0 returns the dependency overlays.
|
||||
%% {re, Pattern} to match application names using regexp pattern
|
||||
-spec deps(string()) -> [{app(), [deps_overlay()]}].
|
||||
deps("emqx-edge" ++ _) ->
|
||||
%% special case for edge
|
||||
base_deps() ++ [{{re, ".+"}, [{exclude, App} || App <- edge_excludes()]}];
|
||||
deps(_Profile) ->
|
||||
base_deps().
|
||||
|
||||
edge_excludes() ->
|
||||
[ emqx_lwm2m
|
||||
, emqx_auth_ldap
|
||||
, emqx_auth_pgsql
|
||||
, emqx_auth_redis
|
||||
, emqx_auth_mongo
|
||||
, emqx_lua_hook
|
||||
, emqx_exhook
|
||||
, emqx_exproto
|
||||
, emqx_prometheus
|
||||
, emqx_psk_file
|
||||
].
|
||||
|
||||
base_deps() ->
|
||||
%% make sure emqx_dashboard depends on all other emqx_xxx apps
|
||||
%% so the appup instructions for emqx_dashboard is always the last
|
||||
%% to be executed
|
||||
[ {emqx_dashboard, [{re, "emqx_.*"}]}
|
||||
, {emqx_management, [{re, "emqx_.*"}, {exclude, emqx_dashboard}]}
|
||||
, {{re, "emqx_.*"}, [emqx]}
|
||||
].
|
||||
|
||||
main([Profile | _]) ->
|
||||
ok = inject(Profile);
|
||||
main(_Args) ->
|
||||
io:format(standard_error, "~s", [usage()]),
|
||||
erlang:halt(1).
|
||||
|
||||
expand_names({Name, Deps}, AppNames) ->
|
||||
Names = match_pattern(Name, AppNames),
|
||||
[{N, Deps} || N <- Names].
|
||||
|
||||
%% merge k-v pairs with v1 ++ v2
|
||||
merge([], Acc) -> Acc;
|
||||
merge([{K, V0} | Rest], Acc) ->
|
||||
V = case lists:keyfind(K, 1, Acc) of
|
||||
{K, V1} -> V1 ++ V0;
|
||||
false -> V0
|
||||
end,
|
||||
NewAcc = lists:keystore(K, 1, Acc, {K, V}),
|
||||
merge(Rest, NewAcc).
|
||||
|
||||
expand_deps([], _AppNames, Acc) -> Acc;
|
||||
expand_deps([{exclude, Dep} | Deps], AppNames, Acc) ->
|
||||
Matches = expand_deps([Dep], AppNames, []),
|
||||
expand_deps(Deps, AppNames, Acc -- Matches);
|
||||
expand_deps([Dep | Deps], AppNames, Acc) ->
|
||||
NewAcc = add_to_list(Acc, match_pattern(Dep, AppNames)),
|
||||
expand_deps(Deps, AppNames, NewAcc).
|
||||
|
||||
inject(Profile) ->
|
||||
LibDir = lib_dir(Profile),
|
||||
AppNames = list_apps(LibDir),
|
||||
Deps0 = lists:flatmap(fun(Dep) -> expand_names(Dep, AppNames) end, deps(Profile)),
|
||||
Deps1 = merge(Deps0, []),
|
||||
Deps2 = lists:map(fun({Name, DepsX}) ->
|
||||
NewDeps = expand_deps(DepsX, AppNames, []),
|
||||
{Name, NewDeps}
|
||||
end, Deps1),
|
||||
lists:foreach(fun({App, Deps}) -> inject(App, Deps, LibDir) end, Deps2).
|
||||
|
||||
%% list the profile/lib dir to get all apps
|
||||
list_apps(LibDir) ->
|
||||
Apps = filelib:wildcard("*", LibDir),
|
||||
lists:foldl(fun(App, Acc) -> [App || is_app(LibDir, App)] ++ Acc end, [], Apps).
|
||||
|
||||
is_app(_LibDir, "." ++ _) -> false; %% ignore hidden dir
|
||||
is_app(LibDir, AppName) ->
|
||||
Path = filename:join([ebin_dir(LibDir, AppName), AppName ++ ".app"]),
|
||||
filelib:is_regular(Path) orelse error({unknown_app, AppName, Path}). %% wtf
|
||||
|
||||
lib_dir(Profile) ->
|
||||
filename:join(["_build", Profile, lib]).
|
||||
|
||||
ebin_dir(LibDir, AppName) -> filename:join([LibDir, AppName, "ebin"]).
|
||||
|
||||
inject(App0, DepsToAdd, LibDir) ->
|
||||
App = str(App0),
|
||||
AppEbinDir = ebin_dir(LibDir, App),
|
||||
[AppFile0] = filelib:wildcard("*.app", AppEbinDir),
|
||||
AppFile = filename:join(AppEbinDir, AppFile0),
|
||||
{ok, [{application, AppName, Props}]} = file:consult(AppFile),
|
||||
Deps0 = case lists:keyfind(relup_deps, 1, Props) of
|
||||
{_, X} -> X;
|
||||
false -> []
|
||||
end,
|
||||
%% merge extra deps, but do not self-include
|
||||
Deps = add_to_list(Deps0, DepsToAdd) -- [App0],
|
||||
case Deps =:= [] of
|
||||
true -> ok;
|
||||
_ ->
|
||||
NewProps = lists:keystore(relup_deps, 1, Props, {relup_deps, Deps}),
|
||||
AppSpec = {application, AppName, NewProps},
|
||||
AppSpecIoData = io_lib:format("~p.", [AppSpec]),
|
||||
io:format(user, "updated_relup_deps for ~p~n", [App]),
|
||||
file:write_file(AppFile, AppSpecIoData)
|
||||
end.
|
||||
|
||||
str(A) when is_atom(A) -> atom_to_list(A).
|
||||
|
||||
match_pattern({re, Re}, AppNames) ->
|
||||
Match = fun(AppName) -> re:run(AppName, Re) =/= nomatch end,
|
||||
AppNamesToAdd = lists:filter(Match, AppNames),
|
||||
AppsToAdd = lists:map(fun(N) -> list_to_atom(N) end, AppNamesToAdd),
|
||||
case AppsToAdd =:= [] of
|
||||
true -> error({nomatch, Re});
|
||||
false -> AppsToAdd
|
||||
end;
|
||||
match_pattern(NameAtom, AppNames) ->
|
||||
case lists:member(str(NameAtom), AppNames) of
|
||||
true -> [NameAtom];
|
||||
false -> error({notfound, NameAtom})
|
||||
end.
|
||||
|
||||
%% Append elements to list without duplication. No reordering.
|
||||
add_to_list(List, []) -> List;
|
||||
add_to_list(List, [H | T]) ->
|
||||
case lists:member(H, List) of
|
||||
true -> add_to_list(List, T);
|
||||
false -> add_to_list(List ++ [H], T)
|
||||
end.
|
Loading…
Reference in New Issue