Merge branch 'umbrella-for-430-auto-sync' into umbrella-for-430
This commit is contained in:
commit
7fdbfba06a
|
@ -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,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',
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,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);
|
||||
|
|
|
@ -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)}.
|
||||
{ok, RequestTimeout} = application:get_env(?APP, request_timeout),
|
||||
#http_request{method = Method, path = Path, headers = NewHeaders, params = Params, request_timeout = RequestTimeout}.
|
||||
|
||||
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;
|
||||
_ -> []
|
||||
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.
|
|
@ -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(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(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(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)).
|
||||
|
||||
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_(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.
|
||||
|
||||
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) ->
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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),
|
||||
ServerAddr = http_server(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"},
|
||||
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
|
||||
|
|
|
@ -25,3 +25,4 @@ rebar3.crashdump
|
|||
etc/emqx_auth_jwt.conf.rendered
|
||||
.rebar3/
|
||||
*.swp
|
||||
Mnesia.nonode@nohost/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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}]}.
|
||||
|
|
|
@ -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"]},
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
%% -*-: erlang -*-
|
||||
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -28,42 +28,55 @@
|
|||
|
||||
-define(APP, emqx_auth_jwt).
|
||||
|
||||
-define(JWT_ACTION, {emqx_auth_jwt, check, [auth_env()]}).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
ok = emqx_auth_jwt:register_metrics(),
|
||||
emqx:hook('client.authenticate', ?JWT_ACTION),
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
{ok, Sup} = supervisor:start_link({local, ?MODULE}, ?MODULE, []),
|
||||
|
||||
stop(_State) ->
|
||||
emqx:unhook('client.authenticate', ?JWT_ACTION).
|
||||
{ok, Pid} = start_auth_server(jwks_svr_options()),
|
||||
ok = emqx_auth_jwt:register_metrics(),
|
||||
|
||||
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).
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -21,3 +21,4 @@
|
|||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -0,0 +1,9 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -22,3 +22,5 @@ erlang.mk
|
|||
.rebar3/
|
||||
*.swp
|
||||
rebar.lock
|
||||
/.idea/
|
||||
.DS_Store
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
%% -*-: erlang -*-
|
||||
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -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)]
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
%% -*-: erlang -*-
|
||||
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<"*.">>, []}
|
||||
]
|
||||
}.
|
|
@ -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) ->
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)};
|
||||
|
|
|
@ -22,6 +22,12 @@
|
|||
, stop/1
|
||||
]).
|
||||
|
||||
-export([ start_listener/1
|
||||
, start_listener/3
|
||||
, stop_listener/1
|
||||
, stop_listener/2
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
## Usage
|
||||
|
||||
### Python
|
||||
### gRPC service
|
||||
|
||||
***Requirements:***
|
||||
See: `priv/protos/exhook.proto`
|
||||
|
||||
- It requires the emqx hosted machine has Python3 Runtimes (not support python2)
|
||||
- The `python3` executable commands in your shell
|
||||
### CLI
|
||||
|
||||
***Examples:***
|
||||
## Example
|
||||
|
||||
See `test/scripts/main.py`
|
||||
## Recommended gRPC Framework
|
||||
|
||||
### Java
|
||||
See: https://github.com/grpc-ecosystem/awesome-grpc
|
||||
|
||||
***Requirements:***
|
||||
## Thanks
|
||||
|
||||
- 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
|
||||
|
||||
| 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 |
|
||||
|
||||
## SDK
|
||||
|
||||
See `sdk/README.md`
|
||||
|
||||
## Known Issues or TODOs
|
||||
|
||||
**Configurable Log System**
|
||||
|
||||
- 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.
|
||||
|
||||
## Reference
|
||||
|
||||
- [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)
|
||||
|
|
|
@ -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 服务发送钩子的请求,并处理其返回的值。
|
||||
|
||||
|
||||
和 emqx 原生的钩子一致,emqx-exhook 也支持链式的方式计算和返回:
|
||||
|
||||
<img src="https://docs.emqx.net/broker/latest/cn/advanced/assets/chain_of_responsiblity.png" style="zoom:50%;" />
|
||||
|
||||
### gRPC 服务示例
|
||||
|
||||
用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中。例如,其支持的接口有:
|
||||
|
||||
```protobuff
|
||||
syntax = "proto3";
|
||||
|
||||
package emqx.exhook.v1;
|
||||
|
||||
service HookProvider {
|
||||
|
||||
rpc OnProviderLoaded(ProviderLoadedRequest) returns (LoadedResponse) {};
|
||||
|
||||
rpc OnProviderUnloaded(ProviderUnloadedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnClientConnect(ClientConnectRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnClientConnack(ClientConnackRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnClientConnected(ClientConnectedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnClientDisconnected(ClientDisconnectedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnClientAuthenticate(ClientAuthenticateRequest) returns (ValuedResponse) {};
|
||||
|
||||
rpc OnClientCheckAcl(ClientCheckAclRequest) returns (ValuedResponse) {};
|
||||
|
||||
rpc OnClientSubscribe(ClientSubscribeRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnClientUnsubscribe(ClientUnsubscribeRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionCreated(SessionCreatedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionSubscribed(SessionSubscribedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionUnsubscribed(SessionUnsubscribedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionResumed(SessionResumedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionDiscarded(SessionDiscardedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionTakeovered(SessionTakeoveredRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSessionTerminated(SessionTerminatedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnMessagePublish(MessagePublishRequest) returns (ValuedResponse) {};
|
||||
|
||||
rpc OnMessageDelivered(MessageDeliveredRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnMessageDropped(MessageDroppedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnMessageAcked(MessageAckedRequest) returns (EmptySuccess) {};
|
||||
}
|
||||
```
|
||||
|
||||
### 配置文件示例
|
||||
|
||||
#### 驱动 配置
|
||||
|
||||
```properties
|
||||
## Driver type
|
||||
```
|
||||
## 配置 gRPC 服务地址 (HTTP)
|
||||
##
|
||||
## Exmaples:
|
||||
## - python3 --- 仅配置 python3
|
||||
## - python3, java, webhook --- 配置多个 Driver
|
||||
exhook.dirvers = python3, java, webhook
|
||||
## s1 为服务器的名称
|
||||
exhook.server.s1.url = http://127.0.0.1:9001
|
||||
|
||||
## --- 具体 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
|
||||
...
|
||||
```
|
||||
|
||||
#### 钩子配置
|
||||
|
||||
钩子支持配置在配置文件中,例如:
|
||||
|
||||
```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/#"]}
|
||||
```
|
||||
|
||||
***已废弃!!(冗余)***
|
||||
|
||||
|
||||
### 驱动抽象
|
||||
|
||||
#### APIs
|
||||
|
||||
| 方法名 | 说明 | 入参 | 返回 |
|
||||
| ------------------------ | -------- | ------ | ------ |
|
||||
| `init` | 初始化 | - | 见下表 |
|
||||
| `deinit` | 销毁 | - | - |
|
||||
| `xxx `*(由init函数定义)* | 钩子回调 | 见下表 | 见下表 |
|
||||
|
||||
|
||||
|
||||
##### init 函数规格
|
||||
|
||||
```erlang
|
||||
%% init 函数
|
||||
%% HookSpec : 为用户在脚本中的 初始化函数指定的;他会与配置文件中的内容作为默认值,进行合并
|
||||
%% 该参数的目的,用于 EMQ X 判断需要执行哪些 Hook 和 如何执行 Hook
|
||||
%% State : 为用户自己管理的数据内容,EMQ X 不关心它,只来回透传
|
||||
init() -> {HookSpec, State}.
|
||||
|
||||
%% 例如:
|
||||
{[{client_connect, callback_m(), callback_f(),#{}, {}}]}
|
||||
|
||||
%%--------------------------------------------------------------
|
||||
%% Type Defines
|
||||
|
||||
-tpye hook_spec() :: [{hookname(), callback_m(), callback_f(), hook_opts()}].
|
||||
|
||||
-tpye state :: any().
|
||||
|
||||
-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.
|
||||
|
||||
-type callback_m() :: atom(). -- 回调的模块名称;python 为脚本文件名称;java 为类名;webhook 为 URI 地址
|
||||
|
||||
-type callback_f() :: atom(). -- 回调的方法名称;python,java 等为方法名;webhook 为资源地址
|
||||
|
||||
-tpye hook_opts() :: [{hook_key(), any()}]. -- 配置项;配置该项钩子的行为
|
||||
|
||||
-type hook_key() :: topics | ...
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### deinit 函数规格
|
||||
|
||||
``` erlang
|
||||
%% deinit 函数;不关心返回的任何内容
|
||||
deinit() -> any().
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 回调函数规格
|
||||
|
||||
| 钩子 | 入参 | 返回 |
|
||||
| -------------------- | ----------------------------------------------------- | --------- |
|
||||
| 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.
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 统计
|
||||
|
||||
在驱动运行过程中,应有对每种钩子调用计数,例如:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
./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
|
||||
```
|
||||
|
|
|
@ -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 规范、和对应语言的开发手册
|
||||
|
|
@ -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 编写了其回调的代码模块。
|
|
@ -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
|
|
@ -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
|
|
@ -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}.
|
|
@ -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"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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>"]},
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -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.
|
||||
|
|
|
@ -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},
|
||||
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).
|
||||
|
||||
supervisor:start_child(?MODULE, Spec).
|
||||
|
||||
-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.
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
@ -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()).
|
|
@ -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.
|
||||
|
|
@ -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).
|
|
@ -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).
|
|
@ -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.
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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}.
|
||||
|
|
@ -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) ->
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
## Drivers
|
||||
## Usage
|
||||
|
||||
### Python
|
||||
### gRPC service
|
||||
|
||||
***Requirements:***
|
||||
See: `priv/protos/exproto.proto`
|
||||
|
||||
- It requires the emqx hosted machine has Python3 Runtimes
|
||||
- An executable commands in your shell, i,g: `python3` or `python`
|
||||
## Example
|
||||
|
||||
***Examples:***
|
||||
## Recommended gRPC Framework
|
||||
|
||||
See [example/main.python](https://github.com/emqx/emqx-exproto/blob/master/example/main.py)
|
||||
See: https://github.com/grpc-ecosystem/awesome-grpc
|
||||
|
||||
### Java
|
||||
## Thanks
|
||||
|
||||
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)
|
||||
|
||||
|
||||
## Benchmark
|
||||
|
||||
***Work in progress...***
|
||||
|
||||
|
||||
## Known Issues or TODOs
|
||||
|
||||
- Configurable Log System.
|
||||
* The Java driver can not redirect the `stderr` stream to erlang vm on Windows platform
|
||||
|
||||
## Reference
|
||||
|
||||
- [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)
|
||||
|
|
|
@ -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 黑名单 等
|
||||
|
||||
## 架构
|
||||
## 架构
|
||||
|
||||

