Pārlūkot izejas kodu

chore(merge): merge code from master

Shawn 4 gadi atpakaļ
vecāks
revīzija
4d279e47d8
100 mainītis faili ar 2744 papildinājumiem un 3097 dzēšanām
  1. 0 14
      .ci/docker-compose-file/docker-compose-emqx-cluster.yaml
  2. 1 0
      .ci/fvt_tests/relup.lux
  3. 0 1
      apps/emqx/rebar.config
  4. 0 25
      apps/emqx_coap/.gitignore
  5. 0 13
      apps/emqx_coap/TODO
  6. BIN
      apps/emqx_coap/docs/rfc7049.pdf
  7. BIN
      apps/emqx_coap/docs/rfc7228.pdf
  8. BIN
      apps/emqx_coap/docs/rfc7252.pdf
  9. 0 129
      apps/emqx_coap/intergration_test/Makefile
  10. 0 8
      apps/emqx_coap/intergration_test/README.md
  11. 0 52
      apps/emqx_coap/intergration_test/check_result.py
  12. 0 4
      apps/emqx_coap/rebar.config
  13. 0 677
      apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl
  14. 0 29
      apps/emqx_exhook/.gitignore
  15. 0 116
      apps/emqx_exhook/docs/design-cn.md
  16. 0 48
      apps/emqx_exhook/rebar.config
  17. 0 23
      apps/emqx_exhook/src/emqx_exhook.appup.src
  18. 0 96
      apps/emqx_exhook/test/emqx_exhook_SUITE.erl
  19. 0 339
      apps/emqx_exhook/test/emqx_exhook_demo_svr.erl
  20. 0 531
      apps/emqx_exhook/test/props/prop_exhook_hooks.erl
  21. 0 48
      apps/emqx_exproto/.gitignore
  22. 0 127
      apps/emqx_exproto/docs/design-cn.md
  23. BIN
      apps/emqx_exproto/docs/images/exproto-arch.jpg
  24. BIN
      apps/emqx_exproto/docs/images/exproto-grpc-arch.jpg
  25. 0 51
      apps/emqx_exproto/rebar.config
  26. 0 454
      apps/emqx_exproto/test/emqx_exproto_SUITE.erl
  27. 0 278
      apps/emqx_exproto/test/emqx_exproto_echo_svr.erl
  28. 0 0
      apps/emqx_gateway/etc/emqx_coap.conf
  29. 0 0
      apps/emqx_gateway/etc/emqx_exhook.conf
  30. 0 0
      apps/emqx_gateway/etc/emqx_exproto.conf
  31. 0 0
      apps/emqx_gateway/etc/emqx_lwm2m.conf
  32. 0 0
      apps/emqx_gateway/etc/priv/emqx_coap.schema
  33. 0 0
      apps/emqx_gateway/etc/priv/emqx_exhook.schema
  34. 0 0
      apps/emqx_gateway/etc/priv/emqx_exproto.schema
  35. 0 0
      apps/emqx_gateway/etc/priv/emqx_lwm2m.schema
  36. 0 0
      apps/emqx_gateway/etc/priv/exhook.proto
  37. 0 0
      apps/emqx_gateway/etc/priv/exproto.proto
  38. 16 1
      apps/emqx_gateway/rebar.config
  39. 0 0
      apps/emqx_gateway/src/coap/README.md
  40. 0 0
      apps/emqx_gateway/src/coap/emqx_coap.app.src
  41. 1 1
      apps/emqx_coap/src/emqx_coap_app.erl
  42. 1 1
      apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl
  43. 2 2
      apps/emqx_coap/src/emqx_coap_pubsub_resource.erl
  44. 1 1
      apps/emqx_coap/src/emqx_coap_pubsub_topics.erl
  45. 1 1
      apps/emqx_coap/src/emqx_coap_registry.erl
  46. 2 2
      apps/emqx_coap/src/emqx_coap_resource.erl
  47. 1 1
      apps/emqx_coap/src/emqx_coap_server.erl
  48. 0 0
      apps/emqx_gateway/src/coap/emqx_coap_sup.erl
  49. 1 1
      apps/emqx_coap/src/emqx_coap_timer.erl
  50. 0 0
      apps/emqx_gateway/src/coap/include/emqx_coap.hrl
  51. 319 0
      apps/emqx_gateway/src/coap/test/emqx_coap_SUITE.erl
  52. 678 0
      apps/emqx_gateway/src/coap/test/emqx_coap_pubsub_SUITE.erl
  53. 0 0
      apps/emqx_gateway/src/exhook/README.md
  54. 0 0
      apps/emqx_gateway/src/exhook/emqx_exhook.app.src
  55. 1 1
      apps/emqx_exhook/src/emqx_exhook.erl
  56. 1 1
      apps/emqx_exhook/src/emqx_exhook_app.erl
  57. 1 1
      apps/emqx_exhook/src/emqx_exhook_cli.erl
  58. 1 1
      apps/emqx_exhook/src/emqx_exhook_handler.erl
  59. 1 1
      apps/emqx_exhook/src/emqx_exhook_server.erl
  60. 0 0
      apps/emqx_gateway/src/exhook/emqx_exhook_sup.erl
  61. 0 0
      apps/emqx_gateway/src/exhook/include/emqx_exhook.hrl
  62. 531 0
      apps/emqx_gateway/src/exhook/prop_exhook_hooks.erl
  63. 97 0
      apps/emqx_gateway/src/exhook/test/emqx_exhook_SUITE.erl
  64. 339 0
      apps/emqx_gateway/src/exhook/test/emqx_exhook_demo_svr.erl
  65. 0 0
      apps/emqx_gateway/src/exproto/README.md
  66. 0 0
      apps/emqx_gateway/src/exproto/emqx_exproto.app.src
  67. 1 1
      apps/emqx_exproto/src/emqx_exproto.erl
  68. 0 0
      apps/emqx_gateway/src/exproto/emqx_exproto_app.erl
  69. 1 2
      apps/emqx_exproto/src/emqx_exproto_channel.erl
  70. 0 0
      apps/emqx_gateway/src/exproto/emqx_exproto_conn.erl
  71. 0 0
      apps/emqx_gateway/src/exproto/emqx_exproto_gcli.erl
  72. 2 2
      apps/emqx_exproto/src/emqx_exproto_gsvr.erl
  73. 0 0
      apps/emqx_gateway/src/exproto/emqx_exproto_sup.erl
  74. 0 0
      apps/emqx_gateway/src/exproto/include/emqx_exproto.hrl
  75. 454 0
      apps/emqx_gateway/src/exproto/test/emqx_exproto_SUITE.erl
  76. 278 0
      apps/emqx_gateway/src/exproto/test/emqx_exproto_echo_svr.erl
  77. 0 0
      apps/emqx_gateway/src/lwm2m/.gitignore
  78. 0 0
      apps/emqx_gateway/src/lwm2m/README.md
  79. 0 0
      apps/emqx_gateway/src/lwm2m/binary_util.erl
  80. 0 0
      apps/emqx_gateway/src/lwm2m/emqx_lwm2m.app.src
  81. 0 0
      apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl
  82. 1 2
      apps/emqx_lwm2m/src/emqx_lwm2m_app.erl
  83. 0 0
      apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl
  84. 0 0
      apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm_sup.erl
  85. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_cmd_handler.erl
  86. 2 2
      apps/emqx_lwm2m/src/emqx_lwm2m_coap_resource.erl
  87. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_coap_server.erl
  88. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_json.erl
  89. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_message.erl
  90. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl
  91. 0 0
      apps/emqx_gateway/src/lwm2m/emqx_lwm2m_sup.erl
  92. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_timer.erl
  93. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_tlv.erl
  94. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_xml_object.erl
  95. 1 1
      apps/emqx_lwm2m/src/emqx_lwm2m_xml_object_db.erl
  96. 0 0
      apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl
  97. 0 0
      apps/emqx_gateway/src/lwm2m/lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml
  98. 0 0
      apps/emqx_gateway/src/lwm2m/lwm2m_xml/LWM2M_Connectivity_Statistics-v1_0_1.xml
  99. 0 0
      apps/emqx_gateway/src/lwm2m/lwm2m_xml/LWM2M_Device-v1_0_1.xml
  100. 0 0
      apps/emqx_lwm2m/lwm2m_xml/LWM2M_Firmware_Update-v1_0_1.xml

