Quellcode durchsuchen

Merge branch 'umbrella-for-430-auto-sync' into umbrella-for-430

Zaiming Shi vor 5 Jahren
Ursprung
Commit
7fdbfba06a
100 geänderte Dateien mit 2923 neuen und 3771 gelöschten Zeilen
  1. 3 3
      apps/emqx_auth_http/etc/emqx_auth_http.conf
  2. 1 1
      apps/emqx_auth_http/include/emqx_auth_http.hrl
  3. 40 36
      apps/emqx_auth_http/priv/emqx_auth_http.schema
  4. 6 2
      apps/emqx_auth_http/rebar.config
  5. 13 16
      apps/emqx_auth_http/src/emqx_acl_http.erl
  6. 1 1
      apps/emqx_auth_http/src/emqx_auth_http.app.src
  7. 25 29
      apps/emqx_auth_http/src/emqx_auth_http.erl
  8. 75 31
      apps/emqx_auth_http/src/emqx_auth_http_app.erl
  9. 20 29
      apps/emqx_auth_http/src/emqx_auth_http_cli.erl
  10. 256 0
      apps/emqx_auth_http/src/emqx_http_client.erl
  11. 48 0
      apps/emqx_auth_http/src/emqx_http_client_sup.erl
  12. 24 18
      apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl
  13. 1 0
      apps/emqx_auth_jwt/.gitignore
  14. 17 11
      apps/emqx_auth_jwt/etc/emqx_auth_jwt.conf
  15. 13 12
      apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema
  16. 2 1
      apps/emqx_auth_jwt/rebar.config
  17. 1 1
      apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src
  18. 10 0
      apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src
  19. 17 64
      apps/emqx_auth_jwt/src/emqx_auth_jwt.erl
  20. 32 19
      apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl
  21. 222 0
      apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl
  22. 29 24
      apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl
  23. 6 0
      apps/emqx_auth_ldap/rebar.config
  24. 1 0
      apps/emqx_auth_mongo/rebar.config
  25. 9 0
      apps/emqx_auth_mysql/src/emqx_auth_mysql.appup.src
  26. 9 0
      apps/emqx_auth_pgsql/src/emqx_auth_pgsql.appup.src
  27. 2 0
      apps/emqx_auth_redis/.gitignore
  28. 1 1
      apps/emqx_auth_redis/rebar.config
  29. 10 0
      apps/emqx_auth_redis/src/emqx_auth_redis.appup.src
  30. 1 1
      apps/emqx_auth_redis/src/emqx_auth_redis_sup.erl
  31. 10 0
      apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src
  32. 12 6
      apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl
  33. 34 21
      apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_actions.erl
  34. 10 9
      apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl
  35. 6 0
      apps/emqx_coap/src/emqx_coap_server.erl
  36. 8 1
      apps/emqx_dashboard/src/emqx_dashboard.erl
  37. 4 0
      apps/emqx_exhook/.gitignore
  38. 20 56
      apps/emqx_exhook/README.md
  39. 65 204
      apps/emqx_exhook/docs/design.md
  40. 0 84
      apps/emqx_exhook/docs/introduction.md
  41. 0 79
      apps/emqx_exhook/docs/sdk-specification.md
  42. 15 0
      apps/emqx_exhook/etc/emqx_exhook.conf
  43. 0 24
      apps/emqx_exhook/etc/emqx_extension_hook.conf
  44. 0 43
      apps/emqx_exhook/priv/emqx_extension_hook.schema
  45. 30 1
      apps/emqx_exhook/rebar.config
  46. 0 12
      apps/emqx_exhook/sdk/README.md
  47. 2 2
      apps/emqx_exhook/src/emqx_exhook.app.src
  48. 24 0
      apps/emqx_exhook/src/emqx_exhook.app.src.script
  49. 9 0
      apps/emqx_exhook/src/emqx_exhook.appup.src
  50. 51 39
      apps/emqx_exhook/src/emqx_exhook_server.erl
  51. 14 29
      apps/emqx_exhook/src/emqx_exhook_sup.erl
  52. 0 136
      apps/emqx_exhook/src/emqx_extension_hook.erl
  53. 0 108
      apps/emqx_exhook/src/emqx_extension_hook_app.erl
  54. 0 80
      apps/emqx_exhook/src/emqx_extension_hook_cli.erl
  55. 0 305
      apps/emqx_exhook/src/emqx_extension_hook_driver.erl
  56. 0 249
      apps/emqx_exhook/src/emqx_extension_hook_handler.erl
  57. 0 50
      apps/emqx_exhook/src/emqx_extension_hook_sup.erl
  58. 4 4
      apps/emqx_exhook/test/emqx_exhook_SUITE.erl
  59. 114 116
      apps/emqx_exhook/test/emqx_exhook_demo_svr.erl
  60. 0 139
      apps/emqx_exhook/test/emqx_extension_hook_SUITE.erl
  61. 2 0
      apps/emqx_exhook/test/props/prop_exhook_hooks.erl
  62. 0 160
      apps/emqx_exhook/test/scripts/Main.java
  63. 0 134
      apps/emqx_exhook/test/scripts/main.py
  64. 5 2
      apps/emqx_exproto/.gitignore
  65. 10 38
      apps/emqx_exproto/README.md
  66. 71 120
      apps/emqx_exproto/docs/design.md
  67. BIN
      apps/emqx_exproto/docs/images/exproto-arch.jpg
  68. 0 84
      apps/emqx_exproto/docs/sdk-specification.md
  69. 12 10
      apps/emqx_exproto/etc/emqx_exproto.conf
  70. 0 136
      apps/emqx_exproto/example/Main.java
  71. 0 80
      apps/emqx_exproto/example/main.py
  72. 13 0
      apps/emqx_exproto/include/emqx_exproto.hrl
  73. 67 17
      apps/emqx_exproto/priv/emqx_exproto.schema
  74. 31 8
      apps/emqx_exproto/rebar.config
  75. 0 11
      apps/emqx_exproto/sdk/README.md
  76. 2 4
      apps/emqx_exproto/src/emqx_exproto.app.src
  77. 24 0
      apps/emqx_exproto/src/emqx_exproto.app.src.script
  78. 9 0
      apps/emqx_exproto/src/emqx_exproto.appup.src
  79. 78 69
      apps/emqx_exproto/src/emqx_exproto.erl
  80. 2 1
      apps/emqx_exproto/src/emqx_exproto_app.erl
  81. 345 132
      apps/emqx_exproto/src/emqx_exproto_channel.erl
  82. 38 24
      apps/emqx_exproto/src/emqx_exproto_conn.erl
  83. 0 302
      apps/emqx_exproto/src/emqx_exproto_driver_mngr.erl
  84. 110 0
      apps/emqx_exproto/src/emqx_exproto_gcli.erl
  85. 154 0
      apps/emqx_exproto/src/emqx_exproto_gsvr.erl
  86. 55 8
      apps/emqx_exproto/src/emqx_exproto_sup.erl
  87. 0 179
      apps/emqx_exproto/src/emqx_exproto_types.erl
  88. 283 41
      apps/emqx_exproto/test/emqx_exproto_SUITE.erl
  89. 37 26
      apps/emqx_exproto/test/emqx_exproto_echo_svr.erl
  90. 1 1
      apps/emqx_retainer/rebar.config
  91. 4 1
      apps/emqx_retainer/src/emqx_retainer.erl
  92. 11 0
      apps/emqx_rule_engine/include/rule_actions.hrl
  93. 2 1
      apps/emqx_rule_engine/include/rule_engine.hrl
  94. 106 64
      apps/emqx_rule_engine/src/emqx_rule_actions.erl
  95. 70 0
      apps/emqx_rule_engine/src/emqx_rule_actions_trans.erl
  96. 8 0
      apps/emqx_rule_engine/src/emqx_rule_engine.appup.src
  97. 31 17
      apps/emqx_rule_engine/src/emqx_rule_engine.erl
  98. 14 4
      apps/emqx_rule_engine/src/emqx_rule_engine_api.erl
  99. 1 0
      apps/emqx_rule_engine/src/emqx_rule_engine_app.erl
  100. 0 0
      apps/emqx_rule_engine/src/emqx_rule_engine_sup.erl

+ 3 - 3
apps/emqx_auth_http/etc/emqx_auth_http.conf

@@ -101,8 +101,8 @@ auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,
 ## -m: minute, e.g. '5m' for 5 minutes
 ## -s: second, e.g. '30s' for 30 seconds
 ##
-## Default: 0
-## auth.http.request.timeout = 0
+## Default: 5s
+## auth.http.request.timeout = 5s
 
 ## Connection time-out time, used during the initial request
 ## when the client is connecting to the server
@@ -117,7 +117,7 @@ auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,
 ## Value: integer
 ##
 ## Default: 3
-auth.http.request.retry_times = 3
+auth.http.request.retry_times = 5
 
 ## The interval for re-sending the http request
 ##

+ 1 - 1
apps/emqx_auth_http/include/emqx_auth_http.hrl

@@ -1,7 +1,7 @@
 
 -define(APP, emqx_auth_http).
 
