chore: merge coap/lwm2m/exhook/exproto to emqx_gateway dir

This commit is contained in:
Turtle 2021-07-16 23:08:11 +08:00 committed by turtleDeng
parent 69f06b3631
commit ed1cf33b9d
115 changed files with 5107 additions and 6425 deletions

View File

@ -1,25 +0,0 @@
deps/
ebin/
_rel/
.erlang.mk/
*.d
*.o
*.exe
data/
*.iml
.idea/
logs/
*.beam
emqx_coap.d
intergration_test/emqx-rel/
intergration_test/libcoap/
intergration_test/case*.txt
.DS_Store
_build/
rebar.lock
rebar3.crashdump
*.swp
erlang.mk
.rebar3/
etc/emqx_coap.conf.rendered
.tags*

View File

@ -1,13 +0,0 @@
1. Remove the test/test_mqtt_broker and use emqx-ct-helpers -> Done!
- Enhance all test case
2. Remove the mqtt adaptor
3. Remove the emqx_coap_pubsub_topics.erl
### Problems
1. The coap-client of libcoap does not support Fragment DTLS handshake frame
* So, the connection will be established failed, if the 'Server Hello' frame is too big
* Why is the 'Server Hello' too big when enable the 'coap.dtls.cacertfile' option?
2.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,129 +0,0 @@
.PHONY: clean, clean_result, start_broker stop_broker case1 case2 case3
RELX_CONF = emqx-rel/relx.config
LIBCOAP_GIT = libcoap/README.md
all: clean_result $(RELX_CONF) $(LIBCOAP_GIT) start_broker clean_result case1 case2 case3 case4 stop_broker
@echo " "
@echo " test complete"
@echo " "
clean_result:
-rm -f case*.txt
start_broker:
-rm -f emqx-rel/_rel/emqx/log/*
-emqx-rel/_rel/emqx/bin/emqx stop
sleep 1
emqx-rel/_rel/emqx/bin/emqx start
sleep 1
emqx-rel/_rel/emqx/bin/emqx_ctl plugins load emqx_coap
stop_broker:
-emqx-rel/_rel/emqx/bin/emqx stop
case1:
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret" > case1_output.txt &
sleep 1
libcoap/examples/coap-client -m put -e w123G45 "coap://127.0.0.1/mqtt/topic1?c=client2&u=mike&p=pw12"
sleep 6
python check_result.py case1 case1_output.txt==w123G45
case2:
# subscribe to topic="x/y"
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/x%2Fy?c=client3&u=tom&p=secret" > case2_output1.txt &
# subscribe to topic="+/z"
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/%2B%2Fz?c=client4&u=mike&p=pw12" > case2_output2.txt &
sleep 1
# publish to topic="x/y"
libcoap/examples/coap-client -m put -e big9wolf "coap://127.0.0.1/mqtt/x%2Fy?c=client5&u=sun&p=pw3"
# publish to topic="p/z"
libcoap/examples/coap-client -m put -e black2ant "coap://127.0.0.1/mqtt/p%2Fz?c=client5&u=sun&p=pw3"
sleep 6
python check_result.py case2 case2_output1.txt==big9wolf case2_output1.txt!=black2ant case2_output2.txt!=big9wolf case2_output2.txt==black2ant
case3:
libcoap/examples/coap-client -m get -T tk12 -s 5 "coap://127.0.0.1/mqtt/a%2Fb?c=client3&u=tom&p=secret" > case3_output1.txt &
libcoap/examples/coap-client -m get -T tk34 -s 5 "coap://127.0.0.1/mqtt/c%2Fd?c=client3&u=tom&p=secret" > case3_output2.txt &
sleep 1
libcoap/examples/coap-client -m put -e big9wolf "coap://127.0.0.1/mqtt/c%2Fd?c=client5&u=sun&p=pw3"
libcoap/examples/coap-client -m put -e black2ant "coap://127.0.0.1/mqtt/a%2Fb?c=client5&u=sun&p=pw3"
sleep 6
python check_result.py case3 case3_output1.txt==black2ant case3_output2.txt==big9wolf case3_output2.txt!=black2ant
case4:
# reload emqx_coap, does it work as expected?
sleep 1
emqx-rel/_rel/emqx/bin/emqx_ctl plugins unload emqx_coap
sleep 1
emqx-rel/_rel/emqx/bin/emqx_ctl plugins load emqx_coap
sleep 1
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret" > case4_output.txt &
sleep 1
libcoap/examples/coap-client -m put -e w6J3G45 "coap://127.0.0.1/mqtt/topic1?c=client2&u=mike&p=pw12"
sleep 6
python check_result.py case4 case4_output.txt==w6J3G45
$(RELX_CONF):
git clone https://github.com/emqx/emqx-rel.git
git clone https://github.com/emqx/emq-coap.git
@echo "update emq-coap with this development code"
mv emq-coap emqx_coap
-rm -rf emqx_coap/etc
-rm -rf emqx_coap/include
-rm -rf emqx_coap/priv
-rm -rf emqx_coap/src
-rm -rf emqx_coap/Makefile
cp -rf ../etc emqx_coap/
cp -rf ../include emqx_coap/
cp -rf ../priv emqx_coap/
cp -rf ../src emqx_coap/
cp -rf ../Makefile emqx_coap/Makefile
-mkdir emqx-rel/deps
mv emqx_coap emqx-rel/deps/
@echo "start building ..."
make -C emqx-rel -f Makefile
coap: $(LIBCOAP_GIT)
@echo "make coap"
$(LIBCOAP_GIT):
git clone -b v4.1.2 http://github.com/obgm/libcoap
cd libcoap && ./autogen.sh && ./configure --enable-documentation=no --enable-tests=no
make -C libcoap -f Makefile
r: rebuild_emq
# r short for rebuild_emq
@echo " rebuild complete "
rebuild_emq:
-emqx-rel/_rel/emqx/bin/emqx stop
-rm -rf emqx-rel/deps/emqx_coap/etc
-rm -rf emqx-rel/deps/emqx_coap/include
-rm -rf emqx-rel/deps/emqx_coap/priv
-rm -rf emqx-rel/deps/emqx_coap/src
-rm -rf emqx-rel/deps/emqx_coap/Makefile
cp -rf ../etc emqx-rel/deps/emqx_coap/
cp -rf ../include emqx-rel/deps/emqx_coap/
cp -rf ../priv emqx-rel/deps/emqx_coap/
cp -rf ../src emqx-rel/deps/emqx_coap/
cp -rf ../Makefile emqx-rel/deps/emqx_coap/Makefile
make -C emqx-rel -f Makefile
clean: clean_result
-rm -f client/*.exe
-rm -f client/*.o
-rm -rf emqx-rel
-rm -rf libcoap
lazy: clean_result start_broker case2 stop_broker
# custom your command here
@echo "you are so lazy"

View File

@ -1,8 +0,0 @@
Integration test for emq-coap
======
execute following command
```
make
```

View File

@ -1,52 +0,0 @@
import sys
def have_string(filename, text):
data = open(filename, "rb").read()
if data.find(text) > 0:
return True
else:
return False
def mark(case_number, result, description):
if result:
f = open(case_number+"_PASS.txt", "wb")
f.close()
print("\n\n"+case_number+" PASS\n\n")
else:
f = open(case_number+"_FAIL.txt", "wb")
f.write(description)
f.close()
print("\n\n"+case_number+" FAIL\n\n")
def parse_condition(condition):
if condition.find("==") > 0:
r = condition.split("==")
return r[0], r[1], True
elif condition.find("!=") > 0:
r = condition.split("!=")
return r[0], r[1], False
else:
print("\ncondition syntax error\n\n\n")
sys.exit("condition syntax error")
def main():
case_number = sys.argv[1]
description = ""
conclustion = True
for condition in sys.argv[2:]:
filename, text, result = parse_condition(condition)
if have_string(filename, text) == result:
pass
else:
conclustion = False
description = description + "\n" + condition + " failed\n"
mark(case_number, conclustion, description)
if __name__ == "__main__":
main()

View File

@ -1,4 +0,0 @@
{deps,
[
{gen_coap, {git, "https://github.com/emqx/gen_coap", {tag, "v0.3.2"}}}
]}.

View File

@ -1,319 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_coap_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("gen_coap/include/coap.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("emqx/include/emqx.hrl").
-define(LOGT(Format, Args), ct:pal(Format, Args)).
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_coap], fun set_special_cfg/1),
Config.
set_special_cfg(emqx_coap) ->
Opts = application:get_env(emqx_coap, dtls_opts,[]),
Opts2 = [{keyfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/key.pem")},
{certfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/cert.pem")}],
application:set_env(emqx_coap, dtls_opts, emqx_misc:merge_opts(Opts, Opts2)),
application:set_env(emqx_coap, enable_stats, true);
set_special_cfg(_) ->
ok.
end_per_suite(Config) ->
emqx_ct_helpers:stop_apps([emqx_coap]),
Config.
%%--------------------------------------------------------------------
%% Test Cases
%%--------------------------------------------------------------------
t_publish(_Config) ->
Topic = <<"abc">>, Payload = <<"123">>,
TopicStr = binary_to_list(Topic),
URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
%% Sub topic first
emqx:subscribe(Topic),
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
{ok, changed, _} = Reply,
receive
{deliver, Topic, Msg} ->
?assertEqual(Topic, Msg#message.topic),
?assertEqual(Payload, Msg#message.payload)
after
500 ->
?assert(false)
end.
t_publish_acl_deny(_Config) ->
Topic = <<"abc">>, Payload = <<"123">>,
TopicStr = binary_to_list(Topic),
URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
%% Sub topic first
emqx:subscribe(Topic),
ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]),
ok = meck:expect(emqx_access_control, authorize, 3, deny),
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?assertEqual({error,forbidden}, Reply),
ok = meck:unload(emqx_access_control),
receive
{deliver, Topic, Msg} -> ct:fail({unexpected, {Topic, Msg}})
after
500 -> ok
end.
t_observe(_Config) ->
Topic = <<"abc">>, TopicStr = binary_to_list(Topic),
Payload = <<"123">>,
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
[SubPid] = emqx:subscribers(Topic),
?assert(is_pid(SubPid)),
%% Publish a message
emqx:publish(emqx_message:make(Topic, Payload)),
Notif = receive_notification(),
?LOGT("observer get Notif=~p", [Notif]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
?assertEqual(Payload, PayloadRecv),
er_coap_observer:stop(Pid),
timer:sleep(100),
[] = emqx:subscribers(Topic).
t_observe_acl_deny(_Config) ->
Topic = <<"abc">>, TopicStr = binary_to_list(Topic),
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]),
ok = meck:expect(emqx_access_control, authorize, 3, deny),
?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)),
[] = emqx:subscribers(Topic),
ok = meck:unload(emqx_access_control).
t_observe_wildcard(_Config) ->
Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)),
Payload = <<"123">>,
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
?LOGT("observer Uri=~p, Pid=~p, N=~p, Code=~p, Content=~p", [Uri, Pid, N, Code, Content]),
[SubPid] = emqx:subscribers(Topic),
?assert(is_pid(SubPid)),
%% Publish a message
emqx:publish(emqx_message:make(<<"a/b">>, Payload)),
Notif = receive_notification(),
?LOGT("observer get Notif=~p", [Notif]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
?assertEqual(Payload, PayloadRecv),
er_coap_observer:stop(Pid),
timer:sleep(100),
[] = emqx:subscribers(Topic).
t_observe_pub(_Config) ->
Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)),
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
[SubPid] = emqx:subscribers(Topic),
?assert(is_pid(SubPid)),
Topic2 = <<"a/b">>, Payload2 = <<"UFO">>,
TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)),
URI2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret",
Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = Payload2}),
{ok,changed, _} = Reply2,
Notif2 = receive_notification(),
?LOGT("observer get Notif2=~p", [Notif2]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2,
?assertEqual(Payload2, PayloadRecv2),
Topic3 = <<"j/b">>, Payload3 = <<"ET629">>,
TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)),
URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=mike&p=guess",
Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
{ok,changed, _} = Reply3,
Notif3 = receive_notification(),
?LOGT("observer get Notif3=~p", [Notif3]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv3}} = Notif3,
?assertEqual(Payload3, PayloadRecv3),
er_coap_observer:stop(Pid).
t_one_clientid_sub_2_topics(_Config) ->
Topic1 = <<"abc">>, TopicStr1 = binary_to_list(Topic1),
Payload1 = <<"123">>,
Uri1 = "coap://127.0.0.1/mqtt/"++TopicStr1++"?c=client1&u=tom&p=secret",
{ok, Pid1, N1, Code1, Content1} = er_coap_observer:observe(Uri1),
?LOGT("observer 1 Pid=~p, N=~p, Code=~p, Content=~p", [Pid1, N1, Code1, Content1]),
[SubPid] = emqx:subscribers(Topic1),
?assert(is_pid(SubPid)),
Topic2 = <<"x/y">>, TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)),
Payload2 = <<"456">>,
Uri2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret",
{ok, Pid2, N2, Code2, Content2} = er_coap_observer:observe(Uri2),
?LOGT("observer 2 Pid=~p, N=~p, Code=~p, Content=~p", [Pid2, N2, Code2, Content2]),
[SubPid] = emqx:subscribers(Topic2),
?assert(is_pid(SubPid)),
emqx:publish(emqx_message:make(Topic1, Payload1)),
Notif1 = receive_notification(),
?LOGT("observer 1 get Notif=~p", [Notif1]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv1}} = Notif1,
?assertEqual(Payload1, PayloadRecv1),
emqx:publish(emqx_message:make(Topic2, Payload2)),
Notif2 = receive_notification(),
?LOGT("observer 2 get Notif=~p", [Notif2]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2,
?assertEqual(Payload2, PayloadRecv2),
er_coap_observer:stop(Pid1),
er_coap_observer:stop(Pid2).
t_invalid_parameter(_Config) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% "cid=client2" is invaid
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Topic3 = <<"a/b">>, Payload3 = <<"ET629">>,
TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)),
URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?cid=client2&u=tom&p=simple",
Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
?assertMatch({error,bad_request}, Reply3),
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% "what=hello" is invaid
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
URI4 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?what=hello",
Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
?assertMatch({error, bad_request}, Reply4).
t_invalid_topic(_Config) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% "a/b" is a valid topic string
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Topic3 = <<"a/b">>, Payload3 = <<"ET629">>,
TopicStr3 = binary_to_list(Topic3),
URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=tom&p=simple",
Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
?assertMatch({ok,changed,_Content}, Reply3),
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% "+?#" is invaid topic string
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
URI4 = "coap://127.0.0.1/mqtt/"++"+?#"++"?what=hello",
Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
?assertMatch({error,bad_request}, Reply4).
% mqtt connection kicked by coap with same client id
t_kick_1(_Config) ->
URI = "coap://127.0.0.1/mqtt/abc?c=clientid&u=tom&p=secret",
% workaround: emqx:subscribe does not kick same client id.
spawn_monitor(fun() ->
{ok, C} = emqtt:start_link([{host, "localhost"},
{clientid, <<"clientid">>},
{username, <<"plain">>},
{password, <<"plain">>}]),
{ok, _} = emqtt:connect(C) end),
er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>,
payload = <<"123">>}),
receive
{'DOWN', _, _, _, _} -> ok
after 2000 ->
?assert(false)
end.
% mqtt connection kicked by coap with same client id
t_acl(Config) ->
OldPath = emqx:get_env(plugins_etc_dir),
application:set_env(emqx, plugins_etc_dir,
emqx_ct_helpers:deps_path(emqx_authz, "test")),
Conf = #{<<"authz">> =>
#{<<"rules">> =>
[#{<<"principal">> =>#{<<"username">> => <<"coap">>},
<<"permission">> => deny,
<<"topics">> => [<<"abc">>],
<<"action">> => <<"publish">>}
]}},
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
application:ensure_all_started(emqx_authz),
emqx:subscribe(<<"abc">>),
URI = "coap://127.0.0.1/mqtt/adbc?c=client1&u=coap&p=secret",
er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>,
payload = <<"123">>}),
receive
_Something -> ?assert(false)
after 2000 ->
ok
end,
ok = emqx_hooks:del('client.authorize', {emqx_authz, authorize}),
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
application:set_env(emqx, plugins_etc_dir, OldPath),
application:stop(emqx_authz).
t_stats(_) ->
ok.
t_auth_failure(_) ->
ok.
t_qos_supprot(_) ->
ok.
%%--------------------------------------------------------------------
%% Helpers
receive_notification() ->
receive
{coap_notify, Pid, N2, Code2, Content2} ->
{coap_notify, Pid, N2, Code2, Content2}
after 2000 ->
receive_notification_timeout
end.
testdir(DataPath) ->
Ls = filename:split(DataPath),
filename:join(lists:sublist(Ls, 1, length(Ls) - 1)).

View File

@ -1,677 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_coap_pubsub_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("gen_coap/include/coap.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("emqx/include/emqx.hrl").
-define(LOGT(Format, Args), ct:pal(Format, Args)).
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_coap], fun set_special_cfg/1),
Config.
set_special_cfg(emqx_coap) ->
application:set_env(emqx_coap, enable_stats, true);
set_special_cfg(_) ->
ok.
end_per_suite(Config) ->
emqx_ct_helpers:stop_apps([emqx_coap]),
Config.
%%--------------------------------------------------------------------
%% Test Cases
%%--------------------------------------------------------------------
t_update_max_age(_Config) ->
TopicInPayload = <<"topic1">>,
Payload = <<"<topic1>;ct=42">>,
Payload1 = <<"<topic1>;ct=50">>,
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
URI2 = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT1),
timer:sleep(50),
%% post to create the same topic but with different max age and ct value in payload
Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 70, format = <<"application/link-format">>, payload = Payload1}),
{ok,created, #coap_content{location_path = LocPath}} = Reply1,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{TopicInPayload, MaxAge2, CT2, _ResPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
?assertEqual(70, MaxAge2),
?assertEqual(<<"50">>, CT2),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
t_create_subtopic(_Config) ->
TopicInPayload = <<"topic1">>,
TopicInPayloadStr = "topic1",
Payload = <<"<topic1>;ct=42">>,
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT1),
timer:sleep(50),
%% post to create the a sub topic
SubPayload = <<"<subtopic>;ct=42">>,
SubTopicInPayloadStr = "subtopic",
SubURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"?c=client1&u=tom&p=secret",
SubRealURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"/"++SubTopicInPayloadStr++"?c=client1&u=tom&p=secret",
FullTopic = list_to_binary(TopicInPayloadStr++"/"++SubTopicInPayloadStr),
Reply1 = er_coap_client:request(post, SubURI, #coap_content{format = <<"application/link-format">>, payload = SubPayload}),
?LOGT("Reply =~p", [Reply1]),
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
?assertEqual([<<"/ps/topic1/subtopic">>] ,LocPath1),
[{FullTopic, MaxAge2, CT2, _ResPayload, _}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
?assertEqual(60, MaxAge2),
?assertEqual(<<"42">>, CT2),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, SubRealURI),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI).
t_over_max_age(_Config) ->
TopicInPayload = <<"topic1">>,
Payload = <<"<topic1>;ct=42">>,
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 2, format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(2, MaxAge1),
?assertEqual(<<"42">>, CT1),
timer:sleep(3000),
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)).
t_refreash_max_age(_Config) ->
TopicInPayload = <<"topic1">>,
Payload = <<"<topic1>;ct=42">>,
Payload1 = <<"<topic1>;ct=50">>,
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
?LOGT("lookup topic info=~p", [TopicInfo]),
?LOGT("TimeStamp=~p", [TimeStamp]),
?assertEqual(5, MaxAge1),
?assertEqual(<<"42">>, CT1),
timer:sleep(3000),
%% post to create the same topic, the max age timer will be restarted with the new max age value
Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload1}),
{ok,created, #coap_content{location_path = LocPath}} = Reply1,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{TopicInPayload, MaxAge2, CT2, _ResPayload, TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
?LOGT("TimeStamp1=~p", [TimeStamp1]),
?assertEqual(5, MaxAge2),
?assertEqual(<<"50">>, CT2),
timer:sleep(3000),
?assertEqual(false, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI).
t_case01_publish_post(_Config) ->
timer:sleep(100),
MainTopic = <<"maintopic">>,
TopicInPayload = <<"topic1">>,
Payload = <<"<topic1>;ct=42">>,
MainTopicStr = binary_to_list(MainTopic),
%% post to create topic maintopic/topic1
URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret",
FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)),
Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply1]),
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1),
[{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
?assertEqual(60, MaxAge),
?assertEqual(<<"42">>, CT2),
%% post to publish message to topic maintopic/topic1
FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)),
URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret",
PubPayload = <<"PUBLISH">>,
%% Sub topic first
emqx:subscribe(FullTopic),
Reply2 = er_coap_client:request(post, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}),
?LOGT("Reply =~p", [Reply2]),
{ok,changed, _} = Reply2,
TopicInfo = [{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
?LOGT("the topic info =~p", [TopicInfo]),
assert_recv(FullTopic, PubPayload),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
t_case02_publish_post(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"payload">>,
%% Sub topic first
emqx:subscribe(Topic),
%% post to publish a new topic "topic1", and the topic is created
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(60, MaxAge),
?assertEqual(<<"42">>, CT),
assert_recv(Topic, Payload),
%% post to publish a new message to the same topic "topic1" with different payload
NewPayload = <<"newpayload">>,
Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}),
?LOGT("Reply =~p", [Reply1]),
{ok,changed, _} = Reply1,
[{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
assert_recv(Topic, NewPayload),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case03_publish_post(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"payload">>,
%% Sub topic first
emqx:subscribe(Topic),
%% post to publish a new topic "topic1", and the topic is created
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(60, MaxAge),
?assertEqual(<<"42">>, CT),
assert_recv(Topic, Payload),
%% post to publish a new message to the same topic "topic1", but the ct is not same as created
NewPayload = <<"newpayload">>,
Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}),
?LOGT("Reply =~p", [Reply1]),
?assertEqual({error,bad_request}, Reply1),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case04_publish_post(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"payload">>,
%% post to publish a new topic "topic1", and the topic is created
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(5, MaxAge),
?assertEqual(<<"42">>, CT),
%% after max age timeout, the topic still exists but the status is timeout
timer:sleep(6000),
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case01_publish_put(_Config) ->
MainTopic = <<"maintopic">>,
TopicInPayload = <<"topic1">>,
Payload = <<"<topic1>;ct=42">>,
MainTopicStr = binary_to_list(MainTopic),
%% post to create topic maintopic/topic1
URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret",
FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)),
Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply1]),
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1),
[{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
?assertEqual(60, MaxAge),
?assertEqual(<<"42">>, CT2),
%% put to publish message to topic maintopic/topic1
FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)),
URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret",
PubPayload = <<"PUBLISH">>,
%% Sub topic first
emqx:subscribe(FullTopic),
Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}),
?LOGT("Reply =~p", [Reply2]),
{ok,changed, _} = Reply2,
[{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
assert_recv(FullTopic, PubPayload),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
t_case02_publish_put(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"payload">>,
%% Sub topic first
emqx:subscribe(Topic),
%% put to publish a new topic "topic1", and the topic is created
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(60, MaxAge),
?assertEqual(<<"42">>, CT),
assert_recv(Topic, Payload),
%% put to publish a new message to the same topic "topic1" with different payload
NewPayload = <<"newpayload">>,
Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}),
?LOGT("Reply =~p", [Reply1]),
{ok,changed, _} = Reply1,
[{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
assert_recv(Topic, NewPayload),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case03_publish_put(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"payload">>,
%% Sub topic first
emqx:subscribe(Topic),
%% put to publish a new topic "topic1", and the topic is created
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(60, MaxAge),
?assertEqual(<<"42">>, CT),
assert_recv(Topic, Payload),
%% put to publish a new message to the same topic "topic1", but the ct is not same as created
NewPayload = <<"newpayload">>,
Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}),
?LOGT("Reply =~p", [Reply1]),
?assertEqual({error,bad_request}, Reply1),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case04_publish_put(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"payload">>,
%% put to publish a new topic "topic1", and the topic is created
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(put, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/topic1">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(5, MaxAge),
?assertEqual(<<"42">>, CT),
%% after max age timeout, no publish message to the same topic, the topic info will be deleted
%%%%%%%%%%%%%%%%%%%%%%%%%%
% but there is one thing to do is we don't count in the publish message received from emqx(from other node).TBD!!!!!!!!!!!!!
%%%%%%%%%%%%%%%%%%%%%%%%%%
timer:sleep(6000),
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case01_subscribe(_Config) ->
Topic = <<"topic1">>,
Payload1 = <<"<topic1>;ct=42">>,
timer:sleep(100),
%% First post to create a topic "topic1"
Uri = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
?assertEqual(<<"/ps/topic1">> ,LocPath),
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT1),
%% Subscribe the topic
Uri1 = "coap://127.0.0.1"++binary_to_list(LocPath)++"?c=client1&u=tom&p=secret",
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri1),
?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
[SubPid] = emqx:subscribers(Topic),
?assert(is_pid(SubPid)),
%% Publish a message
Payload = <<"123">>,
emqx:publish(emqx_message:make(Topic, Payload)),
Notif = receive_notification(),
?LOGT("observer get Notif=~p", [Notif]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
?assertEqual(Payload, PayloadRecv),
%% GET to read the publish message of the topic
Reply1 = er_coap_client:request(get, Uri1),
?LOGT("Reply=~p", [Reply1]),
{ok,content, #coap_content{payload = <<"123">>}} = Reply1,
er_coap_observer:stop(Pid),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri1).
t_case02_subscribe(_Config) ->
Topic = <<"a/b">>,
TopicStr = binary_to_list(Topic),
PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
Payload = <<"payload">>,
%% post to publish a new topic "a/b", and the topic is created
URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/a/b">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(5, MaxAge),
?assertEqual(<<"42">>, CT),
%% Wait for the max age of the timer expires
timer:sleep(6000),
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
%% Subscribe to the timeout topic "a/b", still successfullygot {ok, nocontent} Method
Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
Reply1 = {ok, Pid, _N, nocontent, _} = er_coap_observer:observe(Uri),
?LOGT("Subscribe Reply=~p", [Reply1]),
[SubPid] = emqx:subscribers(Topic),
?assert(is_pid(SubPid)),
%% put to publish to topic "a/b"
Reply2 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
{ok,changed, #coap_content{}} = Reply2,
[{Topic, MaxAge1, CT, Payload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT),
?assertEqual(false, TimeStamp =:= timeout),
%% Publish a message
emqx:publish(emqx_message:make(Topic, Payload)),
Notif = receive_notification(),
?LOGT("observer get Notif=~p", [Notif]),
{coap_notify, _, _, {ok,content}, #coap_content{payload = Payload}} = Notif,
er_coap_observer:stop(Pid),
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case03_subscribe(_Config) ->
%% Subscribe to the unexisted topic "a/b", got not_found
Topic = <<"a/b">>,
TopicStr = binary_to_list(Topic),
PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
{error, not_found} = er_coap_observer:observe(Uri),
[] = emqx:subscribers(Topic).
t_case04_subscribe(_Config) ->
%% Subscribe to the wildcad topic "+/b", got bad_request
Topic = <<"+/b">>,
TopicStr = binary_to_list(Topic),
PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
{error, bad_request} = er_coap_observer:observe(Uri),
[] = emqx:subscribers(Topic).
t_case01_read(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"PubPayload">>,
timer:sleep(100),
%% First post to create a topic "topic1"
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
?assertEqual(<<"/ps/topic1">> ,LocPath),
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT1),
%% GET to read the publish message of the topic
timer:sleep(1000),
Reply1 = er_coap_client:request(get, Uri),
?LOGT("Reply=~p", [Reply1]),
{ok,content, #coap_content{payload = Payload}} = Reply1,
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
t_case02_read(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"PubPayload">>,
timer:sleep(100),
%% First post to publish a topic "topic1"
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
?assertEqual(<<"/ps/topic1">> ,LocPath),
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT1),
%% GET to read the publish message of unmatched format, got bad_request
Reply1 = er_coap_client:request(get, Uri, #coap_content{format = <<"application/json">>}),
?LOGT("Reply=~p", [Reply1]),
{error, bad_request} = Reply1,
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
t_case03_read(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
timer:sleep(100),
%% GET to read the nexisted topic "topic1", got not_found
Reply = er_coap_client:request(get, Uri),
?LOGT("Reply=~p", [Reply]),
{error, not_found} = Reply.
t_case04_read(_Config) ->
Topic = <<"topic1">>,
TopicStr = binary_to_list(Topic),
Payload = <<"PubPayload">>,
timer:sleep(100),
%% First post to publish a topic "topic1"
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
?assertEqual(<<"/ps/topic1">> ,LocPath),
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?LOGT("lookup topic info=~p", [TopicInfo]),
?assertEqual(60, MaxAge1),
?assertEqual(<<"42">>, CT1),
%% GET to read the publish message of wildcard topic, got bad_request
WildTopic = binary_to_list(<<"+/topic1">>),
Uri1 = "coap://127.0.0.1/ps/"++WildTopic++"?c=client1&u=tom&p=secret",
Reply1 = er_coap_client:request(get, Uri1, #coap_content{format = <<"application/json">>}),
?LOGT("Reply=~p", [Reply1]),
{error, bad_request} = Reply1,
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
t_case05_read(_Config) ->
Topic = <<"a/b">>,
TopicStr = binary_to_list(Topic),
PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
Payload = <<"payload">>,
%% post to publish a new topic "a/b", and the topic is created
URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/a/b">>] ,LocPath),
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
?assertEqual(5, MaxAge),
?assertEqual(<<"42">>, CT),
%% Wait for the max age of the timer expires
timer:sleep(6000),
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
%% GET to read the expired publish message, supposed to get {ok, nocontent}, but now got {ok, content}
Reply1 = er_coap_client:request(get, URI),
?LOGT("Reply=~p", [Reply1]),
{ok, content, #coap_content{payload = <<>>}}= Reply1,
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
t_case01_delete(_Config) ->
TopicInPayload = <<"a/b">>,
TopicStr = binary_to_list(TopicInPayload),
PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"),
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
%% Client post to CREATE topic "a/b"
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
?LOGT("Reply =~p", [Reply]),
{ok,created, #coap_content{location_path = LocPath}} = Reply,
?assertEqual([<<"/ps/a/b">>] ,LocPath),
%% Client post to CREATE topic "a/b/c"
TopicInPayload1 = <<"a/b/c">>,
PercentEncodedTopic1 = emqx_http_lib:uri_encode(binary_to_list(TopicInPayload1)),
Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"),
Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
?LOGT("Reply =~p", [Reply1]),
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
?assertEqual([<<"/ps/a/b/c">>] ,LocPath1),
timer:sleep(50),
%% DELETE the topic "a/b"
UriD = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
ReplyD = er_coap_client:request(delete, UriD),
?LOGT("Reply=~p", [ReplyD]),
{ok, deleted, #coap_content{}}= ReplyD,
timer:sleep(300), %% Waiting gen_server:cast/2 for deleting operation
?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload)),
?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload1)).
t_case02_delete(_Config) ->
TopicInPayload = <<"a/b">>,
TopicStr = binary_to_list(TopicInPayload),
PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
%% DELETE the unexisted topic "a/b"
Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
Reply1 = er_coap_client:request(delete, Uri1),
?LOGT("Reply=~p", [Reply1]),
{error, not_found} = Reply1.
t_case13_emit_stats_test(_Config) ->
ok.
%%--------------------------------------------------------------------
%% Internal functions
receive_notification() ->
receive
{coap_notify, Pid, N2, Code2, Content2} ->
{coap_notify, Pid, N2, Code2, Content2}
after 2000 ->
receive_notification_timeout
end.
assert_recv(Topic, Payload) ->
receive
{deliver, _, Msg} ->
?assertEqual(Topic, Msg#message.topic),
?assertEqual(Payload, Msg#message.payload)
after
500 ->
?assert(false)
end.

View File

@ -1,29 +0,0 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
_build
.idea
*.iml
rebar3.crashdump
*~
rebar.lock
data/
*.conf.rendered
*.pyc
.DS_Store
*.class
Mnesia.nonode@nohost/
src/emqx_exhook_pb.erl
src/emqx_exhook_v_1_hook_provider_client.erl
src/emqx_exhook_v_1_hook_provider_bhvr.erl

View File

@ -1,116 +0,0 @@
# 设计
## 动机
在 EMQ X Broker v4.1-v4.2 中,我们发布了 2 个插件来扩展 emqx 的编程能力:
1. `emqx-extension-hook` 提供了使用 Java, Python 向 Broker 挂载钩子的功能
2. `emqx-exproto` 提供了使用 JavaPython 编写用户自定义协议接入插件的功能
但在后续的支持中发现许多难以处理的问题:
1. 有大量的编程语言需要支持,需要编写和维护如 Go, JavaScript, Lua.. 等语言的驱动。
2. `erlport` 使用的操作系统的管道进行通信,这让用户代码只能部署在和 emqx 同一个操作系统上。部署方式受到了极大的限制。
3. 用户程序的启动参数直接打包到 Broker 中,导致用户开发无法实时的进行调试,单步跟踪等。
4. `erlport` 会占用 `stdin` `stdout`
因此,我们计划重构这部分的实现,其中主要的内容是:
1. 使用 `gRPC` 替换 `erlport`
2. 将 `emqx-extension-hook` 重命名为 `emqx-exhook`
旧版本的设计:[emqx-extension-hook design in v4.2.0](https://github.com/emqx/emqx-exhook/blob/v4.2.0/docs/design.md)
## 设计
架构如下:
```
EMQ X
+========================+ +========+==========+
| ExHook | | | |
| +----------------+ | gRPC | gRPC | User's |
| | gRPC Client | ------------------> | Server | Codes |
| +----------------+ | (HTTP/2) | | |
| | | | |
+========================+ +========+==========+
```
`emqx-exhook` 通过 gRPC 的方式向用户部署的 gRPC 服务发送钩子的请求,并处理其返回的值。
和 emqx 原生的钩子一致emqx-exhook 也按照链式的方式执行:
<img src="https://docs.emqx.net/broker/latest/cn/advanced/assets/chain_of_responsiblity.png" style="zoom:50%;" />
### gRPC 服务示例
用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中:
```protobuff
syntax = "proto3";
package emqx.exhook.v1;
service HookProvider {
rpc OnProviderLoaded(ProviderLoadedRequest) returns (LoadedResponse) {};
rpc OnProviderUnloaded(ProviderUnloadedRequest) returns (EmptySuccess) {};
rpc OnClientConnect(ClientConnectRequest) returns (EmptySuccess) {};
rpc OnClientConnack(ClientConnackRequest) returns (EmptySuccess) {};
rpc OnClientConnected(ClientConnectedRequest) returns (EmptySuccess) {};
rpc OnClientDisconnected(ClientDisconnectedRequest) returns (EmptySuccess) {};
rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {};
rpc OnClientCheckAcl(ClientCheckAclRequest) returns (ValuedResponse) {};
rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {};
rpc OnClientUnsubscribe(ClientUnsubscribeRequest) returns (EmptySuccess) {};
rpc OnSessionCreated(SessionCreatedRequest) returns (EmptySuccess) {};
rpc OnSessionSubscribed(SessionSubscribedRequest) returns (EmptySuccess) {};
rpc OnSessionUnsubscribed(SessionUnsubscribedRequest) returns (EmptySuccess) {};
rpc OnSessionResumed(SessionResumedRequest) returns (EmptySuccess) {};
rpc OnSessionDiscarded(SessionDiscardedRequest) returns (EmptySuccess) {};
rpc OnSessionTakeovered(SessionTakeoveredRequest) returns (EmptySuccess) {};
rpc OnSessionTerminated(SessionTerminatedRequest) returns (EmptySuccess) {};
rpc OnMessagePublish(MessagePublishRequest) returns (ValuedResponse) {};
rpc OnMessageDelivered(MessageDeliveredRequest) returns (EmptySuccess) {};
rpc OnMessageDropped(MessageDroppedRequest) returns (EmptySuccess) {};
rpc OnMessageAcked(MessageAckedRequest) returns (EmptySuccess) {};
}
```
### 配置文件示例
```
## 配置 gRPC 服务地址 (HTTP)
##
## s1 为服务器的名称
exhook.server.s1.url = http://127.0.0.1:9001
## 配置 gRPC 服务地址 (HTTPS)
##
## s2 为服务器名称
exhook.server.s2.url = https://127.0.0.1:9002
exhook.server.s2.cacertfile = ca.pem
exhook.server.s2.certfile = cert.pem
exhook.server.s2.keyfile = key.pem
```

View File

@ -1,48 +0,0 @@
%%-*- mode: erlang -*-
{plugins,
[rebar3_proper,
{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
]}.
{deps,
[{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.2"}}}
]}.
{grpc,
[{protos, ["priv/protos"]},
{gpb_opts, [{module_name_prefix, "emqx_"},
{module_name_suffix, "_pb"}]}
]}.
{provider_hooks,
[{pre, [{compile, {grpc, gen}},
{clean, {grpc, clean}}]}
]}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
debug_info,
{parse_transform}]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used, deprecated_function_calls,
warnings_as_errors, deprecated_functions]}.
{xref_ignores, [emqx_exhook_pb]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.
{cover_excl_mods, [emqx_exhook_pb,
emqx_exhook_v_1_hook_provider_bhvr,
emqx_exhook_v_1_hook_provider_client]}.
{profiles,
[{test,
[{deps,
[]}
]}
]}.

View File

@ -1,23 +0,0 @@
%% -*-: erlang -*-
{VSN,
[
{"4.3.1", [
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
]},
{"4.3.0", [
{load_module, emqx_exhook_pb, brutal_purge, soft_purge, []},
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
]},
{<<".*">>, []}
],
[
{"4.3.1", [
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
]},
{"4.3.0", [
{load_module, emqx_exhook_pb, brutal_purge, soft_purge, []},
{load_module, emqx_exhook_server, brutal_purge, soft_purge, []}
]},
{<<".*">>, []}
]
}.

View File

@ -1,96 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_exhook_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
%%--------------------------------------------------------------------
%% Setups
%%--------------------------------------------------------------------
all() -> emqx_ct:all(?MODULE).
init_per_suite(Cfg) ->
_ = emqx_exhook_demo_svr:start(),
emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1),
Cfg.
end_per_suite(_Cfg) ->
emqx_ct_helpers:stop_apps([emqx_exhook]),
emqx_exhook_demo_svr:stop().
set_special_cfgs(emqx) ->
application:set_env(emqx, allow_anonymous, false),
application:set_env(emqx, enable_acl_cache, false),
application:set_env(emqx, plugins_loaded_file, undefined),
application:set_env(emqx, modules_loaded_file, undefined);
set_special_cfgs(emqx_exhook) ->
ok.
%%--------------------------------------------------------------------
%% Test cases
%%--------------------------------------------------------------------
t_noserver_nohook(_) ->
emqx_exhook:disable(default),
?assertEqual([], ets:tab2list(emqx_hooks)),
Opts = proplists:get_value(
default,
application:get_env(emqx_exhook, servers, [])
),
ok = emqx_exhook:enable(default, Opts),
?assertNotEqual([], ets:tab2list(emqx_hooks)).
t_cli_list(_) ->
meck_print(),
?assertEqual( [[emqx_exhook_server:format(Svr) || Svr <- emqx_exhook:list()]]
, emqx_exhook_cli:cli(["server", "list"])
),
unmeck_print().
t_cli_enable_disable(_) ->
meck_print(),
?assertEqual([already_started], emqx_exhook_cli:cli(["server", "enable", "default"])),
?assertEqual(ok, emqx_exhook_cli:cli(["server", "disable", "default"])),
?assertEqual([], emqx_exhook_cli:cli(["server", "list"])),
?assertEqual([not_running], emqx_exhook_cli:cli(["server", "disable", "default"])),
?assertEqual(ok, emqx_exhook_cli:cli(["server", "enable", "default"])),
unmeck_print().
t_cli_stats(_) ->
meck_print(),
_ = emqx_exhook_cli:cli(["server", "stats"]),
_ = emqx_exhook_cli:cli(x),
unmeck_print().
%%--------------------------------------------------------------------
%% Utils
%%--------------------------------------------------------------------
meck_print() ->
meck:new(emqx_ctl, [passthrough, no_history, no_link]),
meck:expect(emqx_ctl, print, fun(_) -> ok end),
meck:expect(emqx_ctl, print, fun(_, Args) -> Args end).
unmeck_print() ->
meck:unload(emqx_ctl).

View File

@ -1,339 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_exhook_demo_svr).
-behavior(emqx_exhook_v_1_hook_provider_bhvr).
%%
-export([ start/0
, stop/0
, take/0
, in/1
]).
%% gRPC server HookProvider callbacks
-export([ on_provider_loaded/2
, on_provider_unloaded/2
, on_client_connect/2
, on_client_connack/2
, on_client_connected/2
, on_client_disconnected/2
, on_client_authenticate/2
, on_client_authorize/2
, on_client_subscribe/2
, on_client_unsubscribe/2
, on_session_created/2
, on_session_subscribed/2
, on_session_unsubscribed/2
, on_session_resumed/2
, on_session_discarded/2
, on_session_takeovered/2
, on_session_terminated/2
, on_message_publish/2
, on_message_delivered/2
, on_message_dropped/2
, on_message_acked/2
]).
-define(PORT, 9000).
-define(NAME, ?MODULE).
%%--------------------------------------------------------------------
%% Server APIs
%%--------------------------------------------------------------------
start() ->
Pid = spawn(fun mngr_main/0),
register(?MODULE, Pid),
{ok, Pid}.
stop() ->
grpc:stop_server(?NAME),
?MODULE ! stop.
take() ->
?MODULE ! {take, self()},
receive {value, V} -> V
after 5000 -> error(timeout) end.
in({FunName, Req}) ->
?MODULE ! {in, FunName, Req}.
mngr_main() ->
application:ensure_all_started(grpc),
Services = #{protos => [emqx_exhook_pb],
services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr}
},
Options = [],
Svr = grpc:start_server(?NAME, ?PORT, Services, Options),
mngr_loop([Svr, queue:new(), queue:new()]).
mngr_loop([Svr, Q, Takes]) ->
receive
{in, FunName, Req} ->
{NQ1, NQ2} = reply(queue:in({FunName, Req}, Q), Takes),
mngr_loop([Svr, NQ1, NQ2]);
{take, From} ->
{NQ1, NQ2} = reply(Q, queue:in(From, Takes)),
mngr_loop([Svr, NQ1, NQ2]);
stop ->
exit(normal)
end.
reply(Q1, Q2) ->
case queue:len(Q1) =:= 0 orelse
queue:len(Q2) =:= 0 of
true -> {Q1, Q2};
_ ->
{{value, {Name, V}}, NQ1} = queue:out(Q1),
{{value, From}, NQ2} = queue:out(Q2),
From ! {value, {Name, V}},
{NQ1, NQ2}
end.
%%--------------------------------------------------------------------
%% callbacks
%%--------------------------------------------------------------------
-spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_provider_loaded(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{hooks => [
#{name => <<"client.connect">>},
#{name => <<"client.connack">>},
#{name => <<"client.connected">>},
#{name => <<"client.disconnected">>},
#{name => <<"client.authenticate">>},
#{name => <<"client.authorize">>},
#{name => <<"client.subscribe">>},
#{name => <<"client.unsubscribe">>},
#{name => <<"session.created">>},
#{name => <<"session.subscribed">>},
#{name => <<"session.unsubscribed">>},
#{name => <<"session.resumed">>},
#{name => <<"session.discarded">>},
#{name => <<"session.takeovered">>},
#{name => <<"session.terminated">>},
#{name => <<"message.publish">>},
#{name => <<"message.delivered">>},
#{name => <<"message.acked">>},
#{name => <<"message.dropped">>}]}, Md}.
-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_provider_unloaded(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_connect(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_connack(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_connected(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_disconnected(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
%% some cases for testing
case Username of
<<"baduser">> ->
{ok, #{type => 'STOP_AND_RETURN',
value => {bool_result, false}}, Md};
<<"gooduser">> ->
{ok, #{type => 'STOP_AND_RETURN',
value => {bool_result, true}}, Md};
<<"normaluser">> ->
{ok, #{type => 'CONTINUE',
value => {bool_result, true}}, Md};
_ ->
{ok, #{type => 'IGNORE'}, Md}
end.
-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
%% some cases for testing
case Username of
<<"baduser">> ->
{ok, #{type => 'STOP_AND_RETURN',
value => {bool_result, false}}, Md};
<<"gooduser">> ->
{ok, #{type => 'STOP_AND_RETURN',
value => {bool_result, true}}, Md};
<<"normaluser">> ->
{ok, #{type => 'CONTINUE',
value => {bool_result, true}}, Md};
_ ->
{ok, #{type => 'IGNORE'}, Md}
end.
-spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_subscribe(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_client_unsubscribe(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_created(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_subscribed(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_unsubscribed(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_resumed(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_discarded(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_takeovered(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_session_terminated(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_message_publish(#{message := #{from := From} = Msg} = Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
%% some cases for testing
case From of
<<"baduser">> ->
NMsg = Msg#{qos => 0,
topic => <<"">>,
payload => <<"">>
},
{ok, #{type => 'STOP_AND_RETURN',
value => {message, NMsg}}, Md};
<<"gooduser">> ->
NMsg = Msg#{topic => From,
payload => From},
{ok, #{type => 'STOP_AND_RETURN',
value => {message, NMsg}}, Md};
_ ->
{ok, #{type => 'IGNORE'}, Md}
end.
-spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_message_delivered(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_message_dropped(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.
-spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata())
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}.
on_message_acked(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}.

View File

@ -1,531 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(prop_exhook_hooks).
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
-import(emqx_ct_proper_types,
[ conninfo/0
, clientinfo/0
, sessioninfo/0
, message/0
, connack_return_code/0
, topictab/0
, topic/0
, subopts/0
]).
-define(ALL(Vars, Types, Exprs),
?SETUP(fun() ->
State = do_setup(),
fun() -> do_teardown(State) end
end, ?FORALL(Vars, Types, Exprs))).
%%--------------------------------------------------------------------
%% Properties
%%--------------------------------------------------------------------
prop_client_connect() ->
?ALL({ConnInfo, ConnProps},
{conninfo(), conn_properties()},
begin
ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]),
{'on_client_connect', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{props => properties(ConnProps),
conninfo => from_conninfo(ConnInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_connack() ->
?ALL({ConnInfo, Rc, AckProps},
{conninfo(), connack_return_code(), ack_properties()},
begin
ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]),
{'on_client_connack', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{props => properties(AckProps),
result_code => atom_to_binary(Rc, utf8),
conninfo => from_conninfo(ConnInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_authenticate() ->
?ALL({ClientInfo0, AuthResult},
{clientinfo(), authresult()},
begin
ClientInfo = inject_magic_into(username, ClientInfo0),
OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult),
ExpectedAuthResult = case maps:get(username, ClientInfo) of
<<"baduser">> ->
AuthResult#{
auth_result => not_authorized,
anonymous => false};
<<"gooduser">> ->
AuthResult#{
auth_result => success,
anonymous => false};
<<"normaluser">> ->
AuthResult#{
auth_result => success,
anonymous => false};
_ ->
case maps:get(auth_result, AuthResult) of
success ->
#{auth_result => success,
anonymous => false};
_ ->
#{auth_result => not_authorized,
anonymous => false}
end
end,
?assertEqual(ExpectedAuthResult, OutAuthResult),
{'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{result => authresult_to_bool(AuthResult),
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_authorize() ->
?ALL({ClientInfo0, PubSub, Topic, Result},
{clientinfo(), oneof([publish, subscribe]),
topic(), oneof([allow, deny])},
begin
ClientInfo = inject_magic_into(username, ClientInfo0),
OutResult = emqx_hooks:run_fold(
'client.authorize',
[ClientInfo, PubSub, Topic],
Result),
ExpectedOutResult = case maps:get(username, ClientInfo) of
<<"baduser">> -> deny;
<<"gooduser">> -> allow;
<<"normaluser">> -> allow;
_ -> Result
end,
?assertEqual(ExpectedOutResult, OutResult),
{'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{result => aclresult_to_bool(Result),
type => pubsub_to_enum(PubSub),
topic => Topic,
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_connected() ->
?ALL({ClientInfo, ConnInfo},
{clientinfo(), conninfo()},
begin
ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]),
{'on_client_connected', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_disconnected() ->
?ALL({ClientInfo, Reason, ConnInfo},
{clientinfo(), shutdown_reason(), conninfo()},
begin
ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]),
{'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{reason => stringfy(Reason),
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_subscribe() ->
?ALL({ClientInfo, SubProps, TopicTab},
{clientinfo(), sub_properties(), topictab()},
begin
ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]),
{'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{props => properties(SubProps),
topic_filters => topicfilters(TopicTab),
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_client_unsubscribe() ->
?ALL({ClientInfo, UnSubProps, TopicTab},
{clientinfo(), unsub_properties(), topictab()},
begin
ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]),
{'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{props => properties(UnSubProps),
topic_filters => topicfilters(TopicTab),
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_created() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
begin
ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]),
{'on_session_created', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_subscribed() ->
?ALL({ClientInfo, Topic, SubOpts},
{clientinfo(), topic(), subopts()},
begin
ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]),
{'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{topic => Topic,
subopts => subopts(SubOpts),
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_unsubscribed() ->
?ALL({ClientInfo, Topic, SubOpts},
{clientinfo(), topic(), subopts()},
begin
ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]),
{'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{topic => Topic,
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_resumed() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
begin
ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]),
{'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_discared() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
begin
ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]),
{'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_takeovered() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
begin
ok = emqx_hooks:run('session.takeovered', [ClientInfo, SessInfo]),
{'on_session_takeovered', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_session_terminated() ->
?ALL({ClientInfo, Reason, SessInfo},
{clientinfo(), shutdown_reason(), sessioninfo()},
begin
ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]),
{'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{reason => stringfy(Reason),
clientinfo => from_clientinfo(ClientInfo)
},
?assertEqual(Expected, Resp),
true
end).
prop_message_publish() ->
?ALL(Msg0, message(),
begin
Msg = emqx_message:from_map(
inject_magic_into(from, emqx_message:to_map(Msg0))),
OutMsg= emqx_hooks:run_fold('message.publish', [], Msg),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true ->
?assertEqual(Msg, OutMsg),
skip;
_ ->
ExpectedOutMsg = case emqx_message:from(Msg) of
<<"baduser">> ->
MsgMap = emqx_message:to_map(Msg),
emqx_message:from_map(
MsgMap#{qos => 0,
topic => <<"">>,
payload => <<"">>
});
<<"gooduser">> = From ->
MsgMap = emqx_message:to_map(Msg),
emqx_message:from_map(
MsgMap#{topic => From,
payload => From
});
_ -> Msg
end,
?assertEqual(ExpectedOutMsg, OutMsg),
{'on_message_publish', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{message => from_message(Msg)
},
?assertEqual(Expected, Resp)
end,
true
end).
prop_message_dropped() ->
?ALL({Msg, By, Reason}, {message(), hardcoded, shutdown_reason()},
begin
ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> skip;
_ ->
{'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{reason => stringfy(Reason),
message => from_message(Msg)
},
?assertEqual(Expected, Resp)
end,
true
end).
prop_message_delivered() ->
?ALL({ClientInfo, Msg}, {clientinfo(), message()},
begin
ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> skip;
_ ->
{'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo),
message => from_message(Msg)
},
?assertEqual(Expected, Resp)
end,
true
end).
prop_message_acked() ->
?ALL({ClientInfo, Msg}, {clientinfo(), message()},
begin
ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> skip;
_ ->
{'on_message_acked', Resp} = emqx_exhook_demo_svr:take(),
Expected =
#{clientinfo => from_clientinfo(ClientInfo),
message => from_message(Msg)
},
?assertEqual(Expected, Resp)
end,
true
end).
nodestr() ->
stringfy(node()).
peerhost(#{peername := {Host, _}}) ->
ntoa(Host).
sockport(#{sockname := {_, Port}}) ->
Port.
%% copied from emqx_exhook
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}));
ntoa(IP) ->
list_to_binary(inet_parse:ntoa(IP)).
maybe(undefined) -> <<>>;
maybe(B) -> B.
properties(undefined) -> [];
properties(M) when is_map(M) ->
maps:fold(fun(K, V, Acc) ->
[#{name => stringfy(K),
value => stringfy(V)} | Acc]
end, [], M).
topicfilters(Tfs) when is_list(Tfs) ->
[#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs].
%% @private
stringfy(Term) when is_binary(Term) ->
Term;
stringfy(Term) when is_integer(Term) ->
integer_to_binary(Term);
stringfy(Term) when is_atom(Term) ->
atom_to_binary(Term, utf8);
stringfy(Term) ->
unicode:characters_to_binary((io_lib:format("~0p", [Term]))).
subopts(SubOpts) ->
#{qos => maps:get(qos, SubOpts, 0),
rh => maps:get(rh, SubOpts, 0),
rap => maps:get(rap, SubOpts, 0),
nl => maps:get(nl, SubOpts, 0),
share => maps:get(share, SubOpts, <<>>)
}.
authresult_to_bool(AuthResult) ->
maps:get(auth_result, AuthResult, undefined) == success.
aclresult_to_bool(Result) ->
Result == allow.
pubsub_to_enum(publish) -> 'PUBLISH';
pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
from_conninfo(ConnInfo) ->
#{node => nodestr(),
clientid => maps:get(clientid, ConnInfo),
username => maybe(maps:get(username, ConnInfo, <<>>)),
peerhost => peerhost(ConnInfo),
sockport => sockport(ConnInfo),
proto_name => maps:get(proto_name, ConnInfo),
proto_ver => stringfy(maps:get(proto_ver, ConnInfo)),
keepalive => maps:get(keepalive, ConnInfo)
}.
from_clientinfo(ClientInfo) ->
#{node => nodestr(),
clientid => maps:get(clientid, ClientInfo),
username => maybe(maps:get(username, ClientInfo, <<>>)),
password => maybe(maps:get(password, ClientInfo, <<>>)),
peerhost => ntoa(maps:get(peerhost, ClientInfo)),
sockport => maps:get(sockport, ClientInfo),
protocol => stringfy(maps:get(protocol, ClientInfo)),
mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)),
is_superuser => maps:get(is_superuser, ClientInfo, false),
anonymous => maps:get(anonymous, ClientInfo, true),
cn => maybe(maps:get(cn, ClientInfo, <<>>)),
dn => maybe(maps:get(dn, ClientInfo, <<>>))
}.
from_message(Msg) ->
#{node => nodestr(),
id => emqx_guid:to_hexstr(emqx_message:id(Msg)),
qos => emqx_message:qos(Msg),
from => stringfy(emqx_message:from(Msg)),
topic => emqx_message:topic(Msg),
payload => emqx_message:payload(Msg),
timestamp => emqx_message:timestamp(Msg)
}.
%%--------------------------------------------------------------------
%% Helper
%%--------------------------------------------------------------------
do_setup() ->
logger:set_primary_config(#{level => warning}),
_ = emqx_exhook_demo_svr:start(),
emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1),
%% waiting first loaded event
{'on_provider_loaded', _} = emqx_exhook_demo_svr:take(),
ok.
do_teardown(_) ->
emqx_ct_helpers:stop_apps([emqx_exhook]),
%% waiting last unloaded event
{'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(),
_ = emqx_exhook_demo_svr:stop(),
logger:set_primary_config(#{level => notice}),
timer:sleep(2000),
ok.
set_special_cfgs(emqx) ->
application:set_env(emqx, allow_anonymous, false),
application:set_env(emqx, enable_acl_cache, false),
application:set_env(emqx, modules_loaded_file, undefined),
application:set_env(emqx, plugins_loaded_file,
emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins"));
set_special_cfgs(emqx_exhook) ->
ok.
%%--------------------------------------------------------------------
%% Generators
%%--------------------------------------------------------------------
conn_properties() ->
#{}.
ack_properties() ->
#{}.
sub_properties() ->
#{}.
unsub_properties() ->
#{}.
shutdown_reason() ->
oneof([utf8(), {shutdown, emqx_ct_proper_types:limited_atom()}]).
authresult() ->
?LET(RC, connack_return_code(), #{auth_result => RC}).
inject_magic_into(Key, Object) ->
case castspell() of
muggles -> Object;
Spell ->
Object#{Key => Spell}
end.
castspell() ->
L = [<<"baduser">>, <<"gooduser">>, <<"normaluser">>, muggles],
lists:nth(rand:uniform(length(L)), L).

View File

@ -1,48 +0,0 @@
.eunit
deps
!deps/.placeholder
*.o
*.beam
*.plt
erl_crash.dump
ebin
!ebin/.placeholder
.concrete/DEV_MODE
.rebar
test/ebin/*.beam
.exrc
plugins/*/ebin
log/
*.swp
*.so
.erlang.mk/
cover/
emqx.d
eunit.coverdata
test/ct.cover.spec
logs
ct.coverdata
.idea/
emqx.iml
_rel/
data/
_build
.rebar3
rebar3.crashdump
.DS_Store
emqx.iml
bbmustache/
etc/gen.emqx.conf
compile_commands.json
cuttlefish
rebar.lock
xrefr
erlang.mk
*.coverdata
etc/emqx_exproto.conf.rendered
Mnesia.*/
src/emqx_exproto_pb.erl
src/emqx_exproto_v_1_connection_adapter_bhvr.erl
src/emqx_exproto_v_1_connection_adapter_client.erl
src/emqx_exproto_v_1_connection_handler_bhvr.erl
src/emqx_exproto_v_1_connection_handler_client.erl

