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

This commit is contained in:
Zaiming Shi 2020-12-07 21:47:14 +01:00
commit 7fdbfba06a
117 changed files with 3485 additions and 3987 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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` 提供了使用 JavaPython 编写用户自定义协议接入插件的功能
- 支持多种语言的扩展;并包含该语言的示例程序。
- python
- webhook
- Java
- Lua
- cgo.....
- 热操作
- 允许在插件运行过程中,添加和移除 `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(). -- 回调的方法名称pythonjava 等为方法名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
```

View File

@ -1,84 +0,0 @@
## 简介
`emqx-extension-hook` 插件用于提供钩子Hook的多语言支持。它能够允许其他的语言例如PythonJava 等,能够直接表达如何挂载钩子,和处理相应的钩子事件。
该插件给 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 规范、和对应语言的开发手册

View File

@ -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 编写了其回调的代码模块。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,53 +4,25 @@ The `emqx_exproto` extremly enhance the extensibility for EMQ X. It allow using
## Feature
- [x] Support Python, Java.
- [x] Support the `tcp`, `ssl`, `udp`, `dtls` socket.
- [x] Provide the `PUB/SUB` interface to others language.
We temporarily no plans to support other languages. Plaease open a issue if you have to use other programming languages.
- [x] Based on gRPC, it brings a very wide range of applicability
- [x] Allows you to use the return value to extend emqx behavior.
## Architecture
![EMQ X ExProto Arch](./docs/images/exproto-arch.jpg)
## Drivers
## 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)

View File

@ -4,173 +4,124 @@
该插件给 EMQ X 带来的扩展性十分的强大,它能以你熟悉语言处理任何的私有协议,并享受由 EMQ X 系统带来的高连接,和高并发的优点。
**声明:当前仅实现了 Python、Java 的支持**
## 特性
- 多语言支持。快速将接入层的协议实现迁移到 EMQ X 中进行管理
- 极强的扩展能力。使用 gRPC 作为 RPC 通信框架,支持各个主流编程语言
- 高吞吐。连接层以完全的异步非阻塞式 I/O 的方式实现
- 完善的连接层。完全的支持 TCP\TLS UDP\DTLS 类型的连接
- 连接层透明。完全的支持 TCP\TLS UDP\DTLS 类型的连接管理,并对上层提供统一个 API
- 连接层的管理能力。例如最大连接数连接和吞吐的速率限制IP 黑名单 等
## 架构
## 架构
![Extension-Protocol Arch](images/exproto-arch.jpg)
该插件需要完成的工作包括三部分
该插件主要需要处理的内容包括
**初始化:** (TODO)
- loaded:
- unload:
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 注册的回调函数:
![Extension Protocol gRPC Arch](images/exproto-grpc-arch.jpg)
```erlang
%% Got a new Connection
init(conn(), conninfo()) -> state().
%% Incoming a data
recevied(conn(), data(), state()) -> state().
详情参见:`priv/protos/exproto.proto`,例如接口的定义有:
%% Socket & Connection process terminated
terminated(conn(), reason(), state()) -> ok.
```protobuff
syntax = "proto3";
-opaue conn() :: pid().
package emqx.exproto.v1;
-type conninfo() :: [ {socktype, tcp | tls | udp | dtls},
, {peername, {inet:ip_address(), inet:port_number()}},
, {sockname, {inet:ip_address(), inet:port_number()}},
, {peercert, nossl | [{cn, string()}, {dn, string()}]}
]).
// The Broker side serivce. It provides a set of APIs to
// handle a protcol access
service ConnectionAdapter {
-type reason() :: string().
// -- socket layer
-type state() :: any().
rpc Send(SendBytesRequest) returns (CodeResponse) {};
rpc Close(CloseSocketRequest) returns (CodeResponse) {};
// -- 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 目前仅支持pythonjava
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

View File

@ -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 编写了其回调的代码模块。

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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