--record(http_request, {method = post, content_type, url, params, options = []}).
+-record(http_request, {method = post, path, headers, params, request_timeout}).
 
 -record(auth_metrics, {
         success = 'client.auth.success',

+ 40 - 36
apps/emqx_auth_http/priv/emqx_auth_http.schema

@@ -11,7 +11,7 @@
 
 {mapping, "auth.http.auth_req.content_type", "emqx_auth_http.auth_req", [
   {default, 'x-www-form-urlencoded'},
-  {datatype, {enum, [json, 'x-www-form-urlencoded']}}
+  {datatype, {enum, ['json', 'x-www-form-urlencoded']}}
 ]}.
 
 {mapping, "auth.http.auth_req.params", "emqx_auth_http.auth_req", [
@@ -25,7 +25,7 @@
       Params = cuttlefish:conf_get("auth.http.auth_req.params", Conf),
       [{url, Url},
       {method, cuttlefish:conf_get("auth.http.auth_req.method", Conf)},
-      {content_type, cuttlefish:conf_get("auth.http.auth_req.content_type", Conf)},
+      {content_type, list_to_binary("application/" ++ atom_to_list(cuttlefish:conf_get("auth.http.auth_req.content_type", Conf)))},
       {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
   end
 end}.
@@ -41,7 +41,7 @@ end}.
 
 {mapping, "auth.http.super_req.content_type", "emqx_auth_http.super_req", [
   {default, 'x-www-form-urlencoded'},
-  {datatype, {enum, [json, 'x-www-form-urlencoded']}}
+  {datatype, {enum, ['json', 'x-www-form-urlencoded']}}
 ]}.
 
 {mapping, "auth.http.super_req.params", "emqx_auth_http.super_req", [
@@ -53,7 +53,7 @@ end}.
     undefined -> cuttlefish:unset();
     Url -> Params = cuttlefish:conf_get("auth.http.super_req.params", Conf),
            [{url, Url}, {method, cuttlefish:conf_get("auth.http.super_req.method", Conf)},
-            {content_type, cuttlefish:conf_get("auth.http.super_req.content_type", Conf)},
+            {content_type, list_to_binary("application/" ++ atom_to_list(cuttlefish:conf_get("auth.http.super_req.content_type", Conf)))},
             {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
   end
 end}.
@@ -70,7 +70,7 @@ end}.
 
 {mapping, "auth.http.acl_req.content_type", "emqx_auth_http.acl_req", [
   {default, 'x-www-form-urlencoded'},
-  {datatype, {enum, [json, 'x-www-form-urlencoded']}}
+  {datatype, {enum, ['json', 'x-www-form-urlencoded']}}
 ]}.
 
 {mapping, "auth.http.acl_req.params", "emqx_auth_http.acl_req", [
@@ -81,34 +81,56 @@ end}.
   case cuttlefish:conf_get("auth.http.acl_req", Conf, undefined) of
     undefined -> cuttlefish:unset();
     Url -> Params = cuttlefish:conf_get("auth.http.acl_req.params", Conf),
-           [{url, Url}, {method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)},
-            {content_type, cuttlefish:conf_get("auth.http.acl_req.content_type", Conf)},
+           [{url, Url},
+            {method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)},
+            {content_type, list_to_binary("application/" ++ atom_to_list(cuttlefish:conf_get("auth.http.acl_req.content_type", Conf)))},
             {params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
   end
 end}.
 
-{mapping, "auth.http.request.timeout", "emqx_auth_http.http_opts", [
-  {default, 0},
+{mapping, "auth.http.request.timeout", "emqx_auth_http.request_timeout", [
+  {default, "5s"},
   {datatype, [integer, {duration, ms}]}
 ]}.
 
-{mapping, "auth.http.request.connect_timeout", "emqx_auth_http.http_opts", [
+{mapping, "auth.http.pool_size", "emqx_auth_http.pool_opts", [
+  {default, 8},
+  {datatype, integer}
+]}.
+
+{mapping, "auth.http.request.connect_timeout", "emqx_auth_http.pool_opts", [
+  {default, "5s"},
   {datatype, [integer, {duration, ms}]}
 ]}.
 
-{mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.http_opts", [
+{mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.pool_opts", [
   {datatype, string}
 ]}.
 
-{mapping, "auth.http.ssl.certfile", "emqx_auth_http.http_opts", [
+{mapping, "auth.http.ssl.certfile", "emqx_auth_http.pool_opts", [
   {datatype, string}
 ]}.
 
-{mapping, "auth.http.ssl.keyfile", "emqx_auth_http.http_opts", [
+{mapping, "auth.http.ssl.keyfile", "emqx_auth_http.pool_opts", [
   {datatype, string}
 ]}.
 
-{translation, "emqx_auth_http.http_opts", fun(Conf) ->
+{mapping, "auth.http.request.retry_times", "emqx_auth_http.pool_opts", [
+  {default, 5},
+  {datatype, integer}
+]}.
+
+{mapping, "auth.http.request.retry_interval", "emqx_auth_http.pool_opts", [
+  {default, "1s"},
+  {datatype, {duration, ms}}
+]}.
+
+{mapping, "auth.http.request.retry_backoff", "emqx_auth_http.pool_opts", [
+  {default, 2.0},
+  {datatype, float}
+]}.
+
+{translation, "emqx_auth_http.pool_opts", fun(Conf) ->
   Filter = fun(L) -> [{K, V} || {K, V} <- L, V =/= undefined] end,
   InfinityFun = fun(0) -> infinity;
                    (Duration) -> Duration
@@ -116,8 +138,10 @@ end}.
   SslOpts = Filter([{cacertfile, cuttlefish:conf_get("auth.http.ssl.cacertfile", Conf, undefined)},
                     {certfile, cuttlefish:conf_get("auth.http.ssl.certfile", Conf, undefined)},
                     {keyfile, cuttlefish:conf_get("auth.http.ssl.keyfile", Conf, undefined)}]),
-  Opts = [{timeout, InfinityFun(cuttlefish:conf_get("auth.http.request.timeout", Conf))},
-          {connect_timeout, InfinityFun(cuttlefish:conf_get("auth.http.request.connect_timeout", Conf, undefined))}],
+  Opts = [{pool_size, cuttlefish:conf_get("auth.http.pool_size", Conf)},
+          {connect_timeout, InfinityFun(cuttlefish:conf_get("auth.http.request.connect_timeout", Conf))},
+          {retry, cuttlefish:conf_get("auth.http.request.retry_times", Conf)},
+          {retry_timeout, cuttlefish:conf_get("auth.http.request.retry_interval", Conf)}],
   case SslOpts of
       [] -> Filter(Opts);
       _  ->
@@ -131,26 +155,6 @@ end}.
   end
 end}.
 
-{mapping, "auth.http.request.retry_times", "emqx_auth_http.retry_opts", [
-  {default, 3},
-  {datatype, integer}
-]}.
-
-{mapping, "auth.http.request.retry_interval", "emqx_auth_http.retry_opts", [
-  {default, "1s"},
-  {datatype, {duration, ms}}
-]}.
-
-{mapping, "auth.http.request.retry_backoff", "emqx_auth_http.retry_opts", [
-  {default, 2.0},
-  {datatype, float}
-]}.
-
-{translation, "emqx_auth_http.retry_opts", fun(Conf) ->
-  [{times, cuttlefish:conf_get("auth.http.request.retry_times", Conf)},
-   {interval, cuttlefish:conf_get("auth.http.request.retry_interval", Conf)},
-   {backoff, cuttlefish:conf_get("auth.http.request.retry_backoff", Conf)}]
-end}.
 
 {mapping, "auth.http.header.$field", "emqx_auth_http.headers", [
   {datatype, string}

+ 6 - 2
apps/emqx_auth_http/rebar.config

@@ -1,4 +1,8 @@
-{deps, []}.
+{deps,
+ [{cowlib, {git, "https://github.com/ninenines/cowlib", {tag, "2.7.0"}}},
+  {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.4"}}},
+  {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
+ ]}.
 
 {edoc_opts, [{preprocess, true}]}.
 {erl_opts, [warn_unused_vars,
@@ -20,7 +24,7 @@
  [{test,
    [{deps,
      [{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "1.2.2"}}},
-      {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.0"}}}
+      {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "v1.2.2"}}}
      ]}
    ]}
  ]}.

+ 13 - 16
apps/emqx_auth_http/src/emqx_acl_http.erl

@@ -24,7 +24,7 @@
 -logger_header("[ACL http]").
 
 -import(emqx_auth_http_cli,
-        [ request/8
+        [ request/6
         , feedvar/2
         ]).
 
@@ -48,18 +48,16 @@ check_acl(ClientInfo, PubSub, Topic, AclResult, State) ->
 
 do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Config) ->
     ok;
-do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl_req    := AclReq,
-                                                      http_opts  := HttpOpts,
-                                                      retry_opts := RetryOpts,
-                                                      headers    := Headers}) ->
+do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl_req := AclReq,
+                                                      pool_name := PoolName}) ->
     ClientInfo1 = ClientInfo#{access => access(PubSub), topic => Topic},
-    case check_acl_request(AclReq, ClientInfo1, Headers, HttpOpts, RetryOpts) of
-        {ok, 200, "ignore"} -> ok;
+    case check_acl_request(PoolName, AclReq, ClientInfo1) of
+        {ok, 200, <<"ignore">>} -> ok;
         {ok, 200, _Body}    -> {stop, allow};
         {ok, _Code, _Body}  -> {stop, deny};
         {error, Error}      ->
-            ?LOG(error, "Request ACL url ~s, error: ~p",
-                 [AclReq#http_request.url, Error]),
+            ?LOG(error, "Request ACL path ~s, error: ~p",
+                 [AclReq#http_request.path, Error]),
             ok
     end.
 
@@ -79,13 +77,12 @@ inc_metrics({stop, deny}) ->
 return_with(Fun, Result) ->
     Fun(Result), Result.
 
-check_acl_request(#http_request{url = Url,
-                                method = Method,
-                                content_type = ContentType,
-                                params = Params,
-                                options = Options},
-                  ClientInfo, Headers, HttpOpts, RetryOpts) ->
-    request(Method, ContentType, Url, feedvar(Params, ClientInfo), Headers, HttpOpts, Options, RetryOpts).
+check_acl_request(PoolName, #http_request{path = Path,
+                                          method = Method,
+                                          headers = Headers,
+                                          params = Params,
+                                          request_timeout = RequestTimeout}, ClientInfo) ->
+    request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), RequestTimeout).
 
 access(subscribe) -> 1;
 access(publish)   -> 2.

+ 1 - 1
apps/emqx_auth_http/src/emqx_auth_http.app.src

@@ -3,7 +3,7 @@
   {vsn, "4.3.0"}, % strict semver, bump manually!
   {modules, []},
   {registered, [emqx_auth_http_sup]},
-  {applications, [kernel,stdlib,emqx]},
+  {applications, [kernel,stdlib,gproc,gun,emqx]},
   {mod, {emqx_auth_http_app, []}},
   {env, []},
   {licenses, ["Apache-2.0"]},

+ 25 - 29
apps/emqx_auth_http/src/emqx_auth_http.erl

@@ -25,7 +25,7 @@
 -logger_header("[Auth http]").
 
 -import(emqx_auth_http_cli,
-        [ request/8
+        [ request/6
         , feedvar/2
         ]).
 
@@ -41,28 +41,26 @@ register_metrics() ->
 
 check(ClientInfo, AuthResult, #{auth_req   := AuthReq,
                                 super_req  := SuperReq,
-                                http_opts  := HttpOpts,
-                                retry_opts := RetryOpts,
-                                headers    := Headers}) ->
-    case authenticate(AuthReq, ClientInfo, Headers, HttpOpts, RetryOpts) of
-        {ok, 200, "ignore"} ->
+                                pool_name  := PoolName}) ->
+    case authenticate(PoolName, AuthReq, ClientInfo) of
+        {ok, 200, <<"ignore">>} ->
             emqx_metrics:inc(?AUTH_METRICS(ignore)), ok;
         {ok, 200, Body}  ->
             emqx_metrics:inc(?AUTH_METRICS(success)),
-            IsSuperuser = is_superuser(SuperReq, ClientInfo, Headers, HttpOpts, RetryOpts),
+            IsSuperuser = is_superuser(PoolName, SuperReq, ClientInfo),
             {stop, AuthResult#{is_superuser => IsSuperuser,
                                 auth_result => success,
                                 anonymous   => false,
                                 mountpoint  => mountpoint(Body, ClientInfo)}};
         {ok, Code, _Body} ->
-            ?LOG(error, "Deny connection from url: ~s, response http code: ~p",
-                 [AuthReq#http_request.url, Code]),
+            ?LOG(error, "Deny connection from path: ~s, response http code: ~p",
+                 [AuthReq#http_request.path, Code]),
             emqx_metrics:inc(?AUTH_METRICS(failure)),
             {stop, AuthResult#{auth_result => http_to_connack_error(Code),
                                anonymous   => false}};
         {error, Error} ->
-            ?LOG(error, "Request auth url: ~s, error: ~p",
-                 [AuthReq#http_request.url, Error]),
+            ?LOG(error, "Request auth path: ~s, error: ~p",
+                 [AuthReq#http_request.path, Error]),
             emqx_metrics:inc(?AUTH_METRICS(failure)),
             %%FIXME later: server_unavailable is not right.
             {stop, AuthResult#{auth_result => server_unavailable,
@@ -75,32 +73,30 @@ description() -> "Authentication by HTTP API".
 %% Requests
 %%--------------------------------------------------------------------
 
-authenticate(#http_request{url = Url,
-                           method = Method,
-                           content_type = ContentType,
-                           params = Params,
-                           options = Options},
-             ClientInfo, HttpHeaders, HttpOpts, RetryOpts) ->
-   request(Method, ContentType, Url, feedvar(Params, ClientInfo), HttpHeaders, HttpOpts, Options, RetryOpts).
+authenticate(PoolName, #http_request{path = Path,
+                                     method = Method,
+                                     headers = Headers,
+                                     params = Params,
+                                     request_timeout = RequestTimeout}, ClientInfo) ->
+   request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), RequestTimeout).
 
--spec(is_superuser(maybe(#http_request{}), emqx_types:client(), list(), list(), list()) -> boolean()).
-is_superuser(undefined, _ClientInfo, _HttpHeaders, _HttpOpts, _RetryOpts) ->
+-spec(is_superuser(atom(), maybe(#http_request{}), emqx_types:client()) -> boolean()).
+is_superuser(_PoolName, undefined, _ClientInfo) ->
     false;
-is_superuser(#http_request{url = Url,
-                           method = Method,
-                           content_type = ContentType,
-                           params = Params,
-                           options = Options},
-             ClientInfo, HttpHeaders, HttpOpts, RetryOpts) ->
-    case request(Method, ContentType, Url, feedvar(Params, ClientInfo), HttpHeaders, HttpOpts, Options, RetryOpts) of
+is_superuser(PoolName, #http_request{path = Path,
+                                     method = Method,
+                                     headers = Headers,
+                                     params = Params,
+                                     request_timeout = RequestTimeout}, ClientInfo) ->
+    case request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), RequestTimeout) of
         {ok, 200, _Body}   -> true;
         {ok, _Code, _Body} -> false;
-        {error, Error}     -> ?LOG(error, "Request superuser url ~s, error: ~p", [Url, Error]),
+        {error, Error}     -> ?LOG(error, "Request superuser path ~s, error: ~p", [Path, Error]),
                               false
     end.
 
 mountpoint(Body, #{mountpoint := Mountpoint}) ->
-    case emqx_json:safe_decode(iolist_to_binary(Body), [return_maps]) of
+    case emqx_json:safe_decode(Body, [return_maps]) of
         {error, _} -> Mountpoint;
         {ok, Json} when is_map(Json) ->
             maps:get(<<"mountpoint">>, Json, Mountpoint);

+ 75 - 31
apps/emqx_auth_http/src/emqx_auth_http_app.erl

@@ -17,7 +17,6 @@
 -module(emqx_auth_http_app).
 
 -behaviour(application).
--behaviour(supervisor).
 
 -emqx_plugin(auth).
 
@@ -33,37 +32,35 @@
 %%--------------------------------------------------------------------
 
 start(_StartType, _StartArgs) ->
-    with_env(auth_req, fun load_auth_hook/1),
-    with_env(acl_req,  fun load_acl_hook/1),
-    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+    case translate_env() of
+        ok ->
+            {ok, PoolOpts} = application:get_env(?APP, pool_opts),
+            {ok, Sup} = emqx_http_client_sup:start_link(?APP, ssl(inet(PoolOpts))),
+            with_env(auth_req, fun load_auth_hook/1),
+            with_env(acl_req,  fun load_acl_hook/1),
+            {ok, Sup};
+        {error, Reason} ->
+            {error, Reason}
+    end.
 
 load_auth_hook(AuthReq) ->
     ok = emqx_auth_http:register_metrics(),
     SuperReq = r(application:get_env(?APP, super_req, undefined)),
-    HttpOpts = application:get_env(?APP, http_opts, []),
-    RetryOpts = application:get_env(?APP, retry_opts, []),
-    Headers = application:get_env(?APP, headers, []),
     Params = #{auth_req   => AuthReq,
                super_req  => SuperReq,
-               http_opts  => HttpOpts,
-               retry_opts => maps:from_list(RetryOpts),
-               headers    => Headers},
+               pool_name  => ?APP},
     emqx:hook('client.authenticate', {emqx_auth_http, check, [Params]}).
 
 load_acl_hook(AclReq) ->
     ok = emqx_acl_http:register_metrics(),
-    HttpOpts = application:get_env(?APP, http_opts, []),
-    RetryOpts = application:get_env(?APP, retry_opts, []),
-    Headers = application:get_env(?APP, headers, []),
-    Params = #{acl_req    => AclReq,
-               http_opts  => HttpOpts,
-               retry_opts => maps:from_list(RetryOpts),
-               headers    => Headers},
+    Params = #{acl_req   => AclReq,
+               pool_name => ?APP},
     emqx:hook('client.check_acl', {emqx_acl_http, check_acl, [Params]}).
 
 stop(_State) ->
     emqx:unhook('client.authenticate', {emqx_auth_http, check}),
-    emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}).
+    emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}),
+    emqx_http_client_sup:stop_pool(?APP).
 
 %%--------------------------------------------------------------------
 %% Dummy supervisor
@@ -85,19 +82,66 @@ with_env(Par, Fun) ->
 r(undefined) ->
     undefined;
 r(Config) ->
+    Headers = application:get_env(?APP, headers, []),
     Method = proplists:get_value(method, Config, post),
-    ContentType = proplists:get_value(content_type, Config, 'x-www-form-urlencoded'),
-    Url    = proplists:get_value(url, Config),
+    Path    = proplists:get_value(path, Config),
+    NewHeaders = [{<<"content_type">>, proplists:get_value(content_type, Config, <<"application/x-www-form-urlencoded">>)} | Headers],
     Params = proplists:get_value(params, Config),
-    #http_request{method = Method, content_type = ContentType, url = Url, params = Params, options = inet(Url)}.
-
-inet(Url) ->
-    case uri_string:parse(Url) of
-        #{host := Host} ->
-            case inet:parse_address(Host) of
-                {ok, Ip} when tuple_size(Ip) =:= 8 ->
-                    [{ipv6_host_with_brackets, true}, {socket_opts, [{ipfamily, inet6}]}];
-                _ -> []
-            end;
-        _ -> []
+    {ok, RequestTimeout} = application:get_env(?APP, request_timeout),
+    #http_request{method = Method, path = Path, headers = NewHeaders, params = Params, request_timeout = RequestTimeout}.
+
+inet(PoolOpts) ->
+    case proplists:get_value(host, PoolOpts) of
+        Host when tuple_size(Host) =:= 8 ->
+            TransOpts = proplists:get_value(transport_opts, PoolOpts, []),
+            NewPoolOpts = proplists:delete(transport_opts, PoolOpts),
+            [{transport_opts, [inet6 | TransOpts]} | NewPoolOpts];
+        _ ->
+            PoolOpts
     end.
+
+ssl(PoolOpts) ->
+    case proplists:get_value(ssl, PoolOpts, []) of
+        [] ->
+            PoolOpts;
+        SSLOpts ->
+            TransOpts = proplists:get_value(transport_opts, PoolOpts, []),
+            NewPoolOpts = proplists:delete(transport_opts, PoolOpts),
+            [{transport_opts, SSLOpts ++ TransOpts}, {transport, ssl} | NewPoolOpts]
+    end.
+
+translate_env() ->
+    URLs = lists:foldl(fun(Name, Acc) ->
+                    case application:get_env(?APP, Name, []) of
+                        [] -> Acc;
+                        Env ->
+                            URL = proplists:get_value(url, Env),
+                            #{host := Host0,
+                              port := Port,
+                              path := Path} = uri_string:parse(list_to_binary(URL)),
+                            {ok, Host} = inet:parse_address(binary_to_list(Host0)),
+                            [{Name, {Host, Port, binary_to_list(Path)}} | Acc]
+                    end
+                end, [], [acl_req, auth_req, super_req]),
+    case same_host_and_port(URLs) of
+        true ->
+            [begin
+                 {ok, Req} = application:get_env(?APP, Name),
+                 application:set_env(?APP, Name, [{path, Path} | Req])
+             end || {Name, {_, _, Path}} <- URLs],
+            {_, {Host, Port, _}} = lists:last(URLs),
+            PoolOpts = application:get_env(?APP, pool_opts, []),
+            application:set_env(?APP, pool_opts, [{host, Host}, {port, Port} | PoolOpts]),
+            ok;
+        false ->
+            {error, different_server}
+    end.
+
+same_host_and_port([_]) ->
+    true;
+same_host_and_port([{_, {Host, Port, _}}, {_, {Host, Port, _}}]) ->
+    true;
+same_host_and_port([{_, {Host, Port, _}}, URL = {_, {Host, Port, _}} | Rest]) ->
+    same_host_and_port([URL | Rest]);
+same_host_and_port(_) ->
+    false.

+ 20 - 29
apps/emqx_auth_http/src/emqx_auth_http_cli.erl

@@ -16,7 +16,9 @@
 
 -module(emqx_auth_http_cli).
 
--export([ request/8
+-include("emqx_auth_http.hrl").
+
+-export([ request/6
         , feedvar/2
         , feedvar/3
         ]).
@@ -25,36 +27,25 @@
 %% HTTP Request
 %%--------------------------------------------------------------------
 
-request(get, _ContentType, Url, Params, HttpHeaders, HttpOpts, Options, RetryOpts) ->
-    Req = {Url ++ "?" ++ cow_qs:qs(bin_kw(Params)), HttpHeaders},
-    reply(request_(get, Req, [{autoredirect, true} | HttpOpts], Options, RetryOpts));
-
-request(post, 'x-www-form-urlencoded', Url, Params, HttpHeaders, HttpOpts, Options, RetryOpts) ->
-    Req = {Url, HttpHeaders, "application/x-www-form-urlencoded", cow_qs:qs(bin_kw(Params))},
-    reply(request_(post, Req, [{autoredirect, true} | HttpOpts], Options, RetryOpts));
-
-request(post, json, Url, Params, HttpHeaders, HttpOpts, Options, RetryOpts) ->
-    Req = {Url, HttpHeaders, "application/json", emqx_json:encode(bin_kw(Params))},
-    reply(request_(post, Req, [{autoredirect, true} | HttpOpts], Options, RetryOpts)).
+request(PoolName, get, Path, Headers, Params, Timeout) ->
+    NewPath = Path ++ "?" ++ cow_qs:qs(bin_kw(Params)),
+    reply(emqx_http_client:request(get, PoolName, {NewPath, Headers}, Timeout));
 
-request_(Method, Req, HTTPOpts, Opts, RetryOpts = #{times := Times,
-                                                    interval := Interval,
-                                                    backoff := BackOff}) ->
-    case httpc:request(Method, Req, HTTPOpts, Opts) of
-        {error, _Reason} when Times > 0 ->
-            timer:sleep(trunc(Interval)),
-            RetryOpts1 = RetryOpts#{times := Times - 1,
-                                    interval := Interval * BackOff},
-            request_(Method, Req, HTTPOpts, Opts, RetryOpts1);
-        Other -> Other
-    end.
+request(PoolName, post, Path, Headers, Params, Timeout) ->
+    Body = case proplists:get_value(<<"content_type">>, Headers) of
+               <<"application/x-www-form-urlencoded">> ->
+                   cow_qs:qs(bin_kw(Params));
+               <<"application/json">> -> 
+                   emqx_json:encode(bin_kw(Params))
+           end,
+    reply(emqx_http_client:request(post, PoolName, {Path, Headers, Body}, Timeout)).
 
-reply({ok, {{_, Code, _}, _Headers, Body}}) ->
-    {ok, Code, Body};
-reply({ok, Code, Body}) ->
-    {ok, Code, Body};
-reply({error, Error}) ->
-    {error, Error}.
+reply({ok, StatusCode, _Headers}) ->
+    {ok, StatusCode, <<>>};
+reply({ok, StatusCode, _Headers, Body}) ->
+    {ok, StatusCode, Body};
+reply({error, Reason}) ->
+    {error, Reason}.
 
 %% TODO: move this conversion to cuttlefish config and schema
 bin_kw(KeywordList) when is_list(KeywordList) ->

+ 256 - 0
apps/emqx_auth_http/src/emqx_http_client.erl

@@ -0,0 +1,256 @@
+-module(emqx_http_client).
+
+-behaviour(gen_server).
+
+-include_lib("emqx/include/logger.hrl").
+
+%% APIs
+-export([ start_link/3
+        , request/3
+        , request/4
+        ]).
+
+%% gen_server callbacks
+-export([ init/1
+        , handle_call/3
+        , handle_cast/2
+        , handle_info/2
+        , terminate/2
+        , code_change/3
+        ]).
+
+-record(state, {
+          pool      :: ecpool:poo_name(),
+          id        :: pos_integer(),
+          client    :: pid() | undefined,
+          mref      :: reference() | undefined,
+          host      :: inet:hostname() | inet:ip_address(),
+          port      :: inet:port_number(),
+          gun_opts  :: proplists:proplist(),
+          gun_state :: down | up,
+          requests  :: map()
+         }).
+
+%%--------------------------------------------------------------------
+%% APIs
+%%--------------------------------------------------------------------
+
+start_link(Pool, Id, Opts) ->
+    gen_server:start_link(?MODULE, [Pool, Id, Opts], []).
+
+request(Method, Pool, Req) ->
+    request(Method, Pool, Req, 5000).
+
+request(get, Pool, {Path, Headers}, Timeout) ->
+    call(pick(Pool), {get, {Path, Headers}, Timeout}, Timeout + 1000);
+request(Method, Pool, {Path, Headers, Body}, Timeout) ->
+    call(pick(Pool), {Method, {Path, Headers, Body}, Timeout}, Timeout + 1000).
+
+%%--------------------------------------------------------------------
+%% gen_server callbacks
+%%--------------------------------------------------------------------
+
+init([Pool, Id, Opts]) ->
+    State = #state{pool = Pool,
+                   id = Id,
+                   client = undefined,
+                   mref = undefined,
+                   host = proplists:get_value(host, Opts),
+                   port = proplists:get_value(port, Opts),
+                   gun_opts = gun_opts(Opts),
+                   gun_state = down,
+                   requests = #{}},
+    true = gproc_pool:connect_worker(Pool, {Pool, Id}),
+    {ok, State}.
+
+handle_call(Req = {_, _, _}, From, State = #state{client = undefined, gun_state = down}) ->
+    case open(State) of
+        {ok, NewState} ->
+            handle_call(Req, From, NewState);
+        {error, Reason} ->
+            {reply, {error, Reason}, State}
+    end;
+
+handle_call(Req = {_, _, Timeout}, From, State = #state{client = Client, mref = MRef, gun_state = down}) when is_pid(Client) ->
+    case gun:await_up(Client, Timeout, MRef) of
+        {ok, _} ->
+            handle_call(Req, From, State#state{gun_state = up});
+        {error, timeout} ->
+            {reply, {error, timeout}, State};
+        {error, Reason} ->
+            true = erlang:demonitor(MRef, [flush]),
+            {reply, {error, Reason}, State#state{client = undefined, mref = undefined}}
+    end;
+
+handle_call({Method, Request, Timeout}, From, State = #state{client = Client, requests = Requests, gun_state = up}) when is_pid(Client) ->
+    StreamRef = do_request(Client, Method, Request),
+    ExpirationTime = erlang:system_time(millisecond) + Timeout,
+    {noreply, State#state{requests = maps:put(StreamRef, {From, ExpirationTime, undefined}, Requests)}};
+
+handle_call(Req, _From, State) ->
+    ?LOG(error, "Unexpected call: ~p", [Req]),
+    {reply, ignored, State}.
+
+handle_cast(Msg, State) ->
+    ?LOG(error, "Unexpected cast: ~p", [Msg]),
+    {noreply, State}.
+
+handle_info({gun_response, Client, StreamRef, IsFin, StatusCode, Headers}, State = #state{client = Client, requests = Requests}) ->
+    Now = erlang:system_time(millisecond),
+    case maps:take(StreamRef, Requests) of
+        error ->
+            ?LOG(error, "Received 'gun_response' message from unknown stream ref: ~p", [StreamRef]),
+            {noreply, State};
+        {{_, ExpirationTime, _}, NRequests} when Now > ExpirationTime ->
+            gun:cancel(Client, StreamRef),
+            flush_stream(Client, StreamRef),
+            {noreply, State#state{requests = NRequests}};
+        {{From, ExpirationTime, undefined}, NRequests} ->
+            case IsFin of
+                fin ->
+                    gen_server:reply(From, {ok, StatusCode, Headers}),
+                    {noreply, State#state{requests = NRequests}};
+                nofin ->
+                    {noreply, State#state{requests = NRequests#{StreamRef => {From, ExpirationTime, {StatusCode, Headers, <<>>}}}}}
+            end;
+        _ ->
+            ?LOG(error, "Received 'gun_response' message does not match the state"),
+            {noreply, State}
+    end;
+
+handle_info({gun_data, Client, StreamRef, IsFin, Data}, State = #state{client = Client, requests = Requests}) ->
+    Now = erlang:system_time(millisecond),
+    case maps:take(StreamRef, Requests) of
+        error ->
+            ?LOG(error, "Received 'gun_data' message from unknown stream ref: ~p", [StreamRef]),
+            {noreply, State};
+        {{_, ExpirationTime, _}, NRequests} when Now > ExpirationTime ->
+            gun:cancel(Client, StreamRef),
+            flush_stream(Client, StreamRef),
+            {noreply, State#state{requests = NRequests}};
+        {{From, ExpirationTime, {StatusCode, Headers, Acc}}, NRequests} ->
+            case IsFin of
+                fin ->
+                    gen_server:reply(From, {ok, StatusCode, Headers, <<Acc/binary, Data/binary>>}),
+                    {noreply, State#state{requests = NRequests}};
+                nofin ->
+                    {noreply, State#state{requests = NRequests#{StreamRef => {From, ExpirationTime, {StatusCode, Headers, <<Acc/binary, Data/binary>>}}}}}
+            end;
+        _ ->
+            ?LOG(error, "Received 'gun_data' message does not match the state"),
+            {noreply, State}
+    end;
+
+handle_info({gun_error, Client, StreamRef, Reason}, State = #state{client = Client, requests = Requests}) ->
+    Now = erlang:system_time(millisecond),
+    case maps:take(StreamRef, Requests) of
+        error ->
+            ?LOG(error, "Received 'gun_error' message from unknown stream ref: ~p~n", [StreamRef]),
+            {noreply, State};
+        {{_, ExpirationTime, _}, NRequests} when Now > ExpirationTime ->
+            {noreply, State#state{requests = NRequests}};
+        {{From, _, _}, NRequests} ->
+            gen_server:reply(From, {error, Reason}),
+            {noreply, State#state{requests = NRequests}}
+    end;
+
+handle_info({gun_up, Client, _}, State = #state{client = Client}) ->
+    {noreply, State#state{gun_state = up}};
+
+handle_info({gun_down, Client, _, Reason, KilledStreams, _}, State = #state{client = Client, requests = Requests}) ->
+    Now = erlang:system_time(millisecond),
+    NRequests = lists:foldl(fun(StreamRef, Acc) ->
+                                case maps:take(StreamRef, Acc) of
+                                    error -> Acc;
+                                    {{_, ExpirationTime, _}, NAcc} when Now > ExpirationTime ->
+                                        NAcc;
+                                    {{From, _, _}, NAcc} ->
+                                        gen_server:reply(From, {error, Reason}),
+                                        NAcc
+                                end
+                            end, Requests, KilledStreams),
+    {noreply, State#state{gun_state = down, requests = NRequests}};
+
+handle_info({'DOWN', MRef, process, Client, Reason}, State = #state{mref = MRef, client = Client, requests = Requests}) ->
+    true = erlang:demonitor(MRef, [flush]),
+    Now = erlang:system_time(millisecond),
+    lists:foreach(fun({_, {_, ExpirationTime, _}}) when Now > ExpirationTime ->
+                      ok;
+                     ({_, {From, _, _}}) ->
+                      gen_server:reply(From, {error, Reason})
+                  end, maps:to_list(Requests)),
+    case open(State#state{requests = #{}}) of
+        {ok, NewState} ->
+            {noreply, NewState};
+        {error, Reason} ->
+            {noreply, State#state{mref = undefined, client = undefined}}
+    end;
+
+handle_info(Info, State) ->
+    ?LOG(error, "Unexpected info: ~p", [Info]),
+    {noreply, State}.
+
+terminate(_Reason, #state{pool = Pool, id = Id}) ->
+    gropc:disconnect_worker(Pool, {Pool, Id}),
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Internal functions
+%%--------------------------------------------------------------------
+
+open(State = #state{host = Host, port = Port, gun_opts = GunOpts}) ->
+    case gun:open(Host, Port, GunOpts) of
+        {ok, ConnPid} when is_pid(ConnPid) ->
+            MRef = monitor(process, ConnPid),
+            {ok, State#state{mref = MRef, client = ConnPid}};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+gun_opts(Opts) ->
+    gun_opts(Opts, #{retry => 5,
+                     retry_timeout => 1000,
+                     connect_timeout => 5000,
+                     protocols => [http],
+                     http_opts => #{keepalive => infinity}}).
+
+gun_opts([], Acc) ->
+    Acc;
+gun_opts([{retry, Retry} | Opts], Acc) ->
+    gun_opts(Opts, Acc#{retry => Retry});
+gun_opts([{retry_timeout, RetryTimeout} | Opts], Acc) ->
+    gun_opts(Opts, Acc#{retry_timeout => RetryTimeout});
+gun_opts([{connect_timeout, ConnectTimeout} | Opts], Acc) ->
+    gun_opts(Opts, Acc#{connect_timeout => ConnectTimeout});
+gun_opts([{transport, Transport} | Opts], Acc) ->
+    gun_opts(Opts, Acc#{transport => Transport});
+gun_opts([{transport_opts, TransportOpts} | Opts], Acc) ->
+    gun_opts(Opts, Acc#{transport_opts => TransportOpts});
+gun_opts([_ | Opts], Acc) ->
+    gun_opts(Opts, Acc).
+
+call(ChannPid, Msg, Timeout) ->
+    gen_server:call(ChannPid, Msg, Timeout).
+
+pick(Pool) ->
+    gproc_pool:pick_worker(Pool).
+
+do_request(Client, get, {Path, Headers}) ->
+    gun:get(Client, Path, Headers);
+do_request(Client, post, {Path, Headers, Body}) ->
+    gun:post(Client, Path, Headers, Body).
+
+flush_stream(Client, StreamRef) ->
+    receive
+        {gun_response, Client, StreamRef, _, _, _} ->
+            flush_stream(Client, StreamRef);
+        {gun_data, Client, StreamRef, _, _} ->
+            flush_stream(Client, StreamRef);
+        {gun_error, Client, StreamRef, _} ->
+            flush_stream(Client, StreamRef)
+	after 0 ->
+		ok
+	end.

+ 48 - 0
apps/emqx_auth_http/src/emqx_http_client_sup.erl

@@ -0,0 +1,48 @@
+-module(emqx_http_client_sup).
+
+-behaviour(supervisor).
+
+-export([ start_link/2
+        , init/1
+        , stop_pool/1
+        ]).
+
+start_link(Pool, Opts) ->
+    supervisor:start_link(?MODULE, [Pool, Opts]).
+
+init([Pool, Opts]) ->
+    PoolSize = pool_size(Opts),
+    ok = ensure_pool(Pool, random, [{size, PoolSize}]),
+    {ok, {{one_for_one, 10, 100}, [
+        begin
+            ensure_pool_worker(Pool, {Pool, I}, I),
+            #{id => {Pool, I},
+              start => {emqx_http_client, start_link, [Pool, I, Opts]},
+              restart => transient,
+              shutdown => 5000,
+              type => worker,
+              modules => [emqx_http_client]}
+        end || I <- lists:seq(1, PoolSize)]}}.
+
+
+ensure_pool(Pool, Type, Opts) ->
+    try gproc_pool:new(Pool, Type, Opts)
+    catch
+        error:exists -> ok
+    end.
+
+ensure_pool_worker(Pool, Name, Slot) ->
+    try gproc_pool:add_worker(Pool, Name, Slot)
+    catch
+        error:exists -> ok
+    end.
+
+pool_size(Opts) ->
+    Schedulers = erlang:system_info(schedulers),
+    proplists:get_value(pool_size, Opts, Schedulers).
+
+stop_pool(Name) ->
+    Workers = gproc_pool:defined_workers(Name),
+    [gproc_pool:remove_worker(Name, WokerName) || {WokerName, _, _} <- Workers],
+    gproc_pool:delete(Name),
+    ok.

+ 24 - 18
apps/emqx_auth_http/test/emqx_auth_http_SUITE.erl

@@ -64,32 +64,38 @@ set_special_configs(emqx, _Schmea, _Inet) ->
                         emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
 
 set_special_configs(emqx_auth_http, Schema, Inet) ->
-    AuthReq = maps:from_list(application:get_env(emqx_auth_http, auth_req, [])),
-    SuprReq = maps:from_list(application:get_env(emqx_auth_http, super_req, [])),
-    AclReq  = maps:from_list(application:get_env(emqx_auth_http, acl_req, [])),
-    SvrAddr = http_server_host(Schema, Inet),
-
-    AuthReq1 = AuthReq#{method := get, url := SvrAddr ++ "/mqtt/auth"},
-    SuprReq1 = SuprReq#{method := post, content_type := 'x-www-form-urlencoded', url := SvrAddr ++ "/mqtt/superuser"},
-    AclReq1  = AclReq #{method := post, content_type := json, url := SvrAddr ++ "/mqtt/acl"},
+    ServerAddr = http_server(Schema, Inet),
+
+    AuthReq = #{method => get,
+                url => ServerAddr ++ "/mqtt/auth",
+                content_type => <<"application/x-www-form-urlencoded">>,
+                params => [{"clientid", "%c"}, {"username", "%u"}, {"password", "%P"}]},
+    SuperReq = #{method => post,
+                 url => ServerAddr ++ "/mqtt/superuser",
+                 content_type => <<"application/x-www-form-urlencoded">>,
+                 params => [{"clientid", "%c"}, {"username", "%u"}]},
+    AclReq = #{method => post,
+               url => ServerAddr ++ "/mqtt/acl",
+               content_type => <<"application/json">>,
+               params => [{"access", "%A"}, {"username", "%u"}, {"clientid", "%c"}, {"ipaddr", "%a"}, {"topic", "%t"}, {"mountpoint", "%m"}]},
 
     Schema =:= https andalso set_https_client_opts(),
 
-    application:set_env(emqx_auth_http, auth_req, maps:to_list(AuthReq1)),
-    application:set_env(emqx_auth_http, super_req, maps:to_list(SuprReq1)),
-    application:set_env(emqx_auth_http, acl_req, maps:to_list(AclReq1)).
+    application:set_env(emqx_auth_http, auth_req, maps:to_list(AuthReq)),
+    application:set_env(emqx_auth_http, super_req, maps:to_list(SuperReq)),
+    application:set_env(emqx_auth_http, acl_req, maps:to_list(AclReq)).
 
 %% @private
 set_https_client_opts() ->
-    HttpOpts = maps:from_list(application:get_env(emqx_auth_http, http_opts, [])),
-    HttpOpts1 = HttpOpts#{ssl => emqx_ct_helpers:client_ssl_twoway()},
-    application:set_env(emqx_auth_http, http_opts, maps:to_list(HttpOpts1)).
+    TransportOpts = emqx_ct_helpers:client_ssl_twoway(),
+    {ok, PoolOpts} = application:get_env(emqx_auth_http, pool_opts),
+    application:set_env(emqx_auth_http, pool_opts, [{transport_opts, TransportOpts}, {transport, ssl} | PoolOpts]).
 
 %% @private
-http_server_host(http, inet) -> "http://127.0.0.1:8991";
-http_server_host(http, inet6) -> "http://[::1]:8991";
-http_server_host(https, inet) -> "https://127.0.0.1:8991";
-http_server_host(https, inet6) -> "https://[::1]:8991".
+http_server(http, inet) -> "http://127.0.0.1:8991";
+http_server(http, inet6) -> "http://[::1]:8991";
+http_server(https, inet) -> "https://127.0.0.1:8991";
+http_server(https, inet6) -> "https://[::1]:8991".
 
 %%------------------------------------------------------------------------------
 %% Testcases

+ 1 - 0
apps/emqx_auth_jwt/.gitignore

@@ -25,3 +25,4 @@ rebar3.crashdump
 etc/emqx_auth_jwt.conf.rendered
 .rebar3/
 *.swp
+Mnesia.nonode@nohost/

+ 17 - 11
apps/emqx_auth_jwt/etc/emqx_auth_jwt.conf

@@ -7,17 +7,28 @@
 ## Value: String
 auth.jwt.secret = emqxsecret
 
+## RSA or ECDSA public key file.
+##
+## Value: File
+#auth.jwt.pubkey = etc/certs/jwt_public_key.pem
+
+## The JWKs server address
+##
+## see: http://self-issued.info/docs/draft-ietf-jose-json-web-key.html
+##
+#auth.jwt.jwks = https://127.0.0.1:8080/jwks
+
+## The JWKs refresh interval
+##
+## Value: Duration
+#auth.jwt.jwks.refresh_interval = 5m
+
 ## From where the JWT string can be got
 ##
 ## Value: username | password
 ## Default: password
 auth.jwt.from = password
 
-## RSA or ECDSA public key file.
-##
-## Value: File
-## auth.jwt.pubkey = etc/certs/jwt_public_key.pem
-
 ## Enable to verify claims fields
 ##
 ## Value: on | off
@@ -31,9 +42,4 @@ auth.jwt.verify_claims = off
 ## Variables:
 ##  - %u: username
 ##  - %c: clientid
-# auth.jwt.verify_claims.username = %u
-
-## The Signature format
-##   - `der`: The erlang default format
-##   - `raw`: Compatible with others platform maybe
-#auth.jwt.signature_format = der
+#auth.jwt.verify_claims.username = %u

+ 13 - 12
apps/emqx_auth_jwt/priv/emqx_auth_jwt.schema

@@ -4,6 +4,14 @@
   {datatype, string}
 ]}.
 
+{mapping, "auth.jwt.jwks", "emqx_auth_jwt.jwks", [
+  {datatype, string}
+]}.
+
+{mapping, "auth.jwt.jwks.refresh_interval", "emqx_auth_jwt.refresh_interval", [
+  {datatype, {duration, ms}}
+]}.
+
 {mapping, "auth.jwt.from", "emqx_auth_jwt.from", [
   {default, password},
   {datatype, atom}
@@ -13,6 +21,11 @@
   {datatype, string}
 ]}.
 
+{mapping, "auth.jwt.signature_format", "emqx_auth_jwt.jwerl_opts", [
+  {default, "der"},
+  {datatype, {enum, [raw, der]}}
+]}.
+
 {mapping, "auth.jwt.verify_claims", "emqx_auth_jwt.verify_claims", [
   {default, off},
   {datatype, flag}
@@ -34,15 +47,3 @@
               end, [], cuttlefish_variable:filter_by_prefix("auth.jwt.verify_claims", Conf))
    end
 end}.
-
-{mapping, "auth.jwt.signature_format", "emqx_auth_jwt.jwerl_opts", [
-  {default, "der"},
-  {datatype, {enum, [raw, der]}}
-]}.
-
-{translation, "emqx_auth_jwt.jwerl_opts", fun(Conf) ->
-    Filter = fun(L) -> [I || I <- L, I /= undefined] end,
-    maps:from_list(Filter(
-        [{raw, cuttlefish:conf_get("auth.jwt.signature_format", Conf) == raw}]
-    ))
-end}.

+ 2 - 1
apps/emqx_auth_jwt/rebar.config

@@ -1,5 +1,6 @@
 {deps,
- [{jwerl, {git, "https://github.com/emqx/jwerl.git", {branch, "1.1.1"}}}
+ [
+  {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.10.1"}}}
  ]}.
 
 {edoc_opts, [{preprocess, true}]}.

+ 1 - 1
apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src

@@ -3,7 +3,7 @@
   {vsn, "4.3.0"}, % strict semver, bump manually!
   {modules, []},
   {registered, [emqx_auth_jwt_sup]},
-  {applications, [kernel,stdlib,jwerl,emqx]},
+  {applications, [kernel,stdlib,jose,emqx]},
   {mod, {emqx_auth_jwt_app, []}},
   {env, []},
   {licenses, ["Apache-2.0"]},

+ 10 - 0
apps/emqx_auth_jwt/src/emqx_auth_jwt.appup.src

@@ -0,0 +1,10 @@
+%% -*-: erlang -*-
+
+{VSN,
+  [
+    {<<".*">>, []}
+  ],
+  [
+    {<<".*">>, []}
+  ]
+}.

+ 17 - 64
apps/emqx_auth_jwt/src/emqx_auth_jwt.erl

@@ -46,77 +46,31 @@ register_metrics() ->
 %% Authentication callbacks
 %%--------------------------------------------------------------------
 
-check(ClientInfo, AuthResult, Env = #{from := From, checklists := Checklists}) ->
+check(ClientInfo, AuthResult, #{pid := Pid,
+                                from := From,
+                                checklists := Checklists}) ->
     case maps:find(From, ClientInfo) of
         error ->
-            ok = emqx_metrics:inc(?AUTH_METRICS(ignore)),
-            {ok, AuthResult#{auth_result => token_undefined, anonymous => false}};
+            ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
+        {ok, undefined} ->
+            ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
         {ok, Token} ->
-            try jwerl:header(Token) of
-                Headers ->
-                    case verify_token(Headers, Token, Env) of
-                        {ok, Claims} ->
-                            {stop, maps:merge(AuthResult, verify_claims(Checklists, Claims, ClientInfo))};
-                        {error, Reason} ->
-                            ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
-                            {stop, AuthResult#{auth_result => Reason, anonymous => false}}
-                    end
-            catch
-                _Error:Reason ->
-                    ?LOG(error, "Check token error: ~p", [Reason]),
-                    emqx_metrics:inc(?AUTH_METRICS(ignore))
+            case emqx_auth_jwt_svr:verify(Pid, Token) of
+                {error, not_found} ->
+                    ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
+                {error, not_token} ->
+                    ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
+                {error, Reason} ->
+                    ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
+                    {stop, AuthResult#{auth_result => Reason, anonymous => false}};
+                {ok, Claims} ->
+                    {stop, maps:merge(AuthResult, verify_claims(Checklists, Claims, ClientInfo))}
             end
     end.
 
 description() -> "Authentication with JWT".
 
-%%--------------------------------------------------------------------
-%% Verify Token
-%%--------------------------------------------------------------------
-
-verify_token(#{alg := <<"HS", _/binary>>}, _Token, #{secret := undefined}) ->
-    {error, hmac_secret_undefined};
-verify_token(#{alg := Alg = <<"HS", _/binary>>}, Token, #{secret := Secret, opts := Opts}) ->
-    verify_token2(Alg, Token, Secret, Opts);
-
-verify_token(#{alg := <<"RS", _/binary>>}, _Token, #{pubkey := undefined}) ->
-    {error, rsa_pubkey_undefined};
-verify_token(#{alg := Alg = <<"RS", _/binary>>}, Token, #{pubkey := PubKey, opts := Opts}) ->
-    verify_token2(Alg, Token, PubKey, Opts);
-
-verify_token(#{alg := <<"ES", _/binary>>}, _Token, #{pubkey := undefined}) ->
-    {error, ecdsa_pubkey_undefined};
-verify_token(#{alg := Alg = <<"ES", _/binary>>}, Token, #{pubkey := PubKey, opts := Opts}) ->
-    verify_token2(Alg, Token, PubKey, Opts);
-
-verify_token(Header, _Token, _Env) ->
-    ?LOG(error, "Unsupported token algorithm: ~p", [Header]),
-    {error, token_unsupported}.
-
-verify_token2(Alg, Token, SecretOrKey, Opts) ->
-    try jwerl:verify(Token, decode_algo(Alg), SecretOrKey, #{}, Opts) of
-        {ok, Claims}  ->
-            {ok, Claims};
-        {error, Reason} ->
-            {error, Reason}
-    catch
-        _Error:Reason ->
-            {error, Reason}
-    end.
-
-decode_algo(<<"HS256">>) -> hs256;
-decode_algo(<<"HS384">>) -> hs384;
-decode_algo(<<"HS512">>) -> hs512;
-decode_algo(<<"RS256">>) -> rs256;
-decode_algo(<<"RS384">>) -> rs384;
-decode_algo(<<"RS512">>) -> rs512;
-decode_algo(<<"ES256">>) -> es256;
-decode_algo(<<"ES384">>) -> es384;
-decode_algo(<<"ES512">>) -> es512;
-decode_algo(<<"none">>)  -> none;
-decode_algo(Alg) -> throw({error, {unsupported_algorithm, Alg}}).
-
-%%--------------------------------------------------------------------
+%%------------------------------------------------------------------------------
 %% Verify Claims
 %%--------------------------------------------------------------------
 
@@ -143,4 +97,3 @@ feedvar(Checklists, #{username := Username, clientid := ClientId}) ->
                  ({K, <<"%c">>}) -> {K, ClientId};
                  ({K, Expected}) -> {K, Expected}
               end, Checklists).
-

+ 32 - 19
apps/emqx_auth_jwt/src/emqx_auth_jwt_app.erl

@@ -28,42 +28,55 @@
 
 -define(APP, emqx_auth_jwt).
 
--define(JWT_ACTION, {emqx_auth_jwt, check, [auth_env()]}).
-
 start(_Type, _Args) ->
+    {ok, Sup} = supervisor:start_link({local, ?MODULE}, ?MODULE, []),
+
+    {ok, Pid} = start_auth_server(jwks_svr_options()),
     ok = emqx_auth_jwt:register_metrics(),
-    emqx:hook('client.authenticate', ?JWT_ACTION),
-    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
-stop(_State) ->
-    emqx:unhook('client.authenticate', ?JWT_ACTION).
+    AuthEnv0 = auth_env(),
+    AuthEnv1 = AuthEnv0#{pid => Pid},
+
+    emqx:hook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv1]}),
+    {ok, Sup, AuthEnv1}.
+
+stop(AuthEnv) ->
+    emqx:unhook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv]}).
 
 %%--------------------------------------------------------------------
 %% Dummy supervisor
 %%--------------------------------------------------------------------
 
 init([]) ->
-    {ok, { {one_for_all, 1, 10}, []} }.
+    {ok, {{one_for_all, 1, 10}, []}}.
+
+start_auth_server(Options) ->
+    Spec = #{id => jwt_svr,
+             start => {emqx_auth_jwt_svr, start_link, [Options]},
+             restart => permanent,
+             shutdown => brutal_kill,
+             type => worker,
+             modules => [emqx_auth_jwt_svr]},
+    supervisor:start_child(?MODULE, Spec).
 
 %%--------------------------------------------------------------------
 %% Internal functions
 %%--------------------------------------------------------------------
 
 auth_env() ->
-    #{secret     => env(secret, undefined),
-      from       => env(from, password),
-      pubkey     => read_pubkey(),
-      checklists => env(verify_claims, []),
-      opts       => env(jwerl_opts, #{})
+    Checklists = [{atom_to_binary(K, utf8), V}
+                  || {K, V} <- env(verify_claims, [])],
+    #{ from => env(from, password)
+     , checklists => Checklists
      }.
 
-read_pubkey() ->
-    case env(pubkey, undefined) of
-        undefined  -> undefined;
-        Path ->
-            {ok, PubKey} = file:read_file(Path), PubKey
-    end.
+jwks_svr_options() ->
+    [{K, V} || {K, V}
+             <- [{secret, env(secret, undefined)},
+                 {pubkey, env(pubkey, undefined)},
+                 {jwks_addr, env(jwks, undefined)},
+                 {interval, env(refresh_interval, undefined)}],
+             V /= undefined].
 
 env(Key, Default) ->
     application:get_env(?APP, Key, Default).
-

+ 222 - 0
apps/emqx_auth_jwt/src/emqx_auth_jwt_svr.erl

@@ -0,0 +1,222 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_auth_jwt_svr).
+
+-behaviour(gen_server).
+
+-include_lib("emqx/include/logger.hrl").
+-include_lib("jose/include/jose_jwk.hrl").
+
+-logger_header("[JWT-SVR]").
+
+%% APIs
+-export([start_link/1]).
+
+-export([verify/2]).
+
+%% gen_server callbacks
+-export([ init/1
+        , handle_call/3
+        , handle_cast/2
+        , handle_info/2
+        , terminate/2
+        , code_change/3
+        ]).
+
+-type options() :: [option()].
+-type option() :: {secret, list()}
+                | {pubkey, list()}
+                | {jwks_addr, list()}
+                | {interval, pos_integer()}.
+
+-define(INTERVAL, 300000).
+
+-record(state, {static, remote, addr, tref, intv}).
+
+%%--------------------------------------------------------------------
+%% APIs
+%%--------------------------------------------------------------------
+
+-spec start_link(options()) -> gen_server:start_ret().
+start_link(Options) ->
+    gen_server:start_link(?MODULE, [Options], []).
+
+-spec verify(pid(), binary())
+    -> {error, term()}
+     | {ok, Payload :: map()}.
+verify(S, JwsCompacted) when is_binary(JwsCompacted) ->
+    case catch jose_jws:peek(JwsCompacted) of
+        {'EXIT', _} -> {error, not_token};
+        _ -> gen_server:call(S, {verify, JwsCompacted})
+    end.
+
+%%--------------------------------------------------------------------
+%% gen_server callbacks
+%%--------------------------------------------------------------------
+
+init([Options]) ->
+    ok = jose:json_module(jiffy),
+    {Static, Remote} = do_init_jwks(Options),
+    Intv = proplists:get_value(interval, Options, ?INTERVAL),
+    {ok, reset_timer(
+           #state{
+              static = Static,
+              remote = Remote,
+              addr = proplists:get_value(jwks_addr, Options),
+              intv = Intv})}.
+
+%% @private
+do_init_jwks(Options) ->
+    K2J = fun(K, F) ->
+              case proplists:get_value(K, Options) of
+                  undefined -> undefined;
+                  V ->
+                     try F(V) of
+                         {error, Reason} ->
+                             ?LOG(warning, "Build ~p JWK ~p failed: {error, ~p}~n",
+                                  [K, V, Reason]),
+                             undefined;
+                         J -> J
+                     catch T:R:_ ->
+                         ?LOG(warning, "Build ~p JWK ~p failed: {~p, ~p}~n",
+                              [K, V, T, R]),
+                         undefined
+                     end
+              end
+          end,
+    OctJwk = K2J(secret, fun(V) ->
+                             jose_jwk:from_oct(list_to_binary(V))
+                         end),
+    PemJwk = K2J(pubkey, fun jose_jwk:from_pem_file/1),
+    Remote = K2J(jwks_addr, fun request_jwks/1),
+    {[J ||J <- [OctJwk, PemJwk], J /= undefined], Remote}.
+
+handle_call({verify, JwsCompacted}, _From, State) ->
+    handle_verify(JwsCompacted, State);
+
+handle_call(_Req, _From, State) ->
+    {reply, ok, State}.
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info({timeout, _TRef, refresh}, State = #state{addr = Addr}) ->
+    NState = try
+                 State#state{remote = request_jwks(Addr)}
+             catch _:_ ->
+                 State
+             end,
+    {noreply, reset_timer(NState)};
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+terminate(_Reason, State) ->
+    _ = cancel_timer(State),
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Internal funcs
+%%--------------------------------------------------------------------
+
+handle_verify(JwsCompacted,
+              State = #state{static = Static, remote = Remote}) ->
+    try
+        Jwks = case emqx_json:decode(jose_jws:peek_protected(JwsCompacted), [return_maps]) of
+                   #{<<"kid">> := Kid} ->
+                       [J || J <- Remote, maps:get(<<"kid">>, J#jose_jwk.fields, undefined) =:= Kid];
+                   _ -> Static
+               end,
+        case Jwks of
+            [] -> {reply, {error, not_found}, State};
+            _ ->
+                {reply, do_verify(JwsCompacted, Jwks), State}
+        end
+    catch
+        _:_ ->
+            {reply, {error, invalid_signature}, State}
+    end.
+
+request_jwks(Addr) ->
+    case httpc:request(get, {Addr, []}, [], [{body_format, binary}]) of
+        {error, Reason} ->
+            error(Reason);
+        {ok, {_Code, _Headers, Body}} ->
+            try
+                JwkSet = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
+                {_, Jwks} = JwkSet#jose_jwk.keys, Jwks
+            catch _:_ ->
+                ?LOG(error, "Invalid jwks server response: ~p~n", [Body]),
+                error(badarg)
+            end
+    end.
+
+reset_timer(State = #state{addr = undefined}) ->
+    State;
+reset_timer(State = #state{intv = Intv}) ->
+    State#state{tref = erlang:start_timer(Intv, self(), refresh)}.
+
+cancel_timer(State = #state{tref = undefined}) ->
+    State;
+cancel_timer(State = #state{tref = TRef}) ->
+    erlang:cancel_timer(TRef),
+    State#state{tref = undefined}.
+
+do_verify(_JwsCompated, []) ->
+    {error, invalid_signature};
+do_verify(JwsCompacted, [Jwk|More]) ->
+    case jose_jws:verify(Jwk, JwsCompacted) of
+        {true, Payload, _Jws} ->
+            Claims = emqx_json:decode(Payload, [return_maps]),
+            case check_claims(Claims) of
+                false ->
+                    {error, invalid_signature};
+                NClaims ->
+                    {ok, NClaims}
+            end;
+        {false, _, _} ->
+            do_verify(JwsCompacted, More)
+    end.
+
+check_claims(Claims) ->
+    Now = os:system_time(seconds),
+    Checker = [{<<"exp">>, fun(ExpireTime) ->
+                               Now < ExpireTime
+                           end},
+               {<<"iat">>, fun(IssueAt) ->
+                               IssueAt =< Now
+                           end},
+               {<<"nbf">>, fun(NotBefore) ->
+                               NotBefore =< Now
+                           end}
+              ],
+    do_check_claim(Checker, Claims).
+
+do_check_claim([], Claims) ->
+    Claims;
+do_check_claim([{K, F}|More], Claims) ->
+    case maps:take(K, Claims) of
+        error -> do_check_claim(More, Claims);
+        {V, NClaims} ->
+            case F(V) of
+                true -> do_check_claim(More, NClaims);
+                _ -> false
+            end
+    end.

+ 29 - 24
apps/emqx_auth_jwt/test/emqx_auth_jwt_SUITE.erl

@@ -16,8 +16,8 @@
 
 -module(emqx_auth_jwt_SUITE).
 
--compile(nowarn_export_all).
 -compile(export_all).
+-compile(nowarn_export_all).
 
 -include_lib("emqx/include/emqx.hrl").
 -include_lib("eunit/include/eunit.hrl").
@@ -61,28 +61,34 @@ set_special_configs(emqx_auth_jwt) ->
 set_special_configs(_) ->
     ok.
 
+sign(Payload, Alg, Key) ->
+    Jwk = jose_jwk:from_oct(Key),
+    Jwt = emqx_json:encode(Payload),
+    {_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, #{<<"alg">> => Alg}, Jwt)),
+    Token.
+
 %%------------------------------------------------------------------------------
 %% Testcases
 %%------------------------------------------------------------------------------
 
 t_check_auth(_) ->
     Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
-    Jwt = jwerl:sign([{clientid, <<"client1">>},
-                      {username, <<"plain">>},
-                      {exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
+    Jwt = sign([{clientid, <<"client1">>},
+                {username, <<"plain">>},
+                {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
     ct:pal("Jwt: ~p~n", [Jwt]),
 
     Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
     ct:pal("Auth result: ~p~n", [Result0]),
-    ?assertMatch({ok, #{auth_result := success, jwt_claims := #{clientid := <<"client1">>}}}, Result0),
+    ?assertMatch({ok, #{auth_result := success, jwt_claims := #{<<"clientid">> := <<"client1">>}}}, Result0),
 
     ct:sleep(3100),
     Result1 = emqx_access_control:authenticate(Plain#{password => Jwt}),
     ct:pal("Auth result after 1000ms: ~p~n", [Result1]),
     ?assertMatch({error, _}, Result1),
 
-    Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
-                            {username, <<"plain">>}], hs256, <<"secret">>),
+    Jwt_Error = sign([{client_id, <<"client1">>},
+                      {username, <<"plain">>}], <<"HS256">>, <<"secret">>),
     ct:pal("invalid jwt: ~p~n", [Jwt_Error]),
     Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
     ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
@@ -92,15 +98,15 @@ t_check_auth(_) ->
 t_check_claims(_) ->
     application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]),
     Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
-    Jwt = jwerl:sign([{clientid, <<"client1">>},
-                      {username, <<"plain">>},
-                      {sub, value},
-                      {exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
+    Jwt = sign([{client_id, <<"client1">>},
+                {username, <<"plain">>},
+                {sub, value},
+                {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
     Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
     ct:pal("Auth result: ~p~n", [Result0]),
     ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
-    Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
-                            {username, <<"plain">>}], hs256, <<"secret">>),
+    Jwt_Error = sign([{clientid, <<"client1">>},
+                       {username, <<"plain">>}], <<"HS256">>, <<"secret">>),
     Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
     ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
     ?assertEqual({error, invalid_signature}, Result2).
@@ -108,14 +114,14 @@ t_check_claims(_) ->
 t_check_claims_clientid(_) ->
     application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]),
     Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
-    Jwt = jwerl:sign([{clientid, <<"client23">>},
-                      {username, <<"plain">>},
-                      {exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
+    Jwt = sign([{client_id, <<"client23">>},
+                {username, <<"plain">>},
+                {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
     Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
     ct:pal("Auth result: ~p~n", [Result0]),
     ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
-    Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
-                            {username, <<"plain">>}], hs256, <<"secret">>),
+    Jwt_Error = sign([{clientid, <<"client1">>},
+                      {username, <<"plain">>}], <<"HS256">>, <<"secret">>),
     Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
     ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
     ?assertEqual({error, invalid_signature}, Result2).
@@ -123,15 +129,14 @@ t_check_claims_clientid(_) ->
 t_check_claims_username(_) ->
     application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]),
     Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
-    Jwt = jwerl:sign([{clientid, <<"client23">>},
-                      {username, <<"plain">>},
-                      {exp, os:system_time(seconds) + 3}], hs256, <<"emqxsecret">>),
+    Jwt = sign([{client_id, <<"client23">>},
+                {username, <<"plain">>},
+                {exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
     Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
     ct:pal("Auth result: ~p~n", [Result0]),
     ?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
-    Jwt_Error = jwerl:sign([{clientid, <<"client1">>},
-                            {username, <<"plain">>}], hs256, <<"secret">>),
+    Jwt_Error = sign([{clientid, <<"client1">>},
+                      {username, <<"plain">>}], <<"HS256">>, <<"secret">>),
     Result3 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
     ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]),
     ?assertEqual({error, invalid_signature}, Result3).
-

+ 6 - 0
apps/emqx_auth_ldap/rebar.config

@@ -2,6 +2,12 @@
  [{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}
  ]}.
 
+{profiles,
+ [{test,
+   [{deps, [{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "1.2.2"}}}]}
+   ]}
+ ]}.
+
 {edoc_opts, [{preprocess, true}]}.
 {erl_opts, [warn_unused_vars,
             warn_shadow_vars,

+ 1 - 0
apps/emqx_auth_mongo/rebar.config

@@ -21,3 +21,4 @@
 {cover_enabled, true}.
 {cover_opts, [verbose]}.
 {cover_export_enabled, true}.
+

+ 9 - 0
apps/emqx_auth_mysql/src/emqx_auth_mysql.appup.src

@@ -0,0 +1,9 @@
+%% -*-: erlang -*-
+{VSN,
+   [
+     {<<".*">>, []}
+   ],
+   [
+     {<<".*">>, []}
+   ]
+}.

+ 9 - 0
apps/emqx_auth_pgsql/src/emqx_auth_pgsql.appup.src

@@ -0,0 +1,9 @@
+%% -*-: erlang -*-
+{VSN,
+   [
+     {<<".*">>, []}
+   ],
+   [
+     {<<".*">>, []}
+   ]
+}.

+ 2 - 0
apps/emqx_auth_redis/.gitignore

@@ -22,3 +22,5 @@ erlang.mk
 .rebar3/
 *.swp
 rebar.lock
+/.idea/
+.DS_Store

+ 1 - 1
apps/emqx_auth_redis/rebar.config

@@ -1,5 +1,5 @@
 {deps,
- [{eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.6.2"}}}
+ [{eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.6.3"}}}
  ]}.
 
 {erl_opts, [warn_unused_vars,

+ 10 - 0
apps/emqx_auth_redis/src/emqx_auth_redis.appup.src

@@ -0,0 +1,10 @@
+%% -*-: erlang -*-
+
+{VSN,
+   [
+     {<<".*">>, []}
+   ],
+   [
+     {<<".*">>, []}
+   ]
+}.

+ 1 - 1
apps/emqx_auth_redis/src/emqx_auth_redis_sup.erl

@@ -35,7 +35,7 @@ pool_spec(Server) ->
     Options = application:get_env(?APP, options, []),
     case proplists:get_value(type, Server) of
         cluster ->
-            eredis_cluster:start_pool(?APP, Server ++ Options),
+            {ok, _} = eredis_cluster:start_pool(?APP, Server ++ Options),
             [];
         _ ->
             [ecpool:pool_spec(?APP, ?APP, emqx_auth_redis_cli, Server ++ Options)]

+ 10 - 0
apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.appup.src

@@ -0,0 +1,10 @@
+%% -*-: erlang -*-
+
+{VSN,
+  [
+    {<<".*">>, []}
+  ],
+  [
+    {<<"*.">>, []}
+  ]
+}.

+ 12 - 6
apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl

@@ -31,6 +31,12 @@
         , ensure_unsubscribed/2
         ]).
 
+%% callbacks for emqtt
+-export([ handle_puback/2
+        , handle_publish/2
+        , handle_disconnected/2
+        ]).
+
 -include_lib("emqx/include/logger.hrl").
 -include_lib("emqx/include/emqx_mqtt.hrl").
 
@@ -134,23 +140,23 @@ send(#{client_pid := ClientPid} = Conn, [Msg | Rest], _PktId) ->
     end.
 
 
-handle_puback(Parent, #{packet_id := PktId, reason_code := RC})
+handle_puback(#{packet_id := PktId, reason_code := RC}, Parent)
   when RC =:= ?RC_SUCCESS;
        RC =:= ?RC_NO_MATCHING_SUBSCRIBERS ->
     Parent ! {batch_ack, PktId}, ok;
-handle_puback(_Parent, #{packet_id := PktId, reason_code := RC}) ->
+handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) ->
     ?LOG(warning, "Publish ~p to remote node falied, reason_code: ~p", [PktId, RC]).
 
 handle_publish(Msg, Mountpoint) ->
     emqx_broker:publish(emqx_bridge_msg:to_broker_msg(Msg, Mountpoint)).
 
-handle_disconnected(Parent, Reason) ->
+handle_disconnected(Reason, Parent) ->
     Parent ! {disconnected, self(), Reason}.
 
 make_hdlr(Parent, Mountpoint) ->
-    #{puback => fun(Ack) -> handle_puback(Parent, Ack) end,
-      publish => fun(Msg) -> handle_publish(Msg, Mountpoint) end,
-      disconnected => fun(Reason) -> handle_disconnected(Parent, Reason) end
+    #{puback => {fun ?MODULE:handle_puback/2, [Parent]},
+      publish => {fun ?MODULE:handle_publish/2, [Mountpoint]},
+      disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]}
      }.
 
 subscribe_remote_topics(ClientPid, Subscriptions) ->

+ 34 - 21
apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_actions.erl

@@ -20,6 +20,7 @@
 
 -include_lib("emqx/include/emqx.hrl").
 -include_lib("emqx/include/logger.hrl").
+-include_lib("emqx_rule_engine/include/rule_actions.hrl").
 
 -import(emqx_rule_utils, [str/1]).
 
@@ -33,7 +34,9 @@
 
 -export([subscriptions/1]).
 
--export([on_action_create_data_to_mqtt_broker/2]).
+-export([ on_action_create_data_to_mqtt_broker/2
+        , on_action_data_to_mqtt_broker/2
+        ]).
 
 -define(RESOURCE_TYPE_MQTT, 'bridge_mqtt').
 -define(RESOURCE_TYPE_MQTT_SUB, 'bridge_mqtt_sub').
@@ -625,32 +628,42 @@ on_resource_destroy(ResId, #{<<"pool">> := PoolName}) ->
                 error({{?RESOURCE_TYPE_MQTT, ResId}, destroy_failed})
         end.
 
-on_action_create_data_to_mqtt_broker(_Id, #{<<"pool">> := PoolName,
-                                            <<"forward_topic">> := ForwardTopic,
-                                            <<"payload_tmpl">> := PayloadTmpl}) ->
+on_action_create_data_to_mqtt_broker(ActId, Opts = #{<<"pool">> := PoolName,
+                                                     <<"forward_topic">> := ForwardTopic,
+                                                     <<"payload_tmpl">> := PayloadTmpl}) ->
     ?LOG(info, "Initiating Action ~p.", [?FUNCTION_NAME]),
     PayloadTks = emqx_rule_utils:preproc_tmpl(PayloadTmpl),
     TopicTks = case ForwardTopic == <<"">> of
         true -> undefined;
         false -> emqx_rule_utils:preproc_tmpl(ForwardTopic)
     end,
-    fun(Msg, _Env = #{id := Id, clientid := From, flags := Flags,
-                      topic := Topic, timestamp := TimeStamp, qos := QoS}) ->
-            Topic1 = case TopicTks =:= undefined of
-                true -> Topic;
-                false -> emqx_rule_utils:proc_tmpl(TopicTks, Msg)
-            end,
-            BrokerMsg = #message{id = Id,
-                                 qos = QoS,
-                                 from = From,
-                                 flags = Flags,
-                                 topic = Topic1,
-                                 payload = format_data(PayloadTks, Msg),
-                                 timestamp = TimeStamp},
-            ecpool:with_client(PoolName, fun(BridgePid) ->
-                                             BridgePid ! {deliver, rule_engine, BrokerMsg}
-                                         end)
-    end.
+    Opts.
+
+on_action_data_to_mqtt_broker(Msg, _Env =
+                              #{id := Id, clientid := From, flags := Flags,
+                                topic := Topic, timestamp := TimeStamp, qos := QoS,
+                                ?BINDING_KEYS := #{
+                                    'ActId' := ActId,
+                                    'PoolName' := PoolName,
+                                    'TopicTks' := TopicTks,
+                                    'PayloadTks' := PayloadTks
+                                }}) ->
+    Topic1 = case TopicTks =:= undefined of
+        true -> Topic;
+        false -> emqx_rule_utils:proc_tmpl(TopicTks, Msg)
+    end,
+    BrokerMsg = #message{id = Id,
+                         qos = QoS,
+                         from = From,
+                         flags = Flags,
+                         topic = Topic1,
+                         payload = format_data(PayloadTks, Msg),
+                         timestamp = TimeStamp},
+    ecpool:with_client(PoolName,
+      fun(BridgePid) ->
+        BridgePid ! {deliver, rule_engine, BrokerMsg}
+      end),
+    emqx_rule_metrics:inc_actions_success(ActId).
 
 format_data([], Msg) ->
     emqx_json:encode(Msg);

+ 10 - 9
apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl

@@ -16,7 +16,7 @@
 
 %% @doc Bridge works in two layers (1) batching layer (2) transport layer
 %% The `bridge' batching layer collects local messages in batches and sends over
-%% to remote MQTT node/cluster via `connetion' transport layer.
+%% to remote MQTT node/cluster via `connection' transport layer.
 %% In case `REMOTE' is also an EMQX node, `connection' is recommended to be
 %% the `gen_rpc' based implementation `emqx_bridge_rpc'. Otherwise `connection'
 %% has to be `emqx_bridge_mqtt'.
@@ -98,6 +98,9 @@
         , ensure_subscription_absent/2
         ]).
 
+%% Internal
+-export([msg_marshaller/1]).
+
 -export_type([ config/0
              , batch/0
              , ack_ref/0
@@ -232,13 +235,10 @@ init(Config) ->
     State = init_opts(Config),
     Topics = [iolist_to_binary(T) || T <- Forwards],
     Subs = check_subscriptions(Subscriptions),
-    ConnectConfig = get_conn_cfg(Config),
-    ConnectFun = fun(SubsX) ->
-        emqx_bridge_connect:start(ConnectModule, ConnectConfig#{subscriptions => SubsX})
-    end,
+    ConnectCfg = get_conn_cfg(Config),
     self() ! idle,
     {ok, idle, State#{connect_module => ConnectModule,
-                      connect_fun => ConnectFun,
+                      connect_cfg => ConnectCfg,
                       forwards => Topics,
                       subscriptions => Subs,
                       replayq => Queue
@@ -276,7 +276,7 @@ open_replayq(Config) ->
         false -> #{dir => Dir, seg_bytes => SegBytes, max_total_size => MaxTotalSize}
     end,
     replayq:open(QueueConfig#{sizer => fun emqx_bridge_msg:estimate_size/1,
-                              marshaller => fun msg_marshaller/1}).
+                              marshaller => fun ?MODULE:msg_marshaller/1}).
 
 check_subscriptions(Subscriptions) ->
     lists:map(fun({Topic, QoS}) ->
@@ -433,10 +433,11 @@ is_topic_present(Topic, Topics) ->
 
 do_connect(#{forwards := Forwards,
              subscriptions := Subs,
-             connect_fun := ConnectFun,
+             connect_module := ConnectModule,
+             connect_cfg := ConnectCfg,
              name := Name} = State) ->
     ok = subscribe_local_topics(Forwards, Name),
-    case ConnectFun(Subs) of
+    case emqx_bridge_connect:start(ConnectModule, ConnectCfg#{subscriptions => Subs}) of
         {ok, Conn} ->
             ?LOG(info, "Bridge ~p is connecting......", [Name]),
             {ok, eval_bridge_handler(State#{connection => Conn}, connected)};

+ 6 - 0
apps/emqx_coap/src/emqx_coap_server.erl

@@ -22,6 +22,12 @@
         , stop/1
         ]).
 
+-export([ start_listener/1
+        , start_listener/3
+        , stop_listener/1
+        , stop_listener/2
+        ]).
+
 %%--------------------------------------------------------------------
 %% APIs
 %%--------------------------------------------------------------------

+ 8 - 1
apps/emqx_dashboard/src/emqx_dashboard.erl

@@ -25,6 +25,11 @@
         , stop_listeners/0
         ]).
 
+%% for minirest
+-export([ filter/1
+        , is_authorized/1
+        ]).
+
 -define(APP, ?MODULE).
 
 %%--------------------------------------------------------------------
@@ -81,7 +86,9 @@ listener_name(Proto) ->
 
 http_handlers() ->
     Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()),
-    [{"/api/v4/", minirest:handler(#{apps => Plugins, filter => fun filter/1}),[{authorization, fun is_authorized/1}]}].
+    [{"/api/v4/",
+      minirest:handler(#{apps => Plugins, filter => fun ?MODULE:filter/1}),
+      [{authorization, fun ?MODULE:is_authorized/1}]}].
 
 %%--------------------------------------------------------------------
 %% Basic Authorization

+ 4 - 0
apps/emqx_exhook/.gitignore

@@ -23,3 +23,7 @@ data/
 *.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

+ 20 - 56
apps/emqx_exhook/README.md

@@ -1,75 +1,39 @@
-# emqx_extension_hook
+# emqx_exhook
 
-The `emqx_extension_hook` extremly enhance the extensibility for EMQ X. It allow using an others programming language to mount the hooks intead of erlang.
+The `emqx_exhook` extremly enhance the extensibility for EMQ X. It allow using an others programming language to mount the hooks intead of erlang.
 
 ## Feature
 
-- [x] Support `python` and `java`.
-- [x] Support all hooks of emqx.
+- [x] Based on gRPC, it brings a very wide range of applicability
 - [x] Allows you to use the return value to extend emqx behavior.
 
-We temporarily no plans to support other languages. Plaease open a issue if you have to use other programming languages.
-
 ## Architecture
 
 ```
- EMQ X                                      Third-party Runtimes
-+========================+                 +====================+
-|    Extension           |                 |                    |
-|   +----------------+   |     Hooks       |  Python scripts /  |
-|   |    Drivers     | ------------------> |  Java Classes   /  |
-|   +----------------+   |     (pipe)      |  Others ...        |
-|                        |                 |                    |
-+========================+                 +====================+
+EMQ X                                      Third-party Runtime
++========================+                 +========+==========+
+|    ExHook              |                 |        |          |
+|   +----------------+   |      gRPC       | gRPC   |  User's  |
+|   |   gPRC Client  | ------------------> | Server |  Codes   |
+|   +----------------+   |    (HTTP/2)     |        |          |
+|                        |                 |        |          |
++========================+                 +========+==========+
 ```
 
-## Drivers
-
-### Python
-
-***Requirements:***
-
-- It requires the emqx hosted machine has Python3 Runtimes (not support python2)
-- The `python3` executable commands in your shell
-
-***Examples:***
-
-See `test/scripts/main.py`
-
-### Java
-
-***Requirements:***
-
-- It requires the emqx hosted machine has Java 8+ Runtimes
-- An executable commands in your shell, i,g: `java`
-
-***Examples:***
-
-See `test/scripts/Main.java`
-
-## Configurations
+## Usage
 
-| Name                | Data Type | Options                               | Default          | Description                      |
-| ------------------- | --------- | ------------------------------------- | ---------------- | -------------------------------- |
-| drivers             | Enum      | `python3`<br />`java`                 | `python3`        | Drivers type                     |
-| <type>.path         | String    | -                                     | `data/extension` | The codes/library search path    |
-| <type>.call_timeout | Duration  | -                                     | `5s`             | Function call timeout            |
-| <type>.pool_size    | Integer   | -                                     | `8`              | The pool size for the driver     |
-| <type>.init_module  | String    | -                                     | main             | The module name for initial call |
+### gRPC service
 
-## SDK
+See: `priv/protos/exhook.proto`
 
-See `sdk/README.md`
+### CLI
 
-## Known Issues or TODOs
+## Example
 
-**Configurable Log System**
+## Recommended gRPC Framework
 
-- use stderr to print logs to the emqx console instead of stdout. An alternative is to print the logs to a file.
-- The Java driver can not redirect the `stderr` stream to erlang vm on Windows platform.
+See: https://github.com/grpc-ecosystem/awesome-grpc
 
-## Reference
+## Thanks
 
-- [erlport](https://github.com/hdima/erlport)
-- [Eexternal Term Format](http://erlang.org/doc/apps/erts/erl_ext_dist.html)
-- [The Ports Tutorial of Erlang](http://erlang.org/doc/tutorial/c_port.html)
+- [grpcbox](https://github.com/tsloughter/grpcbox)

+ 65 - 204
apps/emqx_exhook/docs/design.md

@@ -2,254 +2,115 @@
 
 ## 动机
 
-增强系统的扩展性。包含的目的有
+在 EMQ X Broker v4.1-v4.2 中,我们发布了 2 个插件来扩展 emqx 的编程能力
 
-- 完全支持各种钩子,能够根据其返回值修改 EMQ X 或者 Client 的行为。
-  - 例如 `auth/acl`:可以查询数据库或者执行某种算法校验操作权限。然后返回 `false` 表示 `认证/ACL` 失败。
-  - 例如 `message.publish`:可以解析 `消息/主题` 并将其存储至数据库中。
+1. `emqx-extension-hook` 提供了使用 Java, Python 向 Broker 挂载钩子的功能
+2. `emqx-exproto` 提供了使用 Java,Python 编写用户自定义协议接入插件的功能
 
-- 支持多种语言的扩展;并包含该语言的示例程序。
-  - python
-  - webhook
-  - Java
-  - Lua
-  - c,go,.....
-- 热操作
-  - 允许在插件运行过程中,添加和移除 `Driver`。
+但在后续的支持中发现许多难以处理的问题:
 
-- 需要 CLI ,甚至 API 来管理 `Driver`
+1. 有大量的编程语言需要支持,需要编写和维护如 Go, JavaScript, Lua.. 等语言的驱动。
+2. `erlport` 使用的操作系统的管道进行通信,这让用户代码只能部署在和 emqx 同一个操作系统上。部署方式受到了极大的限制。
+3. 用户程序的启动参数直接打包到 Broker 中,导致用户开发无法实时的进行调试,单步跟踪等。
+4. `erlport` 会占用 `stdin` `stdout`。
 
-注:`message` 类钩子仅包括在企业版中。
+因此,我们计划重构这部分的实现,其中主要的内容是:
+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                                      Third-party Runtimes
-+========================+                 +====================+
-|    Extension           |                 |                    |
-|   +----------------+   |     Hooks       |  Python scripts /  |
-|   |    Drivers     | ------------------> |  Java Classes   /  |
-|   +----------------+   |     (pipe)      |  Others ...        |
-|                        |                 |                    |
-+========================+                 +====================+
+  EMQ X                                    
++========================+                 +========+==========+
+|    ExHook              |                 |        |          |
+|   +----------------+   |      gRPC       | gRPC   |  User's  |
+|   |   gRPC Client  | ------------------> | Server |  Codes   |
+|   +----------------+   |    (HTTP/2)     |        |          |
+|                        |                 |        |          |
++========================+                 +========+==========+
 ```
 
-### 配置文件示例
+`emqx-exhook` 通过 gRPC 的方式向用户部署的 gRPC 服务发送钩子的请求,并处理其返回的值。
 
-#### 驱动 配置
-
-```properties
-## Driver type
-##
-## Exmaples:
-##   - python3                   --- 仅配置 python3
-##   - python3, java, webhook    --- 配置多个 Driver
-exhook.dirvers = python3, java, webhook
-
-## --- 具体 driver 的配置详情
-
-## Python
-exhook.dirvers.python3.path = data/extension/python
-exhook.dirvers.python3.call_timeout = 5s
-exhook.dirvers.python3.pool_size = 8
-
-## java
-exhook.drivers.java.path = data/extension/java
-...
-```
 
-#### 钩子配置
+和 emqx 原生的钩子一致,emqx-exhook 也支持链式的方式计算和返回:
 
-钩子支持配置在配置文件中,例如:
+<img src="https://docs.emqx.net/broker/latest/cn/advanced/assets/chain_of_responsiblity.png" style="zoom:50%;" />
 
-```properties
-exhook.rule.python3.client.connected = {"module": "client", "callback": "on_client_connected"}
-exhook.rule.python3.message.publish  = {"module": "client", "callback": "on_client_connected", "topics": ["#", "t/#"]}
-```
+### gRPC 服务示例
 
-***已废弃!!(冗余)***
+用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中。例如,其支持的接口有:
 
+```protobuff
+syntax = "proto3";
 
-###  驱动抽象
+package emqx.exhook.v1;
 
-#### APIs
+service HookProvider {
 
-| 方法名                   | 说明     | 入参   | 返回   |
-| ------------------------ | -------- | ------ | ------ |
-| `init`                   | 初始化   | -      | 见下表 |
-| `deinit`                 | 销毁     | -      | -      |
-| `xxx `*(由init函数定义)* | 钩子回调 | 见下表 | 见下表 |
+  rpc OnProviderLoaded(ProviderLoadedRequest) returns (LoadedResponse) {};
 
+  rpc OnProviderUnloaded(ProviderUnloadedRequest) returns (EmptySuccess) {};
 
+  rpc OnClientConnect(ClientConnectRequest) returns (EmptySuccess) {};
 
-##### init 函数规格
+  rpc OnClientConnack(ClientConnackRequest) returns (EmptySuccess) {};
 
-```erlang
-%% init 函数
-%% HookSpec 			: 为用户在脚本中的 初始化函数指定的;他会与配置文件中的内容作为默认值,进行合并
-%%          			  该参数的目的,用于 EMQ X 判断需要执行哪些 Hook 和 如何执行 Hook
-%% State    			: 为用户自己管理的数据内容,EMQ X 不关心它,只来回透传
-init() -> {HookSpec, State}.
+  rpc OnClientConnected(ClientConnectedRequest) returns (EmptySuccess) {};
 
-%% 例如:
-{[{client_connect, callback_m(), callback_f(),#{}, {}}]}
+  rpc OnClientDisconnected(ClientDisconnectedRequest) returns (EmptySuccess) {};
 
-%%--------------------------------------------------------------
-%% Type Defines
+  rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {};
 
--tpye hook_spec() :: [{hookname(), callback_m(), callback_f(), hook_opts()}].
+  rpc OnClientCheckAcl(ClientCheckAclRequest) returns (ValuedResponse) {};
 
--tpye state :: any().
+  rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {};
 
--type hookname() :: client_connect
-                  | client_connack
-                  | client_connected
-                  | client_disconnected
-                  | client_authenticate
-                  | client_check_acl
-                  | client_subscribe
-                  | client_unsubscribe
-                  | session_created
-                  | session_subscribed
-                  | session_unsubscribed
-                  | session_resumed
-                  | session_discarded      %% TODO: Should squash to `terminated` ?
-                  | session_takeovered     %% TODO: Should squash to `terminated` ?
-                  | session_terminated
-                  | message_publish
-                  | message_delivered
-                  | message_acked
-                  | message_dropped.
+  rpc OnClientUnsubscribe(ClientUnsubscribeRequest) returns (EmptySuccess) {};
 
--type callback_m() :: atom().          -- 回调的模块名称;python 为脚本文件名称;java 为类名;webhook 为 URI 地址
+  rpc OnSessionCreated(SessionCreatedRequest) returns (EmptySuccess) {};
 
--type callback_f() :: atom().          -- 回调的方法名称;python,java 等为方法名;webhook 为资源地址
+  rpc OnSessionSubscribed(SessionSubscribedRequest) returns (EmptySuccess) {};
 
--tpye hook_opts() :: [{hook_key(), any()}].  -- 配置项;配置该项钩子的行为
+  rpc OnSessionUnsubscribed(SessionUnsubscribedRequest) returns (EmptySuccess) {};
 
--type hook_key() :: topics | ...
-```
+  rpc OnSessionResumed(SessionResumedRequest) returns (EmptySuccess) {};
 
+  rpc OnSessionDiscarded(SessionDiscardedRequest) returns (EmptySuccess) {};
 
+  rpc OnSessionTakeovered(SessionTakeoveredRequest) returns (EmptySuccess) {};
 
-##### deinit 函数规格
+  rpc OnSessionTerminated(SessionTerminatedRequest) returns (EmptySuccess) {};
 
-``` erlang
-%% deinit 函数;不关心返回的任何内容
-deinit() -> any().
-```
+  rpc OnMessagePublish(MessagePublishRequest) returns (ValuedResponse) {};
 
+  rpc OnMessageDelivered(MessageDeliveredRequest) returns (EmptySuccess) {};
 
+  rpc OnMessageDropped(MessageDroppedRequest) returns (EmptySuccess) {};
 
-##### 回调函数规格
-
-| 钩子                 | 入参                                                  | 返回      |
-| -------------------- | ----------------------------------------------------- | --------- |
-| client_connect       | `connifno`<br />`props`                               | -         |
-| client_connack       | `connifno`<br />`rc`<br />`props`                     | -         |
-| client_connected     | `clientinfo`<br />                                    | -         |
-| client_disconnected  | `clientinfo`<br />`reason`                            | -         |
-| client_authenticate  | `clientinfo`<br />`result`                            | `result`  |
-| client_check_acl     | `clientinfo`<br />`pubsub`<br />`topic`<br />`result` | `result`  |
-| client_subscribe     | `clientinfo`<br />`props`<br />`topicfilters`         | -         |
-| client_unsubscribe   | `clientinfo`<br />`props`<br />`topicfilters`         | -         |
-| session_created      | `clientinfo`                                          | -         |
-| session_subscribed   | `clientinfo`<br />`topic`<br />`subopts`              | -         |
-| session_unsubscribed | `clientinfo`<br />`topic`                             | -         |
-| session_resumed      | `clientinfo`                                          | -         |
-| session_discared     | `clientinfo`                                          | -         |
-| session_takeovered   | `clientinfo`                                          | -         |
-| session_terminated   | `clientinfo`<br />`reason`                            | -         |
-| message_publish      | `messsage`                                            | `message` |
-| message_delivered    | `clientinfo`<br />`message`                           | -         |
-| message_dropped      | `message`                                             | -         |
-| message_acked        | `clientinfo`<br />`message`                           | -         |
-
-
-
-上表中包含数据格式为:
-
-```erlang
--type conninfo :: [ {node, atom()}
-                  , {clientid, binary()}
-                  , {username, binary()}
-                  , {peerhost, binary()}
-							    , {sockport, integer()}
-                  , {proto_name, binary()}
-                  , {proto_ver, integer()}
-                  , {keepalive, integer()}
-								  ].
-
--type clientinfo :: [ {node, atom()}
-                    , {clientid, binary()}
-                    , {username, binary()}
-                    , {password, binary()}
-                    , {peerhost, binary()}
-							      , {sockport, integer()}
-                    , {protocol, binary()}
-                    , {mountpoint, binary()}
-                    , {is_superuser, boolean()}
-                    , {anonymous, boolean()}
-								    ].
-
--type message :: [ {node, atom()}
-                 , {id, binary()}
-                 , {qos, integer()}
-                 , {from, binary()}
-                 , {topic, binary()}
-                 , {payload, binary()}
-                 , {timestamp, integer()}
-                 ].
-
--type rc :: binary().
--type props :: [{key(), value()}]
-
--type topics :: [topic()].
--type topic :: binary().
--type pubsub :: publish | subscribe.
--type result :: true | false.
+  rpc OnMessageAcked(MessageAckedRequest) returns (EmptySuccess) {};
+}
 ```
 
-
-
-### 统计
-
-在驱动运行过程中,应有对每种钩子调用计数,例如:
-
-```
-exhook.python3.check_acl               10
-```
-
-
-
-### 管理
-
-**CLI 示例:**
-
-
-
-**列出所有的驱动**
-
-```
-./bin/emqx_ctl exhook dirvers list
-Drivers(xxx=yyy)
-Drivers(aaa=bbb)
-```
-
-
-
-**开关驱动**
+### 配置文件示例
 
 ```
-./bin/emqx_ctl exhook drivers enable python3
-ok
-
-./bin/emqx_ctl exhook drivers disable python3
-ok
+## 配置 gRPC 服务地址 (HTTP)
+##
+## s1 为服务器的名称
+exhook.server.s1.url = http://127.0.0.1:9001
 
-./bin/emqx_ctl exhook drivers stats
-python3.client_connect     123
-webhook.check_acl          20
+## 配置 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 - 84
apps/emqx_exhook/docs/introduction.md

@@ -1,84 +0,0 @@
-## 简介
-
-`emqx-extension-hook` 插件用于提供钩子(Hook)的多语言支持。它能够允许其他的语言,例如:Python,Java 等,能够直接表达如何挂载钩子,和处理相应的钩子事件。
-
-该插件给 EMQ X 带来的扩展性十分的强大,甚至于所有基于钩子的插件都可以通过其他编程语言实现。唯一不同的是在性能上肯定有一定的降低。
-
-目前,一些常见的场景有:
-
-- 通过 `client.authenticate` 钩子,使用其他编程语言查询数据库,判断该客户端是否具有接入的权限。
-- 通过 `client.check_acl` 钩子,使用其他编程语言查询数据库,实现发布/订阅的权限控制逻辑。
-- 通过 `message` 类的钩子,实现消息收发的控制和数据格式转换。
-- 获取客户端所有的事件,将其存储进三方的日志、或数据平台中。
-
-**声明:当前仅实现了 Python、Java 的支持** 
-
-**声明:message 类钩子功能仅包含在企业版当中**
-
-### 要求
-
-EMQ X 发行包中不包含其他语言的运行环境。它要求:
-
-- 宿主机需包含其他编程语言对应的执行环境。
-- 必须将源码(或编译后的代码)、资源文件等,放到 `emqx-extension-hook` 指示的路径。
-- 代码的实现,若包含三方依赖、库等,它应该包含在 `emqx-extension-hook` 对其的搜索路径中。
-
-
-### 架构
-
-`emqx-extension-hook` 是 EMQ X 的一个插件,它主要包括:
-
-1. 驱动的管理。例如:如何启动/停止某个驱动。
-2. 事件的分发。例如:根据各个驱动所注册的钩子列表的不同,向各个驱动分发事件,传递返回值等。
-3. 预置了驱动的实现。包括 Python 和 Java 驱动的实现,和方便用户集成开发的 SDK 代码包。
-
-其架构图如下:
-
-```
- EMQ X                                      Third-party Runtimes
-+========================+                 +====================+
-|    Extension           |                 |                    |
-|   +----------------+   |     Hooks       |  Python scripts /  |
-|   |    Drivers     | ------------------> |  Java Classes   /  |
-|   +----------------+   |     (pipe)      |  Others ...        |
-|                        |                 |                    |
-+========================+                 +====================+
-```
-
-图中表明,由 Client 产生的所有的事件,例如:连接、发布、订阅等,都会由 `emqx-extension-hook`插件分发给下面的各个 `驱动(Driver)`;而,驱动则负责如何与三方运行时的进行通信。
-
-广义上的驱动(Driver)可以分为两类:
-
-1. 编程语言类。
-2. 服务类。例如:HTTP 就属于此类。
-
-`emqx-extension-hook` 并不关心驱动实际的类型和实现,只要其实现了对应的接口即可。
-
-
-#### 驱动
-
-本文中,只有未经限定说明的驱动,都是指编程语言类的驱动。
-
-编程语言类驱动是基于 [Erlang - Port](http://erlang.org/doc/tutorial/c_port.html) 进行实现。它本质上是由 `emqx-extension-hook` 是启动一个其他语言的程序,并使用管道(Pipe)实现两个进程间的通信。
-
-
-此类驱动的实现包括两部分的内容:
-
-1. Erlang 侧的实现,它包含如何启动其他语言的运行时系统、和分发请求、处理结果等。
-2. 其他语言侧的实现。它包含如何和 Erlang 虚拟机通信,如何执行函数调用等。
-
-如:
-
-```
-    Erlang VM                       Third Runtimes (e.g: Java VM)
-   +===========+=========+         +=========+================+
-   | Extension | Driver  | <=====> |  Driver | User's Codes   |
-   +===========+=========+         +=========+================+
-```
-
-而,对于基于服务的驱动,原理就很简单了。以 HTTP 为例,它的实现仅需要一个 HTTP 客户端、和指定服务端返回的数据格式即可。
-
-### 集成与调试
-
-参见 SDK 规范、和对应语言的开发手册
-

+ 0 - 79
apps/emqx_exhook/docs/sdk-specification.md

@@ -1,79 +0,0 @@
-## SDK 规范
-
-### 动机
-
-SDK 的目的在于方便用户使用 IDE 集成开发、和模拟调试。
-
-### 位置
-
-```
-    +------------------+
-    |   User's Codes   |
-    +------------------+
-    |       SDK        |    <====   The SDK Located
-    +------------------+
-    |     Raw APIs     |
-    +------------------+
-    |      Driver      |
-    +==================+
-             ||
-    +==================+
-    |   EMQ X Plugin   |
-    +------------------+
-```
-
-因此,SDK 的作用在于封装底层的比较晦涩的数据格式和方法,屏蔽底层细节。直接提供优化 API 供用户使用。
-
-
-### 实现要求
-
-**声明:** stdin, stdout 已用于和 EMQ X 通信,请不要使用。stderr 用于日志输出。
-
-#### 基础项
-
-1. 必须将原始的 `init` `deinit`函数进行封装,方便用户:
-   - 配置需要挂载的钩子列表
-   - 定义用户自己的初始化和销毁的内容
-2. 必须将回调函数的各个松散的数据类型,封装成类或某种结构化类型。
-3. 必须要有对应的开发、部署文档说明
-
-#### 高级项
-
-1. 应能方便用户能在 IDE 中进行,集成和开发
-2. 应提供集成测试用的模拟代码。
-   - 例如,生成模拟的数据,发送至用户的程序,方便直接断点调试
-
-
-### 部署结构
-
-#### 代码依赖结构
-
-从部署的角度看,代码的依赖关系为:
-
-1. 用户代码:
-    * 一定会依赖 SDK
-    * 可能会依赖 某个位置的三方/系统库
-2. SDK 代码:
-    * 只能依赖 erlport
-3. 基础通信库
-    * 无依赖
-
-#### 部署
-
-从文件存放的位置来看,一个标准的部署结构为:
-
-```
-emqx
-|
-|--- data
-|------- extension
-|---------- <some-sdk-package-name>
-|--------------- <some-classes/scripts-in-sdk>
-|---------- <user's classes/scripts>
-|
-|---------- <another-sdk-package-name>
-|--------------- <some-classes/scripts-in-sdk>
-|---------- <user's classes/scripts>
-```
-
-它表达了:在 `data/extension` 目录下安装了两个 SDK,并且用户都基于 SDK 编写了其回调的代码模块。

+ 15 - 0
apps/emqx_exhook/etc/emqx_exhook.conf

@@ -0,0 +1,15 @@
+##====================================================================
+## EMQ X Hooks
+##====================================================================
+
+##--------------------------------------------------------------------
+## Server Address
+
+## The gRPC server url
+##
+## exhook.server.$name.url = url()
+exhook.server.default.url = http://127.0.0.1:9000
+
+#exhook.server.default.ssl.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem
+#exhook.server.default.ssl.certfile = {{ platform_etc_dir }}/certs/cert.pem
+#exhook.server.default.ssl.keyfile = {{ platform_etc_dir }}/certs/key.pem

+ 0 - 24
apps/emqx_exhook/etc/emqx_extension_hook.conf

@@ -1,24 +0,0 @@
-##====================================================================
-## EMQ X Hooks
-##====================================================================
-
-##--------------------------------------------------------------------
-## Driver confs
-
-## Setup the supported drivers
-##
-## Value: python3 | java
-exhook.drivers = python3
-
-## Search path for scripts/library
-##
-exhook.drivers.python3.path = {{ platform_data_dir }}/extension/
-
-## Call timeout
-##
-## Value: Duration
-##exhook.drivers.python3.call_timeout = 5s
-
-## Initial module name
-##
-##exhook.drivers.python3.init_module = main

+ 0 - 43
apps/emqx_exhook/priv/emqx_extension_hook.schema

@@ -1,43 +0,0 @@
-%%-*- mode: erlang -*-
-
-{mapping, "exhook.drivers", "emqx_extension_hook.drivers", [
-  {datatype, string}
-]}.
-
-{mapping, "exhook.drivers.$name.$key", "emqx_extension_hook.drivers", [
-  {datatype, string}
-]}.
-
-{translation, "emqx_extension_hook.drivers", fun(Conf) ->
-
-    Filter  = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
-
-    Duration = fun(S) ->
-                   case cuttlefish_duration:parse(S, ms) of
-                       Ms when is_integer(Ms) -> Ms;
-                       {error, R} -> error(R)
-                   end
-               end,
-    Integer = fun(S) -> list_to_integer(S) end,
-
-    Atom = fun(S) -> list_to_atom(S) end,
-
-    Python = fun(Prefix) ->
-               [{init_module, Atom(cuttlefish:conf_get(Prefix ++ ".init_module", Conf, "main"))}, %% dirver
-                {python_path, cuttlefish:conf_get(Prefix ++ ".path", Conf, undefined)},
-                {call_timeout, Duration(cuttlefish:conf_get(Prefix ++ ".call_timeout", Conf, "5s"))}]
-            end,
-
-    Java = fun(Prefix) ->
-             [{init_module, Atom(cuttlefish:conf_get(Prefix ++ ".init_module", Conf, "Main"))}, %% dirver
-              {java_path, cuttlefish:conf_get(Prefix ++ ".path", Conf, undefined)},
-              {call_timeout, Duration(cuttlefish:conf_get(Prefix ++ ".call_timeout", Conf, "5s"))}]
-           end,
-
-    Options = fun(python) -> Filter(Python("exhook.drivers.python"));
-                 (python3) -> Filter(Python("exhook.drivers.python3"));
-                 (java) -> Filter(Java("exhook.drivers.java"));
-                 (_) -> error(not_supported_drivers_type)
-              end,
-    [{Atom(Name), Options(Atom(Name))} || Name <- string:tokens(cuttlefish:conf_get("exhook.drivers", Conf), ",")]
-end}.

+ 30 - 1
apps/emqx_exhook/rebar.config

@@ -1,5 +1,21 @@
 %%-*- mode: erlang -*-
-{deps, []}.
+{plugins,
+ [rebar3_proper,
+  {grpc_plugin, {git, "https://github.com/HJianBo/grpcbox_plugin", {tag, "v0.9.1"}}}
+]}.
+
+{deps,
+ [{grpc, {git, "https://github.com/emqx/grpc", {tag, "0.5.0"}}}
+]}.
+
+{grpc,
+ [{protos, ["priv/protos"]},
+  {gpb_opts, [{module_name_prefix, "emqx_"},
+              {module_name_suffix, "_pb"}]}
+]}.
+
+{provider_hooks,
+ [{pre, [{compile, {grpc, gen}}]}]}.
 
 {edoc_opts, [{preprocess, true}]}.
 
@@ -13,6 +29,19 @@
 {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,
+      [{emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.3.1"}}}
+      ]}
+    ]}
+]}.

+ 0 - 12
apps/emqx_exhook/sdk/README.md

@@ -1,12 +0,0 @@
-# SDKs
-
-A specific language SDK is a suite of codes for user-oriented friendly. 
-
-Even it does not need it for you to develop the Multiple language support plugins, but it provides more friendly APIs and Abstract for you
-
-
-Now, we provide the following SDKs:
-
-- Java: https://github.com/emqx/emqx-extension-java-sdk
-- Python: https://github.com/emqx/emqx-extension-python-sdk
-

+ 2 - 2
apps/emqx_exhook/src/emqx_exhook.app.src

@@ -1,10 +1,10 @@
 {application, emqx_exhook,
  [{description, "EMQ X Extension for Hook"},
-  {vsn, "4.3.0"}, % strict semver, bump manually!
+  {vsn, "git"},
   {modules, []},
   {registered, []},
   {mod, {emqx_exhook_app, []}},
-  {applications, [kernel,stdlib,emqx]},
+  {applications, [kernel,stdlib,grpc]},
   {env,[]},
   {licenses, ["Apache-2.0"]},
   {maintainers, ["EMQ X Team <contact@emqx.io>"]},

+ 24 - 0
apps/emqx_exhook/src/emqx_exhook.app.src.script

@@ -0,0 +1,24 @@
+%%-*- mode: erlang -*-
+%% .app.src.script
+
+RemoveLeadingV =
+    fun(Tag) ->
+        case re:run(Tag, "^[v]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
+            nomatch ->
+                re:replace(Tag, "/", "-", [{return ,list}]);
+            _ ->
+                %% if it is a version number prefixed by 'v' or 'e', then remove it
+                re:replace(Tag, "[v]", "", [{return ,list}])
+        end
+    end,
+
+case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
+    false -> CONFIG; % env var not defined
+    []    -> CONFIG; % env var set to empty string
+    Tag ->
+       [begin
+           AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
+           {application, App, AppConf0}
+        end || Conf = {application, App, AppConf} <- CONFIG]
+end.
+

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

@@ -0,0 +1,9 @@
+%% -*-: erlang -*-
+{VSN,
+  [
+    {<<".*">>, []}
+  ],
+  [
+    {<<".*">>, []}
+  ]
+}.

+ 51 - 39
apps/emqx_exhook/src/emqx_exhook_server.erl

@@ -36,7 +36,7 @@
         ]).
 
 -record(server, {
-          %% Server name (equal to grpcbox client channel name)
+          %% Server name (equal to grpc client channel name)
           name :: server_name(),
           %% The server started options
           options :: list(),
@@ -44,11 +44,11 @@
           channel :: pid(),
           %% Registered hook names and options
           hookspec :: #{hookpoint() => map()},
-          %% Metric fun
-          incfun :: function()
+          %% Metrcis name prefix
+          prefix :: list()
        }).
 
--type server_name() :: atom().
+-type server_name() :: string().
 -type server() :: #server{}.
 
 -type hookpoint() :: 'client.connect'
@@ -73,54 +73,63 @@
 
 -export_type([server/0]).
 
+-dialyzer({nowarn_function, [inc_metrics/2]}).
+
 %%--------------------------------------------------------------------
 %% Load/Unload APIs
 %%--------------------------------------------------------------------
 
 -spec load(atom(), list()) -> {ok, server()} | {error, term()} .
-load(Name, Opts0) ->
-    {Endpoints, Options} = channel_opts(Opts0),
-    StartFun = case proplists:get_bool(inplace, Opts0) of
-                   true -> start_grpc_client_channel_inplace;
-                   _ -> start_grpc_client_channel
-               end,
-    case emqx_exhook_sup:StartFun(Name, Endpoints, Options) of
-        {ok, ChannPid} ->
+load(Name0, Opts0) ->
+    Name = prefix(Name0),
+    {SvrAddr, ClientOpts} = channel_opts(Opts0),
+    case emqx_exhook_sup:start_grpc_client_channel(Name, SvrAddr, ClientOpts) of
+        {ok, _ChannPoolPid} ->
             case do_init(Name) of
                 {ok, HookSpecs} ->
                     %% Reigster metrics
                     Prefix = lists:flatten(io_lib:format("exhook.~s.", [Name])),
                     ensure_metrics(Prefix, HookSpecs),
                     {ok, #server{name = Name,
-                                  options = Opts0,
-                                  channel = ChannPid,
-                                  hookspec = HookSpecs,
-                                  incfun = incfun(Prefix) }};
-                {error, _} = E -> E
+                                 options = Opts0,
+                                 channel = _ChannPoolPid,
+                                 hookspec = HookSpecs,
+                                 prefix = Prefix }};
+                {error, _} = E ->
+                    emqx_exhook_sup:stop_grpc_client_channel(Name), E
             end;
         {error, _} = E -> E
     end.
 
+%% @private
+prefix(Name) when is_atom(Name) ->
+    "exhook:" ++ atom_to_list(Name);
+prefix(Name) when is_binary(Name) ->
+    "exhook:" ++ binary_to_list(Name);
+prefix(Name) when is_list(Name) ->
+    "exhook:" ++ Name.
+
 %% @private
 channel_opts(Opts) ->
     Scheme = proplists:get_value(scheme, Opts),
     Host = proplists:get_value(host, Opts),
     Port = proplists:get_value(port, Opts),
-    Options = proplists:get_value(options, Opts, []),
-    SslOpts = case Scheme of
-                  https -> proplists:get_value(ssl_options, Opts, []);
-                  _ -> []
-              end,
-    {[{Scheme, Host, Port, SslOpts}], maps:from_list(Options)}.
+    SvrAddr = lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])),
+    ClientOpts = case Scheme of
+                     https ->
+                         SslOpts = lists:keydelete(ssl, 1, proplists:get_value(ssl_options, Opts, [])),
+                         #{gun_opts =>
+                           #{transport => ssl,
+                             transport_opts => SslOpts}};
+                     _ -> #{}
+                 end,
+    {SvrAddr, ClientOpts}.
 
 -spec unload(server()) -> ok.
-unload(#server{name = Name, channel = ChannPid, options = Options}) ->
+unload(#server{name = Name}) ->
     _ = do_deinit(Name),
-    {StopFun, Args} = case proplists:get_bool(inplace, Options) of
-                          true -> {stop_grpc_client_channel_inplace, [ChannPid]};
-                          _ -> {stop_grpc_client_channel, [Name]}
-                      end,
-    apply(emqx_exhook_sup, StopFun, Args).
+    _ = emqx_exhook_sup:stop_grpc_client_channel(Name),
+    ok.
 
 do_deinit(Name) ->
     _ = do_call(Name, 'on_provider_unloaded', #{}),
@@ -168,11 +177,6 @@ ensure_metrics(Prefix, HookSpecs) ->
             || Hookpoint <- maps:keys(HookSpecs)],
     lists:foreach(fun emqx_metrics:ensure/1, Keys).
 
-incfun(Prefix) ->
-    fun(Name) ->
-        emqx_metrics:inc(list_to_atom(Prefix ++ atom_to_list(Name)))
-    end.
-
 format(#server{name = Name, hookspec = Hooks}) ->
     io_lib:format("name=~p, hooks=~0p", [Name, Hooks]).
 
@@ -187,7 +191,7 @@ name(#server{name = Name}) ->
   -> ignore
    | {ok, Resp :: term()}
    | {error, term()}.
-call(Hookpoint, Req, #server{name = ChannName, hookspec = Hooks, incfun = IncFun}) ->
+call(Hookpoint, Req, #server{name = ChannName, hookspec = Hooks, prefix = Prefix}) ->
     GrpcFunc = hk2func(Hookpoint),
     case maps:get(Hookpoint, Hooks, undefined) of
         undefined -> ignore;
@@ -201,18 +205,26 @@ call(Hookpoint, Req, #server{name = ChannName, hookspec = Hooks, incfun = IncFun
             case NeedCall of
                 false -> ignore;
                 _ ->
-                    IncFun(Hookpoint),
+                    inc_metrics(Prefix, Hookpoint),
                     do_call(ChannName, GrpcFunc, Req)
             end
     end.
 
+%% @private
+inc_metrics(IncFun, Name) when is_function(IncFun) ->
+    %% BACKW: e4.2.0-e4.2.2
+    {env, [Prefix|_]} = erlang:fun_info(IncFun, env),
+    inc_metrics(Prefix, Name);
+inc_metrics(Prefix, Name) when is_list(Prefix) ->
+    emqx_metrics:inc(list_to_atom(Prefix ++ atom_to_list(Name))).
+
 -compile({inline, [match_topic_filter/2]}).
 match_topic_filter(_, []) ->
     true;
 match_topic_filter(TopicName, TopicFilter) ->
     lists:any(fun(F) -> emqx_topic:match(TopicName, F) end, TopicFilter).
 
--spec do_call(atom(), atom(), map()) -> {ok, map()} | {error, term()}.
+-spec do_call(string(), atom(), map()) -> {ok, map()} | {error, term()}.
 do_call(ChannName, Fun, Req) ->
     Options = #{channel => ChannName},
     ?LOG(debug, "Call ~0p:~0p(~0p, ~0p)", [?PB_CLIENT_MOD, Fun, Req, Options]),
@@ -228,8 +240,8 @@ do_call(ChannName, Fun, Req) ->
             ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) error: ~0p",
                         [?PB_CLIENT_MOD, Fun, Req, Options, Reason]),
             {error, Reason};
-        {'EXIT', Reason, Stk} ->
-            ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) throw an exception: ~0p, stacktrace: ~p",
+        {'EXIT', {Reason, Stk}} ->
+            ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) throw an exception: ~0p, stacktrace: ~0p",
                         [?PB_CLIENT_MOD, Fun, Req, Options, Reason, Stk]),
             {error, Reason}
     end.

+ 14 - 29
apps/emqx_exhook/src/emqx_exhook_sup.erl

@@ -26,10 +26,6 @@
         , stop_grpc_client_channel/1
         ]).
 
--export([ start_grpc_client_channel_inplace/3
-        , stop_grpc_client_channel_inplace/1
-        ]).
-
 %%--------------------------------------------------------------------
 %%  Supervisor APIs & Callbacks
 %%--------------------------------------------------------------------
@@ -45,30 +41,19 @@ init([]) ->
 %%--------------------------------------------------------------------
 
 -spec start_grpc_client_channel(
-        atom() | string(),
-        [grpcbox_channel:endpoint()],
-        grpcbox_channel:options()) -> {ok, pid()} | {error, term()}.
-start_grpc_client_channel(Name, Endpoints, Options0) ->
-    Options = Options0#{sync_start => true},
-    Spec = #{id => Name,
-             start => {grpcbox_channel, start_link, [Name, Endpoints, Options]},
-             type => worker},
-
-    supervisor:start_child(?MODULE, Spec).
+        string(),
+        uri_string:uri_string(),
+        grpc_client:options()) -> {ok, pid()} | {error, term()}.
+start_grpc_client_channel(Name, SvrAddr, Options) ->
+    grpc_client_sup:create_channel_pool(Name, SvrAddr, Options).
 
--spec stop_grpc_client_channel(atom()) -> ok.
+-spec stop_grpc_client_channel(string()) -> ok.
 stop_grpc_client_channel(Name) ->
-    ok = supervisor:terminate_child(?MODULE, Name),
-    ok = supervisor:delete_child(?MODULE, Name).
-
--spec start_grpc_client_channel_inplace(
-        atom() | string(),
-        [grpcbox_channel:endpoint()],
-        grpcbox_channel:options()) -> {ok, pid()} | {error, term()}.
-start_grpc_client_channel_inplace(Name, Endpoints, Options0) ->
-    Options = Options0#{sync_start => true},
-    grpcbox_channel_sup:start_child(Name, Endpoints, Options).
-
--spec stop_grpc_client_channel_inplace(pid()) -> ok.
-stop_grpc_client_channel_inplace(Pid) ->
-    ok = supervisor:terminate_child(grpcbox_channel_sup, Pid).
+    %% Avoid crash due to hot-upgrade had unloaded
+    %% grpc application
+    try
+        grpc_client_sup:stop_channel_pool(Name)
+    catch
+        _:_:_ ->
+            ok
+    end.

+ 0 - 136
apps/emqx_exhook/src/emqx_extension_hook.erl

@@ -1,136 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook).
-
--include("emqx_extension_hook.hrl").
--include_lib("emqx/include/logger.hrl").
-
--logger_header("[ExHook]").
-
-%% Mgmt APIs
--export([ enable/2
-        , disable/1
-        , disable_all/0
-        , list/0
-        ]).
-
--export([ cast/2
-        , call_fold/4
-        ]).
-
-%%--------------------------------------------------------------------
-%% Mgmt APIs
-%%--------------------------------------------------------------------
-
--spec list() -> [emqx_extension_hook_driver:driver()].
-list() ->
-    [state(Name) || Name <- running()].
-
--spec enable(atom(), list()) -> ok | {error, term()}.
-enable(Name, Opts) ->
-    case lists:member(Name, running()) of
-        true ->
-            {error, already_started};
-        _ ->
-            case emqx_extension_hook_driver:load(Name, Opts) of
-                {ok, DriverState} ->
-                    save(Name, DriverState);
-                {error, Reason} ->
-                    ?LOG(error, "Load driver ~p failed: ~p", [Name, Reason]),
-                    {error, Reason}
-            end
-    end.
-
--spec disable(atom()) -> ok | {error, term()}.
-disable(Name) ->
-    case state(Name) of
-        undefined -> {error, not_running};
-        Driver ->
-            ok = emqx_extension_hook_driver:unload(Driver),
-            unsave(Name)
-    end.
-
--spec disable_all() -> [atom()].
-disable_all() ->
-    [begin disable(Name), Name end || Name <- running()].
-
-%%----------------------------------------------------------
-%% Dispatch APIs
-%%----------------------------------------------------------
-
--spec cast(atom(), list()) -> ok.
-cast(Name, Args) ->
-    cast(Name, Args, running()).
-
-cast(_, _, []) ->
-    ok;
-cast(Name, Args, [DriverName|More]) ->
-    emqx_extension_hook_driver:run_hook(Name, Args, state(DriverName)),
-    cast(Name, Args, More).
-
--spec call_fold(atom(), list(), term(), function()) -> ok | {stop, term()}.
-call_fold(Name, InfoArgs, AccArg, Validator) ->
-    call_fold(Name, InfoArgs, AccArg, Validator, running()).
-
-call_fold(_, _, _, _, []) ->
-    ok;
-call_fold(Name, InfoArgs, AccArg, Validator, [NameDriver|More]) ->
-    Driver = state(NameDriver),
-    case emqx_extension_hook_driver:run_hook_fold(Name, InfoArgs, AccArg, Driver) of
-        ok         -> call_fold(Name, InfoArgs, AccArg, Validator, More);
-        {error, _} -> call_fold(Name, InfoArgs, AccArg, Validator, More);
-        {ok, NAcc} ->
-            case Validator(NAcc) of
-                true ->
-                    {stop, NAcc};
-                _ ->
-                    ?LOG(error, "Got invalid return type for calling ~p on ~p",
-                         [Name, emqx_extension_hook_driver:name(Driver)]),
-                    call_fold(Name, InfoArgs, AccArg, Validator, More)
-            end
-    end.
-
-%%----------------------------------------------------------
-%% Storage
-
--compile({inline, [save/2]}).
-save(Name, DriverState) ->
-    Saved = persistent_term:get(?APP, []),
-    persistent_term:put(?APP, lists:reverse([Name | Saved])),
-    persistent_term:put({?APP, Name}, DriverState).
-
--compile({inline, [unsave/1]}).
-unsave(Name) ->
-    case persistent_term:get(?APP, []) of
-        [] ->
-            persistent_term:erase(?APP);
-        Saved ->
-            persistent_term:put(?APP, lists:delete(Name, Saved))
-    end,
-    persistent_term:erase({?APP, Name}),
-    ok.
-
--compile({inline, [running/0]}).
-running() ->
-    persistent_term:get(?APP, []).
-
--compile({inline, [state/1]}).
-state(Name) ->
-    case catch persistent_term:get({?APP, Name}) of
-        {'EXIT', {badarg,_}} -> undefined;
-        State -> State
-    end.

+ 0 - 108
apps/emqx_exhook/src/emqx_extension_hook_app.erl

@@ -1,108 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook_app).
-
--behaviour(application).
-
--include("emqx_extension_hook.hrl").
-
--emqx_plugin(?MODULE).
-
--export([ start/2
-        , stop/1
-        , prep_stop/1
-        ]).
-
-%%--------------------------------------------------------------------
-%% Application callbacks
-%%--------------------------------------------------------------------
-
-start(_StartType, _StartArgs) ->
-    {ok, Sup} = emqx_extension_hook_sup:start_link(),
-
-    %% Load all dirvers
-    load_all_drivers(),
-
-    %% Register all hooks
-    load_exhooks(),
-
-    %% Register CLI
-    emqx_ctl:register_command(exhook, {emqx_extension_hook_cli, cli}, []),
-    {ok, Sup}.
-
-prep_stop(State) ->
-    emqx_ctl:unregister_command(exhook),
-    unload_exhooks(),
-    unload_all_drivers(),
-    State.
-
-stop(_State) ->
-    ok.
-
-%%--------------------------------------------------------------------
-%% Internal funcs
-%%--------------------------------------------------------------------
-
-load_all_drivers() ->
-    load_all_drivers(application:get_env(?APP, drivers, [])).
-
-load_all_drivers([]) ->
-    ok;
-load_all_drivers([{Name, Opts}|Drivers]) ->
-    ok = emqx_extension_hook:enable(Name, Opts),
-    load_all_drivers(Drivers).
-
-unload_all_drivers() ->
-    emqx_extension_hook:disable_all().
-
-%%--------------------------------------------------------------------
-%% Exhooks
-
-load_exhooks() ->
-    [emqx:hook(Name, {M, F, A}) || {Name, {M, F, A}} <- search_exhooks()].
-
-unload_exhooks() ->
-    [emqx:unhook(Name, {M, F}) || {Name, {M, F, _A}} <- search_exhooks()].
-
-search_exhooks() ->
-    search_exhooks(ignore_lib_apps(application:loaded_applications())).
-search_exhooks(Apps) ->
-    lists:flatten([ExHooks || App <- Apps, {_App, _Mod, ExHooks} <- find_attrs(App, exhooks)]).
-
-ignore_lib_apps(Apps) ->
-    LibApps = [kernel, stdlib, sasl, appmon, eldap, erts,
-               syntax_tools, ssl, crypto, mnesia, os_mon,
-               inets, goldrush, gproc, runtime_tools,
-               snmp, otp_mibs, public_key, asn1, ssh, hipe,
-               common_test, observer, webtool, xmerl, tools,
-               test_server, compiler, debugger, eunit, et,
-               wx],
-    [AppName || {AppName, _, _} <- Apps, not lists:member(AppName, LibApps)].
-
-find_attrs(App, Def) ->
-    [{App, Mod, Attr} || {ok, Modules} <- [application:get_key(App, modules)],
-                         Mod <- Modules,
-                         {Name, Attrs} <- module_attributes(Mod), Name =:= Def,
-                         Attr <- Attrs].
-
-module_attributes(Module) ->
-    try Module:module_info(attributes)
-    catch
-        error:undef -> [];
-        error:Reason -> error(Reason)
-    end.
-

+ 0 - 80
apps/emqx_exhook/src/emqx_extension_hook_cli.erl

@@ -1,80 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook_cli).
-
--include("emqx_extension_hook.hrl").
-
--export([cli/1]).
-
-cli(["drivers", "list"]) ->
-    if_enabled(fun() ->
-        Drivers = emqx_extension_hook:list(),
-        [emqx_ctl:print("Driver(~s)~n", [emqx_extension_hook_driver:format(Driver)]) || Driver <- Drivers]
-    end);
-
-cli(["drivers", "enable", Name0]) ->
-    if_enabled(fun() ->
-        Name = list_to_atom(Name0),
-        case proplists:get_value(Name, application:get_env(?APP, drivers, [])) of
-            undefined ->
-                emqx_ctl:print("not_found~n");
-            Opts ->
-                print(emqx_extension_hook:enable(Name, Opts))
-        end
-    end);
-
-cli(["drivers", "disable", Name]) ->
-    if_enabled(fun() ->
-        print(emqx_extension_hook:disable(list_to_atom(Name)))
-    end);
-
-cli(["drivers", "stats"]) ->
-    if_enabled(fun() ->
-        [emqx_ctl:print("~-35s:~w~n", [Name, N]) || {Name, N} <- stats()]
-    end);
-
-cli(_) ->
-    emqx_ctl:usage([{"exhook drivers list", "List all running drivers"},
-                    {"exhook drivers enable <Name>", "Enable a driver with configurations"},
-                    {"exhook drivers disable <Name>", "Disable a driver"},
-                    {"exhook drivers stats", "Print drivers statistic"}]).
-
-print(ok) ->
-    emqx_ctl:print("ok~n");
-print({error, Reason}) ->
-    emqx_ctl:print("~p~n", [Reason]).
-
-%%--------------------------------------------------------------------
-%% Internal funcs
-%%--------------------------------------------------------------------
-
-if_enabled(Fun) ->
-    case lists:keymember(?APP, 1, application:which_applications()) of
-        true -> Fun();
-        _ -> hint()
-    end.
-
-hint() ->
-    emqx_ctl:print("Please './bin/emqx_ctl plugins load emqx_extension_hook' first.~n").
-
-stats() ->
-    lists:foldr(fun({K, N}, Acc) ->
-        case atom_to_list(K) of
-            "exhook." ++ Key -> [{Key, N}|Acc];
-            _ -> Acc
-        end
-    end, [], emqx_metrics:all()).

+ 0 - 305
apps/emqx_exhook/src/emqx_extension_hook_driver.erl

@@ -1,305 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook_driver).
-
--include_lib("emqx/include/logger.hrl").
-
--logger_header("[ExHook Driver]").
-
-%% Load/Unload
--export([ load/2
-        , unload/1
-        , connect/1
-        ]).
-
-%% APIs
--export([ run_hook/3
-        , run_hook_fold/4]).
-
-%% Infos
--export([ name/1
-        , format/1
-        ]).
-
--record(driver, {
-          %% Driver name (equal to ecpool name)
-          name :: driver_name(),
-          %% Driver type
-          type :: driver_type(),
-          %% Initial Module name
-          init :: atom(),
-          %% Hook Spec
-          hookspec :: hook_spec(),
-          %% Metric fun
-          incfun :: function(),
-          %% low layer state
-          state
-       }).
-
--type driver_name() :: python | python3 | java | webhook | lua | atom().
--type driver_type() :: python | webhok | java | atom().
--type driver() :: #driver{}.
-
--type hook_spec() :: #{hookname() => [{callback_m(), callback_f(), spec()}]}.
--type hookname() :: client_connect
-                  | client_connack
-                  | client_connected
-                  | client_disconnected
-                  | client_authenticate
-                  | client_check_acl
-                  | client_subscribe
-                  | client_unsubscribe
-                  | session_created
-                  | session_subscribed
-                  | session_unsubscribed
-                  | session_resumed
-                  | session_discarded
-                  | session_takeovered
-                  | session_terminated
-                  | message_publish
-                  | message_delivered
-                  | message_acked
-                  | message_dropped.
-
--type callback_m() :: atom().
-
--type callback_f() :: atom().
-
--type spec() :: #{
-        topic => binary()   %% for `message` hook only
-       }.
-
--export_type([driver/0]).
-
-%%--------------------------------------------------------------------
-%% Load/Unload APIs
-%%--------------------------------------------------------------------
-
--spec load(atom(), list()) -> {ok, driver()} | {error, term()} .
-load(Name, Opts0) ->
-    case lists:keytake(init_module, 1, Opts0) of
-        false -> {error, not_found_initial_module};
-        {value, {_,InitM}, Opts} ->
-            Spec = pool_spec(Name, Opts),
-            {ok, _} = emqx_extension_hook_sup:start_driver_pool(Spec),
-            do_init(Name, InitM)
-    end.
-
--spec unload(driver()) -> ok.
-unload(#driver{name = Name, init = InitM}) ->
-    do_deinit(Name, InitM),
-    emqx_extension_hook_sup:stop_driver_pool(Name).
-
-do_deinit(Name, InitM) ->
-    _ = raw_call(type(Name), Name, InitM, 'deinit', []),
-    ok.
-
-do_init(Name, InitM) ->
-    Type = type(Name),
-    case raw_call(Type, Name, InitM, 'init', []) of
-        {ok, {HookSpec, State}} ->
-            NHookSpec = resovle_hook_spec(HookSpec),
-            %% Reigster metrics
-            Prefix = "exhook." ++ atom_to_list(Name) ++ ".",
-            ensure_metrics(Prefix, NHookSpec),
-            {ok, #driver{type = Type,
-                         name = Name,
-                         init = InitM,
-                         state = State,
-                         hookspec = NHookSpec,
-                         incfun = incfun(Prefix) }};
-        {error, Reason} ->
-            emqx_extension_hook_sup:stop_driver_pool(Name),
-            {error, Reason}
-    end.
-
-%% @private
-pool_spec(Name, Opts) ->
-    NOpts = lists:keystore(pool_size, 1, Opts, {pool_size, 1}),
-    ecpool:pool_spec(Name, Name, ?MODULE, [{name, Name} | NOpts]).
-
-resovle_hook_spec(HookSpec) ->
-    Atom = fun(B) -> list_to_atom(B) end,
-    HookSpec1 = lists:map(fun({Name, Module, Func}) ->
-                      {Name, Module, Func, []};
-                 (Other) -> Other
-                end, HookSpec),
-    lists:foldr(
-      fun({Name, Module, Func, Spec}, Acc) ->
-            NameAtom = Atom(Name),
-            Acc#{NameAtom => [{Atom(Module), Atom(Func), maps:from_list(Spec)} | maps:get(NameAtom, Acc, [])]}
-    end, #{}, HookSpec1).
-
-ensure_metrics(Prefix, HookSpec) ->
-    Keys = [ list_to_atom(Prefix ++ atom_to_list(K)) || K <- maps:keys(HookSpec)],
-    lists:foreach(fun emqx_metrics:ensure/1, Keys).
-
-incfun(Prefix) ->
-    fun(Name) ->
-        emqx_metrics:inc(list_to_atom(Prefix ++ atom_to_list(Name)))
-    end.
-
-format(#driver{name = Name, init = InitM, hookspec = Hooks}) ->
-    io_lib:format("name=~p, init_module=~p, hooks=~0p", [Name, InitM, maps:keys(Hooks)]).
-
-%%--------------------------------------------------------------------
-%% ecpool callback
-%%--------------------------------------------------------------------
-
--spec connect(list()) -> {ok, pid()} | {error, any()}.
-connect(Opts0) ->
-    case lists:keytake(name, 1, lists:keydelete(ecpool_worker_id, 1, Opts0)) of
-        {_,{_, Name}, Opts}
-          when Name =:= python;
-               Name =:= python3 ->
-            NOpts = resovle_search_path(python, Opts),
-            python:start_link([{python, atom_to_list(Name)} | NOpts]);
-        {_,{_, Name}, Opts}
-          when Name =:= java ->
-            NOpts = resovle_search_path(java, Opts),
-            java:start_link([{java, atom_to_list(Name)} | NOpts])
-    end.
-
-%% @private
-resovle_search_path(java, Opts) ->
-    case proplists:get_value(java_path, Opts) of
-        undefined -> Opts;
-        Path ->
-            Solved = lists:flatten(
-                       lists:join(pathsep(),
-                                  [expand_jar_packages(filename:absname(P))
-                                   || P <- re:split(Path, pathsep(), [{return, list}]), P /= ""])),
-            lists:keystore(java_path, 1, Opts, {java_path, Solved})
-    end;
-
-resovle_search_path(_, Opts) ->
-    Opts.
-
-expand_jar_packages(Path) ->
-    IsJarPkgs = fun(Name) ->
-                    Ext = filename:extension(Name),
-                    Ext == ".jar" orelse Ext == ".zip"
-                end,
-    case file:list_dir(Path) of
-        {ok, []} -> [Path];
-        {error, _} -> [Path];
-        {ok, Names} ->
-            lists:join(pathsep(),
-                       [Path] ++ [filename:join([Path, Name]) || Name <- Names, IsJarPkgs(Name)])
-    end.
-
-pathsep() ->
-    case os:type() of
-        {win32, _} ->
-            ";";
-        _ ->
-            ":"
-    end.
-
-%%--------------------------------------------------------------------
-%% APIs
-%%--------------------------------------------------------------------
-
-name(#driver{name = Name}) ->
-    Name.
-
--spec run_hook(atom(), list(), driver())
-  -> ok
-   | {ok, term()}
-   | {error, term()}.
-run_hook(Name, Args, Driver = #driver{hookspec = HookSpec, incfun = IncFun}) ->
-    case maps:get(Name, HookSpec, []) of
-        [] -> ok;
-        Cbs ->
-            lists:foldl(fun({M, F, Opts}, _) ->
-                case match_topic_filter(Name, proplists:get_value(topic, Args, null), maps:get(topics, Opts, [])) of
-                    true ->
-                        IncFun(Name),
-                        call(M, F, Args, Driver);
-                    _ -> ok
-                end
-            end, ok, Cbs)
-    end.
-
--spec run_hook_fold(atom(), list(), any(), driver())
-  -> ok
-   | {ok, term()}
-   | {error, term()}.
-run_hook_fold(Name, Args, Acc0, Driver = #driver{hookspec = HookSpec, incfun = IncFun}) ->
-    case maps:get(Name, HookSpec, []) of
-        [] -> ok;
-        Cbs ->
-            lists:foldl(fun({M, F, Opts}, Acc) ->
-                case match_topic_filter(Name, proplists:get_value(topic, Args, null), maps:get(topics, Opts, [])) of
-                    true ->
-                        IncFun(Name),
-                        call(M, F, Args ++ [Acc], Driver);
-                    _ -> ok
-                end
-            end, Acc0, Cbs)
-    end.
-
--compile({inline, [match_topic_filter/3]}).
-match_topic_filter(_Name, null, _TopicFilter) ->
-    true;
-match_topic_filter(Name, TopicName, TopicFilter)
-  when Name =:= message_publish;
-       Name =:= message_delivered;
-       Name =:= message_dropped;
-       Name =:= message_acked ->
-    lists:any(fun(F) -> emqx_topic:match(TopicName, F) end, TopicFilter);
-match_topic_filter(_, _, _) ->
-    true.
-
--spec call(atom(), atom(), list(), driver()) -> ok | {ok, term()} | {error, term()}.
-call(Mod, Fun, Args, #driver{name = Name, type = Type, state = State}) ->
-    with_pool(Name, fun(C) ->
-        do_call(Type, C, Mod, Fun, Args ++ [State])
-    end).
-
-raw_call(Type, Name, Mod, Fun, Args) when is_list(Args) ->
-     with_pool(Name, fun(C) ->
-        do_call(Type, C, Mod, Fun, Args)
-    end).
-
-do_call(Type, C, M, F, A) ->
-    case catch apply(Type, call, [C, M, F, A]) of
-        ok -> ok;
-        undefined -> ok;
-        {_Ok = 0, Return} -> {ok, Return};
-        {_Err = 1, Reason} -> {error, Reason};
-        {'EXIT', Reason, Stk} ->
-            ?LOG(error, "CALL ~p ~p:~p(~p), exception: ~p, stacktrace ~0p",
-                        [Type, M, F, A, Reason, Stk]),
-            {error, Reason};
-        _X ->
-            ?LOG(error, "CALL ~p ~p:~p(~p), unknown return: ~0p",
-                        [Type, M, F, A, _X]),
-            {error, unknown_return_format}
-    end.
-
-%%--------------------------------------------------------------------
-%% Internal funcs
-%%--------------------------------------------------------------------
-
-with_pool(Name, Fun) ->
-    ecpool:with_client(Name, Fun).
-
-type(python3) -> python;
-type(python) -> python;
-type(Name) -> Name.
-

+ 0 - 249
apps/emqx_exhook/src/emqx_extension_hook_handler.erl

@@ -1,249 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook_handler).
-
--include("emqx_extension_hook.hrl").
--include_lib("emqx/include/emqx.hrl").
--include_lib("emqx/include/logger.hrl").
-
--logger_header("[ExHook]").
-
--export([ on_client_connect/2
-        , on_client_connack/3
-        , on_client_connected/2
-        , on_client_disconnected/3
-        , on_client_authenticate/2
-        , on_client_check_acl/4
-        , on_client_subscribe/3
-        , on_client_unsubscribe/3
-        ]).
-
-%% Session Lifecircle Hooks
--export([ on_session_created/2
-        , on_session_subscribed/3
-        , on_session_unsubscribed/3
-        , on_session_resumed/2
-        , on_session_discarded/2
-        , on_session_takeovered/2
-        , on_session_terminated/3
-        ]).
-
-%% Utils
--export([ message/1
-        , validator/1
-        , assign_to_message/2
-        , clientinfo/1
-        , stringfy/1
-        ]).
-
--import(emqx_extension_hook,
-        [ cast/2
-        , call_fold/4
-        ]).
-
--exhooks([ {'client.connect',      {?MODULE, on_client_connect,       []}}
-         , {'client.connack',      {?MODULE, on_client_connack,       []}}
-         , {'client.connected',    {?MODULE, on_client_connected,     []}}
-         , {'client.disconnected', {?MODULE, on_client_disconnected,  []}}
-         , {'client.authenticate', {?MODULE, on_client_authenticate,  []}}
-         , {'client.check_acl',    {?MODULE, on_client_check_acl,     []}}
-         , {'client.subscribe',    {?MODULE, on_client_subscribe,     []}}
-         , {'client.unsubscribe',  {?MODULE, on_client_unsubscribe,   []}}
-         , {'session.created',     {?MODULE, on_session_created,      []}}
-         , {'session.subscribed',  {?MODULE, on_session_subscribed,   []}}
-         , {'session.unsubscribed',{?MODULE, on_session_unsubscribed, []}}
-         , {'session.resumed',     {?MODULE, on_session_resumed,      []}}
-         , {'session.discarded',   {?MODULE, on_session_discarded,    []}}
-         , {'session.takeovered',  {?MODULE, on_session_takeovered,   []}}
-         , {'session.terminated',  {?MODULE, on_session_terminated,   []}}
-         ]).
-
-%%--------------------------------------------------------------------
-%% Clients
-%%--------------------------------------------------------------------
-
-on_client_connect(ConnInfo, _Props) ->
-    cast('client_connect', [conninfo(ConnInfo), props(_Props)]).
-
-on_client_connack(ConnInfo, Rc, _Props) ->
-    cast('client_connack', [conninfo(ConnInfo), Rc, props(_Props)]).
-
-on_client_connected(ClientInfo, _ConnInfo) ->
-    cast('client_connected', [clientinfo(ClientInfo)]).
-
-on_client_disconnected(ClientInfo, {shutdown, Reason}, ConnInfo) when is_atom(Reason) ->
-    on_client_disconnected(ClientInfo, Reason, ConnInfo);
-on_client_disconnected(ClientInfo, Reason, _ConnInfo) ->
-    cast('client_disconnected', [clientinfo(ClientInfo), stringfy(Reason)]).
-
-on_client_authenticate(ClientInfo, AuthResult) ->
-    AccArg = maps:get(auth_result, AuthResult, undefined) == success,
-    Name   = 'client_authenticate',
-    case call_fold(Name, [clientinfo(ClientInfo)], AccArg, validator(Name)) of
-        {stop, Bool} when is_boolean(Bool) ->
-            Result = case Bool of true -> success; _ -> not_authorized end,
-            {stop, AuthResult#{auth_result => Result, anonymous => false}};
-        _ ->
-            {ok, AuthResult}
-    end.
-
-on_client_check_acl(ClientInfo, PubSub, Topic, Result) ->
-    AccArg = Result == allow,
-    Name   = 'client_check_acl',
-    case call_fold(Name, [clientinfo(ClientInfo), PubSub, Topic], AccArg, validator(Name)) of
-        {stop, Bool} when is_boolean(Bool) ->
-            NResult = case Bool of true -> allow; _ -> deny end,
-            {stop, NResult};
-        _ -> {ok, Result}
-    end.
-
-on_client_subscribe(ClientInfo, Props, TopicFilters) ->
-    cast('client_subscribe', [clientinfo(ClientInfo), props(Props), topicfilters(TopicFilters)]).
-
-on_client_unsubscribe(Clientinfo, Props, TopicFilters) ->
-    cast('client_unsubscribe', [clientinfo(Clientinfo), props(Props), topicfilters(TopicFilters)]).
-
-%%--------------------------------------------------------------------
-%% Session
-%%--------------------------------------------------------------------
-
-on_session_created(ClientInfo, _SessInfo) ->
-    cast('session_created', [clientinfo(ClientInfo)]).
-
-on_session_subscribed(Clientinfo, Topic, SubOpts) ->
-    cast('session_subscribed', [clientinfo(Clientinfo), Topic, props(SubOpts)]).
-
-on_session_unsubscribed(ClientInfo, Topic, _SubOpts) ->
-    cast('session_unsubscribed', [clientinfo(ClientInfo), Topic]).
-
-on_session_resumed(ClientInfo, _SessInfo) ->
-    cast('session_resumed', [clientinfo(ClientInfo)]).
-
-on_session_discarded(ClientInfo, _SessInfo) ->
-    cast('session_discarded', [clientinfo(ClientInfo)]).
-
-on_session_takeovered(ClientInfo, _SessInfo) ->
-    cast('session_takeovered', [clientinfo(ClientInfo)]).
-
-on_session_terminated(ClientInfo, Reason, _SessInfo) ->
-    cast('session_terminated', [clientinfo(ClientInfo), stringfy(Reason)]).
-
-%%--------------------------------------------------------------------
-%% Types
-
-props(undefined) -> [];
-props(M) when is_map(M) -> maps:to_list(M).
-
-conninfo(_ConnInfo =
-         #{clientid := ClientId, username := Username, peername := {Peerhost, _},
-           sockname := {_, SockPort}, proto_name := ProtoName, proto_ver := ProtoVer,
-           keepalive := Keepalive}) ->
-    [{node, node()},
-     {clientid, ClientId},
-     {username, maybe(Username)},
-     {peerhost, ntoa(Peerhost)},
-     {sockport, SockPort},
-     {proto_name, ProtoName},
-     {proto_ver, ProtoVer},
-     {keepalive, Keepalive}].
-
-clientinfo(ClientInfo =
-           #{clientid := ClientId, username := Username, peerhost := PeerHost,
-             sockport := SockPort, protocol := Protocol, mountpoint := Mountpoiont}) ->
-    [{node, node()},
-     {clientid, ClientId},
-     {username, maybe(Username)},
-     {password, maybe(maps:get(password, ClientInfo, undefined))},
-     {peerhost, ntoa(PeerHost)},
-     {sockport, SockPort},
-     {protocol, Protocol},
-     {mountpoint, maybe(Mountpoiont)},
-     {is_superuser, maps:get(is_superuser, ClientInfo, false)},
-     {anonymous, maps:get(anonymous, ClientInfo, true)}].
-
-message(#message{id = Id, qos = Qos, from = From, topic = Topic, payload = Payload, timestamp = Ts}) ->
-    [{node, node()},
-     {id, hexstr(Id)},
-     {qos, Qos},
-     {from, From},
-     {topic, Topic},
-     {payload, Payload},
-     {timestamp, Ts}].
-
-topicfilters(Tfs = [{_, _}|_]) ->
-    [{Topic, Qos} || {Topic, #{qos := Qos}} <- Tfs];
-topicfilters(Tfs) ->
-    Tfs.
-
-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.
-
-%% @private
-stringfy(Term) when is_binary(Term) ->
-    Term;
-stringfy(Term) when is_atom(Term) ->
-    atom_to_binary(Term, utf8);
-stringfy(Term) when is_tuple(Term) ->
-    iolist_to_binary(io_lib:format("~p", [Term])).
-
-hexstr(B) ->
-    iolist_to_binary([io_lib:format("~2.16.0B", [X]) || X <- binary_to_list(B)]).
-
-%%--------------------------------------------------------------------
-%% Validator funcs
-
-validator(Name) ->
-    fun(V) -> validate_acc_arg(Name, V) end.
-
-validate_acc_arg('client_authenticate', V) when is_boolean(V) -> true;
-validate_acc_arg('client_check_acl',    V) when is_boolean(V) -> true;
-validate_acc_arg('message_publish',     V) when is_list(V) -> validate_msg(V, true);
-validate_acc_arg(_,                     _) -> false.
-
-validate_msg([], Bool) ->
-    Bool;
-validate_msg(_, false) ->
-    false;
-validate_msg([{topic, T} | More], _) ->
-    validate_msg(More, is_binary(T));
-validate_msg([{payload, P} | More], _) ->
-    validate_msg(More, is_binary(P));
-validate_msg([{qos, Q} | More], _) ->
-    validate_msg(More, Q =< 2 andalso Q >= 0);
-validate_msg([{timestamp, T} | More], _) ->
-    validate_msg(More, is_integer(T));
-validate_msg([_ | More], _) ->
-    validate_msg(More, true).
-
-%%--------------------------------------------------------------------
-%% Misc
-
-assign_to_message([], Message) ->
-    Message;
-assign_to_message([{topic, Topic}|More], Message) ->
-    assign_to_message(More, Message#message{topic = Topic});
-assign_to_message([{qos, Qos}|More], Message) ->
-    assign_to_message(More, Message#message{qos = Qos});
-assign_to_message([{payload, Payload}|More], Message) ->
-    assign_to_message(More, Message#message{payload = Payload});
-assign_to_message([_|More], Message) ->
-    assign_to_message(More, Message).

+ 0 - 50
apps/emqx_exhook/src/emqx_extension_hook_sup.erl

@@ -1,50 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook_sup).
-
--behaviour(supervisor).
-
--export([ start_link/0
-        , init/1
-        ]).
-
--export([ start_driver_pool/1
-        , stop_driver_pool/1
-        ]).
-
-%%--------------------------------------------------------------------
-%%  Supervisor APIs & Callbacks
-%%--------------------------------------------------------------------
-
-start_link() ->
-    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-
-init([]) ->
-    {ok, {{one_for_one, 10, 100}, []}}.
-
-%%--------------------------------------------------------------------
-%% APIs
-%%--------------------------------------------------------------------
-
--spec start_driver_pool(map()) -> {ok, pid()} | {error, term()}.
-start_driver_pool(Spec) ->
-    supervisor:start_child(?MODULE, Spec).
-
--spec stop_driver_pool(atom()) -> ok.
-stop_driver_pool(Name) ->
-    ok = supervisor:terminate_child(?MODULE, Name),
-    ok = supervisor:delete_child(?MODULE, Name).

+ 4 - 4
apps/emqx_exhook/test/emqx_exhook_SUITE.erl

@@ -33,9 +33,9 @@ init_per_suite(Cfg) ->
     emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1),
     Cfg.
 
-end_per_suite(Cfg) ->
-    emqx_exhook_demo_svr:stop(),
-    emqx_ct_helpers:stop_apps([emqx_exhook]).
+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),
@@ -49,5 +49,5 @@ set_special_cfgs(emqx_exhook) ->
 %% Test cases
 %%--------------------------------------------------------------------
 
-t_hooks(Cfg) ->
+t_hooks(_Cfg) ->
     ok.

+ 114 - 116
apps/emqx_exhook/test/emqx_exhook_demo_svr.erl

@@ -50,13 +50,7 @@
         ]).
 
 -define(PORT, 9000).
-
--define(HTTP, #{grpc_opts => #{service_protos => [emqx_exhook_pb],
-                               services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr}},
-                listen_opts => #{port => ?PORT,
-                                 socket_options => [{reuseaddr, true}]},
-                pool_opts => #{size => 8},
-                transport_opts => #{ssl => false}}).
+-define(NAME, ?MODULE).
 
 %%--------------------------------------------------------------------
 %% Server APIs
@@ -68,6 +62,7 @@ start() ->
     {ok, Pid}.
 
 stop() ->
+    grpc:stop_server(?NAME),
     ?MODULE ! stop.
 
 take() ->
@@ -79,8 +74,12 @@ in({FunName, Req}) ->
     ?MODULE ! {in, FunName, Req}.
 
 mngr_main() ->
-    application:ensure_all_started(grpcbox),
-    Svr = grpcbox:start_server(?HTTP),
+    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]) ->
@@ -92,7 +91,6 @@ mngr_loop([Svr, Q, Takes]) ->
             {NQ1, NQ2} = reply(Q, queue:in(From, Takes)),
             mngr_loop([Svr, NQ1, NQ2]);
         stop ->
-            supervisor:terminate_child(grpcbox_services_simple_sup, Svr),
             exit(normal)
     end.
 
@@ -111,10 +109,11 @@ reply(Q1, Q2) ->
 %% callbacks
 %%--------------------------------------------------------------------
 
--spec on_provider_loaded(ctx:ctx(), emqx_exhook_pb:on_provider_loadedial_request())
-  -> {ok, emqx_exhook_pb:on_provider_loaded_response(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_provider_loaded(Ctx, Req) ->
+-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 => [
@@ -136,164 +135,163 @@ on_provider_loaded(Ctx, Req) ->
                      #{name => <<"message.publish">>},
                      #{name => <<"message.delivered">>},
                      #{name => <<"message.acked">>},
-                     #{name => <<"message.dropped">>}]}, Ctx}.
-
--spec on_provider_unloaded(ctx:ctx(), emqx_exhook_pb:on_provider_unloadedial_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_provider_unloaded(Ctx, Req) ->
+                     #{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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_client_connect(ctx:ctx(), emqx_exhook_pb:client_connect_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_connect(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_client_connack(ctx:ctx(), emqx_exhook_pb:client_connack_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_connack(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_client_connected(ctx:ctx(), emqx_exhook_pb:client_connected_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_connected(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_client_disconnected(ctx:ctx(), emqx_exhook_pb:client_disconnected_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_disconnected(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_client_authenticate(ctx:ctx(), emqx_exhook_pb:client_authenticate_request())
-  -> {ok, emqx_exhook_pb:bool_result(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_authenticate(Ctx, Req) ->
+-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(Req, Md) ->
     ?MODULE:in({?FUNCTION_NAME, Req}),
     %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
-    {ok, #{type => 'IGNORE'}, Ctx}.
+    {ok, #{type => 'IGNORE'}, Md}.
 
--spec on_client_check_acl(ctx:ctx(), emqx_exhook_pb:client_check_acl_request())
-  -> {ok, emqx_exhook_pb:bool_result(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_check_acl(Ctx, Req) ->
+-spec on_client_check_acl(emqx_exhook_pb:client_check_acl_request(), grpc:metadata())
+    -> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+on_client_check_acl(Req, Md) ->
     ?MODULE:in({?FUNCTION_NAME, Req}),
     %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
-    {ok, #{type => 'STOP_AND_RETURN', value => {bool_result, true}}, Ctx}.
+    {ok, #{type => 'STOP_AND_RETURN', value => {bool_result, true}}, Md}.
 
--spec on_client_subscribe(ctx:ctx(), emqx_exhook_pb:client_subscribe_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_subscribe(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_client_unsubscribe(ctx:ctx(), emqx_exhook_pb:client_unsubscribe_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_client_unsubscribe(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_created(ctx:ctx(), emqx_exhook_pb:session_created_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_created(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_subscribed(ctx:ctx(), emqx_exhook_pb:session_subscribed_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_subscribed(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_unsubscribed(ctx:ctx(), emqx_exhook_pb:session_unsubscribed_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_unsubscribed(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_resumed(ctx:ctx(), emqx_exhook_pb:session_resumed_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_resumed(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_discarded(ctx:ctx(), emqx_exhook_pb:session_discarded_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_discarded(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_takeovered(ctx:ctx(), emqx_exhook_pb:session_takeovered_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_takeovered(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_session_terminated(ctx:ctx(), emqx_exhook_pb:session_terminated_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_session_terminated(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_message_publish(ctx:ctx(), emqx_exhook_pb:message_publish_request())
-  -> {ok, emqx_exhook_pb:valued_response(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_message_publish(Ctx, Req) ->
+-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(Req, Md) ->
     ?MODULE:in({?FUNCTION_NAME, Req}),
     %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
-    {ok, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_message_delivered(ctx:ctx(), emqx_exhook_pb:message_delivered_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_message_delivered(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_message_dropped(ctx:ctx(), emqx_exhook_pb:message_dropped_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_message_dropped(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_message_acked(ctx:ctx(), emqx_exhook_pb:message_acked_request())
-  -> {ok, emqx_exhook_pb:empty_success(), ctx:ctx()}
-   | grpcbox_stream:grpc_error_response().
-on_message_acked(Ctx, Req) ->
+-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, #{}, Ctx}.
+    {ok, #{}, Md}.

+ 0 - 139
apps/emqx_exhook/test/emqx_extension_hook_SUITE.erl

@@ -1,139 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_extension_hook_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_ct_helpers:start_apps([emqx_extension_hook], fun set_special_cfgs/1),
-    emqx_logger:set_log_level(warning),
-    Cfg.
-
-end_per_suite(_) ->
-    emqx_ct_helpers:stop_apps([emqx_extension_hook]).
-
-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,
-                        emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins"));
-set_special_cfgs(emqx_extension_hook) ->
-    application:set_env(emqx_extension_hook, drivers, []),
-    ok.
-
-reload_plugin_with(_DriverName = python3) ->
-    application:stop(emqx_extension_hook),
-    Path = emqx_ct_helpers:deps_path(emqx_extension_hook, "test/scripts"),
-    Drivers = [{python3, [{init_module, main},
-                          {python_path, Path},
-                          {call_timeout, 5000}]}],
-    application:set_env(emqx_extension_hook, drivers, Drivers),
-    application:ensure_all_started(emqx_extension_hook);
-
-reload_plugin_with(_DriverName = java) ->
-    application:stop(emqx_extension_hook),
-
-    ErlPortJar = emqx_ct_helpers:deps_path(erlport, "priv/java/_pkgs/erlport.jar"),
-    Path = emqx_ct_helpers:deps_path(emqx_extension_hook, "test/scripts"),
-    Drivers = [{java, [{init_module, 'Main'},
-                       {java_path, Path},
-                       {call_timeout, 5000}]}],
-
-    %% Compile it
-    ct:pal(os:cmd(lists:concat(["cd ", Path, " && ",
-                                "rm -rf Main.class State.class && ",
-                                "javac -cp ", ErlPortJar, " Main.java"]))),
-
-    application:set_env(emqx_extension_hook, drivers, Drivers),
-    application:ensure_all_started(emqx_extension_hook).
-
-%%--------------------------------------------------------------------
-%% Test cases
-%%--------------------------------------------------------------------
-
-t_python3(_) ->
-    reload_plugin_with(python3),
-    schedule_all_hooks().
-
-t_java(_) ->
-    reload_plugin_with(java),
-    schedule_all_hooks().
-
-schedule_all_hooks() ->
-    ok = emqx_extension_hook_handler:on_client_connect(conninfo(), #{}),
-    ok = emqx_extension_hook_handler:on_client_connack(conninfo(), success,#{}),
-    ok = emqx_extension_hook_handler:on_client_connected(clientinfo(), conninfo()),
-    ok = emqx_extension_hook_handler:on_client_disconnected(clientinfo(), takeovered, conninfo()),
-    {stop, #{auth_result := success,
-             anonymous := false}} = emqx_extension_hook_handler:on_client_authenticate(clientinfo(), #{auth_result => not_authorised, anonymous => true}),
-    {stop, allow} = emqx_extension_hook_handler:on_client_check_acl(clientinfo(), publish, <<"t/a">>, deny),
-    ok = emqx_extension_hook_handler:on_client_subscribe(clientinfo(), #{}, sub_topicfilters()),
-    ok = emqx_extension_hook_handler:on_client_unsubscribe(clientinfo(), #{}, unsub_topicfilters()),
-
-    ok = emqx_extension_hook_handler:on_session_created(clientinfo(), sessinfo()),
-    ok = emqx_extension_hook_handler:on_session_subscribed(clientinfo(), <<"t/a">>, subopts()),
-    ok = emqx_extension_hook_handler:on_session_unsubscribed(clientinfo(), <<"t/a">>, subopts()),
-    ok = emqx_extension_hook_handler:on_session_resumed(clientinfo(), sessinfo()),
-    ok = emqx_extension_hook_handler:on_session_discarded(clientinfo(), sessinfo()),
-    ok = emqx_extension_hook_handler:on_session_takeovered(clientinfo(), sessinfo()),
-    ok = emqx_extension_hook_handler:on_session_terminated(clientinfo(), sockerr, sessinfo()).
-
-%%--------------------------------------------------------------------
-%% Generator
-%%--------------------------------------------------------------------
-
-conninfo() ->
-    #{clientid => <<"123">>,
-      username => <<"abc">>,
-      peername => {{127,0,0,1}, 2341},
-      sockname => {{0,0,0,0}, 1883},
-      proto_name => <<"MQTT">>,
-      proto_ver => 4,
-      keepalive => 60
-     }.
-
-clientinfo() ->
-    #{clientid => <<"123">>,
-      username => <<"abc">>,
-      peerhost => {127,0,0,1},
-      sockport => 1883,
-      protocol => 'mqtt',
-      mountpoint => undefined
-     }.
-
-sub_topicfilters() ->
-    [{<<"t/a">>, #{qos => 1}}].
-
-unsub_topicfilters() ->
-    [<<"t/a">>].
-
-sessinfo() ->
-    {session,xxx,yyy}.
-
-subopts() ->
-    #{qos => 1, rh => 0, rap => 0, nl => 0}.
-

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

@@ -490,6 +490,7 @@ pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
 do_setup() ->
     _ = emqx_exhook_demo_svr:start(),
     emqx_ct_helpers:start_apps([emqx_exhook], fun set_special_cfgs/1),
+    emqx_logger:set_log_level(warning),
     %% waiting first loaded event
     {'on_provider_loaded', _} = emqx_exhook_demo_svr:take(),
     ok.
@@ -499,6 +500,7 @@ do_teardown(_) ->
     %% waiting last unloaded event
     {'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(),
     _ = emqx_exhook_demo_svr:stop(),
+    timer:sleep(2000),
     ok.
 
 set_special_cfgs(emqx) ->

+ 0 - 160
apps/emqx_exhook/test/scripts/Main.java

@@ -1,160 +0,0 @@
-import java.io.*;
-import java.util.*;
-import com.erlport.erlang.term.*;
-
-class State implements Serializable {
-
-    Integer times;
-
-    public State() {
-        times = 0;
-    }
-
-    public Integer incr() {
-        times += 1;
-        return times;
-    }
-
-    @Override
-    public String toString() {
-        return String.format("State(times: %d)", times);
-    }
-}
-
-public class Main {
-
-    public static Object init() {
-        System.err.printf("Initiate driver...\n");
-
-        // [{"topics", ["t/#", "t/a"]}]
-        List<Object> topics = new ArrayList<Object>();
-        topics.add(new Binary("t/#"));
-        topics.add(new Binary("test/#"));
-
-        List<Object> actionOpts  = new ArrayList<Object>();
-        actionOpts.add(Tuple.two(new Atom("topics"), topics));
-
-        Object[] actions0 = new Object[] {
-            Tuple.three("client_connect", "Main", "on_client_connect"),
-            Tuple.three("client_connack", "Main", "on_client_connack"),
-            Tuple.three("client_connected", "Main", "on_client_connected"),
-            Tuple.three("client_disconnected", "Main", "on_client_disconnected"),
-            Tuple.three("client_authenticate", "Main", "on_client_authenticate"),
-            Tuple.three("client_check_acl", "Main", "on_client_check_acl"),
-            Tuple.three("client_subscribe", "Main", "on_client_subscribe"),
-            Tuple.three("client_unsubscribe", "Main", "on_client_unsubscribe"),
-
-            Tuple.three("session_created", "Main", "on_session_created"),
-            Tuple.three("session_subscribed", "Main", "on_session_subscribed"),
-            Tuple.three("session_unsubscribed", "Main", "on_session_unsubscribed"),
-            Tuple.three("session_resumed", "Main", "on_session_resumed"),
-            Tuple.three("session_discarded", "Main", "on_session_discarded"),
-            Tuple.three("session_takeovered", "Main", "on_session_takeovered"),
-            Tuple.three("session_terminated", "Main", "on_session_terminated"),
-
-            Tuple.four("message_publish", "Main", "on_message_publish", actionOpts),
-            Tuple.four("message_delivered", "Main", "on_message_delivered", actionOpts),
-            Tuple.four("message_acked", "Main", "on_message_acked", actionOpts),
-            Tuple.four("message_dropped", "Main", "on_message_dropped", actionOpts)
-        };
-
-        List<Object> actions = new ArrayList<Object>(Arrays.asList(actions0));
-
-        State state = new State();
-        //Tuple state = new Tuple(0);
-
-        // {0 | 1, [{HookName, CallModule, CallFunction, Opts}]}
-        return Tuple.two(0, Tuple.two(actions, state));
-    }
-
-    public static void deinit() {
-
-    }
-
-    // Callbacks
-
-    public static void on_client_connect(Object connInfo, Object props, Object state) {
-        System.err.printf("[Java] on_client_connect: connInfo: %s, props: %s, state: %s\n", connInfo, props, state);
-    }
-
-    public static void on_client_connack(Object connInfo, Object rc, Object props, Object state) {
-        System.err.printf("[Java] on_client_connack: connInfo: %s, rc: %s, props: %s, state: %s\n", connInfo, rc, props, state);
-    }
-
-    public static void on_client_connected(Object clientInfo, Object state) {
-        System.err.printf("[Java] on_client_connected: clientinfo: %s, state: %s\n", clientInfo, state);
-    }
-
-    public static void on_client_disconnected(Object clientInfo, Object reason, Object state) {
-        System.err.printf("[Java] on_client_disconnected: clientinfo: %s, reason: %s, state: %s\n", clientInfo, reason, state);
-    }
-
-    public static Object on_client_authenticate(Object clientInfo, Object authresult, Object state) {
-        System.err.printf("[Java] on_client_authenticate: clientinfo: %s, authresult: %s, state: %s\n", clientInfo, authresult, state);
-
-        return Tuple.two(0, true);
-    }
-
-    public static Object on_client_check_acl(Object clientInfo, Object pubsub, Object topic, Object result, Object state) {
-        System.err.printf("[Java] on_client_check_acl: clientinfo: %s, pubsub: %s, topic: %s, result: %s, state: %s\n", clientInfo, pubsub, topic, result, state);
-
-        return Tuple.two(0, true);
-    }
-
-    public static void on_client_subscribe(Object clientInfo, Object props, Object topic, Object state) {
-        System.err.printf("[Java] on_client_subscribe: clientinfo: %s, props: %s, topic: %s, state: %s\n", clientInfo, props, topic, state);
-    }
-
-    public static void on_client_unsubscribe(Object clientInfo, Object props, Object topic, Object state) {
-        System.err.printf("[Java] on_client_unsubscribe: clientinfo: %s, props: %s, topic: %s, state: %s\n", clientInfo, props, topic, state);
-    }
-
-    // Sessions
-
-    public static void on_session_created(Object clientInfo, Object state) {
-        System.err.printf("[Java] on_session_created: clientinfo: %s, state: %s\n", clientInfo, state);
-    }
-
-    public static void on_session_subscribed(Object clientInfo, Object topic, Object opts, Object state) {
-        System.err.printf("[Java] on_session_subscribed: clientinfo: %s, topic: %s, subopts: %s, state: %s\n", clientInfo, topic, opts, state);
-    }
-
-    public static void on_session_unsubscribed(Object clientInfo, Object topic, Object state) {
-        System.err.printf("[Java] on_session_unsubscribed: clientinfo: %s, topic: %s, state: %s\n", clientInfo, topic, state);
-    }
-
-    public static void on_session_resumed(Object clientInfo, Object state) {
-        System.err.printf("[Java] on_session_resumed: clientinfo: %s, state: %s\n", clientInfo, state);
-    }
-
-    public static void on_session_discarded(Object clientInfo, Object state) {
-        System.err.printf("[Java] on_session_discarded: clientinfo: %s, state: %s\n", clientInfo, state);
-    }
-
-    public static void on_session_takeovered(Object clientInfo, Object state) {
-        System.err.printf("[Java] on_session_takeovered: clientinfo: %s, state: %s\n", clientInfo, state);
-    }
-
-    public static void on_session_terminated(Object clientInfo, Object reason, Object state) {
-        System.err.printf("[Java] on_session_terminated: clientinfo: %s, reason: %s, state: %s\n", clientInfo, reason, state);
-    }
-
-    // Messages
-
-    public static Object on_message_publish(Object message, Object state) {
-        System.err.printf("[Java] on_message_publish: message: %s, state: %s\n", message, state);
-        return Tuple.two(0, message);
-    }
-
-    public static void on_message_dropped(Object message, Object reason, Object state) {
-        System.err.printf("[Java] on_message_dropped: message: %s, reason: %s, state: %s\n", message, reason, state);
-    }
-
-    public static void on_message_delivered(Object clientInfo, Object message, Object state) {
-        System.err.printf("[Java] on_message_delivered: clientinfo: %s, message: %s, state: %s\n", clientInfo, message, state);
-    }
-
-    public static void on_message_acked(Object clientInfo, Object message, Object state) {
-        System.err.printf("[Java] on_message_acked: clientinfo: %s, message: %s, state: %s\n", clientInfo, message, state);
-    }
-}

+ 0 - 134
apps/emqx_exhook/test/scripts/main.py

@@ -1,134 +0,0 @@
-#!/usr/bin/python
-# -*- coding: UTF-8 -*-
-
-OK = 0
-ERROR = 1
-
-## Return :: (HookSpec, State)
-##
-## HookSpec :: [(HookName, CallbackModule, CallbackFunction, Opts)]
-## State  :: Any
-##
-## HookName :: "client_connect" | "client_connack" | "client_connected" | ...
-## CallbackModule :: ...
-## CallbackFunctiin :: ...
-## Opts :: [(Key, Value)]
-def init():
-    ## Maybe a connection object?
-    state = ()
-    hookspec = [("client_connect",      "main", "on_client_connect", []),
-                ("client_connack",      "main", "on_client_connack", []),
-                ("client_connected",    "main", "on_client_connected", []),
-                ("client_disconnected", "main", "on_client_disconnected", []),
-                ("client_authenticate", "main", "on_client_authenticate", []),
-                ("client_check_acl",    "main", "on_client_check_acl", []),
-                ("client_subscribe",    "main", "on_client_subscribe", []),
-                ("client_unsubscribe",  "main", "on_client_unsubscribe", []),
-                ("session_created",     "main", "on_session_created", []),
-                ("session_subscribed",  "main", "on_session_subscribed", []),
-                ("session_unsubscribed","main", "on_session_unsubscribed", []),
-                ("session_resumed",     "main", "on_session_resumed", []),
-                ("session_discarded",   "main", "on_session_discarded", []),
-                ("session_takeovered",  "main", "on_session_takeovered", []),
-                ("session_terminated",  "main", "on_session_terminated", []),
-                ("message_publish",     "main", "on_message_publish", [("topics", ["t/#"])]),
-                ("message_delivered",   "main", "on_message_delivered", [("topics", ["t/#"])]),
-                ("message_acked",       "main", "on_message_acked", [("topics", ["t/#"])]),
-                ("message_dropped",     "main", "on_message_dropped", [("topics", ["t/#"])])
-               ]
-    return (OK, (hookspec, state))
-
-def deinit():
-    return
-
-##--------------------------------------------------------------------
-## Callback functions
-##--------------------------------------------------------------------
-
-
-##--------------------------------------------------------------------
-## Clients
-
-def on_client_connect(conninfo, props, state):
-    print("on_client_connect: conninfo: {0}, props: {1}, state: {2}".format(conninfo, props, state))
-    return
-
-def on_client_connack(conninfo, rc, props, state):
-    print("on_client_connack: conninfo: {0}, rc{1}, props: {2}, state: {3}".format(conninfo, rc, props, state))
-    return
-
-def on_client_connected(clientinfo, state):
-    print("on_client_connected: clientinfo: {0}, state: {1}".format(clientinfo, state))
-    return
-
-def on_client_disconnected(clientinfo, reason, state):
-    print("on_client_disconnected: clientinfo: {0}, reason: {1}, state: {2}".format(clientinfo, reason, state))
-    return
-
-def on_client_authenticate(clientinfo, authresult, state):
-    print("on_client_authenticate: clientinfo: {0}, authresult: {1}, state: {2}".format(clientinfo, authresult, state))
-    ## True / False
-    return (OK, True)
-
-def on_client_check_acl(clientinfo, pubsub, topic, result, state):
-    print("on_client_check_acl: clientinfo: {0}, pubsub: {1}, topic: {2}, result: {3}, state: {4}".format(clientinfo, pubsub, topic, result, state))
-    ## True / False
-    return (OK, True)
-
-def on_client_subscribe(clientinfo, props, topics, state):
-    print("on_client_subscribe: clientinfo: {0}, props: {1}, topics: {2}, state: {3}".format(clientinfo, props, topics, state))
-    return
-
-def on_client_unsubscribe(clientinfo, props, topics, state):
-    print("on_client_unsubscribe: clientinfo: {0}, props: {1}, topics: {2}, state: {3}".format(clientinfo, props, topics, state))
-    return
-
-##--------------------------------------------------------------------
-## Sessions
-
-def on_session_created(clientinfo, state):
-    print("on_session_created: clientinfo: {0}, state: {1}".format(clientinfo, state))
-    return
-
-def on_session_subscribed(clientinfo, topic, opts, state):
-    print("on_session_subscribed: clientinfo: {0}, topic: {1}, opts: {2}, state: {3}".format(clientinfo, topic, opts, state))
-    return
-
-def on_session_unsubscribed(clientinfo, topic, state):
-    print("on_session_unsubscribed: clientinfo: {0}, topic: {1}, state: {2}".format(clientinfo, topic, state))
-    return
-
-def on_session_resumed(clientinfo, state):
-    print("on_session_resumed: clientinfo: {0}, state: {1}".format(clientinfo, state))
-    return
-
-def on_session_discarded(clientinfo, state):
-    print("on_session_discared: clientinfo: {0}, state: {1}".format(clientinfo, state))
-    return
-
-def on_session_takeovered(clientinfo, state):
-    print("on_session_takeovered: clientinfo: {0}, state: {1}".format(clientinfo, state))
-    return
-
-def on_session_terminated(clientinfo, reason, state):
-    print("on_session_terminated: clientinfo: {0}, reason: {1}, state: {2}".format(clientinfo, reason, state))
-    return
-
-##--------------------------------------------------------------------
-## Messages
-
-def on_message_publish(message, state):
-    print("on_message_publish: message: {0}, state: {1}".format(message, state))
-    return message
-
-def on_message_dropped(message, reason, state):
-    print("on_message_dropped: message: {0}, reason: {1}, state: {2}".format(message, reason, state))
-    return
-
-def on_message_delivered(clientinfo, message, state):
-    print("on_message_delivered: clientinfo: {0}, message: {1}, state: {2}".format(clientinfo, message, state))
-    return
-
-def on_message_acked(clientinfo, message, state):
-    print("on_message_acked: clientinfo: {0}, message: {1}, state: {2}".format(clientinfo, message, state))
-    return

+ 5 - 2
apps/emqx_exproto/.gitignore

@@ -41,5 +41,8 @@ erlang.mk
 *.coverdata
 etc/emqx_exproto.conf.rendered
 Mnesia.*/
-__pycache__
-example/*.class
+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

+ 10 - 38
apps/emqx_exproto/README.md

@@ -4,53 +4,25 @@ The `emqx_exproto` extremly enhance the extensibility for EMQ X. It allow using
 
 ## Feature
 
-- [x] Support Python, Java.
-- [x] Support the `tcp`, `ssl`, `udp`, `dtls` socket.
-- [x] Provide the `PUB/SUB` interface to others language.
-
-We temporarily no plans to support other languages. Plaease open a issue if you have to use other programming languages.
+- [x] Based on gRPC, it brings a very wide range of applicability
+- [x] Allows you to use the return value to extend emqx behavior.
 
 ## Architecture
 
 ![EMQ X ExProto Arch](./docs/images/exproto-arch.jpg)
 
-## Drivers
-
-### Python
-
-***Requirements:***
-
-- It requires the emqx hosted machine has Python3 Runtimes
-- An executable commands in your shell, i,g: `python3` or `python`
-
-***Examples:***
-
-See [example/main.python](https://github.com/emqx/emqx-exproto/blob/master/example/main.py)
-
-### Java
-
-See [example/Main.java](https://github.com/emqx/emqx-exproto/blob/master/example/Main.java)
-
-
-## SDK
-
-The SDK encloses the underlying obscure data types and function interfaces. It only provides a convenience for development, it is not required.
-
-See [sdk/README.md](https://github.com/emqx/emqx-exproto/blob/master/sdk/README.md)
-
+## Usage
 
-## Benchmark
+### gRPC service
 
-***Work in progress...***
+See: `priv/protos/exproto.proto`
 
+## Example
 
-## Known Issues or TODOs
+## Recommended gRPC Framework
 
-- Configurable Log System.
-    * The Java driver can not redirect the `stderr` stream to erlang vm on Windows platform
+See: https://github.com/grpc-ecosystem/awesome-grpc
 
-## Reference
+## Thanks
 
-- [erlport](https://github.com/hdima/erlport)
-- [External Term Format](http://erlang.org/doc/apps/erts/erl_ext_dist.html)
-- [The Ports Tutorial of Erlang](http://erlang.org/doc/tutorial/c_port.html)
+- [grpcbox](https://github.com/tsloughter/grpcbox)

+ 71 - 120
apps/emqx_exproto/docs/design.md

@@ -4,173 +4,124 @@
 
 该插件给 EMQ X 带来的扩展性十分的强大,它能以你熟悉语言处理任何的私有协议,并享受由 EMQ X 系统带来的高连接,和高并发的优点。
 
-**声明:当前仅实现了 Python、Java 的支持**
-
 ## 特性
 
-- 多语言支持。快速将接入层的协议实现迁移到 EMQ X 中进行管理
+- 极强的扩展能力。使用 gRPC 作为 RPC 通信框架,支持各个主流编程语言
 - 高吞吐。连接层以完全的异步非阻塞式 I/O 的方式实现
-- 完善的连接层。完全的支持 TCP\TLS UDP\DTLS 类型的连接
+- 连接层透明。完全的支持 TCP\TLS UDP\DTLS 类型的连接管理,并对上层提供统一个 API
 - 连接层的管理能力。例如,最大连接数,连接和吞吐的速率限制,IP 黑名单 等
 
-##  架构
+## 架构
 
 ![Extension-Protocol Arch](images/exproto-arch.jpg)
 
-该插件需要完成的工作包括三部分:
-
-**初始化:** (TODO)
-- loaded:
-- unload:
-
-**连接层:** 该部分主要**维持 Socket 的生命周期,和数据的收发**。它的功能要求包括:
-
-- 监听某个端口。当有新的 TCP/UDP 连接到达后,启动一个连接进程,来维持连接的状态。
-- 调用 `init` 回调。用于通知外部模块**已新建立了一个连接**。
-- 调用 `terminated` 回调。用于通知外部模块连接**已关闭**。
-- 调用 `received` 回调。用于通知外部模块**该连接新收到的数据包**。
-- 提供 `send` 接口。供外部模块调用,**用于发送数据包**。
-- 提供 `close` 接口。供外部模块调用,**用于主动关闭连接**。
-
+该插件主要需要处理的内容包括:
 
-**协议/会话层:**该部分主要**提供 PUB/SUB 接口**,以实现与 EMQ X Broker 系统的消息互通。包括:
+1.  **连接层:** 该部分主要**维持 Socket 的生命周期,和数据的收发**。它的功能要求包括:
+    - 监听某个端口。当有新的 TCP/UDP 连接到达后,启动一个连接进程,来维持连接的状态。
+    - 调用 `OnSocketCreated` 回调。用于通知外部模块**已新建立了一个连接**。
+    - 调用 `OnScoektClosed` 回调。用于通知外部模块连接**已关闭**。
+    - 调用 `OnReceivedBytes` 回调。用于通知外部模块**该连接新收到的数据包**。
+    - 提供 `Send` 接口。供外部模块调用,**用于发送数据包**。
+    - 提供 `Close` 接口。供外部模块调用,**用于主动关闭连接**。
 
-- 提供 `register` 接口。供外部模块调用,用于向集群注册客户端。
-- 提供 `publish` 接口。供外部模块调用,用于发布消息 EMQ X Broker 中。
-- 提供 `subscribe` 接口。供外部模块调用,用于订阅某主题,以实现从 EMQ X Broker 中接收某些下行消息。
-- 提供 `unsubscribe` 接口。供外部模块调用,用于取消订阅某主题。
-- 调用 `deliver` 回调。用于接收下行消息(在订阅主题成功后,如果主题上有消息,便会回调该方法)
+2. **协议/会话层:**该部分主要**提供 PUB/SUB 接口**,以实现与 EMQ X Broker 系统的消息互通。包括:
 
-
-**管理&统计相关:** 该部分主要提供其他**管理&统计相关的接口**。包括:
-
-- 提供 `Hooks` 类的接口。用于与系统的钩子系统进行交互。
-- 提供 `Metrics` 类的接口。用于统计。
-- 提供 `HTTP or CLI` 管理类接口。
+    - 提供 `Authenticate` 接口。供外部模块调用,用于向集群注册客户端。
+    - 提供 `StartTimer` 接口。供外部模块调用,用于为该连接进程启动心跳等定时器。
+    - 提供 `Publish` 接口。供外部模块调用,用于发布消息 EMQ X Broker 中。
+    - 提供 `Subscribe` 接口。供外部模块调用,用于订阅某主题,以实现从 EMQ X Broker 中接收某些下行消息。
+    - 提供 `Unsubscribe` 接口。供外部模块调用,用于取消订阅某主题。
+    - 调用 `OnTimerTimeout` 回调。用于处理定时器超时的事件。
+    - 调用 `OnReceivedMessages` 回调。用于接收下行消息(在订阅主题成功后,如果主题上有消息,便会回调该方法)
 
 ## 接口设计
 
-### 连接层接口
+从 gRPC 上的逻辑来说,emqx-exproto 会作为客户端向用户的 `ProtocolHandler` 服务发送回调请求。同时,它也会作为服务端向用户提供 `ConnectionAdapter` 服务,以提供 emqx-exproto 各个接口的访问。如图:
 
-多语言组件需要向 EMQ X 注册的回调函数:
+![Extension Protocol gRPC Arch](images/exproto-grpc-arch.jpg)
 
-```erlang
-%% Got a new Connection
-init(conn(), conninfo()) -> state().
 
-%% Incoming a data
-recevied(conn(), data(), state()) -> state().
+详情参见:`priv/protos/exproto.proto`,例如接口的定义有:
 
-%% Socket & Connection process terminated
-terminated(conn(), reason(), state()) -> ok.
+```protobuff
+syntax = "proto3";
 
--opaue conn() :: pid().
+package emqx.exproto.v1;
 
--type conninfo() :: [ {socktype, tcp | tls | udp | dtls},
-                    , {peername, {inet:ip_address(), inet:port_number()}},
-                    , {sockname, {inet:ip_address(), inet:port_number()}},
-                    , {peercert, nossl | [{cn, string()}, {dn, string()}]}
-                    ]).
+// The Broker side serivce. It provides a set of APIs to
+// handle a protcol access
+service ConnectionAdapter {
 
--type reason() :: string().
+  // -- socket layer
 
--type state() :: any().
-```
+  rpc Send(SendBytesRequest) returns (CodeResponse) {};
 
+  rpc Close(CloseSocketRequest) returns (CodeResponse) {};
 
-`emqx-exproto` 需要向多语言插件提供的接口:
+  // -- protocol layer
 
-``` erlang
-%% Send a data to socket
-send(conn(), data()) -> ok.
+  rpc Authenticate(AuthenticateRequest) returns (CodeResponse) {};
 
-%% Close the socket
-close(conn() ) -> ok.
-```
+  rpc StartTimer(TimerRequest) returns (CodeResponse) {};
 
+  // -- pub/sub layer
 
-### 协议/会话层接口
+  rpc Publish(PublishRequest) returns (CodeResponse) {};
 
-多语言组件需要向 EMQ X 注册的回调函数:
+  rpc Subscribe(SubscribeRequest) returns (CodeResponse) {};
 
-```erlang
-%% Received a message from a Topic
-deliver(conn(), [message()], state()) -> state().
+  rpc Unsubscribe(UnsubscribeRequest) returns (CodeResponse) {};
+}
 
--type message() :: [ {id, binary()}
-                   , {qos, integer()}
-                   , {from, binary()}
-                   , {topic, binary()}
-                   , {payload, binary()}
-                   , {timestamp, integer()}
-                   ].
-```
+service ConnectionHandler {
 
+  // -- socket layer
 
-`emqx-exproto` 需要向多语言插件提供的接口:
+  rpc OnSocketCreated(SocketCreatedRequest) returns (EmptySuccess) {};
 
-``` erlang
-%% Reigster the client to Broker
-register(conn(), clientinfo()) -> ok | {error, Reason}.
+  rpc OnSocketClosed(SocketClosedRequest) returns (EmptySuccess) {};
 
-%% Publish a message to Broker
-publish(conn(), message()) -> ok.
+  rpc OnReceivedBytes(ReceivedBytesRequest) returns (EmptySuccess) {};
 
-%% Subscribe a topic
-subscribe(conn(), topic(), qos()) -> ok.
+  // -- pub/sub layer
 
-%% Unsubscribe a topic
-unsubscribe(conn(), topic()) -> ok.
+  rpc OnTimerTimeout(TimerTimeoutRequest) returns (EmptySuccess) {};
 
--type clientinfo() :: [ {proto_name, binary()}
-                      , {proto_ver, integer() | string()}
-                      , {clientid, binary()}
-                      , {username, binary()}
-                      , {mountpoint, binary()}}
-                      , {keepalive, non_neg_integer()}
-                      ].
+  rpc OnReceivedMessages(ReceivedMessagesRequest) returns (EmptySuccess) {};
+}
 ```
 
-### 管理&统计相关接口
-
-*TODO..*
-
 ## 配置项设计
 
 1. 以 **监听器( Listener)** 为基础,提供 TCP/UDP 的监听。
    - Listener 目前仅支持:TCP、TLS、UDP、DTLS。(ws、wss、quic 暂不支持)
-2. 每个监听器,会指定一个多语言的驱动,用于调用外部模块的接口
-   - Driver 目前仅支持:python,java
+2. 每个监听器,会指定一个 `ProtocolHandler` 的服务地址,用于调用外部模块的接口。
+3. emqx-exproto 还会监听一个 gRPC 端口用于提供对 `ConnectionAdapter` 服务的访问。
 
 例如:
 
 ``` properties
-## A JT/T 808 TCP based example:
-exproto.listener.jtt808 = 6799
-exproto.listener.jtt808.type = tcp
-exproto.listener.jtt808.driver = python
-# acceptors, max_connections, max_conn_rate, ...
-# proxy_protocol, ...
-# sndbuff, recbuff, ...
-# ssl, cipher, certfile, psk, ...
-
-exproto.listener.jtt808.<key> = <value>
-
-## A CoAP UDP based example
-exproto.listener.coap = 6799
-exproto.listener.coap.type = udp
-exproto.listener.coap.driver = java
+## 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
+
+## ProtocolHandler 服务地址及 https 的证书配置
+exproto.listener.protoname.proto_handler_url = http://127.0.0.1:9001
+#exproto.listener.protoname.proto_handler_certfile =
+#exproto.listener.protoname.proto_handler_cacertfile =
+#exproto.listener.protoname.proto_handler_keyfile =
+
 # ...
 ```
-
-## 集成与调试
-
-参见 SDK 规范、和对应语言的开发手册
-
-## SDK 实现要求
-
-参见 SDK 规范、和对应语言的开发手册
-
-## TODOs:
-
-- 认证 和 发布 订阅鉴权等钩子接入

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


+ 0 - 84
apps/emqx_exproto/docs/sdk-specification.md

@@ -1,84 +0,0 @@
-## SDK 规范
-
-### 动机
-
-SDK 的目的在于方便用户使用 IDE 集成开发、和模拟调试。
-
-### 位置
-
-```
-    +------------------+
-    |   User's Codes   |
-    +------------------+
-    |       SDK        |    <====   The SDK Located
-    +------------------+
-    |     Raw APIs     |
-    +------------------+
-    |      Driver      |
-    +==================+
-             ||
-    +==================+
-    |   EMQ X Plugin   |
-    +------------------+
-```
-
-因此,SDK 的作用在于封装底层的比较晦涩的数据格式和方法,屏蔽驱动的细节。直接提供优化后的 API 供用户使用。
-
-
-### 实现要求
-
-**声明:** stdin, stdout 已用于和 EMQ X 通信,请不要使用。stderr 用于日志输出。
-
-#### 基础项
-
-1. 必须实现 `emqx-exproto` 要求的回调函数和 API 接口,并能够暴露给用户使用
-2. 可以将 `conn()` 类型,封装成为一个连接类。并:
-   - 将各层的回调函数写为不同的 `Interface`,连接类实现该 `Interface`,并强制子类实现其方法。
-   - 将各层的 API 接口写为连接类的方法
-   用户继承该连接类,并实现各个回调。
-
-3. 必须将各个专有的,晦涩的数据类型封装为清晰的类型结构,例如:
-   - 连接类型 `conn()`
-   - 连接层信息:conninfo()
-   - 客户端信息:clientinfo()
-   - 消息:message()
-3. 必须要有对应的开发、部署文档说明
-
-#### 高级项
-
-1. 应能方便用户能在 IDE 中进行编译,开发
-2. 应提供集成测试用的模拟代码。
-   - 例如,生成模拟的数据,发送至用户的程序,方便直接断点调试,而不需要先部署才能使用。
-3. 提供日志输出的方法
-
-### 部署结构
-
-#### 代码依赖结构
-
-从部署的角度看,代码的依赖关系为:
-
-1. 用户代码:
-    * 一定会依赖 SDK
-    * 允许依赖 某个位置的三方/系统库
-2. SDK 代码:
-    * 只能依赖 erlport
-
-#### 部署
-
-从文件存放的位置来看,一个标准的部署结构为:
-
-```
-emqx
-|
-|--- data
-|------- extension
-|---------- <some-sdk-package-name>
-|--------------- <some-classes/scripts-in-sdk>
-|---------- <user's classes/scripts>
-|
-|---------- <another-sdk-package-name>
-|--------------- <some-classes/scripts-in-sdk>
-|---------- <user's classes/scripts>
-```
-
-它表达了:在 `data/extension` 目录下安装了两个 SDK,并且用户都基于 SDK 编写了其回调的代码模块。

+ 12 - 10
apps/emqx_exproto/etc/emqx_exproto.conf

@@ -2,6 +2,13 @@
 ## EMQ X ExProto
 ##====================================================================
 
+exproto.server.http.port = 9100
+
+exproto.server.https.port = 9101
+exproto.server.https.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem
+exproto.server.https.certfile = {{ platform_etc_dir }}/certs/cert.pem
+exproto.server.https.keyfile = {{ platform_etc_dir }}/certs/key.pem
+
 ##--------------------------------------------------------------------
 ## Listeners
 ##--------------------------------------------------------------------
@@ -16,18 +23,13 @@
 ## Examples: tcp://0.0.0.0:7993 | ssl://127.0.0.1:7994
 exproto.listener.protoname = tcp://0.0.0.0:7993
 
-## Driver type
+## The ConnectionHandler server address
 ##
-## Value: python3 | java
-exproto.listener.protoname.driver = python3
+exproto.listener.protoname.connection_handler_url = http://127.0.0.1:9001
 
-## The Search path for driver codes
-##
-exproto.listener.protoname.driver_search_path = {{ platform_data_dir }}/extension
-
-## The driver callback module/class name
-##
-#exproto.listener.protoname.driver_callback_module = main
+#exproto.listener.protoname.connection_handler_certfile =
+#exproto.listener.protoname.connection_handler_cacertfile =
+#exproto.listener.protoname.connection_handler_keyfile =
 
 ## The acceptor pool for external MQTT/TCP listener.
 ##

+ 0 - 136
apps/emqx_exproto/example/Main.java

@@ -1,136 +0,0 @@
-import java.io.*;
-import java.util.*;
-import com.erlport.erlang.term.*;
-import com.erlport.*;
-
-class State implements Serializable {
-
-    Integer times;
-
-    public State() {
-        times = 0;
-    }
-
-    public Integer incr() {
-        times += 1;
-        return times;
-    }
-
-    @Override
-    public String toString() {
-        return String.format("State(times: %d)", times);
-    }
-}
-
-public class Main {
-
-    static Integer OK = 0;
-    static Integer ERROR = 0;
-
-    //-------------------
-    // Connection level
-
-    public static Object init(Object conn, Object connInfo) {
-        System.err.printf("[java] established a conn=%s, connInfo=%s\n", conn, connInfo);
-
-        // set an instance to be the connection state
-        // it just a example structure to record the callback total times
-         Object state = new State();
-
-        // subscribe the topic `t/dn` with qos0
-        subscribe(conn, new Binary("t/dn"), 0);
-
-        // return the initial conn's state
-        return Tuple.two(OK, state);
-    }
-
-    public static Object received(Object conn, Object data, Object state) {
-        System.err.printf("[java] received data conn=%s, data=%s, state=%s\n", conn, data, state);
-
-        // echo the conn's data
-        send(conn, data);
-
-        // return the new conn's state
-        State nstate = (State) state;
-        nstate.incr();
-        return Tuple.two(OK, nstate);
-    }
-
-    public static void terminated(Object conn, Object reason,  Object state) {
-        System.err.printf("[java] terminated conn=%s, reason=%s, state=%s\n", conn, reason, state);
-        return;
-    }
-
-    //-----------------------
-    // Protocol/Session level
-    
-    public static Object deliver(Object conn, Object msgs0,  Object state) {
-        System.err.printf("[java] received messages conn=%s, msgs=%s, state=%s\n", conn, msgs0, state);
-
-        List<Object> msgs = (List<Object>) msgs0;
-        for(Object msg: msgs) {
-            publish(conn, msg);
-        }
-
-        // return the new conn's state
-        State nstate = (State) state;
-        nstate.incr();
-        return Tuple.two(OK, nstate);
-    }
-
-    //-----------------------
-    // APIs
-    public static void send(Object conn, Object data) {
-        try {
-            Erlang.call("emqx_exproto", "send", new Object[]{conn, data}, 5000);
-        } catch (Exception e) {
-            System.err.printf("[java] send data error: %s\n", e);
-        }
-        return;
-    }
-
-    public static void close(Object conn) {
-        try {
-            Erlang.call("emqx_exproto", "close", new Object[]{conn}, 5000);
-        } catch (Exception e) {
-            System.err.printf("[java] send data error: %s\n", e);
-        }
-        return;
-    }
-
-    public static void register(Object conn, Object clientInfo) {
-        try {
-            Erlang.call("emqx_exproto", "register", new Object[]{conn, clientInfo}, 5000);
-        } catch (Exception e) {
-            System.err.printf("[java] send data error: %s\n", e);
-        }
-        return;
-    }
-
-    public static void publish(Object conn, Object message) {
-        try {
-            Erlang.call("emqx_exproto", "publish", new Object[]{conn, message}, 5000);
-        } catch (Exception e) {
-            System.err.printf("[java] send data error: %s\n", e);
-        }
-        return;
-    }
-
-    public static void subscribe(Object conn, Object topic, Object qos) {
-        try {
-            Erlang.call("emqx_exproto", "subscribe", new Object[]{conn, topic, qos}, 5000);
-        } catch (Exception e) {
-            System.err.printf("[java] send data error: %s\n", e);
-        }
-        return;
-    }
-
-    public static void unsubscribe(Object conn, Object topic) {
-        try {
-            Erlang.call("emqx_exproto", "unsubscribe", new Object[]{conn, topic}, 5000);
-        } catch (Exception e) {
-            System.err.printf("[java] send data error: %s\n", e);
-        }
-        return;
-    }
-}

+ 0 - 80
apps/emqx_exproto/example/main.py

@@ -1,80 +0,0 @@
-#!/usr/bin/python
-# -*- coding: UTF-8 -*-
-
-from erlport import Atom
-from erlport import erlang
-
-OK = 0
-ERROR = 1
-
-##--------------------------------------------------------------------
-## Connection level
-
-def init(conn, conninfo):
-    print(f'[python] established a conn={conn}, conninfo={conninfo}')
-
-    ## set an integer num to the connection state
-    ## it just a example structure to record the callback total times
-    state = 0
-
-    ## subscribe the topic `t/dn` with qos0
-    subscribe(conn, b"t/dn", 0)
-
-    ## return the initial conn's state
-    return (OK, state)
-
-def received(conn, data, state):
-    print(f'[python] received data conn={conn}, data={data}, state={state}')
-
-    ## echo the conn's data
-    send(conn, data)
-
-    ## return the new conn's state
-    return (OK, state+1)
-
-def terminated(conn, reason, state):
-    print(f'[python] terminated conn={conn}, reason={reason}, state={state}')
-    return
-
-##--------------------------------------------------------------------
-## Protocol/Session level
-
-def deliver(conn, msgs, state):
-    print(f'[python] received messages: conn={conn}, msgs={msgs}, state={state}')
-
-    ## echo the protocol/session messages
-    for msg in msgs:
-        msg[3] = (Atom(b'topic'), b't/up')
-        publish(conn, msg)
-
-    ## return the new conn's state
-    return (OK, state+1)
-
-##--------------------------------------------------------------------
-## APIs
-##--------------------------------------------------------------------
-
-def send(conn, data):
-    erlang.call(Atom(b'emqx_exproto'), Atom(b'send'), [conn, data])
-    return
-
-def close(conn):
-    erlang.call(Atom(b'emqx_exproto'), Atom(b'close'), [conn])
-    return
-
-def register(conn, clientinfo):
-    erlang.call(Atom(b'emqx_exproto'), Atom(b'register'), [conn, clientinfo])
-    return
-
-def publish(conn, message):
-    erlang.call(Atom(b'emqx_exproto'), Atom(b'publish'), [conn, message])
-    return
-
-def subscribe(conn, topic, qos):
-    erlang.call(Atom(b'emqx_exproto'), Atom(b'subscribe'), [conn, topic, qos])
-    return
-
-def unsubscribe(conn, topic):
-    erlang.call(Atom(b'emqx_exproto'), Atom(b'subscribe'), [conn, topic])
-    return
-

+ 13 - 0
apps/emqx_exproto/include/emqx_exproto.hrl

@@ -22,3 +22,16 @@
 %% TODO:
 -define(UDP_SOCKOPTS, []).
 
+%%--------------------------------------------------------------------
+%% gRPC result code
+
+-define(RESP_UNKNOWN, 'UNKNOWN').
+-define(RESP_SUCCESS, 'SUCCESS').
+-define(RESP_CONN_PROCESS_NOT_ALIVE, 'CONN_PROCESS_NOT_ALIVE').
+-define(RESP_PARAMS_TYPE_ERROR, 'PARAMS_TYPE_ERROR').
+-define(RESP_REQUIRED_PARAMS_MISSED, 'REQUIRED_PARAMS_MISSED').
+-define(RESP_PERMISSION_DENY, 'PERMISSION_DENY').
+-define(IS_GRPC_RESULT_CODE(C), ( C =:= ?RESP_SUCCESS
+                            orelse C =:= ?RESP_CONN_PROCESS_NOT_ALIVE
+                            orelse C =:= ?RESP_REQUIRED_PARAMS_MISSED
+                            orelse C =:= ?RESP_PERMISSION_DENY)).

+ 67 - 17
apps/emqx_exproto/priv/emqx_exproto.schema

@@ -1,25 +1,66 @@
 %% -*-: erlang -*-
+
 %%--------------------------------------------------------------------
-%% Listeners
-%%--------------------------------------------------------------------
+%% Services
+
+{mapping, "exproto.server.http.port", "emqx_exproto.servers", [
+  {datatype, integer}
+]}.
+
+{mapping, "exproto.server.https.port", "emqx_exproto.servers", [
+  {datatype, integer}
+]}.
+
+{mapping, "exproto.server.https.cacertfile", "emqx_exproto.servers", [
+  {datatype, string}
+]}.
+
+{mapping, "exproto.server.https.certfile", "emqx_exproto.servers", [
+  {datatype, string}
+]}.
+
+{mapping, "exproto.server.https.keyfile", "emqx_exproto.servers", [
+  {datatype, string}
+]}.
+
+{translation, "emqx_exproto.servers", fun(Conf) ->
+    Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
+    Http = case cuttlefish:conf_get("exproto.server.http.port", Conf, undefined) of
+               undefined -> [];
+               P1 -> [{http, P1, []}]
+           end,
+    Https = case cuttlefish:conf_get("exproto.server.https.port", Conf, undefined) of
+                undefined -> [];
+                P2 ->
+                    [{https, P2,
+                        Filter([{ssl, true},
+                                {certfile, cuttlefish:conf_get("exproto.server.https.certfile", Conf)},
+                                {keyfile, cuttlefish:conf_get("exproto.server.https.keyfile", Conf)},
+                                {cacertfile, cuttlefish:conf_get("exproto.server.https.cacertfile", Conf)}])}]
+            end,
+    Http ++ Https
+end}.
 
 %%--------------------------------------------------------------------
-%% TCP Listeners
+%% Listeners
 
 {mapping, "exproto.listener.$proto", "emqx_exproto.listeners", [
   {datatype, string}
 ]}.
 
-{mapping, "exproto.listener.$proto.driver", "emqx_exproto.listeners", [
-  {datatype, {enum, [python3, java]}}
+{mapping, "exproto.listener.$proto.connection_handler_url", "emqx_exproto.listeners", [
+  {datatype, string}
+]}.
+
+{mapping, "exproto.listener.$proto.connection_handler_certfile", "emqx_exproto.listeners", [
+  {datatype, string}
 ]}.
 
-{mapping, "exproto.listener.$proto.driver_search_path", "emqx_exproto.listeners", [
+{mapping, "exproto.listener.$proto.connection_handler_cacertfile", "emqx_exproto.listeners", [
   {datatype, string}
 ]}.
 
-{mapping, "exproto.listener.$proto.driver_callback_module", "emqx_exproto.listeners", [
-  {default, "main"},
+{mapping, "exproto.listener.$proto.connection_handler_keyfile", "emqx_exproto.listeners", [
   {datatype, string}
 ]}.
 
@@ -190,14 +231,23 @@
                         {Rate, Limit}
                 end,
 
-    DriverOpts = fun(Prefix) ->
-                     [{driver,
-                       Filter([{type, cuttlefish:conf_get(Prefix ++ ".driver", Conf)},
-                               {path, cuttlefish:conf_get(Prefix ++ ".driver_search_path", Conf)},
-                               {cbm, Atom(cuttlefish:conf_get(Prefix ++ ".driver_callback_module", Conf))}
-                              ])
-                      }]
-                 end,
+    HandlerOpts = fun(Prefix) ->
+                      Opts =
+                      case http_uri:parse(cuttlefish:conf_get(Prefix ++ ".connection_handler_url", Conf)) of
+                          {ok, {http, _, Host, Port, _, _}} ->
+                              [{scheme, http}, {host, Host}, {port, Port}];
+                          {ok, {https, _, Host, Port, _, _}} ->
+                              [{scheme, https}, {host, Host}, {port, Port},
+                               {ssl_options,
+                                 Filter([{certfile, cuttlefish:conf_get(Prefix ++ ".connection_handler_certfile", Conf)},
+                                         {keyfile, cuttlefish:conf_get(Prefix ++ ".connection_handler_keyfile", Conf)},
+                                         {cacertfile, cuttlefish:conf_get(Prefix ++ ".connection_handler_cacertfile", Conf)}
+                                        ])}];
+                          _ ->
+                              error(invaild_connection_handler_url)
+                      end,
+                      [{handler, Opts}]
+                  end,
 
     ConnOpts = fun(Prefix) ->
                    Filter([{active_n, cuttlefish:conf_get(Prefix ++ ".active_n", Conf, undefined)},
@@ -289,7 +339,7 @@
 
     Listeners = fun(Proto) ->
                     Prefix = string:join(["exproto","listener", Proto], "."),
-                    Opts = DriverOpts(Prefix) ++ ConnOpts(Prefix) ++ LisOpts(Prefix),
+                    Opts = HandlerOpts(Prefix) ++ ConnOpts(Prefix) ++ LisOpts(Prefix),
                     case cuttlefish:conf_get(Prefix, Conf, undefined) of
                         undefined -> [];
                         ListenOn0 ->

+ 31 - 8
apps/emqx_exproto/rebar.config

@@ -1,7 +1,4 @@
 %%-*- mode: erlang -*-
-
-{deps, [{erlport, {git, "https://github.com/emqx/erlport", {tag, "v1.2.2"}}}]}.
-
 {edoc_opts, [{preprocess, true}]}.
 
 {erl_opts, [warn_unused_vars,
@@ -10,18 +7,44 @@
             warn_obsolete_guard,
             debug_info,
             {parse_transform}]}.
+{plugins,
+ [rebar3_proper,
+  {grpc_plugin, {git, "https://github.com/HJianBo/grpcbox_plugin", {tag, "v0.9.1"}}}
+]}.
+
+{deps,
+ [{grpc, {git, "https://github.com/emqx/grpc", {tag, "0.5.0"}}}
+ ]}.
+
+{grpc,
+ [{type, all},
+  {protos, ["priv/protos"]},
+  {gpb_opts, [{module_name_prefix, "emqx_"},
+              {module_name_suffix, "_pb"}]}
+ ]}.
+
+{provider_hooks,
+ [{pre, [{compile, {grpc, gen}}]}]}.
 
 {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, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.3.0"}}}
-               , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}}
-               ]}
-    ]}
+ [{test,
+   [{deps,
+     [{emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.3.0"}}}
+     ]}
+   ]}
 ]}.

+ 0 - 11
apps/emqx_exproto/sdk/README.md

@@ -1,11 +0,0 @@
-# SDKs
-
-A specific language SDK is a suite of codes for user-oriented friendly.
-
-Even it does not need it for you to develop the Multiple language support plugins, but it provides more friendly APIs and Abstract for you
-
-
-Now, we provide the following SDKs:
-
-- Java: https://github.com/emqx/emqx-exproto-java-sdk
-- Python: https://github.com/emqx/emqx-exproto-python-sdk

+ 2 - 4
apps/emqx_exproto/src/emqx_exproto.app.src

@@ -4,11 +4,9 @@
   {modules, []},
   {registered, []},
   {mod, {emqx_exproto_app, []}},
-  {applications, [kernel, stdlib, erlport]},
+  {applications, [kernel,stdlib,grpc]},
   {env,[]},
   {licenses, ["Apache-2.0"]},
   {maintainers, ["EMQ X Team <contact@emqx.io>"]},
-  {links, [{"Homepage", "https://emqx.io/"},
-           {"Github", "https://github.com/emqx/emqx-extension-proto"}
-          ]}
+  {links, [{"Homepage", "https://emqx.io/"}]}
  ]}.

+ 24 - 0
apps/emqx_exproto/src/emqx_exproto.app.src.script

@@ -0,0 +1,24 @@
+%%-*- mode: erlang -*-
+%% .app.src.script
+
+RemoveLeadingV =
+    fun(Tag) ->
+        case re:run(Tag, "^[v]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of
+            nomatch ->
+                re:replace(Tag, "/", "-", [{return ,list}]);
+            _ ->
+                %% if it is a version number prefixed by 'v' or 'e', then remove it
+                re:replace(Tag, "[v]", "", [{return ,list}])
+        end
+    end,
+
+case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
+    false -> CONFIG; % env var not defined
+    []    -> CONFIG; % env var set to empty string
+    Tag ->
+       [begin
+           AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
+           {application, App, AppConf0}
+        end || Conf = {application, App, AppConf} <- CONFIG]
+end.
+

+ 9 - 0
apps/emqx_exproto/src/emqx_exproto.appup.src

@@ -0,0 +1,9 @@
+%% -*-: erlang -*-
+{VSN,
+  [
+    {<<".*">>, []}
+  ],
+  [
+    {<<".*">>, []}
+  ]
+}.

+ 78 - 69
apps/emqx_exproto/src/emqx_exproto.erl

@@ -16,24 +16,20 @@
 
 -module(emqx_exproto).
 
--compile({no_auto_import, [register/1]}).
-
 -include("emqx_exproto.hrl").
 
 -export([ start_listeners/0
         , stop_listeners/0
+        , start_listener/1
+        , start_listener/4
+        , stop_listener/4
+        , stop_listener/1
         ]).
 
-%% APIs: Connection level
--export([ send/2
-        , close/1
-        ]).
-
-%% APIs: Protocol/Session level
--export([ register/2
-        , publish/2
-        , subscribe/3
-        , unsubscribe/2
+-export([ start_servers/0
+        , stop_servers/0
+        , start_server/1
+        , stop_server/1
         ]).
 
 %%--------------------------------------------------------------------
@@ -42,78 +38,71 @@
 
 -spec(start_listeners() -> ok).
 start_listeners() ->
-    lists:foreach(fun start_listener/1, application:get_env(?APP, listeners, [])).
+    Listeners = application:get_env(?APP, listeners, []),
+    NListeners = [start_connection_handler_instance(Listener)
+                  || Listener <- Listeners],
+    lists:foreach(fun start_listener/1, NListeners).
 
 -spec(stop_listeners() -> ok).
 stop_listeners() ->
-    lists:foreach(fun stop_listener/1, application:get_env(?APP, listeners, [])).
+    Listeners = application:get_env(?APP, listeners, []),
+    lists:foreach(fun stop_connection_handler_instance/1, Listeners),
+    lists:foreach(fun stop_listener/1, Listeners).
 
-%%--------------------------------------------------------------------
-%% APIs - Connection level
-%%--------------------------------------------------------------------
+-spec(start_servers() -> ok).
+start_servers() ->
+    lists:foreach(fun start_server/1, application:get_env(?APP, servers, [])).
 
--spec(send(pid(), binary()) -> ok).
-send(Conn, Data) when is_pid(Conn), is_binary(Data) ->
-    emqx_exproto_conn:cast(Conn, {send, Data}).
-
--spec(close(pid()) -> ok).
-close(Conn) when is_pid(Conn) ->
-    emqx_exproto_conn:cast(Conn, close).
+-spec(stop_servers() -> ok).
+stop_servers() ->
+    lists:foreach(fun stop_server/1, application:get_env(?APP, servers, [])).
 
 %%--------------------------------------------------------------------
-%% APIs - Protocol/Session level
+%% Internal functions
 %%--------------------------------------------------------------------
 
--spec(register(pid(), list()) -> ok | {error, any()}).
-register(Conn, ClientInfo0) ->
-    case emqx_exproto_types:parse(clientinfo, ClientInfo0) of
+start_connection_handler_instance({_Proto, _LisType, _ListenOn, Opts}) ->
+    Name = name(_Proto, _LisType),
+    {value, {_, HandlerOpts}, LisOpts} = lists:keytake(handler, 1, Opts),
+    {SvrAddr, ChannelOptions} = handler_opts(HandlerOpts),
+    case emqx_exproto_sup:start_grpc_client_channel(Name, SvrAddr, ChannelOptions) of
+        {ok, _ClientChannelPid} ->
+            {_Proto, _LisType, _ListenOn, [{handler, Name} | LisOpts]};
         {error, Reason} ->
-            {error, Reason};
-        ClientInfo ->
-            emqx_exproto_conn:cast(Conn, {register, ClientInfo})
+            io:format(standard_error, "Failed to start ~s's connection handler - ~0p~n!",
+                      [Name, Reason]),
+            error(Reason)
     end.
 
--spec(publish(pid(), list()) -> ok | {error, any()}).
-publish(Conn, Msg0) when is_pid(Conn), is_list(Msg0) ->
-    case emqx_exproto_types:parse(message, Msg0) of
+stop_connection_handler_instance({_Proto, _LisType, _ListenOn, _Opts}) ->
+    Name = name(_Proto, _LisType),
+    _ = emqx_exproto_sup:stop_grpc_client_channel(Name),
+    ok.
+
+start_server({Name, Port, SSLOptions}) ->
+    case emqx_exproto_sup:start_grpc_server(Name, Port, SSLOptions) of
+        {ok, _} ->
+            io:format("Start ~s gRPC server on ~w successfully.~n",
+                      [Name, Port]);
         {error, Reason} ->
-            {error, Reason};
-        Msg ->
-            emqx_exproto_conn:cast(Conn, {publish, Msg})
+            io:format(standard_error, "Failed to start ~s gRPC server on ~w - ~0p~n!",
+                      [Name, Port, Reason]),
+            error({failed_start_server, Reason})
     end.
 
--spec(subscribe(pid(), binary(), emqx_types:qos()) -> ok | {error, any()}).
-subscribe(Conn, Topic, Qos)
-  when is_pid(Conn), is_binary(Topic),
-       (Qos =:= 0 orelse Qos =:= 1 orelse Qos =:= 2) ->
-    emqx_exproto_conn:cast(Conn, {subscribe, Topic, Qos}).
-
--spec(unsubscribe(pid(), binary()) -> ok | {error, any()}).
-unsubscribe(Conn, Topic)
-  when is_pid(Conn), is_binary(Topic) ->
-    emqx_exproto_conn:cast(Conn, {unsubscribe, Topic}).
-
-%%--------------------------------------------------------------------
-%% Internal functions
-%%--------------------------------------------------------------------
+stop_server({Name, Port, _SSLOptions}) ->
+    ok = emqx_exproto_sup:stop_grpc_server(Name),
+    io:format("Stop ~s gRPC server on ~w successfully.~n", [Name, Port]).
 
 start_listener({Proto, LisType, ListenOn, Opts}) ->
     Name = name(Proto, LisType),
-    {value, {_, DriverOpts}, LisOpts} = lists:keytake(driver, 1, Opts),
-    case emqx_exproto_driver_mngr:ensure_driver(Name, DriverOpts) of
-        {ok, _DriverPid}->
-            case start_listener(LisType, Name, ListenOn, [{driver, Name} |LisOpts]) of
-                {ok, _} ->
-                    io:format("Start ~s listener on ~s successfully.~n",
-                              [Name, format(ListenOn)]);
-                {error, Reason} ->
-                    io:format(standard_error, "Failed to start ~s listener on ~s - ~0p~n!",
-                              [Name, format(ListenOn), Reason]),
-                    error(Reason)
-            end;
+    case start_listener(LisType, Name, ListenOn, Opts) of
+        {ok, _} ->
+            io:format("Start ~s listener on ~s successfully.~n",
+                      [Name, format(ListenOn)]);
         {error, Reason} ->
-            io:format(standard_error, "Failed to start ~s's driver - ~0p~n!",
-                      [Name, Reason]),
+            io:format(standard_error, "Failed to start ~s listener on ~s - ~0p~n!",
+                      [Name, format(ListenOn), Reason]),
             error(Reason)
     end.
 
@@ -137,11 +126,11 @@ start_listener(dtls, Name, ListenOn, LisOpts) ->
 
 stop_listener({Proto, LisType, ListenOn, Opts}) ->
     Name = name(Proto, LisType),
-    _ = emqx_exproto_driver_mngr:stop_driver(Name),
     StopRet = stop_listener(LisType, Name, ListenOn, Opts),
     case StopRet of
-        ok -> io:format("Stop ~s listener on ~s successfully.~n",
-                        [Name, format(ListenOn)]);
+        ok ->
+            io:format("Stop ~s listener on ~s successfully.~n",
+                      [Name, format(ListenOn)]);
         {error, Reason} ->
             io:format(standard_error, "Failed to stop ~s listener on ~s - ~p~n.",
                       [Name, format(ListenOn), Reason])
@@ -157,8 +146,12 @@ name(Proto, LisType) ->
     list_to_atom(lists:flatten(io_lib:format("~s:~s", [Proto, LisType]))).
 
 %% @private
+format(Port) when is_integer(Port) ->
+    io_lib:format("0.0.0.0:~w", [Port]);
 format({Addr, Port}) when is_list(Addr) ->
-    io_lib:format("~s:~w", [Addr, Port]).
+    io_lib:format("~s:~w", [Addr, Port]);
+format({Addr, Port}) when is_tuple(Addr) ->
+    io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
 
 %% @private
 merge_tcp_default(Opts) ->
@@ -176,3 +169,19 @@ merge_udp_default(Opts) ->
         false ->
             [{udp_options, ?UDP_SOCKOPTS} | Opts]
     end.
+
+%% @private
+handler_opts(Opts) ->
+    Scheme = proplists:get_value(scheme, Opts),
+    Host = proplists:get_value(host, Opts),
+    Port = proplists:get_value(port, Opts),
+    SvrAddr = lists:flatten(io_lib:format("~s://~s:~w", [Scheme, Host, Port])),
+    ClientOpts = case Scheme of
+                     https ->
+                         SslOpts = lists:keydelete(ssl, 1, proplists:get_value(ssl_options, Opts, [])),
+                         #{gun_opts =>
+                           #{transport => ssl,
+                             transport_opts => SslOpts}};
+                     _ -> #{}
+                 end,
+    {SvrAddr, ClientOpts}.

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

@@ -24,13 +24,14 @@
 
 start(_StartType, _StartArgs) ->
     {ok, Sup} = emqx_exproto_sup:start_link(),
+    emqx_exproto:start_servers(),
     emqx_exproto:start_listeners(),
     {ok, Sup}.
 
 prep_stop(State) ->
+    emqx_exproto:stop_servers(),
     emqx_exproto:stop_listeners(),
     State.
 
 stop(_State) ->
     ok.
-

+ 345 - 132
apps/emqx_exproto/src/emqx_exproto_channel.erl

@@ -16,6 +16,7 @@
 
 -module(emqx_exproto_channel).
 
+-include("emqx_exproto.hrl").
 -include_lib("emqx/include/emqx.hrl").
 -include_lib("emqx/include/emqx_mqtt.hrl").
 -include_lib("emqx/include/types.hrl").
@@ -41,20 +42,26 @@
 -export_type([channel/0]).
 
 -record(channel, {
-          %% Driver name
-          driver :: atom(),
+          %% gRPC channel options
+          gcli :: map(),
           %% Conn info
           conninfo :: emqx_types:conninfo(),
           %% Client info from `register` function
           clientinfo :: maybe(map()),
-          %% Registered
-          registered = false :: boolean(),
           %% Connection state
           conn_state :: conn_state(),
           %% Subscription
           subscriptions = #{},
-          %% Driver level state
-          state :: any()
+          %% Request queue
+          rqueue = queue:new(),
+          %% Inflight function name
+          inflight = undefined,
+          %% Keepalive
+          keepalive :: maybe(emqx_keepalive:keepalive()),
+          %% Timers
+          timers ::  #{atom() => disabled | maybe(reference())},
+          %% Closed reason
+          closed_reason = undefined
          }).
 
 -opaque(channel() :: #channel{}).
@@ -67,6 +74,11 @@
 
 -type(replies() :: emqx_types:packet() | reply() | [reply()]).
 
+-define(TIMER_TABLE, #{
+          alive_timer => keepalive,
+          force_timer => force_close
+         }).
+
 -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]).
 
 -define(SESSION_STATS_KEYS,
@@ -130,20 +142,44 @@ stats(#channel{subscriptions = Subs}) ->
 %%--------------------------------------------------------------------
 
 -spec(init(emqx_exproto_types:conninfo(), proplists:proplist()) -> channel()).
-init(ConnInfo, Options) ->
-    Driver = proplists:get_value(driver, Options),
-    case cb_init(ConnInfo, Driver) of
-            {ok, DState} ->
-                NConnInfo = default_conninfo(ConnInfo),
-                ClientInfo = default_clientinfo(ConnInfo),
-                #channel{driver = Driver,
-                         state = DState,
-                         conninfo = NConnInfo,
-                         clientinfo = ClientInfo,
-                         conn_state = connected};
-            {error, Reason} ->
-                exit({init_channel_failed, Reason})
-    end.
+init(ConnInfo = #{socktype := Socktype,
+                  peername := Peername,
+                  sockname := Sockname,
+                  peercert := Peercert}, Options) ->
+    GRpcChann = proplists:get_value(handler, Options),
+    NConnInfo = default_conninfo(ConnInfo),
+    ClientInfo = default_clientinfo(ConnInfo),
+    Channel = #channel{gcli = #{channel => GRpcChann},
+                       conninfo = NConnInfo,
+                       clientinfo = ClientInfo,
+                       conn_state = connecting,
+                       timers = #{}
+                      },
+
+    Req = #{conninfo =>
+            peercert(Peercert,
+                     #{socktype => socktype(Socktype),
+                       peername => address(Peername),
+                       sockname => address(Sockname)})},
+    try_dispatch(on_socket_created, wrap(Req), Channel).
+
+%% @private
+peercert(nossl, ConnInfo) ->
+    ConnInfo;
+peercert(Peercert, ConnInfo) ->
+    ConnInfo#{peercert =>
+              #{cn => esockd_peercert:common_name(Peercert),
+                dn => esockd_peercert:subject(Peercert)}}.
+
+%% @private
+socktype(tcp) -> 'TCP';
+socktype(ssl) -> 'SSL';
+socktype(udp) -> 'UDP';
+socktype(dtls) -> 'DTLS'.
+
+%% @private
+address({Host, Port}) ->
+    #{host => inet:ntoa(Host), port => Port}.
 
 %%--------------------------------------------------------------------
 %% Handle incoming packet
@@ -153,81 +189,163 @@ init(ConnInfo, Options) ->
       -> {ok, channel()}
        | {shutdown, Reason :: term(), channel()}).
 handle_in(Data, Channel) ->
-    case cb_received(Data, Channel) of
-        {ok, NChannel} ->
-            {ok, NChannel};
-        {error, Reason} ->
-            {shutdown, Reason, Channel}
-    end.
+    Req = #{bytes => Data},
+    {ok, try_dispatch(on_received_bytes, wrap(Req), Channel)}.
 
 -spec(handle_deliver(list(emqx_types:deliver()), channel())
       -> {ok, channel()}
        | {shutdown, Reason :: term(), channel()}).
-handle_deliver(Delivers, Channel) ->
-    %% TODO: ?? Nack delivers from shared subscriptions
-    case cb_deliver(Delivers, Channel) of
-        {ok, NChannel} ->
-            {ok, NChannel};
-        {error, Reason} ->
-            {shutdown, Reason, Channel}
-    end.
+handle_deliver(Delivers, Channel = #channel{clientinfo = ClientInfo}) ->
+    %% XXX: ?? Nack delivers from shared subscriptions
+    Mountpoint = maps:get(mountpoint, ClientInfo),
+    NodeStr = atom_to_binary(node(), utf8),
+    Msgs = lists:map(fun({_, _, Msg}) ->
+               ok = emqx_metrics:inc('messages.delivered'),
+               Msg1 = emqx_hooks:run_fold('message.delivered',
+                                          [ClientInfo], Msg),
+               NMsg = emqx_mountpoint:unmount(Mountpoint, Msg1),
+               #{node => NodeStr,
+                 id => hexstr(emqx_message:id(NMsg)),
+                 qos => emqx_message:qos(NMsg),
+                 from => fmt_from(emqx_message:from(NMsg)),
+                 topic => emqx_message:topic(NMsg),
+                 payload => emqx_message:payload(NMsg),
+                 timestamp => emqx_message:timestamp(NMsg)
+               }
+           end, Delivers),
+    Req = #{messages => Msgs},
+    {ok, try_dispatch(on_received_messages, wrap(Req), Channel)}.
 
 -spec(handle_timeout(reference(), Msg :: term(), channel())
       -> {ok, channel()}
        | {shutdown, Reason :: term(), channel()}).
+handle_timeout(_TRef, {keepalive, _StatVal},
+               Channel = #channel{keepalive = undefined}) ->
+    {ok, Channel};
+handle_timeout(_TRef, {keepalive, StatVal},
+               Channel = #channel{keepalive = Keepalive}) ->
+    case emqx_keepalive:check(StatVal, Keepalive) of
+        {ok, NKeepalive} ->
+            NChannel = Channel#channel{keepalive = NKeepalive},
+            {ok, reset_timer(alive_timer, NChannel)};
+        {error, timeout} ->
+            Req = #{type => 'KEEPALIVE'},
+            {ok, try_dispatch(on_timer_timeout, wrap(Req), Channel)}
+    end;
+
+handle_timeout(_TRef, force_close, Channel = #channel{closed_reason = Reason}) ->
+    {shutdown, {error, {force_close, Reason}}, Channel};
+
 handle_timeout(_TRef, Msg, Channel) ->
     ?WARN("Unexpected timeout: ~p", [Msg]),
     {ok, Channel}.
 
 -spec(handle_call(any(), channel())
      -> {reply, Reply :: term(), channel()}
+      | {reply, Reply :: term(), replies(), channel()}
       | {shutdown, Reason :: term(), Reply :: term(), channel()}).
+
+handle_call({send, Data}, Channel) ->
+    {reply, ok, [{outgoing, Data}], Channel};
+
+handle_call(close, Channel = #channel{conn_state = connected}) ->
+    {reply, ok, [{event, disconnected}, {close, normal}], Channel};
+handle_call(close, Channel) ->
+    {reply, ok, [{close, normal}], Channel};
+
+handle_call({auth, ClientInfo, _Password}, Channel = #channel{conn_state = connected}) ->
+    ?LOG(warning, "Duplicated authorized command, dropped ~p", [ClientInfo]),
+    {ok, {error, ?RESP_PERMISSION_DENY, <<"Duplicated authenticate command">>}, Channel};
+handle_call({auth, ClientInfo0, Password},
+            Channel = #channel{conninfo = ConnInfo,
+                               clientinfo = ClientInfo}) ->
+    ClientInfo1 = enrich_clientinfo(ClientInfo0, ClientInfo),
+    NConnInfo = enrich_conninfo(ClientInfo1, ConnInfo),
+
+    Channel1 = Channel#channel{conninfo = NConnInfo,
+                               clientinfo = ClientInfo1},
+
+    #{clientid := ClientId, username := Username} = ClientInfo1,
+
+    case emqx_access_control:authenticate(ClientInfo1#{password => Password}) of
+        {ok, AuthResult} ->
+            emqx_logger:set_metadata_clientid(ClientId),
+            is_anonymous(AuthResult) andalso
+                emqx_metrics:inc('client.auth.anonymous'),
+            NClientInfo = maps:merge(ClientInfo1, AuthResult),
+            NChannel = Channel1#channel{clientinfo = NClientInfo},
+            case emqx_cm:open_session(true, NClientInfo, NConnInfo) of
+                {ok, _Session} ->
+                    ?LOG(debug, "Client ~s (Username: '~s') authorized successfully!",
+                                [ClientId, Username]),
+                    {reply, ok, [{event, connected}], ensure_connected(NChannel)};
+                {error, Reason} ->
+                    ?LOG(warning, "Client ~s (Username: '~s') open session failed for ~0p",
+                         [ClientId, Username, Reason]),
+                    {reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel}
+            end;
+        {error, Reason} ->
+            ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
+                 [ClientId, Username, Reason]),
+            {reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel}
+    end;
+
+handle_call({start_timer, keepalive, Interval},
+            Channel = #channel{
+                         conninfo = ConnInfo,
+                         clientinfo = ClientInfo
+                        }) ->
+    NConnInfo = ConnInfo#{keepalive => Interval},
+    NClientInfo = ClientInfo#{keepalive => Interval},
+    NChannel = Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo},
+    {reply, ok, ensure_keepalive(NChannel)};
+
+handle_call({subscribe, TopicFilter, Qos},
+            Channel = #channel{
+                         conn_state = connected,
+                         clientinfo = ClientInfo}) ->
+    case is_acl_enabled(ClientInfo) andalso
+         emqx_access_control:check_acl(ClientInfo, subscribe, TopicFilter) of
+        deny ->
+            {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel};
+        _ ->
+            {ok, NChannel} = do_subscribe([{TopicFilter, #{qos => Qos}}], Channel),
+            {reply, ok, NChannel}
+    end;
+
+handle_call({unsubscribe, TopicFilter},
+            Channel = #channel{conn_state = connected}) ->
+    {ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel),
+    {reply, ok, NChannel};
+
+handle_call({publish, Topic, Qos, Payload},
+            Channel = #channel{
+                         conn_state = connected,
+                         clientinfo = ClientInfo
+                                    = #{clientid := From,
+                                        mountpoint := Mountpoint}}) ->
+    case is_acl_enabled(ClientInfo) andalso
+         emqx_access_control:check_acl(ClientInfo, publish, Topic) of
+        deny ->
+            {reply, {error, ?RESP_PERMISSION_DENY, <<"ACL deny">>}, Channel};
+        _ ->
+            Msg = emqx_message:make(From, Qos, Topic, Payload),
+            NMsg = emqx_mountpoint:mount(Mountpoint, Msg),
+            emqx:publish(NMsg),
+            {reply, ok, Channel}
+    end;
+
 handle_call(kick, Channel) ->
     {shutdown, kicked, ok, Channel};
 
 handle_call(Req, Channel) ->
-    ?WARN("Unexpected call: ~p", [Req]),
-    {reply, ok, Channel}.
+    ?LOG(warning, "Unexpected call: ~p", [Req]),
+    {reply, {error, unexpected_call}, Channel}.
 
 -spec(handle_cast(any(), channel())
      -> {ok, channel()}
       | {ok, replies(), channel()}
       | {shutdown, Reason :: term(), channel()}).
-handle_cast({send, Data}, Channel) ->
-    {ok, [{outgoing, Data}], Channel};
-
-handle_cast(close, Channel) ->
-    {ok, [{close, normal}], Channel};
-
-handle_cast({register, ClientInfo}, Channel = #channel{registered = true}) ->
-    ?WARN("Duplicated register command, dropped ~p", [ClientInfo]),
-    {ok, Channel};
-handle_cast({register, ClientInfo0}, Channel = #channel{conninfo = ConnInfo,
-                                                        clientinfo = ClientInfo}) ->
-    ClientInfo1 = maybe_assign_clientid(ClientInfo0),
-    NConnInfo = enrich_conninfo(ClientInfo1, ConnInfo),
-    NClientInfo = enrich_clientinfo(ClientInfo1, ClientInfo),
-    case emqx_cm:open_session(true, NClientInfo, NConnInfo) of
-        {ok, _Session} ->
-            NChannel = Channel#channel{registered = true,
-                                       conninfo = NConnInfo,
-                                       clientinfo = NClientInfo},
-            {ok, [{event, registered}], NChannel};
-        {error, Reason} ->
-            ?ERROR("Register failed, reason: ~p", [Reason]),
-            {shutdown, Reason, {error, Reason}, Channel}
-    end;
-
-handle_cast({subscribe, TopicFilter, Qos}, Channel) ->
-    do_subscribe([{TopicFilter, #{qos => Qos}}], Channel);
-
-handle_cast({unsubscribe, TopicFilter}, Channel) ->
-    do_unsubscribe([{TopicFilter, #{}}], Channel);
-
-handle_cast({publish, Msg}, Channel) ->
-    emqx:publish(enrich_msg(Msg, Channel)),
-    {ok, Channel};
-
 handle_cast(Req, Channel) ->
     ?WARN("Unexpected call: ~p", [Req]),
     {ok, Channel}.
@@ -241,15 +359,41 @@ handle_info({subscribe, TopicFilters}, Channel) ->
 handle_info({unsubscribe, TopicFilters}, Channel) ->
     do_unsubscribe(TopicFilters, Channel);
 
-handle_info({sock_closed, Reason}, Channel) ->
-    {shutdown, {sock_closed, Reason}, Channel};
+handle_info({sock_closed, Reason},
+            Channel = #channel{rqueue = Queue, inflight = Inflight}) ->
+    case queue:len(Queue) =:= 0
+         andalso Inflight =:= undefined of
+        true ->
+            {shutdown, {sock_closed, Reason}, Channel};
+        _ ->
+            %% delayed close process for flushing all callback funcs to gRPC server
+            Channel1 = Channel#channel{closed_reason = {sock_closed, Reason}},
+            Channel2 = ensure_timer(force_timer, Channel1),
+            {ok, ensure_disconnected({sock_closed, Reason}, Channel2)}
+    end;
+
+handle_info({hreply, on_socket_created, {ok, _}}, Channel) ->
+    dispatch_or_close_process(Channel#channel{inflight = undefined});
+handle_info({hreply, FunName, {ok, _}}, Channel)
+  when FunName == on_socket_closed;
+       FunName == on_received_bytes;
+       FunName == on_received_messages;
+       FunName == on_timer_timeout ->
+    dispatch_or_close_process(Channel#channel{inflight = undefined});
+handle_info({hreply, FunName, {error, Reason}}, Channel) ->
+    {shutdown, {error, {FunName, Reason}}, Channel};
+
 handle_info(Info, Channel) ->
-    ?WARN("Unexpected info: ~p", [Info]),
+    ?LOG(warning, "Unexpected info: ~p", [Info]),
     {ok, Channel}.
 
--spec(terminate(any(), channel()) -> ok).
+-spec(terminate(any(), channel()) -> channel()).
 terminate(Reason, Channel) ->
-    cb_terminated(Reason, Channel), ok.
+    Req = #{reason => stringfy(Reason)},
+    try_dispatch(on_socket_closed, wrap(Req), Channel).
+
+is_anonymous(#{anonymous := true}) -> true;
+is_anonymous(_AuthResult)          -> false.
 
 %%--------------------------------------------------------------------
 %% Sub/UnSub
@@ -266,11 +410,22 @@ do_subscribe(TopicFilters, Channel) ->
 do_subscribe(TopicFilter, SubOpts, Channel =
              #channel{clientinfo = ClientInfo = #{mountpoint := Mountpoint},
                       subscriptions = Subs}) ->
+    %% Mountpoint first
     NTopicFilter = emqx_mountpoint:mount(Mountpoint, TopicFilter),
     NSubOpts = maps:merge(?DEFAULT_SUBOPTS, SubOpts),
     SubId = maps:get(clientid, ClientInfo, undefined),
-    _ = emqx:subscribe(NTopicFilter, SubId, NSubOpts),
-    Channel#channel{subscriptions = Subs#{NTopicFilter => SubOpts}}.
+    IsNew = not maps:is_key(NTopicFilter, Subs),
+    case IsNew of
+        true ->
+            ok = emqx:subscribe(NTopicFilter, SubId, NSubOpts),
+            ok = emqx_hooks:run('session.subscribed',
+                                [ClientInfo, NTopicFilter, NSubOpts#{is_new => IsNew}]),
+            Channel#channel{subscriptions = Subs#{NTopicFilter => NSubOpts}};
+        _ ->
+            %% Update subopts
+            ok = emqx:subscribe(NTopicFilter, SubId, NSubOpts),
+            Channel#channel{subscriptions = Subs#{NTopicFilter => NSubOpts}}
+    end.
 
 do_unsubscribe(TopicFilters, Channel) ->
     NChannel = lists:foldl(
@@ -280,74 +435,133 @@ do_unsubscribe(TopicFilters, Channel) ->
     {ok, NChannel}.
 
 %% @private
-do_unsubscribe(TopicFilter, _SubOpts, Channel =
-               #channel{clientinfo = #{mountpoint := Mountpoint},
+do_unsubscribe(TopicFilter, UnSubOpts, Channel =
+               #channel{clientinfo = ClientInfo = #{mountpoint := Mountpoint},
                         subscriptions = Subs}) ->
-    TopicFilter1 = emqx_mountpoint:mount(Mountpoint, TopicFilter),
-    _ = emqx:unsubscribe(TopicFilter1),
-    Channel#channel{subscriptions = maps:remove(TopicFilter1, Subs)}.
+    NTopicFilter = emqx_mountpoint:mount(Mountpoint, TopicFilter),
+    case maps:find(NTopicFilter, Subs) of
+        {ok, SubOpts} ->
+            ok = emqx:unsubscribe(NTopicFilter),
+            ok = emqx_hooks:run('session.unsubscribed',
+                                [ClientInfo, TopicFilter, maps:merge(SubOpts, UnSubOpts)]),
+            Channel#channel{subscriptions = maps:remove(NTopicFilter, Subs)};
+        _ ->
+            Channel
+    end.
 
 %% @private
 parse_topic_filters(TopicFilters) ->
     lists:map(fun emqx_topic:parse/1, TopicFilters).
 
+-compile({inline, [is_acl_enabled/1]}).
+is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) ->
+    (not IsSuperuser) andalso emqx_zone:enable_acl(Zone).
+
 %%--------------------------------------------------------------------
-%% Cbs for driver
+%% Ensure & Hooks
 %%--------------------------------------------------------------------
 
-cb_init(ConnInfo, Driver) ->
-    Args = [self(), emqx_exproto_types:serialize(conninfo, ConnInfo)],
-    emqx_exproto_driver_mngr:call(Driver, {'init', Args}).
+ensure_connected(Channel = #channel{conninfo = ConnInfo,
+                                    clientinfo = ClientInfo}) ->
+    NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
+    ok = run_hooks('client.connected', [ClientInfo, NConnInfo]),
+    Channel#channel{conninfo   = NConnInfo,
+                    conn_state = connected
+                   }.
+
+ensure_disconnected(Reason, Channel = #channel{
+                                         conn_state = connected,
+                                         conninfo = ConnInfo,
+                                         clientinfo = ClientInfo}) ->
+    NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
+    ok = run_hooks('client.disconnected', [ClientInfo, Reason, NConnInfo]),
+    Channel#channel{conninfo = NConnInfo, conn_state = disconnected};
+
+ensure_disconnected(_Reason, Channel = #channel{conninfo = ConnInfo}) ->
+    NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
+    Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
+
+run_hooks(Name, Args) ->
+    ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args).
 
-cb_received(Data, Channel = #channel{state = DState}) ->
-    Args = [self(), Data, DState],
-    do_call_cb('received', Args, Channel).
+%%--------------------------------------------------------------------
+%% Enrich Keepalive
+
+ensure_keepalive(Channel = #channel{clientinfo = ClientInfo}) ->
+    ensure_keepalive_timer(maps:get(keepalive, ClientInfo, 0), Channel).
+
+ensure_keepalive_timer(Interval, Channel) when Interval =< 0 ->
+    Channel;
+ensure_keepalive_timer(Interval, Channel) ->
+    Keepalive = emqx_keepalive:init(timer:seconds(Interval)),
+    ensure_timer(alive_timer, Channel#channel{keepalive = Keepalive}).
+
+ensure_timer(Name, Channel = #channel{timers = Timers}) ->
+    TRef = maps:get(Name, Timers, undefined),
+    Time = interval(Name, Channel),
+    case TRef == undefined andalso Time > 0 of
+        true  -> ensure_timer(Name, Time, Channel);
+        false -> Channel %% Timer disabled or exists
+    end.
 
-cb_terminated(Reason, Channel = #channel{state = DState}) ->
-    Args = [self(), stringfy(Reason), DState],
-    do_call_cb('terminated', Args, Channel).
+ensure_timer(Name, Time, Channel = #channel{timers = Timers}) ->
+    Msg = maps:get(Name, ?TIMER_TABLE),
+    TRef = emqx_misc:start_timer(Time, Msg),
+    Channel#channel{timers = Timers#{Name => TRef}}.
 
-cb_deliver(Delivers, Channel = #channel{state = DState}) ->
-    Msgs = [emqx_exproto_types:serialize(message, Msg) || {_, _, Msg} <- Delivers],
-    Args = [self(), Msgs, DState],
-    do_call_cb('deliver', Args, Channel).
+reset_timer(Name, Channel) ->
+    ensure_timer(Name, clean_timer(Name, Channel)).
 
-%% @private
-do_call_cb(Fun, Args, Channel = #channel{driver = D}) ->
-    case emqx_exproto_driver_mngr:call(D, {Fun, Args}) of
-        ok ->
-            {ok, Channel};
-        {ok, NDState} ->
-            {ok, Channel#channel{state = NDState}};
-        {error, Reason} ->
-            {error, Reason}
-    end.
+clean_timer(Name, Channel = #channel{timers = Timers}) ->
+    Channel#channel{timers = maps:remove(Name, Timers)}.
+
+interval(force_timer, _) ->
+    15000;
+interval(alive_timer, #channel{keepalive = Keepalive}) ->
+    emqx_keepalive:info(interval, Keepalive).
 
 %%--------------------------------------------------------------------
-%% Format
+%% Dispatch
 %%--------------------------------------------------------------------
 
-maybe_assign_clientid(ClientInfo) ->
-    case maps:get(clientid, ClientInfo, undefined) of
-        undefined ->
-            ClientInfo#{clientid => emqx_guid:to_base62(emqx_guid:gen())};
-        _ ->
-            ClientInfo
+wrap(Req) ->
+     Req#{conn => pid_to_list(self())}.
+
+dispatch_or_close_process(Channel = #channel{
+                                       rqueue = Queue,
+                                       inflight = undefined,
+                                       gcli = GClient}) ->
+    case queue:out(Queue) of
+        {empty, _} ->
+            case Channel#channel.conn_state of
+                disconnected ->
+                    {shutdown, Channel#channel.closed_reason, Channel};
+                _ ->
+                    {ok, Channel}
+            end;
+        {{value, {FunName, Req}}, NQueue} ->
+            emqx_exproto_gcli:async_call(FunName, Req, GClient),
+            {ok, Channel#channel{inflight = FunName, rqueue = NQueue}}
     end.
 
-enrich_msg(Msg, #channel{clientinfo = ClientInfo = #{mountpoint := Mountpoint}}) ->
-    NMsg = emqx_mountpoint:mount(Mountpoint, Msg),
-    case maps:get(clientid, ClientInfo, undefined) of
-        undefined -> NMsg;
-        ClientId -> NMsg#message{from = ClientId}
-    end.
+try_dispatch(FunName, Req, Channel = #channel{inflight = undefined, gcli = GClient}) ->
+    emqx_exproto_gcli:async_call(FunName, Req, GClient),
+    Channel#channel{inflight = FunName};
+try_dispatch(FunName, Req, Channel = #channel{rqueue = Queue}) ->
+    Channel#channel{rqueue = queue:in({FunName, Req}, Queue)}.
+
+%%--------------------------------------------------------------------
+%% Format
+%%--------------------------------------------------------------------
 
 enrich_conninfo(InClientInfo, ConnInfo) ->
-    maps:merge(ConnInfo, maps:with([proto_name, proto_ver, clientid, username, keepalive], InClientInfo)).
+    Ks = [proto_name, proto_ver, clientid, username],
+    maps:merge(ConnInfo, maps:with(Ks, InClientInfo)).
 
 enrich_clientinfo(InClientInfo = #{proto_name := ProtoName}, ClientInfo) ->
-    NClientInfo = maps:merge(ClientInfo, maps:with([clientid, username, mountpoint], InClientInfo)),
-    NClientInfo#{protocol => lowcase_atom(ProtoName)}.
+    Ks = [clientid, username, mountpoint],
+    NClientInfo = maps:merge(ClientInfo, maps:with(Ks, InClientInfo)),
+    NClientInfo#{protocol => ProtoName}.
 
 default_conninfo(ConnInfo) ->
     ConnInfo#{proto_name => undefined,
@@ -363,12 +577,12 @@ default_conninfo(ConnInfo) ->
               expiry_interval => 0}.
 
 default_clientinfo(#{peername := {PeerHost, _},
-                      sockname := {_, SockPort}}) ->
-    #{zone         => undefined,
+                     sockname := {_, SockPort}}) ->
+    #{zone         => external,
       protocol     => undefined,
       peerhost     => PeerHost,
       sockport     => SockPort,
-      clientid     => default_clientid(),
+      clientid     => undefined,
       username     => undefined,
       is_bridge    => false,
       is_superuser => false,
@@ -377,10 +591,9 @@ default_clientinfo(#{peername := {PeerHost, _},
 stringfy(Reason) ->
     unicode:characters_to_binary((io_lib:format("~0p", [Reason]))).
 
-lowcase_atom(undefined) ->
-    undefined;
-lowcase_atom(S) ->
-    binary_to_atom(string:lowercase(S), utf8).
+hexstr(Bin) ->
+    [io_lib:format("~2.16.0B",[X]) || <<X:8>> <= Bin].
 
-default_clientid() ->
-    <<"exproto_client_", (list_to_binary(pid_to_list(self())))/binary>>.
+fmt_from(undefined) -> <<>>;
+fmt_from(Bin) when is_binary(Bin) -> Bin;
+fmt_from(T) -> stringfy(T).

+ 38 - 24
apps/emqx_exproto/src/emqx_exproto_conn.erl

@@ -61,8 +61,9 @@
           sockstate :: emqx_types:sockstate(),
           %% The {active, N} option
           active_n :: pos_integer(),
-          %% Send function
-          sendfun :: function(),
+          %% BACKW: e4.2.0-e4.2.1
+          %% We should remove it
+          sendfun :: function() | undefined,
           %% Limiter
           limiter :: maybe(emqx_limiter:limiter()),
           %% Limit Timer
@@ -173,8 +174,10 @@ esockd_wait({esockd_transport, Sock}) ->
         R = {error, _} -> R
     end.
 
-esockd_close({udp, _SockPid, Sock}) ->
-    gen_udp:close(Sock);
+esockd_close({udp, _SockPid, _Sock}) ->
+    %% nothing to do for udp socket
+    %%gen_udp:close(Sock);
+    ok;
 esockd_close({esockd_transport, Sock}) ->
     esockd_transport:fast_close(Sock).
 
@@ -201,14 +204,10 @@ esockd_getstat({udp, _SockPid, Sock}, Stats) ->
 esockd_getstat({esockd_transport, Sock}, Stats) ->
     esockd_transport:getstat(Sock, Stats).
 
-sendfun({udp, _SockPid, Sock}, {Ip, Port}) ->
-    fun(Data) ->
-        gen_udp:send(Sock, Ip, Port, Data)
-    end;
-sendfun({esockd_transport, Sock}, _) ->
-    fun(Data) ->
-        esockd_transport:async_send(Sock, Data)
-    end.
+send(Data, #state{socket = {udp, _SockPid, Sock}, peername = {Ip, Port}}) ->
+    gen_udp:send(Sock, Ip, Port, Data);
+send(Data, #state{socket = {esockd_transport, Sock}}) ->
+    esockd_transport:async_send(Sock, Data).
 
 %%--------------------------------------------------------------------
 %% callbacks
@@ -253,7 +252,7 @@ init_state(WrappedSock, Peername, Options) ->
            sockname     = Sockname,
            sockstate    = idle,
            active_n     = ActiveN,
-           sendfun      = sendfun(WrappedSock, Peername),
+           sendfun      = undefined,
            limiter      = undefined,
            channel      = Channel,
            gc_state     = GcState,
@@ -357,6 +356,9 @@ handle_msg({'$gen_call', From, Req}, State) ->
         {reply, Reply, NState} ->
             gen_server:reply(From, Reply),
             {ok, NState};
+        {reply, Reply, Msgs, NState} ->
+            gen_server:reply(From, Reply),
+            {ok, next_msgs(Msgs), NState};
         {stop, Reason, Reply, NState} ->
             gen_server:reply(From, Reply),
             stop(Reason, NState)
@@ -419,16 +421,16 @@ handle_msg({close, Reason}, State) ->
     ?LOG(debug, "Force to close the socket due to ~p", [Reason]),
     handle_info({sock_closed, Reason}, close_socket(State));
 
-handle_msg({event, registered}, State = #state{channel = Channel}) ->
+handle_msg({event, connected}, State = #state{channel = Channel}) ->
     ClientId = emqx_exproto_channel:info(clientid, Channel),
     emqx_cm:register_channel(ClientId, info(State), stats(State));
 
-%handle_msg({event, disconnected}, State = #state{channel = Channel}) ->
-%    ClientId = emqx_exproto_channel:info(clientid, Channel),
-%    emqx_cm:set_chan_info(ClientId, info(State)),
-%    emqx_cm:connection_closed(ClientId),
-%    {ok, State};
-%
+handle_msg({event, disconnected}, State = #state{channel = Channel}) ->
+    ClientId = emqx_exproto_channel:info(clientid, Channel),
+    emqx_cm:set_chan_info(ClientId, info(State)),
+    emqx_cm:connection_closed(ClientId),
+    {ok, State};
+
 %handle_msg({event, _Other}, State = #state{channel = Channel}) ->
 %    ClientId = emqx_exproto_channel:info(clientid, Channel),
 %    emqx_cm:set_chan_info(ClientId, info(State)),
@@ -480,6 +482,8 @@ handle_call(_From, Req, State = #state{channel = Channel}) ->
     case emqx_exproto_channel:handle_call(Req, Channel) of
         {reply, Reply, NChannel} ->
             {reply, Reply, State#state{channel = NChannel}};
+        {reply, Reply, Replies, NChannel} ->
+            {reply, Reply, Replies, State#state{channel = NChannel}};
         {shutdown, Reason, Reply, NChannel} ->
             shutdown(Reason, Reply, State#state{channel = NChannel})
     end.
@@ -495,7 +499,18 @@ handle_timeout(_TRef, limit_timeout, State) ->
                          limit_timer = undefined
                         },
     handle_info(activate_socket, NState);
-
+handle_timeout(TRef, keepalive, State = #state{socket = Socket,
+                                               channel = Channel})->
+    case emqx_exproto_channel:info(conn_state, Channel) of
+        disconnected -> {ok, State};
+        _ ->
+            case esockd_getstat(Socket, [recv_oct]) of
+                {ok, [{recv_oct, RecvOct}]} ->
+                    handle_timeout(TRef, {keepalive, RecvOct}, State);
+                {error, Reason} ->
+                    handle_info({sock_error, Reason}, State)
+            end
+    end;
 handle_timeout(_TRef, emit_stats, State =
                #state{channel = Channel}) ->
     ClientId = emqx_exproto_channel:info(clientid, Channel),
@@ -541,7 +556,7 @@ with_channel(Fun, Args, State = #state{channel = Channel}) ->
 %%--------------------------------------------------------------------
 %% Handle outgoing packets
 
-handle_outgoing(IoData, #state{socket = Socket, sendfun = SendFun}) ->
+handle_outgoing(IoData, State = #state{socket = Socket}) ->
     ?LOG(debug, "SEND ~0p", [IoData]),
 
     Oct = iolist_size(IoData),
@@ -553,7 +568,7 @@ handle_outgoing(IoData, #state{socket = Socket, sendfun = SendFun}) ->
 
     %% FIXME:
     %%ok = emqx_metrics:inc('bytes.sent', Oct),
-    case SendFun(IoData) of
+    case send(IoData, State) of
         ok -> ok;
         Error = {error, _Reason} ->
             %% Send an inet_reply to postpone handling the error
@@ -665,4 +680,3 @@ stop(Reason, State) ->
 
 stop(Reason, Reply, State) ->
     {stop, Reason, Reply, State}.
-

+ 0 - 302
apps/emqx_exproto/src/emqx_exproto_driver_mngr.erl

@@ -1,302 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_exproto_driver_mngr).
-
--behaviour(gen_server).
-
--include_lib("emqx/include/logger.hrl").
-
--log_header("[ExProto DMngr]").
-
--compile({no_auto_import, [erase/1, get/1]}).
-
-%% API
--export([start_link/0]).
-
-%% Manager APIs
--export([ ensure_driver/2
-        , stop_drivers/0
-        , stop_driver/1
-        ]).
-
-%% Driver APIs
--export([ lookup/1
-        , call/2
-        ]).
-
-%% gen_server callbacks
--export([ init/1
-        , handle_call/3
-        , handle_cast/2
-        , handle_info/2
-        , terminate/2
-        , code_change/3
-        ]).
-
--define(SERVER, ?MODULE).
--define(DEFAULT_CBM, main).
-
--type driver() :: #{name := driver_name(),
-                    type := atom(),
-                    cbm := atom(),
-                    pid := pid(),
-                    opts := list()
-                   }.
-
--type driver_name() :: atom().
-
--type fargs() :: {atom(), list()}.
-
-%%--------------------------------------------------------------------
-%% APIs
-%%--------------------------------------------------------------------
-
-start_link() ->
-    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-
-%%--------------------------------------------------------------------
-%% APIs - Managers
-%%--------------------------------------------------------------------
-
--spec(ensure_driver(driver_name(), list()) -> {ok, pid()} | {error, any()}).
-ensure_driver(Name, Opts) ->
-    {value, {_, Type}, Opts1} = lists:keytake(type, 1, Opts),
-    {value, {_, Cbm},  Opts2} = lists:keytake(cbm, 1, Opts1),
-    gen_server:call(?SERVER, {ensure, {Type, Name, Cbm, Opts2}}).
-
--spec(stop_drivers() -> ok).
-stop_drivers() ->
-    gen_server:call(?SERVER, stop_all).
-
--spec(stop_driver(driver_name()) -> ok).
-stop_driver(Name) ->
-    gen_server:call(?SERVER, {stop, Name}).
-
-%%--------------------------------------------------------------------
-%% APIs - Drivers
-%%--------------------------------------------------------------------
-
--spec(lookup(driver_name()) -> {ok, driver()} | {error, any()}).
-lookup(Name) ->
-    case catch persistent_term:get({?MODULE, Name}) of
-        {'EXIT', {badarg, _}} -> {error, not_found};
-        Driver when is_map(Driver) -> {ok, Driver}
-    end.
-
--spec(call(driver_name(), fargs()) -> ok | {ok, any()} | {error, any()}).
-call(Name, FArgs) ->
-    ensure_alived(Name, fun(Driver) -> do_call(Driver, FArgs) end).
-
-%% @private
-ensure_alived(Name, Fun) ->
-    case catch get(Name) of
-        {'EXIT', _} ->
-            {error, not_found};
-        Driver ->
-            ensure_alived(10, Driver, Fun)
-    end.
-
-%% @private
-ensure_alived(0, _, _) ->
-    {error, driver_process_exited};
-ensure_alived(N, Driver = #{name := Name, pid := Pid}, Fun) ->
-    case is_process_alive(Pid) of
-        true -> Fun(Driver);
-        _ ->
-            timer:sleep(100),
-            #{pid := NPid} = get(Name),
-            case is_process_alive(NPid) of
-                true -> Fun(Driver);
-                _ -> ensure_alived(N-1, Driver#{pid => NPid}, Fun)
-            end
-    end.
-
-%% @private
-do_call(#{type := Type, pid := Pid, cbm := Cbm}, {F, Args}) ->
-    case catch apply(erlport, call, [Pid, Cbm, F, Args, []]) of
-        ok -> ok;
-        undefined -> ok;
-        {_Ok = 0, Return} -> {ok, Return};
-        {_Err = 1, Reason} -> {error, Reason};
-        {'EXIT', Reason, Stk} ->
-            ?LOG(error, "CALL ~p ~p:~p(~p), exception: ~p, stacktrace ~0p",
-                        [Type, Cbm, F, Args, Reason, Stk]),
-            {error, Reason};
-        _X ->
-            ?LOG(error, "CALL ~p ~p:~p(~p), unknown return: ~0p",
-                        [Type, Cbm, F, Args, _X]),
-            {error, unknown_return_format}
-    end.
-
-%%--------------------------------------------------------------------
-%% gen_server callbacks
-%%--------------------------------------------------------------------
-
-init([]) ->
-    process_flag(trap_exit, true),
-    {ok, #{drivers => []}}.
-
-handle_call({ensure, {Type, Name, Cbm, Opts}}, _From, State = #{drivers := Drivers}) ->
-    case lists:keyfind(Name, 1, Drivers) of
-        false ->
-            case do_start_driver(Type, Opts) of
-                {ok, Pid} ->
-                    Driver = #{name => Name,
-                               type => Type,
-                               cbm => Cbm,
-                               pid => Pid,
-                               opts => Opts},
-                    ok = save(Name, Driver),
-                    reply({ok, Driver}, State#{drivers => [{Name, Driver} | Drivers]});
-                {error, Reason} ->
-                    reply({error, Reason}, State)
-            end;
-        {_, Driver} ->
-            reply({ok, Driver}, State)
-    end;
-
-handle_call(stop_all, _From, State = #{drivers := Drivers}) ->
-    lists:foreach(
-      fun({Name, #{pid := Pid}}) ->
-        _ = do_stop_drviver(Pid),
-        _ = erase(Name)
-      end, Drivers),
-    reply(ok, State#{drivers => []});
-
-handle_call({stop, Name}, _From, State = #{drivers := Drivers}) ->
-    case lists:keyfind(Name, 1, Drivers) of
-        false ->
-            reply({error, not_found}, State);
-        {_, #{pid := Pid}} ->
-            _ = do_stop_drviver(Pid),
-            _ = erase(Name),
-            reply(ok, State#{drivers => Drivers -- [{Name, Pid}]})
-    end;
-
-handle_call(Req, _From, State) ->
-    ?WARN("Unexpected request: ~p", [Req]),
-    {reply, ok, State}.
-
-handle_cast(Msg, State) ->
-    ?WARN("Unexpected cast: ~p", [Msg]),
-    {noreply, State}.
-
-handle_info({'EXIT', _From, normal}, State) ->
-    {noreply, State};
-handle_info({'EXIT', From, Reason}, State = #{drivers := Drivers}) ->
-    case [Drv || {_, Drv = #{pid := P}} <- Drivers, P =:= From] of
-        [] -> {noreply, State};
-        [Driver = #{name := Name, type := Type, opts := Opts}] ->
-            ?WARN("Driver ~p crashed: ~p", [Name, Reason]),
-            case do_start_driver(Type, Opts) of
-                {ok, Pid} ->
-                    NDriver = Driver#{pid => Pid},
-                    ok = save(Name, NDriver),
-                    NDrivers = lists:keyreplace(Name, 1, Drivers, {Name, NDriver}),
-                    ?WARN("Restarted driver ~p, pid: ~p", [Name, Pid]),
-                    {noreply, State#{drivers => NDrivers}};
-                {error, Reason} ->
-                    ?WARN("Restart driver ~p failed: ~p", [Name, Reason]),
-                    {noreply, State}
-            end
-    end;
-
-handle_info(Info, State) ->
-    ?WARN("Unexpected info: ~p", [Info]),
-    {noreply, State}.
-
-terminate(_Reason, _State) ->
-    ok.
-
-code_change(_OldVsn, State, _Extra) ->
-    {ok, State}.
-
-%%--------------------------------------------------------------------
-%% Internal funcs
-%%--------------------------------------------------------------------
-
-do_start_driver(Type, Opts)
-  when Type =:= python2;
-       Type =:= python3 ->
-    NOpts = resovle_search_path(python, Opts),
-    python:start_link([{python, atom_to_list(Type)} | NOpts]);
-
-do_start_driver(Type, Opts)
-  when Type =:= java ->
-    NOpts = resovle_search_path(java, Opts),
-    java:start_link([{java, atom_to_list(Type)} | NOpts]);
-
-do_start_driver(Type, _) ->
-    {error, {invalid_driver_type, Type}}.
-
-do_stop_drviver(DriverPid) ->
-    erlport:stop(DriverPid).
-%% @private
-resovle_search_path(java, Opts) ->
-    case lists:keytake(path, 1, Opts) of
-        false -> Opts;
-        {value, {_, Path}, NOpts} ->
-            Solved = lists:flatten(
-                       lists:join(pathsep(),
-                                  [expand_jar_packages(filename:absname(P))
-                                   || P <- re:split(Path, pathsep(), [{return, list}]), P /= ""])),
-            [{java_path, Solved} | NOpts]
-    end;
-resovle_search_path(python, Opts) ->
-    case lists:keytake(path, 1, Opts) of
-        false -> Opts;
-        {value, {_, Path}, NOpts} ->
-            [{python_path, Path} | NOpts]
-    end.
-
-%% @private
-expand_jar_packages(Path) ->
-    IsJarPkgs = fun(Name) ->
-                    Ext = filename:extension(Name),
-                    Ext == ".jar" orelse Ext == ".zip"
-                end,
-    case file:list_dir(Path) of
-        {ok, []} -> [Path];
-        {error, _} -> [Path];
-        {ok, Names} ->
-            lists:join(pathsep(),
-                       [Path] ++ [filename:join([Path, Name]) || Name <- Names, IsJarPkgs(Name)])
-    end.
-
-%% @private
-pathsep() ->
-    case os:type() of
-        {win32, _} ->
-            ";";
-        _ ->
-            ":"
-    end.
-
-%%--------------------------------------------------------------------
-%% Utils
-
-reply(Term, State) ->
-    {reply, Term, State}.
-
-save(Name, Driver) ->
-    persistent_term:put({?MODULE, Name}, Driver).
-
-erase(Name) ->
-    persistent_term:erase({?MODULE, Name}).
-
-get(Name) ->
-    persistent_term:get({?MODULE, Name}).

+ 110 - 0
apps/emqx_exproto/src/emqx_exproto_gcli.erl

@@ -0,0 +1,110 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+%% the gRPC client worker for ConnectionHandler service
+-module(emqx_exproto_gcli).
+
+-behaviour(gen_server).
+
+-include_lib("emqx/include/logger.hrl").
+
+-logger_header("[ExProto gClient]").
+
+%% APIs
+-export([async_call/3]).
+
+-export([start_link/2]).
+
+%% gen_server callbacks
+-export([ init/1
+        , handle_call/3
+        , handle_cast/2
+        , handle_info/2
+        , terminate/2
+        , code_change/3
+        ]).
+
+-define(CONN_ADAPTER_MOD, emqx_exproto_v_1_connection_handler_client).
+
+%%--------------------------------------------------------------------
+%% APIs
+%%--------------------------------------------------------------------
+
+start_link(Pool, Id) ->
+    gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)},
+                          ?MODULE, [Pool, Id], []).
+
+async_call(FunName, Req = #{conn := Conn}, Options) ->
+    cast(pick(Conn), {rpc, FunName, Req, Options, self()}).
+
+%%--------------------------------------------------------------------
+%% cast, pick
+%%--------------------------------------------------------------------
+
+-compile({inline, [cast/2, pick/1]}).
+
+cast(Deliver, Msg) ->
+    gen_server:cast(Deliver, Msg).
+
+pick(Conn) ->
+    gproc_pool:pick_worker(exproto_gcli_pool, Conn).
+
+%%--------------------------------------------------------------------
+%% gen_server callbacks
+%%--------------------------------------------------------------------
+
+init([Pool, Id]) ->
+    true = gproc_pool:connect_worker(Pool, {Pool, Id}),
+    {ok, #{pool => Pool, id => Id}}.
+
+handle_call(_Request, _From, State) ->
+    {reply, ok, State}.
+
+handle_cast({rpc, Fun, Req, Options, From}, State) ->
+    case catch apply(?CONN_ADAPTER_MOD, Fun, [Req, Options]) of
+        {ok, Resp, _Metadata} ->
+            ?LOG(debug, "~p got {ok, ~0p, ~0p}", [Fun, Resp, _Metadata]),
+            reply(From, Fun, {ok, Resp});
+        {error, {Code, Msg}, _Metadata} ->
+            ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) response errcode: ~0p, errmsg: ~0p",
+                        [?CONN_ADAPTER_MOD, Fun, Req, Options, Code, Msg]),
+            reply(From, Fun, {error, {Code, Msg}});
+        {error, Reason} ->
+            ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) error: ~0p",
+                        [?CONN_ADAPTER_MOD, Fun, Req, Options, Reason]),
+            reply(From, Fun, {error, Reason});
+        {'EXIT', {Reason, Stk}} ->
+            ?LOG(error, "CALL ~0p:~0p(~0p, ~0p) throw an exception: ~0p, stacktrace: ~0p",
+                        [?CONN_ADAPTER_MOD, Fun, Req, Options, Reason, Stk]),
+            reply(From, Fun, {error, Reason})
+    end,
+    {noreply, State}.
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Internal funcs
+%%--------------------------------------------------------------------
+
+reply(Pid, Fun, Result) ->
+    Pid ! {hreply, Fun, Result}.

+ 154 - 0
apps/emqx_exproto/src/emqx_exproto_gsvr.erl

@@ -0,0 +1,154 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+%% The gRPC server for ConnectionAdapter
+-module(emqx_exproto_gsvr).
+
+-behavior(emqx_exproto_v_1_connection_adapter_bhvr).
+
+-include("emqx_exproto.hrl").
+-include_lib("emqx/include/logger.hrl").
+
+-logger_header("[ExProto gServer]").
+
+-define(IS_QOS(X), (X =:= 0 orelse X =:= 1 orelse X =:= 2)).
+
+%% gRPC server callbacks
+-export([ send/2
+        , close/2
+        , authenticate/2
+        , start_timer/2
+        , publish/2
+        , subscribe/2
+        , unsubscribe/2
+        ]).
+
+%%--------------------------------------------------------------------
+%% gRPC ConnectionAdapter service
+%%--------------------------------------------------------------------
+
+-spec send(emqx_exproto_pb:send_bytes_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+send(Req = #{conn := Conn, bytes := Bytes}, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response(call(Conn, {send, Bytes})), Md}.
+
+-spec close(emqx_exproto_pb:close_socket_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+close(Req = #{conn := Conn}, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response(call(Conn, close)), Md}.
+
+-spec authenticate(emqx_exproto_pb:authenticate_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+authenticate(Req = #{conn := Conn,
+                     password := Password,
+                     clientinfo := ClientInfo}, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    case validate(clientinfo, ClientInfo) of
+        false ->
+            {ok, response({error, ?RESP_REQUIRED_PARAMS_MISSED}), Md};
+        _ ->
+            {ok, response(call(Conn, {auth, ClientInfo, Password})), Md}
+    end.
+
+-spec start_timer(emqx_exproto_pb:timer_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+start_timer(Req = #{conn := Conn, type := Type, interval := Interval}, Md)
+  when Type =:= 'KEEPALIVE' andalso Interval > 0 ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response(call(Conn, {start_timer, keepalive, Interval})), Md};
+start_timer(Req, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response({error, ?RESP_PARAMS_TYPE_ERROR}), Md}.
+
+-spec publish(emqx_exproto_pb:publish_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+publish(Req = #{conn := Conn, topic := Topic, qos := Qos, payload := Payload}, Md)
+  when ?IS_QOS(Qos) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response(call(Conn, {publish, Topic, Qos, Payload})), Md};
+
+publish(Req, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response({error, ?RESP_PARAMS_TYPE_ERROR}), Md}.
+
+-spec subscribe(emqx_exproto_pb:subscribe_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+subscribe(Req = #{conn := Conn, topic := Topic, qos := Qos}, Md)
+  when ?IS_QOS(Qos) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response(call(Conn, {subscribe, Topic, Qos})), Md};
+
+subscribe(Req, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response({error, ?RESP_PARAMS_TYPE_ERROR}), Md}.
+
+-spec unsubscribe(emqx_exproto_pb:unsubscribe_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:code_response(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+unsubscribe(Req = #{conn := Conn, topic := Topic}, Md) ->
+    ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]),
+    {ok, response(call(Conn, {unsubscribe, Topic})), Md}.
+
+%%--------------------------------------------------------------------
+%% Internal funcs
+%%--------------------------------------------------------------------
+
+to_pid(ConnStr) ->
+    list_to_pid(binary_to_list(ConnStr)).
+
+call(ConnStr, Req) ->
+    case catch  to_pid(ConnStr) of
+        {'EXIT', {badarg, _}} ->
+            {error, ?RESP_PARAMS_TYPE_ERROR,
+                    <<"The conn type error">>};
+        Pid when is_pid(Pid) ->
+            case erlang:is_process_alive(Pid) of
+                true ->
+                    emqx_exproto_conn:call(Pid, Req);
+                false ->
+                    {error, ?RESP_CONN_PROCESS_NOT_ALIVE,
+                            <<"Connection process is not alive">>}
+            end
+    end.
+
+%%--------------------------------------------------------------------
+%% Data types
+
+stringfy(Reason) ->
+    unicode:characters_to_binary((io_lib:format("~0p", [Reason]))).
+
+validate(clientinfo, M) ->
+    Required = [proto_name, proto_ver, clientid],
+    lists:all(fun(K) -> maps:is_key(K, M) end, Required).
+
+response(ok) ->
+    #{code => ?RESP_SUCCESS};
+response({error, Code, Reason})
+  when ?IS_GRPC_RESULT_CODE(Code) ->
+    #{code => Code, message => stringfy(Reason)};
+response({error, Code})
+  when ?IS_GRPC_RESULT_CODE(Code) ->
+    #{code => Code};
+response(Other) ->
+    #{code => ?RESP_UNKNOWN, message => stringfy(Other)}.

+ 55 - 8
apps/emqx_exproto/src/emqx_exproto_sup.erl

@@ -20,17 +20,64 @@
 
 -export([start_link/0]).
 
+-export([ start_grpc_server/3
+        , stop_grpc_server/1
+        , start_grpc_client_channel/3
+        , stop_grpc_client_channel/1
+        ]).
+
 -export([init/1]).
 
+%%--------------------------------------------------------------------
+%% APIs
+%%--------------------------------------------------------------------
+
 start_link() ->
     supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
-init([]) ->
-    DriverMngr = #{id => driver_mngr,
-                   start => {emqx_exproto_driver_mngr, start_link, []},
-                   restart => permanent,
-                   shutdown => 5000,
-                   type => worker,
-                   modules => [emqx_exproto_driver_mngr]},
-    {ok, {{one_for_all, 10, 5}, [DriverMngr]}}.
+-spec start_grpc_server(atom(), inet:port_number(), list())
+  -> {ok, pid()} | {error, term()}.
+start_grpc_server(Name, Port, SSLOptions) ->
+    Services = #{protos => [emqx_exproto_pb],
+                 services => #{'emqx.exproto.v1.ConnectionAdapter' => emqx_exproto_gsvr}
+                },
+    Options = case SSLOptions of
+                  [] -> [];
+                  _ ->
+                      [{ssl_options, lists:keydelete(ssl, 1, SSLOptions)}]
+              end,
+    grpc:start_server(prefix(Name), Port, Services, Options).
+
+-spec stop_grpc_server(atom()) -> ok.
+stop_grpc_server(Name) ->
+    grpc:stop_server(prefix(Name)).
+
+-spec start_grpc_client_channel(
+        atom(),
+        uri_string:uri_string(),
+        grpc_client:grpc_opts()) -> {ok, pid()} | {error, term()}.
+start_grpc_client_channel(Name, SvrAddr, ClientOpts) ->
+    grpc_client_sup:create_channel_pool(Name, SvrAddr, ClientOpts).
 
+-spec stop_grpc_client_channel(atom()) -> ok.
+stop_grpc_client_channel(Name) ->
+    grpc_client_sup:stop_channel_pool(Name).
+
+%% @private
+prefix(Name) when is_atom(Name) ->
+    "exproto:" ++ atom_to_list(Name);
+prefix(Name) when is_binary(Name) ->
+    "exproto:" ++ binary_to_list(Name);
+prefix(Name) when is_list(Name) ->
+    "exproto:" ++ Name.
+
+%%--------------------------------------------------------------------
+%% Supervisor callbacks
+%%--------------------------------------------------------------------
+
+init([]) ->
+    %% gRPC Client Pool
+    PoolSize = emqx_vm:schedulers() * 2,
+    Pool = emqx_pool_sup:spec([exproto_gcli_pool, hash, PoolSize,
+                               {emqx_exproto_gcli, start_link, []}]),
+    {ok, {{one_for_one, 10, 5}, [Pool]}}.

+ 0 - 179
apps/emqx_exproto/src/emqx_exproto_types.erl

@@ -1,179 +0,0 @@
-%%--------------------------------------------------------------------
-%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%%     http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%--------------------------------------------------------------------
-
--module(emqx_exproto_types).
-
--include_lib("emqx/include/emqx.hrl").
--include_lib("emqx/include/types.hrl").
-
--import(proplists, [get_value/2]).
-
--export([ parse/2
-        , serialize/2
-        ]).
-
--type(clientinfo() :: #{ proto_name := maybe(binary())
-                       , proto_ver  := maybe(non_neg_integer())
-                       , clientid   := maybe(binary())
-                       , username   := maybe(binary())
-                       , mountpoint := maybe(binary())
-                       , keepalive  := maybe(non_neg_integer())
-                       }).
-
--type(conninfo() :: #{ socktype := tcp | tls | udp | dtls
-                     , peername := emqx_types:peername()
-                     , sockname := emqx_types:sockname()
-                     , peercert := nossl | binary() | list()
-                     , conn_mod := atom()
-                     }).
-
--export_type([conninfo/0, clientinfo/0]).
-
--define(UP_DATA_SCHEMA_CLIENTINFO, 
-            [ {proto_name, optional, binary}
-            , {proto_ver, optional, [integer, binary]}
-            , {clientid, optional, binary}
-            , {username, optional, binary}
-            , {mountpoint, optional, binary}
-            , {keepalive, optional, integer}
-            ]).
-
--define(UP_DATA_SCHEMA_MESSAGE,
-            [ {id, {optional, fun emqx_guid:gen/0}, binary}
-            , {qos, required, [{enum, [0, 1, 2]}]}
-            , {from, optional, [binary, atom]}
-            , {topic, required, binary}
-            , {payload, required, binary}
-            , {timestamp, {optional, fun() -> erlang:system_time(millisecond) end}, integer}
-            ]).
-
-%%--------------------------------------------------------------------
-%% APIs
-%%--------------------------------------------------------------------
-
--spec(parse(clientinfo | message, list())
-     -> {error, any()}
-      | clientinfo()
-      | emqx_types:message()).
-parse(clientinfo, Params) ->
-    to_map(do_parsing(?UP_DATA_SCHEMA_CLIENTINFO, Params));
-
-parse(message, Params) ->
-    to_message(do_parsing(?UP_DATA_SCHEMA_MESSAGE, Params));
-
-parse(Type, _) ->
-    {error, {unkown_type, Type}}.
-
-%% @private
-to_map(Err = {error, _}) ->
-    Err;
-to_map(Ls) ->
-    maps:from_list(Ls).
-
-%% @private
-to_message(Err = {error, _}) -> 
-    Err;
-to_message(Ls) ->
-    #message{
-       id = get_value(id, Ls),
-       qos = get_value(qos, Ls),
-       from = get_value(from, Ls),
-       topic = get_value(topic, Ls),
-       payload = get_value(payload, Ls),
-       timestamp = get_value(timestamp, Ls)}.
-
--spec(serialize(Type, Struct)
-      -> {error, any()}
-       | [{atom(), any()}]
-    when Type :: conninfo | message,
-         Struct :: conninfo() | emqx_types:message()).
-serialize(conninfo, #{socktype := A1,
-                      peername := A2,
-                      sockname := A3,
-                      peercert := Peercert
-                     }) ->
-    [{socktype, A1},
-     {peername, A2},
-     {sockname, A3},
-     {peercert, do_serializing(peercert, Peercert)}];
-
-serialize(message, Msg) ->
-    [{id, emqx_message:id(Msg)},
-     {qos, emqx_message:qos(Msg)},
-     {from, emqx_message:from(Msg)},
-     {topic, emqx_message:topic(Msg)},
-     {payload, emqx_message:payload(Msg)},
-     {timestamp, emqx_message:timestamp(Msg)}];
-
-serialize(Type, _) ->
-    {error, {unkown_type, Type}}.
-
-%%--------------------------------------------------------------------
-%% Internal funcs
-%%--------------------------------------------------------------------
-
-do_parsing(Schema, Params) ->
-    try do_parsing(Schema, Params, [])
-    catch
-        throw:{badarg, Reason} -> {error, Reason}
-    end.
-do_parsing([], _Params, Acc) ->
-    lists:reverse(Acc);
-do_parsing([Indictor = {Key, _Optional, Type} | More], Params, Acc) ->
-    Value = case get_value(Key, Params) of
-                undefined -> do_generating(Indictor);
-                InParam -> do_typing(Key, InParam, Type)
-            end,
-    do_parsing(More, Params, [{Key, Value} | Acc]).
-
-%% @private 
-do_generating({Key, required, _}) ->
-    throw({badarg, errmsg("~s is required", [Key])});
-do_generating({_, optional, _}) ->
-    undefined;
-do_generating({_, {_, Generator}, _}) when is_function(Generator) ->
-    Generator();
-do_generating({_, {_, Default}, _}) ->
-    Default.
-
-%% @private 
-do_typing(Key, InParam, Types) when is_list(Types) ->
-    case length(lists:filter(fun(T) -> is_x_type(InParam, T) end, Types)) of
-        0 ->
-            throw({badarg, errmsg("~s: value ~p data type is not validate to ~p", [Key, InParam, Types])});
-        _ ->
-            InParam
-    end;
-do_typing(Key, InParam, Type) ->
-    do_typing(Key, InParam, [Type]).
-
-% @private
-is_x_type(P, atom) when is_atom(P) -> true;
-is_x_type(P, binary) when is_binary(P) -> true;
-is_x_type(P, integer) when is_integer(P) -> true;
-is_x_type(P, {enum, Ls}) ->
-    lists:member(P, Ls);
-is_x_type(_, _) -> false.
-
-do_serializing(peercert, nossl) ->
-    nossl;
-do_serializing(peercert, Peercert) ->
-    [{dn, esockd_peercert:subject(Peercert)},
-     {cn, esockd_peercert:common_name(Peercert)}].
-
-errmsg(Fmt, Args) ->
-    lists:flatten(io_lib:format(Fmt, Args)).
-

+ 283 - 41
apps/emqx_exproto/test/emqx_exproto_SUITE.erl

@@ -19,7 +19,20 @@
 -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}]).
@@ -37,48 +50,38 @@ groups() ->
 
 %% @private
 metrics() ->
-    [ list_to_atom(X ++ "_" ++ Y)
-      || X <- ["python3", "java"], Y <- ["tcp", "ssl", "udp", "dtls"]].
+    [tcp, ssl, udp, dtls].
 
-init_per_group(GrpName, Config) ->
-    [Lang, LisType] = [list_to_atom(X) || X <- string:tokens(atom_to_list(GrpName), "_")],
-    put(grpname, {Lang, LisType}),
+init_per_group(GrpName, Cfg) ->
+    put(grpname, GrpName),
+    Svrs = emqx_exproto_echo_svr:start(),
     emqx_ct_helpers:start_apps([emqx_exproto], fun set_sepecial_cfg/1),
-    [{driver_type, Lang},
-     {listener_type, LisType} | Config].
+    emqx_logger:set_log_level(debug),
+    [{servers, Svrs}, {listener_type, GrpName} | Cfg].
 
-end_per_group(_, _) ->
-    emqx_ct_helpers:stop_apps([emqx_exproto]).
+end_per_group(_, Cfg) ->
+    emqx_ct_helpers:stop_apps([emqx_exproto]),
+    emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)).
 
 set_sepecial_cfg(emqx_exproto) ->
-    {Lang, LisType} = get(grpname),
-    Path = emqx_ct_helpers:deps_path(emqx_exproto, "example/"),
+    LisType = get(grpname),
     Listeners = application:get_env(emqx_exproto, listeners, []),
-    Driver = compile(Lang, Path),
     SockOpts = socketopts(LisType),
     UpgradeOpts = fun(Opts) ->
-                      Opts1 = lists:keydelete(driver, 1, Opts),
-                      Opts2 = lists:keydelete(tcp_options, 1, Opts1),
+                      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),
-                      Driver ++ SockOpts ++ Opts5
+                      SockOpts ++ Opts5
                   end,
     NListeners = [{Proto, LisType, LisOn, UpgradeOpts(Opts)}
                   || {Proto, _Type, LisOn, Opts} <- Listeners],
     application:set_env(emqx_exproto, listeners, NListeners);
-set_sepecial_cfg(_App) ->
+set_sepecial_cfg(emqx) ->
+    application:set_env(emqx, allow_anonymous, true),
+    application:set_env(emqx, enable_acl_cache, false),
     ok.
 
-compile(java, Path) ->
-    ErlPortJar = emqx_ct_helpers:deps_path(erlport, "priv/java/_pkgs/erlport.jar"),
-    ct:pal(os:cmd(lists:concat(["cd ", Path, " && ",
-                                "rm -rf Main.class State.class && ",
-                                "javac -cp ", ErlPortJar, " Main.java"]))),
-    [{driver, [{type, java}, {path, Path}, {cbm, 'Main'}]}];
-compile(python3, Path) ->
-    [{driver, [{type, python3}, {path, Path}, {cbm, main}]}].
-
 %%--------------------------------------------------------------------
 %% Tests cases
 %%--------------------------------------------------------------------
@@ -86,24 +89,263 @@ compile(python3, Path) ->
 t_start_stop(_) ->
     ok.
 
-t_echo(Cfg) ->
+t_mountpoint_echo(Cfg) ->
     SockType = proplists:get_value(listener_type, Cfg),
-    Bin = rand_bytes(),
+    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/#">>, 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),
 
-    send(Sock, Bin),
+    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, check_acl, fun(_, _, _) -> deny end),
 
-    {ok, Bin} = recv(Sock, byte_size(Bin), 5000),
+    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),
 
-    %% pubsub echo
-    emqx:subscribe(<<"t/#">>),
     emqx:publish(emqx_message:make(<<"t/dn">>, <<"echo">>)),
-    First = receive {_, _, X} -> X#message.payload end,
-    First = receive {_, _, Y} -> Y#message.payload end,
 
+    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(),
+    HookFun1 = fun(_, _) -> Parent ! connected, ok end,
+    HookFun2 = fun(_, _, _) -> Parent ! disconnected, ok end,
+    emqx:hook('client.connected', HookFun1),
+    emqx:hook('client.disconnected', HookFun2),
+
+
+    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', HookFun1),
+    emqx:unhook('client.disconnected', HookFun2).
+
+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(),
+    HookFun1 = fun(_, _, _) -> Parent ! subscribed, ok end,
+    HookFun2 = fun(_, _, _) -> Parent ! unsubscribed, ok end,
+    emqx:hook('session.subscribed', HookFun1),
+    emqx:hook('session.unsubscribed', HookFun2),
+
+    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', HookFun1),
+    emqx:unhook('session.unsubscribed', HookFun2).
+
+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),
+
+    HookFun1 = fun(_, Msg) -> {ok, Msg#message{payload = <<"2">>}} end,
+    emqx:hook('message.delivered', HookFun1),
+
+    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', HookFun1).
+
 %%--------------------------------------------------------------------
 %% Utils
 
@@ -137,15 +379,15 @@ send({ssl, Sock}, Bin) ->
 send({dtls, Sock}, Bin) ->
     ssl:send(Sock, Bin).
 
-recv({tcp, Sock}, Size, Ts) ->
-    gen_tcp:recv(Sock, Size, Ts);
-recv({udp, Sock}, Size, Ts) ->
-    {ok, {_, _, Bin}} = gen_udp:recv(Sock, Size, Ts),
+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}, Size, Ts) ->
-    ssl:recv(Sock, Size, Ts);
-recv({dtls, Sock}, Size, Ts) ->
-    ssl:recv(Sock, Size, Ts).
+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);

+ 37 - 26
apps/emqx_exproto/test/emqx_exproto_echo_svr.erl

@@ -71,57 +71,68 @@
 %%--------------------------------------------------------------------
 
 start() ->
-    application:ensure_all_started(grpcbox),
+    application:ensure_all_started(grpc),
     [start_channel(), start_server()].
 
 start_channel() ->
-    grpcbox_channel_sup:start_child(ct_test_channel, [{http, "localhost", 9100, []}], #{}).
+    grpc_client_sup:create_channel_pool(ct_test_channel, "http://127.0.0.1:9100", #{}).
 
 start_server() ->
-    grpcbox:start_server(?HTTP).
+    Services = #{protos => [emqx_exproto_pb],
+                 services => #{'emqx.exproto.v1.ConnectionHandler' => ?MODULE}
+                },
+    Options = [],
+    grpc:start_server(?MODULE, 9001, Services, Options).
 
-stop([ChannPid, SvrPid]) ->
-    supervisor:terminate_child(grpcbox_channel_sup, ChannPid),
-    supervisor:terminate_child(grpcbox_services_simple_sup, SvrPid).
+stop([_ChannPid, _SvrPid]) ->
+    grpc:stop_server(?MODULE),
+    grpc_client_sup:stop_channel_pool(ct_test_channel).
 
 %%--------------------------------------------------------------------
 %% Protocol Adapter callbacks
 %%--------------------------------------------------------------------
 
--spec on_socket_created(ctx:ctx(), emqx_exproto_pb:created_socket_request()) ->
-    {ok, emqx_exproto_pb:empty_success(), ctx:ctx()} | grpcbox_stream:grpc_error_response().
-on_socket_created(Ctx, Req) ->
+-spec on_socket_created(emqx_exproto_pb:socket_created_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:empty_success(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+on_socket_created(Req, Md) ->
     io:format("~p: ~0p~n", [?FUNCTION_NAME, Req]),
-    {ok, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_received_bytes(ctx:ctx(), emqx_exproto_pb:received_bytes_request()) ->
-    {ok, emqx_exproto_pb:empty_success(), ctx:ctx()} | grpcbox_stream:grpc_error_response().
-on_received_bytes(Ctx, Req = #{conn := Conn, bytes := Bytes}) ->
+-spec on_socket_closed(emqx_exproto_pb:socket_closed_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:empty_success(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+on_socket_closed(Req, Md) ->
     io:format("~p: ~0p~n", [?FUNCTION_NAME, Req]),
-    #{<<"type">> := Type} = Params = emqx_json:decode(Bytes, [return_maps]),
-    _ = handle_in(Conn, Type, Params),
-    {ok, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_socket_closed(ctx:ctx(), emqx_exproto_pb:socket_closed_request()) ->
-    {ok, emqx_exproto_pb:empty_success(), ctx:ctx()} | grpcbox_stream:grpc_error_response().
-on_socket_closed(Ctx, Req) ->
+-spec on_received_bytes(emqx_exproto_pb:received_bytes_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:empty_success(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+on_received_bytes(Req = #{conn := Conn, bytes := Bytes}, Md) ->
     io:format("~p: ~0p~n", [?FUNCTION_NAME, Req]),
-    {ok, #{}, Ctx}.
+    #{<<"type">> := Type} = Params = emqx_json:decode(Bytes, [return_maps]),
+    _ = handle_in(Conn, Type, Params),
+    {ok, #{}, Md}.
 
-on_timer_timeout(Ctx, Req = #{conn := Conn, type := 'KEEPALIVE'}) ->
+-spec on_timer_timeout(emqx_exproto_pb:timer_timeout_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:empty_success(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+on_timer_timeout(Req = #{conn := Conn, type := 'KEEPALIVE'}, Md) ->
     io:format("~p: ~0p~n", [?FUNCTION_NAME, Req]),
     handle_out(Conn, ?TYPE_DISCONNECT),
     ?close(#{conn => Conn}),
-    {ok, #{}, Ctx}.
+    {ok, #{}, Md}.
 
--spec on_received_messages(ctx:ctx(), emqx_exproto_pb:received_messages_request()) ->
-    {ok, emqx_exproto_pb:empty_success(), ctx:ctx()} | grpcbox_stream:grpc_error_response().
-on_received_messages(Ctx, Req = #{conn := Conn, messages := Messages}) ->
+-spec on_received_messages(emqx_exproto_pb:received_messages_request(), grpc:metadata())
+    -> {ok, emqx_exproto_pb:empty_success(), grpc:metadata()}
+     | {error, grpc_cowboy_h:error_response()}.
+on_received_messages(Req = #{conn := Conn, messages := Messages}, Md) ->
     io:format("~p: ~0p~n", [?FUNCTION_NAME, Req]),
     lists:foreach(fun(Message) ->
         handle_out(Conn, ?TYPE_PUBLISH, Message)
     end, Messages),
-    {ok, #{}, Ctx}.
+    {ok, #{}, Md}.
 
 %%--------------------------------------------------------------------
 %% The Protocol Example:

+ 1 - 1
apps/emqx_retainer/rebar.config

@@ -19,6 +19,6 @@
  [{test,
    [{deps,
      [{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "1.2.2"}}},
-      {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.0"}}}]}
+      {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3"}}}]}
    ]}
  ]}.

+ 4 - 1
apps/emqx_retainer/src/emqx_retainer.erl

@@ -37,6 +37,9 @@
 
 -export([clean/1]).
 
+%% for emqx_pool task func
+-export([dispatch/2]).
+
 %% gen_server callbacks
 -export([ init/1
         , handle_call/3
@@ -64,7 +67,7 @@ on_session_subscribed(_, _, #{share := ShareName}) when ShareName =/= undefined
     ok;
 on_session_subscribed(_, Topic, #{rh := Rh, is_new := IsNew}) ->
     case Rh =:= 0 orelse (Rh =:= 1 andalso IsNew) of
-        true -> emqx_pool:async_submit(fun dispatch/2, [self(), Topic]);
+        true -> emqx_pool:async_submit(fun ?MODULE:dispatch/2, [self(), Topic]);
         _ -> ok
     end.
 

+ 11 - 0
apps/emqx_rule_engine/include/rule_actions.hrl

@@ -0,0 +1,11 @@
+-compile({parse_transform, emqx_rule_actions_trans}).
+
+-type selected_data() :: map().
+-type env_vars() :: map().
+-type bindings() :: list(#{atom() => term()}).
+
+-define(BINDING_KEYS, '__bindings__').
+
+-define(bound_v(Key, ENVS0),
+    maps:get(Key,
+        maps:get(?BINDING_KEYS, ENVS0, #{}))).

+ 2 - 1
apps/emqx_rule_engine/include/rule_engine.hrl

@@ -111,7 +111,8 @@
 -record(action_instance_params,
         { id :: action_instance_id()
         , params :: #{} %% the params got after initializing the action
-        , apply :: fun((Data::map(), Envs::map()) -> any()) %% the func got after initializing the action
+        , apply :: fun((Data::map(), Envs::map()) -> any())
+                 | {M::module(), F::atom(), Args::list()} %% the func got after initializing the action
         }).
 
 %% Arithmetic operators

+ 106 - 64
apps/emqx_rule_engine/src/emqx_rule_actions.erl

@@ -18,6 +18,7 @@
 -module(emqx_rule_actions).
 
 -include("rule_engine.hrl").
+-include("rule_actions.hrl").
 -include_lib("emqx/include/emqx.hrl").
 -include_lib("emqx/include/logger.hrl").
 
@@ -47,7 +48,7 @@
                 order => 3,
                 type => string,
                 input => textarea,
-                required => true,
+                required => false,
                 default => <<"${payload}">>,
                 title => #{en => <<"Payload Template">>,
                            zh => <<"消息内容模板"/utf8>>},
@@ -84,7 +85,7 @@
                category => debug,
                for => '$any',
                types => [],
-               create => on_action_do_nothing,
+               create => on_action_create_do_nothing,
                params => #{},
                title => #{en => <<"Do Nothing (debug)">>,
                           zh => <<"空动作 (调试)"/utf8>>},
@@ -92,79 +93,120 @@
                                 zh => <<"此动作什么都不做,并且不会失败 (用以调试)"/utf8>>}
               }).
 
--type(action_fun() :: fun((SelectedData::map(), Envs::map()) -> Result::any())).
-
--export_type([action_fun/0]).
-
 -export([on_resource_create/2]).
 
+%% callbacks for rule engine
 -export([ on_action_create_inspect/2
         , on_action_create_republish/2
+        , on_action_create_do_nothing/2
+        ]).
+
+-export([ on_action_inspect/2
+        , on_action_republish/2
         , on_action_do_nothing/2
         ]).
 
-%%------------------------------------------------------------------------------
-%% Default actions for the Rule Engine
-%%------------------------------------------------------------------------------
 
 -spec(on_resource_create(binary(), map()) -> map()).
 on_resource_create(_Name, Conf) ->
     Conf.
 
--spec(on_action_create_inspect(action_instance_id(), Params :: map()) -> action_fun()).
-on_action_create_inspect(_Id, Params) ->
-    fun(Selected, Envs) ->
-        io:format("[inspect]~n"
-                  "\tSelected Data: ~p~n"
-                  "\tEnvs: ~p~n"
-                  "\tAction Init Params: ~p~n", [Selected, Envs, Params])
-    end.
-
-%% A Demo Action.
--spec(on_action_create_republish(action_instance_id(), #{binary() := emqx_topic:topic()})
-      -> action_fun()).
-on_action_create_republish(Id, #{<<"target_topic">> := TargetTopic, <<"target_qos">> := TargetQoS, <<"payload_tmpl">> := PayloadTmpl}) ->
+%%------------------------------------------------------------------------------
+%% Action 'inspect'
+%%------------------------------------------------------------------------------
+-spec on_action_create_inspect(action_instance_id(), Params :: map())
+    -> NewParams :: map().
+on_action_create_inspect(Id, Params) ->
+    Params.
+
+-spec on_action_inspect(selected_data(), env_vars()) -> any().
+on_action_inspect(Selected, Envs) ->
+    io:format("[inspect]~n"
+              "\tSelected Data: ~p~n"
+              "\tEnvs: ~p~n"
+              "\tAction Init Params: ~p~n", [Selected, Envs, ?bound_v('Params', Envs)]),
+    emqx_rule_metrics:inc_actions_success(?bound_v('Id', Envs)).
+
+
+%%------------------------------------------------------------------------------
+%% Action 'republish'
+%%------------------------------------------------------------------------------
+-spec on_action_create_republish(action_instance_id(), Params :: map())
+      -> NewParams :: map().
+on_action_create_republish(Id, Params = #{
+        <<"target_topic">> := TargetTopic,
+        <<"target_qos">> := TargetQoS,
+        <<"payload_tmpl">> := PayloadTmpl
+       }) ->
     TopicTks = emqx_rule_utils:preproc_tmpl(TargetTopic),
     PayloadTks = emqx_rule_utils:preproc_tmpl(PayloadTmpl),
-    fun (_Selected, Envs = #{headers := #{republish_by := ActId},
-                             topic := Topic}) when ActId =:= Id ->
-            ?LOG(error, "[republish] recursively republish detected, msg topic: ~p, target topic: ~p",
-                 [Topic, TargetTopic]),
-            error({recursive_republish, Envs});
-        (Selected, _Envs = #{qos := QoS, flags := Flags, timestamp := Timestamp}) ->
-            ?LOG(debug, "[republish] republish to: ~p, Payload: ~p",
-                [TargetTopic, Selected]),
-            increase_and_publish(
-              #message{
-                id = emqx_guid:gen(),
-                qos = if TargetQoS =:= -1 -> QoS; true -> TargetQoS end,
-                from = Id,
-                flags = Flags,
-                headers = #{republish_by => Id},
-                topic = emqx_rule_utils:proc_tmpl(TopicTks, Selected),
-                payload = emqx_rule_utils:proc_tmpl(PayloadTks, Selected),
-                timestamp = Timestamp
-              });
-        %% in case this is not a "message.publish" request
-        (Selected, _Envs) ->
-            ?LOG(debug, "[republish] republish to: ~p, Payload: ~p",
-                [TargetTopic, Selected]),
-            increase_and_publish(
-              #message{
-                 id = emqx_guid:gen(),
-                 qos = if TargetQoS =:= -1 -> 0; true -> TargetQoS end,
-                 from = Id,
-                 flags = #{dup => false, retain => false},
-                 headers = #{republish_by => Id},
-                 topic = emqx_rule_utils:proc_tmpl(TopicTks, Selected),
-                 payload = emqx_rule_utils:proc_tmpl(PayloadTks, Selected),
-                 timestamp = erlang:system_time(millisecond)
-              })
-    end.
-
-increase_and_publish(Msg) ->
-    emqx_metrics:inc_msg(Msg),
-    emqx_broker:safe_publish(Msg).
-
-on_action_do_nothing(_, _) ->
-    fun(_, _) -> ok end.
+    Params.
+
+-spec on_action_republish(selected_data(), env_vars()) -> any().
+on_action_republish(_Selected, Envs = #{
+            topic := Topic,
+            headers := #{republish_by := ActId},
+            ?BINDING_KEYS := #{'Id' := ActId}
+        }) ->
+    ?LOG(error, "[republish] recursively republish detected, msg topic: ~p, target topic: ~p",
+        [Topic, ?bound_v('TargetTopic', Envs)]),
+    emqx_rule_metrics:inc_actions_error(?bound_v('Id', Envs));
+
+on_action_republish(Selected, _Envs = #{
+            qos := QoS, flags := Flags, timestamp := Timestamp,
+            ?BINDING_KEYS := #{
+                'Id' := ActId,
+                'TargetTopic' := TargetTopic,
+                'TargetQoS' := TargetQoS,
+                'TopicTks' := TopicTks,
+                'PayloadTks' := PayloadTks
+            }}) ->
+    ?LOG(debug, "[republish] republish to: ~p, Payload: ~p",
+        [TargetTopic, Selected]),
+    increase_and_publish(ActId,
+        #message{
+            id = emqx_guid:gen(),
+            qos = if TargetQoS =:= -1 -> QoS; true -> TargetQoS end,
+            from = ActId,
+            flags = Flags,
+            headers = #{republish_by => ActId},
+            topic = emqx_rule_utils:proc_tmpl(TopicTks, Selected),
+            payload = emqx_rule_utils:proc_tmpl(PayloadTks, Selected),
+            timestamp = Timestamp
+        });
+
+%% in case this is not a "message.publish" request
+on_action_republish(Selected, _Envs = #{
+            ?BINDING_KEYS := #{
+                'Id' := ActId,
+                'TargetTopic' := TargetTopic,
+                'TargetQoS' := TargetQoS,
+                'TopicTks' := TopicTks,
+                'PayloadTks' := PayloadTks
+            }}) ->
+    ?LOG(debug, "[republish] republish to: ~p, Payload: ~p",
+        [TargetTopic, Selected]),
+    increase_and_publish(ActId,
+        #message{
+            id = emqx_guid:gen(),
+            qos = if TargetQoS =:= -1 -> 0; true -> TargetQoS end,
+            from = ActId,
+            flags = #{dup => false, retain => false},
+            headers = #{republish_by => ActId},
+            topic = emqx_rule_utils:proc_tmpl(TopicTks, Selected),
+            payload = emqx_rule_utils:proc_tmpl(PayloadTks, Selected),
+            timestamp = erlang:system_time(millisecond)
+        }).
+
+increase_and_publish(ActId, Msg) ->
+    emqx_broker:safe_publish(Msg),
+    emqx_rule_metrics:inc_actions_success(ActId),
+    emqx_metrics:inc_msg(Msg).
+
+-spec on_action_create_do_nothing(action_instance_id(), Params :: map())
+      -> NewParams :: map().
+on_action_create_do_nothing(ActId, Params) when is_binary(ActId) ->
+    Params.
+
+on_action_do_nothing(Selected, Envs) when is_map(Selected) ->
+    emqx_rule_metrics:inc_actions_success(?bound_v('ActId', Envs)).

+ 70 - 0
apps/emqx_rule_engine/src/emqx_rule_actions_trans.erl

@@ -0,0 +1,70 @@
+-module(emqx_rule_actions_trans).
+
+-include_lib("syntax_tools/include/merl.hrl").
+
+-export([parse_transform/2]).
+
+parse_transform(Forms, _Options) ->
+  trans(Forms, []).
+
+trans([], ResAST) ->
+  lists:reverse(ResAST);
+trans([{eof, L} | AST], ResAST) ->
+  lists:reverse([{eof, L} | ResAST]) ++ AST;
+trans([{function, LineNo, FuncName, Arity, Clauses} | AST], ResAST) ->
+  NewClauses = trans_func_clauses(atom_to_list(FuncName), Clauses),
+  trans(AST, [{function, LineNo, FuncName, Arity, NewClauses} | ResAST]);
+trans([Form | AST], ResAST) ->
+  trans(AST, [Form | ResAST]).
+
+trans_func_clauses("on_action_create_" ++ _ = _FuncName , Clauses) ->
+  %io:format("~n[[transing function: ~p]]~n", [_FuncName]),
+  %io:format("~n-----old clauses:~n", []), merl:print(Clauses),
+  NewClauses = [
+    begin
+      Bindings = lists:flatten(get_vars(Args) ++ get_vars(Body, lefth)),
+      Body2 = append_to_result(Bindings, Body),
+      {clause, LineNo, Args, Guards, Body2}
+    end || {clause, LineNo, Args, Guards, Body} <- Clauses],
+  %io:format("~n-----new clauses: ~n"), merl:print(NewClauses),
+  NewClauses;
+trans_func_clauses(_FuncName, Clauses) ->
+  %io:format("~n[[discarding function: ~p]]~n", [_FuncName]),
+  Clauses.
+
+get_vars(Exprs) ->
+  get_vars(Exprs, all).
+get_vars(Exprs, Type) ->
+  do_get_vars(Exprs, [], Type).
+
+do_get_vars([], Vars, _Type) -> Vars;
+do_get_vars([Line | Expr], Vars, all) ->
+  do_get_vars(Expr, [syntax_vars(erl_syntax:form_list([Line])) | Vars], all);
+do_get_vars([Line | Expr], Vars, lefth) ->
+  do_get_vars(Expr,
+    case (Line) of
+      ?Q("_@LeftV = _@@_") -> Vars ++ syntax_vars(LeftV);
+      _ -> Vars
+    end, lefth).
+
+syntax_vars(Line) ->
+  sets:to_list(erl_syntax_lib:variables(Line)).
+
+%% append bindings to the return value as the first tuple element.
+%% e.g. if the original result is R, then the new result will be {[binding()], R}.
+append_to_result(Bindings, Exprs) ->
+  erl_syntax:revert_forms(do_append_to_result(to_keyword(Bindings), Exprs, [])).
+
+do_append_to_result(KeyWordVars, [Line], Res) ->
+  case Line of
+    ?Q("_@LeftV = _@RightV") ->
+      lists:reverse([?Q("{[_@KeyWordVars], _@LeftV}"), Line | Res]);
+    _ ->
+      lists:reverse([?Q("{[_@KeyWordVars], _@Line}") | Res])
+  end;
+do_append_to_result(KeyWordVars, [Line | Exprs], Res) ->
+  do_append_to_result(KeyWordVars, Exprs, [Line | Res]).
+
+to_keyword(Vars) ->
+  [erl_syntax:tuple([erl_syntax:atom(Var), merl:var(Var)])
+   || Var <- Vars].

+ 8 - 0
apps/emqx_rule_engine/src/emqx_rule_engine.appup.src

@@ -0,0 +1,8 @@
+{VSN,
+  [
+    {<<".*">>, []}
+  ],
+  [
+    {<<".*">>, []}
+  ]
+}.

+ 31 - 17
apps/emqx_rule_engine/src/emqx_rule_engine.erl

@@ -20,10 +20,9 @@
 -include_lib("emqx/include/logger.hrl").
 
 -export([ load_providers/0
-        , load_provider/1
         , unload_providers/0
-        , unload_provider/1
         , refresh_resources/0
+        , refresh_resource/1
         , refresh_rule/1
         , refresh_rules/0
         , refresh_actions/1
@@ -174,6 +173,7 @@ create_rule(Params = #{rawsql := Sql, actions := Actions}) ->
                          enabled = Enabled,
                          description = maps:get(description, Params, "")},
             ok = emqx_rule_registry:add_rule(Rule),
+            ok = emqx_rule_metrics:create_rule_metrics(RuleId),
             {ok, Rule};
         Error -> error(Error)
     end.
@@ -198,7 +198,7 @@ delete_rule(RuleId) ->
                 ok = emqx_rule_registry:remove_rule(Rule)
             catch
                 Error:Reason:ST ->
-                    ?LOG(error, "clear_rule rule failed: ~p", [{Error, Reason, ST}]),
+                    ?LOG(error, "clear_rule ~p failed: ~p", [RuleId, {Error, Reason, ST}]),
                     refresh_actions(Actions, fun(_) -> true end)
             end;
         not_found ->
@@ -300,6 +300,9 @@ refresh_resources() ->
             <- emqx_rule_registry:get_resources()],
     ok.
 
+refresh_resource(Type) when is_atom(Type) ->
+    [refresh_resource(Resource)
+     || Resource <- emqx_rule_registry:get_resources_by_type(Type)];
 refresh_resource(#resource{id = ResId, config = Config, type = Type}) ->
     {ok, #resource_type{on_create = {M, F}}} = emqx_rule_registry:find_resource_type(Type),
     cluster_call(init_resource, [M, F, ResId, Config]).
@@ -317,7 +320,8 @@ refresh_rules() ->
             <- emqx_rule_registry:get_rules()],
     ok.
 
-refresh_rule(#rule{actions = Actions}) ->
+refresh_rule(#rule{id = RuleId, actions = Actions}) ->
+    ok = emqx_rule_metrics:create_rule_metrics(RuleId),
     refresh_actions(Actions, fun(_) -> true end).
 
 -spec(refresh_resource_status() -> ok).
@@ -436,14 +440,20 @@ cluster_call(Func, Args) ->
 init_resource(Module, OnCreate, ResId, Config) ->
     Params = ?RAISE(Module:OnCreate(ResId, Config),
                     {{init_resource_failure, node()}, {{Module, OnCreate}, {_REASON_,_ST_}}}),
-    emqx_rule_registry:add_resource_params(#resource_params{id = ResId, params = Params}).
+    emqx_rule_registry:add_resource_params(#resource_params{id = ResId, params = Params, status = #{is_alive => true}}).
 
 init_action(Module, OnCreate, ActionInstId, Params) ->
+    ok = emqx_rule_metrics:create_metrics(ActionInstId),
     case ?RAISE(Module:OnCreate(ActionInstId, Params), {{init_action_failure, node()}, {{Module,OnCreate},{_REASON_,_ST_}}}) of
-        {Apply, NewParams} ->
+        {Apply, NewParams} when is_function(Apply) -> %% BACKW: =< e4.2.2
             ok = emqx_rule_registry:add_action_instance_params(
                 #action_instance_params{id = ActionInstId, params = NewParams, apply = Apply});
-        Apply ->
+        {Bindings, NewParams} when is_list(Bindings) ->
+            ok = emqx_rule_registry:add_action_instance_params(
+            #action_instance_params{
+                id = ActionInstId, params = NewParams,
+                apply = #{mod => Module, bindings => maps:from_list(Bindings)}});
+        Apply when is_function(Apply) -> %% BACKW: =< e4.2.2
             ok = emqx_rule_registry:add_action_instance_params(
                 #action_instance_params{id = ActionInstId, params = Params, apply = Apply})
     end.
@@ -462,7 +472,7 @@ clear_resource(Module, Destroy, ResId) ->
 
 clear_rule(#rule{id = RuleId, actions = Actions}) ->
     clear_actions(Actions),
-    emqx_rule_metrics:clear(RuleId),
+    emqx_rule_metrics:clear_rule_metrics(RuleId),
     ok.
 
 clear_actions(Actions) ->
@@ -474,17 +484,21 @@ clear_actions(Actions) ->
         end, Actions).
 
 clear_action(_Module, undefined, ActionInstId) ->
-    emqx_rule_metrics:clear(ActionInstId),
+    emqx_rule_metrics:clear_metrics(ActionInstId),
     ok = emqx_rule_registry:remove_action_instance_params(ActionInstId);
 clear_action(Module, Destroy, ActionInstId) ->
-    emqx_rule_metrics:clear(ActionInstId),
-    case emqx_rule_registry:get_action_instance_params(ActionInstId) of
-        {ok, #action_instance_params{params = Params}} ->
-            ?RAISE(Module:Destroy(ActionInstId, Params),{{destroy_action_failure, node()},
-                                           {{Module, Destroy}, {_REASON_,_ST_}}}),
-            ok = emqx_rule_registry:remove_action_instance_params(ActionInstId);
-        not_found ->
-            ok
+    case erlang:function_exported(Module, Destroy, 2) of
+        true ->
+            emqx_rule_metrics:clear_metrics(ActionInstId),
+            case emqx_rule_registry:get_action_instance_params(ActionInstId) of
+                {ok, #action_instance_params{params = Params}} ->
+                    ?RAISE(Module:Destroy(ActionInstId, Params),{{destroy_action_failure, node()},
+                                                {{Module, Destroy}, {_REASON_,_ST_}}}),
+                    ok = emqx_rule_registry:remove_action_instance_params(ActionInstId);
+                not_found ->
+                    ok
+            end;
+        false -> ok
     end.
 
 fetch_resource_status(Module, OnStatus, ResId) ->

+ 14 - 4
apps/emqx_rule_engine/src/emqx_rule_engine_api.erl

@@ -297,7 +297,17 @@ do_create_resource(Create, Params) ->
     end.
 
 list_resources(#{}, _Params) ->
-    return_all(emqx_rule_registry:get_resources()).
+    Data0 = lists:foldr(fun maybe_record_to_map/2, [], emqx_rule_registry:get_resources()),
+    Data = lists:map(fun(Res = #{id := Id}) ->
+               Status = lists:all(fun(Node) ->
+                            case emqx_rpc:call(Node, emqx_rule_registry, find_resource_params, [Id]) of
+                                {ok, #resource_params{status = #{is_alive := true}}} -> true;
+                                _ -> false
+                            end
+                        end, ekka_mnesia:running_nodes()),
+               maps:put(status, Status, Res)
+           end, Data0),
+    return({ok, Data}).
 
 list_resources_by_type(#{type := Type}, _Params) ->
     return_all(emqx_rule_registry:get_resources_by_type(Type)).
@@ -309,7 +319,7 @@ show_resource(#{id := Id}, _Params) ->
                 [begin
                     {ok, St} = rpc:call(Node, emqx_rule_engine, get_resource_status, [Id]),
                     maps:put(node, Node, St)
-                end || Node <- [node()| nodes()]],
+                end || Node <- ekka_mnesia:running_nodes()],
             return({ok, maps:put(status, Status, record_to_map(R))});
         not_found ->
             return({error, 404, <<"Not Found">>})
@@ -538,8 +548,8 @@ sort_by(Pos, TplList) ->
 
 get_rule_metrics(Id) ->
     [maps:put(node, Node, rpc:call(Node, emqx_rule_metrics, get_rule_metrics, [Id]))
-     || Node <- [node()| nodes()]].
+     || Node <- ekka_mnesia:running_nodes()].
 
 get_action_metrics(Id) ->
     [maps:put(node, Node, rpc:call(Node, emqx_rule_metrics, get_action_metrics, [Id]))
-     || Node <- [node()| nodes()]].
+     || Node <- ekka_mnesia:running_nodes()].

+ 1 - 0
apps/emqx_rule_engine/src/emqx_rule_engine_app.erl

@@ -28,6 +28,7 @@
 
 start(_Type, _Args) ->
     {ok, Sup} = emqx_rule_engine_sup:start_link(),
+    _ = emqx_rule_engine_sup:start_locker(),
     ok = emqx_rule_engine:load_providers(),
     ok = emqx_rule_engine:refresh_resources(),
     ok = emqx_rule_engine:refresh_rules(),

+ 0 - 0
apps/emqx_rule_engine/src/emqx_rule_engine_sup.erl


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.