View File

@ -1,127 +0,0 @@
# 多语言 - 协议接入
`emqx-exproto` 插件用于协议解析的多语言支持。它能够允许其他编程语言例如PythonJava 等)直接处理数据流实现协议的解析,并提供 Pub/Sub 接口以实现与系统其它组件的通信。
该插件给 EMQ X 带来的扩展性十分的强大,它能以你熟悉语言处理任何的私有协议,并享受由 EMQ X 系统带来的高连接,和高并发的优点。
## 特性
- 极强的扩展能力。使用 gRPC 作为 RPC 通信框架,支持各个主流编程语言
- 高吞吐。连接层以完全的异步非阻塞式 I/O 的方式实现
- 连接层透明。完全的支持 TCP\TLS UDP\DTLS 类型的连接管理,并对上层提供统一的 API 接口
- 连接层的管理能力。例如最大连接数连接和吞吐的速率限制IP 黑名单 等
## 架构
![Extension-Protocol Arch](images/exproto-arch.jpg)
该插件主要需要处理的内容包括:
1. **连接层:** 该部分主要 **维持 Socket 的生命周期,和数据的收发**。它的功能要求包括:
- 监听某个端口。当有新的 TCP/UDP 连接到达后,启动一个连接进程,来维持连接的状态。
- 调用 `OnSocketCreated` 回调。用于通知外部模块**已新建立了一个连接**。
- 调用 `OnScoektClosed` 回调。用于通知外部模块连接**已关闭**。
- 调用 `OnReceivedBytes` 回调。用于通知外部模块**该连接新收到的数据包**。
- 提供 `Send` 接口。供外部模块调用,**用于发送数据包**。
- 提供 `Close` 接口。供外部模块调用,**用于主动关闭连接**。
2. **协议/会话层:**该部分主要**提供 PUB/SUB 接口**,以实现与 EMQ X Broker 系统的消息互通。包括:
- 提供 `Authenticate` 接口。供外部模块调用,用于向集群注册客户端。
- 提供 `StartTimer` 接口。供外部模块调用,用于为该连接进程启动心跳等定时器。
- 提供 `Publish` 接口。供外部模块调用,用于发布消息 EMQ X Broker 中。
- 提供 `Subscribe` 接口。供外部模块调用,用于订阅某主题,以实现从 EMQ X Broker 中接收某些下行消息。
- 提供 `Unsubscribe` 接口。供外部模块调用,用于取消订阅某主题。
- 调用 `OnTimerTimeout` 回调。用于处理定时器超时的事件。
- 调用 `OnReceivedMessages` 回调。用于接收下行消息(在订阅主题成功后,如果主题上有消息,便会回调该方法)
## 接口设计
从 gRPC 上的逻辑来说emqx-exproto 会作为客户端向用户的 `ConnectionHandler` 服务发送回调请求。同时,它也会作为服务端向用户提供 `ConnectionAdapter` 服务,以提供 emqx-exproto 各个接口的访问。如图:
![Extension Protocol gRPC Arch](images/exproto-grpc-arch.jpg)
详情参见:`priv/protos/exproto.proto`,例如接口的定义有:
```protobuff
syntax = "proto3";
package emqx.exproto.v1;
// The Broker side serivce. It provides a set of APIs to
// handle a protcol access
service ConnectionAdapter {
// -- socket layer
rpc Send(SendBytesRequest) returns (CodeResponse) {};
rpc Close(CloseSocketRequest) returns (CodeResponse) {};
// -- protocol layer
rpc Authenticate(AuthenticateRequest) returns (CodeResponse) {};
rpc StartTimer(TimerRequest) returns (CodeResponse) {};
// -- pub/sub layer
rpc Publish(PublishRequest) returns (CodeResponse) {};
rpc Subscribe(SubscribeRequest) returns (CodeResponse) {};
rpc Unsubscribe(UnsubscribeRequest) returns (CodeResponse) {};
}
service ConnectionHandler {
// -- socket layer
rpc OnSocketCreated(stream SocketCreatedRequest) returns (EmptySuccess) {};
rpc OnSocketClosed(stream SocketClosedRequest) returns (EmptySuccess) {};
rpc OnReceivedBytes(stream ReceivedBytesRequest) returns (EmptySuccess) {};
// -- pub/sub layer
rpc OnTimerTimeout(stream TimerTimeoutRequest) returns (EmptySuccess) {};
rpc OnReceivedMessages(stream ReceivedMessagesRequest) returns (EmptySuccess) {};
}
```
## 配置项设计
1. 以 **监听器(Listener)** 为基础,提供 TCP/UDP 的监听。
- Listener 目前仅支持TCP、TLS、UDP、DTLS。(ws、wss、quic 暂不支持)
2. 每个监听器,会指定一个 `ConnectionHandler` 的服务地址,用于调用外部模块的接口。
3. emqx-exproto 还会监听一个 gRPC 端口用于提供对 `ConnectionAdapter` 服务的访问。
例如:
``` properties
## gRPC 服务监听地址 (HTTP)
##
exproto.server.http.url = http://127.0.0.1:9002
## gRPC 服务监听地址 (HTTPS)
##
exproto.server.https.url = https://127.0.0.1:9002
exproto.server.https.cacertfile = ca.pem
exproto.server.https.certfile = cert.pem
exproto.server.https.keyfile = key.pem
## Listener 配置
## 例如,名称为 protoname 协议的 TCP 监听器配置
exproto.listener.protoname = tcp://0.0.0.0:7993
## ConnectionHandler 服务地址及 https 的证书配置
exproto.listener.protoname.connection_handler_url = http://127.0.0.1:9001
#exproto.listener.protoname.connection_handler_certfile =
#exproto.listener.protoname.connection_handler_cacertfile =
#exproto.listener.protoname.connection_handler_keyfile =
# ...
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@ -1,51 +0,0 @@
%%-*- mode: erlang -*-
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
debug_info,
{parse_transform}]}.
{plugins,
[rebar3_proper,
{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
]}.
{deps,
[{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.2"}}}
]}.
{grpc,
[{type, all},
{protos, ["priv/protos"]},
{gpb_opts, [{module_name_prefix, "emqx_"},
{module_name_suffix, "_pb"}]}
]}.
{provider_hooks,
[{pre, [{compile, {grpc, gen}},
{clean, {grpc, clean}}]}
]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used, deprecated_function_calls,
warnings_as_errors, deprecated_functions]}.
{xref_ignores, [emqx_exproto_pb]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.
{cover_excl_mods, [emqx_exproto_pb,
emqx_exproto_v_1_connection_adapter_client,
emqx_exproto_v_1_connection_adapter_bhvr,
emqx_exproto_v_1_connection_handler_client,
emqx_exproto_v_1_connection_handler_bhvr]}.
{profiles,
[{test,
[{deps,
[]}
]}
]}.

View File

@ -1,454 +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_exproto_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-import(emqx_exproto_echo_svr,
[ frame_connect/2
, frame_connack/1
, frame_publish/3
, frame_puback/1
, frame_subscribe/2
, frame_suback/1
, frame_unsubscribe/1
, frame_unsuback/1
, frame_disconnect/0
]).
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-define(TCPOPTS, [binary, {active, false}]).
-define(DTLSOPTS, [binary, {active, false}, {protocol, dtls}]).
%%--------------------------------------------------------------------
%% Setups
%%--------------------------------------------------------------------
all() ->
[{group, Name} || Name <- metrics()].
groups() ->
Cases = emqx_ct:all(?MODULE),
[{Name, Cases} || Name <- metrics()].
%% @private
metrics() ->
[tcp, ssl, udp, dtls].
init_per_group(GrpName, Cfg) ->
put(grpname, GrpName),
Svrs = emqx_exproto_echo_svr:start(),
emqx_ct_helpers:start_apps([emqx_exproto], fun set_special_cfg/1),
emqx_logger:set_log_level(debug),
[{servers, Svrs}, {listener_type, GrpName} | Cfg].
end_per_group(_, Cfg) ->
emqx_ct_helpers:stop_apps([emqx_exproto]),
emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)).
set_special_cfg(emqx_exproto) ->
LisType = get(grpname),
Listeners = application:get_env(emqx_exproto, listeners, []),
SockOpts = socketopts(LisType),
UpgradeOpts = fun(Opts) ->
Opts2 = lists:keydelete(tcp_options, 1, Opts),
Opts3 = lists:keydelete(ssl_options, 1, Opts2),
Opts4 = lists:keydelete(udp_options, 1, Opts3),
Opts5 = lists:keydelete(dtls_options, 1, Opts4),
SockOpts ++ Opts5
end,
NListeners = [{Proto, LisType, LisOn, UpgradeOpts(Opts)}
|| {Proto, _Type, LisOn, Opts} <- Listeners],
application:set_env(emqx_exproto, listeners, NListeners);
set_special_cfg(emqx) ->
application:set_env(emqx, allow_anonymous, true),
application:set_env(emqx, enable_acl_cache, false),
ok.
%%--------------------------------------------------------------------
%% Tests cases
%%--------------------------------------------------------------------
t_start_stop(_) ->
ok.
t_mountpoint_echo(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>,
mountpoint => <<"ct/">>
},
Password = <<"123456">>,
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(0),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
SubBin = frame_subscribe(<<"t/dn">>, 1),
SubAckBin = frame_suback(0),
send(Sock, SubBin),
{ok, SubAckBin} = recv(Sock, 5000),
emqx:publish(emqx_message:make(<<"ct/t/dn">>, <<"echo">>)),
PubBin1 = frame_publish(<<"t/dn">>, 0, <<"echo">>),
{ok, PubBin1} = recv(Sock, 5000),
PubBin2 = frame_publish(<<"t/up">>, 0, <<"echo">>),
PubAckBin = frame_puback(0),
emqx:subscribe(<<"ct/t/up">>),
send(Sock, PubBin2),
{ok, PubAckBin} = recv(Sock, 5000),
receive
{deliver, _, _} -> ok
after 1000 ->
error(echo_not_running)
end,
close(Sock).
t_auth_deny(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>
},
Password = <<"123456">>,
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_access_control, authenticate,
fun(_) -> {error, ?RC_NOT_AUTHORIZED} end),
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(1),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
SockType =/= udp andalso begin
{error, closed} = recv(Sock, 5000)
end,
meck:unload([emqx_access_control]).
t_acl_deny(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>
},
Password = <<"123456">>,
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end),
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(0),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
SubBin = frame_subscribe(<<"t/#">>, 1),
SubAckBin = frame_suback(1),
send(Sock, SubBin),
{ok, SubAckBin} = recv(Sock, 5000),
emqx:publish(emqx_message:make(<<"t/dn">>, <<"echo">>)),
PubBin = frame_publish(<<"t/dn">>, 0, <<"echo">>),
PubBinFailedAck = frame_puback(1),
PubBinSuccesAck = frame_puback(0),
send(Sock, PubBin),
{ok, PubBinFailedAck} = recv(Sock, 5000),
meck:unload([emqx_access_control]),
send(Sock, PubBin),
{ok, PubBinSuccesAck} = recv(Sock, 5000),
close(Sock).
t_keepalive_timeout(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>,
keepalive => 2
},
Password = <<"123456">>,
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(0),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
DisconnectBin = frame_disconnect(),
{ok, DisconnectBin} = recv(Sock, 10000),
SockType =/= udp andalso begin
{error, closed} = recv(Sock, 5000)
end, ok.
t_hook_connected_disconnected(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>
},
Password = <<"123456">>,
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(0),
Parent = self(),
emqx:hook('client.connected', {?MODULE, hook_fun1, [Parent]}),
emqx:hook('client.disconnected',{?MODULE, hook_fun2, [Parent]}),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
receive
connected -> ok
after 1000 ->
error(hook_is_not_running)
end,
DisconnectBin = frame_disconnect(),
send(Sock, DisconnectBin),
receive
disconnected -> ok
after 1000 ->
error(hook_is_not_running)
end,
SockType =/= udp andalso begin
{error, closed} = recv(Sock, 5000)
end,
emqx:unhook('client.connected', {?MODULE, hook_fun1}),
emqx:unhook('client.disconnected', {?MODULE, hook_fun2}).
t_hook_session_subscribed_unsubscribed(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>
},
Password = <<"123456">>,
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(0),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
Parent = self(),
emqx:hook('session.subscribed', {?MODULE, hook_fun3, [Parent]}),
emqx:hook('session.unsubscribed', {?MODULE, hook_fun4, [Parent]}),
SubBin = frame_subscribe(<<"t/#">>, 1),
SubAckBin = frame_suback(0),
send(Sock, SubBin),
{ok, SubAckBin} = recv(Sock, 5000),
receive
subscribed -> ok
after 1000 ->
error(hook_is_not_running)
end,
UnsubBin = frame_unsubscribe(<<"t/#">>),
UnsubAckBin = frame_unsuback(0),
send(Sock, UnsubBin),
{ok, UnsubAckBin} = recv(Sock, 5000),
receive
unsubscribed -> ok
after 1000 ->
error(hook_is_not_running)
end,
close(Sock),
emqx:unhook('session.subscribed', {?MODULE, hook_fun3}),
emqx:unhook('session.unsubscribed', {?MODULE, hook_fun4}).
t_hook_message_delivered(Cfg) ->
SockType = proplists:get_value(listener_type, Cfg),
Sock = open(SockType),
Client = #{proto_name => <<"demo">>,
proto_ver => <<"v0.1">>,
clientid => <<"test_client_1">>
},
Password = <<"123456">>,
ConnBin = frame_connect(Client, Password),
ConnAckBin = frame_connack(0),
send(Sock, ConnBin),
{ok, ConnAckBin} = recv(Sock, 5000),
SubBin = frame_subscribe(<<"t/#">>, 1),
SubAckBin = frame_suback(0),
send(Sock, SubBin),
{ok, SubAckBin} = recv(Sock, 5000),
emqx:hook('message.delivered', {?MODULE, hook_fun5, []}),
emqx:publish(emqx_message:make(<<"t/dn">>, <<"1">>)),
PubBin1 = frame_publish(<<"t/dn">>, 0, <<"2">>),
{ok, PubBin1} = recv(Sock, 5000),
close(Sock),
emqx:unhook('message.delivered', {?MODULE, hook_fun5}).
%%--------------------------------------------------------------------
%% Utils
hook_fun1(_, _, Parent) -> Parent ! connected, ok.
hook_fun2(_, _, _, Parent) -> Parent ! disconnected, ok.
hook_fun3(_, _, _, Parent) -> Parent ! subscribed, ok.
hook_fun4(_, _, _, Parent) -> Parent ! unsubscribed, ok.
hook_fun5(_, Msg) -> {ok, Msg#message{payload = <<"2">>}}.
rand_bytes() ->
crypto:strong_rand_bytes(rand:uniform(256)).
%%--------------------------------------------------------------------
%% Sock funcs
open(tcp) ->
{ok, Sock} = gen_tcp:connect("127.0.0.1", 7993, ?TCPOPTS),
{tcp, Sock};
open(udp) ->
{ok, Sock} = gen_udp:open(0, ?TCPOPTS),
{udp, Sock};
open(ssl) ->
SslOpts = client_ssl_opts(),
{ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?TCPOPTS ++ SslOpts),
{ssl, SslSock};
open(dtls) ->
SslOpts = client_ssl_opts(),
{ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?DTLSOPTS ++ SslOpts),
{dtls, SslSock}.
send({tcp, Sock}, Bin) ->
gen_tcp:send(Sock, Bin);
send({udp, Sock}, Bin) ->
gen_udp:send(Sock, "127.0.0.1", 7993, Bin);
send({ssl, Sock}, Bin) ->
ssl:send(Sock, Bin);
send({dtls, Sock}, Bin) ->
ssl:send(Sock, Bin).
recv({tcp, Sock}, Ts) ->
gen_tcp:recv(Sock, 0, Ts);
recv({udp, Sock}, Ts) ->
{ok, {_, _, Bin}} = gen_udp:recv(Sock, 0, Ts),
{ok, Bin};
recv({ssl, Sock}, Ts) ->
ssl:recv(Sock, 0, Ts);
recv({dtls, Sock}, Ts) ->
ssl:recv(Sock, 0, Ts).
close({tcp, Sock}) ->
gen_tcp:close(Sock);
close({udp, Sock}) ->
gen_udp:close(Sock);
close({ssl, Sock}) ->
ssl:close(Sock);
close({dtls, Sock}) ->
ssl:close(Sock).
%%--------------------------------------------------------------------
%% Server-Opts
socketopts(tcp) ->
[{tcp_options, tcp_opts()}];
socketopts(ssl) ->
[{tcp_options, tcp_opts()},
{ssl_options, ssl_opts()}];
socketopts(udp) ->
[{udp_options, udp_opts()}];
socketopts(dtls) ->
[{udp_options, udp_opts()},
{dtls_options, dtls_opts()}].
tcp_opts() ->
[{send_timeout, 15000},
{send_timeout_close, true},
{backlog, 100},
{nodelay, true} | udp_opts()].
udp_opts() ->
[{recbuf, 1024},
{sndbuf, 1024},
{buffer, 1024},
{reuseaddr, true}].
ssl_opts() ->
Certs = certs("key.pem", "cert.pem", "cacert.pem"),
[{versions, emqx_tls_lib:default_versions()},
{ciphers, emqx_tls_lib:default_ciphers()},
{verify, verify_peer},
{fail_if_no_peer_cert, true},
{secure_renegotiate, false},
{reuse_sessions, true},
{honor_cipher_order, true}]++Certs.
dtls_opts() ->
Opts = ssl_opts(),
lists:keyreplace(versions, 1, Opts, {versions, ['dtlsv1.2', 'dtlsv1']}).
%%--------------------------------------------------------------------
%% Client-Opts
client_ssl_opts() ->
certs( "client-key.pem", "client-cert.pem", "cacert.pem" ).
certs( Key, Cert, CACert ) ->
CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"),
[ { keyfile, filename:join([ CertsPath, Key ]) },
{ certfile, filename:join([ CertsPath, Cert ]) },
{ cacertfile, filename:join([ CertsPath, CACert ]) } ].

View File

@ -1,278 +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_exproto_echo_svr).
-behavior(emqx_exproto_v_1_connection_handler_bhvr).
-export([ start/0
, stop/1
]).
-export([ frame_connect/2
, frame_connack/1
, frame_publish/3
, frame_puback/1
, frame_subscribe/2
, frame_suback/1
, frame_unsubscribe/1
, frame_unsuback/1
, frame_disconnect/0
]).
-export([ on_socket_created/2
, on_received_bytes/2
, on_socket_closed/2
, on_timer_timeout/2
, on_received_messages/2
]).
-define(LOG(Fmt, Args), io:format(standard_error, Fmt, Args)).
-define(HTTP, #{grpc_opts => #{service_protos => [emqx_exproto_pb],
services => #{'emqx.exproto.v1.ConnectionHandler' => ?MODULE}},
listen_opts => #{port => 9001,
socket_options => []},
pool_opts => #{size => 8},
transport_opts => #{ssl => false}}).
-define(CLIENT, emqx_exproto_v_1_connection_adapter_client).
-define(send(Req), ?CLIENT:send(Req, #{channel => ct_test_channel})).
-define(close(Req), ?CLIENT:close(Req, #{channel => ct_test_channel})).
-define(authenticate(Req), ?CLIENT:authenticate(Req, #{channel => ct_test_channel})).
-define(start_timer(Req), ?CLIENT:start_timer(Req, #{channel => ct_test_channel})).
-define(publish(Req), ?CLIENT:publish(Req, #{channel => ct_test_channel})).
-define(subscribe(Req), ?CLIENT:subscribe(Req, #{channel => ct_test_channel})).
-define(unsubscribe(Req), ?CLIENT:unsubscribe(Req, #{channel => ct_test_channel})).
-define(TYPE_CONNECT, 1).
-define(TYPE_CONNACK, 2).
-define(TYPE_PUBLISH, 3).
-define(TYPE_PUBACK, 4).
-define(TYPE_SUBSCRIBE, 5).
-define(TYPE_SUBACK, 6).
-define(TYPE_UNSUBSCRIBE, 7).
-define(TYPE_UNSUBACK, 8).
-define(TYPE_DISCONNECT, 9).
-define(loop_recv_and_reply_empty_success(Stream),
?loop_recv_and_reply_empty_success(Stream, fun(_) -> ok end)).
-define(loop_recv_and_reply_empty_success(Stream, Fun),
begin
LoopRecv = fun _Lp(_St) ->
case grpc_stream:recv(_St) of
{more, _Reqs, _NSt} ->
?LOG("~p: ~p~n", [?FUNCTION_NAME, _Reqs]),
Fun(_Reqs), _Lp(_NSt);
{eos, _Reqs, _NSt} ->
?LOG("~p: ~p~n", [?FUNCTION_NAME, _Reqs]),
Fun(_Reqs), _NSt
end
end,
NStream = LoopRecv(Stream),
grpc_stream:reply(NStream, #{}),
{ok, NStream}
end).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
start() ->
application:ensure_all_started(grpc),
[start_channel(), start_server()].
start_channel() ->
grpc_client_sup:create_channel_pool(ct_test_channel, "http://127.0.0.1:9100", #{}).
start_server() ->
Services = #{protos => [emqx_exproto_pb],
services => #{'emqx.exproto.v1.ConnectionHandler' => ?MODULE}
},
Options = [],
grpc:start_server(?MODULE, 9001, Services, Options).
stop([_ChannPid, _SvrPid]) ->
grpc:stop_server(?MODULE),
grpc_client_sup:stop_channel_pool(ct_test_channel).
%%--------------------------------------------------------------------
%% Protocol Adapter callbacks
%%--------------------------------------------------------------------
-spec on_socket_created(grpc_stream:stream(), grpc:metadata())
-> {ok, grpc_stream:stream()}.
on_socket_created(Stream, _Md) ->
?loop_recv_and_reply_empty_success(Stream).
-spec on_socket_closed(grpc_stream:stream(), grpc:metadata())
-> {ok, grpc_stream:stream()}.
on_socket_closed(Stream, _Md) ->
?loop_recv_and_reply_empty_success(Stream).
-spec on_received_bytes(grpc_stream:stream(), grpc:metadata())
-> {ok, grpc_stream:stream()}.
on_received_bytes(Stream, _Md) ->
?loop_recv_and_reply_empty_success(Stream,
fun(Reqs) ->
lists:foreach(
fun(#{conn := Conn, bytes := Bytes}) ->
#{<<"type">> := Type} = Params = emqx_json:decode(Bytes, [return_maps]),
_ = handle_in(Conn, Type, Params)
end, Reqs)
end).
-spec on_timer_timeout(grpc_stream:stream(), grpc:metadata())
-> {ok, grpc_stream:stream()}.
on_timer_timeout(Stream, _Md) ->
?loop_recv_and_reply_empty_success(Stream,
fun(Reqs) ->
lists:foreach(
fun(#{conn := Conn, type := 'KEEPALIVE'}) ->
?LOG("Close this connection ~p due to keepalive timeout", [Conn]),
handle_out(Conn, ?TYPE_DISCONNECT),
?close(#{conn => Conn})
end, Reqs)
end).
-spec on_received_messages(grpc_stream:stream(), grpc:metadata())
-> {ok, grpc_stream:stream()}.
on_received_messages(Stream, _Md) ->
?loop_recv_and_reply_empty_success(Stream,
fun(Reqs) ->
lists:foreach(
fun(#{conn := Conn, messages := Messages}) ->
lists:foreach(fun(Message) ->
handle_out(Conn, ?TYPE_PUBLISH, Message)
end, Messages)
end, Reqs)
end).
%%--------------------------------------------------------------------
%% The Protocol Example:
%% CONN:
%% {"type": 1, "clientinfo": {...}}
%%
%% CONNACK:
%% {"type": 2, "code": 0}
%%
%% PUBLISH:
%% {"type": 3, "topic": "xxx", "payload": "", "qos": 0}
%%
%% PUBACK:
%% {"type": 4, "code": 0}
%%
%% SUBSCRIBE:
%% {"type": 5, "topic": "xxx", "qos": 1}
%%
%% SUBACK:
%% {"type": 6, "code": 0}
%%
%% DISCONNECT:
%% {"type": 7, "code": 1}
%%--------------------------------------------------------------------
handle_in(Conn, ?TYPE_CONNECT, #{<<"clientinfo">> := ClientInfo, <<"password">> := Password}) ->
NClientInfo = maps:from_list([{binary_to_atom(K, utf8), V} || {K, V} <- maps:to_list(ClientInfo)]),
case ?authenticate(#{conn => Conn, clientinfo => NClientInfo, password => Password}) of
{ok, #{code := 'SUCCESS'}, _} ->
case maps:get(keepalive, NClientInfo, 0) of
0 -> ok;
Intv ->
io:format("Try call start_timer with ~ps", [Intv]),
?start_timer(#{conn => Conn, type => 'KEEPALIVE', interval => Intv})
end,
handle_out(Conn, ?TYPE_CONNACK, 0);
_ ->
handle_out(Conn, ?TYPE_CONNACK, 1),
?close(#{conn => Conn})
end;
handle_in(Conn, ?TYPE_PUBLISH, #{<<"topic">> := Topic,
<<"qos">> := Qos,
<<"payload">> := Payload}) ->
case ?publish(#{conn => Conn, topic => Topic, qos => Qos, payload => Payload}) of
{ok, #{code := 'SUCCESS'}, _} ->
handle_out(Conn, ?TYPE_PUBACK, 0);
_ ->
handle_out(Conn, ?TYPE_PUBACK, 1)
end;
handle_in(Conn, ?TYPE_SUBSCRIBE, #{<<"qos">> := Qos, <<"topic">> := Topic}) ->
case ?subscribe(#{conn => Conn, topic => Topic, qos => Qos}) of
{ok, #{code := 'SUCCESS'}, _} ->
handle_out(Conn, ?TYPE_SUBACK, 0);
_ ->
handle_out(Conn, ?TYPE_SUBACK, 1)
end;
handle_in(Conn, ?TYPE_UNSUBSCRIBE, #{<<"topic">> := Topic}) ->
case ?unsubscribe(#{conn => Conn, topic => Topic}) of
{ok, #{code := 'SUCCESS'}, _} ->
handle_out(Conn, ?TYPE_UNSUBACK, 0);
_ ->
handle_out(Conn, ?TYPE_UNSUBACK, 1)
end;
handle_in(Conn, ?TYPE_DISCONNECT, _) ->
?close(#{conn => Conn}).
handle_out(Conn, ?TYPE_CONNACK, Code) ->
?send(#{conn => Conn, bytes => frame_connack(Code)});
handle_out(Conn, ?TYPE_PUBACK, Code) ->
?send(#{conn => Conn, bytes => frame_puback(Code)});
handle_out(Conn, ?TYPE_SUBACK, Code) ->
?send(#{conn => Conn, bytes => frame_suback(Code)});
handle_out(Conn, ?TYPE_UNSUBACK, Code) ->
?send(#{conn => Conn, bytes => frame_unsuback(Code)});
handle_out(Conn, ?TYPE_PUBLISH, #{qos := Qos, topic := Topic, payload := Payload}) ->
?send(#{conn => Conn, bytes => frame_publish(Topic, Qos, Payload)}).
handle_out(Conn, ?TYPE_DISCONNECT) ->
?send(#{conn => Conn, bytes => frame_disconnect()}).
%%--------------------------------------------------------------------
%% Frame
frame_connect(ClientInfo, Password) ->
emqx_json:encode(#{type => ?TYPE_CONNECT,
clientinfo => ClientInfo,
password => Password}).
frame_connack(Code) ->
emqx_json:encode(#{type => ?TYPE_CONNACK, code => Code}).
frame_publish(Topic, Qos, Payload) ->
emqx_json:encode(#{type => ?TYPE_PUBLISH,
topic => Topic,
qos => Qos,
payload => Payload}).
frame_puback(Code) ->
emqx_json:encode(#{type => ?TYPE_PUBACK, code => Code}).
frame_subscribe(Topic, Qos) ->
emqx_json:encode(#{type => ?TYPE_SUBSCRIBE, topic => Topic, qos => Qos}).
frame_suback(Code) ->
emqx_json:encode(#{type => ?TYPE_SUBACK, code => Code}).
frame_unsubscribe(Topic) ->
emqx_json:encode(#{type => ?TYPE_UNSUBSCRIBE, topic => Topic}).
frame_unsuback(Code) ->
emqx_json:encode(#{type => ?TYPE_UNSUBACK, code => Code}).
frame_disconnect() ->
emqx_json:encode(#{type => ?TYPE_DISCONNECT}).

View File

@ -1,7 +1,22 @@
{erl_opts, [debug_info]}.
{deps, []}.
{deps, [
{gen_coap, {git, "https://github.com/emqx/gen_coap", {tag, "v0.3.2"}}},
{lwm2m_coap, {git, "https://github.com/emqx/lwm2m-coap", {tag, "v2.0.0"}}},
{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.2"}}}
]}.
{shell, [
% {config, "config/sys.config"},
{apps, [emqx_gateway]}
]}.
% {plugins,
% [rebar3_proper,
% {grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
% ]}.
% {grpc,
% [{protos, ["priv/protos"]},
% {gpb_opts, [{module_name_prefix, "emqx_"},
% {module_name_suffix, "_pb"}]}
% ]}.

View File

@ -20,7 +20,7 @@
-emqx_plugin(protocol).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-export([ start/2
, stop/1

View File

@ -18,7 +18,7 @@
-behaviour(gen_server).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").

View File

@ -16,9 +16,9 @@
-module(emqx_coap_pubsub_resource).
-behaviour(coap_resource).
% -behaviour(coap_resource).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-include_lib("gen_coap/include/coap.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").

View File

@ -18,7 +18,7 @@
-behaviour(gen_server).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/logger.hrl").

View File

@ -18,7 +18,7 @@
-author("Feng Lee <feng@emqx.io>").
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[CoAP-Registry]").

View File

@ -16,9 +16,9 @@
-module(emqx_coap_resource).
-behaviour(coap_resource).
% -behaviour(coap_resource).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").

View File

@ -16,7 +16,7 @@
-module(emqx_coap_server).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-export([ start/1
, stop/1

View File

@ -16,7 +16,7 @@
-module(emqx_coap_timer).
-include("emqx_coap.hrl").
-include("src/coap/include/emqx_coap.hrl").
-export([ cancel_timer/1
, start_timer/2

View File

@ -0,0 +1,319 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_coap_SUITE).
% -compile(export_all).
% -compile(nowarn_export_all).
% -include_lib("gen_coap/include/coap.hrl").
% -include_lib("eunit/include/eunit.hrl").
% -include_lib("emqx/include/emqx.hrl").
% -define(LOGT(Format, Args), ct:pal(Format, Args)).
% all() -> emqx_ct:all(?MODULE).
% init_per_suite(Config) ->
% emqx_ct_helpers:start_apps([emqx_coap], fun set_special_cfg/1),
% Config.
% set_special_cfg(emqx_coap) ->
% Opts = application:get_env(emqx_coap, dtls_opts,[]),
% Opts2 = [{keyfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/key.pem")},
% {certfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/cert.pem")}],
% application:set_env(emqx_coap, dtls_opts, emqx_misc:merge_opts(Opts, Opts2)),
% application:set_env(emqx_coap, enable_stats, true);
% set_special_cfg(_) ->
% ok.
% end_per_suite(Config) ->
% emqx_ct_helpers:stop_apps([emqx_coap]),
% Config.
% %%--------------------------------------------------------------------
% %% Test Cases
% %%--------------------------------------------------------------------
% t_publish(_Config) ->
% Topic = <<"abc">>, Payload = <<"123">>,
% TopicStr = binary_to_list(Topic),
% URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
% %% Sub topic first
% emqx:subscribe(Topic),
% Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% {ok, changed, _} = Reply,
% receive
% {deliver, Topic, Msg} ->
% ?assertEqual(Topic, Msg#message.topic),
% ?assertEqual(Payload, Msg#message.payload)
% after
% 500 ->
% ?assert(false)
% end.
% t_publish_acl_deny(_Config) ->
% Topic = <<"abc">>, Payload = <<"123">>,
% TopicStr = binary_to_list(Topic),
% URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
% %% Sub topic first
% emqx:subscribe(Topic),
% ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]),
% ok = meck:expect(emqx_access_control, authorize, 3, deny),
% Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?assertEqual({error,forbidden}, Reply),
% ok = meck:unload(emqx_access_control),
% receive
% {deliver, Topic, Msg} -> ct:fail({unexpected, {Topic, Msg}})
% after
% 500 -> ok
% end.
% t_observe(_Config) ->
% Topic = <<"abc">>, TopicStr = binary_to_list(Topic),
% Payload = <<"123">>,
% Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
% {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
% ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
% [SubPid] = emqx:subscribers(Topic),
% ?assert(is_pid(SubPid)),
% %% Publish a message
% emqx:publish(emqx_message:make(Topic, Payload)),
% Notif = receive_notification(),
% ?LOGT("observer get Notif=~p", [Notif]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
% ?assertEqual(Payload, PayloadRecv),
% er_coap_observer:stop(Pid),
% timer:sleep(100),
% [] = emqx:subscribers(Topic).
% t_observe_acl_deny(_Config) ->
% Topic = <<"abc">>, TopicStr = binary_to_list(Topic),
% Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
% ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]),
% ok = meck:expect(emqx_access_control, authorize, 3, deny),
% ?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)),
% [] = emqx:subscribers(Topic),
% ok = meck:unload(emqx_access_control).
% t_observe_wildcard(_Config) ->
% Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)),
% Payload = <<"123">>,
% Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
% {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
% ?LOGT("observer Uri=~p, Pid=~p, N=~p, Code=~p, Content=~p", [Uri, Pid, N, Code, Content]),
% [SubPid] = emqx:subscribers(Topic),
% ?assert(is_pid(SubPid)),
% %% Publish a message
% emqx:publish(emqx_message:make(<<"a/b">>, Payload)),
% Notif = receive_notification(),
% ?LOGT("observer get Notif=~p", [Notif]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
% ?assertEqual(Payload, PayloadRecv),
% er_coap_observer:stop(Pid),
% timer:sleep(100),
% [] = emqx:subscribers(Topic).
% t_observe_pub(_Config) ->
% Topic = <<"+/b">>, TopicStr = emqx_http_lib:uri_encode(binary_to_list(Topic)),
% Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
% {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
% ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
% [SubPid] = emqx:subscribers(Topic),
% ?assert(is_pid(SubPid)),
% Topic2 = <<"a/b">>, Payload2 = <<"UFO">>,
% TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)),
% URI2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret",
% Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = Payload2}),
% {ok,changed, _} = Reply2,
% Notif2 = receive_notification(),
% ?LOGT("observer get Notif2=~p", [Notif2]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2,
% ?assertEqual(Payload2, PayloadRecv2),
% Topic3 = <<"j/b">>, Payload3 = <<"ET629">>,
% TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)),
% URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=mike&p=guess",
% Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
% {ok,changed, _} = Reply3,
% Notif3 = receive_notification(),
% ?LOGT("observer get Notif3=~p", [Notif3]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv3}} = Notif3,
% ?assertEqual(Payload3, PayloadRecv3),
% er_coap_observer:stop(Pid).
% t_one_clientid_sub_2_topics(_Config) ->
% Topic1 = <<"abc">>, TopicStr1 = binary_to_list(Topic1),
% Payload1 = <<"123">>,
% Uri1 = "coap://127.0.0.1/mqtt/"++TopicStr1++"?c=client1&u=tom&p=secret",
% {ok, Pid1, N1, Code1, Content1} = er_coap_observer:observe(Uri1),
% ?LOGT("observer 1 Pid=~p, N=~p, Code=~p, Content=~p", [Pid1, N1, Code1, Content1]),
% [SubPid] = emqx:subscribers(Topic1),
% ?assert(is_pid(SubPid)),
% Topic2 = <<"x/y">>, TopicStr2 = emqx_http_lib:uri_encode(binary_to_list(Topic2)),
% Payload2 = <<"456">>,
% Uri2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret",
% {ok, Pid2, N2, Code2, Content2} = er_coap_observer:observe(Uri2),
% ?LOGT("observer 2 Pid=~p, N=~p, Code=~p, Content=~p", [Pid2, N2, Code2, Content2]),
% [SubPid] = emqx:subscribers(Topic2),
% ?assert(is_pid(SubPid)),
% emqx:publish(emqx_message:make(Topic1, Payload1)),
% Notif1 = receive_notification(),
% ?LOGT("observer 1 get Notif=~p", [Notif1]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv1}} = Notif1,
% ?assertEqual(Payload1, PayloadRecv1),
% emqx:publish(emqx_message:make(Topic2, Payload2)),
% Notif2 = receive_notification(),
% ?LOGT("observer 2 get Notif=~p", [Notif2]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2,
% ?assertEqual(Payload2, PayloadRecv2),
% er_coap_observer:stop(Pid1),
% er_coap_observer:stop(Pid2).
% t_invalid_parameter(_Config) ->
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %% "cid=client2" is invaid
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Topic3 = <<"a/b">>, Payload3 = <<"ET629">>,
% TopicStr3 = emqx_http_lib:uri_encode(binary_to_list(Topic3)),
% URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?cid=client2&u=tom&p=simple",
% Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
% ?assertMatch({error,bad_request}, Reply3),
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %% "what=hello" is invaid
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% URI4 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?what=hello",
% Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
% ?assertMatch({error, bad_request}, Reply4).
% t_invalid_topic(_Config) ->
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %% "a/b" is a valid topic string
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Topic3 = <<"a/b">>, Payload3 = <<"ET629">>,
% TopicStr3 = binary_to_list(Topic3),
% URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=tom&p=simple",
% Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
% ?assertMatch({ok,changed,_Content}, Reply3),
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %% "+?#" is invaid topic string
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% URI4 = "coap://127.0.0.1/mqtt/"++"+?#"++"?what=hello",
% Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
% ?assertMatch({error,bad_request}, Reply4).
% % mqtt connection kicked by coap with same client id
% t_kick_1(_Config) ->
% URI = "coap://127.0.0.1/mqtt/abc?c=clientid&u=tom&p=secret",
% % workaround: emqx:subscribe does not kick same client id.
% spawn_monitor(fun() ->
% {ok, C} = emqtt:start_link([{host, "localhost"},
% {clientid, <<"clientid">>},
% {username, <<"plain">>},
% {password, <<"plain">>}]),
% {ok, _} = emqtt:connect(C) end),
% er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>,
% payload = <<"123">>}),
% receive
% {'DOWN', _, _, _, _} -> ok
% after 2000 ->
% ?assert(false)
% end.
% % mqtt connection kicked by coap with same client id
% t_acl(_Config) ->
% OldPath = emqx:get_env(plugins_etc_dir),
% application:set_env(emqx, plugins_etc_dir,
% emqx_ct_helpers:deps_path(emqx_authz, "test")),
% Conf = #{<<"authz">> =>
% #{<<"rules">> =>
% [#{<<"principal">> =>#{<<"username">> => <<"coap">>},
% <<"permission">> => deny,
% <<"topics">> => [<<"abc">>],
% <<"action">> => <<"publish">>}
% ]}},
% ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
% application:ensure_all_started(emqx_authz),
% emqx:subscribe(<<"abc">>),
% URI = "coap://127.0.0.1/mqtt/adbc?c=client1&u=coap&p=secret",
% er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>,
% payload = <<"123">>}),
% receive
% _Something -> ?assert(false)
% after 2000 ->
% ok
% end,
% ok = emqx_hooks:del('client.authorize', {emqx_authz, authorize}),
% file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
% application:set_env(emqx, plugins_etc_dir, OldPath),
% application:stop(emqx_authz).
% t_stats(_) ->
% ok.
% t_auth_failure(_) ->
% ok.
% t_qos_supprot(_) ->
% ok.
% %%--------------------------------------------------------------------
% %% Helpers
% receive_notification() ->
% receive
% {coap_notify, Pid, N2, Code2, Content2} ->
% {coap_notify, Pid, N2, Code2, Content2}
% after 2000 ->
% receive_notification_timeout
% end.
% testdir(DataPath) ->
% Ls = filename:split(DataPath),
% filename:join(lists:sublist(Ls, 1, length(Ls) - 1)).

View File

@ -0,0 +1,678 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_coap_pubsub_SUITE).
% -compile(export_all).
% -compile(nowarn_export_all).
% -include_lib("gen_coap/include/coap.hrl").
% -include_lib("eunit/include/eunit.hrl").
% -include_lib("emqx/include/emqx.hrl").
% -define(LOGT(Format, Args), ct:pal(Format, Args)).
% all() -> emqx_ct:all(?MODULE).
% init_per_suite(Config) ->
% emqx_ct_helpers:start_apps([emqx_coap], fun set_special_cfg/1),
% Config.
% set_special_cfg(emqx_coap) ->
% application:set_env(emqx_coap, enable_stats, true);
% set_special_cfg(_) ->
% ok.
% end_per_suite(Config) ->
% emqx_ct_helpers:stop_apps([emqx_coap]),
% Config.
% %%--------------------------------------------------------------------
% %% Test Cases
% %%--------------------------------------------------------------------
% t_update_max_age(_Config) ->
% TopicInPayload = <<"topic1">>,
% Payload = <<"<topic1>;ct=42">>,
% Payload1 = <<"<topic1>;ct=50">>,
% URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
% URI2 = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% timer:sleep(50),
% %% post to create the same topic but with different max age and ct value in payload
% Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 70, format = <<"application/link-format">>, payload = Payload1}),
% {ok,created, #coap_content{location_path = LocPath}} = Reply1,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{TopicInPayload, MaxAge2, CT2, _ResPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
% ?assertEqual(70, MaxAge2),
% ?assertEqual(<<"50">>, CT2),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
% t_create_subtopic(_Config) ->
% TopicInPayload = <<"topic1">>,
% TopicInPayloadStr = "topic1",
% Payload = <<"<topic1>;ct=42">>,
% URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
% RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% timer:sleep(50),
% %% post to create the a sub topic
% SubPayload = <<"<subtopic>;ct=42">>,
% SubTopicInPayloadStr = "subtopic",
% SubURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"?c=client1&u=tom&p=secret",
% SubRealURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"/"++SubTopicInPayloadStr++"?c=client1&u=tom&p=secret",
% FullTopic = list_to_binary(TopicInPayloadStr++"/"++SubTopicInPayloadStr),
% Reply1 = er_coap_client:request(post, SubURI, #coap_content{format = <<"application/link-format">>, payload = SubPayload}),
% ?LOGT("Reply =~p", [Reply1]),
% {ok,created, #coap_content{location_path = LocPath1}} = Reply1,
% ?assertEqual([<<"/ps/topic1/subtopic">>] ,LocPath1),
% [{FullTopic, MaxAge2, CT2, _ResPayload, _}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
% ?assertEqual(60, MaxAge2),
% ?assertEqual(<<"42">>, CT2),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, SubRealURI),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI).
% t_over_max_age(_Config) ->
% TopicInPayload = <<"topic1">>,
% Payload = <<"<topic1>;ct=42">>,
% URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{max_age = 2, format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(2, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% timer:sleep(3000),
% ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)).
% t_refreash_max_age(_Config) ->
% TopicInPayload = <<"topic1">>,
% Payload = <<"<topic1>;ct=42">>,
% Payload1 = <<"<topic1>;ct=50">>,
% URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
% RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?LOGT("TimeStamp=~p", [TimeStamp]),
% ?assertEqual(5, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% timer:sleep(3000),
% %% post to create the same topic, the max age timer will be restarted with the new max age value
% Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload1}),
% {ok,created, #coap_content{location_path = LocPath}} = Reply1,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{TopicInPayload, MaxAge2, CT2, _ResPayload, TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
% ?LOGT("TimeStamp1=~p", [TimeStamp1]),
% ?assertEqual(5, MaxAge2),
% ?assertEqual(<<"50">>, CT2),
% timer:sleep(3000),
% ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI).
% t_case01_publish_post(_Config) ->
% timer:sleep(100),
% MainTopic = <<"maintopic">>,
% TopicInPayload = <<"topic1">>,
% Payload = <<"<topic1>;ct=42">>,
% MainTopicStr = binary_to_list(MainTopic),
% %% post to create topic maintopic/topic1
% URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret",
% FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)),
% Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply1]),
% {ok,created, #coap_content{location_path = LocPath1}} = Reply1,
% ?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1),
% [{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
% ?assertEqual(60, MaxAge),
% ?assertEqual(<<"42">>, CT2),
% %% post to publish message to topic maintopic/topic1
% FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)),
% URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret",
% PubPayload = <<"PUBLISH">>,
% %% Sub topic first
% emqx:subscribe(FullTopic),
% Reply2 = er_coap_client:request(post, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}),
% ?LOGT("Reply =~p", [Reply2]),
% {ok,changed, _} = Reply2,
% TopicInfo = [{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
% ?LOGT("the topic info =~p", [TopicInfo]),
% assert_recv(FullTopic, PubPayload),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
% t_case02_publish_post(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"payload">>,
% %% Sub topic first
% emqx:subscribe(Topic),
% %% post to publish a new topic "topic1", and the topic is created
% URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(60, MaxAge),
% ?assertEqual(<<"42">>, CT),
% assert_recv(Topic, Payload),
% %% post to publish a new message to the same topic "topic1" with different payload
% NewPayload = <<"newpayload">>,
% Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}),
% ?LOGT("Reply =~p", [Reply1]),
% {ok,changed, _} = Reply1,
% [{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% assert_recv(Topic, NewPayload),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case03_publish_post(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"payload">>,
% %% Sub topic first
% emqx:subscribe(Topic),
% %% post to publish a new topic "topic1", and the topic is created
% URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(60, MaxAge),
% ?assertEqual(<<"42">>, CT),
% assert_recv(Topic, Payload),
% %% post to publish a new message to the same topic "topic1", but the ct is not same as created
% NewPayload = <<"newpayload">>,
% Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}),
% ?LOGT("Reply =~p", [Reply1]),
% ?assertEqual({error,bad_request}, Reply1),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case04_publish_post(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"payload">>,
% %% post to publish a new topic "topic1", and the topic is created
% URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(5, MaxAge),
% ?assertEqual(<<"42">>, CT),
% %% after max age timeout, the topic still exists but the status is timeout
% timer:sleep(6000),
% ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case01_publish_put(_Config) ->
% MainTopic = <<"maintopic">>,
% TopicInPayload = <<"topic1">>,
% Payload = <<"<topic1>;ct=42">>,
% MainTopicStr = binary_to_list(MainTopic),
% %% post to create topic maintopic/topic1
% URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret",
% FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)),
% Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply1]),
% {ok,created, #coap_content{location_path = LocPath1}} = Reply1,
% ?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1),
% [{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
% ?assertEqual(60, MaxAge),
% ?assertEqual(<<"42">>, CT2),
% %% put to publish message to topic maintopic/topic1
% FullTopicStr = emqx_http_lib:uri_encode(binary_to_list(FullTopic)),
% URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret",
% PubPayload = <<"PUBLISH">>,
% %% Sub topic first
% emqx:subscribe(FullTopic),
% Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}),
% ?LOGT("Reply =~p", [Reply2]),
% {ok,changed, _} = Reply2,
% [{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
% assert_recv(FullTopic, PubPayload),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
% t_case02_publish_put(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"payload">>,
% %% Sub topic first
% emqx:subscribe(Topic),
% %% put to publish a new topic "topic1", and the topic is created
% URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(60, MaxAge),
% ?assertEqual(<<"42">>, CT),
% assert_recv(Topic, Payload),
% %% put to publish a new message to the same topic "topic1" with different payload
% NewPayload = <<"newpayload">>,
% Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}),
% ?LOGT("Reply =~p", [Reply1]),
% {ok,changed, _} = Reply1,
% [{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% assert_recv(Topic, NewPayload),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case03_publish_put(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"payload">>,
% %% Sub topic first
% emqx:subscribe(Topic),
% %% put to publish a new topic "topic1", and the topic is created
% URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(60, MaxAge),
% ?assertEqual(<<"42">>, CT),
% assert_recv(Topic, Payload),
% %% put to publish a new message to the same topic "topic1", but the ct is not same as created
% NewPayload = <<"newpayload">>,
% Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}),
% ?LOGT("Reply =~p", [Reply1]),
% ?assertEqual({error,bad_request}, Reply1),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case04_publish_put(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"payload">>,
% %% put to publish a new topic "topic1", and the topic is created
% URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(put, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/topic1">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(5, MaxAge),
% ?assertEqual(<<"42">>, CT),
% %% after max age timeout, no publish message to the same topic, the topic info will be deleted
% %%%%%%%%%%%%%%%%%%%%%%%%%%
% % but there is one thing to do is we don't count in the publish message received from emqx(from other node).TBD!!!!!!!!!!!!!
% %%%%%%%%%%%%%%%%%%%%%%%%%%
% timer:sleep(6000),
% ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case01_subscribe(_Config) ->
% Topic = <<"topic1">>,
% Payload1 = <<"<topic1>;ct=42">>,
% timer:sleep(100),
% %% First post to create a topic "topic1"
% Uri = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
% ?assertEqual(<<"/ps/topic1">> ,LocPath),
% TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% %% Subscribe the topic
% Uri1 = "coap://127.0.0.1"++binary_to_list(LocPath)++"?c=client1&u=tom&p=secret",
% {ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri1),
% ?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
% [SubPid] = emqx:subscribers(Topic),
% ?assert(is_pid(SubPid)),
% %% Publish a message
% Payload = <<"123">>,
% emqx:publish(emqx_message:make(Topic, Payload)),
% Notif = receive_notification(),
% ?LOGT("observer get Notif=~p", [Notif]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
% ?assertEqual(Payload, PayloadRecv),
% %% GET to read the publish message of the topic
% Reply1 = er_coap_client:request(get, Uri1),
% ?LOGT("Reply=~p", [Reply1]),
% {ok,content, #coap_content{payload = <<"123">>}} = Reply1,
% er_coap_observer:stop(Pid),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri1).
% t_case02_subscribe(_Config) ->
% Topic = <<"a/b">>,
% TopicStr = binary_to_list(Topic),
% PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
% Payload = <<"payload">>,
% %% post to publish a new topic "a/b", and the topic is created
% URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/a/b">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(5, MaxAge),
% ?assertEqual(<<"42">>, CT),
% %% Wait for the max age of the timer expires
% timer:sleep(6000),
% ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
% %% Subscribe to the timeout topic "a/b", still successfullygot {ok, nocontent} Method
% Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% Reply1 = {ok, Pid, _N, nocontent, _} = er_coap_observer:observe(Uri),
% ?LOGT("Subscribe Reply=~p", [Reply1]),
% [SubPid] = emqx:subscribers(Topic),
% ?assert(is_pid(SubPid)),
% %% put to publish to topic "a/b"
% Reply2 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% {ok,changed, #coap_content{}} = Reply2,
% [{Topic, MaxAge1, CT, Payload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT),
% ?assertEqual(false, TimeStamp =:= timeout),
% %% Publish a message
% emqx:publish(emqx_message:make(Topic, Payload)),
% Notif = receive_notification(),
% ?LOGT("observer get Notif=~p", [Notif]),
% {coap_notify, _, _, {ok,content}, #coap_content{payload = Payload}} = Notif,
% er_coap_observer:stop(Pid),
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case03_subscribe(_Config) ->
% %% Subscribe to the unexisted topic "a/b", got not_found
% Topic = <<"a/b">>,
% TopicStr = binary_to_list(Topic),
% PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
% Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% {error, not_found} = er_coap_observer:observe(Uri),
% [] = emqx:subscribers(Topic).
% t_case04_subscribe(_Config) ->
% %% Subscribe to the wildcad topic "+/b", got bad_request
% Topic = <<"+/b">>,
% TopicStr = binary_to_list(Topic),
% PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
% Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% {error, bad_request} = er_coap_observer:observe(Uri),
% [] = emqx:subscribers(Topic).
% t_case01_read(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"PubPayload">>,
% timer:sleep(100),
% %% First post to create a topic "topic1"
% Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
% ?assertEqual(<<"/ps/topic1">> ,LocPath),
% TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% %% GET to read the publish message of the topic
% timer:sleep(1000),
% Reply1 = er_coap_client:request(get, Uri),
% ?LOGT("Reply=~p", [Reply1]),
% {ok,content, #coap_content{payload = Payload}} = Reply1,
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
% t_case02_read(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"PubPayload">>,
% timer:sleep(100),
% %% First post to publish a topic "topic1"
% Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
% ?assertEqual(<<"/ps/topic1">> ,LocPath),
% TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% %% GET to read the publish message of unmatched format, got bad_request
% Reply1 = er_coap_client:request(get, Uri, #coap_content{format = <<"application/json">>}),
% ?LOGT("Reply=~p", [Reply1]),
% {error, bad_request} = Reply1,
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
% t_case03_read(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% timer:sleep(100),
% %% GET to read the nexisted topic "topic1", got not_found
% Reply = er_coap_client:request(get, Uri),
% ?LOGT("Reply=~p", [Reply]),
% {error, not_found} = Reply.
% t_case04_read(_Config) ->
% Topic = <<"topic1">>,
% TopicStr = binary_to_list(Topic),
% Payload = <<"PubPayload">>,
% timer:sleep(100),
% %% First post to publish a topic "topic1"
% Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
% ?assertEqual(<<"/ps/topic1">> ,LocPath),
% TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?LOGT("lookup topic info=~p", [TopicInfo]),
% ?assertEqual(60, MaxAge1),
% ?assertEqual(<<"42">>, CT1),
% %% GET to read the publish message of wildcard topic, got bad_request
% WildTopic = binary_to_list(<<"+/topic1">>),
% Uri1 = "coap://127.0.0.1/ps/"++WildTopic++"?c=client1&u=tom&p=secret",
% Reply1 = er_coap_client:request(get, Uri1, #coap_content{format = <<"application/json">>}),
% ?LOGT("Reply=~p", [Reply1]),
% {error, bad_request} = Reply1,
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
% t_case05_read(_Config) ->
% Topic = <<"a/b">>,
% TopicStr = binary_to_list(Topic),
% PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
% Payload = <<"payload">>,
% %% post to publish a new topic "a/b", and the topic is created
% URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/a/b">>] ,LocPath),
% [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
% ?assertEqual(5, MaxAge),
% ?assertEqual(<<"42">>, CT),
% %% Wait for the max age of the timer expires
% timer:sleep(6000),
% ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
% %% GET to read the expired publish message, supposed to get {ok, nocontent}, but now got {ok, content}
% Reply1 = er_coap_client:request(get, URI),
% ?LOGT("Reply=~p", [Reply1]),
% {ok, content, #coap_content{payload = <<>>}}= Reply1,
% {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
% t_case01_delete(_Config) ->
% TopicInPayload = <<"a/b">>,
% TopicStr = binary_to_list(TopicInPayload),
% PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
% Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"),
% URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
% %% Client post to CREATE topic "a/b"
% Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
% ?LOGT("Reply =~p", [Reply]),
% {ok,created, #coap_content{location_path = LocPath}} = Reply,
% ?assertEqual([<<"/ps/a/b">>] ,LocPath),
% %% Client post to CREATE topic "a/b/c"
% TopicInPayload1 = <<"a/b/c">>,
% PercentEncodedTopic1 = emqx_http_lib:uri_encode(binary_to_list(TopicInPayload1)),
% Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"),
% Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
% ?LOGT("Reply =~p", [Reply1]),
% {ok,created, #coap_content{location_path = LocPath1}} = Reply1,
% ?assertEqual([<<"/ps/a/b/c">>] ,LocPath1),
% timer:sleep(50),
% %% DELETE the topic "a/b"
% UriD = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% ReplyD = er_coap_client:request(delete, UriD),
% ?LOGT("Reply=~p", [ReplyD]),
% {ok, deleted, #coap_content{}}= ReplyD,
% timer:sleep(300), %% Waiting gen_server:cast/2 for deleting operation
% ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload)),
% ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload1)).
% t_case02_delete(_Config) ->
% TopicInPayload = <<"a/b">>,
% TopicStr = binary_to_list(TopicInPayload),
% PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
% %% DELETE the unexisted topic "a/b"
% Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
% Reply1 = er_coap_client:request(delete, Uri1),
% ?LOGT("Reply=~p", [Reply1]),
% {error, not_found} = Reply1.
% t_case13_emit_stats_test(_Config) ->
% ok.
% %%--------------------------------------------------------------------
% %% Internal functions
% receive_notification() ->
% receive
% {coap_notify, Pid, N2, Code2, Content2} ->
% {coap_notify, Pid, N2, Code2, Content2}
% after 2000 ->
% receive_notification_timeout
% end.
% assert_recv(Topic, Payload) ->
% receive
% {deliver, _, Msg} ->
% ?assertEqual(Topic, Msg#message.topic),
% ?assertEqual(Payload, Msg#message.payload)
% after
% 500 ->
% ?assert(false)
% end.

View File

@ -16,7 +16,7 @@
-module(emqx_exhook).
-include("emqx_exhook.hrl").
-include("src/exhook/include/emqx_exhook.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[ExHook]").

View File

@ -18,7 +18,7 @@
-behaviour(application).
-include("emqx_exhook.hrl").
-include("src/exhook/include/emqx_exhook.hrl").
-emqx_plugin(extension).

View File

@ -16,7 +16,7 @@
-module(emqx_exhook_cli).
-include("emqx_exhook.hrl").
-include("src/exhook/include/emqx_exhook.hrl").
-export([cli/1]).

View File

@ -16,7 +16,7 @@
-module(emqx_exhook_handler).
-include("emqx_exhook.hrl").
-include("src/exhook/include/emqx_exhook.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl").

View File

@ -16,7 +16,7 @@
-module(emqx_exhook_server).
-include("emqx_exhook.hrl").
-include("src/exhook/include/emqx_exhook.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[ExHook Svr]").

View File

@ -0,0 +1,531 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(prop_exhook_hooks).
% -include_lib("proper/include/proper.hrl").
% -include_lib("eunit/include/eunit.hrl").
% -import(emqx_ct_proper_types,
% [ conninfo/0
% , clientinfo/0
% , sessioninfo/0
% , message/0
% , connack_return_code/0
% , topictab/0
% , topic/0
% , subopts/0
% ]).
% -define(ALL(Vars, Types, Exprs),
% ?SETUP(fun() ->
% State = do_setup(),
% fun() -> do_teardown(State) end
% end, ?FORALL(Vars, Types, Exprs))).
% %%--------------------------------------------------------------------
% %% Properties
% %%--------------------------------------------------------------------
% prop_client_connect() ->
% ?ALL({ConnInfo, ConnProps},
% {conninfo(), conn_properties()},
% begin
% ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]),
% {'on_client_connect', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{props => properties(ConnProps),
% conninfo => from_conninfo(ConnInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_connack() ->
% ?ALL({ConnInfo, Rc, AckProps},
% {conninfo(), connack_return_code(), ack_properties()},
% begin
% ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]),
% {'on_client_connack', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{props => properties(AckProps),
% result_code => atom_to_binary(Rc, utf8),
% conninfo => from_conninfo(ConnInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_authenticate() ->
% ?ALL({ClientInfo0, AuthResult},
% {clientinfo(), authresult()},
% begin
% ClientInfo = inject_magic_into(username, ClientInfo0),
% OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult),
% ExpectedAuthResult = case maps:get(username, ClientInfo) of
% <<"baduser">> ->
% AuthResult#{
% auth_result => not_authorized,
% anonymous => false};
% <<"gooduser">> ->
% AuthResult#{
% auth_result => success,
% anonymous => false};
% <<"normaluser">> ->
% AuthResult#{
% auth_result => success,
% anonymous => false};
% _ ->
% case maps:get(auth_result, AuthResult) of
% success ->
% #{auth_result => success,
% anonymous => false};
% _ ->
% #{auth_result => not_authorized,
% anonymous => false}
% end
% end,
% ?assertEqual(ExpectedAuthResult, OutAuthResult),
% {'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{result => authresult_to_bool(AuthResult),
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_authorize() ->
% ?ALL({ClientInfo0, PubSub, Topic, Result},
% {clientinfo(), oneof([publish, subscribe]),
% topic(), oneof([allow, deny])},
% begin
% ClientInfo = inject_magic_into(username, ClientInfo0),
% OutResult = emqx_hooks:run_fold(
% 'client.authorize',
% [ClientInfo, PubSub, Topic],
% Result),
% ExpectedOutResult = case maps:get(username, ClientInfo) of
% <<"baduser">> -> deny;
% <<"gooduser">> -> allow;
% <<"normaluser">> -> allow;
% _ -> Result
% end,
% ?assertEqual(ExpectedOutResult, OutResult),
% {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{result => aclresult_to_bool(Result),
% type => pubsub_to_enum(PubSub),
% topic => Topic,
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_connected() ->
% ?ALL({ClientInfo, ConnInfo},
% {clientinfo(), conninfo()},
% begin
% ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]),
% {'on_client_connected', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_disconnected() ->
% ?ALL({ClientInfo, Reason, ConnInfo},
% {clientinfo(), shutdown_reason(), conninfo()},
% begin
% ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]),
% {'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{reason => stringfy(Reason),
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_subscribe() ->
% ?ALL({ClientInfo, SubProps, TopicTab},
% {clientinfo(), sub_properties(), topictab()},
% begin
% ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]),
% {'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{props => properties(SubProps),
% topic_filters => topicfilters(TopicTab),
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_client_unsubscribe() ->
% ?ALL({ClientInfo, UnSubProps, TopicTab},
% {clientinfo(), unsub_properties(), topictab()},
% begin
% ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]),
% {'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{props => properties(UnSubProps),
% topic_filters => topicfilters(TopicTab),
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_created() ->
% ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
% begin
% ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]),
% {'on_session_created', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_subscribed() ->
% ?ALL({ClientInfo, Topic, SubOpts},
% {clientinfo(), topic(), subopts()},
% begin
% ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]),
% {'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{topic => Topic,
% subopts => subopts(SubOpts),
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_unsubscribed() ->
% ?ALL({ClientInfo, Topic, SubOpts},
% {clientinfo(), topic(), subopts()},
% begin
% ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]),
% {'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{topic => Topic,
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_resumed() ->
% ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
% begin
% ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]),
% {'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_discared() ->
% ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
% begin
% ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]),
% {'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_takeovered() ->
% ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
% begin
% ok = emqx_hooks:run('session.takeovered', [ClientInfo, SessInfo]),
% {'on_session_takeovered', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_session_terminated() ->
% ?ALL({ClientInfo, Reason, SessInfo},
% {clientinfo(), shutdown_reason(), sessioninfo()},
% begin
% ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]),
% {'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{reason => stringfy(Reason),
% clientinfo => from_clientinfo(ClientInfo)
% },
% ?assertEqual(Expected, Resp),
% true
% end).
% prop_message_publish() ->
% ?ALL(Msg0, message(),
% begin
% Msg = emqx_message:from_map(
% inject_magic_into(from, emqx_message:to_map(Msg0))),
% OutMsg= emqx_hooks:run_fold('message.publish', [], Msg),
% case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
% true ->
% ?assertEqual(Msg, OutMsg),
% skip;
% _ ->
% ExpectedOutMsg = case emqx_message:from(Msg) of
% <<"baduser">> ->
% MsgMap = emqx_message:to_map(Msg),
% emqx_message:from_map(
% MsgMap#{qos => 0,
% topic => <<"">>,
% payload => <<"">>
% });
% <<"gooduser">> = From ->
% MsgMap = emqx_message:to_map(Msg),
% emqx_message:from_map(
% MsgMap#{topic => From,
% payload => From
% });
% _ -> Msg
% end,
% ?assertEqual(ExpectedOutMsg, OutMsg),
% {'on_message_publish', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{message => from_message(Msg)
% },
% ?assertEqual(Expected, Resp)
% end,
% true
% end).
% prop_message_dropped() ->
% ?ALL({Msg, By, Reason}, {message(), hardcoded, shutdown_reason()},
% begin
% ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]),
% case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
% true -> skip;
% _ ->
% {'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{reason => stringfy(Reason),
% message => from_message(Msg)
% },
% ?assertEqual(Expected, Resp)
% end,
% true
% end).
% prop_message_delivered() ->
% ?ALL({ClientInfo, Msg}, {clientinfo(), message()},
% begin
% ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]),
% case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
% true -> skip;
% _ ->
% {'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo),
% message => from_message(Msg)
% },
% ?assertEqual(Expected, Resp)
% end,
% true
% end).
% prop_message_acked() ->
% ?ALL({ClientInfo, Msg}, {clientinfo(), message()},
% begin
% ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
% case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
% true -> skip;
% _ ->
% {'on_message_acked', Resp} = emqx_exhook_demo_svr:take(),
% Expected =
% #{clientinfo => from_clientinfo(ClientInfo),
% message => from_message(Msg)
% },
% ?assertEqual(Expected, Resp)
% end,
% true
% end).
% nodestr() ->
% stringfy(node()).
% peerhost(#{peername := {Host, _}}) ->
% ntoa(Host).
% sockport(#{sockname := {_, Port}}) ->
% Port.
% %% copied from emqx_exhook
% ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
% list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}));
% ntoa(IP) ->
% list_to_binary(inet_parse:ntoa(IP)).
% maybe(undefined) -> <<>>;
% maybe(B) -> B.
% properties(undefined) -> [];
% properties(M) when is_map(M) ->
% maps:fold(fun(K, V, Acc) ->
% [#{name => stringfy(K),
% value => stringfy(V)} | Acc]
% end, [], M).
% topicfilters(Tfs) when is_list(Tfs) ->
% [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs].
% %% @private
% stringfy(Term) when is_binary(Term) ->
% Term;
% stringfy(Term) when is_integer(Term) ->
% integer_to_binary(Term);
% stringfy(Term) when is_atom(Term) ->
% atom_to_binary(Term, utf8);
% stringfy(Term) ->
% unicode:characters_to_binary((io_lib:format("~0p", [Term]))).
% subopts(SubOpts) ->
% #{qos => maps:get(qos, SubOpts, 0),
% rh => maps:get(rh, SubOpts, 0),
% rap => maps:get(rap, SubOpts, 0),
% nl => maps:get(nl, SubOpts, 0),
% share => maps:get(share, SubOpts, <<>>)
% }.
% authresult_to_bool(AuthResult) ->
% maps:get(auth_result, AuthResult, undefined) == success.
% aclresult_to_bool(Result) ->
% Result == allow.
% pubsub_to_enum(publish) -> 'PUBLISH';
% pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
% from_conninfo(ConnInfo) ->
% #{node => nodestr(),
% clientid => maps:get(clientid, ConnInfo),
% username => maybe(maps:get(username, ConnInfo, <<>>)),
% peerhost => peerhost(ConnInfo),
% sockport => sockport(ConnInfo),
% proto_name => maps:get(proto_name, ConnInfo),
% proto_ver => stringfy(maps:get(proto_ver, ConnInfo)),
% keepalive => maps:get(keepalive, ConnInfo)
% }.
% from_clientinfo(ClientInfo) ->
% #{node => nodestr(),
% clientid => maps:get(clientid, ClientInfo),
% username => maybe(maps:get(username, ClientInfo, <<>>)),
% password => maybe(maps:get(password, ClientInfo, <<>>)),
% peerhost => ntoa(maps:get(peerhost, ClientInfo)),
% sockport => maps:get(sockport, ClientInfo),
% protocol => stringfy(maps:get(protocol, ClientInfo)),
% mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)),
% is_superuser => maps:get(is_superuser, ClientInfo, false),
% anonymous => maps:get(anonymous, ClientInfo, true),
% cn => maybe(maps:get(cn, ClientInfo, <<>>)),
% dn => maybe(maps:get(dn, ClientInfo, <<>>))
% }.
% from_message(Msg) ->
% #{node => nodestr(),
% id => emqx_guid:to_hexstr(emqx_message:id(Msg)),
% qos => emqx_message:qos(Msg),
% from => stringfy(emqx_message:from(Msg)),
% topic => emqx_message:topic(Msg),
% payload => emqx_message:payload(Msg),
% timestamp => emqx_message:timestamp(Msg)
% }.
% %%--------------------------------------------------------------------
% %% Helper
% %%--------------------------------------------------------------------
% do_setup() ->
% logger:set_primary_config(#{level => warning}),
% _ = emqx_exhook_demo_svr:start(),
% emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1),
% %% waiting first loaded event
% {'on_provider_loaded', _} = emqx_exhook_demo_svr:take(),
% ok.
% do_teardown(_) ->
% emqx_ct_helpers:stop_apps([emqx_exhook]),
% %% waiting last unloaded event
% {'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(),
% _ = emqx_exhook_demo_svr:stop(),
% logger:set_primary_config(#{level => notice}),
% timer:sleep(2000),
% ok.
% set_special_cfgs(emqx) ->
% application:set_env(emqx, allow_anonymous, false),
% application:set_env(emqx, enable_acl_cache, false),
% application:set_env(emqx, modules_loaded_file, undefined),
% application:set_env(emqx, plugins_loaded_file,
% emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins"));
% set_special_cfgs(emqx_exhook) ->
% ok.
% %%--------------------------------------------------------------------
% %% Generators
% %%--------------------------------------------------------------------
% conn_properties() ->
% #{}.
% ack_properties() ->
% #{}.
% sub_properties() ->
% #{}.
% unsub_properties() ->
% #{}.
% shutdown_reason() ->
% oneof([utf8(), {shutdown, emqx_ct_proper_types:limited_atom()}]).
% authresult() ->
% ?LET(RC, connack_return_code(), #{auth_result => RC}).
% inject_magic_into(Key, Object) ->
% case castspell() of
% muggles -> Object;
% Spell ->
% Object#{Key => Spell}
% end.
% castspell() ->
% L = [<<"baduser">>, <<"gooduser">>, <<"normaluser">>, muggles],
% lists:nth(rand:uniform(length(L)), L).

View File

@ -0,0 +1,97 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_exhook_SUITE).
% -compile(export_all).
% -compile(nowarn_export_all).
% -include_lib("eunit/include/eunit.hrl").
% -include_lib("common_test/include/ct.hrl").
% %%--------------------------------------------------------------------
% %% Setups
% %%--------------------------------------------------------------------
% all() -> emqx_ct:all(?MODULE).
% init_per_suite(Cfg) ->
% _ = emqx_exhook_demo_svr:start(),
% emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1),
% Cfg.
% end_per_suite(_Cfg) ->
% emqx_ct_helpers:stop_apps([emqx_exhook]),
% emqx_exhook_demo_svr:stop().
% set_special_cfgs(emqx) ->
% application:set_env(emqx, allow_anonymous, false),
% application:set_env(emqx, enable_acl_cache, false),
% application:set_env(emqx, plugins_loaded_file, undefined),
% application:set_env(emqx, modules_loaded_file, undefined);
% set_special_cfgs(emqx_exhook) ->
% ok.
% %%--------------------------------------------------------------------
% %% Test cases
% %%--------------------------------------------------------------------
% t_noserver_nohook(_) ->
% emqx_exhook:disable(default),
% ?assertEqual([], ets:tab2list(emqx_hooks)),
% Opts = proplists:get_value(
% default,
% application:get_env(emqx_exhook, servers, [])
% ),
% ok = emqx_exhook:enable(default, Opts),
% ?assertNotEqual([], ets:tab2list(emqx_hooks)).
% t_cli_list(_) ->
% meck_print(),
% ?assertEqual( [[emqx_exhook_server:format(Svr) || Svr <- emqx_exhook:list()]]
% , emqx_exhook_cli:cli(["server", "list"])
% ),
% unmeck_print().
% t_cli_enable_disable(_) ->
% meck_print(),
% ?assertEqual([already_started], emqx_exhook_cli:cli(["server", "enable", "default"])),
% ?assertEqual(ok, emqx_exhook_cli:cli(["server", "disable", "default"])),
% ?assertEqual([], emqx_exhook_cli:cli(["server", "list"])),
% ?assertEqual([not_running], emqx_exhook_cli:cli(["server", "disable", "default"])),
% ?assertEqual(ok, emqx_exhook_cli:cli(["server", "enable", "default"])),
% unmeck_print().
% t_cli_stats(_) ->
% meck_print(),
% _ = emqx_exhook_cli:cli(["server", "stats"]),
% _ = emqx_exhook_cli:cli(x),
% unmeck_print().
% %%--------------------------------------------------------------------
% %% Utils
% %%--------------------------------------------------------------------
% meck_print() ->
% meck:new(emqx_ctl, [passthrough, no_history, no_link]),
% meck:expect(emqx_ctl, print, fun(_) -> ok end),
% meck:expect(emqx_ctl, print, fun(_, Args) -> Args end).
% unmeck_print() ->
% meck:unload(emqx_ctl).

View File

@ -0,0 +1,339 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_exhook_demo_svr).
% -behavior(emqx_exhook_v_1_hook_provider_bhvr).
% %%
% -export([ start/0
% , stop/0
% , take/0
% , in/1
% ]).
% %% gRPC server HookProvider callbacks
% -export([ on_provider_loaded/2
% , on_provider_unloaded/2
% , on_client_connect/2
% , on_client_connack/2
% , on_client_connected/2
% , on_client_disconnected/2
% , on_client_authenticate/2
% , on_client_authorize/2
% , on_client_subscribe/2
% , on_client_unsubscribe/2
% , on_session_created/2
% , on_session_subscribed/2
% , on_session_unsubscribed/2
% , on_session_resumed/2
% , on_session_discarded/2
% , on_session_takeovered/2
% , on_session_terminated/2
% , on_message_publish/2
% , on_message_delivered/2
% , on_message_dropped/2
% , on_message_acked/2
% ]).
% -define(PORT, 9000).
% -define(NAME, ?MODULE).
% %%--------------------------------------------------------------------
% %% Server APIs
% %%--------------------------------------------------------------------
% start() ->
% Pid = spawn(fun mngr_main/0),
% register(?MODULE, Pid),
% {ok, Pid}.
% stop() ->
% grpc:stop_server(?NAME),
% ?MODULE ! stop.
% take() ->
% ?MODULE ! {take, self()},
% receive {value, V} -> V
% after 5000 -> error(timeout) end.
% in({FunName, Req}) ->
% ?MODULE ! {in, FunName, Req}.
% mngr_main() ->
% application:ensure_all_started(grpc),
% Services = #{protos => [emqx_exhook_pb],
% services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr}
% },
% Options = [],
% Svr = grpc:start_server(?NAME, ?PORT, Services, Options),
% mngr_loop([Svr, queue:new(), queue:new()]).
% mngr_loop([Svr, Q, Takes]) ->
% receive
% {in, FunName, Req} ->
% {NQ1, NQ2} = reply(queue:in({FunName, Req}, Q), Takes),
% mngr_loop([Svr, NQ1, NQ2]);
% {take, From} ->
% {NQ1, NQ2} = reply(Q, queue:in(From, Takes)),
% mngr_loop([Svr, NQ1, NQ2]);
% stop ->
% exit(normal)
% end.
% reply(Q1, Q2) ->
% case queue:len(Q1) =:= 0 orelse
% queue:len(Q2) =:= 0 of
% true -> {Q1, Q2};
% _ ->
% {{value, {Name, V}}, NQ1} = queue:out(Q1),
% {{value, From}, NQ2} = queue:out(Q2),
% From ! {value, {Name, V}},
% {NQ1, NQ2}
% end.
% %%--------------------------------------------------------------------
% %% callbacks
% %%--------------------------------------------------------------------
% -spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_provider_loaded(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{hooks => [
% #{name => <<"client.connect">>},
% #{name => <<"client.connack">>},
% #{name => <<"client.connected">>},
% #{name => <<"client.disconnected">>},
% #{name => <<"client.authenticate">>},
% #{name => <<"client.authorize">>},
% #{name => <<"client.subscribe">>},
% #{name => <<"client.unsubscribe">>},
% #{name => <<"session.created">>},
% #{name => <<"session.subscribed">>},
% #{name => <<"session.unsubscribed">>},
% #{name => <<"session.resumed">>},
% #{name => <<"session.discarded">>},
% #{name => <<"session.takeovered">>},
% #{name => <<"session.terminated">>},
% #{name => <<"message.publish">>},
% #{name => <<"message.delivered">>},
% #{name => <<"message.acked">>},
% #{name => <<"message.dropped">>}]}, Md}.
% -spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_provider_unloaded(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_connect(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_connack(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_connected(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_disconnected(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% %% some cases for testing
% case Username of
% <<"baduser">> ->
% {ok, #{type => 'STOP_AND_RETURN',
% value => {bool_result, false}}, Md};
% <<"gooduser">> ->
% {ok, #{type => 'STOP_AND_RETURN',
% value => {bool_result, true}}, Md};
% <<"normaluser">> ->
% {ok, #{type => 'CONTINUE',
% value => {bool_result, true}}, Md};
% _ ->
% {ok, #{type => 'IGNORE'}, Md}
% end.
% -spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% %% some cases for testing
% case Username of
% <<"baduser">> ->
% {ok, #{type => 'STOP_AND_RETURN',
% value => {bool_result, false}}, Md};
% <<"gooduser">> ->
% {ok, #{type => 'STOP_AND_RETURN',
% value => {bool_result, true}}, Md};
% <<"normaluser">> ->
% {ok, #{type => 'CONTINUE',
% value => {bool_result, true}}, Md};
% _ ->
% {ok, #{type => 'IGNORE'}, Md}
% end.
% -spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_subscribe(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_client_unsubscribe(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_created(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_subscribed(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_unsubscribed(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_resumed(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_discarded(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_takeovered(emqx_exhook_pb:session_takeovered_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_takeovered(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_session_terminated(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_message_publish(#{message := #{from := From} = Msg} = Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% %% some cases for testing
% case From of
% <<"baduser">> ->
% NMsg = Msg#{qos => 0,
% topic => <<"">>,
% payload => <<"">>
% },
% {ok, #{type => 'STOP_AND_RETURN',
% value => {message, NMsg}}, Md};
% <<"gooduser">> ->
% NMsg = Msg#{topic => From,
% payload => From},
% {ok, #{type => 'STOP_AND_RETURN',
% value => {message, NMsg}}, Md};
% _ ->
% {ok, #{type => 'IGNORE'}, Md}
% end.
% -spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_message_delivered(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_message_dropped(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.
% -spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata())
% -> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
% | {error, grpc_cowboy_h:error_response()}.
% on_message_acked(Req, Md) ->
% ?MODULE:in({?FUNCTION_NAME, Req}),
% %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
% {ok, #{}, Md}.

View File

@ -16,7 +16,7 @@
-module(emqx_exproto).
-include("emqx_exproto.hrl").
-include("src/exproto/include/emqx_exproto.hrl").
-export([ start_listeners/0
, stop_listeners/0

View File

@ -15,8 +15,7 @@
%%--------------------------------------------------------------------
-module(emqx_exproto_channel).
-include("emqx_exproto.hrl").
-include("src/exproto/include/emqx_exproto.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/types.hrl").

View File

@ -17,9 +17,9 @@
%% The gRPC server for ConnectionAdapter
-module(emqx_exproto_gsvr).
-behavior(emqx_exproto_v_1_connection_adapter_bhvr).
% -behavior(emqx_exproto_v_1_connection_adapter_bhvr).
-include("emqx_exproto.hrl").
-include("src/exproto/include/emqx_exproto.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[ExProto gServer]").

View File

@ -0,0 +1,454 @@
%%--------------------------------------------------------------------
%% 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_exproto_SUITE).
% -compile(export_all).
% -compile(nowarn_export_all).
% -import(emqx_exproto_echo_svr,
% [ frame_connect/2
% , frame_connack/1
% , frame_publish/3
% , frame_puback/1
% , frame_subscribe/2
% , frame_suback/1
% , frame_unsubscribe/1
% , frame_unsuback/1
% , frame_disconnect/0
% ]).
% -include_lib("emqx/include/emqx.hrl").
% -include_lib("emqx/include/emqx_mqtt.hrl").
% -define(TCPOPTS, [binary, {active, false}]).
% -define(DTLSOPTS, [binary, {active, false}, {protocol, dtls}]).
% %%--------------------------------------------------------------------
% %% Setups
% %%--------------------------------------------------------------------
% all() ->
% [{group, Name} || Name <- metrics()].
% groups() ->
% Cases = emqx_ct:all(?MODULE),
% [{Name, Cases} || Name <- metrics()].
% %% @private
% metrics() ->
% [tcp, ssl, udp, dtls].
% init_per_group(GrpName, Cfg) ->
% put(grpname, GrpName),
% Svrs = emqx_exproto_echo_svr:start(),
% emqx_ct_helpers:start_apps([emqx_exproto], fun set_special_cfg/1),
% emqx_logger:set_log_level(debug),
% [{servers, Svrs}, {listener_type, GrpName} | Cfg].
% end_per_group(_, Cfg) ->
% emqx_ct_helpers:stop_apps([emqx_exproto]),
% emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)).
% set_special_cfg(emqx_exproto) ->
% LisType = get(grpname),
% Listeners = application:get_env(emqx_exproto, listeners, []),
% SockOpts = socketopts(LisType),
% UpgradeOpts = fun(Opts) ->
% Opts2 = lists:keydelete(tcp_options, 1, Opts),
% Opts3 = lists:keydelete(ssl_options, 1, Opts2),
% Opts4 = lists:keydelete(udp_options, 1, Opts3),
% Opts5 = lists:keydelete(dtls_options, 1, Opts4),
% SockOpts ++ Opts5
% end,
% NListeners = [{Proto, LisType, LisOn, UpgradeOpts(Opts)}
% || {Proto, _Type, LisOn, Opts} <- Listeners],
% application:set_env(emqx_exproto, listeners, NListeners);
% set_special_cfg(emqx) ->
% application:set_env(emqx, allow_anonymous, true),
% application:set_env(emqx, enable_acl_cache, false),
% ok.
% %%--------------------------------------------------------------------
% %% Tests cases
% %%--------------------------------------------------------------------
% t_start_stop(_) ->
% ok.
% t_mountpoint_echo(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>,
% mountpoint => <<"ct/">>
% },
% Password = <<"123456">>,
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(0),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% SubBin = frame_subscribe(<<"t/dn">>, 1),
% SubAckBin = frame_suback(0),
% send(Sock, SubBin),
% {ok, SubAckBin} = recv(Sock, 5000),
% emqx:publish(emqx_message:make(<<"ct/t/dn">>, <<"echo">>)),
% PubBin1 = frame_publish(<<"t/dn">>, 0, <<"echo">>),
% {ok, PubBin1} = recv(Sock, 5000),
% PubBin2 = frame_publish(<<"t/up">>, 0, <<"echo">>),
% PubAckBin = frame_puback(0),
% emqx:subscribe(<<"ct/t/up">>),
% send(Sock, PubBin2),
% {ok, PubAckBin} = recv(Sock, 5000),
% receive
% {deliver, _, _} -> ok
% after 1000 ->
% error(echo_not_running)
% end,
% close(Sock).
% t_auth_deny(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>
% },
% Password = <<"123456">>,
% ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
% ok = meck:expect(emqx_access_control, authenticate,
% fun(_) -> {error, ?RC_NOT_AUTHORIZED} end),
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(1),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% SockType =/= udp andalso begin
% {error, closed} = recv(Sock, 5000)
% end,
% meck:unload([emqx_access_control]).
% t_acl_deny(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>
% },
% Password = <<"123456">>,
% ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
% ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> deny end),
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(0),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% SubBin = frame_subscribe(<<"t/#">>, 1),
% SubAckBin = frame_suback(1),
% send(Sock, SubBin),
% {ok, SubAckBin} = recv(Sock, 5000),
% emqx:publish(emqx_message:make(<<"t/dn">>, <<"echo">>)),
% PubBin = frame_publish(<<"t/dn">>, 0, <<"echo">>),
% PubBinFailedAck = frame_puback(1),
% PubBinSuccesAck = frame_puback(0),
% send(Sock, PubBin),
% {ok, PubBinFailedAck} = recv(Sock, 5000),
% meck:unload([emqx_access_control]),
% send(Sock, PubBin),
% {ok, PubBinSuccesAck} = recv(Sock, 5000),
% close(Sock).
% t_keepalive_timeout(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>,
% keepalive => 2
% },
% Password = <<"123456">>,
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(0),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% DisconnectBin = frame_disconnect(),
% {ok, DisconnectBin} = recv(Sock, 10000),
% SockType =/= udp andalso begin
% {error, closed} = recv(Sock, 5000)
% end, ok.
% t_hook_connected_disconnected(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>
% },
% Password = <<"123456">>,
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(0),
% Parent = self(),
% emqx:hook('client.connected', {?MODULE, hook_fun1, [Parent]}),
% emqx:hook('client.disconnected',{?MODULE, hook_fun2, [Parent]}),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% receive
% connected -> ok
% after 1000 ->
% error(hook_is_not_running)
% end,
% DisconnectBin = frame_disconnect(),
% send(Sock, DisconnectBin),
% receive
% disconnected -> ok
% after 1000 ->
% error(hook_is_not_running)
% end,
% SockType =/= udp andalso begin
% {error, closed} = recv(Sock, 5000)
% end,
% emqx:unhook('client.connected', {?MODULE, hook_fun1}),
% emqx:unhook('client.disconnected', {?MODULE, hook_fun2}).
% t_hook_session_subscribed_unsubscribed(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>
% },
% Password = <<"123456">>,
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(0),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% Parent = self(),
% emqx:hook('session.subscribed', {?MODULE, hook_fun3, [Parent]}),
% emqx:hook('session.unsubscribed', {?MODULE, hook_fun4, [Parent]}),
% SubBin = frame_subscribe(<<"t/#">>, 1),
% SubAckBin = frame_suback(0),
% send(Sock, SubBin),
% {ok, SubAckBin} = recv(Sock, 5000),
% receive
% subscribed -> ok
% after 1000 ->
% error(hook_is_not_running)
% end,
% UnsubBin = frame_unsubscribe(<<"t/#">>),
% UnsubAckBin = frame_unsuback(0),
% send(Sock, UnsubBin),
% {ok, UnsubAckBin} = recv(Sock, 5000),
% receive
% unsubscribed -> ok
% after 1000 ->
% error(hook_is_not_running)
% end,
% close(Sock),
% emqx:unhook('session.subscribed', {?MODULE, hook_fun3}),
% emqx:unhook('session.unsubscribed', {?MODULE, hook_fun4}).
% t_hook_message_delivered(Cfg) ->
% SockType = proplists:get_value(listener_type, Cfg),
% Sock = open(SockType),
% Client = #{proto_name => <<"demo">>,
% proto_ver => <<"v0.1">>,
% clientid => <<"test_client_1">>
% },
% Password = <<"123456">>,
% ConnBin = frame_connect(Client, Password),
% ConnAckBin = frame_connack(0),
% send(Sock, ConnBin),
% {ok, ConnAckBin} = recv(Sock, 5000),
% SubBin = frame_subscribe(<<"t/#">>, 1),
% SubAckBin = frame_suback(0),
% send(Sock, SubBin),
% {ok, SubAckBin} = recv(Sock, 5000),
% emqx:hook('message.delivered', {?MODULE, hook_fun5, []}),
% emqx:publish(emqx_message:make(<<"t/dn">>, <<"1">>)),
% PubBin1 = frame_publish(<<"t/dn">>, 0, <<"2">>),
% {ok, PubBin1} = recv(Sock, 5000),
% close(Sock),
% emqx:unhook('message.delivered', {?MODULE, hook_fun5}).
% %%--------------------------------------------------------------------
% %% Utils
% hook_fun1(_, _, Parent) -> Parent ! connected, ok.
% hook_fun2(_, _, _, Parent) -> Parent ! disconnected, ok.
% hook_fun3(_, _, _, Parent) -> Parent ! subscribed, ok.
% hook_fun4(_, _, _, Parent) -> Parent ! unsubscribed, ok.
% hook_fun5(_, Msg) -> {ok, Msg#message{payload = <<"2">>}}.
% rand_bytes() ->
% crypto:strong_rand_bytes(rand:uniform(256)).
% %%--------------------------------------------------------------------
% %% Sock funcs
% open(tcp) ->
% {ok, Sock} = gen_tcp:connect("127.0.0.1", 7993, ?TCPOPTS),
% {tcp, Sock};
% open(udp) ->
% {ok, Sock} = gen_udp:open(0, ?TCPOPTS),
% {udp, Sock};
% open(ssl) ->
% SslOpts = client_ssl_opts(),
% {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?TCPOPTS ++ SslOpts),
% {ssl, SslSock};
% open(dtls) ->
% SslOpts = client_ssl_opts(),
% {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?DTLSOPTS ++ SslOpts),
% {dtls, SslSock}.
% send({tcp, Sock}, Bin) ->
% gen_tcp:send(Sock, Bin);
% send({udp, Sock}, Bin) ->
% gen_udp:send(Sock, "127.0.0.1", 7993, Bin);
% send({ssl, Sock}, Bin) ->
% ssl:send(Sock, Bin);
% send({dtls, Sock}, Bin) ->
% ssl:send(Sock, Bin).
% recv({tcp, Sock}, Ts) ->
% gen_tcp:recv(Sock, 0, Ts);
% recv({udp, Sock}, Ts) ->
% {ok, {_, _, Bin}} = gen_udp:recv(Sock, 0, Ts),
% {ok, Bin};
% recv({ssl, Sock}, Ts) ->
% ssl:recv(Sock, 0, Ts);
% recv({dtls, Sock}, Ts) ->
% ssl:recv(Sock, 0, Ts).
% close({tcp, Sock}) ->
% gen_tcp:close(Sock);
% close({udp, Sock}) ->
% gen_udp:close(Sock);
% close({ssl, Sock}) ->
% ssl:close(Sock);
% close({dtls, Sock}) ->
% ssl:close(Sock).
% %%--------------------------------------------------------------------
% %% Server-Opts
% socketopts(tcp) ->
% [{tcp_options, tcp_opts()}];
% socketopts(ssl) ->
% [{tcp_options, tcp_opts()},
% {ssl_options, ssl_opts()}];
% socketopts(udp) ->
% [{udp_options, udp_opts()}];
% socketopts(dtls) ->
% [{udp_options, udp_opts()},
% {dtls_options, dtls_opts()}].
% tcp_opts() ->
% [{send_timeout, 15000},
% {send_timeout_close, true},
% {backlog, 100},
% {nodelay, true} | udp_opts()].
% udp_opts() ->
% [{recbuf, 1024},
% {sndbuf, 1024},
% {buffer, 1024},
% {reuseaddr, true}].
% ssl_opts() ->
% Certs = certs("key.pem", "cert.pem", "cacert.pem"),
% [{versions, emqx_tls_lib:default_versions()},
% {ciphers, emqx_tls_lib:default_ciphers()},
% {verify, verify_peer},
% {fail_if_no_peer_cert, true},
% {secure_renegotiate, false},
% {reuse_sessions, true},
% {honor_cipher_order, true}]++Certs.
% dtls_opts() ->
% Opts = ssl_opts(),
% lists:keyreplace(versions, 1, Opts, {versions, ['dtlsv1.2', 'dtlsv1']}).
% %%--------------------------------------------------------------------
% %% Client-Opts
% client_ssl_opts() ->
% certs( "client-key.pem", "client-cert.pem", "cacert.pem" ).
% certs( Key, Cert, CACert ) ->
% CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"),
% [ { keyfile, filename:join([ CertsPath, Key ]) },
% { certfile, filename:join([ CertsPath, Cert ]) },
% { cacertfile, filename:join([ CertsPath, CACert ]) } ].

View File

@ -0,0 +1,278 @@
%%--------------------------------------------------------------------
%% 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_exproto_echo_svr).
% -behavior(emqx_exproto_v_1_connection_handler_bhvr).
% -export([ start/0
% , stop/1
% ]).
% -export([ frame_connect/2
% , frame_connack/1
% , frame_publish/3
% , frame_puback/1
% , frame_subscribe/2
% , frame_suback/1
% , frame_unsubscribe/1
% , frame_unsuback/1
% , frame_disconnect/0
% ]).
% -export([ on_socket_created/2
% , on_received_bytes/2
% , on_socket_closed/2
% , on_timer_timeout/2
% , on_received_messages/2
% ]).
% -define(LOG(Fmt, Args), io:format(standard_error, Fmt, Args)).
% -define(HTTP, #{grpc_opts => #{service_protos => [emqx_exproto_pb],
% services => #{'emqx.exproto.v1.ConnectionHandler' => ?MODULE}},
% listen_opts => #{port => 9001,
% socket_options => []},
% pool_opts => #{size => 8},
% transport_opts => #{ssl => false}}).
% -define(CLIENT, emqx_exproto_v_1_connection_adapter_client).
% -define(send(Req), ?CLIENT:send(Req, #{channel => ct_test_channel})).
% -define(close(Req), ?CLIENT:close(Req, #{channel => ct_test_channel})).
% -define(authenticate(Req), ?CLIENT:authenticate(Req, #{channel => ct_test_channel})).
% -define(start_timer(Req), ?CLIENT:start_timer(Req, #{channel => ct_test_channel})).
% -define(publish(Req), ?CLIENT:publish(Req, #{channel => ct_test_channel})).
% -define(subscribe(Req), ?CLIENT:subscribe(Req, #{channel => ct_test_channel})).
% -define(unsubscribe(Req), ?CLIENT:unsubscribe(Req, #{channel => ct_test_channel})).
% -define(TYPE_CONNECT, 1).
% -define(TYPE_CONNACK, 2).
% -define(TYPE_PUBLISH, 3).
% -define(TYPE_PUBACK, 4).
% -define(TYPE_SUBSCRIBE, 5).
% -define(TYPE_SUBACK, 6).
% -define(TYPE_UNSUBSCRIBE, 7).
% -define(TYPE_UNSUBACK, 8).
% -define(TYPE_DISCONNECT, 9).
% -define(loop_recv_and_reply_empty_success(Stream),
% ?loop_recv_and_reply_empty_success(Stream, fun(_) -> ok end)).
% -define(loop_recv_and_reply_empty_success(Stream, Fun),
% begin
% LoopRecv = fun _Lp(_St) ->
% case grpc_stream:recv(_St) of
% {more, _Reqs, _NSt} ->
% ?LOG("~p: ~p~n", [?FUNCTION_NAME, _Reqs]),
% Fun(_Reqs), _Lp(_NSt);
% {eos, _Reqs, _NSt} ->
% ?LOG("~p: ~p~n", [?FUNCTION_NAME, _Reqs]),
% Fun(_Reqs), _NSt
% end
% end,
% NStream = LoopRecv(Stream),
% grpc_stream:reply(NStream, #{}),
% {ok, NStream}
% end).
% %%--------------------------------------------------------------------
% %% APIs
% %%--------------------------------------------------------------------
% start() ->
% application:ensure_all_started(grpc),
% [start_channel(), start_server()].
% start_channel() ->
% grpc_client_sup:create_channel_pool(ct_test_channel, "http://127.0.0.1:9100", #{}).
% start_server() ->
% Services = #{protos => [emqx_exproto_pb],
% services => #{'emqx.exproto.v1.ConnectionHandler' => ?MODULE}
% },
% Options = [],
% grpc:start_server(?MODULE, 9001, Services, Options).
% stop([_ChannPid, _SvrPid]) ->
% grpc:stop_server(?MODULE),
% grpc_client_sup:stop_channel_pool(ct_test_channel).
% %%--------------------------------------------------------------------
% %% Protocol Adapter callbacks
% %%--------------------------------------------------------------------
% -spec on_socket_created(grpc_stream:stream(), grpc:metadata())
% -> {ok, grpc_stream:stream()}.
% on_socket_created(Stream, _Md) ->
% ?loop_recv_and_reply_empty_success(Stream).
% -spec on_socket_closed(grpc_stream:stream(), grpc:metadata())
% -> {ok, grpc_stream:stream()}.
% on_socket_closed(Stream, _Md) ->
% ?loop_recv_and_reply_empty_success(Stream).
% -spec on_received_bytes(grpc_stream:stream(), grpc:metadata())
% -> {ok, grpc_stream:stream()}.
% on_received_bytes(Stream, _Md) ->
% ?loop_recv_and_reply_empty_success(Stream,
% fun(Reqs) ->
% lists:foreach(
% fun(#{conn := Conn, bytes := Bytes}) ->
% #{<<"type">> := Type} = Params = emqx_json:decode(Bytes, [return_maps]),
% _ = handle_in(Conn, Type, Params)
% end, Reqs)
% end).
% -spec on_timer_timeout(grpc_stream:stream(), grpc:metadata())
% -> {ok, grpc_stream:stream()}.
% on_timer_timeout(Stream, _Md) ->
% ?loop_recv_and_reply_empty_success(Stream,
% fun(Reqs) ->
% lists:foreach(
% fun(#{conn := Conn, type := 'KEEPALIVE'}) ->
% ?LOG("Close this connection ~p due to keepalive timeout", [Conn]),
% handle_out(Conn, ?TYPE_DISCONNECT),
% ?close(#{conn => Conn})
% end, Reqs)
% end).
% -spec on_received_messages(grpc_stream:stream(), grpc:metadata())
% -> {ok, grpc_stream:stream()}.
% on_received_messages(Stream, _Md) ->
% ?loop_recv_and_reply_empty_success(Stream,
% fun(Reqs) ->
% lists:foreach(
% fun(#{conn := Conn, messages := Messages}) ->
% lists:foreach(fun(Message) ->
% handle_out(Conn, ?TYPE_PUBLISH, Message)
% end, Messages)
% end, Reqs)
% end).
% %%--------------------------------------------------------------------
% %% The Protocol Example:
% %% CONN:
% %% {"type": 1, "clientinfo": {...}}
% %%
% %% CONNACK:
% %% {"type": 2, "code": 0}
% %%
% %% PUBLISH:
% %% {"type": 3, "topic": "xxx", "payload": "", "qos": 0}
% %%
% %% PUBACK:
% %% {"type": 4, "code": 0}
% %%
% %% SUBSCRIBE:
% %% {"type": 5, "topic": "xxx", "qos": 1}
% %%
% %% SUBACK:
% %% {"type": 6, "code": 0}
% %%
% %% DISCONNECT:
% %% {"type": 7, "code": 1}
% %%--------------------------------------------------------------------
% handle_in(Conn, ?TYPE_CONNECT, #{<<"clientinfo">> := ClientInfo, <<"password">> := Password}) ->
% NClientInfo = maps:from_list([{binary_to_atom(K, utf8), V} || {K, V} <- maps:to_list(ClientInfo)]),
% case ?authenticate(#{conn => Conn, clientinfo => NClientInfo, password => Password}) of
% {ok, #{code := 'SUCCESS'}, _} ->
% case maps:get(keepalive, NClientInfo, 0) of
% 0 -> ok;
% Intv ->
% io:format("Try call start_timer with ~ps", [Intv]),
% ?start_timer(#{conn => Conn, type => 'KEEPALIVE', interval => Intv})
% end,
% handle_out(Conn, ?TYPE_CONNACK, 0);
% _ ->
% handle_out(Conn, ?TYPE_CONNACK, 1),
% ?close(#{conn => Conn})
% end;
% handle_in(Conn, ?TYPE_PUBLISH, #{<<"topic">> := Topic,
% <<"qos">> := Qos,
% <<"payload">> := Payload}) ->
% case ?publish(#{conn => Conn, topic => Topic, qos => Qos, payload => Payload}) of
% {ok, #{code := 'SUCCESS'}, _} ->
% handle_out(Conn, ?TYPE_PUBACK, 0);
% _ ->
% handle_out(Conn, ?TYPE_PUBACK, 1)
% end;
% handle_in(Conn, ?TYPE_SUBSCRIBE, #{<<"qos">> := Qos, <<"topic">> := Topic}) ->
% case ?subscribe(#{conn => Conn, topic => Topic, qos => Qos}) of
% {ok, #{code := 'SUCCESS'}, _} ->
% handle_out(Conn, ?TYPE_SUBACK, 0);
% _ ->
% handle_out(Conn, ?TYPE_SUBACK, 1)
% end;
% handle_in(Conn, ?TYPE_UNSUBSCRIBE, #{<<"topic">> := Topic}) ->
% case ?unsubscribe(#{conn => Conn, topic => Topic}) of
% {ok, #{code := 'SUCCESS'}, _} ->
% handle_out(Conn, ?TYPE_UNSUBACK, 0);
% _ ->
% handle_out(Conn, ?TYPE_UNSUBACK, 1)
% end;
% handle_in(Conn, ?TYPE_DISCONNECT, _) ->
% ?close(#{conn => Conn}).
% handle_out(Conn, ?TYPE_CONNACK, Code) ->
% ?send(#{conn => Conn, bytes => frame_connack(Code)});
% handle_out(Conn, ?TYPE_PUBACK, Code) ->
% ?send(#{conn => Conn, bytes => frame_puback(Code)});
% handle_out(Conn, ?TYPE_SUBACK, Code) ->
% ?send(#{conn => Conn, bytes => frame_suback(Code)});
% handle_out(Conn, ?TYPE_UNSUBACK, Code) ->
% ?send(#{conn => Conn, bytes => frame_unsuback(Code)});
% handle_out(Conn, ?TYPE_PUBLISH, #{qos := Qos, topic := Topic, payload := Payload}) ->
% ?send(#{conn => Conn, bytes => frame_publish(Topic, Qos, Payload)}).
% handle_out(Conn, ?TYPE_DISCONNECT) ->
% ?send(#{conn => Conn, bytes => frame_disconnect()}).
% %%--------------------------------------------------------------------
% %% Frame
% frame_connect(ClientInfo, Password) ->
% emqx_json:encode(#{type => ?TYPE_CONNECT,
% clientinfo => ClientInfo,
% password => Password}).
% frame_connack(Code) ->
% emqx_json:encode(#{type => ?TYPE_CONNACK, code => Code}).
% frame_publish(Topic, Qos, Payload) ->
% emqx_json:encode(#{type => ?TYPE_PUBLISH,
% topic => Topic,
% qos => Qos,
% payload => Payload}).
% frame_puback(Code) ->
% emqx_json:encode(#{type => ?TYPE_PUBACK, code => Code}).
% frame_subscribe(Topic, Qos) ->
% emqx_json:encode(#{type => ?TYPE_SUBSCRIBE, topic => Topic, qos => Qos}).
% frame_suback(Code) ->
% emqx_json:encode(#{type => ?TYPE_SUBACK, code => Code}).
% frame_unsubscribe(Topic) ->
% emqx_json:encode(#{type => ?TYPE_UNSUBSCRIBE, topic => Topic}).
% frame_unsuback(Code) ->
% emqx_json:encode(#{type => ?TYPE_UNSUBACK, code => Code}).
% frame_disconnect() ->
% emqx_json:encode(#{type => ?TYPE_DISCONNECT}).

View File

@ -25,8 +25,7 @@
, prep_stop/1
]).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
start(_Type, _Args) ->
Pid = emqx_lwm2m_sup:start_link(),

View File

@ -16,7 +16,7 @@
-module(emqx_lwm2m_cmd_handler).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-include_lib("lwm2m_coap/include/coap.hrl").

View File

@ -22,7 +22,7 @@
-include_lib("lwm2m_coap/include/coap.hrl").
-behaviour(lwm2m_coap_resource).
% -behaviour(lwm2m_coap_resource).
-export([ coap_discover/2
, coap_get/5
@ -41,7 +41,7 @@
-export([parse_object_list/1]).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-define(PREFIX, <<"rd">>).

View File

@ -16,7 +16,7 @@
-module(emqx_lwm2m_coap_server).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-export([ start/1
, stop/1

View File

@ -22,7 +22,7 @@
, opaque_to_json/2
]).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)).

View File

@ -23,7 +23,7 @@
, translate_json/1
]).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)).

View File

@ -16,7 +16,7 @@
-module(emqx_lwm2m_protocol).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-include_lib("emqx/include/emqx.hrl").

View File

@ -16,7 +16,7 @@
-module(emqx_lwm2m_timer).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-export([ cancel_timer/1
, start_timer/2

View File

@ -25,7 +25,7 @@
-export([binary_to_hex_string/1]).
-endif.
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-define(LOG(Level, Format, Args), logger:Level("LWM2M-TLV: " ++ Format, Args)).

View File

@ -16,7 +16,7 @@
-module(emqx_lwm2m_xml_object).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-include_lib("xmerl/include/xmerl.hrl").
-export([ get_obj_def/2

View File

@ -16,7 +16,7 @@
-module(emqx_lwm2m_xml_object_db).
-include("emqx_lwm2m.hrl").
-include("src/lwm2m/include/emqx_lwm2m.hrl").
-include_lib("xmerl/include/xmerl.hrl").
% This module is for future use. Disabled now.

Some files were not shown because too many files have changed in this diff Show More