+ 0 - 14
.ci/docker-compose-file/docker-compose-emqx-cluster.yaml

@@ -33,13 +33,6 @@ services:
       - conf.cluster.env
     environment:
       - "EMQX_HOST=node1.emqx.io"
-    command:
-        - /bin/sh
-        - -c
-        - |
-          sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf
-          # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins
-          /opt/emqx/bin/emqx foreground
     healthcheck:
       test: ["CMD", "/opt/emqx/bin/emqx_ctl", "status"]
       interval: 5s
@@ -57,13 +50,6 @@ services:
       - conf.cluster.env
     environment:
       - "EMQX_HOST=node2.emqx.io"
-    command:
-      - /bin/sh
-      - -c
-      - |
-        sed -i "s 127.0.0.1 $$(ip route show |grep "link" |awk '{print $$1}') g" /opt/emqx/etc/acl.conf
-        # sed -i '/emqx_telemetry/d' /opt/emqx/data/loaded_plugins
-        /opt/emqx/bin/emqx foreground
     healthcheck:
       test: ["CMD", "/opt/emqx/bin/emqx", "ping"]
       interval: 5s

+ 1 - 0
.ci/fvt_tests/relup.lux

@@ -131,6 +131,7 @@
 [shell bench]
     ???publish complete
     ??SH-PROMPT:
+    !sleep 5
     !curl http://127.0.0.1:8080/counter
     ???{"data":300,"code":0}
     ?SH-PROMPT

+ 0 - 1
apps/emqx/rebar.config

@@ -15,7 +15,6 @@
     , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}}
     , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.3"}}}
     , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
-    , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v4.0.1"}}} %% todo delete when plugins use hocon
     , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.10.3"}}}
     , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
     , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}

+ 0 - 25
apps/emqx_coap/.gitignore

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

+ 0 - 13
apps/emqx_coap/TODO

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

BIN
apps/emqx_coap/docs/rfc7049.pdf


BIN
apps/emqx_coap/docs/rfc7228.pdf


BIN
apps/emqx_coap/docs/rfc7252.pdf


+ 0 - 129
apps/emqx_coap/intergration_test/Makefile

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

+ 0 - 8
apps/emqx_coap/intergration_test/README.md

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

+ 0 - 52
apps/emqx_coap/intergration_test/check_result.py

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

+ 0 - 4
apps/emqx_coap/rebar.config

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

+ 0 - 677
apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl

@@ -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 successfully,got {ok, nocontent} Method
-    Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
-    Reply1 = {ok, Pid, _N, nocontent, _} = er_coap_observer:observe(Uri),
-    ?LOGT("Subscribe Reply=~p", [Reply1]), 
-
-    [SubPid] = emqx:subscribers(Topic),
-    ?assert(is_pid(SubPid)),
-
-    %% put to publish to topic "a/b"
-    Reply2 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
-    {ok,changed, #coap_content{}} = Reply2,
-    [{Topic, MaxAge1, CT, Payload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
-    ?assertEqual(60, MaxAge1),
-    ?assertEqual(<<"42">>, CT),
-    ?assertEqual(false, TimeStamp =:= timeout),
-
-    %% Publish a message
-    emqx:publish(emqx_message:make(Topic, Payload)),
-
-    Notif = receive_notification(),
-    ?LOGT("observer get Notif=~p", [Notif]),
-    {coap_notify, _, _, {ok,content}, #coap_content{payload = Payload}} = Notif,
-
-    er_coap_observer:stop(Pid),
-    {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
-
-t_case03_subscribe(_Config) ->
-    %% Subscribe to the unexisted topic "a/b", got not_found
-    Topic = <<"a/b">>,
-    TopicStr = binary_to_list(Topic),
-    PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
-    Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
-    {error, not_found} = er_coap_observer:observe(Uri),
-
-    [] = emqx:subscribers(Topic).
-
-t_case04_subscribe(_Config) ->
-    %% Subscribe to the wildcad topic "+/b", got bad_request
-    Topic = <<"+/b">>,
-    TopicStr = binary_to_list(Topic),
-    PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
-    Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
-    {error, bad_request} = er_coap_observer:observe(Uri),
-
-    [] = emqx:subscribers(Topic).
-
-t_case01_read(_Config) ->
-    Topic = <<"topic1">>,
-    TopicStr = binary_to_list(Topic),
-    Payload = <<"PubPayload">>,
-    timer:sleep(100),
-
-    %% First post to create a topic "topic1"
-    Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
-    Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
-    ?LOGT("Reply =~p", [Reply]),
-    {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
-    ?assertEqual(<<"/ps/topic1">> ,LocPath),
-    TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
-    ?LOGT("lookup topic info=~p", [TopicInfo]),
-    ?assertEqual(60, MaxAge1),
-    ?assertEqual(<<"42">>, CT1),
-
-    %% GET to read the publish message of the topic
-    timer:sleep(1000),
-    Reply1 = er_coap_client:request(get, Uri),
-    ?LOGT("Reply=~p", [Reply1]),
-    {ok,content, #coap_content{payload = Payload}} = Reply1,
-
-    {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
-
-t_case02_read(_Config) ->
-    Topic = <<"topic1">>,
-    TopicStr = binary_to_list(Topic),
-    Payload = <<"PubPayload">>,
-    timer:sleep(100),
-
-    %% First post to publish a topic "topic1"
-    Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
-    Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
-    ?LOGT("Reply =~p", [Reply]),
-    {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
-    ?assertEqual(<<"/ps/topic1">> ,LocPath),
-    TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
-    ?LOGT("lookup topic info=~p", [TopicInfo]),
-    ?assertEqual(60, MaxAge1),
-    ?assertEqual(<<"42">>, CT1),
-
-    %% GET to read the publish message of unmatched format, got bad_request
-    Reply1 = er_coap_client:request(get, Uri, #coap_content{format = <<"application/json">>}),
-    ?LOGT("Reply=~p", [Reply1]),
-    {error, bad_request} = Reply1,
-
-    {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
-
-t_case03_read(_Config) ->
-    Topic = <<"topic1">>,
-    TopicStr = binary_to_list(Topic),
-    Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
-    timer:sleep(100),
-
-    %% GET to read the nexisted topic "topic1", got not_found
-    Reply = er_coap_client:request(get, Uri),
-    ?LOGT("Reply=~p", [Reply]),
-    {error, not_found} = Reply.
-
-t_case04_read(_Config) ->
-    Topic = <<"topic1">>,
-    TopicStr = binary_to_list(Topic),
-    Payload = <<"PubPayload">>,
-    timer:sleep(100),
-
-    %% First post to publish a topic "topic1"
-    Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
-    Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
-    ?LOGT("Reply =~p", [Reply]),
-    {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
-    ?assertEqual(<<"/ps/topic1">> ,LocPath),
-    TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
-    ?LOGT("lookup topic info=~p", [TopicInfo]),
-    ?assertEqual(60, MaxAge1),
-    ?assertEqual(<<"42">>, CT1),
-
-    %% GET to read the publish message of wildcard topic, got bad_request
-    WildTopic = binary_to_list(<<"+/topic1">>),
-    Uri1 = "coap://127.0.0.1/ps/"++WildTopic++"?c=client1&u=tom&p=secret",
-    Reply1 = er_coap_client:request(get, Uri1, #coap_content{format = <<"application/json">>}),
-    ?LOGT("Reply=~p", [Reply1]),
-    {error, bad_request} = Reply1,
-
-    {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
-
-t_case05_read(_Config) ->
-    Topic = <<"a/b">>,
-    TopicStr = binary_to_list(Topic),
-    PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
-    Payload = <<"payload">>,
-
-    %% post to publish a new topic "a/b", and the topic is created
-    URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
-    Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
-    ?LOGT("Reply =~p", [Reply]),
-    {ok,created, #coap_content{location_path = LocPath}} = Reply,
-    ?assertEqual([<<"/ps/a/b">>] ,LocPath),
-    [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
-    ?assertEqual(5, MaxAge),
-    ?assertEqual(<<"42">>, CT),
-
-    %% Wait for the max age of the timer expires
-    timer:sleep(6000),
-    ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
-
-    %% GET to read the expired publish message, supposed to get {ok, nocontent}, but now got {ok, content}
-    Reply1 = er_coap_client:request(get, URI),
-    ?LOGT("Reply=~p", [Reply1]),
-    {ok, content, #coap_content{payload = <<>>}}= Reply1,
-
-    {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
-
-t_case01_delete(_Config) ->
-    TopicInPayload = <<"a/b">>,
-    TopicStr = binary_to_list(TopicInPayload),
-    PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
-    Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"),
-    URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
-
-    %% Client post to CREATE topic "a/b"
-    Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
-    ?LOGT("Reply =~p", [Reply]),
-    {ok,created, #coap_content{location_path = LocPath}} = Reply,
-    ?assertEqual([<<"/ps/a/b">>] ,LocPath),
-
-    %% Client post to CREATE topic "a/b/c"
-    TopicInPayload1 = <<"a/b/c">>,
-    PercentEncodedTopic1 = emqx_http_lib:uri_encode(binary_to_list(TopicInPayload1)),
-    Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"),
-    Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
-    ?LOGT("Reply =~p", [Reply1]),
-    {ok,created, #coap_content{location_path = LocPath1}} = Reply1,
-    ?assertEqual([<<"/ps/a/b/c">>] ,LocPath1),
-
-    timer:sleep(50),
-
-    %% DELETE the topic "a/b"
-    UriD = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
-    ReplyD = er_coap_client:request(delete, UriD),
-    ?LOGT("Reply=~p", [ReplyD]),
-    {ok, deleted, #coap_content{}}= ReplyD,
-
-    timer:sleep(300), %% Waiting gen_server:cast/2 for deleting operation
-    ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload)),
-    ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload1)).
-
-t_case02_delete(_Config) ->
-    TopicInPayload = <<"a/b">>,
-    TopicStr = binary_to_list(TopicInPayload),
-    PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
-
-    %% DELETE the unexisted topic "a/b"
-    Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
-    Reply1 = er_coap_client:request(delete, Uri1),
-    ?LOGT("Reply=~p", [Reply1]),
-    {error, not_found} = Reply1.
-
-t_case13_emit_stats_test(_Config) ->
-    ok.
-
-%%--------------------------------------------------------------------
-%% Internal functions
-
-receive_notification() ->
-    receive
-        {coap_notify, Pid, N2, Code2, Content2} ->
-            {coap_notify, Pid, N2, Code2, Content2}
-    after 2000 ->
-        receive_notification_timeout
-    end.
-
-assert_recv(Topic, Payload) ->
-    receive
-        {deliver, _, Msg} ->
-            ?assertEqual(Topic, Msg#message.topic),
-            ?assertEqual(Payload, Msg#message.payload)
-    after
-        500 ->
-            ?assert(false)
-    end.
-

+ 0 - 29
apps/emqx_exhook/.gitignore

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

+ 0 - 116
apps/emqx_exhook/docs/design-cn.md

@@ -1,116 +0,0 @@
-# 设计
-
-## 动机
-
-在 EMQ X Broker v4.1-v4.2 中,我们发布了 2 个插件来扩展 emqx 的编程能力:
-
-1. `emqx-extension-hook` 提供了使用 Java, Python 向 Broker 挂载钩子的功能
-2. `emqx-exproto` 提供了使用 Java,Python 编写用户自定义协议接入插件的功能
-
-但在后续的支持中发现许多难以处理的问题:
-
-1. 有大量的编程语言需要支持,需要编写和维护如 Go, JavaScript, Lua.. 等语言的驱动。
-2. `erlport` 使用的操作系统的管道进行通信,这让用户代码只能部署在和 emqx 同一个操作系统上。部署方式受到了极大的限制。
-3. 用户程序的启动参数直接打包到 Broker 中,导致用户开发无法实时的进行调试,单步跟踪等。
-4. `erlport` 会占用 `stdin` `stdout`。
-
-因此,我们计划重构这部分的实现,其中主要的内容是:
-1. 使用 `gRPC` 替换 `erlport`。
-2. 将 `emqx-extension-hook` 重命名为 `emqx-exhook`
-
-
-旧版本的设计:[emqx-extension-hook design in v4.2.0](https://github.com/emqx/emqx-exhook/blob/v4.2.0/docs/design.md)
-
-## 设计
-
-架构如下:
-
-```
-  EMQ X                                    
-+========================+                 +========+==========+
-|    ExHook              |                 |        |          |
-|   +----------------+   |      gRPC       | gRPC   |  User's  |
-|   |   gRPC Client  | ------------------> | Server |  Codes   |
-|   +----------------+   |    (HTTP/2)     |        |          |
-|                        |                 |        |          |
-+========================+                 +========+==========+
-```
-
-`emqx-exhook` 通过 gRPC 的方式向用户部署的 gRPC 服务发送钩子的请求,并处理其返回的值。
-
-
-和 emqx 原生的钩子一致,emqx-exhook 也按照链式的方式执行:
-
-<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
-```

+ 0 - 48
apps/emqx_exhook/rebar.config

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

+ 0 - 23
apps/emqx_exhook/src/emqx_exhook.appup.src

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

+ 0 - 96
apps/emqx_exhook/test/emqx_exhook_SUITE.erl

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

+ 0 - 339
apps/emqx_exhook/test/emqx_exhook_demo_svr.erl

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

+ 0 - 531
apps/emqx_exhook/test/props/prop_exhook_hooks.erl

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

+ 0 - 48
apps/emqx_exproto/.gitignore

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

+ 0 - 127
apps/emqx_exproto/docs/design-cn.md

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

BIN
apps/emqx_exproto/docs/images/exproto-arch.jpg


BIN
apps/emqx_exproto/docs/images/exproto-grpc-arch.jpg


+ 0 - 51
apps/emqx_exproto/rebar.config

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

+ 0 - 454
apps/emqx_exproto/test/emqx_exproto_SUITE.erl

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

+ 0 - 278
apps/emqx_exproto/test/emqx_exproto_echo_svr.erl

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

apps/emqx_coap/etc/emqx_coap.conf → apps/emqx_gateway/etc/emqx_coap.conf


apps/emqx_exhook/etc/emqx_exhook.conf → apps/emqx_gateway/etc/emqx_exhook.conf


apps/emqx_exproto/etc/emqx_exproto.conf → apps/emqx_gateway/etc/emqx_exproto.conf


apps/emqx_lwm2m/etc/emqx_lwm2m.conf → apps/emqx_gateway/etc/emqx_lwm2m.conf


apps/emqx_coap/priv/emqx_coap.schema → apps/emqx_gateway/etc/priv/emqx_coap.schema


apps/emqx_exhook/priv/emqx_exhook.schema → apps/emqx_gateway/etc/priv/emqx_exhook.schema


apps/emqx_exproto/priv/emqx_exproto.schema → apps/emqx_gateway/etc/priv/emqx_exproto.schema


apps/emqx_lwm2m/priv/emqx_lwm2m.schema → apps/emqx_gateway/etc/priv/emqx_lwm2m.schema


apps/emqx_exhook/priv/protos/exhook.proto → apps/emqx_gateway/etc/priv/exhook.proto


apps/emqx_exproto/priv/protos/exproto.proto → apps/emqx_gateway/etc/priv/exproto.proto


+ 16 - 1
apps/emqx_gateway/rebar.config

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

apps/emqx_coap/README.md → apps/emqx_gateway/src/coap/README.md


apps/emqx_coap/src/emqx_coap.app.src → apps/emqx_gateway/src/coap/emqx_coap.app.src


+ 1 - 1
apps/emqx_coap/src/emqx_coap_app.erl

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

+ 1 - 1
apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl

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

+ 2 - 2
apps/emqx_coap/src/emqx_coap_pubsub_resource.erl

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

+ 1 - 1
apps/emqx_coap/src/emqx_coap_pubsub_topics.erl

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

+ 1 - 1
apps/emqx_coap/src/emqx_coap_registry.erl

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

+ 2 - 2
apps/emqx_coap/src/emqx_coap_resource.erl

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

+ 1 - 1
apps/emqx_coap/src/emqx_coap_server.erl

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

apps/emqx_coap/src/emqx_coap_sup.erl → apps/emqx_gateway/src/coap/emqx_coap_sup.erl


+ 1 - 1
apps/emqx_coap/src/emqx_coap_timer.erl

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

apps/emqx_coap/include/emqx_coap.hrl → apps/emqx_gateway/src/coap/include/emqx_coap.hrl


+ 319 - 0
apps/emqx_gateway/src/coap/test/emqx_coap_SUITE.erl

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

+ 678 - 0
apps/emqx_gateway/src/coap/test/emqx_coap_pubsub_SUITE.erl

@@ -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 successfully,got {ok, nocontent} Method
+%     Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
+%     Reply1 = {ok, Pid, _N, nocontent, _} = er_coap_observer:observe(Uri),
+%     ?LOGT("Subscribe Reply=~p", [Reply1]), 
+
+%     [SubPid] = emqx:subscribers(Topic),
+%     ?assert(is_pid(SubPid)),
+
+%     %% put to publish to topic "a/b"
+%     Reply2 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
+%     {ok,changed, #coap_content{}} = Reply2,
+%     [{Topic, MaxAge1, CT, Payload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
+%     ?assertEqual(60, MaxAge1),
+%     ?assertEqual(<<"42">>, CT),
+%     ?assertEqual(false, TimeStamp =:= timeout),
+
+%     %% Publish a message
+%     emqx:publish(emqx_message:make(Topic, Payload)),
+
+%     Notif = receive_notification(),
+%     ?LOGT("observer get Notif=~p", [Notif]),
+%     {coap_notify, _, _, {ok,content}, #coap_content{payload = Payload}} = Notif,
+
+%     er_coap_observer:stop(Pid),
+%     {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
+
+% t_case03_subscribe(_Config) ->
+%     %% Subscribe to the unexisted topic "a/b", got not_found
+%     Topic = <<"a/b">>,
+%     TopicStr = binary_to_list(Topic),
+%     PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
+%     Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
+%     {error, not_found} = er_coap_observer:observe(Uri),
+
+%     [] = emqx:subscribers(Topic).
+
+% t_case04_subscribe(_Config) ->
+%     %% Subscribe to the wildcad topic "+/b", got bad_request
+%     Topic = <<"+/b">>,
+%     TopicStr = binary_to_list(Topic),
+%     PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
+%     Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
+%     {error, bad_request} = er_coap_observer:observe(Uri),
+
+%     [] = emqx:subscribers(Topic).
+
+% t_case01_read(_Config) ->
+%     Topic = <<"topic1">>,
+%     TopicStr = binary_to_list(Topic),
+%     Payload = <<"PubPayload">>,
+%     timer:sleep(100),
+
+%     %% First post to create a topic "topic1"
+%     Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
+%     Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
+%     ?LOGT("Reply =~p", [Reply]),
+%     {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
+%     ?assertEqual(<<"/ps/topic1">> ,LocPath),
+%     TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
+%     ?LOGT("lookup topic info=~p", [TopicInfo]),
+%     ?assertEqual(60, MaxAge1),
+%     ?assertEqual(<<"42">>, CT1),
+
+%     %% GET to read the publish message of the topic
+%     timer:sleep(1000),
+%     Reply1 = er_coap_client:request(get, Uri),
+%     ?LOGT("Reply=~p", [Reply1]),
+%     {ok,content, #coap_content{payload = Payload}} = Reply1,
+
+%     {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
+
+% t_case02_read(_Config) ->
+%     Topic = <<"topic1">>,
+%     TopicStr = binary_to_list(Topic),
+%     Payload = <<"PubPayload">>,
+%     timer:sleep(100),
+
+%     %% First post to publish a topic "topic1"
+%     Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
+%     Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
+%     ?LOGT("Reply =~p", [Reply]),
+%     {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
+%     ?assertEqual(<<"/ps/topic1">> ,LocPath),
+%     TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
+%     ?LOGT("lookup topic info=~p", [TopicInfo]),
+%     ?assertEqual(60, MaxAge1),
+%     ?assertEqual(<<"42">>, CT1),
+
+%     %% GET to read the publish message of unmatched format, got bad_request
+%     Reply1 = er_coap_client:request(get, Uri, #coap_content{format = <<"application/json">>}),
+%     ?LOGT("Reply=~p", [Reply1]),
+%     {error, bad_request} = Reply1,
+
+%     {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
+
+% t_case03_read(_Config) ->
+%     Topic = <<"topic1">>,
+%     TopicStr = binary_to_list(Topic),
+%     Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
+%     timer:sleep(100),
+
+%     %% GET to read the nexisted topic "topic1", got not_found
+%     Reply = er_coap_client:request(get, Uri),
+%     ?LOGT("Reply=~p", [Reply]),
+%     {error, not_found} = Reply.
+
+% t_case04_read(_Config) ->
+%     Topic = <<"topic1">>,
+%     TopicStr = binary_to_list(Topic),
+%     Payload = <<"PubPayload">>,
+%     timer:sleep(100),
+
+%     %% First post to publish a topic "topic1"
+%     Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
+%     Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
+%     ?LOGT("Reply =~p", [Reply]),
+%     {ok,created, #coap_content{location_path = [LocPath]}} = Reply,
+%     ?assertEqual(<<"/ps/topic1">> ,LocPath),
+%     TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
+%     ?LOGT("lookup topic info=~p", [TopicInfo]),
+%     ?assertEqual(60, MaxAge1),
+%     ?assertEqual(<<"42">>, CT1),
+
+%     %% GET to read the publish message of wildcard topic, got bad_request
+%     WildTopic = binary_to_list(<<"+/topic1">>),
+%     Uri1 = "coap://127.0.0.1/ps/"++WildTopic++"?c=client1&u=tom&p=secret",
+%     Reply1 = er_coap_client:request(get, Uri1, #coap_content{format = <<"application/json">>}),
+%     ?LOGT("Reply=~p", [Reply1]),
+%     {error, bad_request} = Reply1,
+
+%     {ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
+
+% t_case05_read(_Config) ->
+%     Topic = <<"a/b">>,
+%     TopicStr = binary_to_list(Topic),
+%     PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
+%     Payload = <<"payload">>,
+
+%     %% post to publish a new topic "a/b", and the topic is created
+%     URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
+%     Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
+%     ?LOGT("Reply =~p", [Reply]),
+%     {ok,created, #coap_content{location_path = LocPath}} = Reply,
+%     ?assertEqual([<<"/ps/a/b">>] ,LocPath),
+%     [{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
+%     ?assertEqual(5, MaxAge),
+%     ?assertEqual(<<"42">>, CT),
+
+%     %% Wait for the max age of the timer expires
+%     timer:sleep(6000),
+%     ?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
+
+%     %% GET to read the expired publish message, supposed to get {ok, nocontent}, but now got {ok, content}
+%     Reply1 = er_coap_client:request(get, URI),
+%     ?LOGT("Reply=~p", [Reply1]),
+%     {ok, content, #coap_content{payload = <<>>}}= Reply1,
+
+%     {ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
+
+% t_case01_delete(_Config) ->
+%     TopicInPayload = <<"a/b">>,
+%     TopicStr = binary_to_list(TopicInPayload),
+%     PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
+%     Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"),
+%     URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
+
+%     %% Client post to CREATE topic "a/b"
+%     Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
+%     ?LOGT("Reply =~p", [Reply]),
+%     {ok,created, #coap_content{location_path = LocPath}} = Reply,
+%     ?assertEqual([<<"/ps/a/b">>] ,LocPath),
+
+%     %% Client post to CREATE topic "a/b/c"
+%     TopicInPayload1 = <<"a/b/c">>,
+%     PercentEncodedTopic1 = emqx_http_lib:uri_encode(binary_to_list(TopicInPayload1)),
+%     Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"),
+%     Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
+%     ?LOGT("Reply =~p", [Reply1]),
+%     {ok,created, #coap_content{location_path = LocPath1}} = Reply1,
+%     ?assertEqual([<<"/ps/a/b/c">>] ,LocPath1),
+
+%     timer:sleep(50),
+
+%     %% DELETE the topic "a/b"
+%     UriD = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
+%     ReplyD = er_coap_client:request(delete, UriD),
+%     ?LOGT("Reply=~p", [ReplyD]),
+%     {ok, deleted, #coap_content{}}= ReplyD,
+
+%     timer:sleep(300), %% Waiting gen_server:cast/2 for deleting operation
+%     ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload)),
+%     ?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload1)).
+
+% t_case02_delete(_Config) ->
+%     TopicInPayload = <<"a/b">>,
+%     TopicStr = binary_to_list(TopicInPayload),
+%     PercentEncodedTopic = emqx_http_lib:uri_encode(TopicStr),
+
+%     %% DELETE the unexisted topic "a/b"
+%     Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
+%     Reply1 = er_coap_client:request(delete, Uri1),
+%     ?LOGT("Reply=~p", [Reply1]),
+%     {error, not_found} = Reply1.
+
+% t_case13_emit_stats_test(_Config) ->
+%     ok.
+
+% %%--------------------------------------------------------------------
+% %% Internal functions
+
+% receive_notification() ->
+%     receive
+%         {coap_notify, Pid, N2, Code2, Content2} ->
+%             {coap_notify, Pid, N2, Code2, Content2}
+%     after 2000 ->
+%         receive_notification_timeout
+%     end.
+
+% assert_recv(Topic, Payload) ->
+%     receive
+%         {deliver, _, Msg} ->
+%             ?assertEqual(Topic, Msg#message.topic),
+%             ?assertEqual(Payload, Msg#message.payload)
+%     after
+%         500 ->
+%             ?assert(false)
+%     end.
+

apps/emqx_exhook/README.md → apps/emqx_gateway/src/exhook/README.md


apps/emqx_exhook/src/emqx_exhook.app.src → apps/emqx_gateway/src/exhook/emqx_exhook.app.src


+ 1 - 1
apps/emqx_exhook/src/emqx_exhook.erl

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

+ 1 - 1
apps/emqx_exhook/src/emqx_exhook_app.erl

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

+ 1 - 1
apps/emqx_exhook/src/emqx_exhook_cli.erl

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

+ 1 - 1
apps/emqx_exhook/src/emqx_exhook_handler.erl

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

+ 1 - 1
apps/emqx_exhook/src/emqx_exhook_server.erl

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

apps/emqx_exhook/src/emqx_exhook_sup.erl → apps/emqx_gateway/src/exhook/emqx_exhook_sup.erl


apps/emqx_exhook/include/emqx_exhook.hrl → apps/emqx_gateway/src/exhook/include/emqx_exhook.hrl


+ 531 - 0
apps/emqx_gateway/src/exhook/prop_exhook_hooks.erl

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

+ 97 - 0
apps/emqx_gateway/src/exhook/test/emqx_exhook_SUITE.erl

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

+ 339 - 0
apps/emqx_gateway/src/exhook/test/emqx_exhook_demo_svr.erl

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

apps/emqx_exproto/README.md → apps/emqx_gateway/src/exproto/README.md


apps/emqx_exproto/src/emqx_exproto.app.src → apps/emqx_gateway/src/exproto/emqx_exproto.app.src


+ 1 - 1
apps/emqx_exproto/src/emqx_exproto.erl

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

apps/emqx_exproto/src/emqx_exproto_app.erl → apps/emqx_gateway/src/exproto/emqx_exproto_app.erl


+ 1 - 2
apps/emqx_exproto/src/emqx_exproto_channel.erl

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

apps/emqx_exproto/src/emqx_exproto_conn.erl → apps/emqx_gateway/src/exproto/emqx_exproto_conn.erl


apps/emqx_exproto/src/emqx_exproto_gcli.erl → apps/emqx_gateway/src/exproto/emqx_exproto_gcli.erl


+ 2 - 2
apps/emqx_exproto/src/emqx_exproto_gsvr.erl

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

apps/emqx_exproto/src/emqx_exproto_sup.erl → apps/emqx_gateway/src/exproto/emqx_exproto_sup.erl


apps/emqx_exproto/include/emqx_exproto.hrl → apps/emqx_gateway/src/exproto/include/emqx_exproto.hrl


+ 454 - 0
apps/emqx_gateway/src/exproto/test/emqx_exproto_SUITE.erl

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

+ 278 - 0
apps/emqx_gateway/src/exproto/test/emqx_exproto_echo_svr.erl

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

apps/emqx_lwm2m/.gitignore → apps/emqx_gateway/src/lwm2m/.gitignore


apps/emqx_lwm2m/README.md → apps/emqx_gateway/src/lwm2m/README.md


apps/emqx_lwm2m/src/binary_util.erl → apps/emqx_gateway/src/lwm2m/binary_util.erl


apps/emqx_lwm2m/src/emqx_lwm2m.app.src → apps/emqx_gateway/src/lwm2m/emqx_lwm2m.app.src


apps/emqx_lwm2m/src/emqx_lwm2m_api.erl → apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl


+ 1 - 2
apps/emqx_lwm2m/src/emqx_lwm2m_app.erl

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

apps/emqx_lwm2m/src/emqx_lwm2m_cm.erl → apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl


apps/emqx_lwm2m/src/emqx_lwm2m_cm_sup.erl → apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm_sup.erl


+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_cmd_handler.erl

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

+ 2 - 2
apps/emqx_lwm2m/src/emqx_lwm2m_coap_resource.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_coap_server.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_json.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_message.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl

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

apps/emqx_lwm2m/src/emqx_lwm2m_sup.erl → apps/emqx_gateway/src/lwm2m/emqx_lwm2m_sup.erl


+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_timer.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_tlv.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_xml_object.erl

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

+ 1 - 1
apps/emqx_lwm2m/src/emqx_lwm2m_xml_object_db.erl

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

apps/emqx_lwm2m/include/emqx_lwm2m.hrl → apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl


apps/emqx_lwm2m/lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml → apps/emqx_gateway/src/lwm2m/lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml


apps/emqx_lwm2m/lwm2m_xml/LWM2M_Connectivity_Statistics-v1_0_1.xml → apps/emqx_gateway/src/lwm2m/lwm2m_xml/LWM2M_Connectivity_Statistics-v1_0_1.xml


apps/emqx_lwm2m/lwm2m_xml/LWM2M_Device-v1_0_1.xml → apps/emqx_gateway/src/lwm2m/lwm2m_xml/LWM2M_Device-v1_0_1.xml


+ 0 - 0
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Firmware_Update-v1_0_1.xml


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels