Merge branch 'master' into EMQX-788

This commit is contained in:
x1001100011 2021-06-27 23:20:47 -07:00
commit ed34783dd7
70 changed files with 3273 additions and 2974 deletions

View File

@ -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, [

View File

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

View File

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

View File

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

View File

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

View File

@ -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">>}]).

View File

@ -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,[]},

View File

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

View File

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

View File

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

View File

@ -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, []}}
]},
{<<".*">>, []}
]
}.

View File

@ -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, []}},

View File

@ -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, []}
]},
{<<".*">>, []}
]

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
##--------------------------------------------------------------------
## EMQ X Lua Hook
##--------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

@ -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"}
]}
]}.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]},

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@
-export([ mqtt2coap/2
, coap2mqtt/4
, ack2mqtt/1
, extract_path/1
]).
-export([path_list/1]).

View File

@ -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]),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]},

View File

@ -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]}}
]},
{<<".*">>, []}
]

View File

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

View File

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

View File

@ -1,2 +0,0 @@
# emqx-sasl
Simple Authentication and Security Layer

View File

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

View File

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

View File

@ -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"}
]}
]}.

View File

@ -1,13 +0,0 @@
%% -*-: erlang -*-
{VSN,
[
{"4.3.0", [
{restart_application, emqx_sasl}
]}
],
[
{"4.3.0", [
{restart_application, emqx_sasl}
]}
]
}.

View File

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

View File

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

View File

@ -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}, []} }.

View File

@ -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"}]).

View File

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

View File

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

View File

@ -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}
]}

View File

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

View File

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

View File

@ -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]},

View File

@ -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]}}
]},
{<<".*">>, []}
]

View File

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

View File

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

View File

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

View File

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

2470
priv/emqx.schema Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

157
scripts/inject-deps.escript Executable file
View File

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