|
||||
|
||||
该插件需要完成的工作包括三部分:
|
||||
该插件主要需要处理的内容包括:
|
||||
|
||||
**初始化:** (TODO)
|
||||
- loaded:
|
||||
- unload:
|
||||
1. **连接层:** 该部分主要**维持 Socket 的生命周期,和数据的收发**。它的功能要求包括:
|
||||
- 监听某个端口。当有新的 TCP/UDP 连接到达后,启动一个连接进程,来维持连接的状态。
|
||||
- 调用 `OnSocketCreated` 回调。用于通知外部模块**已新建立了一个连接**。
|
||||
- 调用 `OnScoektClosed` 回调。用于通知外部模块连接**已关闭**。
|
||||
- 调用 `OnReceivedBytes` 回调。用于通知外部模块**该连接新收到的数据包**。
|
||||
- 提供 `Send` 接口。供外部模块调用,**用于发送数据包**。
|
||||
- 提供 `Close` 接口。供外部模块调用,**用于主动关闭连接**。
|
||||
|
||||
**连接层:** 该部分主要**维持 Socket 的生命周期,和数据的收发**。它的功能要求包括:
|
||||
2. **协议/会话层:**该部分主要**提供 PUB/SUB 接口**,以实现与 EMQ X Broker 系统的消息互通。包括:
|
||||
|
||||
- 监听某个端口。当有新的 TCP/UDP 连接到达后,启动一个连接进程,来维持连接的状态。
|
||||
- 调用 `init` 回调。用于通知外部模块**已新建立了一个连接**。
|
||||
- 调用 `terminated` 回调。用于通知外部模块连接**已关闭**。
|
||||
- 调用 `received` 回调。用于通知外部模块**该连接新收到的数据包**。
|
||||
- 提供 `send` 接口。供外部模块调用,**用于发送数据包**。
|
||||
- 提供 `close` 接口。供外部模块调用,**用于主动关闭连接**。
|
||||
|
||||
|
||||
**协议/会话层:**该部分主要**提供 PUB/SUB 接口**,以实现与 EMQ X Broker 系统的消息互通。包括:
|
||||
|
||||
- 提供 `register` 接口。供外部模块调用,用于向集群注册客户端。
|
||||
- 提供 `publish` 接口。供外部模块调用,用于发布消息 EMQ X Broker 中。
|
||||
- 提供 `subscribe` 接口。供外部模块调用,用于订阅某主题,以实现从 EMQ X Broker 中接收某些下行消息。
|
||||
- 提供 `unsubscribe` 接口。供外部模块调用,用于取消订阅某主题。
|
||||
- 调用 `deliver` 回调。用于接收下行消息(在订阅主题成功后,如果主题上有消息,便会回调该方法)
|
||||
|
||||
|
||||
**管理&统计相关:** 该部分主要提供其他**管理&统计相关的接口**。包括:
|
||||
|
||||
- 提供 `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 注册的回调函数:
|
||||

|
||||
|
||||
```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) {};
|
||||
|
||||
// -- protocol layer
|
||||
|
||||
rpc Authenticate(AuthenticateRequest) returns (CodeResponse) {};
|
||||
|
||||
rpc StartTimer(TimerRequest) returns (CodeResponse) {};
|
||||
|
||||
// -- pub/sub layer
|
||||
|
||||
rpc Publish(PublishRequest) returns (CodeResponse) {};
|
||||
|
||||
rpc Subscribe(SubscribeRequest) returns (CodeResponse) {};
|
||||
|
||||
rpc Unsubscribe(UnsubscribeRequest) returns (CodeResponse) {};
|
||||
}
|
||||
|
||||
service ConnectionHandler {
|
||||
|
||||
// -- socket layer
|
||||
|
||||
rpc OnSocketCreated(SocketCreatedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnSocketClosed(SocketClosedRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnReceivedBytes(ReceivedBytesRequest) returns (EmptySuccess) {};
|
||||
|
||||
// -- pub/sub layer
|
||||
|
||||
rpc OnTimerTimeout(TimerTimeoutRequest) returns (EmptySuccess) {};
|
||||
|
||||
rpc OnReceivedMessages(ReceivedMessagesRequest) returns (EmptySuccess) {};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
`emqx-exproto` 需要向多语言插件提供的接口:
|
||||
|
||||
``` erlang
|
||||
%% Send a data to socket
|
||||
send(conn(), data()) -> ok.
|
||||
|
||||
%% Close the socket
|
||||
close(conn() ) -> ok.
|
||||
```
|
||||
|
||||
|
||||
### 协议/会话层接口
|
||||
|
||||
多语言组件需要向 EMQ X 注册的回调函数:
|
||||
|
||||
```erlang
|
||||
%% Received a message from a Topic
|
||||
deliver(conn(), [message()], state()) -> state().
|
||||
|
||||
-type message() :: [ {id, binary()}
|
||||
, {qos, integer()}
|
||||
, {from, binary()}
|
||||
, {topic, binary()}
|
||||
, {payload, binary()}
|
||||
, {timestamp, integer()}
|
||||
].
|
||||
```
|
||||
|
||||
|
||||
`emqx-exproto` 需要向多语言插件提供的接口:
|
||||
|
||||
``` erlang
|
||||
%% Reigster the client to Broker
|
||||
register(conn(), clientinfo()) -> ok | {error, Reason}.
|
||||
|
||||
%% Publish a message to Broker
|
||||
publish(conn(), message()) -> ok.
|
||||
|
||||
%% Subscribe a topic
|
||||
subscribe(conn(), topic(), qos()) -> ok.
|
||||
|
||||
%% Unsubscribe a topic
|
||||
unsubscribe(conn(), topic()) -> ok.
|
||||
|
||||
-type clientinfo() :: [ {proto_name, binary()}
|
||||
, {proto_ver, integer() | string()}
|
||||
, {clientid, binary()}
|
||||
, {username, binary()}
|
||||
, {mountpoint, binary()}}
|
||||
, {keepalive, non_neg_integer()}
|
||||
].
|
||||
```
|
||||
|
||||
### 管理&统计相关接口
|
||||
|
||||
*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, ...
|
||||
## gRPC 服务监听地址 (HTTP)
|
||||
##
|
||||
exproto.server.http.url = http://127.0.0.1:9002
|
||||
|
||||
exproto.listener.jtt808.<key> = <value>
|
||||
## 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 =
|
||||
|
||||
## A CoAP UDP based example
|
||||
exproto.listener.coap = 6799
|
||||
exproto.listener.coap.type = udp
|
||||
exproto.listener.coap.driver = java
|
||||
# ...
|
||||
```
|
||||
|
||||
## 集成与调试
|
||||
|
||||
参见 SDK 规范、和对应语言的开发手册
|
||||
|
||||
## SDK 实现要求
|
||||
|
||||
参见 SDK 规范、和对应语言的开发手册
|
||||
|
||||
## TODOs:
|
||||
|
||||
- 认证 和 发布 订阅鉴权等钩子接入
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 71 KiB |
|
@ -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 编写了其回调的代码模块。
|
|
@ -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.
|
||||
##
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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)).
|
||||
|
|
|
@ -1,25 +1,66 @@
|
|||
%% -*-: erlang -*-
|
||||
%%--------------------------------------------------------------------
|
||||
%% Listeners
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% TCP 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}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% 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.driver_search_path", "emqx_exproto.listeners", [
|
||||
{mapping, "exproto.listener.$proto.connection_handler_url", "emqx_exproto.listeners", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "exproto.listener.$proto.driver_callback_module", "emqx_exproto.listeners", [
|
||||
{default, "main"},
|
||||
{mapping, "exproto.listener.$proto.connection_handler_certfile", "emqx_exproto.listeners", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "exproto.listener.$proto.connection_handler_cacertfile", "emqx_exproto.listeners", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{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 ->
|
||||
|
|
|
@ -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"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
||||
|
|
|
@ -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
|
|
@ -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/"}]}
|
||||
]}.
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -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).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs - Protocol/Session level
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(register(pid(), list()) -> ok | {error, any()}).
|
||||
register(Conn, ClientInfo0) ->
|
||||
case emqx_exproto_types:parse(clientinfo, ClientInfo0) of
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
ClientInfo ->
|
||||
emqx_exproto_conn:cast(Conn, {register, ClientInfo})
|
||||
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
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
Msg ->
|
||||
emqx_exproto_conn:cast(Conn, {publish, Msg})
|
||||
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}).
|
||||
-spec(stop_servers() -> ok).
|
||||
stop_servers() ->
|
||||
lists:foreach(fun stop_server/1, application:get_env(?APP, servers, [])).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
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} ->
|
||||
io:format(standard_error, "Failed to start ~s's connection handler - ~0p~n!",
|
||||
[Name, Reason]),
|
||||
error(Reason)
|
||||
end.
|
||||
|
||||
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} ->
|
||||
io:format(standard_error, "Failed to start ~s gRPC server on ~w - ~0p~n!",
|
||||
[Name, Port, Reason]),
|
||||
error({failed_start_server, Reason})
|
||||
end.
|
||||
|
||||
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}.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
}.
|
||||
|
||||
cb_received(Data, Channel = #channel{state = DState}) ->
|
||||
Args = [self(), Data, DState],
|
||||
do_call_cb('received', Args, Channel).
|
||||
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};
|
||||
|
||||
cb_terminated(Reason, Channel = #channel{state = DState}) ->
|
||||
Args = [self(), stringfy(Reason), DState],
|
||||
do_call_cb('terminated', Args, Channel).
|
||||
ensure_disconnected(_Reason, Channel = #channel{conninfo = ConnInfo}) ->
|
||||
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
|
||||
|
||||
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).
|
||||
run_hooks(Name, Args) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args).
|
||||
|
||||
%% @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}
|
||||
%%--------------------------------------------------------------------
|
||||
%% 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.
|
||||
|
||||
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}}.
|
||||
|
||||
reset_timer(Name, Channel) ->
|
||||
ensure_timer(Name, clean_timer(Name, Channel)).
|
||||
|
||||
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).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Dispatch
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
maybe_assign_clientid(ClientInfo) ->
|
||||
case maps:get(clientid, ClientInfo, undefined) of
|
||||
undefined ->
|
||||
ClientInfo#{clientid => emqx_guid:to_base62(emqx_guid:gen())};
|
||||
_ ->
|
||||
ClientInfo
|
||||
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.
|
||||
|
||||
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).
|
||||
|
|
|
@ -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}.
|
||||
|
||||
|
|
|
@ -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}).
|
|
@ -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}.
|
|
@ -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)}.
|
|
@ -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]}}.
|
||||
|
|
|
@ -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)).
|
||||
|
|
@ -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),
|
||||
|
||||
send(Sock, Bin),
|
||||
Client = #{proto_name => <<"demo">>,
|
||||
proto_ver => <<"v0.1">>,
|
||||
clientid => <<"test_client_1">>,
|
||||
mountpoint => <<"ct/">>
|
||||
},
|
||||
Password = <<"123456">>,
|
||||
|
||||
{ok, Bin} = recv(Sock, byte_size(Bin), 5000),
|
||||
ConnBin = frame_connect(Client, Password),
|
||||
ConnAckBin = frame_connack(0),
|
||||
|
||||
%% 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,
|
||||
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),
|
||||
|
||||
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),
|
||||
|
||||
ConnBin = frame_connect(Client, Password),
|
||||
ConnAckBin = frame_connack(0),
|
||||
|
||||
send(Sock, ConnBin),
|
||||
{ok, ConnAckBin} = recv(Sock, 5000),
|
||||
|
||||
SubBin = frame_subscribe(<<"t/#">>, 1),
|
||||
SubAckBin = frame_suback(1),
|
||||
|
||||
send(Sock, SubBin),
|
||||
{ok, SubAckBin} = recv(Sock, 5000),
|
||||
|
||||
emqx:publish(emqx_message:make(<<"t/dn">>, <<"echo">>)),
|
||||
|
||||
PubBin = frame_publish(<<"t/dn">>, 0, <<"echo">>),
|
||||
PubBinFailedAck = frame_puback(1),
|
||||
PubBinSuccesAck = frame_puback(0),
|
||||
|
||||
send(Sock, PubBin),
|
||||
{ok, PubBinFailedAck} = recv(Sock, 5000),
|
||||
|
||||
meck:unload([emqx_access_control]),
|
||||
|
||||
send(Sock, PubBin),
|
||||
{ok, PubBinSuccesAck} = recv(Sock, 5000),
|
||||
close(Sock).
|
||||
|
||||
t_keepalive_timeout(Cfg) ->
|
||||
SockType = proplists:get_value(listener_type, Cfg),
|
||||
Sock = open(SockType),
|
||||
|
||||
Client = #{proto_name => <<"demo">>,
|
||||
proto_ver => <<"v0.1">>,
|
||||
clientid => <<"test_client_1">>,
|
||||
keepalive => 2
|
||||
},
|
||||
Password = <<"123456">>,
|
||||
|
||||
ConnBin = frame_connect(Client, Password),
|
||||
ConnAckBin = frame_connack(0),
|
||||
|
||||
send(Sock, ConnBin),
|
||||
{ok, ConnAckBin} = recv(Sock, 5000),
|
||||
|
||||
DisconnectBin = frame_disconnect(),
|
||||
{ok, DisconnectBin} = recv(Sock, 10000),
|
||||
|
||||
SockType =/= udp andalso begin
|
||||
{error, closed} = recv(Sock, 5000)
|
||||
end, ok.
|
||||
|
||||
t_hook_connected_disconnected(Cfg) ->
|
||||
SockType = proplists:get_value(listener_type, Cfg),
|
||||
Sock = open(SockType),
|
||||
|
||||
Client = #{proto_name => <<"demo">>,
|
||||
proto_ver => <<"v0.1">>,
|
||||
clientid => <<"test_client_1">>
|
||||
},
|
||||
Password = <<"123456">>,
|
||||
|
||||
ConnBin = frame_connect(Client, Password),
|
||||
ConnAckBin = frame_connack(0),
|
||||
|
||||
Parent = self(),
|
||||
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);
|
||||
|
|
|
@ -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]),
|
||||
{ok, #{}, Md}.
|
||||
|
||||
-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]),
|
||||
#{<<"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) ->
|
||||
io:format("~p: ~0p~n", [?FUNCTION_NAME, Req]),
|
||||
{ok, #{}, Ctx}.
|
||||
|
||||
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:
|
||||
|
|
|
@ -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"}}}]}
|
||||
]}
|
||||
]}.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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, #{}))).
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Action 'inspect'
|
||||
%%------------------------------------------------------------------------------
|
||||
-spec on_action_create_inspect(action_instance_id(), Params :: map())
|
||||
-> NewParams :: map().
|
||||
on_action_create_inspect(Id, Params) ->
|
||||
Params.
|
||||
|
||||
%% 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}) ->
|
||||
-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.
|
||||
Params.
|
||||
|
||||
increase_and_publish(Msg) ->
|
||||
emqx_metrics:inc_msg(Msg),
|
||||
emqx_broker:safe_publish(Msg).
|
||||
-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_do_nothing(_, _) ->
|
||||
fun(_, _) -> ok end.
|
||||
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)).
|
||||
|
|
|
@ -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].
|
|
@ -0,0 +1,8 @@
|
|||
{VSN,
|
||||
[
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -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) ->
|
||||
|
|
|
@ -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()].
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([start_locker/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
|
@ -45,3 +47,11 @@ init([]) ->
|
|||
modules => [emqx_rule_metrics]},
|
||||
{ok, {{one_for_all, 10, 100}, [Registry, Metrics]}}.
|
||||
|
||||
start_locker() ->
|
||||
Locker = #{id => emqx_rule_locker,
|
||||
start => {emqx_rule_locker, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [emqx_rule_locker]},
|
||||
supervisor:start_child(?MODULE, Locker).
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue